mirror of
https://github.com/codex-team/editor.js
synced 2024-05-15 12:57:38 +02:00
feat(ui): the toolbox became vertical (#2014)
* the popover component, vertical toolbox * toolbox position improved * popover width improved * always show the plus button * search field added * search input in popover * trying to create mobile toolbox * feat(toolbox): popover adapted for mobile devices (#2004) * FIx mobile popover fixed positioning * Add mobile popover overlay * Hide mobile popover on scroll * Alter toolbox buttons hover * Fix closing popover on overlay click * Tests fix * Fix onchange test * restore focus after toolbox closing by ESC * don't move toolbar by block-hover on mobile Resolves #1972 * popover mobile styles improved * Cleanup * Remove scroll event listener * Lock scroll on mobile * don't show shortcuts in mobile popover * Change data attr name * Remove unused styles * Remove unused listeners * disable hover on mobile popover * Scroll fix * Lint * Revert "Scroll fix" This reverts commit82deae543e
. * Return back background color for active state of toolbox buttons Co-authored-by: Peter Savchenko <specc.dev@gmail.com> * Vertical toolbox fixes (#2017) * Replace visibility property with display for hiding popover * Disable arrow right and left keys for popover * Revert "Replace visibility property with display for hiding popover" This reverts commitaf521cf6f2
. * Hide popover via setting max-height to 0 to fix animation in safari * Remove redundant condition * Extend element interface to avoid ts errors * Do not subscribe to block hovered if mobile * Add unsubscribing from overlay click event * Rename isMobile to isMobileScreen * Cleanup * fix: popover opening direction (#2022) * Change popover opening direction based on available space below it * Update check * Use cacheable decorator * Update src/components/flipper.ts Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com> * Fixes * Fix test * Clear search on popover hide * Fix popover width * Fix for tests * Update todos * Linter fixes * rm todo about beforeInsert because I have no idea what does it mean * i18n for search labels done * rm methods for hiding/showing of + * some code style update * Update CHANGELOG.md * make the list items a little bit compact * fix z-index issue caused by block-appearing animation also, improve popover padding for two reasons: - make the popover more consistent with the Table tool popover (in future, it can be done with the same api method) - make popover looks better Co-authored-by: Tanya Fomina <fomina.tatianaaa@yandex.ru> Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com>
This commit is contained in:
parent
f9e280fcad
commit
8f156a87ea
|
@ -1,8 +1,11 @@
|
|||
# Changelog
|
||||
|
||||
### 2.23.3
|
||||
### 2.24.0
|
||||
|
||||
- `New` — *UI* — The Toolbox became vertical 🥳
|
||||
- `Improvement` — *UI* — the Plus button will always be shown (previously, it appears only for empty blocks)
|
||||
- `Improvement` — *Dev Example Page* - Server added to allow opening example page on other devices in network.
|
||||
- `Fix` - `UI` - the Toolbar won't move on hover at mobile viewports. Resolves [#1972](https://github.com/codex-team/editor.js/issues/1972)
|
||||
- `Fix` — `OnChange` event invocation after block insertion. [#1997](https://github.com/codex-team/editor.js/issues/1997)
|
||||
|
||||
### 2.23.2
|
||||
|
@ -435,4 +438,4 @@ See a whole [Changelog](/docs/)
|
|||
- `New` New [Editor.js PHP](http://github.com/codex-team/codex.editor.backend) — example of server-side implementation with HTML purifying and data validation.
|
||||
- `Improvements` - Improvements of Toolbar's position calculation.
|
||||
- `Improvements` — Improved zero-configuration initialization.
|
||||
- and many little improvements.
|
||||
- and many little improvements.
|
||||
|
|
|
@ -193,7 +193,9 @@
|
|||
},
|
||||
"toolbar": {
|
||||
"toolbox": {
|
||||
"Add": "Добавить"
|
||||
"Add": "Добавить",
|
||||
"Filter": "Поиск",
|
||||
"Noting found": "Ничего не найдено"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
3
src/assets/search.svg
Normal file
3
src/assets/search.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<path d="M12.711 2.18a7.448 7.448 0 0 1 .79 9.603l2.143 2.144a1.214 1.214 0 1 1-1.717 1.717L11.783 13.5a7.446 7.446 0 1 1 .928-11.32ZM11.39 3.61a5.5 5.5 0 1 0-7.778 7.78 5.5 5.5 0 0 0 7.778-7.78Z" fill-rule="evenodd"/>
|
||||
</svg>
|
After Width: | Height: | Size: 289 B |
|
@ -17,21 +17,23 @@ export interface FlipperOptions {
|
|||
*/
|
||||
items?: HTMLElement[];
|
||||
|
||||
/**
|
||||
* Defines arrows usage. By default Flipper leafs items also via RIGHT/LEFT.
|
||||
*
|
||||
* true by default
|
||||
*
|
||||
* Pass 'false' if you don't need this behaviour
|
||||
* (for example, Inline Toolbar should be closed by arrows,
|
||||
* because it means caret moving with selection clearing)
|
||||
*/
|
||||
allowArrows?: boolean;
|
||||
|
||||
/**
|
||||
* Optional callback for button click
|
||||
*/
|
||||
activateCallback?: (item: HTMLElement) => void;
|
||||
|
||||
/**
|
||||
* List of keys allowed for handling.
|
||||
* Can include codes of the following keys:
|
||||
* - Tab
|
||||
* - Enter
|
||||
* - Arrow up
|
||||
* - Arrow down
|
||||
* - Arrow right
|
||||
* - Arrow left
|
||||
* If not specified all keys are enabled
|
||||
*/
|
||||
allowedKeys?: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -53,11 +55,9 @@ export default class Flipper {
|
|||
private activated = false;
|
||||
|
||||
/**
|
||||
* Flag that allows arrows usage to flip items
|
||||
*
|
||||
* @type {boolean}
|
||||
* List codes of the keys allowed for handling
|
||||
*/
|
||||
private readonly allowArrows: boolean = true;
|
||||
private readonly allowedKeys: number[];
|
||||
|
||||
/**
|
||||
* Call back for button click/enter
|
||||
|
@ -68,9 +68,9 @@ export default class Flipper {
|
|||
* @param {FlipperOptions} options - different constructing settings
|
||||
*/
|
||||
constructor(options: FlipperOptions) {
|
||||
this.allowArrows = _.isBoolean(options.allowArrows) ? options.allowArrows : true;
|
||||
this.iterator = new DomIterator(options.items, options.focusedItemClass);
|
||||
this.activateCallback = options.activateCallback;
|
||||
this.allowedKeys = options.allowedKeys || Flipper.usedKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -120,15 +120,6 @@ export default class Flipper {
|
|||
document.removeEventListener('keydown', this.onKeyDown);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return current focused button
|
||||
*
|
||||
* @returns {HTMLElement|null}
|
||||
*/
|
||||
public get currentItem(): HTMLElement|null {
|
||||
return this.iterator.currentItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus first item
|
||||
*/
|
||||
|
@ -142,6 +133,7 @@ export default class Flipper {
|
|||
*/
|
||||
public flipLeft(): void {
|
||||
this.iterator.previous();
|
||||
this.flipCallback();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -149,6 +141,14 @@ export default class Flipper {
|
|||
*/
|
||||
public flipRight(): void {
|
||||
this.iterator.next();
|
||||
this.flipCallback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if some button is focused
|
||||
*/
|
||||
public hasFocus(): boolean {
|
||||
return !!this.iterator.currentItem;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -206,23 +206,7 @@ export default class Flipper {
|
|||
* @returns {boolean}
|
||||
*/
|
||||
private isEventReadyForHandling(event: KeyboardEvent): boolean {
|
||||
const handlingKeyCodeList = [
|
||||
_.keyCodes.TAB,
|
||||
_.keyCodes.ENTER,
|
||||
];
|
||||
|
||||
const isCurrentItemIsFocusedInput = this.iterator.currentItem == document.activeElement;
|
||||
|
||||
if (this.allowArrows && !isCurrentItemIsFocusedInput) {
|
||||
handlingKeyCodeList.push(
|
||||
_.keyCodes.LEFT,
|
||||
_.keyCodes.RIGHT,
|
||||
_.keyCodes.UP,
|
||||
_.keyCodes.DOWN
|
||||
);
|
||||
}
|
||||
|
||||
return this.activated && handlingKeyCodeList.indexOf(event.keyCode) !== -1;
|
||||
return this.activated && this.allowedKeys.includes(event.keyCode);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -266,4 +250,13 @@ export default class Flipper {
|
|||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired after flipping in any direction
|
||||
*/
|
||||
private flipCallback(): void {
|
||||
if (this.iterator.currentItem) {
|
||||
this.iterator.currentItem.scrollIntoViewIfNeeded();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,9 @@
|
|||
},
|
||||
"toolbar": {
|
||||
"toolbox": {
|
||||
"Add": ""
|
||||
"Add": "",
|
||||
"Filter": "",
|
||||
"Noting found": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -33,7 +33,6 @@ export default class Saver extends Module {
|
|||
chainData = [];
|
||||
|
||||
try {
|
||||
|
||||
blocks.forEach((block: Block) => {
|
||||
chainData.push(this.getSavedData(block));
|
||||
});
|
||||
|
@ -46,7 +45,7 @@ export default class Saver extends Module {
|
|||
return this.makeOutput(sanitizedData);
|
||||
} catch (e) {
|
||||
_.logLabeled(`Saving failed due to the Error %o`, 'error', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -13,6 +13,10 @@ import Toolbox, { ToolboxEvent } from '../../ui/toolbox';
|
|||
* @todo Tab on non-empty block should open Block Settings of the hoveredBlock (not where caret is set)
|
||||
* - make Block Settings a standalone module
|
||||
*
|
||||
* @todo - Keyboard-only mode bug:
|
||||
* press Tab, flip to the Checkbox. press Enter (block will be added), Press Tab
|
||||
* (Block Tunes will be opened with Move up focused), press Enter, press Tab ———— both Block Tunes and Toolbox will be opened
|
||||
*
|
||||
* @todo TESTCASE - show toggler after opening and closing the Inline Toolbar
|
||||
* @todo TESTCASE - Click outside Editor holder should close Toolbar and Clear Focused blocks
|
||||
* @todo TESTCASE - Click inside Editor holder should close Toolbar and Clear Focused blocks
|
||||
|
@ -33,11 +37,7 @@ interface ToolbarNodes {
|
|||
content: HTMLElement;
|
||||
actions: HTMLElement;
|
||||
|
||||
// Content Zone
|
||||
plusButton: HTMLElement;
|
||||
|
||||
// Actions Zone
|
||||
blockActionsButtons: HTMLElement;
|
||||
settingsToggler: HTMLElement;
|
||||
}
|
||||
/**
|
||||
|
@ -137,14 +137,10 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
toolbarOpened: 'ce-toolbar--opened',
|
||||
openedToolboxHolderModifier: 'codex-editor--toolbox-opened',
|
||||
|
||||
// Content Zone
|
||||
plusButton: 'ce-toolbar__plus',
|
||||
plusButtonShortcut: 'ce-toolbar__plus-shortcut',
|
||||
plusButtonHidden: 'ce-toolbar__plus--hidden',
|
||||
|
||||
// Actions Zone
|
||||
blockActionsButtons: 'ce-toolbar__actions-buttons',
|
||||
settingsToggler: 'ce-toolbar__settings-btn',
|
||||
settingsTogglerHidden: 'ce-toolbar__settings-btn--hidden',
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -157,23 +153,6 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
return this.nodes.wrapper.classList.contains(this.CSS.toolbarOpened);
|
||||
}
|
||||
|
||||
/**
|
||||
* Plus Button public methods
|
||||
*
|
||||
* @returns {{hide: function(): void, show: function(): void}}
|
||||
*/
|
||||
public get plusButton(): { hide: () => void; show: () => void } {
|
||||
return {
|
||||
hide: (): void => this.nodes.plusButton.classList.add(this.CSS.plusButtonHidden),
|
||||
show: (): void => {
|
||||
if (this.toolboxInstance.isEmpty) {
|
||||
return;
|
||||
}
|
||||
this.nodes.plusButton.classList.remove(this.CSS.plusButtonHidden);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Public interface for accessing the Toolbox
|
||||
*/
|
||||
|
@ -182,11 +161,14 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
close: () => void;
|
||||
open: () => void;
|
||||
toggle: () => void;
|
||||
flipperHasFocus: boolean;
|
||||
hasFocus: () => boolean;
|
||||
} {
|
||||
return {
|
||||
opened: this.toolboxInstance.opened,
|
||||
close: (): void => this.toolboxInstance.close(),
|
||||
close: (): void => {
|
||||
this.toolboxInstance.close();
|
||||
this.Editor.Caret.setToBlock(this.Editor.BlockManager.currentBlock);
|
||||
},
|
||||
open: (): void => {
|
||||
/**
|
||||
* Set current block to cover the case when the Toolbar showed near hovered Block but caret is set to another Block.
|
||||
|
@ -196,14 +178,12 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
this.toolboxInstance.open();
|
||||
},
|
||||
toggle: (): void => this.toolboxInstance.toggle(),
|
||||
flipperHasFocus: this.toolboxInstance.flipperHasFocus,
|
||||
hasFocus: (): boolean => this.toolboxInstance.hasFocus(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Block actions appearance manipulations
|
||||
*
|
||||
* @returns {{hide: function(): void, show: function(): void}}
|
||||
*/
|
||||
private get blockActions(): { hide: () => void; show: () => void } {
|
||||
return {
|
||||
|
@ -216,6 +196,16 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Methods for working with Block Tunes toggler
|
||||
*/
|
||||
private get blockTunesToggler(): { hide: () => void; show: () => void } {
|
||||
return {
|
||||
hide: (): void => this.nodes.settingsToggler.classList.add(this.CSS.settingsTogglerHidden),
|
||||
show: (): void => this.nodes.settingsToggler.classList.remove(this.CSS.settingsTogglerHidden),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles read-only mode
|
||||
*
|
||||
|
@ -276,15 +266,15 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
/**
|
||||
* Move Toolbar to the Top coordinate of Block
|
||||
*/
|
||||
this.nodes.wrapper.style.transform = `translate3D(0, ${Math.floor(toolbarY)}px, 0)`;
|
||||
this.nodes.wrapper.style.top = `${Math.floor(toolbarY)}px`;
|
||||
|
||||
/**
|
||||
* Plus Button should be shown only for __empty__ __default__ block
|
||||
* Do not show Block Tunes Toggler near single and empty block
|
||||
*/
|
||||
if (block.tool.isDefault && block.isEmpty) {
|
||||
this.plusButton.show();
|
||||
if (this.Editor.BlockManager.blocks.length === 1 && block.isEmpty) {
|
||||
this.blockTunesToggler.hide();
|
||||
} else {
|
||||
this.plusButton.hide();
|
||||
this.blockTunesToggler.show();
|
||||
}
|
||||
|
||||
this.open();
|
||||
|
@ -381,13 +371,11 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
* - Remove Block Button
|
||||
* - Settings Panel
|
||||
*/
|
||||
this.nodes.blockActionsButtons = $.make('div', this.CSS.blockActionsButtons);
|
||||
this.nodes.settingsToggler = $.make('span', this.CSS.settingsToggler);
|
||||
const settingsIcon = $.svg('dots', 16, 16);
|
||||
|
||||
$.append(this.nodes.settingsToggler, settingsIcon);
|
||||
$.append(this.nodes.blockActionsButtons, this.nodes.settingsToggler);
|
||||
$.append(this.nodes.actions, this.nodes.blockActionsButtons);
|
||||
$.append(this.nodes.actions, this.nodes.settingsToggler);
|
||||
|
||||
this.tooltip.onHover(
|
||||
this.nodes.settingsToggler,
|
||||
|
@ -400,7 +388,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
/**
|
||||
* Appending Toolbar components to itself
|
||||
*/
|
||||
$.append(this.nodes.content, this.makeToolbox());
|
||||
$.append(this.nodes.actions, this.makeToolbox());
|
||||
$.append(this.nodes.actions, this.Editor.BlockSettings.nodes.wrapper);
|
||||
|
||||
/**
|
||||
|
@ -419,6 +407,10 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
this.toolboxInstance = new Toolbox({
|
||||
api: this.Editor.API.methods,
|
||||
tools: this.Editor.Tools.blockTools,
|
||||
i18nLabels: {
|
||||
filter: I18n.ui(I18nInternalNS.ui.toolbar.toolbox, 'Filter'),
|
||||
nothingFound: I18n.ui(I18nInternalNS.ui.toolbar.toolbox, 'Noting found'),
|
||||
},
|
||||
});
|
||||
|
||||
this.toolboxInstance.on(ToolboxEvent.Opened, () => {
|
||||
|
@ -487,18 +479,25 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
}, true);
|
||||
|
||||
/**
|
||||
* Subscribe to the 'block-hovered' event
|
||||
* Subscribe to the 'block-hovered' event if currenct view is not mobile
|
||||
*
|
||||
* @see https://github.com/codex-team/editor.js/issues/1972
|
||||
*/
|
||||
this.eventsDispatcher.on(this.Editor.UI.events.blockHovered, (data: {block: Block}) => {
|
||||
if (!_.isMobileScreen()) {
|
||||
/**
|
||||
* Do not move toolbar if Block Settings or Toolbox opened
|
||||
* Subscribe to the 'block-hovered' event
|
||||
*/
|
||||
if (this.Editor.BlockSettings.opened || this.toolboxInstance.opened) {
|
||||
return;
|
||||
}
|
||||
this.eventsDispatcher.on(this.Editor.UI.events.blockHovered, (data: {block: Block}) => {
|
||||
/**
|
||||
* Do not move toolbar if Block Settings or Toolbox opened
|
||||
*/
|
||||
if (this.Editor.BlockSettings.opened || this.toolboxInstance.opened) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.moveAndOpen(data.block);
|
||||
});
|
||||
this.moveAndOpen(data.block);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -698,7 +698,10 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
private enableFlipper(): void {
|
||||
this.flipper = new Flipper({
|
||||
focusedItemClass: this.CSS.focusedButton,
|
||||
allowArrows: false,
|
||||
allowedKeys: [
|
||||
_.keyCodes.ENTER,
|
||||
_.keyCodes.TAB,
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -231,7 +231,7 @@ export default class UI extends Module<UINodes> {
|
|||
* Toolbar has internal module (Toolbox) that has own Flipper,
|
||||
* so we check it manually
|
||||
*/
|
||||
if (this.Editor.Toolbar.toolbox.flipperHasFocus) {
|
||||
if (this.Editor.Toolbar.toolbox.hasFocus()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -239,7 +239,7 @@ export default class UI extends Module<UINodes> {
|
|||
return moduleClass.flipper instanceof Flipper;
|
||||
})
|
||||
.some(([moduleName, moduleClass]) => {
|
||||
return moduleClass.flipper.currentItem;
|
||||
return moduleClass.flipper.hasFocus();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -385,7 +385,7 @@ export default class UI extends Module<UINodes> {
|
|||
*/
|
||||
private watchBlockHoveredEvents(): void {
|
||||
/**
|
||||
* Used to not to emit the same block multiple times to the 'block-hovered' event on every mousemove
|
||||
* Used to not emit the same block multiple times to the 'block-hovered' event on every mousemove
|
||||
*/
|
||||
let blockHoveredEmitted;
|
||||
|
||||
|
|
|
@ -96,3 +96,46 @@ if (!Element.prototype.prepend) {
|
|||
this.insertBefore(docFrag, this.firstChild);
|
||||
};
|
||||
}
|
||||
|
||||
interface Element {
|
||||
/**
|
||||
* Scrolls the current element into the visible area of the browser window
|
||||
*
|
||||
* @param centerIfNeeded - true, if the element should be aligned so it is centered within the visible area of the scrollable ancestor.
|
||||
*/
|
||||
scrollIntoViewIfNeeded(centerIfNeeded?: boolean): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* ScrollIntoViewIfNeeded polyfill by KilianSSL (forked from hsablonniere)
|
||||
*
|
||||
* @see {@link https://gist.github.com/KilianSSL/774297b76378566588f02538631c3137}
|
||||
* @param centerIfNeeded - true, if the element should be aligned so it is centered within the visible area of the scrollable ancestor.
|
||||
*/
|
||||
if (!Element.prototype.scrollIntoViewIfNeeded) {
|
||||
Element.prototype.scrollIntoViewIfNeeded = function (centerIfNeeded): void {
|
||||
centerIfNeeded = arguments.length === 0 ? true : !!centerIfNeeded;
|
||||
|
||||
const parent = this.parentNode,
|
||||
parentComputedStyle = window.getComputedStyle(parent, null),
|
||||
parentBorderTopWidth = parseInt(parentComputedStyle.getPropertyValue('border-top-width')),
|
||||
parentBorderLeftWidth = parseInt(parentComputedStyle.getPropertyValue('border-left-width')),
|
||||
overTop = this.offsetTop - parent.offsetTop < parent.scrollTop,
|
||||
overBottom = (this.offsetTop - parent.offsetTop + this.clientHeight - parentBorderTopWidth) > (parent.scrollTop + parent.clientHeight),
|
||||
overLeft = this.offsetLeft - parent.offsetLeft < parent.scrollLeft,
|
||||
overRight = (this.offsetLeft - parent.offsetLeft + this.clientWidth - parentBorderLeftWidth) > (parent.scrollLeft + parent.clientWidth),
|
||||
alignWithTop = overTop && !overBottom;
|
||||
|
||||
if ((overTop || overBottom) && centerIfNeeded) {
|
||||
parent.scrollTop = this.offsetTop - parent.offsetTop - parent.clientHeight / 2 - parentBorderTopWidth + this.clientHeight / 2;
|
||||
}
|
||||
|
||||
if ((overLeft || overRight) && centerIfNeeded) {
|
||||
parent.scrollLeft = this.offsetLeft - parent.offsetLeft - parent.clientWidth / 2 - parentBorderLeftWidth + this.clientWidth / 2;
|
||||
}
|
||||
|
||||
if ((overTop || overBottom || overLeft || overRight) && !centerIfNeeded) {
|
||||
this.scrollIntoView(alignWithTop);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import $ from '../dom';
|
||||
import * as _ from '../utils';
|
||||
import Flipper from '../flipper';
|
||||
import { BlockToolAPI } from '../block';
|
||||
import I18n from '../i18n';
|
||||
import { I18nInternalNS } from '../i18n/namespace-internal';
|
||||
import Shortcuts from '../utils/shortcuts';
|
||||
import Tooltip from '../utils/tooltip';
|
||||
import BlockTool from '../tools/block';
|
||||
import ToolsCollection from '../tools/collection';
|
||||
import { API } from '../../../types';
|
||||
import EventsDispatcher from '../utils/events';
|
||||
import Popover, { PopoverEvent } from '../utils/popover';
|
||||
|
||||
/**
|
||||
* @todo the first Tab on the Block — focus Plus Button, the second — focus Block Tunes Toggler, the third — focus next Block
|
||||
*/
|
||||
|
||||
/**
|
||||
* Event that can be triggered by the Toolbox
|
||||
|
@ -31,6 +31,11 @@ export enum ToolboxEvent {
|
|||
BlockAdded = 'toolbox-block-added',
|
||||
}
|
||||
|
||||
/**
|
||||
* Available i18n dict keys that should be passed to the constructor
|
||||
*/
|
||||
type toolboxTextLabelsKeys = 'filter' | 'nothingFound';
|
||||
|
||||
/**
|
||||
* Toolbox
|
||||
* This UI element contains list of Block Tools available to be inserted
|
||||
|
@ -45,7 +50,7 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
|
|||
* @returns {boolean}
|
||||
*/
|
||||
public get isEmpty(): boolean {
|
||||
return this.displayedToolsCount === 0;
|
||||
return this.toolsToBeDisplayed.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -60,21 +65,29 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
|
|||
*/
|
||||
private api: API;
|
||||
|
||||
/**
|
||||
* Popover instance. There is a util for vertical lists.
|
||||
*/
|
||||
private popover: Popover;
|
||||
|
||||
/**
|
||||
* List of Tools available. Some of them will be shown in the Toolbox
|
||||
*/
|
||||
private tools: ToolsCollection<BlockTool>;
|
||||
|
||||
/**
|
||||
* Text labels used in the Toolbox. Should be passed from the i18n module
|
||||
*/
|
||||
private i18nLabels: Record<toolboxTextLabelsKeys, string>;
|
||||
|
||||
/**
|
||||
* Current module HTML Elements
|
||||
*/
|
||||
private nodes: {
|
||||
toolbox: HTMLElement;
|
||||
buttons: HTMLElement[];
|
||||
} = {
|
||||
toolbox: null,
|
||||
buttons: [],
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* CSS styles
|
||||
|
@ -84,34 +97,10 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
|
|||
private static get CSS(): { [name: string]: string } {
|
||||
return {
|
||||
toolbox: 'ce-toolbox',
|
||||
toolboxButton: 'ce-toolbox__button',
|
||||
toolboxButtonActive: 'ce-toolbox__button--active',
|
||||
toolboxOpened: 'ce-toolbox--opened',
|
||||
|
||||
buttonTooltip: 'ce-toolbox-button-tooltip',
|
||||
buttonShortcut: 'ce-toolbox-button-tooltip__shortcut',
|
||||
toolboxOpenedTop: 'ce-toolbox--opened-top',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* How many tools displayed in Toolbox
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
private displayedToolsCount = 0;
|
||||
|
||||
/**
|
||||
* Instance of class that responses for leafing buttons by arrows/tab
|
||||
*
|
||||
* @type {Flipper|null}
|
||||
*/
|
||||
private flipper: Flipper = null;
|
||||
|
||||
/**
|
||||
* Tooltip utility Instance
|
||||
*/
|
||||
private tooltip: Tooltip;
|
||||
|
||||
/**
|
||||
* Id of listener added used to remove it on destroy()
|
||||
*/
|
||||
|
@ -124,67 +113,78 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
|
|||
* @param options.api - Editor API methods
|
||||
* @param options.tools - Tools available to check whether some of them should be displayed at the Toolbox or not
|
||||
*/
|
||||
constructor({ api, tools }) {
|
||||
constructor({ api, tools, i18nLabels }: {api: API; tools: ToolsCollection<BlockTool>; i18nLabels: Record<toolboxTextLabelsKeys, string>}) {
|
||||
super();
|
||||
|
||||
this.api = api;
|
||||
this.tools = tools;
|
||||
|
||||
this.tooltip = new Tooltip();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the Toolbox has the Flipper activated and the Flipper has selected button
|
||||
*/
|
||||
public get flipperHasFocus(): boolean {
|
||||
return this.flipper && this.flipper.currentItem !== null;
|
||||
this.i18nLabels = i18nLabels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the Toolbox
|
||||
*/
|
||||
public make(): Element {
|
||||
this.nodes.toolbox = $.make('div', Toolbox.CSS.toolbox);
|
||||
this.popover = new Popover({
|
||||
className: Toolbox.CSS.toolbox,
|
||||
searchable: true,
|
||||
filterLabel: this.i18nLabels.filter,
|
||||
nothingFoundLabel: this.i18nLabels.nothingFound,
|
||||
items: this.toolsToBeDisplayed.map(tool => {
|
||||
return {
|
||||
icon: tool.toolbox.icon,
|
||||
label: tool.toolbox.title,
|
||||
name: tool.name,
|
||||
onClick: (item): void => {
|
||||
this.toolButtonActivated(tool.name);
|
||||
},
|
||||
secondaryLabel: tool.shortcut ? _.beautifyShortcut(tool.shortcut) : '',
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
this.addTools();
|
||||
this.enableFlipper();
|
||||
this.popover.on(PopoverEvent.OverlayClicked, this.onOverlayClicked);
|
||||
|
||||
/**
|
||||
* Enable tools shortcuts
|
||||
*/
|
||||
this.enableShortcuts();
|
||||
|
||||
this.nodes.toolbox = this.popover.getElement();
|
||||
|
||||
return this.nodes.toolbox;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the Toolbox has the Flipper activated and the Flipper has selected button
|
||||
*/
|
||||
public hasFocus(): boolean {
|
||||
return this.popover.hasFocus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy Module
|
||||
*/
|
||||
public destroy(): void {
|
||||
super.destroy();
|
||||
|
||||
/**
|
||||
* Sometimes (in read-only mode) there is no Flipper
|
||||
*/
|
||||
if (this.flipper) {
|
||||
this.flipper.deactivate();
|
||||
this.flipper = null;
|
||||
}
|
||||
|
||||
if (this.nodes && this.nodes.toolbox) {
|
||||
this.nodes.toolbox.remove();
|
||||
this.nodes.toolbox = null;
|
||||
this.nodes.buttons = [];
|
||||
}
|
||||
|
||||
this.api.listeners.offById(this.clickListenerId);
|
||||
|
||||
this.removeAllShortcuts();
|
||||
this.tooltip.destroy();
|
||||
this.popover.off(PopoverEvent.OverlayClicked, this.onOverlayClicked);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toolbox Tool's button click handler
|
||||
*
|
||||
* @param {MouseEvent|KeyboardEvent} event - event that activates toolbox button
|
||||
* @param {string} toolName - button to activate
|
||||
* @param toolName - tool type to be activated
|
||||
*/
|
||||
public toolButtonActivate(event: MouseEvent|KeyboardEvent, toolName: string): void {
|
||||
public toolButtonActivated(toolName: string): void {
|
||||
this.insertNewBlock(toolName);
|
||||
}
|
||||
|
||||
|
@ -196,24 +196,28 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
|
|||
return;
|
||||
}
|
||||
|
||||
this.emit(ToolboxEvent.Opened);
|
||||
|
||||
this.nodes.toolbox.classList.add(Toolbox.CSS.toolboxOpened);
|
||||
/**
|
||||
* Open the popover above the button
|
||||
* if there is not enough available space below it
|
||||
*/
|
||||
if (!this.shouldOpenPopoverBottom) {
|
||||
this.nodes.toolbox.style.setProperty('--popover-height', this.popover.calculateHeight() + 'px');
|
||||
this.nodes.toolbox.classList.add(Toolbox.CSS.toolboxOpenedTop);
|
||||
}
|
||||
|
||||
this.popover.show();
|
||||
this.opened = true;
|
||||
this.flipper.activate();
|
||||
this.emit(ToolboxEvent.Opened);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close Toolbox
|
||||
*/
|
||||
public close(): void {
|
||||
this.emit(ToolboxEvent.Closed);
|
||||
|
||||
this.nodes.toolbox.classList.remove(Toolbox.CSS.toolboxOpened);
|
||||
|
||||
this.popover.hide();
|
||||
this.opened = false;
|
||||
this.flipper.deactivate();
|
||||
this.nodes.toolbox.classList.remove(Toolbox.CSS.toolboxOpenedTop);
|
||||
this.emit(ToolboxEvent.Closed);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -228,106 +232,65 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Iterates available tools and appends them to the Toolbox
|
||||
* Checks if there popover should be opened downwards.
|
||||
* It happens in case there is enough space below or not enough space above
|
||||
*/
|
||||
private addTools(): void {
|
||||
Array
|
||||
private get shouldOpenPopoverBottom(): boolean {
|
||||
const toolboxRect = this.nodes.toolbox.getBoundingClientRect();
|
||||
const editorElementRect = this.api.ui.nodes.redactor.getBoundingClientRect();
|
||||
const popoverHeight = this.popover.calculateHeight();
|
||||
const popoverPotentialBottomEdge = toolboxRect.top + popoverHeight;
|
||||
const popoverPotentialTopEdge = toolboxRect.top - popoverHeight;
|
||||
const bottomEdgeForComparison = Math.min(window.innerHeight, editorElementRect.bottom);
|
||||
|
||||
return popoverPotentialTopEdge < editorElementRect.top || popoverPotentialBottomEdge <= bottomEdgeForComparison;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles overlay click
|
||||
*/
|
||||
private onOverlayClicked = (): void => {
|
||||
this.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of tools that enables the Toolbox (by specifying the 'toolbox' getter)
|
||||
*/
|
||||
@_.cacheable
|
||||
private get toolsToBeDisplayed(): BlockTool[] {
|
||||
return Array
|
||||
.from(this.tools.values())
|
||||
.forEach((tool) => this.addTool(tool));
|
||||
.filter(tool => {
|
||||
const toolToolboxSettings = tool.toolbox;
|
||||
|
||||
/**
|
||||
* Skip tools that don't pass 'toolbox' property
|
||||
*/
|
||||
if (!toolToolboxSettings) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (toolToolboxSettings && !toolToolboxSettings.icon) {
|
||||
_.log('Toolbar icon is missed. Tool %o skipped', 'warn', tool.name);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Append Tool to the Toolbox
|
||||
*
|
||||
* @param {BlockToolConstructable} tool - BlockTool object
|
||||
* Iterate all tools and enable theirs shortcuts if specified
|
||||
*/
|
||||
private addTool(tool: BlockTool): void {
|
||||
const toolToolboxSettings = tool.toolbox;
|
||||
private enableShortcuts(): void {
|
||||
this.toolsToBeDisplayed.forEach((tool: BlockTool) => {
|
||||
const shortcut = tool.shortcut;
|
||||
|
||||
/**
|
||||
* Skip tools that don't pass 'toolbox' property
|
||||
*/
|
||||
if (!toolToolboxSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (toolToolboxSettings && !toolToolboxSettings.icon) {
|
||||
_.log('Toolbar icon is missed. Tool %o skipped', 'warn', tool.name);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Add checkup for the render method
|
||||
*/
|
||||
// if (typeof tool.render !== 'function') {
|
||||
// _.log('render method missed. Tool %o skipped', 'warn', tool);
|
||||
// return;
|
||||
// }
|
||||
|
||||
const button = $.make('li', [ Toolbox.CSS.toolboxButton ]);
|
||||
|
||||
button.dataset.tool = tool.name;
|
||||
button.innerHTML = toolToolboxSettings.icon;
|
||||
|
||||
$.append(this.nodes.toolbox, button);
|
||||
|
||||
this.nodes.toolbox.appendChild(button);
|
||||
this.nodes.buttons.push(button);
|
||||
|
||||
/**
|
||||
* Add click listener
|
||||
*/
|
||||
this.clickListenerId = this.api.listeners.on(button, 'click', (event: KeyboardEvent|MouseEvent) => {
|
||||
this.toolButtonActivate(event, tool.name);
|
||||
if (shortcut) {
|
||||
this.enableShortcutForTool(tool.name, shortcut);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Add listeners to show/hide toolbox tooltip
|
||||
*/
|
||||
const tooltipContent = this.drawTooltip(tool);
|
||||
|
||||
this.tooltip.onHover(button, tooltipContent, {
|
||||
placement: 'bottom',
|
||||
hidingDelay: 200,
|
||||
});
|
||||
|
||||
const shortcut = tool.shortcut;
|
||||
|
||||
if (shortcut) {
|
||||
this.enableShortcut(tool.name, shortcut);
|
||||
}
|
||||
|
||||
/** Increment Tools count */
|
||||
this.displayedToolsCount++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw tooltip for toolbox tools
|
||||
*
|
||||
* @param tool - BlockTool object
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
private drawTooltip(tool: BlockTool): HTMLElement {
|
||||
const toolboxSettings = tool.toolbox || {};
|
||||
const name = I18n.t(I18nInternalNS.toolNames, toolboxSettings.title || tool.name);
|
||||
|
||||
let shortcut = tool.shortcut;
|
||||
|
||||
const tooltip = $.make('div', Toolbox.CSS.buttonTooltip);
|
||||
const hint = document.createTextNode(_.capitalize(name));
|
||||
|
||||
tooltip.appendChild(hint);
|
||||
|
||||
if (shortcut) {
|
||||
shortcut = _.beautifyShortcut(shortcut);
|
||||
|
||||
tooltip.appendChild($.make('div', Toolbox.CSS.buttonShortcut, {
|
||||
textContent: shortcut,
|
||||
}));
|
||||
}
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -336,7 +299,7 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
|
|||
* @param {string} toolName - Tool name
|
||||
* @param {string} shortcut - shortcut according to the ShortcutData Module format
|
||||
*/
|
||||
private enableShortcut(toolName: string, shortcut: string): void {
|
||||
private enableShortcutForTool(toolName: string, shortcut: string): void {
|
||||
Shortcuts.add({
|
||||
name: shortcut,
|
||||
on: this.api.ui.nodes.redactor,
|
||||
|
@ -352,26 +315,12 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
|
|||
* Fired when the Read-Only mode is activated
|
||||
*/
|
||||
private removeAllShortcuts(): void {
|
||||
Array
|
||||
.from(this.tools.values())
|
||||
.forEach((tool) => {
|
||||
const shortcut = tool.shortcut;
|
||||
this.toolsToBeDisplayed.forEach((tool: BlockTool) => {
|
||||
const shortcut = tool.shortcut;
|
||||
|
||||
if (shortcut) {
|
||||
Shortcuts.remove(this.api.ui.nodes.redactor, shortcut);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates Flipper instance to be able to leaf tools
|
||||
*/
|
||||
private enableFlipper(): void {
|
||||
const tools = Array.from(this.nodes.toolbox.childNodes) as HTMLElement[];
|
||||
|
||||
this.flipper = new Flipper({
|
||||
items: tools,
|
||||
focusedItemClass: Toolbox.CSS.toolboxButtonActive,
|
||||
if (shortcut) {
|
||||
Shortcuts.remove(this.api.ui.nodes.redactor, shortcut);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -762,3 +762,10 @@ export function cacheable<Target, Value, Arguments extends unknown[] = unknown[]
|
|||
|
||||
return descriptor;
|
||||
};
|
||||
|
||||
/**
|
||||
* True if screen has mobile size
|
||||
*/
|
||||
export function isMobileScreen(): boolean {
|
||||
return window.matchMedia('(max-width: 650px)').matches;
|
||||
}
|
||||
|
|
391
src/components/utils/popover.ts
Normal file
391
src/components/utils/popover.ts
Normal file
|
@ -0,0 +1,391 @@
|
|||
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';
|
||||
|
||||
/**
|
||||
* Describe parameters for rendering the single item of Popover
|
||||
*/
|
||||
export interface PopoverItem {
|
||||
/**
|
||||
* Item icon to be appeared near a title
|
||||
*/
|
||||
icon: string;
|
||||
|
||||
/**
|
||||
* Displayed text
|
||||
*/
|
||||
label: string;
|
||||
|
||||
/**
|
||||
* Item name
|
||||
* Used in data attributes needed for cypress tests
|
||||
*/
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* Additional displayed text
|
||||
*/
|
||||
secondaryLabel?: string;
|
||||
|
||||
/**
|
||||
* Itm click handler
|
||||
*
|
||||
* @param item - clicked item
|
||||
*/
|
||||
onClick: (item: PopoverItem) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event that can be triggered by the Popover
|
||||
*/
|
||||
export enum PopoverEvent {
|
||||
/**
|
||||
* When popover overlay is clicked
|
||||
*/
|
||||
OverlayClicked = 'overlay-clicked',
|
||||
}
|
||||
|
||||
/**
|
||||
* Popover is the UI element for displaying vertical lists
|
||||
*/
|
||||
export default class Popover extends EventsDispatcher<PopoverEvent> {
|
||||
/**
|
||||
* Items list to be displayed
|
||||
*/
|
||||
private readonly items: PopoverItem[];
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Flipper - module for keyboard iteration between elements
|
||||
*/
|
||||
private flipper: Flipper;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private static get CSS(): {
|
||||
popover: string;
|
||||
popoverOpened: string;
|
||||
itemsWrapper: string;
|
||||
item: string;
|
||||
itemHidden: string;
|
||||
itemFocused: string;
|
||||
itemLabel: string;
|
||||
itemIcon: string;
|
||||
itemSecondaryLabel: string;
|
||||
noFoundMessage: string;
|
||||
noFoundMessageShown: string;
|
||||
popoverOverlay: string;
|
||||
popoverOverlayHidden: string;
|
||||
documentScrollLocked: 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',
|
||||
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',
|
||||
documentScrollLocked: 'ce-scroll-locked',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
constructor({ items, className, searchable, filterLabel, nothingFoundLabel }: {
|
||||
items: PopoverItem[];
|
||||
className?: string;
|
||||
searchable?: boolean;
|
||||
filterLabel: string;
|
||||
nothingFoundLabel: string;
|
||||
}) {
|
||||
super();
|
||||
this.items = items;
|
||||
this.className = className || '';
|
||||
this.searchable = searchable;
|
||||
this.listeners = new Listeners();
|
||||
|
||||
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 {
|
||||
this.nodes.popover.classList.add(Popover.CSS.popoverOpened);
|
||||
this.nodes.overlay.classList.remove(Popover.CSS.popoverOverlayHidden);
|
||||
this.flipper.activate();
|
||||
|
||||
if (this.searchable) {
|
||||
window.requestAnimationFrame(() => {
|
||||
this.search.focus();
|
||||
});
|
||||
}
|
||||
|
||||
if (isMobileScreen()) {
|
||||
document.documentElement.classList.add(Popover.CSS.documentScrollLocked);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the Popover
|
||||
*/
|
||||
public hide(): void {
|
||||
this.search.clear();
|
||||
this.nodes.popover.classList.remove(Popover.CSS.popoverOpened);
|
||||
this.nodes.overlay.classList.add(Popover.CSS.popoverOverlayHidden);
|
||||
this.flipper.deactivate();
|
||||
|
||||
if (isMobileScreen()) {
|
||||
document.documentElement.classList.remove(Popover.CSS.documentScrollLocked);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears memory
|
||||
*/
|
||||
public destroy(): void {
|
||||
this.listeners.removeAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
public 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);
|
||||
}
|
||||
|
||||
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: KeyboardEvent|MouseEvent) => {
|
||||
const clickedItem = (event.target as HTMLElement).closest(`.${Popover.CSS.item}`) as HTMLElement;
|
||||
|
||||
if (clickedItem) {
|
||||
this.itemClicked(clickedItem);
|
||||
}
|
||||
});
|
||||
|
||||
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 itemsVisible = [];
|
||||
|
||||
this.items.forEach((item, index) => {
|
||||
const itemElement = this.nodes.items.children[index];
|
||||
|
||||
if (filteredItems.includes(item)) {
|
||||
itemsVisible.push(itemElement);
|
||||
itemElement.classList.remove(Popover.CSS.itemHidden);
|
||||
} else {
|
||||
itemElement.classList.add(Popover.CSS.itemHidden);
|
||||
}
|
||||
});
|
||||
|
||||
this.nodes.nothingFound.classList.toggle(Popover.CSS.noFoundMessageShown, itemsVisible.length === 0);
|
||||
|
||||
/**
|
||||
* Update flipper items with only visible
|
||||
*/
|
||||
this.flipper.deactivate();
|
||||
this.flipper.activate(itemsVisible);
|
||||
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);
|
||||
|
||||
el.dataset.itemName = item.name;
|
||||
const label = Dom.make('div', Popover.CSS.itemLabel, {
|
||||
innerHTML: item.label,
|
||||
});
|
||||
|
||||
if (item.icon) {
|
||||
el.appendChild(Dom.make('div', Popover.CSS.itemIcon, {
|
||||
innerHTML: item.icon,
|
||||
}));
|
||||
}
|
||||
|
||||
el.appendChild(label);
|
||||
|
||||
if (item.secondaryLabel) {
|
||||
el.appendChild(Dom.make('div', Popover.CSS.itemSecondaryLabel, {
|
||||
textContent: item.secondaryLabel,
|
||||
}));
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
/**
|
||||
* Item click handler
|
||||
*
|
||||
* @param itemEl - clicked item
|
||||
*/
|
||||
private itemClicked(itemEl: HTMLElement): void {
|
||||
const allItems = this.nodes.wrapper.querySelectorAll(`.${Popover.CSS.item}`);
|
||||
const itemIndex = Array.from(allItems).indexOf(itemEl);
|
||||
const clickedItem = this.items[itemIndex];
|
||||
|
||||
clickedItem.onClick(clickedItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates Flipper instance to be able to leaf tools
|
||||
*/
|
||||
private enableFlipper(): void {
|
||||
const tools = Array.from(this.nodes.wrapper.querySelectorAll(`.${Popover.CSS.item}`)) as HTMLElement[];
|
||||
|
||||
this.flipper = new Flipper({
|
||||
items: tools,
|
||||
focusedItemClass: Popover.CSS.itemFocused,
|
||||
allowedKeys: [
|
||||
keyCodes.TAB,
|
||||
keyCodes.UP,
|
||||
keyCodes.DOWN,
|
||||
keyCodes.ENTER,
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
152
src/components/utils/search-input.ts
Normal file
152
src/components/utils/search-input.ts
Normal file
|
@ -0,0 +1,152 @@
|
|||
import Dom from '../dom';
|
||||
import Listeners from './listeners';
|
||||
|
||||
/**
|
||||
* Item that could be searched
|
||||
*/
|
||||
interface SearchableItem {
|
||||
label: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides search input element and search logic
|
||||
*/
|
||||
export default class SearchInput {
|
||||
/**
|
||||
* Input wrapper element
|
||||
*/
|
||||
private wrapper: HTMLElement;
|
||||
|
||||
/**
|
||||
* Editable input itself
|
||||
*/
|
||||
private input: HTMLInputElement;
|
||||
|
||||
/**
|
||||
* The instance of the Listeners util
|
||||
*/
|
||||
private listeners: Listeners;
|
||||
|
||||
/**
|
||||
* Items for local search
|
||||
*/
|
||||
private items: SearchableItem[];
|
||||
|
||||
/**
|
||||
* Current search query
|
||||
*/
|
||||
private searchQuery: string;
|
||||
|
||||
/**
|
||||
* Externally passed callback for the search
|
||||
*/
|
||||
private readonly onSearch: (items: SearchableItem[]) => void;
|
||||
|
||||
/**
|
||||
* Styles
|
||||
*/
|
||||
private static get CSS(): {
|
||||
input: string;
|
||||
icon: string;
|
||||
wrapper: string;
|
||||
} {
|
||||
return {
|
||||
wrapper: 'cdx-search-field',
|
||||
icon: 'cdx-search-field__icon',
|
||||
input: 'cdx-search-field__input',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param options - available config
|
||||
* @param options.items - searchable items list
|
||||
* @param options.onSearch - search callback
|
||||
* @param options.placeholder - input placeholder
|
||||
*/
|
||||
constructor({ items, onSearch, placeholder }: {
|
||||
items: SearchableItem[];
|
||||
onSearch: (items: SearchableItem[]) => void;
|
||||
placeholder: string;
|
||||
}) {
|
||||
this.listeners = new Listeners();
|
||||
this.items = items;
|
||||
this.onSearch = onSearch;
|
||||
|
||||
this.render(placeholder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns search field element
|
||||
*/
|
||||
public getElement(): HTMLElement {
|
||||
return this.wrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets focus to the input
|
||||
*/
|
||||
public focus(): void {
|
||||
this.input.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears search query and results
|
||||
*/
|
||||
public clear(): void {
|
||||
this.input.value = '';
|
||||
this.searchQuery = '';
|
||||
this.onSearch(this.foundItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears memory
|
||||
*/
|
||||
public destroy(): void {
|
||||
this.listeners.removeAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the search field
|
||||
*
|
||||
* @param placeholder - input placeholder
|
||||
*/
|
||||
private render(placeholder: string): void {
|
||||
this.wrapper = Dom.make('div', SearchInput.CSS.wrapper);
|
||||
|
||||
const iconWrapper = Dom.make('div', SearchInput.CSS.icon);
|
||||
const icon = Dom.svg('search', 16, 16);
|
||||
|
||||
this.input = Dom.make('input', SearchInput.CSS.input, {
|
||||
placeholder,
|
||||
}) as HTMLInputElement;
|
||||
|
||||
iconWrapper.appendChild(icon);
|
||||
this.wrapper.appendChild(iconWrapper);
|
||||
this.wrapper.appendChild(this.input);
|
||||
|
||||
this.listeners.on(this.input, 'input', () => {
|
||||
this.searchQuery = this.input.value;
|
||||
|
||||
this.onSearch(this.foundItems);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of found items for the current search query
|
||||
*/
|
||||
private get foundItems(): SearchableItem[] {
|
||||
return this.items.filter(item => this.checkItem(item));
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains logic for checking whether passed item conforms the search query
|
||||
*
|
||||
* @param item - item to be checked
|
||||
*/
|
||||
private checkItem(item: SearchableItem): boolean {
|
||||
const text = item.label.toLowerCase();
|
||||
const query = this.searchQuery.toLowerCase();
|
||||
|
||||
return text.includes(query);
|
||||
}
|
||||
}
|
|
@ -117,3 +117,20 @@
|
|||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes panelShowingMobile {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(14px) scale(0.98);
|
||||
}
|
||||
|
||||
70% {
|
||||
opacity: 1;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
to {
|
||||
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,17 @@
|
|||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.ce-block {
|
||||
animation: fade-in 300ms ease;
|
||||
animation-fill-mode: initial;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
|
46
src/styles/input.css
Normal file
46
src/styles/input.css
Normal file
|
@ -0,0 +1,46 @@
|
|||
.cdx-search-field {
|
||||
--icon-margin-right: 10px;
|
||||
|
||||
background: rgba(232,232,235,0.49);
|
||||
border: 1px solid rgba(226,226,229,0.20);
|
||||
border-radius: 6px;
|
||||
padding: 2px;
|
||||
display: grid;
|
||||
grid-template-columns: auto auto 1fr;
|
||||
grid-template-rows: auto;
|
||||
|
||||
&__icon {
|
||||
width: var(--toolbox-buttons-size);
|
||||
height: var(--toolbox-buttons-size);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: var(--icon-margin-right);
|
||||
|
||||
.icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: var(--grayText);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&__input {
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 22px;
|
||||
min-width: calc(100% - var(--toolbox-buttons-size) - var(--icon-margin-right));
|
||||
|
||||
&::placeholder {
|
||||
color: var(--grayText);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,3 +10,5 @@
|
|||
@import './export.css';
|
||||
@import './stub.css';
|
||||
@import './rtl.css';
|
||||
@import './popover.css';
|
||||
@import './input.css';
|
||||
|
|
142
src/styles/popover.css
Normal file
142
src/styles/popover.css
Normal file
|
@ -0,0 +1,142 @@
|
|||
.ce-popover {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
will-change: opacity, transform;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 6px;
|
||||
min-width: 200px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
max-height: 0;
|
||||
|
||||
@apply --overlay-pane;
|
||||
|
||||
z-index: 4;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
&--opened {
|
||||
opacity: 1;
|
||||
max-height: 270px;
|
||||
animation: panelShowing 100ms ease;
|
||||
|
||||
@media (--mobile) {
|
||||
animation: panelShowingMobile 250ms ease;
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 7px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
box-sizing: border-box;
|
||||
box-shadow: inset 0 0 2px 2px var(--bg-light);
|
||||
border: 3px solid transparent;
|
||||
border-left-width: 0px;
|
||||
border-top-width: 4px;
|
||||
border-bottom-width: 4px;
|
||||
}
|
||||
|
||||
@media (--mobile) {
|
||||
position: fixed;
|
||||
max-width: none;
|
||||
min-width: auto;
|
||||
left: 5px;
|
||||
right: 5px;
|
||||
bottom: calc(5px + env(safe-area-inset-bottom));
|
||||
top: auto;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
&__items {
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
|
||||
@media (--not-mobile) {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&__item {
|
||||
@apply --popover-button;
|
||||
|
||||
&--focused {
|
||||
@apply --button-focused;
|
||||
}
|
||||
|
||||
&--hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
@apply --tool-icon;
|
||||
}
|
||||
|
||||
&-label {
|
||||
&::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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__no-found {
|
||||
@apply --popover-button;
|
||||
|
||||
color: var(--grayText);
|
||||
display: none;
|
||||
cursor: default;
|
||||
|
||||
&--shown {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
.ce-settings {
|
||||
@apply --overlay-pane;
|
||||
right: -1px;
|
||||
top: 30px;
|
||||
top: var(--toolbar-buttons-size);
|
||||
left: 0;
|
||||
min-width: 114px;
|
||||
box-sizing: content-box;
|
||||
|
||||
|
|
|
@ -4,34 +4,18 @@
|
|||
right: 0;
|
||||
top: 0;
|
||||
transition: opacity 100ms ease;
|
||||
will-change: opacity, transform;
|
||||
display: none;
|
||||
will-change: opacity, top;
|
||||
|
||||
@media (--mobile) {
|
||||
@apply --overlay-pane;
|
||||
padding: 3px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
display: none;
|
||||
|
||||
&--opened {
|
||||
display: block;
|
||||
|
||||
@media (--mobile){
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
max-width: var(--content-width);
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
|
||||
@media (--mobile){
|
||||
display: flex;
|
||||
align-content: center;
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__plus {
|
||||
|
@ -44,14 +28,9 @@
|
|||
margin-top: 5px;
|
||||
}
|
||||
|
||||
&--hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (--mobile){
|
||||
display: inline-flex !important;
|
||||
@apply --overlay-pane;
|
||||
position: static;
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,37 +43,37 @@
|
|||
right: 100%;
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
|
||||
@media (--mobile){
|
||||
position: absolute;
|
||||
right: auto;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
padding-right: 5px;
|
||||
|
||||
&--opened {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&-buttons {
|
||||
text-align: right;
|
||||
@media (--mobile){
|
||||
right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__settings-btn {
|
||||
@apply --toolbox-button;
|
||||
width: 18px;
|
||||
margin: 0 5px;
|
||||
|
||||
margin-left: 5px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
.codex-editor--toolbox-opened .ce-toolbar__actions {
|
||||
display: none;
|
||||
@media (--not-mobile){
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
&--hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (--mobile){
|
||||
@apply --overlay-pane;
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,44 +1,24 @@
|
|||
.ce-toolbox {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
transition: opacity 100ms ease;
|
||||
will-change: opacity;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
--gap: 8px;
|
||||
|
||||
@media (--mobile){
|
||||
position: static;
|
||||
transform: none !important;
|
||||
align-items: center;
|
||||
overflow-x: auto;
|
||||
}
|
||||
@media (--not-mobile){
|
||||
position: absolute;
|
||||
top: calc(var(--toolbox-buttons-size) + var(--gap));
|
||||
left: 0;
|
||||
|
||||
&--opened {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
&__button {
|
||||
@apply --toolbox-button;
|
||||
flex-shrink: 0;
|
||||
margin-left: 5px;
|
||||
&--opened-top {
|
||||
top: calc(-1 * (var(--gap) + var(--popover-height)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ce-toolbox-button-tooltip {
|
||||
&__shortcut {
|
||||
opacity: 0.6;
|
||||
word-spacing: -3px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Styles for Narrow mode
|
||||
*/
|
||||
.codex-editor--narrow .ce-toolbox {
|
||||
@media (--not-mobile) {
|
||||
background: #fff;
|
||||
z-index: 2;
|
||||
@media (--not-mobile){
|
||||
left: auto;
|
||||
right: 0;
|
||||
|
||||
.ce-popover {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -127,3 +127,12 @@
|
|||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.ce-scroll-locked, .ce-scroll-locked > body {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
/**
|
||||
* Mobile Safari fix
|
||||
*/
|
||||
position: relative;
|
||||
}
|
|
@ -1,5 +1,9 @@
|
|||
/**
|
||||
* Updating values in media queries should also include changes in utils.ts@isMobile
|
||||
*/
|
||||
@custom-media --mobile (width <= 650px);
|
||||
@custom-media --not-mobile (width >= 651px);
|
||||
@custom-media --can-hover (hover: hover);
|
||||
|
||||
:root {
|
||||
/**
|
||||
|
@ -21,7 +25,7 @@
|
|||
/**
|
||||
* Gray icons hover
|
||||
*/
|
||||
--color-dark: #1D202B;
|
||||
--color-dark: #1D202B;
|
||||
|
||||
/**
|
||||
* Blue icons
|
||||
|
@ -95,6 +99,11 @@
|
|||
}
|
||||
};
|
||||
|
||||
--button-focused: {
|
||||
box-shadow: inset 0 0 0px 1px rgba(7, 161, 227, 0.08);
|
||||
background: rgba(34, 186, 255, 0.08) !important;
|
||||
};
|
||||
|
||||
/**
|
||||
* Styles for Toolbox Buttons and Plus Button
|
||||
*/
|
||||
|
@ -103,22 +112,25 @@
|
|||
cursor: pointer;
|
||||
width: var(--toolbox-buttons-size);
|
||||
height: var(--toolbox-buttons-size);
|
||||
border-radius: 3px;
|
||||
border-radius: 7px;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
|
||||
@media (--mobile){
|
||||
width: var(--toolbox-buttons-size--mobile);
|
||||
height: var(--toolbox-buttons-size--mobile);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&--active {
|
||||
background-color: var(--bg-light);
|
||||
@media (--can-hover) {
|
||||
&:hover {
|
||||
background-color: var(--bg-light);
|
||||
}
|
||||
}
|
||||
|
||||
&--active{
|
||||
&--active {
|
||||
background-color: var(--bg-light);
|
||||
animation: bounceIn 0.75s 1;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
@ -132,9 +144,9 @@
|
|||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
line-height: 34px;
|
||||
width: var(--toolbar-buttons-size);
|
||||
height: var(--toolbar-buttons-size);
|
||||
line-height: var(--toolbar-buttons-size);
|
||||
padding: 0 !important;
|
||||
text-align: center;
|
||||
border-radius: 3px;
|
||||
|
@ -155,8 +167,7 @@
|
|||
}
|
||||
|
||||
&--focused {
|
||||
box-shadow: inset 0 0 0px 1px rgba(7, 161, 227, 0.08);
|
||||
background: rgba(34, 186, 255, 0.08) !important;
|
||||
@apply --button-focused;
|
||||
|
||||
&-animated {
|
||||
animation-name: buttonClicked;
|
||||
|
@ -164,5 +175,66 @@
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 (--can-hover) {
|
||||
&:hover {
|
||||
background-color: var(--bg-light);
|
||||
}
|
||||
}
|
||||
|
||||
@media (--mobile) {
|
||||
font-size: 16px;
|
||||
padding: 4px;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Tool icon with border
|
||||
*/
|
||||
--tool-icon: {
|
||||
display: inline-flex;
|
||||
width: var(--toolbox-buttons-size);
|
||||
height: var(--toolbox-buttons-size);
|
||||
border: 1px solid var(--color-gray-border);
|
||||
border-radius: 5px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
margin-right: 10px;
|
||||
|
||||
@media (--mobile) {
|
||||
width: var(--toolbox-buttons-size--mobile);
|
||||
height: var(--toolbox-buttons-size--mobile);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ describe.only('Block ids', () => {
|
|||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('li.ce-toolbox__button[data-tool=header]')
|
||||
.get('div.ce-popover__item[data-item-name=header]')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
|
|
|
@ -131,7 +131,7 @@ describe('onChange callback', () => {
|
|||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('li.ce-toolbox__button[data-tool=delimiter]')
|
||||
.get('div.ce-popover__item[data-item-name=delimiter]')
|
||||
.click();
|
||||
|
||||
cy.get('@onChange').should('be.calledThrice');
|
||||
|
@ -178,7 +178,7 @@ describe('onChange callback', () => {
|
|||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('li.ce-toolbox__button[data-tool=header]')
|
||||
.get('div.ce-popover__item[data-item-name=header]')
|
||||
.click();
|
||||
|
||||
cy.get('@onChange').should('be.calledTwice');
|
||||
|
@ -245,6 +245,14 @@ describe('onChange callback', () => {
|
|||
it('should fire onChange callback when block is removed', () => {
|
||||
createEditor();
|
||||
|
||||
/**
|
||||
* The only block does not have Tune menu, so need to create at least 2 blocks to test deleting
|
||||
*/
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
.type('some text');
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click();
|
||||
|
|
Loading…
Reference in a new issue