[Feature/issue-704]: Conversion toolbar (#787)

* [issue-748]: update Readme (#773)

* 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

* update link to the minified script

* bump version

* Prefer user settings during tools merge (#774)

* Prefer user settings during tools merge

* issue-705: Leaf BlockSettings and InlineTools via keyboard (#723)

* 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

* leaf buttons: initial

* leaf inline toolbar buttons

* 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

* leaf inline tools and drop index after click

* leaf toolbox and clear active button after activation

* debugging blockSettings

* 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

* done

* requested changes

* production build

* update package.json

* some improvements

* ready for testing

* update

* ready

* requested changes

* updates

* use setToBlock instead of focus

* active -> focused

* update

* refactor types

* fix inline tools flipping

* inhancements

* rm check for focus at the handleShowingEvent

* fix IT closing after second enter

* add animation to settings buttons

* Click animation

* Add changelog

* do not patch version

* conversion toolbar

* positioning and flipping initial

* conversion toolbar is ready

* save changes

* update

* bump version

* save changes

* ready

* open conversion toolbar via keyup

* save changes

* ready to be reviewed

* restore some useless changes

* several improvements

* Update src/components/modules/toolbar/conversion.ts

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

* Update src/components/modules/toolbar/conversion.ts

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

* Update src/components/modules/toolbar/conversion.ts

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

* Update src/components/modules/toolbar/conversion.ts

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

* Update src/components/modules/toolbar/conversion.ts

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

* Update src/components/modules/toolbar/conversion.ts

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

* ready

* Update src/components/modules/toolbar/conversion.ts

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

* requested changes

* update

* update comment

* define types

* simplify keydowns

* Update blockEvents.ts

* Don't use handleShowingEvent to close Inline/Conversion Toolbars

* battle with events

* bundle

* bundle

* rm console.trace

* bundle

* small improvements

* improve types description

* add docs, improve types

* improve anchor links

* fix move

* fix toolbar movement

* conversion toolbar fix movement

* improve margins

* bundle

* add changelog

* rm commented code

* tools goes to master
This commit is contained in:
Murod Khaydarov 2019-06-29 23:41:18 +03:00 committed by Peter Savchenko
commit a6194c19fc
22 changed files with 837 additions and 135 deletions

44
dist/editor.js vendored

File diff suppressed because one or more lines are too long

View file

@ -5,6 +5,7 @@
- `Improvements` — Inline Toolbar now works on mobile devices [#706](https://github.com/codex-team/editor.js/issues/706)
- `Improvements` — Toolbar looks better on mobile devices [#706](https://github.com/codex-team/editor.js/issues/706)
- `Fix` — Added `typeof` util method to check exact object type [#805](https://github.com/codex-team/editor.js/issues/805)
- `New` *Conversion Toolbar* — Ability to convert one block to another [#704](https://github.com/codex-team/editor.js/issues/704)
### 2.14

View file

@ -58,6 +58,8 @@ Options that Tool can specify. All settings should be passed as static propertie
| `toolbox` | _Object_ | `undefined` | Pass here `icon` and `title` to display this `Tool` in the Editor's `Toolbox` <br /> `icon` - HTML string with icon for Toolbox <br /> `title` - optional title to display in Toolbox |
| `enableLineBreaks` | _Boolean_ | `false` | With this option, Editor.js won't handle Enter keydowns. Can be helpful for Tools like `<code>` where line breaks should be handled by default behaviour. |
| `isInline` | _Boolean_ | `false` | Describes Tool as a [Tool for the Inline Toolbar](tools-inline.md) |
| `sanitize` | _Object_ | `undefined` | Config for automatic sanitizing of saved data. See [Sanitize](#sanitize) section. |
| `conversionConfig` | _Object_ | `undefined` | Config allows Tool to specify how it can be converted into/from another Tool. See [Conversion config](#conversion-config) section. |
## User configuration
@ -224,7 +226,7 @@ onPaste (event) {
}
```
## Sanitize
## Sanitize <a name="sanitize"></a>
Editor.js provides [API](sanitizer.md) to clean taint strings.
Use it manually at the `save()` method or or pass `sanitizer` config to do it automatically.
@ -341,4 +343,111 @@ static get sanitize() {
}
```
## Conversion config <a name="conversion-config"></a>
Editor.js has a Conversion Toolbar that allows user to convert one Block to another.
![](https://capella.pics/6c1f708b-a30c-4ffd-a427-5b59a1a472e0.jpg)
1. You can add ability to your Tool to be converted. Specify «export» property of `conversionConfig`.
2. You can add ability to convert other Tools to your Tool. Specify «import» property of `conversionConfig`.
Conversion Toolbar will be shown only near Blocks that specified an «export» rule, when user selected almost all block's content.
This Toolbar will contain only Tools that specified an «import» rule.
Example:
```js
class Header {
constructor(){
this.data = {
text: '',
level: 2
}
}
/**
* Rules specified how our Tool can be converted to/from other Tool.
*/
static get conversionConfig() {
return {
export: 'text', // this property of tool data will be used as string to pass to other tool
import: 'text' // to this property imported string will be passed
};
}
}
```
### Your Tool -> other Tool
The «export» field specifies how to represent your Tool's data as a string to pass it to other tool.
It can be a `String` or a `Function`.
`String` means a key of your Tool data object that should be used as string to export.
`Function` is a method that accepts your Tool data and compose a string to export from it. See example below:
```js
class ListTool {
constructor(){
this.data = {
items: [
'Fisrt item',
'Second item',
'Third item'
],
type: 'ordered'
}
}
static get conversionConfig() {
return {
export: (data) => {
return data.items.join('.'); // in this example, all list items will be concatenated to an export string
},
// ... import rule
};
}
}
```
### Other Tool -> your Tool
The «import» rule specifies how to create your Tool's data object from the string created by original block.
It can be a `String` or a `Function`.
`String` means the key in tool data that will be filled by an exported string.
For example, `import: 'text'` means that `constructor` of your block will accept a `data` object with `text` property filled with string composed by original block.
`Function` allows you to specify own logic, how a string should be converted to your tool data. For example:
```js
class ListTool {
constructor(data){
this.data = data || {
items: [],
type: 'unordered'
}
}
static get conversionConfig() {
return {
// ... export rule
/**
* In this example, List Tool creates items by splitting original text by a dot symbol.
*/
import: (string) => {
const items = string.split('.');
return {
items: items.filter( (text) => text.trim() !== ''),
type: 'unordered'
};
}
};
}
}
```

View file

@ -1,6 +1,6 @@
{
"name": "@editorjs/editorjs",
"version": "2.14.0",
"version": "2.15.0",
"description": "Editor.js — Native JS, based on API and Open Source",
"main": "dist/editor.js",
"types": "./types/index.d.ts",

View file

@ -162,13 +162,13 @@ export default class Block {
/**
* Returns Plugins content
* @return {Node}
* @return {HTMLElement}
*/
get pluginsContent(): Node {
get pluginsContent(): HTMLElement {
const pluginsContent = this.holder.querySelector(`.${Block.CSS.content}`);
if (pluginsContent && pluginsContent.childNodes.length) {
return pluginsContent.childNodes[0];
return pluginsContent.childNodes[0] as HTMLElement;
}
return null;

View file

@ -21,7 +21,7 @@ export default class InlineToolbarAPI extends Module {
* Open Inline Toolbar
*/
public open(): void {
this.Editor.InlineToolbar.open();
this.Editor.InlineToolbar.tryToShow();
}
/**

View file

@ -56,7 +56,7 @@ export default class BlockEvents extends Module {
* Fires on keydown before event processing
* @param {KeyboardEvent} event - keydown
*/
public beforeKeydownProcessing(event): void {
public beforeKeydownProcessing(event: KeyboardEvent): void {
/**
* Do not close Toolbox on Tabs or on Enter with opened Toolbox
*/
@ -65,45 +65,53 @@ export default class BlockEvents extends Module {
}
/**
* Close Toolbar on any keypress except TAB, because TAB leafs Tools
* When user type something:
* - close Toolbar
* - close Conversion Toolbar
* - clear block highlighting
*/
if (event.keyCode !== _.keyCodes.TAB) {
if (_.isPrintableKey(event.keyCode)) {
this.Editor.Toolbar.close();
}
this.Editor.ConversionToolbar.close();
const cmdKey = event.ctrlKey || event.metaKey;
const altKey = event.altKey;
const shiftKey = event.shiftKey;
/** clear selecton when it is not CMD, SHIFT, ALT keys */
if (cmdKey || altKey || shiftKey) {
return;
}
/**
* Clear all highlightings
*/
this.Editor.BlockManager.clearFocused();
if (event.keyCode !== _.keyCodes.ENTER && event.keyCode !== _.keyCodes.BACKSPACE) {
/**
* Clear selection and restore caret before navigation
* Allow to use shortcuts with selected blocks
* @type {boolean}
*/
this.Editor.BlockSelection.clearSelection(true);
const isShortcut = event.ctrlKey || event.metaKey || event.altKey || event.shiftKey;
if (!isShortcut) {
this.Editor.BlockManager.clearFocused();
this.Editor.BlockSelection.clearSelection();
}
}
}
/**
* Key up on Block:
* - shows Inline Toolbar if something selected
* - shows conversion toolbar with 85% of block selection
*/
public keyup(event): void {
this.Editor.InlineToolbar.handleShowingEvent(event);
const { InlineToolbar, ConversionToolbar, UI, BlockManager } = this.Editor;
const block = BlockManager.getBlock(event.target);
/**
* Conversion Toolbar will be opened when user selects 85% of plugins content
* that why we must with the length of pluginsContent
*/
if (SelectionUtils.almostAllSelected(block.pluginsContent.textContent)) {
InlineToolbar.close();
ConversionToolbar.tryToShow(block);
} else {
ConversionToolbar.close();
InlineToolbar.tryToShow();
}
/**
* Check if editor is empty on each keyup and add special css class to wrapper
*/
this.Editor.UI.checkEmptiness();
UI.checkEmptiness();
}
/**
@ -111,7 +119,36 @@ export default class BlockEvents extends Module {
* - shows Inline Toolbar if something selected
*/
public mouseUp(event): void {
this.Editor.InlineToolbar.handleShowingEvent(event);
const { InlineToolbar, ConversionToolbar, BlockManager, BlockSelection } = this.Editor;
const block = BlockManager.getBlock(event.target);
/**
* Timeout uses to wait if selection will cleared after mouse up (regular click on block)
*/
_.delay(() => {
/**
* 1) selected 85% of block - open Conversion Toolbar
* 2) select something inside block - open Inline Toolbar
* 3) nothing selected - close Toolbars
*/
if (SelectionUtils.almostAllSelected(block.pluginsContent.textContent)) {
InlineToolbar.close();
ConversionToolbar.tryToShow(block);
} else if (!SelectionUtils.isCollapsed) {
InlineToolbar.tryToShow();
ConversionToolbar.close();
} else {
InlineToolbar.close();
/**
* Don't close Conversion toolbar when Rectangle Selection ended with one block selected
* @see RectangleSelection#endSelection
*/
if (BlockSelection.selectedBlocks.length !== 1) {
ConversionToolbar.close();
}
}
}, 30)();
}
/**
@ -119,8 +156,13 @@ export default class BlockEvents extends Module {
* @param {KeyboardEvent} event
*/
public tabPressed(event): void {
/**
* Clear blocks selection by tab
*/
this.Editor.BlockSelection.clearSelection();
const {currentBlock} = this.Editor.BlockManager;
const { BlockManager, Tools, ConversionToolbar, InlineToolbar } = this.Editor;
const currentBlock = BlockManager.currentBlock;
if (!currentBlock) {
return;
@ -134,58 +176,21 @@ export default class BlockEvents extends Module {
const shiftKey = event.shiftKey,
direction = shiftKey ? 'left' : 'right';
const canLeafToolbox = Tools.isInitial(currentBlock.tool) && currentBlock.isEmpty;
const canLeafInlineToolbar = !currentBlock.isEmpty && !SelectionUtils.isCollapsed && InlineToolbar.opened;
const canLeafConversionToolbar = !currentBlock.isEmpty && ConversionToolbar.opened;
/**
* For empty Blocks we show Plus button via Toobox only for initial Blocks
*/
if (this.Editor.Tools.isInitial(currentBlock.tool) && currentBlock.isEmpty) {
/**
* Work with Toolbox
* ------------------
*
* If Toolbox is not open, then just open it and show plus button
* Next Tab press will leaf Toolbox Tools
*/
if (!this.Editor.Toolbar.opened) {
this.Editor.Toolbar.open(false , false);
this.Editor.Toolbar.plusButton.show();
} else {
this.Editor.Toolbox.leaf(direction);
}
this.Editor.Toolbox.open();
} else if (!currentBlock.isEmpty && !SelectionUtils.isCollapsed) {
/**
* Work with Inline Tools
* -----------------------
*
* If InlineToolbar is not open, just open it and focus first button
* Next Tab press will leaf InlineToolbar Tools
*/
if (this.Editor.InlineToolbar.opened) {
this.Editor.InlineToolbar.leaf(direction);
}
if (canLeafToolbox) {
this.leafToolboxTools(direction);
} else if (canLeafInlineToolbar) {
this.leafInlineToolbarTools(direction);
} else if (canLeafConversionToolbar) {
this.leafConversionToolbarTools(direction);
} else {
/**
* Open Toolbar and show BlockSettings
*/
if (!this.Editor.Toolbar.opened) {
this.Editor.BlockManager.currentBlock.focused = true;
this.Editor.Toolbar.open(true, false);
this.Editor.Toolbar.plusButton.hide();
}
/**
* Work with Block Tunes
* ----------------------
*
* If BlockSettings is not open, then open BlockSettings
* Next Tab press will leaf Settings Buttons
*/
if (!this.Editor.BlockSettings.opened) {
this.Editor.BlockSettings.open();
}
this.Editor.BlockSettings.leaf(direction);
this.leafBlockSettingsTools(direction);
}
}
@ -196,6 +201,11 @@ export default class BlockEvents extends Module {
* @param {Event} event
*/
public escapePressed(event): void {
/**
* Clear blocks selection by ESC
*/
this.Editor.BlockSelection.clearSelection();
if (this.Editor.Toolbox.opened) {
this.Editor.Toolbox.close();
} else if (this.Editor.BlockSettings.opened) {
@ -285,7 +295,7 @@ export default class BlockEvents extends Module {
* @param {KeyboardEvent} event - keydown
*/
private enter(event: KeyboardEvent): void {
const {BlockManager, Tools} = this.Editor;
const { BlockManager, Toolbox, BlockSettings, InlineToolbar, ConversionToolbar, Tools } = this.Editor;
const currentBlock = BlockManager.currentBlock;
const tool = Tools.available[currentBlock.name];
@ -294,27 +304,28 @@ export default class BlockEvents extends Module {
* Uses for Tools like <code> where line breaks should be handled by default behaviour.
*/
if (tool
&& tool[this.Editor.Tools.apiSettings.IS_ENABLED_LINE_BREAKS]
&& !this.Editor.BlockSettings.opened
&& !this.Editor.InlineToolbar.opened) {
&& tool[Tools.apiSettings.IS_ENABLED_LINE_BREAKS]
&& !BlockSettings.opened
&& !InlineToolbar.opened
&& !ConversionToolbar.opened) {
return;
}
if (this.Editor.Toolbox.opened && this.Editor.Toolbox.getActiveTool) {
if (Toolbox.opened && Toolbox.getActiveTool) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
this.Editor.Toolbox.toolButtonActivate(event, this.Editor.Toolbox.getActiveTool);
Toolbox.toolButtonActivate(event, Toolbox.getActiveTool);
return;
}
if (this.Editor.InlineToolbar.opened && this.Editor.InlineToolbar.focusedButton) {
if (InlineToolbar.opened && InlineToolbar.focusedButton) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
this.Editor.InlineToolbar.focusedButton.click();
InlineToolbar.focusedButton.click();
return;
}
@ -482,9 +493,17 @@ export default class BlockEvents extends Module {
* After caret is set, update Block input index
*/
_.delay(() => {
this.Editor.BlockManager.currentBlock.updateCurrentInput();
/** Check currentBlock for case when user moves selection out of Editor */
if (this.Editor.BlockManager.currentBlock) {
this.Editor.BlockManager.currentBlock.updateCurrentInput();
}
}, 20)();
}
/**
* Clear blocks selection by arrows
*/
this.Editor.BlockSelection.clearSelection();
}
/**
@ -501,9 +520,17 @@ export default class BlockEvents extends Module {
* After caret is set, update Block input index
*/
_.delay(() => {
this.Editor.BlockManager.currentBlock.updateCurrentInput();
/** Check currentBlock for case when user ends selection out of Editor and then press arrow-key */
if (this.Editor.BlockManager.currentBlock) {
this.Editor.BlockManager.currentBlock.updateCurrentInput();
}
}, 20)();
}
/**
* Clear blocks selection by arrows
*/
this.Editor.BlockSelection.clearSelection();
}
/**
@ -517,6 +544,8 @@ export default class BlockEvents extends Module {
private needToolbarClosing(event) {
const toolboxItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.Toolbox.opened),
blockSettingsItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.BlockSettings.opened),
inlineToolbarItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.InlineToolbar.opened),
conversionToolbarItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.ConversionToolbar.opened),
flippingToolbarItems = event.keyCode === _.keyCodes.TAB;
/**
@ -525,6 +554,71 @@ export default class BlockEvents extends Module {
* 2. When Toolbar is opened and Tab leafs its Tools
* 3. When Toolbar's component is opened and some its item selected
*/
return !(event.shiftKey || flippingToolbarItems || toolboxItemSelected || blockSettingsItemSelected);
return !(event.shiftKey
|| flippingToolbarItems
|| toolboxItemSelected
|| blockSettingsItemSelected
|| inlineToolbarItemSelected
|| conversionToolbarItemSelected
);
}
/**
* If Toolbox is not open, then just open it and show plus button
* Next Tab press will leaf Toolbox Tools
*
* @param {string} direction
*/
private leafToolboxTools(direction: string): void {
if (!this.Editor.Toolbar.opened) {
this.Editor.Toolbar.open(false , false);
this.Editor.Toolbar.plusButton.show();
} else {
this.Editor.Toolbox.leaf(direction);
}
this.Editor.Toolbox.open();
}
/**
* If InlineToolbar is not open, just open it and focus first button
* Next Tab press will leaf InlineToolbar Tools
*
* @param {string} direction
*/
private leafInlineToolbarTools(direction: string): void {
if (this.Editor.InlineToolbar.opened) {
this.Editor.InlineToolbar.leaf(direction);
}
}
/**
* Leaf Conversion Toolbar Tools
* @param {string} direction
*/
private leafConversionToolbarTools(direction: string): void {
this.Editor.ConversionToolbar.leaf(direction);
}
/**
* Open Toolbar and show BlockSettings before flipping Tools
* @param {string} direction
*/
private leafBlockSettingsTools(direction: string): void {
if (!this.Editor.Toolbar.opened) {
this.Editor.BlockManager.currentBlock.focused = true;
this.Editor.Toolbar.open(true, false);
this.Editor.Toolbar.plusButton.hide();
}
/**
* If BlockSettings is not open, then open BlockSettings
* Next Tab press will leaf Settings Buttons
*/
if (!this.Editor.BlockSettings.opened) {
this.Editor.BlockSettings.open();
}
this.Editor.BlockSettings.leaf(direction);
}
}

View file

@ -6,6 +6,7 @@
* @version 1.0.0
*/
import Module from '../__module';
import Block from '../block';
import _ from '../utils';
import $ from '../dom';
@ -74,6 +75,14 @@ export default class BlockSelection extends Module {
return BlockManager.blocks.some((block) => block.selected === true);
}
/**
* Return selected Blocks array
* @return {Block[]}
*/
public get selectedBlocks(): Block[] {
return this.Editor.BlockManager.blocks.filter((block) => block.selected);
}
/**
* Flag used to define block selection
* First CMD+A defines it as true and then second CMD+A selects all Blocks
@ -179,15 +188,13 @@ export default class BlockSelection extends Module {
* Reduce each Block and copy its content
*/
public copySelectedBlocks(): void {
const {BlockManager, Sanitizer} = this.Editor;
const fakeClipboard = $.make('div');
BlockManager.blocks.filter((block) => block.selected)
.forEach((block) => {
this.selectedBlocks.forEach((block) => {
/**
* Make <p> tag that holds clean HTML
*/
const cleanHTML = Sanitizer.clean(block.holder.innerHTML, this.sanitizerConfig);
const cleanHTML = this.Editor.Sanitizer.clean(block.holder.innerHTML, this.sanitizerConfig);
const fragment = $.make('p');
fragment.innerHTML = cleanHTML;
@ -226,12 +233,12 @@ export default class BlockSelection extends Module {
}
/**
* First CMD+A Selects current focused blocks,
* and consequent second CMD+A keypress selects all blocks
* First CMD+A selects all input content by native behaviour,
* next CMD+A keypress selects all blocks
*
* @param {keydown} event
* @param {KeyboardEvent} event
*/
private handleCommandA(event): void {
private handleCommandA(event: KeyboardEvent): void {
this.Editor.RectangleSelection.clearSelection();
/** allow default selection on native inputs */
@ -240,7 +247,8 @@ export default class BlockSelection extends Module {
return;
}
const inputs = this.Editor.BlockManager.currentBlock.inputs;
const workingBlock = this.Editor.BlockManager.getBlock(event.target as HTMLElement);
const inputs = workingBlock.inputs;
/**
* If Block has more than one editable element allow native selection
@ -251,14 +259,30 @@ export default class BlockSelection extends Module {
return;
}
/** Prevent default selection */
event.preventDefault();
if (this.needToSelectAll) {
/** Prevent default selection */
event.preventDefault();
/**
* Save selection
* Will be restored when closeSelection fired
*/
this.selection.save();
/**
* Remove Ranges from Selection
*/
SelectionUtils.get()
.removeAllRanges();
this.selectAllBlocks();
this.needToSelectAll = false;
/**
* Close ConversionToolbar when all Blocks selected
*/
this.Editor.ConversionToolbar.close();
} else {
this.selectBlockByIndex();
this.needToSelectAll = true;
}
}

View file

@ -64,11 +64,17 @@ export default class Caret extends Module {
return (firstNode as HTMLInputElement).selectionEnd === 0;
}
/** Case when selection have been cleared programmatically, for example after CBS */
if (!selection.anchorNode) {
return false;
}
/**
* Workaround case when caret in the text like " |Hello!"
* selection.anchorOffset is 1, but real caret visible position is 0
* @type {number}
*/
let firstLetterPosition = anchorNode.textContent.search(/\S/);
if (firstLetterPosition === -1) { // empty text
@ -140,6 +146,11 @@ export default class Caret extends Module {
return (lastNode as HTMLInputElement).selectionEnd === (lastNode as HTMLInputElement).value.length;
}
/** Case when selection have been cleared programmatically, for example after CBS */
if (!selection.anchorNode) {
return false;
}
/**
* If caret was set by external code, it might be set to text node wrapper.
* <div>hello|</div> <---- Selection references to <div> instead of text node

View file

@ -10,7 +10,6 @@ import $ from '../dom';
import SelectionUtils from '../selection';
import Block from '../block';
import Timeout = NodeJS.Timeout;
export default class RectangleSelection extends Module {
/**
@ -167,6 +166,15 @@ export default class RectangleSelection extends Module {
this.startX = 0;
this.startY = 0;
this.overlayRectangle.style.display = 'none';
/**
* Show Conversion Toolbar when user select one Block
*/
const { selectedBlocks } = this.Editor.BlockSelection;
if (selectedBlocks.length === 1) {
this.Editor.ConversionToolbar.tryToShow(selectedBlocks[0]);
}
}
/**

View file

@ -0,0 +1,308 @@
import Module from '../../__module';
import $ from '../../dom';
import {BlockToolConstructable} from '../../../../types';
import _ from '../../utils';
import {SavedData} from '../../../types-internal/block-data';
import Block from '../../block';
/**
* Block Converter
*/
export default class ConversionToolbar extends Module {
/**
* CSS getter
*/
public static get CSS(): { [key: string]: string } {
return {
conversionToolbarWrapper: 'ce-conversion-toolbar',
conversionToolbarShowed: 'ce-conversion-toolbar--showed',
conversionToolbarTools: 'ce-conversion-toolbar__tools',
conversionTool: 'ce-conversion-tool',
conversionToolFocused : 'ce-conversion-tool--focused',
conversionToolActive : 'ce-conversion-tool--active',
};
}
/**
* HTML Elements used for UI
*/
public nodes: { [key: string]: HTMLElement } = {
wrapper: null,
tools: null,
};
/**
* Conversion Toolbar open/close state
* @type {boolean}
*/
public opened: boolean = false;
/**
* Focused button index
* -1 equals no chosen Tool
* @type {number}
*/
private focusedButtonIndex: number = -1;
/**
* Available tools
*/
private tools: { [key: string]: HTMLElement } = {};
/**
* Create UI of Conversion Toolbar
*/
public make(): void {
this.nodes.wrapper = $.make('div', ConversionToolbar.CSS.conversionToolbarWrapper);
this.nodes.tools = $.make('div', ConversionToolbar.CSS.conversionToolbarTools);
/**
* Add Tools that has 'import' method
*/
this.addTools();
$.append(this.nodes.wrapper, this.nodes.tools);
$.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper);
}
/**
* Try to show Conversion Toolbar near passed Block
* @param {Block} block - block to convert
*/
public tryToShow(block: Block): void {
const hasExportConfig = block.class.conversionConfig && block.class.conversionConfig.export;
if (!hasExportConfig) {
return;
}
const currentToolName = block.name;
/**
* Focus current tool in conversion toolbar
*/
if (this.tools[currentToolName]) {
/**
* Drop previous active button before moving
*/
if (this.focusedButton && this.focusedButton.classList.contains(ConversionToolbar.CSS.conversionToolActive)) {
this.focusedButton.classList.remove(ConversionToolbar.CSS.conversionToolActive);
}
this.tools[currentToolName].classList.add(ConversionToolbar.CSS.conversionToolActive);
}
this.move(block);
if (!this.opened) {
this.open();
}
}
/**
* Shows Conversion Toolbar
*/
public open(): void {
this.opened = true;
this.nodes.wrapper.classList.add(ConversionToolbar.CSS.conversionToolbarShowed);
}
/**
* Closes Conversion Toolbar
*/
public close(): void {
this.opened = false;
this.nodes.wrapper.classList.remove(ConversionToolbar.CSS.conversionToolbarShowed);
this.dropFocusedButton();
}
/**
* Leaf tools by Tab
* @todo use class with tool iterator
*/
public leaf(direction: string = 'right'): void {
const toolsElements = (Array.from(this.nodes.tools.childNodes) as HTMLElement[]);
this.focusedButtonIndex = $.leafNodesAndReturnIndex(
toolsElements, this.focusedButtonIndex, direction, ConversionToolbar.CSS.conversionToolFocused,
);
}
/**
* Returns focused tool as HTML element
* @return {HTMLElement}
*/
public get focusedButton(): HTMLElement {
if (this.focusedButtonIndex === -1) {
return null;
}
return (this.nodes.tools.childNodes[this.focusedButtonIndex] as HTMLElement);
}
/**
* Drops focused button
*/
public dropFocusedButton() {
Object.values(this.tools).forEach( (tool) => {
(tool as HTMLElement).classList
.remove(ConversionToolbar.CSS.conversionToolActive, ConversionToolbar.CSS.conversionToolFocused);
});
this.focusedButtonIndex = -1;
}
/**
* Replaces one Block with another
* For that Tools must provide import/export methods
*
* @param {string} replacingToolName
*/
public async replaceWithBlock(replacingToolName: string): Promise <void> {
/**
* At first, we get current Block data
* @type {BlockToolConstructable}
*/
const currentBlockClass = this.Editor.BlockManager.currentBlock.class;
const currentBlockName = this.Editor.BlockManager.currentBlock.name;
const savedBlock = await this.Editor.BlockManager.currentBlock.save() as SavedData;
const blockData = savedBlock.data;
/**
* When current Block name is equals to the replacing tool Name,
* than convert this Block back to the initial Block
*/
if (currentBlockName === replacingToolName) {
replacingToolName = this.config.initialBlock;
}
/**
* Getting a class of replacing Tool
* @type {BlockToolConstructable}
*/
const replacingTool = this.Editor.Tools.toolsClasses[replacingToolName] as BlockToolConstructable;
/**
* Export property can be:
* 1) Function Tool defines which data to return
* 2) String the name of saved property
*
* In both cases returning value must be a string
*/
let exportData: string = '';
const exportProp = currentBlockClass.conversionConfig.export;
if (typeof exportProp === 'function') {
exportData = exportProp(blockData);
} else if (typeof exportProp === 'string') {
exportData = blockData[exportProp];
} else {
_.log('Conversion «export» property must be a string or function. ' +
'String means key of saved data object to export. Function should export processed string to export.');
return;
}
/**
* Clean exported data with replacing sanitizer config
*/
const cleaned: string = this.Editor.Sanitizer.clean(
exportData,
replacingTool.sanitize,
);
/**
* «import» property can be Function or String
* function accept imported string and compose tool data object
* string the name of data field to import
*/
let newBlockData = {};
const importProp = replacingTool.conversionConfig.import;
if (typeof importProp === 'function') {
newBlockData = importProp(cleaned);
} else if (typeof importProp === 'string') {
newBlockData[importProp] = cleaned;
} else {
_.log('Conversion «import» property must be a string or function. ' +
'String means key of tool data to import. Function accepts a imported string and return composed tool data.');
return;
}
this.Editor.BlockManager.replace(replacingToolName, newBlockData);
this.Editor.BlockSelection.clearSelection();
this.close();
_.delay(() => {
this.Editor.Caret.setToBlock(this.Editor.BlockManager.currentBlock);
}, 10)();
}
/**
* Move Conversion Toolbar to the working Block
*/
private move(block: Block): void {
const blockRect = block.pluginsContent.getBoundingClientRect();
const wrapperRect = this.Editor.UI.nodes.wrapper.getBoundingClientRect();
const newCoords = {
x: blockRect.left - wrapperRect.left,
y: blockRect.top + blockRect.height - wrapperRect.top,
};
this.nodes.wrapper.style.left = Math.floor(newCoords.x) + 'px';
this.nodes.wrapper.style.top = Math.floor(newCoords.y) + 'px';
}
/**
* Iterates existing Tools and inserts to the ConversionToolbar
* if tools have ability to import
*/
private addTools(): void {
const tools = this.Editor.Tools.blockTools;
for (const toolName in tools) {
if (!tools.hasOwnProperty(toolName)) {
continue;
}
const api = this.Editor.Tools.apiSettings;
const toolClass = tools[toolName] as BlockToolConstructable;
const toolToolboxSettings = toolClass[api.TOOLBOX];
const conversionConfig = toolClass[api.CONVERSION_CONFIG];
/**
* Skip tools that don't pass 'toolbox' property
*/
if (_.isEmpty(toolToolboxSettings) || !toolToolboxSettings.icon) {
continue;
}
/**
* Skip tools without «import» rule specified
*/
if (!conversionConfig || !conversionConfig.import) {
continue;
}
this.addTool(toolName, toolToolboxSettings.icon);
}
}
/**
* Add tool to the Conversion Toolbar
*/
private addTool(toolName: string, toolIcon: string): void {
const tool = $.make('div', [ ConversionToolbar.CSS.conversionTool ]);
tool.dataset.tool = toolName;
tool.innerHTML = toolIcon;
$.append(this.nodes.tools, tool);
this.tools[toolName] = tool;
this.Editor.Listeners.on(tool, 'click', async () => {
await this.replaceWithBlock(toolName);
});
}
}

View file

@ -149,12 +149,10 @@ export default class InlineToolbar extends Module {
*/
/**
* Shows Inline Toolbar by keyup/mouseup
* @param {KeyboardEvent|MouseEvent} event
* Shows Inline Toolbar if something is selected
*/
public handleShowingEvent(event): void {
public tryToShow(): void {
if (!this.allowedToShow()) {
this.close();
return;
}
@ -272,12 +270,6 @@ export default class InlineToolbar extends Module {
* Shows Inline Toolbar
*/
public open(): void {
/**
* Check if inline toolbar is allowed to show or not
*/
if (!this.allowedToShow()) {
return;
}
/**
* Filter inline-tools and show only allowed by Block's Tool
@ -334,6 +326,7 @@ export default class InlineToolbar extends Module {
// The selection of the element only in contenteditable
const contenteditable = target.closest('[contenteditable="true"]');
if (contenteditable === null) {
return false;
}

View file

@ -123,6 +123,7 @@ export default class Tools extends Module {
/**
* Constant for available Tools Settings
* @todo separate internal and external options
* @return {object}
*/
public get apiSettings() {
@ -135,6 +136,7 @@ export default class Tools extends Module {
SHORTCUT: 'shortcut',
TOOLBOX: 'toolbox',
SANITIZE_CONFIG: 'sanitize',
CONVERSION_CONFIG: 'conversionConfig',
};
}
@ -151,7 +153,7 @@ export default class Tools extends Module {
private readonly toolsAvailable: {[name: string]: ToolConstructable} = {};
/**
* Tools` classes not availbale to use beacause of preparation failure
* Tools` classes not available to use because of preparation failure
*/
private readonly toolsUnavailable: {[name: string]: ToolConstructable} = {};

View file

@ -161,6 +161,11 @@ export default class UI extends Module {
*/
await this.Editor.InlineToolbar.make();
/**
* Make the Converter tool holder
*/
await this.Editor.ConversionToolbar.make();
/**
* Load and append CSS
*/
@ -368,7 +373,7 @@ export default class UI extends Module {
* @param event
*/
private enterPressed(event: KeyboardEvent): void {
const {BlockManager, BlockSelection, Caret, BlockSettings} = this.Editor;
const { BlockManager, BlockSelection, Caret, BlockSettings, ConversionToolbar } = this.Editor;
const hasPointerToBlock = BlockManager.currentBlockIndex >= 0;
/**
@ -413,6 +418,15 @@ export default class UI extends Module {
return;
}
if (ConversionToolbar.opened && ConversionToolbar.focusedButton) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
ConversionToolbar.focusedButton.click();
return;
}
if (BlockSelection.anyBlockSelected) {
const selectionPositionIndex = BlockManager.removeSelectedBlocks();
Caret.setToBlock(BlockManager.insertAtIndex(selectionPositionIndex, true), Caret.positions.START);
@ -484,7 +498,7 @@ export default class UI extends Module {
this.Editor.InlineToolbar.close();
this.Editor.Toolbar.close();
this.Editor.BlockSelection.clearSelection();
this.Editor.ConversionToolbar.close();
}
if (Selection.isAtEditor) {
@ -525,7 +539,6 @@ export default class UI extends Module {
*
*/
private redactorClicked(event: MouseEvent): void {
if (!Selection.isCollapsed) {
return;
}
@ -616,7 +629,7 @@ export default class UI extends Module {
return;
}
this.Editor.InlineToolbar.handleShowingEvent(event);
this.Editor.InlineToolbar.tryToShow();
}
/**

View file

@ -101,6 +101,24 @@ export default class SelectionUtils {
return selection ? selection.isCollapsed : null;
}
/**
* Returns true if 85% of text content is selected
* @return {boolean}
*/
public static almostAllSelected(targetText: string): boolean {
const range = SelectionUtils.range;
if (!range) {
return false;
}
const copiedFragment = range.cloneContents();
const lengthOfWholeText = targetText.length;
const lengthOfCopiedText = copiedFragment.textContent.length;
return lengthOfCopiedText / lengthOfWholeText > 0.85;
}
/**
* Check current selection if it is at Editor's zone
* @return {boolean}

View file

@ -83,6 +83,20 @@ export default class Util {
};
}
/**
* Returns true if passed key code is printable (a-Z, 0-9, etc) character.
* @param {number} keyCode
* @return {boolean}
*/
public static isPrintableKey( keyCode: number ): boolean {
return (keyCode > 47 && keyCode < 58) || // number keys
keyCode === 32 || keyCode === 13 || // Spacebar & return key(s)
(keyCode > 64 && keyCode < 91) || // letter keys
(keyCode > 95 && keyCode < 112) || // Numpad keys
(keyCode > 185 && keyCode < 193) || // ;=,-./` (in order)
(keyCode > 218 && keyCode < 223); // [\]' (in order)
}
/**
* Fires a promise sequence asyncronically
*

View file

@ -0,0 +1,54 @@
.ce-conversion-toolbar {
@apply --overlay-pane;
padding: 3px;
box-shadow: 0 6px 12px -6px rgba(131, 147, 173, 0.46),
5px -12px 34px -13px rgba(97, 105, 134, 0.6),
0 26px 52px 3px rgba(147, 165, 186, 0.24);
opacity: 0;
visibility: hidden;
will-change: transform, opacity;
transition: transform 150ms ease, opacity 250ms ease;
transform: translateY(8px) scale(0.9);
&::before {
left: 20px;
}
&--showed {
opacity: 1;
visibility: visible;
transform: none;
}
[hidden] {
display: none !important;
}
&__buttons {
display: flex;
}
}
.ce-conversion-tool {
@apply --toolbar-button;
line-height: normal;
&:not(:last-of-type) {
margin-right: 2px;
}
&--last {
margin-right: 0 !important;
}
&:hover,
&--active {
color: var(--color-active-icon) !important;
}
&--active {
animation: bounceIn 0.75s 1;
animation-fill-mode: forwards;
}
}

View file

@ -3,6 +3,7 @@
@import './toolbar.css';
@import './toolbox.css';
@import './inline-toolbar.css';
@import './conversion-toolbar.css';
@import './settings.css';
@import './block.css';
@import './animations.css';

View file

@ -31,6 +31,7 @@ import Saver from '../components/modules/saver';
import BlockSelection from '../components/modules/blockSelection';
import RectangleSelection from '../components/modules/RectangleSelection';
import InlineToolbarAPI from '../components/modules/api/inlineToolbar';
import ConversionToolbar from '../components/modules/toolbar/conversion';
export interface EditorModules {
UI: UI;
@ -42,6 +43,7 @@ export interface EditorModules {
InlineToolbar: InlineToolbar;
Toolbox: Toolbox;
BlockSettings: BlockSettings;
ConversionToolbar: ConversionToolbar;
Events: Events;
Shortcuts: Shortcuts;
Paste: Paste;

View file

@ -0,0 +1,26 @@
import {BlockToolData} from '../tools';
/**
* Config allows Tool to specify how it can be converted into/from another Tool
*/
export interface ConversionConfig {
/**
* How to import string to this Tool.
*
* Can be a String or Function:
*
* 1. String the key of Tool data object to fill it with imported string on render.
* 2. Function method that accepts importing string and composes Tool data to render.
*/
import: ((data: string) => string) | string;
/**
* How to export this Tool to make other Block.
*
* Can be a String or Function:
*
* 1. String which property of saved Tool data should be used as exported string.
* 2. Function accepts saved Tool data and create a string to export
*/
export: ((data: BlockToolData) => string) | string;
}

View file

@ -1,3 +1,4 @@
export * from './editor-config';
export * from './sanitizer-config';
export * from './paste-config';
export * from './conversion-config';

View file

@ -1,4 +1,4 @@
import {PasteConfig, SanitizerConfig} from '../configs';
import {ConversionConfig, PasteConfig, SanitizerConfig} from '../configs';
import {BlockToolData} from './block-tool-data';
import {BaseTool, BaseToolConstructable} from './tool';
import {ToolConfig} from './tool-config';
@ -69,5 +69,14 @@ export interface BlockToolConstructable extends BaseToolConstructable {
*/
pasteConfig?: PasteConfig;
/**
* Rules that specified how this Tool can be converted into/from another Tool
*/
conversionConfig?: ConversionConfig;
/**
* @constructor
* @return {BlockTool}
*/
new (config: {api: API, config: ToolConfig, data: BlockToolData}): BlockTool;
}