mirror of
https://github.com/codex-team/editor.js
synced 2024-06-03 06:22:39 +02:00
Chore/popover refactoring (#2249)
* Add new popover class * Add flipper * confirmation * confirmation * Add confirmation support * Add search * Add toggle group support and update popover tests * Add custom content support * Fix scroll issue * Add mobile version * Integration * Fix animation * Cleanup * Fix popover position for narrow mode * Fix tests * Update version and changelog * Rename css classes * Move files * Stop using PopoverItem from outside of popover context * Fix jsdoc * Move error animation to popover item * Update css variables * Update docs/CHANGELOG.md Co-authored-by: Ilya Maroz <37909603+ilyamore88@users.noreply.github.com> * Update src/components/block-tunes/block-tune-move-down.ts Co-authored-by: Peter Savchenko <specc.dev@gmail.com> * Update src/components/block-tunes/block-tune-move-up.ts Co-authored-by: Peter Savchenko <specc.dev@gmail.com> * Fixes * Fix imports * Fix toolbox close event * Move search-input file * Fix comment * Rename method * Cleanup * Remove onFlip callback from popover item * Rename * Fix removing event listener * Move popover animations to popover.css file * Cleanup styles * Fix jsdoc * Fix confirmation chains * Close toolbox oly when it's open * Change activation error animation * Update version and changelog * Fix overlay * Update icon border-radius on mobile * Disable item text select * Update changelog * Update yarn.lock * Add rc postfix to version --------- Co-authored-by: Ilya Maroz <37909603+ilyamore88@users.noreply.github.com> Co-authored-by: Peter Savchenko <specc.dev@gmail.com>
This commit is contained in:
parent
551e3f10b9
commit
07b1ce2aca
|
@ -1,5 +1,10 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
### 2.27.0
|
||||||
|
|
||||||
|
- `Refactoring` — Popover class refactored.
|
||||||
|
- `Improvement` — *Toolbox* — Number of `close()` method calls optimized.
|
||||||
|
|
||||||
### 2.26.5
|
### 2.26.5
|
||||||
|
|
||||||
- `Fix` — *Types* — Remove unnecessary import that creates a dependency on the `cypress`.
|
- `Fix` — *Types* — Remove unnecessary import that creates a dependency on the `cypress`.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@editorjs/editorjs",
|
"name": "@editorjs/editorjs",
|
||||||
"version": "2.26.5",
|
"version": "2.27.0-rc.0",
|
||||||
"description": "Editor.js — Native JS, based on API and Open Source",
|
"description": "Editor.js — Native JS, based on API and Open Source",
|
||||||
"main": "dist/editor.js",
|
"main": "dist/editor.js",
|
||||||
"types": "./types/index.d.ts",
|
"types": "./types/index.d.ts",
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { API, BlockTune } from '../../../types';
|
import { API, BlockTune } from '../../../types';
|
||||||
import Popover from '../utils/popover';
|
|
||||||
import { IconChevronDown } from '@codexteam/icons';
|
import { IconChevronDown } from '@codexteam/icons';
|
||||||
import { TunesMenuConfig } from '../../../types/tools';
|
import { TunesMenuConfig } from '../../../types/tools';
|
||||||
|
|
||||||
|
@ -49,34 +48,21 @@ export default class MoveDownTune implements BlockTune {
|
||||||
return {
|
return {
|
||||||
icon: IconChevronDown,
|
icon: IconChevronDown,
|
||||||
title: this.api.i18n.t('Move down'),
|
title: this.api.i18n.t('Move down'),
|
||||||
onActivate: (item, event): void => this.handleClick(event),
|
onActivate: (): void => this.handleClick(),
|
||||||
name: 'move-down',
|
name: 'move-down',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle clicks on 'move down' button
|
* Handle clicks on 'move down' button
|
||||||
*
|
|
||||||
* @param event - click event
|
|
||||||
*/
|
*/
|
||||||
public handleClick(event: MouseEvent): void {
|
public handleClick(): void {
|
||||||
const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();
|
const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();
|
||||||
const nextBlock = this.api.blocks.getBlockByIndex(currentBlockIndex + 1);
|
const nextBlock = this.api.blocks.getBlockByIndex(currentBlockIndex + 1);
|
||||||
|
|
||||||
// If Block is last do nothing
|
// If Block is last do nothing
|
||||||
if (!nextBlock) {
|
if (!nextBlock) {
|
||||||
const button = (event.target as HTMLElement)
|
throw new Error('Unable to move Block down since it is already the last');
|
||||||
.closest('.' + Popover.CSS.item)
|
|
||||||
.querySelector('.' + Popover.CSS.itemIcon);
|
|
||||||
|
|
||||||
button.classList.add(this.CSS.animation);
|
|
||||||
|
|
||||||
window.setTimeout(() => {
|
|
||||||
button.classList.remove(this.CSS.animation);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextBlockElement = nextBlock.holder;
|
const nextBlockElement = nextBlock.holder;
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
* @copyright <CodeX Team> 2018
|
* @copyright <CodeX Team> 2018
|
||||||
*/
|
*/
|
||||||
import { API, BlockTune } from '../../../types';
|
import { API, BlockTune } from '../../../types';
|
||||||
import Popover from '../../components/utils/popover';
|
|
||||||
import { IconChevronUp } from '@codexteam/icons';
|
import { IconChevronUp } from '@codexteam/icons';
|
||||||
import { TunesMenuConfig } from '../../../types/tools';
|
import { TunesMenuConfig } from '../../../types/tools';
|
||||||
|
|
||||||
|
@ -47,34 +46,21 @@ export default class MoveUpTune implements BlockTune {
|
||||||
return {
|
return {
|
||||||
icon: IconChevronUp,
|
icon: IconChevronUp,
|
||||||
title: this.api.i18n.t('Move up'),
|
title: this.api.i18n.t('Move up'),
|
||||||
onActivate: (item, e): void => this.handleClick(e),
|
onActivate: (): void => this.handleClick(),
|
||||||
name: 'move-up',
|
name: 'move-up',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move current block up
|
* Move current block up
|
||||||
*
|
|
||||||
* @param {MouseEvent} event - click event
|
|
||||||
*/
|
*/
|
||||||
public handleClick(event: MouseEvent): void {
|
public handleClick(): void {
|
||||||
const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();
|
const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();
|
||||||
const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex);
|
const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex);
|
||||||
const previousBlock = this.api.blocks.getBlockByIndex(currentBlockIndex - 1);
|
const previousBlock = this.api.blocks.getBlockByIndex(currentBlockIndex - 1);
|
||||||
|
|
||||||
if (currentBlockIndex === 0 || !currentBlock || !previousBlock) {
|
if (currentBlockIndex === 0 || !currentBlock || !previousBlock) {
|
||||||
const button = (event.target as HTMLElement)
|
throw new Error('Unable to move Block up since it is already the first');
|
||||||
.closest('.' + Popover.CSS.item)
|
|
||||||
.querySelector('.' + Popover.CSS.itemIcon);
|
|
||||||
|
|
||||||
button.classList.add(this.CSS.animation);
|
|
||||||
|
|
||||||
window.setTimeout(() => {
|
|
||||||
button.classList.remove(this.CSS.animation);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentBlockElement = currentBlock.holder;
|
const currentBlockElement = currentBlock.holder;
|
||||||
|
|
|
@ -2,12 +2,12 @@ import Module from '../../__module';
|
||||||
import $ from '../../dom';
|
import $ from '../../dom';
|
||||||
import SelectionUtils from '../../selection';
|
import SelectionUtils from '../../selection';
|
||||||
import Block from '../../block';
|
import Block from '../../block';
|
||||||
import Popover, { PopoverEvent } from '../../utils/popover';
|
|
||||||
import I18n from '../../i18n';
|
import I18n from '../../i18n';
|
||||||
import { I18nInternalNS } from '../../i18n/namespace-internal';
|
import { I18nInternalNS } from '../../i18n/namespace-internal';
|
||||||
import Flipper from '../../flipper';
|
import Flipper from '../../flipper';
|
||||||
import { TunesMenuConfigItem } from '../../../../types/tools';
|
import { TunesMenuConfigItem } from '../../../../types/tools';
|
||||||
import { resolveAliases } from '../../utils/resolve-aliases';
|
import { resolveAliases } from '../../utils/resolve-aliases';
|
||||||
|
import Popover, { PopoverEvent } from '../../utils/popover';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTML Elements that used for BlockSettings
|
* HTML Elements that used for BlockSettings
|
||||||
|
@ -67,13 +67,14 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
|
||||||
*/
|
*/
|
||||||
private popover: Popover | undefined;
|
private popover: Popover | undefined;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Panel with block settings with 2 sections:
|
* Panel with block settings with 2 sections:
|
||||||
* - Tool's Settings
|
* - Tool's Settings
|
||||||
* - Default Settings [Move, Remove, etc]
|
* - Default Settings [Move, Remove, etc]
|
||||||
*/
|
*/
|
||||||
public make(): void {
|
public make(): void {
|
||||||
this.nodes.wrapper = $.make('div');
|
this.nodes.wrapper = $.make('div', [ this.CSS.settings ]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -110,19 +111,19 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
|
||||||
|
|
||||||
/** Tell to subscribers that block settings is opened */
|
/** Tell to subscribers that block settings is opened */
|
||||||
this.eventsDispatcher.emit(this.events.opened);
|
this.eventsDispatcher.emit(this.events.opened);
|
||||||
|
|
||||||
this.popover = new Popover({
|
this.popover = new Popover({
|
||||||
className: this.CSS.settings,
|
|
||||||
searchable: true,
|
searchable: true,
|
||||||
filterLabel: I18n.ui(I18nInternalNS.ui.popover, 'Filter'),
|
|
||||||
nothingFoundLabel: I18n.ui(I18nInternalNS.ui.popover, 'Nothing found'),
|
|
||||||
items: tunesItems.map(tune => this.resolveTuneAliases(tune)),
|
items: tunesItems.map(tune => this.resolveTuneAliases(tune)),
|
||||||
customContent: customHtmlTunesContainer,
|
customContent: customHtmlTunesContainer,
|
||||||
customContentFlippableItems: this.getControls(customHtmlTunesContainer),
|
customContentFlippableItems: this.getControls(customHtmlTunesContainer),
|
||||||
scopeElement: this.Editor.API.methods.ui.nodes.redactor,
|
scopeElement: this.Editor.API.methods.ui.nodes.redactor,
|
||||||
|
messages: {
|
||||||
|
nothingFound: I18n.ui(I18nInternalNS.ui.popover, 'Nothing found'),
|
||||||
|
search: I18n.ui(I18nInternalNS.ui.popover, 'Filter'),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
this.popover.on(PopoverEvent.OverlayClicked, this.onOverlayClicked);
|
|
||||||
this.popover.on(PopoverEvent.Close, () => this.close());
|
this.popover.on(PopoverEvent.Close, this.onPopoverClose);
|
||||||
|
|
||||||
this.nodes.wrapper.append(this.popover.getElement());
|
this.nodes.wrapper.append(this.popover.getElement());
|
||||||
|
|
||||||
|
@ -166,13 +167,20 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
|
||||||
this.eventsDispatcher.emit(this.events.closed);
|
this.eventsDispatcher.emit(this.events.closed);
|
||||||
|
|
||||||
if (this.popover) {
|
if (this.popover) {
|
||||||
this.popover.off(PopoverEvent.OverlayClicked, this.onOverlayClicked);
|
this.popover.off(PopoverEvent.Close, this.onPopoverClose);
|
||||||
this.popover.destroy();
|
this.popover.destroy();
|
||||||
this.popover.getElement().remove();
|
this.popover.getElement().remove();
|
||||||
this.popover = null;
|
this.popover = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles popover close event
|
||||||
|
*/
|
||||||
|
private onPopoverClose = (): void => {
|
||||||
|
this.close();
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns list of buttons and inputs inside specified container
|
* Returns list of buttons and inputs inside specified container
|
||||||
*
|
*
|
||||||
|
@ -188,13 +196,6 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
|
||||||
return Array.from(controls);
|
return Array.from(controls);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles overlay click
|
|
||||||
*/
|
|
||||||
private onOverlayClicked = (): void => {
|
|
||||||
this.close();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves aliases in tunes menu items
|
* Resolves aliases in tunes menu items
|
||||||
*
|
*
|
||||||
|
|
|
@ -228,8 +228,13 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
||||||
/**
|
/**
|
||||||
* Close Toolbox when we move toolbar
|
* Close Toolbox when we move toolbar
|
||||||
*/
|
*/
|
||||||
this.toolboxInstance.close();
|
if (this.toolboxInstance.opened) {
|
||||||
this.Editor.BlockSettings.close();
|
this.toolboxInstance.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.Editor.BlockSettings.opened) {
|
||||||
|
this.Editor.BlockSettings.close();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If no one Block selected as a Current
|
* If no one Block selected as a Current
|
||||||
|
@ -468,7 +473,9 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
||||||
|
|
||||||
this.settingsTogglerClicked();
|
this.settingsTogglerClicked();
|
||||||
|
|
||||||
this.toolboxInstance.close();
|
if (this.toolboxInstance.opened) {
|
||||||
|
this.toolboxInstance.close();
|
||||||
|
}
|
||||||
|
|
||||||
this.tooltip.hide(true);
|
this.tooltip.hide(true);
|
||||||
}, true);
|
}, true);
|
||||||
|
|
|
@ -123,14 +123,15 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
|
||||||
public make(): Element {
|
public make(): Element {
|
||||||
this.popover = new Popover({
|
this.popover = new Popover({
|
||||||
scopeElement: this.api.ui.nodes.redactor,
|
scopeElement: this.api.ui.nodes.redactor,
|
||||||
className: Toolbox.CSS.toolbox,
|
|
||||||
searchable: true,
|
searchable: true,
|
||||||
filterLabel: this.i18nLabels.filter,
|
messages: {
|
||||||
nothingFoundLabel: this.i18nLabels.nothingFound,
|
nothingFound: this.i18nLabels.nothingFound,
|
||||||
|
search: this.i18nLabels.filter,
|
||||||
|
},
|
||||||
items: this.toolboxItemsToBeDisplayed,
|
items: this.toolboxItemsToBeDisplayed,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.popover.on(PopoverEvent.OverlayClicked, this.onOverlayClicked);
|
this.popover.on(PopoverEvent.Close, this.onPopoverClose);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable tools shortcuts
|
* Enable tools shortcuts
|
||||||
|
@ -138,6 +139,7 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
|
||||||
this.enableShortcuts();
|
this.enableShortcuts();
|
||||||
|
|
||||||
this.nodes.toolbox = this.popover.getElement();
|
this.nodes.toolbox = this.popover.getElement();
|
||||||
|
this.nodes.toolbox.classList.add(Toolbox.CSS.toolbox);
|
||||||
|
|
||||||
return this.nodes.toolbox;
|
return this.nodes.toolbox;
|
||||||
}
|
}
|
||||||
|
@ -161,7 +163,7 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.removeAllShortcuts();
|
this.removeAllShortcuts();
|
||||||
this.popover?.off(PopoverEvent.OverlayClicked, this.onOverlayClicked);
|
this.popover?.off(PopoverEvent.Close, this.onPopoverClose);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -208,10 +210,11 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles overlay click
|
* Handles popover close event
|
||||||
*/
|
*/
|
||||||
private onOverlayClicked = (): void => {
|
private onPopoverClose = (): void => {
|
||||||
this.close();
|
this.opened = false;
|
||||||
|
this.emit(ToolboxEvent.Closed);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,724 +0,0 @@
|
||||||
import Dom from '../dom';
|
|
||||||
import Listeners from './listeners';
|
|
||||||
import Flipper from '../flipper';
|
|
||||||
import SearchInput from './search-input';
|
|
||||||
import EventsDispatcher from './events';
|
|
||||||
import { isMobileScreen, keyCodes, cacheable } from '../utils';
|
|
||||||
import ScrollLocker from './scroll-locker';
|
|
||||||
import { PopoverItem, PopoverItemWithConfirmation } from '../../../types';
|
|
||||||
import { IconDotCircle } from '@codexteam/icons';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Event that can be triggered by the Popover
|
|
||||||
*/
|
|
||||||
export enum PopoverEvent {
|
|
||||||
/**
|
|
||||||
* When popover overlay is clicked
|
|
||||||
*/
|
|
||||||
OverlayClicked = 'overlay-clicked',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When popover closes
|
|
||||||
*/
|
|
||||||
Close = 'close'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Popover is the UI element for displaying vertical lists
|
|
||||||
*/
|
|
||||||
export default class Popover extends EventsDispatcher<PopoverEvent> {
|
|
||||||
/**
|
|
||||||
* Flipper - module for keyboard iteration between elements
|
|
||||||
*/
|
|
||||||
public flipper: Flipper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Items list to be displayed
|
|
||||||
*/
|
|
||||||
private readonly items: PopoverItem[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Arbitrary html element to be inserted before items list
|
|
||||||
*/
|
|
||||||
private readonly customContent: HTMLElement;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of html elements inside custom content area that should be available for keyboard navigation
|
|
||||||
*/
|
|
||||||
private readonly customContentFlippableItems: HTMLElement[] = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores the visibility state.
|
|
||||||
*/
|
|
||||||
private isShown = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created nodes
|
|
||||||
*/
|
|
||||||
private nodes: {
|
|
||||||
wrapper: HTMLElement;
|
|
||||||
popover: HTMLElement;
|
|
||||||
items: HTMLElement;
|
|
||||||
nothingFound: HTMLElement;
|
|
||||||
overlay: HTMLElement;
|
|
||||||
} = {
|
|
||||||
wrapper: null,
|
|
||||||
popover: null,
|
|
||||||
items: null,
|
|
||||||
nothingFound: null,
|
|
||||||
overlay: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Additional wrapper's class name
|
|
||||||
*/
|
|
||||||
private readonly className: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listeners util instance
|
|
||||||
*/
|
|
||||||
private listeners: Listeners;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pass true to enable local search field
|
|
||||||
*/
|
|
||||||
private readonly searchable: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instance of the Search Input
|
|
||||||
*/
|
|
||||||
private search: SearchInput;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Label for the 'Filter' placeholder
|
|
||||||
*/
|
|
||||||
private readonly filterLabel: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Label for the 'Nothing found' message
|
|
||||||
*/
|
|
||||||
private readonly nothingFoundLabel: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Style classes
|
|
||||||
*/
|
|
||||||
public static get CSS(): {
|
|
||||||
popover: string;
|
|
||||||
popoverOpened: string;
|
|
||||||
itemsWrapper: string;
|
|
||||||
item: string;
|
|
||||||
itemHidden: string;
|
|
||||||
itemFocused: string;
|
|
||||||
itemActive: string;
|
|
||||||
itemDisabled: string;
|
|
||||||
itemLabel: string;
|
|
||||||
itemIcon: string;
|
|
||||||
itemSecondaryLabel: string;
|
|
||||||
itemConfirmation: string;
|
|
||||||
itemNoHover: string;
|
|
||||||
itemNoFocus: string;
|
|
||||||
noFoundMessage: string;
|
|
||||||
noFoundMessageShown: string;
|
|
||||||
popoverOverlay: string;
|
|
||||||
popoverOverlayHidden: string;
|
|
||||||
customContent: string;
|
|
||||||
customContentHidden: string;
|
|
||||||
} {
|
|
||||||
return {
|
|
||||||
popover: 'ce-popover',
|
|
||||||
popoverOpened: 'ce-popover--opened',
|
|
||||||
itemsWrapper: 'ce-popover__items',
|
|
||||||
item: 'ce-popover__item',
|
|
||||||
itemHidden: 'ce-popover__item--hidden',
|
|
||||||
itemFocused: 'ce-popover__item--focused',
|
|
||||||
itemActive: 'ce-popover__item--active',
|
|
||||||
itemDisabled: 'ce-popover__item--disabled',
|
|
||||||
itemConfirmation: 'ce-popover__item--confirmation',
|
|
||||||
itemNoHover: 'ce-popover__item--no-visible-hover',
|
|
||||||
itemNoFocus: 'ce-popover__item--no-visible-focus',
|
|
||||||
itemLabel: 'ce-popover__item-label',
|
|
||||||
itemIcon: 'ce-popover__item-icon',
|
|
||||||
itemSecondaryLabel: 'ce-popover__item-secondary-label',
|
|
||||||
noFoundMessage: 'ce-popover__no-found',
|
|
||||||
noFoundMessageShown: 'ce-popover__no-found--shown',
|
|
||||||
popoverOverlay: 'ce-popover__overlay',
|
|
||||||
popoverOverlayHidden: 'ce-popover__overlay--hidden',
|
|
||||||
customContent: 'ce-popover__custom-content',
|
|
||||||
customContentHidden: 'ce-popover__custom-content--hidden',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ScrollLocker instance
|
|
||||||
*/
|
|
||||||
private scrollLocker = new ScrollLocker();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Editor container element
|
|
||||||
*/
|
|
||||||
private scopeElement: HTMLElement;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores data on popover items that are in confirmation state
|
|
||||||
*/
|
|
||||||
private itemsRequiringConfirmation: { [itemIndex: number]: PopoverItem } = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the 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
|
|
||||||
* @param options.customContent - arbitrary html element to be inserted before items list
|
|
||||||
* @param options.customContentFlippableItems - list of html elements inside custom content area that should be available for keyboard navigation
|
|
||||||
* @param options.scopeElement - editor container element
|
|
||||||
*/
|
|
||||||
constructor({ items, className, searchable, filterLabel, nothingFoundLabel, customContent, customContentFlippableItems, scopeElement }: {
|
|
||||||
items: PopoverItem[];
|
|
||||||
className?: string;
|
|
||||||
searchable?: boolean;
|
|
||||||
filterLabel: string;
|
|
||||||
nothingFoundLabel: string;
|
|
||||||
customContent?: HTMLElement;
|
|
||||||
customContentFlippableItems?: HTMLElement[];
|
|
||||||
scopeElement: HTMLElement;
|
|
||||||
}) {
|
|
||||||
super();
|
|
||||||
this.items = items;
|
|
||||||
this.customContent = customContent;
|
|
||||||
this.customContentFlippableItems = customContentFlippableItems;
|
|
||||||
this.className = className || '';
|
|
||||||
this.searchable = searchable;
|
|
||||||
this.listeners = new Listeners();
|
|
||||||
this.scopeElement = scopeElement;
|
|
||||||
|
|
||||||
this.filterLabel = filterLabel;
|
|
||||||
this.nothingFoundLabel = nothingFoundLabel;
|
|
||||||
|
|
||||||
this.render();
|
|
||||||
this.enableFlipper();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns rendered wrapper
|
|
||||||
*/
|
|
||||||
public getElement(): HTMLElement {
|
|
||||||
return this.nodes.wrapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows the Popover
|
|
||||||
*/
|
|
||||||
public show(): void {
|
|
||||||
/**
|
|
||||||
* Open the popover above the button
|
|
||||||
* if there is not enough available space below it
|
|
||||||
*/
|
|
||||||
if (!this.shouldOpenPopoverBottom) {
|
|
||||||
this.nodes.wrapper.style.setProperty('--popover-height', this.calculateHeight() + 'px');
|
|
||||||
this.nodes.wrapper.classList.add(this.className + '--opened-top');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear search and items scrolling
|
|
||||||
*/
|
|
||||||
if (this.search) {
|
|
||||||
this.search.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.nodes.items.scrollTop = 0;
|
|
||||||
|
|
||||||
this.nodes.popover.classList.add(Popover.CSS.popoverOpened);
|
|
||||||
this.nodes.overlay.classList.remove(Popover.CSS.popoverOverlayHidden);
|
|
||||||
this.flipper.activate(this.flippableElements);
|
|
||||||
|
|
||||||
if (this.searchable) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.search.focus();
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMobileScreen()) {
|
|
||||||
this.scrollLocker.lock();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isShown = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hides the Popover
|
|
||||||
*/
|
|
||||||
public hide(): void {
|
|
||||||
/**
|
|
||||||
* If it's already hidden, do nothing
|
|
||||||
* to prevent extra DOM operations
|
|
||||||
*/
|
|
||||||
if (!this.isShown) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.nodes.popover.classList.remove(Popover.CSS.popoverOpened);
|
|
||||||
this.nodes.overlay.classList.add(Popover.CSS.popoverOverlayHidden);
|
|
||||||
this.flipper.deactivate();
|
|
||||||
|
|
||||||
if (isMobileScreen()) {
|
|
||||||
this.scrollLocker.unlock();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isShown = false;
|
|
||||||
this.nodes.wrapper.classList.remove(this.className + '--opened-top');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove confirmation state from items
|
|
||||||
*/
|
|
||||||
const confirmationStateItems = Array.from(this.nodes.items.querySelectorAll(`.${Popover.CSS.itemConfirmation}`));
|
|
||||||
|
|
||||||
confirmationStateItems.forEach((itemEl: HTMLElement) => this.cleanUpConfirmationStateForItem(itemEl));
|
|
||||||
|
|
||||||
this.disableSpecialHoverAndFocusBehavior();
|
|
||||||
|
|
||||||
this.emit(PopoverEvent.Close);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears memory
|
|
||||||
*/
|
|
||||||
public destroy(): void {
|
|
||||||
this.flipper.deactivate();
|
|
||||||
this.listeners.removeAll();
|
|
||||||
this.disableSpecialHoverAndFocusBehavior();
|
|
||||||
|
|
||||||
if (isMobileScreen()) {
|
|
||||||
this.scrollLocker.unlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if some item is focused
|
|
||||||
*/
|
|
||||||
public hasFocus(): boolean {
|
|
||||||
return this.flipper.hasFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helps to calculate height of popover while it is not displayed on screen.
|
|
||||||
* Renders invisible clone of popover to get actual height.
|
|
||||||
*/
|
|
||||||
@cacheable
|
|
||||||
private calculateHeight(): number {
|
|
||||||
let height = 0;
|
|
||||||
const popoverClone = this.nodes.popover.cloneNode(true) as HTMLElement;
|
|
||||||
|
|
||||||
popoverClone.style.visibility = 'hidden';
|
|
||||||
popoverClone.style.position = 'absolute';
|
|
||||||
popoverClone.style.top = '-1000px';
|
|
||||||
popoverClone.classList.add(Popover.CSS.popoverOpened);
|
|
||||||
document.body.appendChild(popoverClone);
|
|
||||||
height = popoverClone.offsetHeight;
|
|
||||||
popoverClone.remove();
|
|
||||||
|
|
||||||
return height;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes the UI
|
|
||||||
*/
|
|
||||||
private render(): void {
|
|
||||||
this.nodes.wrapper = Dom.make('div', this.className);
|
|
||||||
this.nodes.popover = Dom.make('div', Popover.CSS.popover);
|
|
||||||
this.nodes.wrapper.appendChild(this.nodes.popover);
|
|
||||||
|
|
||||||
this.nodes.overlay = Dom.make('div', [Popover.CSS.popoverOverlay, Popover.CSS.popoverOverlayHidden]);
|
|
||||||
this.nodes.wrapper.appendChild(this.nodes.overlay);
|
|
||||||
|
|
||||||
if (this.searchable) {
|
|
||||||
this.addSearch(this.nodes.popover);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.customContent) {
|
|
||||||
this.customContent.classList.add(Popover.CSS.customContent);
|
|
||||||
this.nodes.popover.appendChild(this.customContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.nodes.items = Dom.make('div', Popover.CSS.itemsWrapper);
|
|
||||||
this.items.forEach(item => {
|
|
||||||
this.nodes.items.appendChild(this.createItem(item));
|
|
||||||
});
|
|
||||||
|
|
||||||
this.nodes.popover.appendChild(this.nodes.items);
|
|
||||||
this.nodes.nothingFound = Dom.make('div', [ Popover.CSS.noFoundMessage ], {
|
|
||||||
textContent: this.nothingFoundLabel,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.nodes.popover.appendChild(this.nodes.nothingFound);
|
|
||||||
|
|
||||||
this.listeners.on(this.nodes.popover, 'click', (event: PointerEvent) => {
|
|
||||||
const clickedItem = (event.target as HTMLElement).closest(`.${Popover.CSS.item}`) as HTMLElement;
|
|
||||||
|
|
||||||
if (clickedItem) {
|
|
||||||
this.itemClicked(clickedItem, event as PointerEvent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.listeners.on(this.nodes.overlay, 'click', () => {
|
|
||||||
this.emit(PopoverEvent.OverlayClicked);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds the s4arch field to passed element
|
|
||||||
*
|
|
||||||
* @param holder - where to append search input
|
|
||||||
*/
|
|
||||||
private addSearch(holder: HTMLElement): void {
|
|
||||||
this.search = new SearchInput({
|
|
||||||
items: this.items,
|
|
||||||
placeholder: this.filterLabel,
|
|
||||||
onSearch: (filteredItems): void => {
|
|
||||||
const searchResultElements = [];
|
|
||||||
|
|
||||||
this.items.forEach((item, index) => {
|
|
||||||
const itemElement = this.nodes.items.children[index];
|
|
||||||
|
|
||||||
if (filteredItems.includes(item)) {
|
|
||||||
searchResultElements.push(itemElement);
|
|
||||||
itemElement.classList.remove(Popover.CSS.itemHidden);
|
|
||||||
} else {
|
|
||||||
itemElement.classList.add(Popover.CSS.itemHidden);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.nodes.nothingFound.classList.toggle(Popover.CSS.noFoundMessageShown, searchResultElements.length === 0);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* In order to make keyboard navigation work correctly, flipper should be reactivated with only visible items.
|
|
||||||
* As custom html content is not displayed while search, it should be excluded from keyboard navigation.
|
|
||||||
*/
|
|
||||||
const allItemsDisplayed = filteredItems.length === this.items.length;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contains list of elements available for keyboard navigation considering search query applied
|
|
||||||
*/
|
|
||||||
const flippableElements = allItemsDisplayed ? this.flippableElements : searchResultElements;
|
|
||||||
|
|
||||||
if (this.customContent) {
|
|
||||||
this.customContent.classList.toggle(Popover.CSS.customContentHidden, !allItemsDisplayed);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.flipper.isActivated) {
|
|
||||||
/**
|
|
||||||
* Update flipper items with only visible
|
|
||||||
*/
|
|
||||||
this.reactivateFlipper(flippableElements);
|
|
||||||
this.flipper.focusFirst();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const searchField = this.search.getElement();
|
|
||||||
|
|
||||||
holder.appendChild(searchField);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the single item
|
|
||||||
*
|
|
||||||
* @param item - item data to be rendered
|
|
||||||
*/
|
|
||||||
private createItem(item: PopoverItem): HTMLElement {
|
|
||||||
const el = Dom.make('div', Popover.CSS.item);
|
|
||||||
|
|
||||||
if (item.name) {
|
|
||||||
el.dataset.itemName = item.name;
|
|
||||||
}
|
|
||||||
const label = Dom.make('div', Popover.CSS.itemLabel, {
|
|
||||||
innerHTML: item.title || '',
|
|
||||||
});
|
|
||||||
|
|
||||||
el.appendChild(Dom.make('div', Popover.CSS.itemIcon, {
|
|
||||||
innerHTML: item.icon || IconDotCircle,
|
|
||||||
}));
|
|
||||||
|
|
||||||
el.appendChild(label);
|
|
||||||
|
|
||||||
if (item.secondaryLabel) {
|
|
||||||
el.appendChild(Dom.make('div', Popover.CSS.itemSecondaryLabel, {
|
|
||||||
textContent: item.secondaryLabel,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.isActive) {
|
|
||||||
el.classList.add(Popover.CSS.itemActive);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.isDisabled) {
|
|
||||||
el.classList.add(Popover.CSS.itemDisabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Item click handler
|
|
||||||
*
|
|
||||||
* @param itemEl - clicked item
|
|
||||||
* @param event - click event
|
|
||||||
*/
|
|
||||||
private itemClicked(itemEl: HTMLElement, event: PointerEvent): void {
|
|
||||||
const allItems = Array.from(this.nodes.items.children);
|
|
||||||
const itemIndex = allItems.indexOf(itemEl);
|
|
||||||
const clickedItem = this.items[itemIndex];
|
|
||||||
|
|
||||||
if (clickedItem.isDisabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If there is any other item in confirmation state except the clicked one, clean it up
|
|
||||||
*/
|
|
||||||
allItems
|
|
||||||
.filter(item => item !== itemEl)
|
|
||||||
.forEach(item => {
|
|
||||||
this.cleanUpConfirmationStateForItem(item);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (clickedItem.confirmation) {
|
|
||||||
this.enableConfirmationStateForItem(clickedItem as PopoverItemWithConfirmation, itemEl, itemIndex);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
clickedItem.onActivate(clickedItem, event);
|
|
||||||
|
|
||||||
this.toggleIfNeeded(itemIndex, allItems);
|
|
||||||
|
|
||||||
if (clickedItem.closeOnActivate) {
|
|
||||||
this.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* - Toggles item active state, if the item has property 'toggle' set to true.
|
|
||||||
*
|
|
||||||
* - Performs radiobutton-like behavior if the item has property 'toggle' set to string key.
|
|
||||||
* (All the other items with the same key get unactive, and the item gets active)
|
|
||||||
*
|
|
||||||
* @param index - clicked item index
|
|
||||||
* @param itemEls - array of html elements representing popover items
|
|
||||||
*/
|
|
||||||
private toggleIfNeeded(index: number, itemEls: Element[]): void {
|
|
||||||
const clickedItem = this.items[index];
|
|
||||||
|
|
||||||
if (clickedItem.toggle === true) {
|
|
||||||
clickedItem.isActive = !clickedItem.isActive;
|
|
||||||
itemEls[index].classList.toggle(Popover.CSS.itemActive);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof clickedItem.toggle === 'string') {
|
|
||||||
const itemsInToggleGroup = this.items.filter(item => item.toggle === clickedItem.toggle);
|
|
||||||
|
|
||||||
/** If there's only one item in toggle group, toggle it */
|
|
||||||
if (itemsInToggleGroup.length === 1) {
|
|
||||||
clickedItem.isActive = !clickedItem.isActive;
|
|
||||||
itemEls[index].classList.toggle(Popover.CSS.itemActive);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Set clicked item as active and the rest items with same toggle key value as inactive */
|
|
||||||
itemsInToggleGroup.forEach((item: PopoverItem) => {
|
|
||||||
const i = this.items.indexOf(item);
|
|
||||||
const newState = item === clickedItem;
|
|
||||||
|
|
||||||
item.isActive = newState;
|
|
||||||
itemEls[i].classList.toggle(Popover.CSS.itemActive, newState);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enables confirmation state for specified item.
|
|
||||||
* Replaces item element in popover so that is becomes highlighted in a special way
|
|
||||||
*
|
|
||||||
* @param item - item to enable confirmation state for
|
|
||||||
* @param itemEl - html element corresponding to the item
|
|
||||||
* @param itemIndex - index of the item in all items list
|
|
||||||
*/
|
|
||||||
private enableConfirmationStateForItem(item: PopoverItemWithConfirmation, itemEl: HTMLElement, itemIndex: number): void {
|
|
||||||
/** Save root item requiring confirmation to restore original state on popover hide */
|
|
||||||
if (this.itemsRequiringConfirmation[itemIndex] === undefined) {
|
|
||||||
this.itemsRequiringConfirmation[itemIndex] = item;
|
|
||||||
}
|
|
||||||
const newItemData = {
|
|
||||||
...item,
|
|
||||||
...item.confirmation,
|
|
||||||
confirmation: item.confirmation.confirmation,
|
|
||||||
} as PopoverItem;
|
|
||||||
|
|
||||||
this.items[itemIndex] = newItemData;
|
|
||||||
|
|
||||||
const confirmationStateItemEl = this.createItem(newItemData as PopoverItem);
|
|
||||||
|
|
||||||
confirmationStateItemEl.classList.add(Popover.CSS.itemConfirmation, ...Array.from(itemEl.classList));
|
|
||||||
itemEl.parentElement.replaceChild(confirmationStateItemEl, itemEl);
|
|
||||||
|
|
||||||
this.enableSpecialHoverAndFocusBehavior(confirmationStateItemEl);
|
|
||||||
|
|
||||||
this.reactivateFlipper(
|
|
||||||
this.flippableElements,
|
|
||||||
this.flippableElements.indexOf(confirmationStateItemEl)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Brings specified element corresponding to popover item to its original state
|
|
||||||
*
|
|
||||||
* @param itemEl - item in confirmation state
|
|
||||||
*/
|
|
||||||
private cleanUpConfirmationStateForItem(itemEl: Element): void {
|
|
||||||
const allItems = Array.from(this.nodes.items.children);
|
|
||||||
const index = allItems.indexOf(itemEl);
|
|
||||||
|
|
||||||
const originalItem = this.itemsRequiringConfirmation[index];
|
|
||||||
|
|
||||||
if (originalItem === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const originalStateItemEl = this.createItem(originalItem);
|
|
||||||
|
|
||||||
itemEl.parentElement.replaceChild(originalStateItemEl, itemEl);
|
|
||||||
this.items[index] = originalItem;
|
|
||||||
|
|
||||||
delete this.itemsRequiringConfirmation[index];
|
|
||||||
|
|
||||||
itemEl.removeEventListener('mouseleave', this.removeSpecialHoverBehavior);
|
|
||||||
this.disableSpecialHoverAndFocusBehavior();
|
|
||||||
|
|
||||||
this.reactivateFlipper(
|
|
||||||
this.flippableElements,
|
|
||||||
this.flippableElements.indexOf(originalStateItemEl)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enables special focus and hover behavior for item in confirmation state.
|
|
||||||
* This is needed to prevent item from being highlighted as hovered/focused just after click.
|
|
||||||
*
|
|
||||||
* @param item - html element of the item to enable special behavior for
|
|
||||||
*/
|
|
||||||
private enableSpecialHoverAndFocusBehavior(item: HTMLElement): void {
|
|
||||||
item.classList.add(Popover.CSS.itemNoHover);
|
|
||||||
item.classList.add(Popover.CSS.itemNoFocus);
|
|
||||||
|
|
||||||
item.addEventListener('mouseleave', this.removeSpecialHoverBehavior, { once: true });
|
|
||||||
this.flipper.onFlip(this.onFlip);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disables special focus and hover behavior.
|
|
||||||
*/
|
|
||||||
private disableSpecialHoverAndFocusBehavior(): void {
|
|
||||||
this.removeSpecialFocusBehavior();
|
|
||||||
this.removeSpecialHoverBehavior();
|
|
||||||
|
|
||||||
this.flipper.removeOnFlip(this.onFlip);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes class responsible for special hover behavior on an item
|
|
||||||
*/
|
|
||||||
private removeSpecialHoverBehavior = (): void => {
|
|
||||||
const el = this.nodes.items.querySelector(`.${Popover.CSS.itemNoHover}`);
|
|
||||||
|
|
||||||
if (!el) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
el.classList.remove(Popover.CSS.itemNoHover);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes class responsible for special focus behavior on an item
|
|
||||||
*/
|
|
||||||
private removeSpecialFocusBehavior(): void {
|
|
||||||
const el = this.nodes.items.querySelector(`.${Popover.CSS.itemNoFocus}`);
|
|
||||||
|
|
||||||
if (!el) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
el.classList.remove(Popover.CSS.itemNoFocus);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called on flipper navigation
|
|
||||||
*/
|
|
||||||
private onFlip = (): void => {
|
|
||||||
this.disableSpecialHoverAndFocusBehavior();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reactivates flipper instance.
|
|
||||||
* Should be used if popover items html elements get replaced to preserve workability of keyboard navigation
|
|
||||||
*
|
|
||||||
* @param items - html elements to navigate through
|
|
||||||
* @param focusedIndex - index of element to be focused
|
|
||||||
*/
|
|
||||||
private reactivateFlipper(items: HTMLElement[], focusedIndex?: number): void {
|
|
||||||
this.flipper.deactivate();
|
|
||||||
this.flipper.activate(items, focusedIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates Flipper instance to be able to leaf tools
|
|
||||||
*/
|
|
||||||
private enableFlipper(): void {
|
|
||||||
this.flipper = new Flipper({
|
|
||||||
items: this.flippableElements,
|
|
||||||
focusedItemClass: Popover.CSS.itemFocused,
|
|
||||||
allowedKeys: [
|
|
||||||
keyCodes.TAB,
|
|
||||||
keyCodes.UP,
|
|
||||||
keyCodes.DOWN,
|
|
||||||
keyCodes.ENTER,
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns list of elements available for keyboard navigation.
|
|
||||||
* Contains both usual popover items elements and custom html content.
|
|
||||||
*/
|
|
||||||
private get flippableElements(): HTMLElement[] {
|
|
||||||
/**
|
|
||||||
* Select html elements of popover items
|
|
||||||
*/
|
|
||||||
const popoverItemsElements = Array.from(this.nodes.wrapper.querySelectorAll(`.${Popover.CSS.item}`)) as HTMLElement[];
|
|
||||||
|
|
||||||
const customContentControlsElements = this.customContentFlippableItems || [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Combine elements inside custom content area with popover items elements
|
|
||||||
*/
|
|
||||||
return customContentControlsElements.concat(popoverItemsElements);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if popover should be opened bottom.
|
|
||||||
* It should happen when there is enough space below or not enough space above
|
|
||||||
*/
|
|
||||||
private get shouldOpenPopoverBottom(): boolean {
|
|
||||||
const toolboxRect = this.nodes.wrapper.getBoundingClientRect();
|
|
||||||
const scopeElementRect = this.scopeElement.getBoundingClientRect();
|
|
||||||
const popoverHeight = this.calculateHeight();
|
|
||||||
const popoverPotentialBottomEdge = toolboxRect.top + popoverHeight;
|
|
||||||
const popoverPotentialTopEdge = toolboxRect.top - popoverHeight;
|
|
||||||
const bottomEdgeForComparison = Math.min(window.innerHeight, scopeElementRect.bottom);
|
|
||||||
|
|
||||||
return popoverPotentialTopEdge < scopeElementRect.top || popoverPotentialBottomEdge <= bottomEdgeForComparison;
|
|
||||||
}
|
|
||||||
}
|
|
521
src/components/utils/popover/index.ts
Normal file
521
src/components/utils/popover/index.ts
Normal file
|
@ -0,0 +1,521 @@
|
||||||
|
import { PopoverItem } from './popover-item';
|
||||||
|
import Dom from '../../dom';
|
||||||
|
import { cacheable, keyCodes, isMobileScreen } from '../../utils';
|
||||||
|
import Flipper from '../../flipper';
|
||||||
|
import { PopoverItem as PopoverItemParams } from '../../../../types';
|
||||||
|
import SearchInput from './search-input';
|
||||||
|
import EventsDispatcher from '../events';
|
||||||
|
import Listeners from '../listeners';
|
||||||
|
import ScrollLocker from '../scroll-locker';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Params required to render popover
|
||||||
|
*/
|
||||||
|
interface PopoverParams {
|
||||||
|
/**
|
||||||
|
* Popover items config
|
||||||
|
*/
|
||||||
|
items: PopoverItemParams[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Element of the page that creates 'scope' of the popover
|
||||||
|
*/
|
||||||
|
scopeElement?: HTMLElement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Arbitrary html element to be inserted before items list
|
||||||
|
*/
|
||||||
|
customContent?: HTMLElement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of html elements inside custom content area that should be available for keyboard navigation
|
||||||
|
*/
|
||||||
|
customContentFlippableItems?: HTMLElement[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if popover should contain search field
|
||||||
|
*/
|
||||||
|
searchable?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Popover texts overrides
|
||||||
|
*/
|
||||||
|
messages?: PopoverMessages
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Texts used inside popover
|
||||||
|
*/
|
||||||
|
interface PopoverMessages {
|
||||||
|
/** Text displayed when search has no results */
|
||||||
|
nothingFound?: string;
|
||||||
|
|
||||||
|
/** Search input label */
|
||||||
|
search?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event that can be triggered by the Popover
|
||||||
|
*/
|
||||||
|
export enum PopoverEvent {
|
||||||
|
/**
|
||||||
|
* When popover closes
|
||||||
|
*/
|
||||||
|
Close = 'close'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class responsible for rendering popover and handling its behaviour
|
||||||
|
*/
|
||||||
|
export default class Popover extends EventsDispatcher<PopoverEvent> {
|
||||||
|
/**
|
||||||
|
* Flipper - module for keyboard iteration between elements
|
||||||
|
*/
|
||||||
|
public flipper: Flipper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of popover items
|
||||||
|
*/
|
||||||
|
private items: PopoverItem[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Element of the page that creates 'scope' of the popover.
|
||||||
|
* If possible, popover will not cross specified element's borders when opening.
|
||||||
|
*/
|
||||||
|
private scopeElement: HTMLElement = document.body;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of html elements inside custom content area that should be available for keyboard navigation
|
||||||
|
*/
|
||||||
|
private customContentFlippableItems: HTMLElement[] | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instance of the Search Input
|
||||||
|
*/
|
||||||
|
private search: SearchInput | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listeners util instance
|
||||||
|
*/
|
||||||
|
private listeners: Listeners = new Listeners();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ScrollLocker instance
|
||||||
|
*/
|
||||||
|
private scrollLocker = new ScrollLocker();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Popover CSS classes
|
||||||
|
*/
|
||||||
|
private static get CSS(): {
|
||||||
|
popover: string;
|
||||||
|
popoverOpenTop: string;
|
||||||
|
popoverOpened: string;
|
||||||
|
search: string;
|
||||||
|
nothingFoundMessage: string;
|
||||||
|
nothingFoundMessageDisplayed: string;
|
||||||
|
customContent: string;
|
||||||
|
customContentHidden: string;
|
||||||
|
items: string;
|
||||||
|
overlay: string;
|
||||||
|
overlayHidden: string;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
popover: 'ce-popover',
|
||||||
|
popoverOpenTop: 'ce-popover--open-top',
|
||||||
|
popoverOpened: 'ce-popover--opened',
|
||||||
|
search: 'ce-popover__search',
|
||||||
|
nothingFoundMessage: 'ce-popover__nothing-found-message',
|
||||||
|
nothingFoundMessageDisplayed: 'ce-popover__nothing-found-message--displayed',
|
||||||
|
customContent: 'ce-popover__custom-content',
|
||||||
|
customContentHidden: 'ce-popover__custom-content--hidden',
|
||||||
|
items: 'ce-popover__items',
|
||||||
|
overlay: 'ce-popover__overlay',
|
||||||
|
overlayHidden: 'ce-popover__overlay--hidden',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refs to created HTML elements
|
||||||
|
*/
|
||||||
|
private nodes: {
|
||||||
|
wrapper: HTMLElement | null;
|
||||||
|
popover: HTMLElement | null;
|
||||||
|
nothingFoundMessage: HTMLElement | null;
|
||||||
|
customContent: HTMLElement | null;
|
||||||
|
items: HTMLElement | null;
|
||||||
|
overlay: HTMLElement | null;
|
||||||
|
} = {
|
||||||
|
wrapper: null,
|
||||||
|
popover: null,
|
||||||
|
nothingFoundMessage: null,
|
||||||
|
customContent: null,
|
||||||
|
items: null,
|
||||||
|
overlay: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Messages that will be displayed in popover
|
||||||
|
*/
|
||||||
|
private messages: PopoverMessages = {
|
||||||
|
nothingFound: 'Nothing found',
|
||||||
|
search: 'Search',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs the instance
|
||||||
|
*
|
||||||
|
* @param params - popover construction params
|
||||||
|
*/
|
||||||
|
constructor(params: PopoverParams) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.items = params.items.map(item => new PopoverItem(item));
|
||||||
|
|
||||||
|
if (params.scopeElement !== undefined) {
|
||||||
|
this.scopeElement = params.scopeElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.messages) {
|
||||||
|
this.messages = {
|
||||||
|
...this.messages,
|
||||||
|
...params.messages,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.customContentFlippableItems) {
|
||||||
|
this.customContentFlippableItems = params.customContentFlippableItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.make();
|
||||||
|
|
||||||
|
if (params.customContent) {
|
||||||
|
this.addCustomContent(params.customContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.searchable) {
|
||||||
|
this.addSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
this.initializeFlipper();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTML element correcponding to the popover
|
||||||
|
*/
|
||||||
|
public getElement(): HTMLElement | null {
|
||||||
|
return this.nodes.wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if some item inside popover is focused
|
||||||
|
*/
|
||||||
|
public hasFocus(): boolean {
|
||||||
|
return this.flipper.hasFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open popover
|
||||||
|
*/
|
||||||
|
public show(): void {
|
||||||
|
if (!this.shouldOpenBottom) {
|
||||||
|
this.nodes.popover.style.setProperty('--popover-height', this.height + 'px');
|
||||||
|
this.nodes.popover.classList.add(Popover.CSS.popoverOpenTop);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.nodes.overlay.classList.remove(Popover.CSS.overlayHidden);
|
||||||
|
this.nodes.popover.classList.add(Popover.CSS.popoverOpened);
|
||||||
|
this.flipper.activate(this.flippableElements);
|
||||||
|
|
||||||
|
if (this.search !== undefined) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.search.focus();
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMobileScreen()) {
|
||||||
|
this.scrollLocker.lock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes popover
|
||||||
|
*/
|
||||||
|
public hide(): void {
|
||||||
|
this.nodes.popover.classList.remove(Popover.CSS.popoverOpened);
|
||||||
|
this.nodes.popover.classList.remove(Popover.CSS.popoverOpenTop);
|
||||||
|
this.nodes.overlay.classList.add(Popover.CSS.overlayHidden);
|
||||||
|
this.flipper.deactivate();
|
||||||
|
this.items.forEach(item => item.reset());
|
||||||
|
|
||||||
|
if (this.search !== undefined) {
|
||||||
|
this.search.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMobileScreen()) {
|
||||||
|
this.scrollLocker.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit(PopoverEvent.Close);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears memory
|
||||||
|
*/
|
||||||
|
public destroy(): void {
|
||||||
|
this.flipper.deactivate();
|
||||||
|
this.listeners.removeAll();
|
||||||
|
|
||||||
|
if (isMobileScreen()) {
|
||||||
|
this.scrollLocker.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs HTML element corresponding to popover
|
||||||
|
*/
|
||||||
|
private make(): void {
|
||||||
|
this.nodes.popover = Dom.make('div', [ Popover.CSS.popover ]);
|
||||||
|
|
||||||
|
this.nodes.nothingFoundMessage = Dom.make('div', [ Popover.CSS.nothingFoundMessage ], {
|
||||||
|
textContent: this.messages.nothingFound,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.nodes.popover.appendChild(this.nodes.nothingFoundMessage);
|
||||||
|
this.nodes.items = Dom.make('div', [ Popover.CSS.items ]);
|
||||||
|
|
||||||
|
this.items.forEach(item => {
|
||||||
|
this.nodes.items.appendChild(item.getElement());
|
||||||
|
});
|
||||||
|
|
||||||
|
this.nodes.popover.appendChild(this.nodes.items);
|
||||||
|
|
||||||
|
this.listeners.on(this.nodes.popover, 'click', (event: PointerEvent) => {
|
||||||
|
const item = this.getTargetItem(event);
|
||||||
|
|
||||||
|
if (item === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handleItemClick(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.nodes.wrapper = Dom.make('div');
|
||||||
|
this.nodes.overlay = Dom.make('div', [Popover.CSS.overlay, Popover.CSS.overlayHidden]);
|
||||||
|
|
||||||
|
this.listeners.on(this.nodes.overlay, 'click', () => {
|
||||||
|
this.hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.nodes.wrapper.appendChild(this.nodes.overlay);
|
||||||
|
this.nodes.wrapper.appendChild(this.nodes.popover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds seach to the popover
|
||||||
|
*/
|
||||||
|
private addSearch(): void {
|
||||||
|
this.search = new SearchInput({
|
||||||
|
items: this.items,
|
||||||
|
placeholder: this.messages.search,
|
||||||
|
onSearch: (query: string, result: PopoverItem[]): void => {
|
||||||
|
this.items.forEach(item => {
|
||||||
|
const isHidden = !result.includes(item);
|
||||||
|
|
||||||
|
item.toggleHidden(isHidden);
|
||||||
|
});
|
||||||
|
this.toggleNothingFoundMessage(result.length === 0);
|
||||||
|
this.toggleCustomContent(query !== '');
|
||||||
|
|
||||||
|
/** List of elements available for keyboard navigation considering search query applied */
|
||||||
|
const flippableElements = query === '' ? this.flippableElements : result.map(item => item.getElement());
|
||||||
|
|
||||||
|
if (this.flipper.isActivated) {
|
||||||
|
/** Update flipper items with only visible */
|
||||||
|
this.flipper.deactivate();
|
||||||
|
this.flipper.activate(flippableElements);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchElement = this.search.getElement();
|
||||||
|
|
||||||
|
searchElement.classList.add(Popover.CSS.search);
|
||||||
|
|
||||||
|
this.nodes.popover.insertBefore(searchElement, this.nodes.popover.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds custom html content to the popover
|
||||||
|
*
|
||||||
|
* @param content - html content to append
|
||||||
|
*/
|
||||||
|
private addCustomContent(content: HTMLElement): void {
|
||||||
|
this.nodes.customContent = content;
|
||||||
|
this.nodes.customContent.classList.add(Popover.CSS.customContent);
|
||||||
|
this.nodes.popover.insertBefore(content, this.nodes.popover.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves popover item that is the target of the specified event
|
||||||
|
*
|
||||||
|
* @param event - event to retrieve popover item from
|
||||||
|
*/
|
||||||
|
private getTargetItem(event: PointerEvent): PopoverItem | undefined {
|
||||||
|
return this.items.find(el => event.composedPath().includes(el.getElement()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles item clicks
|
||||||
|
*
|
||||||
|
* @param item - item to handle click of
|
||||||
|
*/
|
||||||
|
private handleItemClick(item: PopoverItem): void {
|
||||||
|
if (item.isDisabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cleanup other items state */
|
||||||
|
this.items.filter(x => x !== item).forEach(x => x.reset());
|
||||||
|
|
||||||
|
item.handleClick();
|
||||||
|
|
||||||
|
this.toggleItemActivenessIfNeeded(item);
|
||||||
|
|
||||||
|
if (item.closeOnActivate) {
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates Flipper instance which allows to navigate between popover items via keyboard
|
||||||
|
*/
|
||||||
|
private initializeFlipper(): void {
|
||||||
|
this.flipper = new Flipper({
|
||||||
|
items: this.flippableElements,
|
||||||
|
focusedItemClass: PopoverItem.CSS.focused,
|
||||||
|
allowedKeys: [
|
||||||
|
keyCodes.TAB,
|
||||||
|
keyCodes.UP,
|
||||||
|
keyCodes.DOWN,
|
||||||
|
keyCodes.ENTER,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.flipper.onFlip(this.onFlip);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns list of elements available for keyboard navigation.
|
||||||
|
* Contains both usual popover items elements and custom html content.
|
||||||
|
*/
|
||||||
|
private get flippableElements(): HTMLElement[] {
|
||||||
|
const popoverItemsElements = this.items.map(item => item.getElement());
|
||||||
|
const customContentControlsElements = this.customContentFlippableItems || [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combine elements inside custom content area with popover items elements
|
||||||
|
*/
|
||||||
|
return customContentControlsElements.concat(popoverItemsElements);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helps to calculate height of popover while it is not displayed on screen.
|
||||||
|
* Renders invisible clone of popover to get actual height.
|
||||||
|
*/
|
||||||
|
@cacheable
|
||||||
|
private get height(): number {
|
||||||
|
let height = 0;
|
||||||
|
|
||||||
|
if (this.nodes.popover === null) {
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
const popoverClone = this.nodes.popover.cloneNode(true) as HTMLElement;
|
||||||
|
|
||||||
|
popoverClone.style.visibility = 'hidden';
|
||||||
|
popoverClone.style.position = 'absolute';
|
||||||
|
popoverClone.style.top = '-1000px';
|
||||||
|
popoverClone.classList.add(Popover.CSS.popoverOpened);
|
||||||
|
document.body.appendChild(popoverClone);
|
||||||
|
height = popoverClone.offsetHeight;
|
||||||
|
popoverClone.remove();
|
||||||
|
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if popover should be opened bottom.
|
||||||
|
* It should happen when there is enough space below or not enough space above
|
||||||
|
*/
|
||||||
|
private get shouldOpenBottom(): boolean {
|
||||||
|
const popoverRect = this.nodes.popover.getBoundingClientRect();
|
||||||
|
const scopeElementRect = this.scopeElement.getBoundingClientRect();
|
||||||
|
const popoverHeight = this.height;
|
||||||
|
const popoverPotentialBottomEdge = popoverRect.top + popoverHeight;
|
||||||
|
const popoverPotentialTopEdge = popoverRect.top - popoverHeight;
|
||||||
|
const bottomEdgeForComparison = Math.min(window.innerHeight, scopeElementRect.bottom);
|
||||||
|
|
||||||
|
return popoverPotentialTopEdge < scopeElementRect.top || popoverPotentialBottomEdge <= bottomEdgeForComparison;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called on flipper navigation
|
||||||
|
*/
|
||||||
|
private onFlip = (): void => {
|
||||||
|
const focusedItem = this.items.find(item => item.isFocused);
|
||||||
|
|
||||||
|
focusedItem.onFocus();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles nothing found message visibility
|
||||||
|
*
|
||||||
|
* @param isDislayed - true if the message should be displayed
|
||||||
|
*/
|
||||||
|
private toggleNothingFoundMessage(isDislayed: boolean): void {
|
||||||
|
this.nodes.nothingFoundMessage.classList.toggle(Popover.CSS.nothingFoundMessageDisplayed, isDislayed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles custom content visibility
|
||||||
|
*
|
||||||
|
* @param isDisplayed - true if custom content should be displayed
|
||||||
|
*/
|
||||||
|
private toggleCustomContent(isDisplayed: boolean): void {
|
||||||
|
this.nodes.customContent?.classList.toggle(Popover.CSS.customContentHidden, isDisplayed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* - Toggles item active state, if clicked popover item has property 'toggle' set to true.
|
||||||
|
*
|
||||||
|
* - Performs radiobutton-like behavior if the item has property 'toggle' set to string key.
|
||||||
|
* (All the other items with the same key get unactive, and the item gets active)
|
||||||
|
*
|
||||||
|
* @param clickedItem - popover item that was clicked
|
||||||
|
*/
|
||||||
|
private toggleItemActivenessIfNeeded(clickedItem: PopoverItem): void {
|
||||||
|
if (clickedItem.toggle === true) {
|
||||||
|
clickedItem.toggleActive();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof clickedItem.toggle === 'string') {
|
||||||
|
const itemsInToggleGroup = this.items.filter(item => item.toggle === clickedItem.toggle);
|
||||||
|
|
||||||
|
/** If there's only one item in toggle group, toggle it */
|
||||||
|
if (itemsInToggleGroup.length === 1) {
|
||||||
|
clickedItem.toggleActive();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set clicked item as active and the rest items with same toggle key value as inactive */
|
||||||
|
itemsInToggleGroup.forEach(item => {
|
||||||
|
item.toggleActive(item === clickedItem);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
316
src/components/utils/popover/popover-item.ts
Normal file
316
src/components/utils/popover/popover-item.ts
Normal file
|
@ -0,0 +1,316 @@
|
||||||
|
import Dom from '../../dom';
|
||||||
|
import { IconDotCircle } from '@codexteam/icons';
|
||||||
|
import { PopoverItem as PopoverItemParams } from '../../../../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents sigle popover item node
|
||||||
|
*/
|
||||||
|
export class PopoverItem {
|
||||||
|
/**
|
||||||
|
* True if item is disabled and hence not clickable
|
||||||
|
*/
|
||||||
|
public get isDisabled(): boolean {
|
||||||
|
return this.params.isDisabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposes popover item toggle parameter
|
||||||
|
*/
|
||||||
|
public get toggle(): boolean | string | undefined {
|
||||||
|
return this.params.toggle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Item title
|
||||||
|
*/
|
||||||
|
public get title(): string | undefined {
|
||||||
|
return this.params.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if popover should close once item is activated
|
||||||
|
*/
|
||||||
|
public get closeOnActivate(): boolean | undefined {
|
||||||
|
return this.params.closeOnActivate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if confirmation state is enabled for popover item
|
||||||
|
*/
|
||||||
|
public get isConfirmationStateEnabled(): boolean {
|
||||||
|
return this.confirmationState !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if item is focused in keyboard navigation process
|
||||||
|
*/
|
||||||
|
public get isFocused(): boolean {
|
||||||
|
return this.nodes.root.classList.contains(PopoverItem.CSS.focused);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Item html elements
|
||||||
|
*/
|
||||||
|
private nodes: {
|
||||||
|
root: null | HTMLElement,
|
||||||
|
icon: null | HTMLElement
|
||||||
|
} = {
|
||||||
|
root: null,
|
||||||
|
icon: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Popover item params
|
||||||
|
*/
|
||||||
|
private params: PopoverItemParams;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If item is in confirmation state, stores confirmation params such as icon, label, onActivate callback and so on
|
||||||
|
*/
|
||||||
|
private confirmationState: PopoverItemParams | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Popover item CSS classes
|
||||||
|
*/
|
||||||
|
public static get CSS(): {
|
||||||
|
container: string,
|
||||||
|
title: string,
|
||||||
|
secondaryTitle: string,
|
||||||
|
icon: string,
|
||||||
|
active: string,
|
||||||
|
disabled: string,
|
||||||
|
focused: string,
|
||||||
|
hidden: string,
|
||||||
|
confirmationState: string,
|
||||||
|
noHover: string,
|
||||||
|
noFocus: string,
|
||||||
|
wobbleAnimation: string
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
container: 'ce-popover-item',
|
||||||
|
title: 'ce-popover-item__title',
|
||||||
|
secondaryTitle: 'ce-popover-item__secondary-title',
|
||||||
|
icon: 'ce-popover-item__icon',
|
||||||
|
active: 'ce-popover-item--active',
|
||||||
|
disabled: 'ce-popover-item--disabled',
|
||||||
|
focused: 'ce-popover-item--focused',
|
||||||
|
hidden: 'ce-popover-item--hidden',
|
||||||
|
confirmationState: 'ce-popover-item--confirmation',
|
||||||
|
noHover: 'ce-popover-item--no-hover',
|
||||||
|
noFocus: 'ce-popover-item--no-focus',
|
||||||
|
wobbleAnimation: 'wobble',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs popover item instance
|
||||||
|
*
|
||||||
|
* @param params - popover item construction params
|
||||||
|
*/
|
||||||
|
constructor(params: PopoverItemParams) {
|
||||||
|
this.params = params;
|
||||||
|
this.nodes.root = this.make(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns popover item root element
|
||||||
|
*/
|
||||||
|
public getElement(): HTMLElement {
|
||||||
|
return this.nodes.root;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called on popover item click
|
||||||
|
*/
|
||||||
|
public handleClick(): void {
|
||||||
|
if (this.isConfirmationStateEnabled) {
|
||||||
|
this.activateOrEnableConfirmationMode(this.confirmationState);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activateOrEnableConfirmationMode(this.params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles item active state
|
||||||
|
*
|
||||||
|
* @param isActive - true if item should strictly should become active
|
||||||
|
*/
|
||||||
|
public toggleActive(isActive?: boolean): void {
|
||||||
|
this.nodes.root.classList.toggle(PopoverItem.CSS.active, isActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles item hidden state
|
||||||
|
*
|
||||||
|
* @param isHidden - true if item should be hidden
|
||||||
|
*/
|
||||||
|
public toggleHidden(isHidden: boolean): void {
|
||||||
|
this.nodes.root.classList.toggle(PopoverItem.CSS.hidden, isHidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets popover item to its original state
|
||||||
|
*/
|
||||||
|
public reset(): void {
|
||||||
|
if (this.isConfirmationStateEnabled) {
|
||||||
|
this.disableConfirmationMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called once item becomes focused during keyboard navigation
|
||||||
|
*/
|
||||||
|
public onFocus(): void {
|
||||||
|
this.disableSpecialHoverAndFocusBehavior();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs HTML element corresponding to popover item params
|
||||||
|
*
|
||||||
|
* @param params - item construction params
|
||||||
|
*/
|
||||||
|
private make(params: PopoverItemParams): HTMLElement {
|
||||||
|
const el = Dom.make('div', PopoverItem.CSS.container);
|
||||||
|
|
||||||
|
if (params.name) {
|
||||||
|
el.dataset.itemName = params.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.nodes.icon = Dom.make('div', PopoverItem.CSS.icon, {
|
||||||
|
innerHTML: params.icon || IconDotCircle,
|
||||||
|
});
|
||||||
|
|
||||||
|
el.appendChild(this.nodes.icon);
|
||||||
|
|
||||||
|
el.appendChild(Dom.make('div', PopoverItem.CSS.title, {
|
||||||
|
innerHTML: params.title || '',
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (params.secondaryLabel) {
|
||||||
|
el.appendChild(Dom.make('div', PopoverItem.CSS.secondaryTitle, {
|
||||||
|
textContent: params.secondaryLabel,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.isActive) {
|
||||||
|
el.classList.add(PopoverItem.CSS.active);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.isDisabled) {
|
||||||
|
el.classList.add(PopoverItem.CSS.disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activates confirmation mode for the item.
|
||||||
|
*
|
||||||
|
* @param newState - new popover item params that should be applied
|
||||||
|
*/
|
||||||
|
private enableConfirmationMode(newState: PopoverItemParams): void {
|
||||||
|
const params = {
|
||||||
|
...this.params,
|
||||||
|
...newState,
|
||||||
|
confirmation: newState.confirmation,
|
||||||
|
} as PopoverItemParams;
|
||||||
|
const confirmationEl = this.make(params);
|
||||||
|
|
||||||
|
this.nodes.root.innerHTML = confirmationEl.innerHTML;
|
||||||
|
this.nodes.root.classList.add(PopoverItem.CSS.confirmationState);
|
||||||
|
|
||||||
|
this.confirmationState = newState;
|
||||||
|
|
||||||
|
this.enableSpecialHoverAndFocusBehavior();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns item to its original state
|
||||||
|
*/
|
||||||
|
private disableConfirmationMode(): void {
|
||||||
|
const itemWithOriginalParams = this.make(this.params);
|
||||||
|
|
||||||
|
this.nodes.root.innerHTML = itemWithOriginalParams.innerHTML;
|
||||||
|
this.nodes.root.classList.remove(PopoverItem.CSS.confirmationState);
|
||||||
|
|
||||||
|
this.confirmationState = null;
|
||||||
|
|
||||||
|
this.disableSpecialHoverAndFocusBehavior();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables special focus and hover behavior for item in confirmation state.
|
||||||
|
* This is needed to prevent item from being highlighted as hovered/focused just after click.
|
||||||
|
*/
|
||||||
|
private enableSpecialHoverAndFocusBehavior(): void {
|
||||||
|
this.nodes.root.classList.add(PopoverItem.CSS.noHover);
|
||||||
|
this.nodes.root.classList.add(PopoverItem.CSS.noFocus);
|
||||||
|
|
||||||
|
this.nodes.root.addEventListener('mouseleave', this.removeSpecialHoverBehavior, { once: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables special focus and hover behavior
|
||||||
|
*/
|
||||||
|
private disableSpecialHoverAndFocusBehavior(): void {
|
||||||
|
this.removeSpecialFocusBehavior();
|
||||||
|
this.removeSpecialHoverBehavior();
|
||||||
|
|
||||||
|
this.nodes.root.removeEventListener('mouseleave', this.removeSpecialHoverBehavior);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes class responsible for special focus behavior on an item
|
||||||
|
*/
|
||||||
|
private removeSpecialFocusBehavior = (): void => {
|
||||||
|
this.nodes.root.classList.remove(PopoverItem.CSS.noFocus);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes class responsible for special hover behavior on an item
|
||||||
|
*/
|
||||||
|
private removeSpecialHoverBehavior = (): void => {
|
||||||
|
this.nodes.root.classList.remove(PopoverItem.CSS.noHover);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes item's onActivate callback if the item has no confirmation configured
|
||||||
|
*
|
||||||
|
* @param item - item to activate or bring to confirmation mode
|
||||||
|
*/
|
||||||
|
private activateOrEnableConfirmationMode(item: PopoverItemParams): void {
|
||||||
|
if (item.confirmation === undefined) {
|
||||||
|
try {
|
||||||
|
item.onActivate(item);
|
||||||
|
this.disableConfirmationMode();
|
||||||
|
} catch {
|
||||||
|
this.animateError();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.enableConfirmationMode(item.confirmation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animates item which symbolizes that error occured while executing 'onActivate()' callback
|
||||||
|
*/
|
||||||
|
private animateError(): void {
|
||||||
|
if (this.nodes.icon.classList.contains(PopoverItem.CSS.wobbleAnimation)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.nodes.icon.classList.add(PopoverItem.CSS.wobbleAnimation);
|
||||||
|
|
||||||
|
this.nodes.icon.addEventListener('animationend', this.onErrorAnimationEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles finish of error animation
|
||||||
|
*/
|
||||||
|
private onErrorAnimationEnd = (): void => {
|
||||||
|
this.nodes.icon.classList.remove(PopoverItem.CSS.wobbleAnimation);
|
||||||
|
this.nodes.icon.removeEventListener('animationend', this.onErrorAnimationEnd);
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import Dom from '../dom';
|
import Dom from '../../dom';
|
||||||
import Listeners from './listeners';
|
import Listeners from '../listeners';
|
||||||
import { IconSearch } from '@codexteam/icons';
|
import { IconSearch } from '@codexteam/icons';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -41,7 +41,7 @@ export default class SearchInput {
|
||||||
/**
|
/**
|
||||||
* Externally passed callback for the search
|
* Externally passed callback for the search
|
||||||
*/
|
*/
|
||||||
private readonly onSearch: (items: SearchableItem[]) => void;
|
private readonly onSearch: (query: string, items: SearchableItem[]) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Styles
|
* Styles
|
||||||
|
@ -66,7 +66,7 @@ export default class SearchInput {
|
||||||
*/
|
*/
|
||||||
constructor({ items, onSearch, placeholder }: {
|
constructor({ items, onSearch, placeholder }: {
|
||||||
items: SearchableItem[];
|
items: SearchableItem[];
|
||||||
onSearch: (items: SearchableItem[]) => void;
|
onSearch: (query: string, items: SearchableItem[]) => void;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
}) {
|
}) {
|
||||||
this.listeners = new Listeners();
|
this.listeners = new Listeners();
|
||||||
|
@ -96,7 +96,7 @@ export default class SearchInput {
|
||||||
public clear(): void {
|
public clear(): void {
|
||||||
this.input.value = '';
|
this.input.value = '';
|
||||||
this.searchQuery = '';
|
this.searchQuery = '';
|
||||||
this.onSearch(this.foundItems);
|
this.onSearch('', this.foundItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -128,7 +128,7 @@ export default class SearchInput {
|
||||||
this.listeners.on(this.input, 'input', () => {
|
this.listeners.on(this.input, 'input', () => {
|
||||||
this.searchQuery = this.input.value;
|
this.searchQuery = this.input.value;
|
||||||
|
|
||||||
this.onSearch(this.foundItems);
|
this.onSearch(this.searchQuery, this.foundItems);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,41 +1,3 @@
|
||||||
.wobble {
|
|
||||||
animation-name: wobble;
|
|
||||||
animation-duration: 400ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Nick Pettit - https://github.com/nickpettit/glide
|
|
||||||
*/
|
|
||||||
@keyframes wobble {
|
|
||||||
from {
|
|
||||||
transform: translate3d(0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
15% {
|
|
||||||
transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -5deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
30% {
|
|
||||||
transform: translate3d(2%, 0, 0) rotate3d(0, 0, 1, 3deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
45% {
|
|
||||||
transform: translate3d(-3%, 0, 0) rotate3d(0, 0, 1, -3deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
60% {
|
|
||||||
transform: translate3d(2%, 0, 0) rotate3d(0, 0, 1, 2deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
75% {
|
|
||||||
transform: translate3d(-1%, 0, 0) rotate3d(0, 0, 1, -1deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
transform: translate3d(0, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bounceIn {
|
@keyframes bounceIn {
|
||||||
from,
|
from,
|
||||||
20%,
|
20%,
|
||||||
|
@ -101,36 +63,3 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes panelShowing {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-8px) scale(0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
70% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes panelShowingMobile {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(14px) scale(0.98);
|
|
||||||
}
|
|
||||||
|
|
||||||
70% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(-4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -10,5 +10,6 @@
|
||||||
@import './export.css';
|
@import './export.css';
|
||||||
@import './stub.css';
|
@import './stub.css';
|
||||||
@import './rtl.css';
|
@import './rtl.css';
|
||||||
@import './popover.css';
|
|
||||||
@import './input.css';
|
@import './input.css';
|
||||||
|
@import './popover.css';
|
||||||
|
|
||||||
|
|
|
@ -1,45 +1,88 @@
|
||||||
|
/**
|
||||||
|
* Popover styles
|
||||||
|
*/
|
||||||
.ce-popover {
|
.ce-popover {
|
||||||
position: absolute;
|
--border-radius: 6px;
|
||||||
opacity: 0;
|
--width: 200px;
|
||||||
will-change: opacity, transform;
|
--max-height: 270px;
|
||||||
display: flex;
|
--padding: 6px;
|
||||||
flex-direction: column;
|
--offset-from-target: 8px;
|
||||||
padding: 6px;
|
--color-border: #e8e8eb;
|
||||||
min-width: 200px;
|
--color-shadow: rgba(13,20,33,0.13);
|
||||||
width: 200px;
|
--color-background: white;
|
||||||
|
--color-text-primary: black;
|
||||||
|
--color-text-secondary: #707684;
|
||||||
|
--color-border-icon: rgb(201 201 204 / 48%);
|
||||||
|
--color-border-icon-disabled: #EFF0F1;
|
||||||
|
--color-text-icon-active: #388AE5;
|
||||||
|
--color-background-icon-active: rgba(56, 138, 229, 0.1);
|
||||||
|
--color-background-item-focus: rgba(34, 186, 255, 0.08);
|
||||||
|
--color-shadow-item-focus: rgba(7, 161, 227, 0.08);
|
||||||
|
--color-background-item-hover: #eff2f5;
|
||||||
|
--color-background-item-confirm: #E24A4A;
|
||||||
|
--color-background-item-confirm-hover: #CE4343;
|
||||||
|
|
||||||
|
min-width: var(--width);
|
||||||
|
width: var(--width);
|
||||||
|
max-height: var(--max-height);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
flex-shrink: 0;
|
box-shadow: 0 3px 15px -3px var(--color-shadow);
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: calc(100% + var(--offset-from-target));
|
||||||
|
background: var(--color-background);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
z-index: 4;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
max-height: 0;
|
max-height: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
padding: 0;
|
||||||
@apply --overlay-pane;
|
border: none;
|
||||||
|
|
||||||
z-index: 4;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
|
|
||||||
&--opened {
|
&--opened {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
max-height: 270px;
|
padding: var(--padding);
|
||||||
pointer-events: auto;
|
max-height: var(--max-height);
|
||||||
animation: panelShowing 100ms ease;
|
pointer-events: auto;
|
||||||
|
animation: panelShowing 100ms ease;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
|
||||||
@media (--mobile) {
|
@media (--mobile) {
|
||||||
animation: panelShowingMobile 250ms ease;
|
animation: panelShowingMobile 250ms ease;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&__items {
|
||||||
width: 7px;
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
&__overlay {
|
||||||
box-sizing: border-box;
|
@media (--mobile) {
|
||||||
box-shadow: inset 0 0 2px 2px var(--bg-light);
|
position: fixed;
|
||||||
border: 3px solid transparent;
|
top: 0;
|
||||||
border-left-width: 0px;
|
bottom: 0;
|
||||||
border-top-width: 4px;
|
left: 0;
|
||||||
border-bottom-width: 4px;
|
right: 0;
|
||||||
|
background: var(--color-dark);
|
||||||
|
z-index: 3;
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 0.12s ease-in;
|
||||||
|
will-change: opacity;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--open-top {
|
||||||
|
top: calc(-1 * (var(--offset-from-target) + var(--popover-height)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (--mobile) {
|
@media (--mobile) {
|
||||||
|
@ -53,163 +96,37 @@
|
||||||
bottom: calc(var(--offset) + env(safe-area-inset-bottom));
|
bottom: calc(var(--offset) + env(safe-area-inset-bottom));
|
||||||
top: auto;
|
top: auto;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
|
||||||
|
|
||||||
&__items {
|
.ce-popover__search {
|
||||||
overflow-y: auto;
|
|
||||||
overscroll-behavior: contain;
|
|
||||||
|
|
||||||
@media (--not-mobile) {
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__item {
|
|
||||||
@apply --popover-button;
|
|
||||||
|
|
||||||
@media (--can-hover) {
|
|
||||||
&:hover {
|
|
||||||
|
|
||||||
&:not(.ce-popover__item--no-visible-hover) {
|
|
||||||
background-color: var(--bg-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ce-popover__item-icon {
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--disabled {
|
|
||||||
@apply --button-disabled;
|
|
||||||
|
|
||||||
.ce-popover__item-icon {
|
|
||||||
box-shadow: 0 0 0 1px var(--color-line-gray);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--focused {
|
|
||||||
&:not(.ce-popover__item--no-visible-focus) {
|
|
||||||
@apply --button-focused;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--hidden {
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--active {
|
|
||||||
@apply --button-active;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--confirmation {
|
|
||||||
background: var(--color-confirm);
|
|
||||||
|
|
||||||
.ce-popover__item-icon {
|
|
||||||
color: var(--color-confirm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ce-popover__item-label {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.ce-popover__item--no-visible-hover) {
|
|
||||||
@media (--can-hover) {
|
|
||||||
&:hover {
|
|
||||||
background: var(--color-confirm-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.ce-popover__item--no-visible-focus) {
|
|
||||||
&.ce-popover__item--focused {
|
|
||||||
background: var(--color-confirm-hover) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
&-icon {
|
|
||||||
@apply --tool-icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-label {
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
width: 25px;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-secondary-label {
|
|
||||||
color: var(--grayText);
|
|
||||||
font-size: 12px;
|
|
||||||
margin-left: auto;
|
|
||||||
white-space: nowrap;
|
|
||||||
letter-spacing: -0.1em;
|
|
||||||
padding-right: 5px;
|
|
||||||
margin-bottom: -2px;
|
|
||||||
opacity: 0.6;
|
|
||||||
|
|
||||||
@media (--mobile){
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--confirmation, &--active, &--focused {
|
|
||||||
.ce-popover__item-icon {
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__no-found {
|
&__search, &__custom-content:not(:empty) {
|
||||||
@apply --popover-button;
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__nothing-found-message {
|
||||||
color: var(--grayText);
|
color: var(--grayText);
|
||||||
display: none;
|
display: none;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
padding: 3px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
&--shown {
|
&--displayed {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (--mobile) {
|
|
||||||
&__overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
background: var(--color-dark);
|
|
||||||
opacity: 0.5;
|
|
||||||
z-index: 3;
|
|
||||||
transition: opacity 0.12s ease-in;
|
|
||||||
will-change: opacity;
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cdx-search-field {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__overlay--hidden {
|
|
||||||
z-index: 0;
|
|
||||||
opacity: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__custom-content:not(:empty) {
|
&__custom-content:not(:empty) {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
|
|
||||||
@media (--not-mobile) {
|
@media (--not-mobile) {
|
||||||
margin-top: 5px;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -218,3 +135,241 @@
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Popover item styles
|
||||||
|
*/
|
||||||
|
.ce-popover-item {
|
||||||
|
--border-radius: 6px;
|
||||||
|
--icon-size: 20px;
|
||||||
|
--icon-size-mobile: 28px;
|
||||||
|
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 3px;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
@media (--mobile) {
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:last-of-type) {
|
||||||
|
margin-bottom: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
border-radius: 5px;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
box-shadow: 0 0 0 1px var(--color-border-icon);
|
||||||
|
background: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 10px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: var(--icon-size);
|
||||||
|
height: var(--icon-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (--mobile){
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: var(--icon-size-mobile);
|
||||||
|
height: var(--icon-size-mobile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
@media (--mobile) {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__secondary-title {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
letter-spacing: -0.1em;
|
||||||
|
padding-right: 5px;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
opacity: 0.6;
|
||||||
|
|
||||||
|
@media (--mobile){
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
background: var(--color-background-icon-active);
|
||||||
|
color: var(--color-text-icon-active);
|
||||||
|
|
||||||
|
.ce-popover-item__icon {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--disabled {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
cursor: default;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
.ce-popover-item__icon {
|
||||||
|
box-shadow: 0 0 0 1px var(--color-border-icon-disabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--focused {
|
||||||
|
&:not(.ce-popover-item--no-focus) {
|
||||||
|
box-shadow: inset 0 0 0px 1px var(--color-shadow-item-focus);
|
||||||
|
background: var(--color-background-item-focus) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (--can-hover) {
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:not(.ce-popover-item--no-hover) {
|
||||||
|
background-color: var(--color-background-item-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ce-popover-item__icon {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--confirmation {
|
||||||
|
background: var(--color-background-item-confirm);
|
||||||
|
|
||||||
|
.ce-popover-item__icon {
|
||||||
|
color: var(--color-background-item-confirm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ce-popover-item__title {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* confirmation hover */
|
||||||
|
&:not(.ce-popover-item--no-hover) {
|
||||||
|
@media (--can-hover) {
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-background-item-confirm-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* confirmation focus */
|
||||||
|
&:not(.ce-popover-item--no-focus) {
|
||||||
|
&.ce-popover-item--focused {
|
||||||
|
background: var(--color-background-item-confirm-hover) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
&--confirmation, &--active, &--focused {
|
||||||
|
.ce-popover-item__icon {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animations
|
||||||
|
*/
|
||||||
|
@keyframes panelShowing {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-8px) scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
70% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes panelShowingMobile {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(14px) scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
70% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.wobble {
|
||||||
|
animation-name: wobble;
|
||||||
|
animation-duration: 400ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Nick Pettit - https://github.com/nickpettit/glide
|
||||||
|
*/
|
||||||
|
@keyframes wobble {
|
||||||
|
from {
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
15% {
|
||||||
|
transform: translate3d(-9%, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
30% {
|
||||||
|
transform: translate3d(9%, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
45% {
|
||||||
|
transform: translate3d(-4%, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
60% {
|
||||||
|
transform: translate3d(4%, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
75% {
|
||||||
|
transform: translate3d(-1%, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,18 +1,4 @@
|
||||||
.ce-settings {
|
.ce-settings {
|
||||||
position: absolute;
|
|
||||||
z-index: 2;
|
|
||||||
--gap: 8px;
|
|
||||||
|
|
||||||
@media (--not-mobile){
|
|
||||||
position: absolute;
|
|
||||||
top: calc(var(--toolbox-buttons-size) + var(--gap));
|
|
||||||
left: 0;
|
|
||||||
|
|
||||||
&--opened-top {
|
|
||||||
top: calc(-1 * (var(--gap) + var(--popover-height)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__button {
|
&__button {
|
||||||
@apply --toolbar-button;
|
@apply --toolbar-button;
|
||||||
|
|
||||||
|
@ -36,3 +22,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.codex-editor--narrow .ce-settings {
|
||||||
|
@media (--not-mobile){
|
||||||
|
.ce-popover {
|
||||||
|
right: 0;
|
||||||
|
left: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,24 +1,12 @@
|
||||||
.ce-toolbox {
|
.ce-toolbox {
|
||||||
--gap: 8px;
|
|
||||||
|
|
||||||
@media (--not-mobile){
|
|
||||||
position: absolute;
|
|
||||||
top: calc(var(--toolbox-buttons-size) + var(--gap));
|
|
||||||
left: 0;
|
|
||||||
|
|
||||||
&--opened-top {
|
|
||||||
top: calc(-1 * (var(--gap) + var(--popover-height)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.codex-editor--narrow .ce-toolbox {
|
.codex-editor--narrow .ce-toolbox {
|
||||||
@media (--not-mobile){
|
@media (--not-mobile){
|
||||||
left: auto;
|
|
||||||
right: 0;
|
|
||||||
|
|
||||||
.ce-popover {
|
.ce-popover {
|
||||||
right: 0;
|
right: 0;
|
||||||
|
left: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,12 +68,6 @@
|
||||||
*/
|
*/
|
||||||
--block-padding-vertical: 0.4em;
|
--block-padding-vertical: 0.4em;
|
||||||
|
|
||||||
/**
|
|
||||||
* Confirm deletion bg
|
|
||||||
*/
|
|
||||||
--color-confirm: #E24A4A;
|
|
||||||
--color-confirm-hover: #CE4343;
|
|
||||||
|
|
||||||
--color-line-gray: #EFF0F1;
|
--color-line-gray: #EFF0F1;
|
||||||
|
|
||||||
--overlay-pane: {
|
--overlay-pane: {
|
||||||
|
@ -146,7 +140,6 @@
|
||||||
animation: bounceIn 0.75s 1;
|
animation: bounceIn 0.75s 1;
|
||||||
animation-fill-mode: forwards;
|
animation-fill-mode: forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -197,33 +190,6 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Element of the Toolbox. Has icon and label
|
|
||||||
*/
|
|
||||||
--popover-button: {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto auto 1fr;
|
|
||||||
grid-template-rows: auto;
|
|
||||||
justify-content: start;
|
|
||||||
white-space: nowrap;
|
|
||||||
padding: 3px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 20px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 6px;
|
|
||||||
|
|
||||||
&:not(:last-of-type){
|
|
||||||
margin-bottom: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (--mobile) {
|
|
||||||
font-size: 16px;
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tool icon with border
|
* Tool icon with border
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -39,11 +39,11 @@ describe('Editor Tools Api', () => {
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
cy.get('[data-cy=editorjs]')
|
cy.get('[data-cy=editorjs]')
|
||||||
.get('div.ce-popover__item[data-item-name=testTool]')
|
.get('div.ce-popover-item[data-item-name=testTool]')
|
||||||
.should('have.length', 1);
|
.should('have.length', 1);
|
||||||
|
|
||||||
cy.get('[data-cy=editorjs]')
|
cy.get('[data-cy=editorjs]')
|
||||||
.get('div.ce-popover__item[data-item-name=testTool] .ce-popover__item-icon')
|
.get('div.ce-popover-item[data-item-name=testTool] .ce-popover-item__icon')
|
||||||
.should('contain.html', TestTool.toolbox.icon);
|
.should('contain.html', TestTool.toolbox.icon);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -84,16 +84,16 @@ describe('Editor Tools Api', () => {
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
cy.get('[data-cy=editorjs]')
|
cy.get('[data-cy=editorjs]')
|
||||||
.get('div.ce-popover__item[data-item-name=testTool]')
|
.get('div.ce-popover-item[data-item-name=testTool]')
|
||||||
.should('have.length', 2);
|
.should('have.length', 2);
|
||||||
|
|
||||||
cy.get('[data-cy=editorjs]')
|
cy.get('[data-cy=editorjs]')
|
||||||
.get('div.ce-popover__item[data-item-name=testTool]')
|
.get('div.ce-popover-item[data-item-name=testTool]')
|
||||||
.first()
|
.first()
|
||||||
.should('contain.text', TestTool.toolbox[0].title);
|
.should('contain.text', TestTool.toolbox[0].title);
|
||||||
|
|
||||||
cy.get('[data-cy=editorjs]')
|
cy.get('[data-cy=editorjs]')
|
||||||
.get('div.ce-popover__item[data-item-name=testTool]')
|
.get('div.ce-popover-item[data-item-name=testTool]')
|
||||||
.last()
|
.last()
|
||||||
.should('contain.text', TestTool.toolbox[1].title);
|
.should('contain.text', TestTool.toolbox[1].title);
|
||||||
});
|
});
|
||||||
|
@ -173,7 +173,7 @@ describe('Editor Tools Api', () => {
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
cy.get('[data-cy=editorjs]')
|
cy.get('[data-cy=editorjs]')
|
||||||
.get('div.ce-popover__item[data-item-name=testTool]')
|
.get('div.ce-popover-item[data-item-name=testTool]')
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
cy.get('[data-cy=editorjs]')
|
cy.get('[data-cy=editorjs]')
|
||||||
|
|
|
@ -228,13 +228,13 @@ describe('Editor Tunes Api', () => {
|
||||||
|
|
||||||
/** Check test tune is inserted at index 0 */
|
/** Check test tune is inserted at index 0 */
|
||||||
cy.get('[data-cy=editorjs]')
|
cy.get('[data-cy=editorjs]')
|
||||||
.get('.ce-settings .ce-popover__item')
|
.get('.ce-settings .ce-popover-item')
|
||||||
.eq(0)
|
.eq(0)
|
||||||
.should('have.attr', 'data-item-name', 'test-tune' );
|
.should('have.attr', 'data-item-name', 'test-tune' );
|
||||||
|
|
||||||
/** Check default Move Up tune is inserted below the test tune */
|
/** Check default Move Up tune is inserted below the test tune */
|
||||||
cy.get('[data-cy=editorjs]')
|
cy.get('[data-cy=editorjs]')
|
||||||
.get('.ce-settings .ce-popover__item')
|
.get('.ce-settings .ce-popover-item')
|
||||||
.eq(1)
|
.eq(1)
|
||||||
.should('have.attr', 'data-item-name', 'move-up' );
|
.should('have.attr', 'data-item-name', 'move-up' );
|
||||||
});
|
});
|
||||||
|
|
|
@ -33,7 +33,7 @@ describe('Block ids', () => {
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
cy.get('[data-cy=editorjs]')
|
cy.get('[data-cy=editorjs]')
|
||||||
.get('div.ce-popover__item[data-item-name=header]')
|
.get('div.ce-popover-item[data-item-name=header]')
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
cy.get('[data-cy=editorjs]')
|
cy.get('[data-cy=editorjs]')
|
||||||
|
|
|
@ -31,7 +31,7 @@ describe('Editor i18n', () => {
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
cy.get('[data-cy=editorjs]')
|
cy.get('[data-cy=editorjs]')
|
||||||
.get('div.ce-popover__item[data-item-name=header]')
|
.get('div.ce-popover-item[data-item-name=header]')
|
||||||
.should('contain.text', toolNamesDictionary.Heading);
|
.should('contain.text', toolNamesDictionary.Heading);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -85,12 +85,12 @@ describe('Editor i18n', () => {
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
cy.get('[data-cy=editorjs]')
|
cy.get('[data-cy=editorjs]')
|
||||||
.get('div.ce-popover__item[data-item-name=testTool]')
|
.get('div.ce-popover-item[data-item-name=testTool]')
|
||||||
.first()
|
.first()
|
||||||
.should('contain.text', toolNamesDictionary.Title1);
|
.should('contain.text', toolNamesDictionary.Title1);
|
||||||
|
|
||||||
cy.get('[data-cy=editorjs]')
|
cy.get('[data-cy=editorjs]')
|
||||||
.get('div.ce-popover__item[data-item-name=testTool]')
|
.get('div.ce-popover-item[data-item-name=testTool]')
|
||||||
.last()
|
.last()
|
||||||
.should('contain.text', toolNamesDictionary.Title2);
|
.should('contain.text', toolNamesDictionary.Title2);
|
||||||
});
|
});
|
||||||
|
@ -137,7 +137,7 @@ describe('Editor i18n', () => {
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
cy.get('[data-cy=editorjs]')
|
cy.get('[data-cy=editorjs]')
|
||||||
.get('div.ce-popover__item[data-item-name=testTool]')
|
.get('div.ce-popover-item[data-item-name=testTool]')
|
||||||
.should('contain.text', toolNamesDictionary.TestTool);
|
.should('contain.text', toolNamesDictionary.TestTool);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -131,7 +131,7 @@ describe('onChange callback', () => {
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
cy.get('[data-cy=editorjs]')
|
cy.get('[data-cy=editorjs]')
|
||||||
.get('div.ce-popover__item[data-item-name=delimiter]')
|
.get('div.ce-popover-item[data-item-name=delimiter]')
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
cy.get('@onChange').should('be.calledThrice');
|
cy.get('@onChange').should('be.calledThrice');
|
||||||
|
@ -178,7 +178,7 @@ describe('onChange callback', () => {
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
cy.get('[data-cy=editorjs]')
|
cy.get('[data-cy=editorjs]')
|
||||||
.get('div.ce-popover__item[data-item-name=header]')
|
.get('div.ce-popover-item[data-item-name=header]')
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
cy.get('@onChange').should('be.calledTwice');
|
cy.get('@onChange').should('be.calledTwice');
|
||||||
|
|
|
@ -85,7 +85,7 @@ describe('Flipper', () => {
|
||||||
* Check whether we focus the Delete Tune or not
|
* Check whether we focus the Delete Tune or not
|
||||||
*/
|
*/
|
||||||
cy.get('[data-item-name="delete"]')
|
cy.get('[data-item-name="delete"]')
|
||||||
.should('have.class', 'ce-popover__item--focused');
|
.should('have.class', 'ce-popover-item--focused');
|
||||||
|
|
||||||
cy.get('[data-cy=editorjs]')
|
cy.get('[data-cy=editorjs]')
|
||||||
.get('.cdx-some-plugin')
|
.get('.cdx-some-plugin')
|
||||||
|
|
|
@ -31,20 +31,17 @@ describe('Popover', () => {
|
||||||
|
|
||||||
const popover = new Popover({
|
const popover = new Popover({
|
||||||
items,
|
items,
|
||||||
filterLabel: '',
|
|
||||||
nothingFoundLabel: '',
|
|
||||||
scopeElement: null,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.document().then(doc => {
|
cy.document().then(doc => {
|
||||||
doc.body.append(popover.getElement());
|
doc.body.append(popover.getElement());
|
||||||
|
|
||||||
cy.get('[data-item-name=testItem]')
|
cy.get('[data-item-name=testItem]')
|
||||||
.get('.ce-popover__item-icon')
|
.get('.ce-popover-item__icon')
|
||||||
.should('have.text', actionIcon);
|
.should('have.text', actionIcon);
|
||||||
|
|
||||||
cy.get('[data-item-name=testItem]')
|
cy.get('[data-item-name=testItem]')
|
||||||
.get('.ce-popover__item-label')
|
.get('.ce-popover-item__title')
|
||||||
.should('have.text', actionTitle);
|
.should('have.text', actionTitle);
|
||||||
|
|
||||||
// First click on item
|
// First click on item
|
||||||
|
@ -52,12 +49,12 @@ describe('Popover', () => {
|
||||||
|
|
||||||
// Check icon has changed
|
// Check icon has changed
|
||||||
cy.get('[data-item-name=testItem]')
|
cy.get('[data-item-name=testItem]')
|
||||||
.get('.ce-popover__item-icon')
|
.get('.ce-popover-item__icon')
|
||||||
.should('have.text', confirmActionIcon);
|
.should('have.text', confirmActionIcon);
|
||||||
|
|
||||||
// Check label has changed
|
// Check label has changed
|
||||||
cy.get('[data-item-name=testItem]')
|
cy.get('[data-item-name=testItem]')
|
||||||
.get('.ce-popover__item-label')
|
.get('.ce-popover-item__title')
|
||||||
.should('have.text', confirmActionTitle);
|
.should('have.text', confirmActionTitle);
|
||||||
|
|
||||||
// Second click
|
// Second click
|
||||||
|
@ -83,9 +80,6 @@ describe('Popover', () => {
|
||||||
|
|
||||||
const popover = new Popover({
|
const popover = new Popover({
|
||||||
items,
|
items,
|
||||||
filterLabel: '',
|
|
||||||
nothingFoundLabel: '',
|
|
||||||
scopeElement: null,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.document().then(doc => {
|
cy.document().then(doc => {
|
||||||
|
@ -93,7 +87,7 @@ describe('Popover', () => {
|
||||||
|
|
||||||
/* Check item has active class */
|
/* Check item has active class */
|
||||||
cy.get('[data-item-name=testItem]')
|
cy.get('[data-item-name=testItem]')
|
||||||
.should('have.class', 'ce-popover__item--active');
|
.should('have.class', 'ce-popover-item--active');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -110,9 +104,6 @@ describe('Popover', () => {
|
||||||
|
|
||||||
const popover = new Popover({
|
const popover = new Popover({
|
||||||
items,
|
items,
|
||||||
filterLabel: '',
|
|
||||||
nothingFoundLabel: '',
|
|
||||||
scopeElement: null,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.document().then(doc => {
|
cy.document().then(doc => {
|
||||||
|
@ -120,7 +111,7 @@ describe('Popover', () => {
|
||||||
|
|
||||||
/* Check item has disabled class */
|
/* Check item has disabled class */
|
||||||
cy.get('[data-item-name=testItem]')
|
cy.get('[data-item-name=testItem]')
|
||||||
.should('have.class', 'ce-popover__item--disabled')
|
.should('have.class', 'ce-popover-item--disabled')
|
||||||
.click()
|
.click()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Check onActivate callback has never been called
|
// Check onActivate callback has never been called
|
||||||
|
@ -141,9 +132,6 @@ describe('Popover', () => {
|
||||||
];
|
];
|
||||||
const popover = new Popover({
|
const popover = new Popover({
|
||||||
items,
|
items,
|
||||||
filterLabel: '',
|
|
||||||
nothingFoundLabel: '',
|
|
||||||
scopeElement: null,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.spy(popover, 'hide');
|
cy.spy(popover, 'hide');
|
||||||
|
@ -171,9 +159,6 @@ describe('Popover', () => {
|
||||||
];
|
];
|
||||||
const popover = new Popover({
|
const popover = new Popover({
|
||||||
items,
|
items,
|
||||||
filterLabel: '',
|
|
||||||
nothingFoundLabel: '',
|
|
||||||
scopeElement: null,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.document().then(doc => {
|
cy.document().then(doc => {
|
||||||
|
@ -182,7 +167,7 @@ describe('Popover', () => {
|
||||||
/* Check item has active class */
|
/* Check item has active class */
|
||||||
cy.get('[data-item-name=testItem]')
|
cy.get('[data-item-name=testItem]')
|
||||||
.click()
|
.click()
|
||||||
.should('have.class', 'ce-popover__item--active');
|
.should('have.class', 'ce-popover-item--active');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -207,9 +192,6 @@ describe('Popover', () => {
|
||||||
|
|
||||||
const popover = new Popover({
|
const popover = new Popover({
|
||||||
items,
|
items,
|
||||||
filterLabel: '',
|
|
||||||
nothingFoundLabel: '',
|
|
||||||
scopeElement: null,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.document().then(doc => {
|
cy.document().then(doc => {
|
||||||
|
@ -217,20 +199,20 @@ describe('Popover', () => {
|
||||||
|
|
||||||
/** Check first item is active */
|
/** Check first item is active */
|
||||||
cy.get('[data-item-name=testItem1]')
|
cy.get('[data-item-name=testItem1]')
|
||||||
.should('have.class', 'ce-popover__item--active');
|
.should('have.class', 'ce-popover-item--active');
|
||||||
|
|
||||||
/** Check second item is not active */
|
/** Check second item is not active */
|
||||||
cy.get('[data-item-name=testItem2]')
|
cy.get('[data-item-name=testItem2]')
|
||||||
.should('not.have.class', 'ce-popover__item--active');
|
.should('not.have.class', 'ce-popover-item--active');
|
||||||
|
|
||||||
/* Click second item and check it became active */
|
/* Click second item and check it became active */
|
||||||
cy.get('[data-item-name=testItem2]')
|
cy.get('[data-item-name=testItem2]')
|
||||||
.click()
|
.click()
|
||||||
.should('have.class', 'ce-popover__item--active');
|
.should('have.class', 'ce-popover-item--active');
|
||||||
|
|
||||||
/** Check first item became not active */
|
/** Check first item became not active */
|
||||||
cy.get('[data-item-name=testItem1]')
|
cy.get('[data-item-name=testItem1]')
|
||||||
.should('not.have.class', 'ce-popover__item--active');
|
.should('not.have.class', 'ce-popover-item--active');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -246,9 +228,6 @@ describe('Popover', () => {
|
||||||
];
|
];
|
||||||
const popover = new Popover({
|
const popover = new Popover({
|
||||||
items,
|
items,
|
||||||
filterLabel: '',
|
|
||||||
nothingFoundLabel: '',
|
|
||||||
scopeElement: null,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.document().then(doc => {
|
cy.document().then(doc => {
|
||||||
|
@ -257,7 +236,25 @@ describe('Popover', () => {
|
||||||
/* Check item has active class */
|
/* Check item has active class */
|
||||||
cy.get('[data-item-name=testItem]')
|
cy.get('[data-item-name=testItem]')
|
||||||
.click()
|
.click()
|
||||||
.should('have.class', 'ce-popover__item--active');
|
.should('have.class', 'ce-popover-item--active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render custom html content', () => {
|
||||||
|
const customHtml = document.createElement('div');
|
||||||
|
|
||||||
|
customHtml.setAttribute('data-cy-name', 'customContent');
|
||||||
|
customHtml.innerText = 'custom html content';
|
||||||
|
const popover = new Popover({
|
||||||
|
customContent: customHtml,
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.document().then(doc => {
|
||||||
|
doc.body.append(popover.getElement());
|
||||||
|
|
||||||
|
/* Check custom content exists in the popover */
|
||||||
|
cy.get('[data-cy-name=customContent]');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue