editor.js/src/components/modules/ui.ts
George Berezhnoy 79f8660637 Version 2.13 (#719)
* Do not start multi-block selection on UI elements (#662)

* Do not start multi-block selection on UI elements

* Do not prevent mousedown event on inline toolbar actions

* Remove log

* Add comment

* Add link to issue

closes #646

* Fix loss of pointer (#666)

* Fix loss of pointer when click is outside of the editor but selection is inside

* Remove log

* Update shortcuts module (#685)

* Fixed possible grammatical typo (#681)

Thanks

* Update shortcuts module

* update changelog

* update

* Remove margin top for inline-link icon (#690)

* Remove margin top for inline-link icon

resolves #674

* Update CHANGELOG.md

* Remove unused style

* Pull fresh tools

* Remove changelog contents from readme (#700)

* #665 API to open and close inline-toolbar (#711)

* API to open and close inline-toolbar

* Fixed documentation

* renamed inline -> inline-toolbar

* removed dist

* reset editor.js

* added editor.js bundle

* Fixed build error

* Null checks on toolbar/inline@open

* updated bundle

* Improve some comments

* Updatd api.md CHANGELOG.md

* Change feature to new instead of improvement

* Allow holderId work with ref on dom element (#710)

* done

* update types

* attempt to simplify code

* remove useless helper

* revert holderId logic and add holder property

* Apply suggestions from code review

Co-Authored-By: dimensi <eddimensi@gmail.com>

* update holder type on string | HTMLElement

* fix typo

* add deprecated notice and fix typos

* fix wrong compare

* fix comments

* swap console.log on _.log

* update types for editor config

* update examples

* update docs

* update build

* Activating Open Collective (#736)

Hi, I'm making updates for Open Collective. Either you or a supporter signed this repo up for Open Collective. This pull request adds backers and sponsors from your Open Collective https://opencollective.com/editorjs❤️

It adds two badges at the top to show the latest number of backers and sponsors. It also adds placeholders so that the avatar/logo of new backers/sponsors can automatically be shown without having to update your README.md. [more info](https://github.com/opencollective/opencollective/wiki/Github-banner). See how it looks on this [repo](https://github.com/apex/apex#backers).

You can also add a postinstall script to let people know after npm|yarn install that you are welcoming donations (optional). [More info](https://github.com/OpenCollective/opencollective-cli)
You can also add a "Donate" button to your website and automatically show your backers and sponsors there with our widgets. Have a look here: https://opencollective.com/widgets

P.S: As with any pull request, feel free to comment or suggest changes. The only thing "required" are the placeholders on the README because we believe it's important to acknowledge the people in your community that are contributing (financially or with code!).

Thank you for your great contribution to the open source community. You are awesome! 🙌
And welcome to the open collective community! 😊

Come chat with us in the #opensource channel on https://slack.opencollective.com - great place to ask questions and share best practices with other open source sustainers!

* Do not install editor.js as dev-dependency (#731)

Resolves #730

* Move codex-notifier to dependencies for typescript declarations (#728)

* Close inline toolbar after creating new link by pressing ENTER (#722)

* Method to clear current selection and close inline toolbar

* clearSelection with optional collapsed range

* refactored selection.ts

* removed experimental function

* Update src/components/selection.ts

Co-Authored-By: tanmayv <12tanmayvijay@gmail.com>

* update version, add changelog

* Link Logo Image to homepage (#738)

* Update README.md (#744)

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

Co-Authored-By: neSpecc <specc.dev@gmail.com>

* Config minHeight option that allows to customize bottom zone (#745)

* issue-739: allow Block's editable element selection (#747)

* issue-739: allow Block's input selection

* little improvements

* update Changelog and cache inputs

* small fix

* delete map file

* fix inputs.count condition

* Fix typo in example paragraph (#749)

* Fix typo

* Update example-dev.html

* minor release
2019-04-29 15:52:54 +03:00

476 lines
12 KiB
TypeScript

/**
* Prebuilded sprite of SVG icons
*/
import sprite from '../../../dist/sprite.svg';
/**
* Module UI
*
* @type {UI}
*/
import Module from '../__module';
import $ from '../dom';
import _ from '../utils';
import Selection from '../selection';
/**
* @class
*
* @classdesc Makes Editor.js UI:
* <codex-editor>
* <ce-redactor />
* <ce-toolbar />
* <ce-inline-toolbar />
* </codex-editor>
*
* @typedef {UI} UI
* @property {EditorConfig} config - editor configuration {@link EditorJS#configuration}
* @property {Object} Editor - available editor modules {@link EditorJS#moduleInstances}
* @property {Object} nodes -
* @property {Element} nodes.holder - element where we need to append redactor
* @property {Element} nodes.wrapper - <codex-editor>
* @property {Element} nodes.redactor - <ce-redactor>
*/
export default class UI extends Module {
/**
* Editor.js UI CSS class names
* @return {{editorWrapper: string, editorZone: string}}
*/
public get CSS(): {
editorWrapper: string, editorWrapperNarrow: string, editorZone: string, editorZoneHidden: string,
editorLoader: string, editorEmpty: string,
} {
return {
editorWrapper : 'codex-editor',
editorWrapperNarrow : 'codex-editor--narrow',
editorZone : 'codex-editor__redactor',
editorZoneHidden : 'codex-editor__redactor--hidden',
editorLoader : 'codex-editor__loader',
editorEmpty : 'codex-editor--empty',
};
}
/**
* Width of center column of Editor
* @type {number}
*/
public contentWidth: number = 650;
/**
* HTML Elements used for UI
*/
public nodes: { [key: string]: HTMLElement } = {
holder: null,
wrapper: null,
redactor: null,
};
/**
* Adds loader to editor while content is not ready
*/
public addLoader(): void {
this.nodes.loader = $.make('div', this.CSS.editorLoader);
this.nodes.wrapper.prepend(this.nodes.loader);
this.nodes.redactor.classList.add(this.CSS.editorZoneHidden);
}
/**
* Removes loader when content has loaded
*/
public removeLoader(): void {
this.nodes.loader.remove();
this.nodes.redactor.classList.remove(this.CSS.editorZoneHidden);
}
/**
* Making main interface
*/
public async prepare(): Promise<void> {
await this.make();
this.addLoader();
/**
* Append SVG sprite
*/
await this.appendSVGSprite();
/**
* Make toolbar
*/
await this.Editor.Toolbar.make();
/**
* Make the Inline toolbar
*/
await this.Editor.InlineToolbar.make();
/**
* Load and append CSS
*/
await this.loadStyles();
/**
* Bind events for the UI elements
*/
await this.bindEvents();
}
/**
* Clean editor`s UI
*/
public destroy(): void {
this.nodes.holder.innerHTML = '';
}
/**
* Makes Editor.js interface
* @return {Promise<void>}
*/
private async make(): Promise<void> {
/**
* Element where we need to append Editor.js
* @type {Element}
*/
this.nodes.holder = $.getHolder(this.config.holder);
/**
* Create and save main UI elements
*/
this.nodes.wrapper = $.make('div', this.CSS.editorWrapper);
this.nodes.redactor = $.make('div', this.CSS.editorZone);
/**
* If Editor has injected into the narrow container, enable Narrow Mode
*/
if (this.nodes.holder.offsetWidth < this.contentWidth) {
this.nodes.wrapper.classList.add(this.CSS.editorWrapperNarrow);
}
/**
* Set customizable bottom zone height
*/
this.nodes.redactor.style.paddingBottom = this.config.minHeight + 'px';
this.nodes.wrapper.appendChild(this.nodes.redactor);
this.nodes.holder.appendChild(this.nodes.wrapper);
}
/**
* Appends CSS
*/
private loadStyles(): void {
/**
* Load CSS
*/
const styles = require('../../styles/main.css');
/**
* Make tag
*/
const tag = $.make('style', null, {
textContent: styles.toString(),
});
/**
* Append styles at the top of HEAD tag
*/
$.prepend(document.head, tag);
}
/**
* Bind events on the Editor.js interface
*/
private bindEvents(): void {
this.Editor.Listeners.on(
this.nodes.redactor,
'click',
(event) => this.redactorClicked(event as MouseEvent),
false,
);
this.Editor.Listeners.on(document, 'keydown', (event) => this.documentKeydown(event as KeyboardEvent), true);
this.Editor.Listeners.on(document, 'click', (event) => this.documentClicked(event as MouseEvent), true);
}
/**
* All keydowns on document
* @param {Event} event
*/
private documentKeydown(event: KeyboardEvent): void {
switch (event.keyCode) {
case _.keyCodes.ENTER:
this.enterPressed(event);
break;
case _.keyCodes.BACKSPACE:
this.backspacePressed(event);
break;
default:
this.defaultBehaviour(event);
break;
}
}
/**
* Ignore all other document's keydown events
* @param {KeyboardEvent} event
*/
private defaultBehaviour(event: KeyboardEvent): void {
const keyDownOnEditor = (event.target as HTMLElement).closest(`.${this.CSS.editorWrapper}`);
const {currentBlock} = this.Editor.BlockManager;
const isMetaKey = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
/**
* Ignore keydowns on editor and meta keys
*/
if (keyDownOnEditor || (currentBlock && isMetaKey)) {
return;
}
/**
* Remove all highlights and remove caret
*/
this.Editor.BlockManager.dropPointer();
/**
* Close Toolbar
*/
this.Editor.Toolbar.close();
}
/**
* @param {KeyboardEvent} event
*/
private backspacePressed(event: KeyboardEvent): void {
const {BlockManager, BlockSelection, Caret} = this.Editor;
if (BlockSelection.anyBlockSelected) {
const selectionPositionIndex = BlockManager.removeSelectedBlocks();
Caret.setToBlock(BlockManager.insertAtIndex(selectionPositionIndex, true), Caret.positions.START);
/** Clear selection */
BlockSelection.clearSelection();
/**
* Stop propagations
* Manipulation with BlockSelections is handled in global backspacePress because they may occur
* with CMD+A or RectangleSelection and they can be handled on document event
*/
event.stopPropagation();
event.stopImmediatePropagation();
}
}
/**
* Enter pressed on document
* @param event
*/
private enterPressed(event: KeyboardEvent): void {
const {BlockManager, BlockSelection, Caret} = this.Editor;
const hasPointerToBlock = BlockManager.currentBlockIndex >= 0;
if (BlockSelection.anyBlockSelected) {
const selectionPositionIndex = BlockManager.removeSelectedBlocks();
Caret.setToBlock(BlockManager.insertAtIndex(selectionPositionIndex, true), Caret.positions.START);
/** Clear selection */
BlockSelection.clearSelection();
/**
* Stop propagations
* Manipulation with BlockSelections is handled in global enterPress because they may occur
* with CMD+A or RectangleSelection
*/
event.preventDefault();
event.stopImmediatePropagation();
event.stopPropagation();
return;
}
/**
* If Caret is not set anywhere, event target on Enter is always Element that we handle
* In our case it is document.body
*
* So, BlockManager points some Block and Enter press is on Body
* We can create a new block
*/
if (hasPointerToBlock && (event.target as HTMLElement).tagName === 'BODY') {
/**
* Insert initial typed Block
*/
const newBlock = this.Editor.BlockManager.insert();
this.Editor.Caret.setToBlock(newBlock);
/**
* And highlight
*/
this.Editor.BlockManager.highlightCurrentNode();
/**
* Move toolbar and show plus button because new Block is empty
*/
this.Editor.Toolbar.move();
this.Editor.Toolbar.plusButton.show();
}
this.Editor.BlockSelection.clearSelection();
}
/**
* All clicks on document
* @param {MouseEvent} event - Click
*/
private documentClicked(event: MouseEvent): void {
/**
* Close Inline Toolbar when nothing selected
* Do not fire check on clicks at the Inline Toolbar buttons
*/
const target = event.target as HTMLElement;
const clickedOnInlineToolbarButton = target.closest(`.${this.Editor.InlineToolbar.CSS.inlineToolbar}`);
const clickedInsideOfEditor = this.nodes.holder.contains(target) || Selection.isAtEditor;
if (!clickedInsideOfEditor) {
/**
* Clear highlightings and pointer on BlockManager
*
* Current page might contain several instances
* Click between instances MUST clear focus, pointers and close toolbars
*/
this.Editor.BlockManager.dropPointer();
this.Editor.InlineToolbar.close();
this.Editor.Toolbar.close();
this.Editor.BlockSelection.clearSelection();
} else if (!clickedOnInlineToolbarButton) {
/**
* Move inline toolbar to the focused Block
*/
this.Editor.InlineToolbar.handleShowingEvent(event);
}
if (Selection.isAtEditor) {
/**
* Focus clicked Block
*/
this.Editor.BlockManager.setCurrentBlockByChildNode(Selection.anchorNode);
}
}
/**
* All clicks on the redactor zone
*
* @param {MouseEvent} event
*
* @description
* 1. Save clicked Block as a current {@link BlockManager#currentNode}
* it uses for the following:
* - add CSS modifier for the selected Block
* - on Enter press, we make a new Block under that
*
* 2. Move and show the Toolbar
*
* 3. Set a Caret
*
* 4. By clicks on the Editor's bottom zone:
* - if last Block is empty, set a Caret to this
* - otherwise, add a new empty Block and set a Caret to that
*
* 5. Hide the Inline Toolbar
*
* @see selectClickedBlock
*
*/
private redactorClicked(event: MouseEvent): void {
if (!Selection.isCollapsed) {
return;
}
let clickedNode = event.target as HTMLElement;
/**
* If click was fired is on Editor`s wrapper, try to get clicked node by elementFromPoint method
*/
if (clickedNode === this.nodes.redactor) {
clickedNode = document.elementFromPoint(event.clientX, event.clientY) as HTMLElement;
}
/**
* Select clicked Block as Current
*/
try {
/**
* Renew Current Block
*/
this.Editor.BlockManager.setCurrentBlockByChildNode(clickedNode);
/**
* Highlight Current Node
*/
this.Editor.BlockManager.highlightCurrentNode();
} catch (e) {
/**
* If clicked outside first-level Blocks and it is not RectSelection, set Caret to the last empty Block
*/
if (!this.Editor.RectangleSelection.isRectActivated()) {
this.Editor.Caret.setToTheLastBlock();
}
}
event.stopImmediatePropagation();
event.stopPropagation();
/**
* Move and open toolbar
*/
this.Editor.Toolbar.open();
/**
* Hide the Plus Button
*/
this.Editor.Toolbar.plusButton.hide();
if (!this.Editor.BlockManager.currentBlock) {
this.Editor.BlockManager.insert();
}
/**
* Show the Plus Button if:
* - Block is an initial-block (Text)
* - Block is empty
*/
const isInitialBlock = this.Editor.Tools.isInitial(this.Editor.BlockManager.currentBlock.tool);
if (isInitialBlock) {
/**
* Check isEmpty only for paragraphs to prevent unnecessary tree-walking on Tools with many nodes (for ex. Table)
*/
const isEmptyBlock = this.Editor.BlockManager.currentBlock.isEmpty;
if (isEmptyBlock) {
this.Editor.Toolbar.plusButton.show();
}
}
/** Clear selection */
this.Editor.BlockSelection.clearSelection();
}
/**
* Append prebuilded sprite with SVG icons
*/
private appendSVGSprite(): void {
const spriteHolder = $.make('div');
spriteHolder.hidden = true;
spriteHolder.style.display = 'none';
spriteHolder.innerHTML = sprite;
$.append(this.nodes.wrapper, spriteHolder);
}
}