feat(toolbar): toolbar refactored and ui improved (#1815)

* chore(block-tune-toggler): toggler moved to the left (draft)

* toolbox ui updated

* fixd caret jumpling, improved some styles

* toolbar moving by block-hover

- UI module triggers 'block-hovered' event
- Toolbar uses 'block-hovered' for appearing
- `currentBlock` setter added to the BlockManager
- (reactangle-selection): the throttling added to the mousemove and scroll handlers
- `getBlockIndex` method added to the Api
- (api-blocks): toolbar moving logic removed from `blocks.move()` and `blocks.swap()` methods. Instead, MoveUp and MoveDown tunes uses Toolbar API

* the dark-theme to the example-dev.html

* positioning improved

* fix(rectangle-selection): first click after RS does not clears selection state

* toolbox position fixed

* the toolbox module became a standalone class

- Toolbox became a standalone class from the editor module. It can be accessed only via the owner (the Toolbar module)
- (api.blocks) the insert() method now has the `replace` param. Also, it returns inserted Block API now.

* new(api.listeners): `on()` now returns the listener id. The new `offById()` method added

* fix bug with Tab pressing on hovered but not focused block

* mobile version improved

* upd example dev

* small updaets

* add nested-list

* linting

* (api.toolbar): `toggleBlockSettings` now fires toggling event with the same state

* EventDispatcher used instead of callbacks for the Toolbox

* UIApi added

* fix ci

* git submodules removed from the ci flow

* add paragraph submodule to the ci flow

* Update CHANGELOG.md

* Update package.json

* use ubuntu-latest for chrome ci
This commit is contained in:
Peter Savchenko 2021-11-24 21:14:24 +03:00 committed by GitHub
parent acdd1f5b4e
commit ff91466b14
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 1131 additions and 798 deletions

View file

@ -8,17 +8,17 @@ jobs:
options: --user 1001
steps:
- uses: actions/checkout@v2
- run: yarn pull_tools && yarn tools:update
- run: yarn ci:pull_paragraph
- uses: cypress-io/github-action@v2
with:
config: video=false
browser: firefox
build: yarn build
chrome:
runs-on: ubuntu-16.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: yarn pull_tools && yarn tools:update
- run: yarn ci:pull_paragraph
- uses: cypress-io/github-action@v2
with:
config: video=false
@ -28,7 +28,7 @@ jobs:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- run: yarn pull_tools && yarn tools:update
- run: yarn ci:pull_paragraph
- uses: cypress-io/github-action@v2
with:
config: video=false

3
.gitmodules vendored
View file

@ -49,3 +49,6 @@
[submodule "example/tools/underline"]
path = example/tools/underline
url = https://github.com/editor-js/underline
[submodule "example/tools/nested-list"]
path = example/tools/nested-list
url = https://github.com/editor-js/nested-list

View file

@ -251,4 +251,4 @@ CodeX is a team of digital specialists around the world interested in building h
| 🌐 | Join 👋 | Twitter | Instagram |
| -- | -- | -- | -- |
| [codex.so](https://codex.so) | [codex.so/join](https://codex.so/join) |[@codex_team](http://twitter.com/codex_team) | [@codex_team](http://instagram.com/codex_team) |
| [codex.so](https://codex.so) | [codex.so/join](https://codex.so/join) |[@codex_team](http://twitter.com/codex_team) | [@codex_team](http://instagram.com/codex_team/) |

View file

@ -2,8 +2,24 @@
### 2.23.0
- `Improvement` — The `onChange` callback now accepts two arguments: EditorJS API and the CustomEvent with `type` and `detail` allowing to determine what happened with a Block
- `New` *Block API* — The new `dispatchChange()` method allows to manually trigger the 'onChange' callback. Useful when Tool made a state mutation that is invisible for editor core.
- `Improvement`*EditorConfig* — The `onChange` callback now accepts two arguments: EditorJS API and the CustomEvent with `type` and `detail` allowing to determine what happened with a Block
- `New`*Block API* — The new `dispatchChange()` method allows to manually trigger the 'onChange' callback. Useful when Tool made a state mutation that is invisible for editor core.
- `Improvement`*UI* — Block Tunes toggler moved to the left
- `Improvement`*UI* — Block Actions (BT toggler + Plus Button) will appear on block hovering instead of click
- `Improvement`*UI* — Block Tunes toggler icon and Plus button icon updated
- `Improvement`*Dev Example Page* — The menu with helpful buttons added to the bottom of the screen
- `Improvement`*Dev Example Page* — The 'dark' theme added. Now we can code at night more comfortably.
- `Improvement`*Rectangle Selection* — paint optimized
- `Fix`*Rectangle Selection* — the first click after RS was not clear selection state. Now does.
- `Improvement`*Blocks API* — toolbar moving logic removed from `blocks.move()` and `blocks.swap()` methods. Instead, you should use Toolbar API (it was used by MoveUp and MoveDown tunes, they were updated).
- `New`*Blocks API* — The `getBlockIndex()` method added
- `New`*Blocks API* — the `insert()` method now has the `replace: boolean` parameter
- `New`*Blocks API* — the `insert()` method now returns the inserted `Block API`
- `New`*Listeners API* — the `on()` method now returns the listener id.
- `New`*Listeners API* — the new `offById()` method added
- `New``API` — The new `UiApi` section was added. It allows accessing some editor UI nodes and methods.
- `Refactoring` — Toolbox became a standalone class instead of a Module. It can be accessed only through the Toolbar module.
- `Refactoring` — CI flow optimized.
### 2.22.3

View file

@ -1,11 +1,27 @@
/**
* Styles for the example page
*/
:root {
--color-bg-main: #fff;
--color-border-light: #E8E8EB;
--color-text-main: #000;
}
.dark-mode {
--color-border-light: rgba(255, 255, 255,.08);
--color-bg-main: #1c1e24;
--color-text-main: #737886;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
font-size: 14px;
line-height: 1.5em;
margin: 0;
background: var(--color-bg-main);
color: var(--color-text-main);
}
.ce-example {
@ -13,7 +29,7 @@ body {
}
.ce-example__header {
border-bottom: 1px solid #E8E8EB;
border-bottom: 1px solid var(--color-border-light);
height: 50px;
line-height: 50px;
display: flex;
@ -62,23 +78,17 @@ body {
-moz-osx-font-smoothing: grayscale;
}
.ce-example__content--small {
.thin-mode .ce-example__content {
max-width: 500px;
border-left: 1px solid #eee;
border-right: 1px solid #eee;
padding: 0 15px;
}
.ce-example__content--with-bg {
background: #f4f4f4;
max-width: none;
margin-top: -30px;
}
.ce-example__output {
background: #1B202B;
overflow-x: auto;
padding: 0 30px;
padding: 0 30px 80px;
}
.ce-example__output-content {
@ -127,29 +137,94 @@ body {
}
.ce-example__statusbar {
display: flex;
align-items: center;
position: fixed;
bottom: 10px;
right: 10px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18);
font-size: 12px;
bottom: 0;
right: 0;
left: 0;
background: var(--color-bg-main);
border-radius: 8px 8px 0 0;
border-top: 1px solid var(--color-border-light);
box-shadow: 0 2px 6px var(--color-border-light);
font-size: 13px;
padding: 8px 15px;
z-index: 1;
user-select: none;
}
@media (max-width: 768px) {
.ce-example__statusbar {
display: none;
}
}
.ce-example__statusbar-item:not(:last-of-type)::after {
content: '|';
color: #ddd;
margin: 0 15px 0 12px;
}
.ce-example__statusbar-item--right {
margin-left: auto;
}
.ce-example__statusbar-button {
display: inline-flex;
margin-left: 10px;
background: #4A9DF8;
padding: 6px 12px;
box-shadow: 0 7px 8px -4px rgba(137, 207, 255, 0.77);
display: inline-block;
padding: 3px 12px;
transition: all 150ms ease;
cursor: pointer;
border-radius: 31px;
color: #fff;
font-family: 'PT Mono', Menlo, Monaco, Consolas, Courier New, monospace;
background: #eff1f4;
text-align: center;
user-select: none;
}
.ce-example__statusbar-button:hover {
background: #e0e4eb;
}
.ce-example__statusbar-button-primary {
background: #4A9DF8;
color: #fff;
box-shadow: 0 7px 8px -4px rgba(137, 207, 255, 0.77);
font-family: 'PT Mono', Menlo, Monaco, Consolas, Courier New, monospace;
}
.ce-example__statusbar {
--toggler-size: 20px;
}
.ce-example__statusbar-toggler {
position: relative;
background: #7b8799;
border-radius: 20px;
padding: 2px;
width: calc(var(--toggler-size) * 2.2);
cursor: pointer;
user-select: none;
}
.ce-example__statusbar-toggler::before {
display: block;
content: '';
width: var(--toggler-size);
height: var(--toggler-size);
background: #fff;
border-radius: 50%;
transition: transform 100ms ease-in;
}
.ce-example__statusbar-toggler::after {
--moon-size: calc(var(--toggler-size) * 0.5);
content: '';
position: absolute;
top: 5px;
right: 5px;
height: var(--moon-size);
width: var(--moon-size);
box-shadow: calc(var(--moon-size) * 0.25 * -1) calc(var(--moon-size) * 0.18) 0 calc(var(--moon-size) * 0.05) white;
border-radius: 50%;
}
@media all and (max-width: 730px){
@ -178,22 +253,10 @@ body {
color: rgb(247, 60, 173);
}
.ce-example .ce-block:first-of-type h2.ce-header{
.ce-example .ce-block:first-of-type h1.ce-header{
font-size: 50px;
}
.ce-example h2.ce-header{
font-size: 30px;
}
.ce-example h3.ce-header {
font-size: 24px;
}
.ce-example h4.ce-header {
font-size: 18px;
}
.ce-example-multiple {
display: grid;
grid-template-columns: calc(50% - 15px) calc(50% - 15px);
@ -206,3 +269,101 @@ body {
border-radius: 7px;
padding: 30px;
}
.show-block-boundaries .ce-block {
box-shadow: inset 0 0 0 1px #eff2f5;
}
.show-block-boundaries .ce-block__content {
box-shadow: 0 0 0 1px rgba(224, 231, 241, 0.61) inset;
}
.show-block-boundaries #showBlocksBoundariesButton span,
.thin-mode #enableThinModeButton span {
font-size: 0;
vertical-align: bottom;
}
.show-block-boundaries #showBlocksBoundariesButton span::before,
.thin-mode #enableThinModeButton span::before {
content: attr(data-toggled-text);
display: inline;
font-size: 13px;
}
/**
* Dark theme overrides
*/
.dark-mode img {
opacity: 0.5;
}
.dark-mode .cdx-simple-image__picture--with-border,
.dark-mode .cdx-input {
border-color: var(--color-border-light);
}
.dark-mode .ce-example__button {
box-shadow: 0 24px 18px -14px rgba(4, 154, 255, 0.24);
}
.dark-mode .ce-example__output {
background-color: #17191f;
}
.dark-mode .inline-code {
background-color: rgba(53, 56, 68, 0.62);
color: #727683;
}
.dark-mode a {
color: #959ba8;
}
.dark-mode .ce-example__statusbar-toggler,
.dark-mode .ce-example__statusbar-button {
background-color: #343842;
}
.dark-mode .ce-example__statusbar-toggler::before {
transform: translateX(calc(var(--toggler-size) * 2.2 - var(--toggler-size)));
}
.dark-mode .ce-example__statusbar-toggler::after {
content: '*';
right: auto;
left: 6px;
top: 7px;
color: #fff;
box-shadow: none;
font-size: 32px;
}
.dark-mode.show-block-boundaries .ce-block,
.dark-mode.show-block-boundaries .ce-block__content {
box-shadow: 0 0 0 1px rgba(128, 144, 159, 0.09) inset;
}
.dark-mode.thin-mode .ce-example__content{
border-color: var(--color-border-light);
}
.dark-mode .ce-example__statusbar-item:not(:last-of-type)::after {
color: var(--color-border-light);
}
.dark-mode .ce-block--selected .ce-block__content,
.dark-mode ::selection{
background-color: rgba(57, 68, 84, 0.57);
}
.dark-mode .ce-toolbox__button,
.dark-mode .ce-toolbar__settings-btn,
.dark-mode .ce-toolbar__plus {
color: inherit;
}
.dark-mode .ce-stub {
opacity: 0.3;
}

View file

@ -1,5 +1,5 @@
<!--
Use this page for debugging purposes.
Use this page is for debugging purposes.
Editor Tools are loaded as git-submodules.
You can pull modules by running `yarn pull_tools` and start experimenting.
@ -15,6 +15,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
</head>
<body>
<script>
if (localStorage.getItem('theme') === 'dark') {
document.body.classList.add("dark-mode");
}
</script>
<div class="ce-example">
<div class="ce-example__header">
<a class="ce-example__header-logo" href="https://codex.so/editor">Editor.js 🤩🧦🤨</a>
@ -26,7 +31,7 @@
<a href="https://editorjs.io/creating-a-block-tool" target="_blank">API</a>
</div>
</div>
<div class="ce-example__content _ce-example__content--small">
<div class="ce-example__content">
<div id="editorjs"></div>
<div id="hint-core" style="text-align: center;">
No core bundle file found. Run <code class="inline-code">yarn build</code>
@ -38,12 +43,31 @@
editor.save()
</div>
<div class="ce-example__statusbar">
Readonly:
<b id="readonly-state">
Off
</b>
<div class="ce-example__statusbar-button" id="toggleReadOnlyButton">
toggle
<div class="ce-example__statusbar-item">
Readonly:
<b id="readonly-state">
Off
</b>
&nbsp;
<div class="ce-example__statusbar-button" id="toggleReadOnlyButton">
toggle
</div>
</div>
<div class="ce-example__statusbar-item">
<div class="ce-example__statusbar-button" id="showBlocksBoundariesButton">
<span data-toggled-text="Hide">Show</span>
blocks boundaries
</div>
</div>
<div class="ce-example__statusbar-item">
<div class="ce-example__statusbar-button" id="enableThinModeButton">
<span data-toggled-text="Disable">Enable</span>
thin mode
</div>
</div>
<div class="ce-example__statusbar-item ce-example__statusbar-item--right">
<div class="ce-example__statusbar-toggler" id="darkThemeToggler">
</div>
</div>
</div>
</div>
@ -68,7 +92,7 @@
<script src="./tools/header/dist/bundle.js" onload="document.getElementById('hint-tools').hidden = true"></script><!-- Header -->
<script src="./tools/simple-image/dist/bundle.js"></script><!-- Image -->
<script src="./tools/delimiter/dist/bundle.js"></script><!-- Delimiter -->
<!-- <script src="./tools/list/dist/bundle.js"></script> List -->
<!-- <script src="./tools/list/dist/bundle.js"></script> List-->
<script src="./tools/nested-list/dist/nested-list.js"></script><!-- Nested List -->
<script src="./tools/checklist/dist/bundle.js"></script><!-- Checklist -->
<script src="./tools/quote/dist/bundle.js"></script><!-- Quote -->
@ -88,10 +112,10 @@
<!-- Initialization -->
<script>
/**
* To initialize the Editor, create a new instance with configuration object
* @see docs/installation.md for mode details
* Editor init config
* @see https://editorjs.io/configuration
*/
var editor = new EditorJS({
const editorConfig = {
/**
* Enable/Disable the read only mode
*/
@ -200,7 +224,7 @@
type: "header",
data: {
text: "Editor.js",
level: 2
level: 1
}
},
{
@ -215,7 +239,7 @@
id: "7ItVl5biRo",
data: {
text: "Key features",
level: 3
level: 2
}
},
{
@ -244,7 +268,7 @@
id: "QZFox1m_ul",
data: {
text: "What does it mean «block-styled editor»",
level: 3
level: 2
}
},
{
@ -266,7 +290,7 @@
id: "1sYMhUrznu",
data: {
text: "What does it mean clean data output",
level: 3
level: 2
}
},
{
@ -321,7 +345,12 @@
onChange: function(api, event) {
console.log('something changed', event);
},
});
}
/**
* To initialize the Editor, create a new instance with configuration object
* @see docs/installation.md for mode details
*/
var editor = new EditorJS(editorConfig);
/**
* Saving button
@ -355,6 +384,39 @@
readOnlyIndicator.textContent = readOnlyState ? 'On' : 'Off';
});
/**
* Button for displaying blocks borders. Useful for UI development
*/
const showBlocksBoundariesButton = document.getElementById("showBlocksBoundariesButton");
showBlocksBoundariesButton.addEventListener('click', () => {
document.body.classList.toggle("show-block-boundaries")
})
/**
* Button for enabling the 'Thin' mode
*/
const enableThinModeButton = document.getElementById("enableThinModeButton");
enableThinModeButton.addEventListener('click', () => {
document.body.classList.toggle("thin-mode")
editor.destroy();
editor = new EditorJS(editorConfig);
})
/**
* Toggler for toggling the dark mode
*/
const darkThemeToggler = document.getElementById("darkThemeToggler");
darkThemeToggler.addEventListener('click', () => {
document.body.classList.toggle("dark-mode");
localStorage.setItem('theme', document.body.classList.contains("dark-mode") ? 'dark' : 'default');
})
</script>
</body>
</html>

@ -1 +1 @@
Subproject commit f511dc066aab6bf82b4ffcd4cdee35ad71db8d36
Subproject commit 491ada2f1653de52cfaac6130244736f57f7afde

@ -1 +1 @@
Subproject commit e26b3e7c106486d2d776219e18cd125469991a25
Subproject commit e3df500fc62a88d3490fa4ba4030c07f0cd79d64

@ -0,0 +1 @@
Subproject commit 8375ae17756fa2677d57e716e12096437d01e8f8

View file

@ -1,6 +1,6 @@
{
"name": "@editorjs/editorjs",
"version": "2.23.0-rc.0",
"version": "2.23.0-rc.1",
"description": "Editor.js — Native JS, based on API and Open Source",
"main": "dist/editor.js",
"types": "./types/index.d.ts",
@ -22,8 +22,9 @@
"lint:fix": "eslint src/ --ext .ts --fix",
"lint:tests": "eslint test/ --ext .ts",
"svg": "svg-sprite-generate -d src/assets/ -o dist/sprite.svg",
"ci:pull_paragraph": "git submodule update --init ./src/tools/paragraph",
"pull_tools": "git submodule update --init --recursive",
"_tools:checkout": "git submodule foreach git checkout master",
"_tools:checkout": "git submodule foreach 'git checkout master || git checkout main'",
"_tools:pull": "git submodule foreach git pull",
"_tools:yarn": "git submodule foreach yarn",
"_tools:build": "git submodule foreach yarn build",
@ -49,6 +50,8 @@
"@codexteam/shortcuts": "^1.1.1",
"@cypress/code-coverage": "^3.9.2",
"@cypress/webpack-preprocessor": "^5.6.0",
"@editorjs/header": "^2.6.1",
"@editorjs/simple-image": "^1.4.1",
"@types/node": "^14.14.35",
"@types/webpack": "^4.41.12",
"@types/webpack-env": "^1.15.2",
@ -93,7 +96,7 @@
},
"dependencies": {
"codex-notifier": "^1.1.2",
"codex-tooltip": "^1.0.2",
"codex-tooltip": "^1.0.4",
"nanoid": "^3.1.22"
}
}

View file

@ -1,6 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8">
<circle cx="6.5" cy="1.5" r="1.5"/>
<circle cx="6.5" cy="6.5" r="1.5"/>
<circle cx="1.5" cy="1.5" r="1.5"/>
<circle cx="1.5" cy="6.5" r="1.5"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g transform="translate(4 1.5)" fill-rule="evenodd">
<circle cx="1.3" cy="1.3" r="1.3"/>
<circle cx="6.5" cy="1.3" r="1.3"/>
<circle cx="6.5" cy="6.5" r="1.3"/>
<circle cx="1.3" cy="6.5" r="1.3"/>
<circle cx="6.5" cy="11.7" r="1.3"/>
<circle cx="1.3" cy="11.7" r="1.3"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 218 B

After

Width:  |  Height:  |  Size: 372 B

View file

@ -1,3 +1,6 @@
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg">
<path d="M8.05 5.8h4.625a1.125 1.125 0 0 1 0 2.25H8.05v4.625a1.125 1.125 0 0 1-2.25 0V8.05H1.125a1.125 1.125 0 0 1 0-2.25H5.8V1.125a1.125 1.125 0 0 1 2.25 0V5.8z"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g transform="translate(1 1.5)" fill-rule="evenodd">
<rect x="6" width="2" height="13" rx="1"/>
<rect x=".5" y="5.5" width="13" height="2" rx="1"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 260 B

After

Width:  |  Height:  |  Size: 233 B

View file

@ -109,6 +109,8 @@ export default class MoveDownTune implements BlockTune {
/** Change blocks positions */
this.api.blocks.move(currentBlockIndex + 1);
this.api.toolbar.toggleBlockSettings(true);
/** Hide the Tooltip */
this.api.tooltip.hide();
}

View file

@ -117,6 +117,8 @@ export default class MoveUpTune implements BlockTune {
/** Change blocks positions */
this.api.blocks.move(currentBlockIndex - 1);
this.api.toolbar.toggleBlockSettings(true);
/** Hide the Tooltip */
this.api.tooltip.hide();
}

View file

@ -347,7 +347,7 @@ export default class Core {
eventsDispatcher: this.eventsDispatcher,
});
} catch (e) {
_.log(`Module ${Module.displayName} skipped because`, 'warn', e);
_.log(`Module ${Module.displayName} skipped because`, 'error', e);
}
});
}

View file

@ -612,4 +612,26 @@ export default class Dom {
public static isAnchor(element: Element): element is HTMLAnchorElement {
return element.tagName.toLowerCase() === 'a';
}
/**
* Return element's offset related to the document
*
* @todo handle case when editor initialized in scrollable popup
* @param el - element to compute offset
*/
public static offset(el): {top: number; left: number; right: number; bottom: number} {
const rect = el.getBoundingClientRect();
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const top = rect.top + scrollTop;
const left = rect.left + scrollLeft;
return {
top,
left,
bottom: top + rect.height,
right: left + rect.width,
};
}
}

View file

@ -25,6 +25,7 @@ export default class BlocksAPI extends Module {
getBlockByIndex: (index: number): BlockAPIInterface | void => this.getBlockByIndex(index),
getById: (id: string): BlockAPIInterface | null => this.getById(id),
getCurrentBlockIndex: (): number => this.getCurrentBlockIndex(),
getBlockIndex: (id: string): number => this.getBlockIndex(id),
getBlocksCount: (): number => this.getBlocksCount(),
stretchBlock: (index: number, status = true): void => this.stretchBlock(index, status),
insertNewBlock: (): void => this.insertNewBlock(),
@ -51,6 +52,24 @@ export default class BlocksAPI extends Module {
return this.Editor.BlockManager.currentBlockIndex;
}
/**
* Returns the index of Block by id;
*
* @param id - block id
* @returns {number}
*/
public getBlockIndex(id: string): number | undefined {
const block = this.Editor.BlockManager.getBlockById(id);
if (!block) {
_.logLabeled('There is no block with id `' + id + '`', 'warn');
return;
}
return this.Editor.BlockManager.getBlockIndex(block);
}
/**
* Returns BlockAPI object by Block index
*
@ -100,12 +119,6 @@ export default class BlocksAPI extends Module {
);
this.Editor.BlockManager.swap(fromIndex, toIndex);
/**
* Move toolbar
* DO not close the settings
*/
this.Editor.Toolbar.move(false);
}
/**
@ -116,12 +129,6 @@ export default class BlocksAPI extends Module {
*/
public move(toIndex: number, fromIndex?: number): void {
this.Editor.BlockManager.move(toIndex, fromIndex);
/**
* Move toolbar
* DO not close the settings
*/
this.Editor.Toolbar.move(false);
}
/**
@ -212,27 +219,32 @@ export default class BlocksAPI extends Module {
}
/**
* Insert new Block
* Insert new Block and returns it's API
*
* @param {string} type Tool name
* @param {BlockToolData} data Tool data to insert
* @param {ToolConfig} config Tool config
* @param {number?} index index where to insert new Block
* @param {boolean?} needToFocus - flag to focus inserted Block
* @param replace - pass true to replace the Block existed under passed index
*/
public insert = (
type: string = this.config.defaultBlock,
data: BlockToolData = {},
config: ToolConfig = {},
index?: number,
needToFocus?: boolean
): void => {
this.Editor.BlockManager.insert({
needToFocus?: boolean,
replace?: boolean
): BlockAPIInterface => {
const insertedBlock = this.Editor.BlockManager.insert({
tool: type,
data,
index,
needToFocus,
replace,
});
return new BlockAPI(insertedBlock);
}
/**

View file

@ -32,6 +32,7 @@ export default class API extends Module {
tooltip: this.Editor.TooltipAPI.methods,
i18n: this.Editor.I18nAPI.methods,
readOnly: this.Editor.ReadOnlyAPI.methods,
ui: this.Editor.UiAPI.methods,
};
}

View file

@ -13,21 +13,22 @@ export default class ListenersAPI extends Module {
*/
public get methods(): Listeners {
return {
on: (element: HTMLElement, eventType, handler, useCapture): void => this.on(element, eventType, handler, useCapture),
on: (element: HTMLElement, eventType, handler, useCapture): string => this.on(element, eventType, handler, useCapture),
off: (element, eventType, handler, useCapture): void => this.off(element, eventType, handler, useCapture),
offById: (id): void => this.offById(id),
};
}
/**
* adds DOM event listener
* Ads a DOM event listener. Return it's id.
*
* @param {HTMLElement} element - Element to set handler to
* @param {string} eventType - event type
* @param {() => void} handler - event handler
* @param {boolean} useCapture - capture event or not
*/
public on(element: HTMLElement, eventType: string, handler: () => void, useCapture?: boolean): void {
this.listeners.on(element, eventType, handler, useCapture);
public on(element: HTMLElement, eventType: string, handler: () => void, useCapture?: boolean): string {
return this.listeners.on(element, eventType, handler, useCapture);
}
/**
@ -41,4 +42,13 @@ export default class ListenersAPI extends Module {
public off(element: Element, eventType: string, handler: () => void, useCapture?: boolean): void {
this.listeners.off(element, eventType, handler, useCapture);
}
/**
* Removes DOM listener by the listener id
*
* @param id - id of the listener to remove
*/
public offById(id: string): void {
this.listeners.offById(id);
}
}

View file

@ -23,7 +23,7 @@ export default class ToolbarAPI extends Module {
* Open toolbar
*/
public open(): void {
this.Editor.Toolbar.open();
this.Editor.Toolbar.moveAndOpen();
}
/**
@ -48,16 +48,8 @@ export default class ToolbarAPI extends Module {
/** Check that opening state is set or not */
const canOpenBlockSettings = openingState ?? !this.Editor.BlockSettings.opened;
/** Check if state same as current state */
if (openingState === this.Editor.BlockSettings.opened) {
return;
}
if (canOpenBlockSettings) {
if (!this.Editor.Toolbar.opened) {
this.Editor.Toolbar.open(true, false);
this.Editor.Toolbar.plusButton.hide();
}
this.Editor.Toolbar.moveAndOpen();
this.Editor.BlockSettings.open();
} else {
this.Editor.BlockSettings.close();

View file

@ -1,10 +1,8 @@
import { Tooltip as ITooltip } from '../../../../types/api';
import { TooltipContent, TooltipOptions } from 'codex-tooltip';
import type { TooltipOptions, TooltipContent } from 'codex-tooltip/types';
import Module from '../../__module';
import { ModuleConfig } from '../../../types-internal/module-config';
import Tooltip from '../../utils/tooltip';
import EventsDispatcher from '../../utils/events';
import { EditorConfig } from '../../../../types';
/**
* @class TooltipAPI
* @classdesc Tooltip API
@ -16,9 +14,9 @@ export default class TooltipAPI extends Module {
private tooltip: Tooltip;
/**
* @class
* @param {object} moduleConfiguration - Module Configuration
* @param {EditorConfig} moduleConfiguration.config - Editor's config
* @param {EventsDispatcher} moduleConfiguration.eventsDispatcher - Editor's event dispatcher
* @param moduleConfiguration - Module Configuration
* @param moduleConfiguration.config - Editor's config
* @param moduleConfiguration.eventsDispatcher - Editor's event dispatcher
*/
constructor({ config, eventsDispatcher }: ModuleConfig) {
super({

View file

@ -0,0 +1,36 @@
import Module from '../../__module';
import { Ui, UiNodes } from '../../../../types/api';
/**
* API module allowing to access some Editor UI elements
*/
export default class UiAPI extends Module {
/**
* Available methods / getters
*/
public get methods(): Ui {
return {
nodes: this.editorNodes,
/**
* There can be added some UI methods, like toggleThinMode() etc
*/
};
}
/**
* Exported classes
*/
private get editorNodes(): UiNodes {
return {
/**
* Top-level editor instance wrapper
*/
wrapper: this.Editor.UI.nodes.wrapper,
/**
* Element that holds all the Blocks
*/
redactor: this.Editor.UI.nodes.redactor,
};
}
}

View file

@ -125,9 +125,10 @@ export default class BlockEvents extends Module {
return;
}
const canOpenToolbox = currentBlock.tool.isDefault && currentBlock.isEmpty;
const conversionToolbarOpened = !currentBlock.isEmpty && ConversionToolbar.opened;
const inlineToolbarOpened = !currentBlock.isEmpty && !SelectionUtils.isCollapsed && InlineToolbar.opened;
const isEmptyBlock = currentBlock.isEmpty;
const canOpenToolbox = currentBlock.tool.isDefault && isEmptyBlock;
const conversionToolbarOpened = !isEmptyBlock && ConversionToolbar.opened;
const inlineToolbarOpened = !isEmptyBlock && !SelectionUtils.isCollapsed && InlineToolbar.opened;
/**
* For empty Blocks we show Plus button via Toolbox only for default Blocks
@ -255,19 +256,9 @@ export default class BlockEvents extends Module {
this.Editor.Caret.setToBlock(newCurrent);
/**
* If new Block is empty
* Show Toolbar
*/
if (newCurrent.tool.isDefault && newCurrent.isEmpty) {
/**
* Show Toolbar
*/
this.Editor.Toolbar.open(false);
/**
* Show Plus Button
*/
this.Editor.Toolbar.plusButton.show();
}
this.Editor.Toolbar.moveAndOpen(newCurrent);
event.preventDefault();
}
@ -505,7 +496,7 @@ export default class BlockEvents extends Module {
* @param {KeyboardEvent} event - keyboard event
*/
private needToolbarClosing(event: KeyboardEvent): boolean {
const toolboxItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.Toolbox.opened),
const toolboxItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.Toolbar.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),
@ -531,11 +522,10 @@ export default class BlockEvents extends Module {
*/
private activateToolbox(): void {
if (!this.Editor.Toolbar.opened) {
this.Editor.Toolbar.open(false, false);
this.Editor.Toolbar.plusButton.show();
}
this.Editor.Toolbar.moveAndOpen();
} // else Flipper will leaf through it
this.Editor.Toolbox.open();
this.Editor.Toolbar.toolbox.open();
}
/**
@ -544,8 +534,7 @@ export default class BlockEvents extends Module {
private activateBlockSettings(): void {
if (!this.Editor.Toolbar.opened) {
this.Editor.BlockManager.currentBlock.focused = true;
this.Editor.Toolbar.open(true, false);
this.Editor.Toolbar.plusButton.hide();
this.Editor.Toolbar.moveAndOpen();
}
/**
@ -553,6 +542,11 @@ export default class BlockEvents extends Module {
* Next Tab press will leaf Settings Buttons
*/
if (!this.Editor.BlockSettings.opened) {
/**
* @todo Debug the case when we set caret to some block, hovering another block
* wrong settings will be opened.
* To fix it, we should refactor the Block Settings module make it a standalone class, like the Toolbox
*/
this.Editor.BlockSettings.open();
}
}

View file

@ -75,6 +75,15 @@ export default class BlockManager extends Module {
return this._blocks[this.currentBlockIndex];
}
/**
* Set passed Block as a current
*
* @param block - block to set as a current
*/
public set currentBlock(block: Block) {
this.currentBlockIndex = this.getBlockIndex(block);
}
/**
* Returns next Block instance
*
@ -549,11 +558,15 @@ export default class BlockManager extends Module {
/**
* Returns Block by passed index
*
* @param {number} index - index to get
* @param {number} index - index to get. -1 to get last
*
* @returns {Block}
*/
public getBlockByIndex(index): Block {
if (index === -1) {
index = this._blocks.length - 1;
}
return this._blocks[index];
}
@ -746,7 +759,7 @@ export default class BlockManager extends Module {
/**
* Sets current Block Index -1 which means unknown
* and clear highlightings
* and clear highlights
*/
public dropPointer(): void {
this.currentBlockIndex = -1;

View file

@ -10,6 +10,7 @@ import $ from '../dom';
import SelectionUtils from '../selection';
import Block from '../block';
import * as _ from '../utils';
/**
*
@ -185,17 +186,21 @@ export default class RectangleSelection extends Module {
this.processMouseDown(mouseEvent);
}, false);
this.listeners.on(document.body, 'mousemove', (mouseEvent: MouseEvent) => {
this.listeners.on(document.body, 'mousemove', _.throttle((mouseEvent: MouseEvent) => {
this.processMouseMove(mouseEvent);
}, false);
}, 10), {
passive: true,
});
this.listeners.on(document.body, 'mouseleave', () => {
this.processMouseLeave();
});
this.listeners.on(window, 'scroll', (mouseEvent: MouseEvent) => {
this.listeners.on(window, 'scroll', _.throttle((mouseEvent: MouseEvent) => {
this.processScroll(mouseEvent);
}, false);
}, 10), {
passive: true,
});
this.listeners.on(document.body, 'mouseup', () => {
this.processMouseUp();
@ -252,6 +257,7 @@ export default class RectangleSelection extends Module {
* Handle mouse up
*/
private processMouseUp(): void {
this.clearSelection();
this.endSelection();
}
@ -356,6 +362,11 @@ export default class RectangleSelection extends Module {
this.updateRectangleSize();
/**
* Hide Block Settings Toggler (along with the Toolbar) (if showed) when the Rectangle Selection is activated
*/
this.Editor.Toolbar.close();
if (index === undefined) {
return;
}

View file

@ -3,6 +3,7 @@ import $ from '../../dom';
import Flipper, { FlipperOptions } from '../../flipper';
import * as _ from '../../utils';
import SelectionUtils from '../../selection';
import Block from '../../block';
/**
* HTML Elements that used for BlockSettings
@ -23,6 +24,8 @@ interface BlockSettingsNodes {
* | . Default Settings . |
* | ...................... |
* |________________________|
*
* @todo Make Block Settings no-module but a standalone class, like Toolbox
*/
export default class BlockSettings extends Module<BlockSettingsNodes> {
/**
@ -120,8 +123,10 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
/**
* Open Block Settings pane
*
* @param targetBlock - near which Block we should open BlockSettings
*/
public open(): void {
public open(targetBlock: Block = this.Editor.BlockManager.currentBlock): void {
this.nodes.wrapper.classList.add(this.CSS.wrapperOpened);
/**
@ -133,18 +138,18 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
/**
* Highlight content of a Block we are working with
*/
this.Editor.BlockManager.currentBlock.selected = true;
targetBlock.selected = true;
this.Editor.BlockSelection.clearCache();
/**
* Fill Tool's settings
*/
this.addToolSettings();
this.addToolSettings(targetBlock);
/**
* Add default settings that presents for all Blocks
*/
this.addTunes();
this.addTunes(targetBlock);
/** Tell to subscribers that block settings is opened */
this.eventsDispatcher.emit(this.events.opened);
@ -227,9 +232,11 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
/**
* Add Tool's settings
*
* @param targetBlock - Block to render settings
*/
private addToolSettings(): void {
const settingsElement = this.Editor.BlockManager.currentBlock.renderSettings();
private addToolSettings(targetBlock): void {
const settingsElement = targetBlock.renderSettings();
if (settingsElement) {
$.append(this.nodes.toolSettings, settingsElement);
@ -238,9 +245,11 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
/**
* Add tunes: provided by user and default ones
*
* @param targetBlock - Block to render its Tunes set
*/
private addTunes(): void {
const [toolTunes, defaultTunes] = this.Editor.BlockManager.currentBlock.renderTunes();
private addTunes(targetBlock): void {
const [toolTunes, defaultTunes] = targetBlock.renderTunes();
$.append(this.nodes.toolSettings, toolTunes);
$.append(this.nodes.defaultSettings, defaultTunes);

View file

@ -17,6 +17,8 @@ interface ConversionToolbarNodes {
/**
* Block Converter
*
* @todo Make the Conversion Toolbar no-module but a standalone class, like Toolbox
*/
export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
/**

View file

@ -5,8 +5,25 @@ import I18n from '../../i18n';
import { I18nInternalNS } from '../../i18n/namespace-internal';
import Tooltip from '../../utils/tooltip';
import { ModuleConfig } from '../../../types-internal/module-config';
import { EditorConfig } from '../../../../types';
import SelectionUtils from '../../selection';
import { BlockAPI } from '../../../../types';
import Block from '../../block';
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 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
* @todo TESTCASE - Click inside Redactor zone when Block Settings are opened:
* - should close Block Settings
* - should not close Toolbar
* - should move Toolbar to the clicked Block
* @todo TESTCASE - Toolbar should be closed on the Cross Block Selection
* @todo TESTCASE - Toolbar should be closed on the Rectangle Selection
* @todo TESTCASE - If Block Settings or Toolbox are opened, the Toolbar should not be moved by Bocks hovering
*/
/**
* HTML Elements used for Toolbar UI
@ -80,11 +97,22 @@ export default class Toolbar extends Module<ToolbarNodes> {
* Tooltip utility Instance
*/
private tooltip: Tooltip;
/**
* Block near which we display the Toolbox
*/
private hoveredBlock: Block;
/**
* Toolbox class instance
*/
private toolboxInstance: Toolbox;
/**
* @class
* @param {object} moduleConfiguration - Module Configuration
* @param {EditorConfig} moduleConfiguration.config - Editor's config
* @param {EventsDispatcher} moduleConfiguration.eventsDispatcher - Editor's event dispatcher
* @param moduleConfiguration - Module Configuration
* @param moduleConfiguration.config - Editor's config
* @param moduleConfiguration.eventsDispatcher - Editor's event dispatcher
*/
constructor({ config, eventsDispatcher }: ModuleConfig) {
super({
@ -107,6 +135,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
actionsOpened: 'ce-toolbar__actions--opened',
toolbarOpened: 'ce-toolbar--opened',
openedToolboxHolderModifier: 'codex-editor--toolbox-opened',
// Content Zone
plusButton: 'ce-toolbar__plus',
@ -137,7 +166,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
return {
hide: (): void => this.nodes.plusButton.classList.add(this.CSS.plusButtonHidden),
show: (): void => {
if (this.Editor.Toolbox.isEmpty) {
if (this.toolboxInstance.isEmpty) {
return;
}
this.nodes.plusButton.classList.remove(this.CSS.plusButtonHidden);
@ -145,6 +174,32 @@ export default class Toolbar extends Module<ToolbarNodes> {
};
}
/**
* Public interface for accessing the Toolbox
*/
public get toolbox(): {
opened: boolean;
close: () => void;
open: () => void;
toggle: () => void;
flipperHasFocus: boolean;
} {
return {
opened: this.toolboxInstance.opened,
close: (): void => this.toolboxInstance.close(),
open: (): void => {
/**
* Set current block to cover the case when the Toolbar showed near hovered Block but caret is set to another Block.
*/
this.Editor.BlockManager.currentBlock = this.hoveredBlock;
this.toolboxInstance.open();
},
toggle: (): void => this.toolboxInstance.toggle(),
flipperHasFocus: this.toolboxInstance.flipperHasFocus,
};
}
/**
* Block actions appearance manipulations
*
@ -172,54 +227,80 @@ export default class Toolbar extends Module<ToolbarNodes> {
this.enableModuleBindings();
} else {
this.destroy();
this.Editor.Toolbox.destroy();
this.toolboxInstance.destroy();
this.Editor.BlockSettings.destroy();
this.disableModuleBindings();
}
}
/**
* Move Toolbar to the Current Block
* Move Toolbar to the passed (or current) Block
*
* @param {boolean} forceClose - force close Toolbar Settings and Toolbar
* @param block - block to move Toolbar near it
*/
public move(forceClose = true): void {
if (forceClose) {
/** Close Toolbox when we move toolbar */
this.Editor.Toolbox.close();
this.Editor.BlockSettings.close();
}
const currentBlock = this.Editor.BlockManager.currentBlock.holder;
public moveAndOpen(block: Block = this.Editor.BlockManager.currentBlock): void {
/**
* Close Toolbox when we move toolbar
*/
this.toolboxInstance.close();
this.Editor.BlockSettings.close();
/**
* If no one Block selected as a Current
*/
if (!currentBlock) {
if (!block) {
return;
}
this.hoveredBlock = block;
const targetBlockHolder = block.holder;
const { isMobile } = this.Editor.UI;
const blockHeight = currentBlock.offsetHeight;
let toolbarY = currentBlock.offsetTop;
const renderedContent = block.pluginsContent;
const renderedContentStyle = window.getComputedStyle(renderedContent);
const blockRenderedElementPaddingTop = parseInt(renderedContentStyle.paddingTop, 10);
const blockHeight = targetBlockHolder.offsetHeight;
let toolbarY;
/**
* 1) On desktop Toolbar at the top of Block, Plus/Toolbox moved the center of Block
* 2) On mobile Toolbar at the bottom of Block
* On mobile Toolbar at the bottom of Block
* On Desktop Toolbar should be moved to the first line of block text
* To do that, we compute the block offset and the padding-top of the plugin content
*/
if (!isMobile) {
const contentOffset = Math.floor(blockHeight / 2);
this.nodes.plusButton.style.transform = `translate3d(0, calc(${contentOffset}px - 50%), 0)`;
this.Editor.Toolbox.nodes.toolbox.style.transform = `translate3d(0, calc(${contentOffset}px - 50%), 0)`;
if (isMobile) {
toolbarY = targetBlockHolder.offsetTop + blockHeight;
} else {
toolbarY += blockHeight;
toolbarY = targetBlockHolder.offsetTop + blockRenderedElementPaddingTop;
}
/**
* Move Toolbar to the Top coordinate of Block
*/
this.nodes.wrapper.style.transform = `translate3D(0, ${Math.floor(toolbarY)}px, 0)`;
/**
* Plus Button should be shown only for __empty__ __default__ block
*/
if (block.tool.isDefault && block.isEmpty) {
this.plusButton.show();
} else {
this.plusButton.hide();
}
this.open();
}
/**
* Close the Toolbar
*/
public close(): void {
this.nodes.wrapper.classList.remove(this.CSS.toolbarOpened);
/** Close components */
this.blockActions.hide();
this.toolboxInstance.close();
this.Editor.BlockSettings.close();
}
/**
@ -232,9 +313,8 @@ export default class Toolbar extends Module<ToolbarNodes> {
* with closing Toolbox and Block Settings
* This flag allows to open Toolbar with Toolbox
*/
public open(withBlockActions = true, needToCloseToolbox = true): void {
private open(withBlockActions = true, needToCloseToolbox = true): void {
_.delay(() => {
this.move(needToCloseToolbox);
this.nodes.wrapper.classList.add(this.CSS.toolbarOpened);
if (withBlockActions) {
@ -245,18 +325,6 @@ export default class Toolbar extends Module<ToolbarNodes> {
}, 50)();
}
/**
* Close the Toolbar
*/
public close(): void {
this.nodes.wrapper.classList.remove(this.CSS.toolbarOpened);
/** Close components */
this.blockActions.hide();
this.Editor.Toolbox.close();
this.Editor.BlockSettings.close();
}
/**
* Draws Toolbar elements
*/
@ -282,10 +350,11 @@ export default class Toolbar extends Module<ToolbarNodes> {
* - Toolbox
*/
this.nodes.plusButton = $.make('div', this.CSS.plusButton);
$.append(this.nodes.plusButton, $.svg('plus', 14, 14));
$.append(this.nodes.content, this.nodes.plusButton);
$.append(this.nodes.plusButton, $.svg('plus', 16, 16));
$.append(this.nodes.actions, this.nodes.plusButton);
this.readOnlyMutableListeners.on(this.nodes.plusButton, 'click', () => {
this.tooltip.hide(true);
this.plusButtonClicked();
}, false);
@ -299,7 +368,9 @@ export default class Toolbar extends Module<ToolbarNodes> {
textContent: '⇥ Tab',
}));
this.tooltip.onHover(this.nodes.plusButton, tooltipContent);
this.tooltip.onHover(this.nodes.plusButton, tooltipContent, {
hidingDelay: 400,
});
/**
* Fill Actions Zone:
@ -309,7 +380,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
*/
this.nodes.blockActionsButtons = $.make('div', this.CSS.blockActionsButtons);
this.nodes.settingsToggler = $.make('span', this.CSS.settingsToggler);
const settingsIcon = $.svg('dots', 8, 8);
const settingsIcon = $.svg('dots', 16, 16);
$.append(this.nodes.settingsToggler, settingsIcon);
$.append(this.nodes.blockActionsButtons, this.nodes.settingsToggler);
@ -319,14 +390,14 @@ export default class Toolbar extends Module<ToolbarNodes> {
this.nodes.settingsToggler,
I18n.ui(I18nInternalNS.ui.blockTunes.toggler, 'Click to tune'),
{
placement: 'top',
hidingDelay: 400,
}
);
/**
* Appending Toolbar components to itself
*/
$.append(this.nodes.content, this.Editor.Toolbox.nodes.toolbox);
$.append(this.nodes.content, this.makeToolbox());
$.append(this.nodes.actions, this.Editor.BlockSettings.nodes.wrapper);
/**
@ -335,11 +406,57 @@ export default class Toolbar extends Module<ToolbarNodes> {
$.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper);
}
/**
* Creates the Toolbox instance and return it's rendered element
*/
private makeToolbox(): Element {
/**
* Make the Toolbox
*/
this.toolboxInstance = new Toolbox({
api: this.Editor.API.methods,
tools: this.Editor.Tools.blockTools,
});
this.toolboxInstance.on(ToolboxEvent.Opened, () => {
this.Editor.UI.nodes.wrapper.classList.add(this.CSS.openedToolboxHolderModifier);
});
this.toolboxInstance.on(ToolboxEvent.Closed, () => {
this.Editor.UI.nodes.wrapper.classList.remove(this.CSS.openedToolboxHolderModifier);
});
this.toolboxInstance.on(ToolboxEvent.BlockAdded, ({ block }: {block: BlockAPI }) => {
const { BlockManager, Caret } = this.Editor;
const newBlock = BlockManager.getBlockById(block.id);
/**
* If the new block doesn't contain inputs, insert the new paragraph below
*/
if (newBlock.inputs.length === 0) {
if (newBlock === BlockManager.lastBlock) {
BlockManager.insertAtEnd();
Caret.setToBlock(BlockManager.lastBlock);
} else {
Caret.setToBlock(BlockManager.nextBlock);
}
}
});
return this.toolboxInstance.make();
}
/**
* Handler for Plus Button
*/
private plusButtonClicked(): void {
this.Editor.Toolbox.toggle();
/**
* We need to update Current Block because user can click on the Plus Button (thanks to appearing by hover) without any clicks on editor
* In this case currentBlock will point last block
*/
this.Editor.BlockManager.currentBlock = this.hoveredBlock;
this.toolboxInstance.toggle();
}
/**
@ -360,7 +477,25 @@ export default class Toolbar extends Module<ToolbarNodes> {
e.stopPropagation();
this.settingsTogglerClicked();
this.toolboxInstance.close();
this.tooltip.hide(true);
}, true);
/**
* Subscribe to the 'block-hovered' event
*/
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);
});
}
/**
@ -374,10 +509,16 @@ export default class Toolbar extends Module<ToolbarNodes> {
* Clicks on the Block Settings toggler
*/
private settingsTogglerClicked(): void {
/**
* We need to update Current Block because user can click on toggler (thanks to appearing by hover) without any clicks on editor
* In this case currentBlock will point last block
*/
this.Editor.BlockManager.currentBlock = this.hoveredBlock;
if (this.Editor.BlockSettings.opened) {
this.Editor.BlockSettings.close();
} else {
this.Editor.BlockSettings.open();
this.Editor.BlockSettings.open(this.hoveredBlock);
}
}
@ -385,7 +526,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
* Draws Toolbar UI
*
* Toolbar contains BlockSettings and Toolbox.
* Thats why at first we draw its components and then Toolbar itself
* That's why at first we draw its components and then Toolbar itself
*
* Steps:
* - Make Toolbar dependent components like BlockSettings, Toolbox and so on
@ -398,11 +539,6 @@ export default class Toolbar extends Module<ToolbarNodes> {
*/
this.Editor.BlockSettings.make();
/**
* Make Toolbox
*/
this.Editor.Toolbox.make();
/**
* Make Toolbar
*/
@ -415,6 +551,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
*/
private destroy(): void {
this.removeAllNodes();
this.toolboxInstance.destroy();
this.tooltip.destroy();
}
}

View file

@ -2,7 +2,7 @@ import Module from '../../__module';
import $ from '../../dom';
import SelectionUtils from '../../selection';
import * as _ from '../../utils';
import { InlineTool as IInlineTool, EditorConfig } from '../../../../types';
import { InlineTool as IInlineTool } from '../../../../types';
import Flipper from '../../flipper';
import I18n from '../../i18n';
import { I18nInternalNS } from '../../i18n/namespace-internal';
@ -100,9 +100,9 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
private tooltip: Tooltip;
/**
* @class
* @param {object} moduleConfiguration - Module Configuration
* @param {EditorConfig} moduleConfiguration.config - Editor's config
* @param {EventsDispatcher} moduleConfiguration.eventsDispatcher - Editor's event dispatcher
* @param moduleConfiguration - Module Configuration
* @param moduleConfiguration.config - Editor's config
* @param moduleConfiguration.eventsDispatcher - Editor's event dispatcher
*/
constructor({ config, eventsDispatcher }: ModuleConfig) {
super({

View file

@ -46,6 +46,15 @@ interface UINodes {
* @property {Element} nodes.redactor - <ce-redactor>
*/
export default class UI extends Module<UINodes> {
/**
* Events could be emitted by this module.
*/
public get events(): { blockHovered: string } {
return {
blockHovered: 'block-hovered',
};
}
/**
* Editor.js UI CSS class names
*
@ -209,15 +218,23 @@ export default class UI extends Module<UINodes> {
* @returns {boolean}
*/
public get someToolbarOpened(): boolean {
const { Toolbox, BlockSettings, InlineToolbar, ConversionToolbar } = this.Editor;
const { Toolbar, BlockSettings, InlineToolbar, ConversionToolbar } = this.Editor;
return BlockSettings.opened || InlineToolbar.opened || ConversionToolbar.opened || Toolbox.opened;
return BlockSettings.opened || InlineToolbar.opened || ConversionToolbar.opened || Toolbar.toolbox.opened;
}
/**
* Check for some Flipper-buttons is under focus
*/
public get someFlipperButtonFocused(): boolean {
/**
* Toolbar has internal module (Toolbox) that has own Flipper,
* so we check it manually
*/
if (this.Editor.Toolbar.toolbox.flipperHasFocus) {
return true;
}
return Object.entries(this.Editor).filter(([moduleName, moduleClass]) => {
return moduleClass.flipper instanceof Flipper;
})
@ -237,12 +254,12 @@ export default class UI extends Module<UINodes> {
* Close all Editor's toolbars
*/
public closeAllToolbars(): void {
const { Toolbox, BlockSettings, InlineToolbar, ConversionToolbar } = this.Editor;
const { Toolbar, BlockSettings, InlineToolbar, ConversionToolbar } = this.Editor;
BlockSettings.close();
InlineToolbar.close();
ConversionToolbar.close();
Toolbox.close();
Toolbar.toolbox.close();
}
/**
@ -356,6 +373,48 @@ export default class UI extends Module<UINodes> {
}, {
passive: true,
});
/**
* Start watching 'block-hovered' events that is used by Toolbar for moving
*/
this.watchBlockHoveredEvents();
}
/**
* Listen redactor mousemove to emit 'block-hovered' event
*/
private watchBlockHoveredEvents(): void {
/**
* Used to not to emit the same block multiple times to the 'block-hovered' event on every mousemove
*/
let blockHoveredEmitted;
this.readOnlyMutableListeners.on(this.nodes.redactor, 'mousemove', _.throttle((event: MouseEvent | TouchEvent) => {
const hoveredBlock = (event.target as Element).closest('.ce-block');
/**
* Do not trigger 'block-hovered' for cross-block selection
*/
if (this.Editor.BlockSelection.anyBlockSelected) {
return;
}
if (!hoveredBlock) {
return;
}
if (blockHoveredEmitted === hoveredBlock) {
return;
}
blockHoveredEmitted = hoveredBlock;
this.eventsDispatcher.emit(this.events.blockHovered, {
block: this.Editor.BlockManager.getBlockByChildNode(hoveredBlock),
});
}, 20), {
passive: true,
});
}
/**
@ -483,8 +542,8 @@ export default class UI extends Module<UINodes> {
*/
this.Editor.BlockSelection.clearSelection(event);
if (this.Editor.Toolbox.opened) {
this.Editor.Toolbox.close();
if (this.Editor.Toolbar.toolbox.opened) {
this.Editor.Toolbar.toolbox.close();
} else if (this.Editor.BlockSettings.opened) {
this.Editor.BlockSettings.close();
} else if (this.Editor.ConversionToolbar.opened) {
@ -548,8 +607,7 @@ export default class UI extends Module<UINodes> {
/**
* Move toolbar and show plus button because new Block is empty
*/
this.Editor.Toolbar.move();
this.Editor.Toolbar.plusButton.show();
this.Editor.Toolbar.moveAndOpen(newBlock);
}
this.Editor.BlockSelection.clearSelection(event);
@ -577,15 +635,31 @@ export default class UI extends Module<UINodes> {
if (!clickedInsideOfEditor) {
/**
* Clear highlightings and pointer on BlockManager
* Clear highlights 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.ConversionToolbar.close();
}
/**
* If Block Settings opened, close them by click on document.
*
* But allow clicking inside Block Settings.
* Also, do not process clicks on the Block Settings Toggler, because it has own click listener
*/
const isClickedInsideBlockSettings = this.Editor.BlockSettings.nodes.wrapper.contains(target);
const isClickedInsideBlockSettingsToggler = this.Editor.Toolbar.nodes.settingsToggler.contains(target);
const doNotProcess = isClickedInsideBlockSettings || isClickedInsideBlockSettingsToggler;
if (this.Editor.BlockSettings.opened && !doNotProcess) {
this.Editor.BlockSettings.close();
const clickedBlock = this.Editor.BlockManager.getBlockByChildNode(target);
this.Editor.Toolbar.moveAndOpen(clickedBlock);
}
/**
@ -609,7 +683,7 @@ export default class UI extends Module<UINodes> {
let clickedNode = event.target as HTMLElement;
/**
* If click was fired is on Editor`s wrapper, try to get clicked node by elementFromPoint method
* If click was fired on Editor`s wrapper, try to get clicked node by elementFromPoint method
*/
if (clickedNode === this.nodes.redactor) {
const clientX = event instanceof MouseEvent ? event.clientX : event.touches[0].clientX;
@ -642,13 +716,9 @@ export default class UI extends Module<UINodes> {
/**
* Move and open toolbar
* (used for showing Block Settings toggler after opening and closing Inline Toolbar)
*/
this.Editor.Toolbar.open();
/**
* Hide the Plus Button
*/
this.Editor.Toolbar.plusButton.hide();
this.Editor.Toolbar.moveAndOpen();
}
/**
@ -691,12 +761,21 @@ export default class UI extends Module<UINodes> {
return;
}
const lastBlock = this.Editor.BlockManager.getBlockByIndex(-1);
const lastBlockBottomCoord = $.offset(lastBlock.holder).bottom;
const clickedCoord = event.pageY;
const isClickedBottom = event.target instanceof Element &&
event.target.isEqualNode(this.nodes.redactor) &&
/**
* If there is cross block selection started, target will be equal to redactor so we need additional check
*/
!BlockSelection.anyBlockSelected;
!BlockSelection.anyBlockSelected &&
/**
* Prevent caret jumping (to last block) when clicking between blocks
*/
lastBlockBottomCoord < clickedCoord;
if (isClickedBottom) {
stopPropagation();
@ -717,27 +796,7 @@ export default class UI extends Module<UINodes> {
* Set the caret and toolbar to empty Block
*/
Caret.setToTheLastBlock();
Toolbar.move();
}
/**
* Show the Plus Button if:
* - Block is an default-block (Text)
* - Block is empty
*/
const isDefaultBlock = this.Editor.BlockManager.currentBlock.tool.isDefault;
if (isDefaultBlock) {
stopPropagation();
/**
* 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();
}
Toolbar.moveAndOpen(BlockManager.lastBlock);
}
}

View file

@ -1,61 +1,44 @@
import Module from '../../__module';
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 { ModuleConfig } from '../../../types-internal/module-config';
import EventsDispatcher from '../../utils/events';
import BlockTool from '../../tools/block';
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';
/**
* HTMLElements used for Toolbox UI
* Event that can be triggered by the Toolbox
*/
interface ToolboxNodes {
toolbox: HTMLElement;
buttons: HTMLElement[];
export enum ToolboxEvent {
/**
* When the Toolbox is opened
*/
Opened = 'toolbox-opened',
/**
* When the Toolbox is closed
*/
Closed = 'toolbox-closed',
/**
* When the new Block added by Toolbox
*/
BlockAdded = 'toolbox-block-added',
}
/**
* @class Toolbox
* @classdesc Holder for Tools
*
* @typedef {Toolbox} Toolbox
* @property {boolean} opened - opening state
* @property {object} nodes - Toolbox nodes
* @property {object} CSS - CSS class names
* Toolbox
* This UI element contains list of Block Tools available to be inserted
* It appears after click on the Plus Button
*
* @implements {EventsDispatcher} with some events, see {@link ToolboxEvent}
*/
export default class Toolbox extends Module<ToolboxNodes> {
/**
* Current module HTML Elements
*/
public nodes = {
toolbox: null,
buttons: [],
}
/**
* CSS styles
*
* @returns {object.<string, string>}
*/
public get CSS(): { [name: string]: string } {
return {
toolbox: 'ce-toolbox',
toolboxButton: 'ce-toolbox__button',
toolboxButtonActive: 'ce-toolbox__button--active',
toolboxOpened: 'ce-toolbox--opened',
openedToolbarHolderModifier: 'codex-editor--toolbox-opened',
buttonTooltip: 'ce-toolbox-button-tooltip',
buttonShortcut: 'ce-toolbox-button-tooltip__shortcut',
};
}
export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
/**
* Returns True if Toolbox is Empty and nothing to show
*
@ -72,6 +55,44 @@ export default class Toolbox extends Module<ToolboxNodes> {
*/
public opened = false;
/**
* Editor API
*/
private api: API;
/**
* List of Tools available. Some of them will be shown in the Toolbox
*/
private tools: ToolsCollection<BlockTool>;
/**
* Current module HTML Elements
*/
private nodes: {
toolbox: HTMLElement;
buttons: HTMLElement[];
} = {
toolbox: null,
buttons: [],
}
/**
* CSS styles
*
* @returns {object.<string, string>}
*/
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',
};
}
/**
* How many tools displayed in Toolbox
*
@ -90,34 +111,53 @@ export default class Toolbox extends Module<ToolboxNodes> {
* Tooltip utility Instance
*/
private tooltip: Tooltip;
/**
* @class
* @param {object} moduleConfiguration - Module Configuration
* @param {EditorConfig} moduleConfiguration.config - Editor's config
* @param {EventsDispatcher} moduleConfiguration.eventsDispatcher - Editor's event dispatcher
* Id of listener added used to remove it on destroy()
*/
constructor({ config, eventsDispatcher }: ModuleConfig) {
super({
config,
eventsDispatcher,
});
private clickListenerId: string = null;
/**
* Toolbox constructor
*
* @param options - available parameters
* @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 }) {
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;
}
/**
* Makes the Toolbox
*/
public make(): void {
this.nodes.toolbox = $.make('div', this.CSS.toolbox);
public make(): Element {
this.nodes.toolbox = $.make('div', Toolbox.CSS.toolbox);
this.addTools();
this.enableFlipper();
return this.nodes.toolbox;
}
/**
* Destroy Module
*/
public destroy(): void {
super.destroy();
/**
* Sometimes (in read-only mode) there is no Flipper
*/
@ -126,7 +166,14 @@ export default class Toolbox extends Module<ToolboxNodes> {
this.flipper = null;
}
this.removeAllNodes();
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();
}
@ -149,8 +196,9 @@ export default class Toolbox extends Module<ToolboxNodes> {
return;
}
this.Editor.UI.nodes.wrapper.classList.add(this.CSS.openedToolbarHolderModifier);
this.nodes.toolbox.classList.add(this.CSS.toolboxOpened);
this.emit(ToolboxEvent.Opened);
this.nodes.toolbox.classList.add(Toolbox.CSS.toolboxOpened);
this.opened = true;
this.flipper.activate();
@ -160,8 +208,9 @@ export default class Toolbox extends Module<ToolboxNodes> {
* Close Toolbox
*/
public close(): void {
this.nodes.toolbox.classList.remove(this.CSS.toolboxOpened);
this.Editor.UI.nodes.wrapper.classList.remove(this.CSS.openedToolbarHolderModifier);
this.emit(ToolboxEvent.Closed);
this.nodes.toolbox.classList.remove(Toolbox.CSS.toolboxOpened);
this.opened = false;
this.flipper.deactivate();
@ -182,10 +231,8 @@ export default class Toolbox extends Module<ToolboxNodes> {
* Iterates available tools and appends them to the Toolbox
*/
private addTools(): void {
const tools = this.Editor.Tools.blockTools;
Array
.from(tools.values())
.from(this.tools.values())
.forEach((tool) => this.addTool(tool));
}
@ -218,7 +265,7 @@ export default class Toolbox extends Module<ToolboxNodes> {
// return;
// }
const button = $.make('li', [ this.CSS.toolboxButton ]);
const button = $.make('li', [ Toolbox.CSS.toolboxButton ]);
button.dataset.tool = tool.name;
button.innerHTML = toolToolboxSettings.icon;
@ -231,7 +278,7 @@ export default class Toolbox extends Module<ToolboxNodes> {
/**
* Add click listener
*/
this.listeners.on(button, 'click', (event: KeyboardEvent|MouseEvent) => {
this.clickListenerId = this.api.listeners.on(button, 'click', (event: KeyboardEvent|MouseEvent) => {
this.toolButtonActivate(event, tool.name);
});
@ -267,7 +314,7 @@ export default class Toolbox extends Module<ToolboxNodes> {
let shortcut = tool.shortcut;
const tooltip = $.make('div', this.CSS.buttonTooltip);
const tooltip = $.make('div', Toolbox.CSS.buttonTooltip);
const hint = document.createTextNode(_.capitalize(name));
tooltip.appendChild(hint);
@ -275,7 +322,7 @@ export default class Toolbox extends Module<ToolboxNodes> {
if (shortcut) {
shortcut = _.beautifyShortcut(shortcut);
tooltip.appendChild($.make('div', this.CSS.buttonShortcut, {
tooltip.appendChild($.make('div', Toolbox.CSS.buttonShortcut, {
textContent: shortcut,
}));
}
@ -292,11 +339,11 @@ export default class Toolbox extends Module<ToolboxNodes> {
private enableShortcut(toolName: string, shortcut: string): void {
Shortcuts.add({
name: shortcut,
on: this.api.ui.nodes.redactor,
handler: (event: KeyboardEvent) => {
event.preventDefault();
this.insertNewBlock(toolName);
},
on: this.Editor.UI.nodes.redactor,
});
}
@ -305,15 +352,13 @@ export default class Toolbox extends Module<ToolboxNodes> {
* Fired when the Read-Only mode is activated
*/
private removeAllShortcuts(): void {
const tools = this.Editor.Tools.blockTools;
Array
.from(tools.values())
.from(this.tools.values())
.forEach((tool) => {
const shortcut = tool.shortcut;
if (shortcut) {
Shortcuts.remove(this.Editor.UI.nodes.redactor, shortcut);
Shortcuts.remove(this.api.ui.nodes.redactor, shortcut);
}
});
}
@ -326,7 +371,7 @@ export default class Toolbox extends Module<ToolboxNodes> {
this.flipper = new Flipper({
items: tools,
focusedItemClass: this.CSS.toolboxButtonActive,
focusedItemClass: Toolbox.CSS.toolboxButtonActive,
});
}
@ -337,34 +382,42 @@ export default class Toolbox extends Module<ToolboxNodes> {
* @param {string} toolName - Tool name
*/
private insertNewBlock(toolName: string): void {
const { BlockManager, Caret } = this.Editor;
const { currentBlock } = BlockManager;
const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();
const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex);
const newBlock = BlockManager.insert({
tool: toolName,
replace: currentBlock.isEmpty,
});
if (!currentBlock) {
return;
}
/**
* On mobile version, we see the Plus Button even near non-empty blocks,
* so if current block is not empty, add the new block below the current
*/
const index = currentBlock.isEmpty ? currentBlockIndex : currentBlockIndex + 1;
const newBlock = this.api.blocks.insert(
toolName,
undefined,
undefined,
index,
undefined,
currentBlock.isEmpty
);
/**
* Apply callback before inserting html
*/
newBlock.call(BlockToolAPI.APPEND_CALLBACK);
this.Editor.Caret.setToBlock(newBlock);
this.api.caret.setToBlock(index);
/** If new block doesn't contain inpus, insert new paragraph above */
if (newBlock.inputs.length === 0) {
if (newBlock === BlockManager.lastBlock) {
BlockManager.insertAtEnd();
Caret.setToBlock(BlockManager.lastBlock);
} else {
Caret.setToBlock(BlockManager.nextBlock);
}
}
this.emit(ToolboxEvent.BlockAdded, {
block: newBlock,
});
/**
* close toolbar when node is changed
*/
this.Editor.Toolbar.close();
this.api.toolbar.close();
}
}

View file

@ -454,6 +454,68 @@ export function debounce(func: (...args: unknown[]) => void, wait?: number, imme
};
}
/**
* Returns a function, that, when invoked, will only be triggered at most once during a given window of time.
*
* @param func - function to throttle
* @param wait - function will be called only once for that period
* @param options - Normally, the throttled function will run as much as it can
* without ever going more than once per `wait` duration;
* but if you'd like to disable the execution on the leading edge, pass
* `{leading: false}`. To disable execution on the trailing edge, ditto.
*/
export function throttle(func, wait, options: {leading?: boolean; trailing?: boolean} = undefined): () => void {
let context, args, result;
let timeout = null;
let previous = 0;
if (!options) {
options = {};
}
const later = function (): void {
previous = options.leading === false ? 0 : Date.now();
timeout = null;
result = func.apply(context, args);
if (!timeout) {
context = args = null;
}
};
return function (): unknown {
const now = Date.now();
if (!previous && options.leading === false) {
previous = now;
}
const remaining = wait - (now - previous);
context = this;
// eslint-disable-next-line prefer-rest-params
args = arguments;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) {
context = args = null;
}
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};
}
/**
* Copies passed text to the clipboard
*

View file

@ -19,7 +19,7 @@ export default class EventsDispatcher<Events extends string = string> {
*
* @type {{}}
*/
private subscribers: {[name: string]: Array<(data?: object) => object>} = {};
private subscribers: {[name: string]: Array<(data?: object) => unknown>} = {};
/**
* Subscribe any event on callback
@ -27,7 +27,7 @@ export default class EventsDispatcher<Events extends string = string> {
* @param {string} eventName - event name
* @param {Function} callback - subscriber
*/
public on(eventName: Events, callback: (data: object) => object): void {
public on(eventName: Events, callback: (data: object) => unknown): void {
if (!(eventName in this.subscribers)) {
this.subscribers[eventName] = [];
}
@ -42,12 +42,12 @@ export default class EventsDispatcher<Events extends string = string> {
* @param {string} eventName - event name
* @param {Function} callback - subscriber
*/
public once(eventName: Events, callback: (data: object) => object): void {
public once(eventName: Events, callback: (data: object) => unknown): void {
if (!(eventName in this.subscribers)) {
this.subscribers[eventName] = [];
}
const wrappedCallback = (data: object): object => {
const wrappedCallback = (data: object): unknown => {
const result = callback(data);
const indexOfHandler = this.subscribers[eventName].indexOf(wrappedCallback);
@ -87,7 +87,7 @@ export default class EventsDispatcher<Events extends string = string> {
* @param {string} eventName - event name
* @param {Function} callback - event handler
*/
public off(eventName: Events, callback: (data: object) => object): void {
public off(eventName: Events, callback: (data: object) => unknown): void {
for (let i = 0; i < this.subscribers[eventName].length; i++) {
if (this.subscribers[eventName][i] === callback) {
delete this.subscribers[eventName][i];
@ -98,7 +98,7 @@ export default class EventsDispatcher<Events extends string = string> {
/**
* Destroyer
* clears subsribers list
* clears subscribers list
*/
public destroy(): void {
this.subscribers = null;

View file

@ -2,7 +2,8 @@
/**
* Use external module CodeX Tooltip
*/
import CodeXTooltips, { TooltipContent, TooltipOptions } from 'codex-tooltip';
import CodeXTooltips from 'codex-tooltip';
import type { TooltipOptions, TooltipContent } from 'codex-tooltip/types';
/**
* Tooltip
@ -28,8 +29,8 @@ export default class Tooltip {
* Shows tooltip on element with passed HTML content
*
* @param {HTMLElement} element - any HTML element in DOM
* @param {TooltipContent} content - tooltip's content
* @param {TooltipOptions} options - showing settings
* @param content - tooltip's content
* @param options - showing settings
*/
public show(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void {
this.lib.show(element, content, options);
@ -37,17 +38,19 @@ export default class Tooltip {
/**
* Hides tooltip
*
* @param skipHidingDelay pass true to immediately hide the tooltip
*/
public hide(): void {
this.lib.hide();
public hide(skipHidingDelay = false): void {
this.lib.hide(skipHidingDelay);
}
/**
* Binds 'mouseenter' and 'mouseleave' events that shows/hides the Tooltip
*
* @param {HTMLElement} element - any HTML element in DOM
* @param {TooltipContent} content - tooltip's content
* @param {TooltipOptions} options - showing settings
* @param content - tooltip's content
* @param options - showing settings
*/
public onHover(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void {
this.lib.onHover(element, content, options);

View file

@ -2,7 +2,11 @@
* Block Tool wrapper
*/
.cdx-block {
padding: 0.4em 0;
padding: var(--block-padding-vertical) 0;
&::-webkit-input-placeholder {
line-height:normal!important;
}
}
/**

View file

@ -7,7 +7,7 @@
@media (--mobile){
bottom: 40px;
right: -11px;
right: auto;
top: auto;
}

View file

@ -3,8 +3,6 @@
left: 0;
right: 0;
top: 0;
/*opacity: 0;*/
/*visibility: hidden;*/
transition: opacity 100ms ease;
will-change: opacity, transform;
display: none;
@ -32,15 +30,12 @@
display: flex;
align-content: center;
margin: 0;
max-width: calc(100% - 35px);
max-width: 100%;
}
}
&__plus {
@apply --toolbox-button;
position: absolute;
left: calc(var(--toolbox-buttons-size) * -1);
flex-shrink: 0;
&-shortcut {
@ -60,25 +55,19 @@
}
}
&__plus,
.ce-toolbox {
top: 50%;
transform: translateY(-50%);
}
/**
* Block actions Zone
* -------------------------
*/
&__actions {
position: absolute;
right: -30px;
top: 5px;
right: 100%;
opacity: 0;
display: flex;
@media (--mobile){
position: absolute;
right: -28px;
right: auto;
top: 50%;
transform: translateY(-50%);
display: flex;
@ -95,26 +84,19 @@
}
&__settings-btn {
display: flex;
align-items: center;
justify-content: center;
@apply --toolbox-button;
width: 18px;
height: 18px;
color: var(--grayText);
margin: 0 5px;
cursor: pointer;
background: var(--bg-light);
user-select: none;
&:hover {
color: var(--color-dark);
}
@media (--mobile){
background: transparent;
}
}
}
.codex-editor--toolbox-opened .ce-toolbar__actions {
display: none;
}
/**
* Styles for Narrow mode
*/

View file

@ -21,6 +21,7 @@
&__button {
@apply --toolbox-button;
flex-shrink: 0;
margin-left: 5px;
}
}

View file

@ -21,7 +21,7 @@
/**
* Gray icons hover
*/
--color-dark: #1D202B;
--color-dark: #1D202B;
/**
* Blue icons
@ -52,7 +52,14 @@
/**
* Toolbar Plus Button and Toolbox buttons height and width
*/
--toolbox-buttons-size: 34px;
--toolbox-buttons-size: 26px;
--toolbox-buttons-size--mobile: 36px;
/**
* The main `.cdx-block` wrapper has such vertical paddings
* And the Block Actions toggler too
*/
--block-padding-vertical: 0.4em;
/**
* Confirm deletion bg
@ -62,14 +69,14 @@
--overlay-pane: {
position: absolute;
background-color: #FFFFFF;
border: 1px solid #EAEAEA;
border: 1px solid #E8E8EB;
box-shadow: 0 3px 15px -3px rgba(13,20,33,0.13);
border-radius: 4px;
border-radius: 6px;
z-index: 2;
@media (--mobile){
box-shadow: 0 13px 7px -5px rgba(26, 38, 49, 0.09),6px 15px 34px -6px rgba(33, 48, 73, 0.29);
border-bottom-color: #d5d7db;
box-shadow: 0 8px 6px -6px rgb(33 48 73 / 19%);
border-bottom-color: #c7c7c7;
}
&--left-oriented {
@ -92,17 +99,23 @@
* Styles for Toolbox Buttons and Plus Button
*/
--toolbox-button: {
color: var(--grayText);
color: var(--color-dark);
cursor: pointer;
width: var(--toolbox-buttons-size);
height: var(--toolbox-buttons-size);
border-radius: 3px;
display: inline-flex;
justify-content: center;
align-items: center;
@media (--mobile){
width: var(--toolbox-buttons-size--mobile);
height: var(--toolbox-buttons-size--mobile);
}
&:hover,
&--active {
color: var(--color-active-icon);
background-color: var(--bg-light);
}
&--active{
@ -152,3 +165,4 @@
}
};
}

View file

@ -2,7 +2,6 @@ import UI from '../components/modules/ui';
import BlockEvents from '../components/modules/blockEvents';
import Toolbar from '../components/modules/toolbar/index';
import InlineToolbar from '../components/modules/toolbar/inline';
import Toolbox from '../components/modules/toolbar/toolbox';
import BlockSettings from '../components/modules/toolbar/blockSettings';
import Paste from '../components/modules/paste';
import DragNDrop from '../components/modules/dragNDrop';
@ -31,6 +30,7 @@ import TooltipAPI from '../components/modules/api/tooltip';
import ReadOnly from '../components/modules/readonly';
import ReadOnlyAPI from '../components/modules/api/readonly';
import I18nAPI from '../components/modules/api/i18n';
import UiAPI from '../components/modules/api/ui';
import ModificationsObserver from '../components/modules/modificationsObserver';
export interface EditorModules {
@ -40,7 +40,6 @@ export interface EditorModules {
RectangleSelection: RectangleSelection;
Toolbar: Toolbar;
InlineToolbar: InlineToolbar;
Toolbox: Toolbox;
BlockSettings: BlockSettings;
ConversionToolbar: ConversionToolbar;
Paste: Paste;
@ -67,5 +66,6 @@ export interface EditorModules {
ReadOnly: ReadOnly;
ReadOnlyAPI: ReadOnlyAPI;
I18nAPI: I18nAPI;
UiAPI: UiAPI;
ModificationsObserver: ModificationsObserver;
}

View file

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import Header from '../../../example/tools/header';
import Header from '@editorjs/header';
import { nanoid } from 'nanoid';
describe.only('Block ids', () => {

View file

@ -1,5 +1,5 @@
import Header from '../../../example/tools/header';
import Image from '../../../example/tools/simple-image';
import Header from '@editorjs/header';
import Image from '@editorjs/simple-image';
import * as _ from '../../../src/components/utils';
describe('Copy pasting from Editor', () => {

View file

@ -96,7 +96,10 @@ describe('Tools module', () => {
await module.prepare();
expect(WithSuccessfulPrepare.prepare).to.be.calledWithExactly({ toolName: 'withSuccessfulPrepare', config });
expect(WithSuccessfulPrepare.prepare).to.be.calledWithExactly({
toolName: 'withSuccessfulPrepare',
config,
});
});
});

View file

@ -1,4 +1,4 @@
import Header from '../../../example/tools/header';
import Header from '@editorjs/header';
import { BlockMutationType } from '../../../types/events/block/mutation-type';
/**

11
types/api/blocks.d.ts vendored
View file

@ -66,6 +66,11 @@ export interface Blocks {
*/
getCurrentBlockIndex(): number;
/**
* Returns the index of Block by id;
*/
getBlockIndex(blockId: string): number;
/**
* Mark Block as stretched
* @param {number} index - Block to mark
@ -89,13 +94,14 @@ export interface Blocks {
insertNewBlock(): void;
/**
* Insert new Block
* Insert new Block and return inserted Block API
*
* @param {string} type Tool name
* @param {BlockToolData} data Tool data to insert
* @param {ToolConfig} config Tool config
* @param {number?} index index where to insert new Block
* @param {boolean?} needToFocus - flag to focus inserted Block
* @param {boolean?} replace - should the existed Block on that index be replaced or not
*/
insert(
type?: string,
@ -103,7 +109,8 @@ export interface Blocks {
config?: ToolConfig,
index?: number,
needToFocus?: boolean,
): void;
replace?: boolean,
): BlockAPI;
/**

View file

@ -13,3 +13,4 @@ export * from './inline-toolbar';
export * from './block';
export * from './readonly';
export * from './i18n';
export * from './ui';

View file

@ -3,14 +3,14 @@
*/
export interface Listeners {
/**
* Subscribe to event dispatched on passed element
* Subscribe to event dispatched on passed element. Returns listener id.
*
* @param {Element} element
* @param {string} eventType
* @param {(event: Event) => void}handler
* @param {boolean} useCapture
*/
on(element: Element, eventType: string, handler: (event?: Event) => void, useCapture?: boolean): void;
on(element: Element, eventType: string, handler: (event?: Event) => void, useCapture?: boolean): string;
/**
* Unsubscribe from event dispatched on passed element
@ -21,4 +21,12 @@ export interface Listeners {
* @param {boolean} useCapture
*/
off(element: Element, eventType: string, handler: (event?: Event) => void, useCapture?: boolean): void;
/**
* Unsubscribe from event dispatched by the listener id
*
* @param id - id of the listener to remove
*/
offById(id: string): void;
}

24
types/api/ui.d.ts vendored Normal file
View file

@ -0,0 +1,24 @@
/**
* Describes API module allowing to access some Editor UI elements and methods
*/
export interface Ui {
/**
* Allows accessing some Editor UI elements
*/
nodes: UiNodes,
}
/**
* Allows accessing some Editor UI elements
*/
export interface UiNodes {
/**
* Top-level editor instance wrapper
*/
wrapper: HTMLElement,
/**
* Element that holds all the Blocks
*/
redactor: HTMLElement,
}

2
types/index.d.ts vendored
View file

@ -27,6 +27,7 @@ import {
Toolbar,
Tooltip,
I18n,
Ui,
} from './api';
import { OutputData } from './data-formats';
@ -92,6 +93,7 @@ export interface API {
tooltip: Tooltip;
i18n: I18n;
readOnly: ReadOnly;
ui: Ui;
}
/**

406
yarn.lock

File diff suppressed because it is too large Load diff