Move to typescript (#474)

* Move all modules to ts

* It works

* Update README.md

Co-Authored-By: gohabereg <gohabereg@users.noreply.github.com>

* Interfaces

* Interfaces

* Move depending interfaces to external types

* Update README.md

* update tools

* add some docs

* Add some fixes

* Add desctiprion for Block declaration and Core properties

* Fixes due comments
:

* Remove Block from external types

* Bump version

* Update src/components/modules/tools.ts

Co-Authored-By: gohabereg <gohabereg@users.noreply.github.com>

* Update src/components/core.ts

Co-Authored-By: gohabereg <gohabereg@users.noreply.github.com>

* Rename gteBlockHTMLByIndex to getBlockByIndex

* Remove unnecessary constructors

* Clean up bindEvents method

* Add InlineToolConstructable interface

* Delete legacy notifications class

* Fix zero-configuration bugs

* Update inline tools and block tunes constructors
This commit is contained in:
George Berezhnoy 2018-11-23 19:11:50 +03:00 committed by GitHub
parent 531f1d253b
commit bcdfcdadbc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
106 changed files with 4536 additions and 15882 deletions

View file

@ -1,7 +1,8 @@
{ {
"presets": [ "presets": [
["@babel/preset-env", { ["@babel/preset-env", {
"modules": "umd" "modules": "umd",
"useBuiltIns": "entry"
}] }]
], ],
"plugins": [ "plugins": [

View file

@ -85,6 +85,8 @@
"$": true, "$": true,
"_": true, "_": true,
"setTimeout": true, "setTimeout": true,
"process": true,
"__dirname": true,
"Map": true "Map": true
} }
} }

1
.gitignore vendored
View file

@ -8,3 +8,4 @@ Thumbs.db
node_modules/* node_modules/*
npm-debug.log npm-debug.log
yarn-error.log

View file

@ -15,10 +15,10 @@ Welcome to testing stage. Please, join a [public Telegram-chat](//t.me/codex_edi
- `New` Added `onChange`-callback, fired after any modifications at the Editor. See [documentation](https://github.com/codex-team/codex.editor/blob/master/docs/installation.md#features). - `New` Added `onChange`-callback, fired after any modifications at the Editor. See [documentation](https://github.com/codex-team/codex.editor/blob/master/docs/installation.md#features).
- `New` New Inline Tool example — [Marker](https://github.com/codex-editor/marker) - `New` New Inline Tool example — [Marker](https://github.com/codex-editor/marker)
- `New` New Inline Tool example — [Code](https://github.com/codex-editor/code) - `New` New Inline Tool example — [Code](https://github.com/codex-editor/code)
- `New` New [CodeX Editor PHP](http://github.com/codex-team/codex.editor.backend) — example of server-side implementation with HTML purifying and data validation. - `New` New [CodeX Editor PHP](http://github.com/codex-team/codex.editor.backend) — example of server-side implementation with HTML purifying and data validation.
- `Improvements` - Improvements of Toolbar's position calculation. - `Improvements` - Improvements of Toolbar's position calculation.
- `Improvements` — Improved zero-configuration initialization. - `Improvements` — Improved zero-configuration initialization.
- and many little improvements. - and many little improvements.
## Documentation ## Documentation
@ -28,7 +28,7 @@ While we develop the new Documentation Site with all stuff, you can check some a
- [How to use](docs/usage.md) - [How to use](docs/usage.md)
- [How to create a Block Tool Plugin](docs/tools.md) - [How to create a Block Tool Plugin](docs/tools.md)
- [How to create an Inline Tool Plugin](docs/tools-inline.md) - [How to create an Inline Tool Plugin](docs/tools-inline.md)
- [API for Tools](src/components/interfaces/api.ts) - [API for Tools](docs/api.md)
Sorry if we missed something. You can join a [Telegram-chat](//t.me/codex_editor) and ask a question. Sorry if we missed something. You can join a [Telegram-chat](//t.me/codex_editor) and ask a question.
@ -38,8 +38,8 @@ Sorry if we missed something. You can join a [Telegram-chat](//t.me/codex_editor
## Basics ## Basics
CodeX Editor is a Block-Styled editor. Blocks is a structural units, of which the Entry is composed. CodeX Editor is a Block-Styled editor. Blocks are structural units, of which the Entry is composed.
For example, `Paragraph`, `Heading`, `Image`, `Video`, `List` are Blocks. Each Block is represented by a Plugin. For example, `Paragraph`, `Heading`, `Image`, `Video`, `List` are Blocks. Each Block is represented by Plugin.
We have [many](http://github.com/codex-editor) ready-to-use Plugins and the [simple API](docs/tools.md) for creation new ones. We have [many](http://github.com/codex-editor) ready-to-use Plugins and the [simple API](docs/tools.md) for creation new ones.
So how to use the Editor after [Installation](docs/installation.md). So how to use the Editor after [Installation](docs/installation.md).
@ -50,21 +50,21 @@ So how to use the Editor after [Installation](docs/installation.md).
![](https://github.com/codex-editor/list/raw/master/assets/example.gif) ![](https://github.com/codex-editor/list/raw/master/assets/example.gif)
- Select text fragment and apply a style or insert a link from the Inline Toolbar - Select text fragment and apply a style or insert a link from the Inline Toolbar
![](https://capella.pics/7ccbcfcd-1c49-4674-bea7-71021468a1bd.jpg) ![](https://capella.pics/7ccbcfcd-1c49-4674-bea7-71021468a1bd.jpg)
- Use «three-dots» button on the right to open Block Settings. From here, you can move and delete a Block - Use «three-dots» button on the right to open Block Settings. From here, you can move and delete a Block
or apply Tool's settings, if it provided. For example, set a Heading level or List style. or apply Tool's settings, if it provided. For example, set a Heading level or List style.
![](https://capella.pics/01a55381-46cd-47c7-b92e-34765434f2ca.jpg) ![](https://capella.pics/01a55381-46cd-47c7-b92e-34765434f2ca.jpg)
## Shortcuts ## Shortcuts
We really appreciate shortcuts. So there are few presets. We really appreciate shortcuts. So there are few presets.
Action | Shortcut | Restrictions Shortcut | Action | Restrictions
-- | -- | -- -- | -- | --
`TAB` | Show/leaf a Toolbox. | On empty block `TAB` | Show/leaf a Toolbox. | On empty block
`SHIFT+TAB` | Leaf back a Toolbox. | While Toolbox is opened `SHIFT+TAB` | Leaf back a Toolbox. | While Toolbox is opened
@ -72,7 +72,7 @@ Action | Shortcut | Restrictions
`CMD+B` | Bold style | On selection `CMD+B` | Bold style | On selection
`CMD+I` | Italic style | On selection `CMD+I` | Italic style | On selection
`CMD+K` | Insert a link | On selection `CMD+K` | Insert a link | On selection
Also we support shortcuts on the all type of Tools. Specify a shortcut with the Tools configuration. For example: Also we support shortcuts on the all type of Tools. Specify a shortcut with the Tools configuration. For example:
```js ```js
@ -104,7 +104,7 @@ There are few steps to run CodeX Editor on your site.
## Load Editor's core ## Load Editor's core
Firstly you need to get CodeX Editor itself. It is a [minified script](build/codex-editor.js) with minimal available Firstly you need to get CodeX Editor itself. It is a [minified script](build/codex-editor.js) with Editor's core and some default must-have tools.
Choose the most usable method of getting Editor for you. Choose the most usable method of getting Editor for you.
@ -120,7 +120,7 @@ Install the package via NPM or Yarn
npm i codex.editor --save-dev npm i codex.editor --save-dev
``` ```
Include module at your application Include module in your application
```javascript ```javascript
const CodexEditor = require('codex.editor'); const CodexEditor = require('codex.editor');
@ -164,7 +164,7 @@ Check [CodeX Editor's community](https://github.com/codex-editor) to see more re
## Create Editor instance ## Create Editor instance
Create an instance of CodeX Editor and pass [Configuration Object](src/components/interfaces/editor-config.ts) with `holderId` and tools list. Create an instance of CodeX Editor and pass [Configuration Object](types/configs/editor-config.d.ts) with `holderId` and tools list.
```html ```html
<div id="codex-editor"></div> <div id="codex-editor"></div>
@ -217,7 +217,7 @@ editor.saver.save()
.then((savedData) => { .then((savedData) => {
console.log(savedData); console.log(savedData);
}); });
``` ```
## Example ## Example

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -68,7 +68,7 @@ Check [CodeX Editor's community](https://github.com/codex-editor) to see Tools e
## Create Editor instance ## Create Editor instance
Create an instance of CodeX Editor and pass [Configuration Object](../src/components/interfaces/editor-config.ts). Create an instance of CodeX Editor and pass [Configuration Object](../src/types-internal/editor-config.ts).
Minimal params is a `holderId`, `tools` list and `initialBlock` marker. Minimal params is a `holderId`, `tools` list and `initialBlock` marker.
```html ```html

View file

@ -18,7 +18,7 @@ Each Tool's instance called with an params object.
| config | `object` | Special configuration params passed in «config» | | config | `object` | Special configuration params passed in «config» |
| data | `object` | Data to be rendered in this Tool | | data | `object` | Data to be rendered in this Tool |
[iapi-link]: ../src/components/interfaces/api.ts [iapi-link]: ../src/types-internal/api.ts
#### Example #### Example

@ -1 +1 @@
Subproject commit 040b0822a9166670d51374d2fa1ffe7caa095736 Subproject commit cfde1bc77e32ca884756f11832da282ba73b16b2

12536
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,9 @@
{ {
"name": "codex.editor", "name": "codex.editor",
"version": "2.4.3", "version": "2.5.0",
"description": "Codex Editor. Native JS, based on API and Open Source", "description": "Codex Editor. Native JS, based on API and Open Source",
"main": "build/codex-editor.js", "main": "build/codex-editor.js",
"types": "./types/index.d.ts",
"scripts": { "scripts": {
"build": "rimraf dist && yarn svg && yarn build:dev", "build": "rimraf dist && yarn svg && yarn build:dev",
"svg": "svg-sprite-generate -d src/assets/ -o build/sprite.svg", "svg": "svg-sprite-generate -d src/assets/ -o build/sprite.svg",
@ -22,6 +23,8 @@
"@babel/register": "^7.0.0", "@babel/register": "^7.0.0",
"@babel/runtime": "^7.1.2", "@babel/runtime": "^7.1.2",
"@codexteam/shortcuts": "^1.0.0", "@codexteam/shortcuts": "^1.0.0",
"@types/webpack": "^4.4.16",
"@types/webpack-env": "^1.13.6",
"babel-loader": "^8.0.4", "babel-loader": "^8.0.4",
"babel-plugin-add-module-exports": "^1.0.0", "babel-plugin-add-module-exports": "^1.0.0",
"babel-plugin-class-display-name": "^2.1.0", "babel-plugin-class-display-name": "^2.1.0",
@ -51,11 +54,11 @@
"rimraf": "^2.6.2", "rimraf": "^2.6.2",
"stylelint": "^9.3.0", "stylelint": "^9.3.0",
"svg-sprite-generator": "0.0.7", "svg-sprite-generator": "0.0.7",
"ts-loader": "^4.4.2", "ts-loader": "^5.3.0",
"tslint": "^5.11.0", "tslint": "^5.11.0",
"tslint-loader": "^3.6.0", "tslint-loader": "^3.6.0",
"typescript": "^2.9.2", "typescript": "^2.9.2",
"webpack": "^4.16.2", "webpack": "4.20.2",
"webpack-cli": "^3.1.0" "webpack-cli": "^3.1.0"
} }
} }

View file

@ -1,10 +1,14 @@
'use strict'; 'use strict';
import {EditorConfig} from '../types';
declare const VERSION: string;
/** /**
* Apply polyfills * Apply polyfills
*/ */
import '@babel/register'; import '@babel/register';
if (!window || !window._babelPolyfill) require('@babel/polyfill');
import 'components/polyfills'; import 'components/polyfills';
import Core from './components/core'; import Core from './components/core';
@ -17,17 +21,28 @@ import Core from './components/core';
* @author CodeX-Team <https://ifmo.su> * @author CodeX-Team <https://ifmo.su>
*/ */
export default class CodexEditor { export default class CodexEditor {
/**
* Promise that resolves when core modules are ready and UI is rendered on the page
*/
public isReady: Promise<void>;
/**
* Stores destroy method implementation.
* Clear heap occupied by Editor and remove UI components from the DOM.
*/
public destroy: () => void;
/** Editor version */ /** Editor version */
static get version() { static get version(): string {
return VERSION; return VERSION;
} }
/** /**
* @constructor * @constructor
* *
* @param {EditorConfig|String} [configuration] - user configuration * @param {EditorConfig|String|undefined} [configuration] - user configuration
*/ */
constructor(configuration) { public constructor(configuration?: EditorConfig|string) {
/** /**
* Set default onReady function * Set default onReady function
*/ */
@ -48,7 +63,7 @@ export default class CodexEditor {
/** /**
* We need to export isReady promise in the constructor * We need to export isReady promise in the constructor
* as it can be used before other API methods are exported * as it can be used before other API methods are exported
* @type {Promise<any | never>} * @type {Promise<void>}
*/ */
this.isReady = editor.isReady.then(() => { this.isReady = editor.isReady.then(() => {
this.exportAPI(editor); this.exportAPI(editor);
@ -61,7 +76,7 @@ export default class CodexEditor {
* *
* @param editor * @param editor
*/ */
exportAPI(editor) { public exportAPI(editor: Core): void {
const fieldsToExport = [ 'configuration' ]; const fieldsToExport = [ 'configuration' ];
const destroy = () => { const destroy = () => {
editor.moduleInstances.Listeners.removeAll(); editor.moduleInstances.Listeners.removeAll();
@ -70,13 +85,15 @@ export default class CodexEditor {
editor = null; editor = null;
for (const field in this) { for (const field in this) {
delete this[field]; if (this.hasOwnProperty(field)) {
delete this[field];
}
} }
Object.setPrototypeOf(this, null); Object.setPrototypeOf(this, null);
}; };
fieldsToExport.forEach(field => { fieldsToExport.forEach((field) => {
this[field] = editor[field]; this[field] = editor[field];
}); });
@ -84,6 +101,6 @@ export default class CodexEditor {
Object.setPrototypeOf(this, editor.moduleInstances.API.methods); Object.setPrototypeOf(this, editor.moduleInstances.API.methods);
delete this['exportAPI']; delete this.exportAPI;
} }
} }

View file

@ -1,6 +1,6 @@
import IEditor from './interfaces/editor'; import {EditorModules} from '../types-internal/editor-modules';
import IEditorConfig from './interfaces/editor-config'; import {EditorConfig} from '../../types';
import IModuleConfig from './interfaces/module-config'; import {ModuleConfig} from '../types-internal/module-config';
/** /**
* @abstract * @abstract
@ -9,27 +9,27 @@ import IModuleConfig from './interfaces/module-config';
* *
* @typedef {Module} Module * @typedef {Module} Module
* @property {Object} config - Editor user settings * @property {Object} config - Editor user settings
* @property {IEditorConfig} Editor - List of Editor modules * @property {EditorModules} Editor - List of Editor modules
*/ */
export default class Module { export default class Module {
/** /**
* Editor modules list * Editor modules list
* @type {IEditor} * @type {EditorModules}
*/ */
protected Editor: IEditor; protected Editor: EditorModules;
/** /**
* Editor configuration object * Editor configuration object
* @type {IEditorConfig} * @type {EditorConfig}
*/ */
protected config: IEditorConfig; protected config: EditorConfig;
/** /**
* @constructor * @constructor
* @param {IModuleConfig} * @param {EditorConfig}
*/ */
constructor({config}: IModuleConfig) { constructor({config}: ModuleConfig) {
if (new.target === Module) { if (new.target === Module) {
throw new TypeError('Constructors for abstract class Module are not allowed.'); throw new TypeError('Constructors for abstract class Module are not allowed.');
} }
@ -39,9 +39,9 @@ export default class Module {
/** /**
* Editor modules setter * Editor modules setter
* @param {IEditor} Editor * @param {EditorModules} Editor
*/ */
set state(Editor) { set state(Editor: EditorModules) {
this.Editor = Editor; this.Editor = Editor;
} }
} }

View file

@ -4,18 +4,16 @@
* *
* @copyright <CodeX Team> 2018 * @copyright <CodeX Team> 2018
*/ */
import IBlockTune from '../interfaces/block-tune'; import {API, BlockTune} from '../../../types';
import $ from '../dom';
declare var $: any; export default class DeleteTune implements BlockTune {
declare var _: any;
export default class DeleteTune implements IBlockTune {
/** /**
* Property that contains CodeX Editor API methods * Property that contains CodeX Editor API methods
* @see {docs/api.md} * @see {docs/api.md}
*/ */
private readonly api: any; private readonly api: API;
/** /**
* Styles * Styles
@ -41,14 +39,14 @@ export default class DeleteTune implements IBlockTune {
/** /**
* Tune nodes * Tune nodes
*/ */
private nodes = { private nodes: {button: HTMLElement} = {
button: null, button: null,
}; };
/** /**
* DeleteTune constructor * DeleteTune constructor
* *
* @param {Object} api * @param {{api: API}} api
*/ */
constructor({api}) { constructor({api}) {
this.api = api; this.api = api;
@ -65,7 +63,7 @@ export default class DeleteTune implements IBlockTune {
public render() { public render() {
this.nodes.button = $.make('div', [this.CSS.button, this.CSS.buttonDelete], {}); this.nodes.button = $.make('div', [this.CSS.button, this.CSS.buttonDelete], {});
this.nodes.button.appendChild($.svg('cross', 12, 12)); this.nodes.button.appendChild($.svg('cross', 12, 12));
this.api.listener.on(this.nodes.button, 'click', (event) => this.handleClick(event), false); this.api.listeners.on(this.nodes.button, 'click', (event: MouseEvent) => this.handleClick(event), false);
return this.nodes.button; return this.nodes.button;
} }

View file

@ -4,17 +4,16 @@
* *
* @copyright <CodeX Team> 2018 * @copyright <CodeX Team> 2018
*/ */
import IBlockTune from '../interfaces/block-tune';
declare var $: any; import $ from '../dom';
declare var _: any; import {API, BlockTune} from '../../../types';
export default class MoveDownTune implements IBlockTune { export default class MoveDownTune implements BlockTune {
/** /**
* Property that contains CodeX Editor API methods * Property that contains CodeX Editor API methods
* @see {api.md} * @see {api.md}
*/ */
private readonly api: any; private readonly api: API;
/** /**
* Styles * Styles
@ -29,7 +28,7 @@ export default class MoveDownTune implements IBlockTune {
/** /**
* MoveDownTune constructor * MoveDownTune constructor
* *
* @param {Object} api * @param {{api: API}} api
*/ */
public constructor({api}) { public constructor({api}) {
this.api = api; this.api = api;
@ -41,7 +40,12 @@ export default class MoveDownTune implements IBlockTune {
public render() { public render() {
const moveDownButton = $.make('div', [this.CSS.button, this.CSS.wrapper], {}); const moveDownButton = $.make('div', [this.CSS.button, this.CSS.wrapper], {});
moveDownButton.appendChild($.svg('arrow-down', 14, 14)); moveDownButton.appendChild($.svg('arrow-down', 14, 14));
this.api.listener.on(moveDownButton, 'click', (event) => this.handleClick(event, moveDownButton), false); this.api.listeners.on(
moveDownButton,
'click',
(event) => this.handleClick(event as MouseEvent, moveDownButton),
false,
);
return moveDownButton; return moveDownButton;
} }
@ -64,8 +68,8 @@ export default class MoveDownTune implements IBlockTune {
return; return;
} }
const nextBlockElement = this.api.blocks.getBlockByIndex(currentBlockIndex + 1).holder, const nextBlockElement = this.api.blocks.getBlockByIndex(currentBlockIndex + 1);
nextBlockCoords = nextBlockElement.getBoundingClientRect(); const nextBlockCoords = nextBlockElement.getBoundingClientRect();
let scrollOffset = Math.abs(window.innerHeight - nextBlockElement.offsetHeight); let scrollOffset = Math.abs(window.innerHeight - nextBlockElement.offsetHeight);

View file

@ -4,18 +4,16 @@
* *
* @copyright <CodeX Team> 2018 * @copyright <CodeX Team> 2018
*/ */
import IBlockTune from '../interfaces/block-tune'; import $ from '../dom';
import {API, BlockTune} from '../../../types';
declare var $: any; export default class MoveUpTune implements BlockTune {
declare var _: any;
export default class MoveUpTune implements IBlockTune {
/** /**
* Property that contains CodeX Editor API methods * Property that contains CodeX Editor API methods
* @see {api.md} * @see {api.md}
*/ */
private readonly api: any; private readonly api: API;
/** /**
* Styles * Styles
@ -30,7 +28,7 @@ export default class MoveUpTune implements IBlockTune {
/** /**
* MoveUpTune constructor * MoveUpTune constructor
* *
* @param {Object} api * @param {{api: API}} api
*/ */
public constructor({api}) { public constructor({api}) {
this.api = api; this.api = api;
@ -43,7 +41,12 @@ export default class MoveUpTune implements IBlockTune {
public render(): HTMLElement { public render(): HTMLElement {
const moveUpButton = $.make('div', [this.CSS.button, this.CSS.wrapper], {}); const moveUpButton = $.make('div', [this.CSS.button, this.CSS.wrapper], {});
moveUpButton.appendChild($.svg('arrow-up', 14, 14)); moveUpButton.appendChild($.svg('arrow-up', 14, 14));
this.api.listener.on(moveUpButton, 'click', (event) => this.handleClick(event, moveUpButton), false); this.api.listeners.on(
moveUpButton,
'click',
(event) => this.handleClick(event as MouseEvent, moveUpButton),
false,
);
return moveUpButton; return moveUpButton;
} }
@ -65,8 +68,8 @@ export default class MoveUpTune implements IBlockTune {
return; return;
} }
const currentBlockElement = this.api.blocks.getBlockByIndex(currentBlockIndex).holder, const currentBlockElement = this.api.blocks.getBlockByIndex(currentBlockIndex);
previousBlockElement = this.api.blocks.getBlockByIndex(currentBlockIndex - 1).holder; const previousBlockElement = this.api.blocks.getBlockByIndex(currentBlockIndex - 1);
/** /**
* Here is two cases: * Here is two cases:

View file

@ -1,14 +1,22 @@
import IBlockTune, {IBlockTuneConstructor} from './interfaces/block-tune'; import {
API,
BlockTool,
BlockToolConstructable,
BlockToolData,
BlockTune,
BlockTuneConstructable,
SanitizerConfig,
ToolConfig,
} from '../../types';
import $ from './dom'; import $ from './dom';
import _ from './utils'; import _ from './utils';
type Tool = any;
/** /**
* @class Block * @class Block
* @classdesc This class describes editor`s block, including block`s HTMLElement, data and tool * @classdesc This class describes editor`s block, including block`s HTMLElement, data and tool
* *
* @property {Tool} tool current block tool (Paragraph, for example) * @property {BlockTool} tool current block tool (Paragraph, for example)
* @property {Object} CSS block`s css classes * @property {Object} CSS block`s css classes
* *
*/ */
@ -17,7 +25,6 @@ type Tool = any;
import MoveUpTune from './block-tunes/block-tune-move-up'; import MoveUpTune from './block-tunes/block-tune-move-up';
import DeleteTune from './block-tunes/block-tune-delete'; import DeleteTune from './block-tunes/block-tune-delete';
import MoveDownTune from './block-tunes/block-tune-move-down'; import MoveDownTune from './block-tunes/block-tune-move-down';
import {IAPI} from './interfaces/api';
/** /**
* @classdesc Abstract Block class that contains Block information, Tool name and Tool class instance * @classdesc Abstract Block class that contains Block information, Tool name and Tool class instance
@ -154,7 +161,7 @@ export default class Block {
* Returns tool's sanitizer config * Returns tool's sanitizer config
* @return {object} * @return {object}
*/ */
get sanitize(): object { get sanitize(): SanitizerConfig {
return this.tool.sanitize; return this.tool.sanitize;
} }
@ -251,13 +258,45 @@ export default class Block {
this.holder.classList.toggle(Block.CSS.wrapperStretched, state); this.holder.classList.toggle(Block.CSS.wrapperStretched, state);
} }
/**
* Block Tool`s name
*/
public name: string; public name: string;
public tool: Tool;
public class: any; /**
public settings: object; * Instance of the Tool Block represents
*/
public tool: BlockTool;
/**
* Class blueprint of the ool Block represents
*/
public class: BlockToolConstructable;
/**
* User Tool configuration
*/
public settings: ToolConfig;
/**
* Wrapper for Block`s content
*/
public holder: HTMLDivElement; public holder: HTMLDivElement;
public tunes: IBlockTune[];
private readonly api: IAPI; /**
* Tunes used by Tool
*/
public tunes: BlockTune[];
/**
* Editor`s API
*/
private readonly api: API;
/**
* Focused input index
* @type {number}
*/
private inputIndex = 0; private inputIndex = 0;
/** /**
@ -268,7 +307,13 @@ export default class Block {
* @param {Object} settings - default settings * @param {Object} settings - default settings
* @param {Object} apiMethods - Editor API * @param {Object} apiMethods - Editor API
*/ */
constructor(toolName: string, toolInstance: Tool, toolClass: object, settings: object, apiMethods: IAPI) { constructor(
toolName: string,
toolInstance: BlockTool,
toolClass: BlockToolConstructable,
settings: ToolConfig,
apiMethods: API,
) {
this.name = toolName; this.name = toolName;
this.tool = toolInstance; this.tool = toolInstance;
this.class = toolClass; this.class = toolClass;
@ -277,7 +322,7 @@ export default class Block {
this.holder = this.compose(); this.holder = this.compose();
/** /**
* @type {IBlockTune[]} * @type {BlockTune[]}
*/ */
this.tunes = this.makeTunes(); this.tunes = this.makeTunes();
} }
@ -303,19 +348,16 @@ export default class Block {
* Call plugins merge method * Call plugins merge method
* @param {Object} data * @param {Object} data
*/ */
public mergeWith(data: object): Promise<void> { public async mergeWith(data: BlockToolData): Promise<void> {
return Promise.resolve() await this.tool.merge(data);
.then(() => {
this.tool.merge(data);
});
} }
/** /**
* Extracts data from Block * Extracts data from Block
* Groups Tool's save processing time * Groups Tool's save processing time
* @return {Object} * @return {Object}
*/ */
public async save(): Promise<void|{tool: string, data: any, time: number}> { public async save(): Promise<void|{tool: string, data: BlockToolData, time: number}> {
const extractedBlock = await this.tool.save(this.pluginsContent); const extractedBlock = await this.tool.save(this.pluginsContent as HTMLElement);
/** /**
* Measuring execution time * Measuring execution time
@ -334,8 +376,8 @@ export default class Block {
time : measuringEnd - measuringStart, time : measuringEnd - measuringStart,
}; };
}) })
.catch(function(error) { .catch((error) => {
_.log(`Saving proccess for ${this.tool.name} tool failed due to the ${error}`, 'log', 'red'); _.log(`Saving proccess for ${this.name} tool failed due to the ${error}`, 'log', 'red');
}); });
} }
@ -348,7 +390,7 @@ export default class Block {
* @param {Object} data * @param {Object} data
* @returns {Boolean|Object} valid * @returns {Boolean|Object} valid
*/ */
public validateData(data: object): object|false { public validateData(data: BlockToolData): BlockToolData|false {
let isValid = true; let isValid = true;
if (this.tool.validate instanceof Function) { if (this.tool.validate instanceof Function) {
@ -365,13 +407,13 @@ export default class Block {
/** /**
* Make an array with default settings * Make an array with default settings
* Each block has default tune instance that have states * Each block has default tune instance that have states
* @return {IBlockTune[]} * @return {BlockTune[]}
*/ */
public makeTunes(): IBlockTune[] { public makeTunes(): BlockTune[] {
const tunesList = [MoveUpTune, DeleteTune, MoveDownTune]; const tunesList = [MoveUpTune, DeleteTune, MoveDownTune];
// Pluck tunes list and return tune instances with passed Editor API and settings // Pluck tunes list and return tune instances with passed Editor API and settings
return tunesList.map( (tune: IBlockTuneConstructor) => { return tunesList.map( (tune: BlockTuneConstructable) => {
return new tune({ return new tune({
api: this.api, api: this.api,
settings: this.settings, settings: this.settings,

223
src/components/blocks.ts Normal file
View file

@ -0,0 +1,223 @@
import _ from './utils';
import $ from './dom';
import Block from './block';
/**
* @class Blocks
* @classdesc Class to work with Block instances array
*
* @private
*
* @property {HTMLElement} workingArea editor`s working node
*
*/
export default class Blocks {
/**
* Get length of Block instances array
*
* @returns {Number}
*/
public get length(): number {
return this.blocks.length;
}
/**
* Get Block instances array
*
* @returns {Block[]}
*/
public get array(): Block[] {
return this.blocks;
}
/**
* Get blocks html elements array
*
* @returns {HTMLElement[]}
*/
public get nodes(): HTMLElement[] {
return _.array(this.workingArea.children);
}
/**
* Proxy trap to implement array-like setter
*
* @example
* blocks[0] = new Block(...)
*
* @param {Blocks} instance Blocks instance
* @param {Number|String} index block index
* @param {Block} block Block to set
* @returns {Boolean}
*/
public static set(instance: Blocks, index: number, block: Block) {
if (isNaN(Number(index))) {
return false;
}
instance.insert(index, block);
return true;
}
/**
* Proxy trap to implement array-like getter
*
* @param {Blocks} instance Blocks instance
* @param {Number|String} index Block index
* @returns {Block|*}
*/
public static get(instance: Blocks, index: number) {
if (isNaN(Number(index))) {
return instance[index];
}
return instance.get(index);
}
/**
* Array of Block instances in order of addition
*/
public blocks: Block[];
/**
* Editor`s area where to add Block`s HTML
*/
public workingArea: HTMLElement;
/**
* @constructor
*
* @param {HTMLElement} workingArea editor`s working node
*/
constructor(workingArea: HTMLElement) {
this.blocks = [];
this.workingArea = workingArea;
}
/**
* Push new Block to the blocks array and append it to working area
*
* @param {Block} block
*/
public push(block: Block): void {
this.blocks.push(block);
this.workingArea.appendChild(block.holder);
}
/**
* Swaps blocks with indexes first and second
* @param {Number} first - first block index
* @param {Number} second - second block index
*/
public swap(first: number, second: number): void {
const secondBlock = this.blocks[second];
/**
* Change in DOM
*/
$.swap(this.blocks[first].holder, secondBlock.holder);
/**
* Change in array
*/
this.blocks[second] = this.blocks[first];
this.blocks[first] = secondBlock;
}
/**
* Insert new Block at passed index
*
* @param {Number} index index to insert Block
* @param {Block} block Block to insert
* @param {Boolean} replace it true, replace block on given index
*/
public insert(index: number, block: Block, replace: boolean = false): void {
if (!this.length) {
this.push(block);
return;
}
if (index > this.length) {
index = this.length;
}
if (replace) {
this.blocks[index].holder.remove();
}
const deleteCount = replace ? 1 : 0;
this.blocks.splice(index, deleteCount, block);
if (index > 0) {
const previousBlock = this.blocks[index - 1];
previousBlock.holder.insertAdjacentElement('afterend', block.holder);
} else {
const nextBlock = this.blocks[index + 1];
if (nextBlock) {
nextBlock.holder.insertAdjacentElement('beforebegin', block.holder);
} else {
this.workingArea.appendChild(block.holder);
}
}
}
/**
* Remove block
* @param {Number|null} index
*/
public remove(index: number): void {
if (isNaN(index)) {
index = this.length - 1;
}
this.blocks[index].holder.remove();
this.blocks.splice(index, 1);
}
/**
* Remove all blocks
*/
public removeAll(): void {
this.workingArea.innerHTML = '';
this.blocks.length = 0;
}
/**
* Insert Block after passed target
*
* @todo decide if this method is necessary
*
* @param {Block} targetBlock target after wich Block should be inserted
* @param {Block} newBlock Block to insert
*/
public insertAfter(targetBlock: Block, newBlock: Block): void {
const index = this.blocks.indexOf(targetBlock);
this.insert(index + 1, newBlock);
}
/**
* Get Block by index
*
* @param {Number} index Block index
* @returns {Block}
*/
public get(index: number): Block {
return this.blocks[index];
}
/**
* Return index of passed Block
*
* @param {Block} block
* @returns {Number}
*/
public indexOf(block: Block): number {
return this.blocks.indexOf(block);
}
}

View file

@ -1,19 +1,29 @@
import $ from './dom';
import _ from './utils';
import {EditorConfig, OutputData, SanitizerConfig, ToolSettings} from '../../types';
import {EditorModules} from '../types-internal/editor-modules';
/** /**
* @typedef {Core} Core - editor core class * @typedef {Core} Core - editor core class
*/ */
/**
* Dynamically imported utils
*
* @typedef {Dom} $ - {@link components/dom.js}
* @typedef {Util} _ - {@link components/utils.js}
*/
/** /**
* Require Editor modules places in components/modules dir * Require Editor modules places in components/modules dir
*/ */
// eslint-disable-next-line const contextRequire = require.context('./modules', true);
let modules = editorModules.map( module => require('./modules/' + module ));
const modules = [];
contextRequire.keys().forEach((filename) => {
/**
* Include files if:
* - extension is .js or .ts
* - does not starts with _
*/
if (filename.match(/^\.\/[^_][\w/]*\.([tj])s$/)) {
modules.push(contextRequire(filename));
}
});
/** /**
* @class Core * @class Core
@ -26,31 +36,27 @@ let modules = editorModules.map( module => require('./modules/' + module ));
* @type {Core} * @type {Core}
*/ */
export default class Core { export default class Core {
/**
* Editor configuration passed by user to the constructor
*/
public config: EditorConfig;
/**
* Object with core modules instances
*/
public moduleInstances: EditorModules = {} as EditorModules;
/**
* Promise that resolves when all core modules are prepared and UI is rendered on the page
*/
public isReady: Promise<void>;
/** /**
* @param {EditorConfig} config - user configuration * @param {EditorConfig} config - user configuration
* *
*/ */
constructor(config) { constructor(config?: EditorConfig|string) {
/**
* Configuration object
* @type {EditorConfig}
*/
this.config = {};
/**
* @typedef {Object} EditorComponents
* @property {BlockManager} BlockManager
* @property {Tools} Tools
* @property {Events} Events
* @property {UI} UI
* @property {Toolbar} Toolbar
* @property {Toolbox} Toolbox
* @property {BlockSettings} BlockSettings
* @property {Renderer} Renderer
* @property {InlineToolbar} InlineToolbar
*/
this.moduleInstances = {};
/** /**
* Ready promise. Resolved if CodeX Editor is ready to work, rejected otherwise * Ready promise. Resolved if CodeX Editor is ready to work, rejected otherwise
*/ */
@ -62,13 +68,13 @@ export default class Core {
}); });
Promise.resolve() Promise.resolve()
.then(() => { .then(async () => {
this.configuration = config; this.configuration = config;
})
.then(() => this.validate()) await this.validate();
.then(() => this.init()) await this.init();
.then(() => this.start()) await this.start();
.then(() => {
_.log('I\'m ready! (ノ◕ヮ◕)ノ*:・゚✧'); _.log('I\'m ready! (ノ◕ヮ◕)ノ*:・゚✧');
setTimeout(() => { setTimeout(() => {
@ -78,7 +84,7 @@ export default class Core {
onReady(); onReady();
}, 500); }, 500);
}) })
.catch(error => { .catch((error) => {
_.log(`CodeX Editor does not ready because of ${error}`, 'error'); _.log(`CodeX Editor does not ready because of ${error}`, 'error');
/** /**
@ -90,52 +96,53 @@ export default class Core {
/** /**
* Setting for configuration * Setting for configuration
* @param {IEditorConfig|string|null} config * @param {EditorConfig|string|undefined} config
*/ */
set configuration(config) { set configuration(config: EditorConfig|string) {
/** /**
* Process zero-configuration or with only holderId * Process zero-configuration or with only holderId
*/ */
if (typeof config === 'string' || typeof config === 'undefined') { if (typeof config === 'string' || typeof config === 'undefined') {
config = { this.config = {
holderId: config holderId: config || 'codex-editor',
}; };
} else {
this.config = config;
} }
/** /**
* If initial Block's Tool was not passed, use the Paragraph Tool * If initial Block's Tool was not passed, use the Paragraph Tool
*/ */
this.config.initialBlock = config.initialBlock || 'paragraph'; this.config.initialBlock = this.config.initialBlock || 'paragraph';
/** /**
* Initial block type * Initial block type
* Uses in case when there is no blocks passed * Uses in case when there is no blocks passed
* @type {{type: (*), data: {text: null}}} * @type {{type: (*), data: {text: null}}}
*/ */
let initialBlockData = { const initialBlockData = {
type : this.config.initialBlock, type : this.config.initialBlock,
data : {} data : {},
}; };
this.config.holderId = config.holderId || 'codex-editor'; this.config.placeholder = this.config.placeholder || 'write your story...';
this.config.placeholder = config.placeholder || 'write your story...'; this.config.sanitizer = this.config.sanitizer || {
this.config.sanitizer = config.sanitizer || {
p: true, p: true,
b: true, b: true,
a: true a: true,
}; } as SanitizerConfig;
this.config.hideToolbar = config.hideToolbar ? config.hideToolbar : false; this.config.hideToolbar = this.config.hideToolbar ? this.config.hideToolbar : false;
this.config.tools = config.tools || {}; this.config.tools = this.config.tools || {};
this.config.data = config.data || {}; this.config.data = this.config.data || {} as OutputData;
this.config.onReady = config.onReady || function () {}; this.config.onReady = this.config.onReady || (() => {});
this.config.onChange = config.onChange || function () {}; this.config.onChange = this.config.onChange || (() => {});
/** /**
* Initialize Blocks to pass data to the Renderer * Initialize Blocks to pass data to the Renderer
*/ */
if (_.isEmpty(this.config.data)) { if (_.isEmpty(this.config.data)) {
this.config.data = {}; this.config.data = {} as OutputData;
this.config.data.blocks = [ initialBlockData ]; this.config.data.blocks = [ initialBlockData ];
} else { } else {
if (!this.config.data.blocks || this.config.data.blocks.length === 0) { if (!this.config.data.blocks || this.config.data.blocks.length === 0) {
@ -148,37 +155,41 @@ export default class Core {
* Returns private property * Returns private property
* @returns {EditorConfig} * @returns {EditorConfig}
*/ */
get configuration() { get configuration(): EditorConfig|string {
return this.config; return this.config;
} }
/** /**
* Checks for required fields in Editor's config * Checks for required fields in Editor's config
* @returns {void|Promise<string>} * @returns {Promise<void>}
*/ */
validate() { public async validate(): Promise<void> {
/** /**
* Check if holderId is not empty * Check if holderId is not empty
*/ */
if (!this.config.holderId) { if (!this.config.holderId) {
return Promise.reject('«holderId» param must being not empty'); throw Error('«holderId» param must being not empty');
} }
/** /**
* Check for a holder element's existence * Check for a holder element's existence
*/ */
if (!$.get(this.config.holderId)) { if (!$.get(this.config.holderId)) {
return Promise.reject(`element with ID «${this.config.holderId}» is missing. Pass correct holder's ID.`); throw Error(`element with ID «${this.config.holderId}» is missing. Pass correct holder's ID.`);
} }
/** /**
* Check Tools for a class containing * Check Tools for a class containing
*/ */
for (let toolName in this.config.tools) { for (const toolName in this.config.tools) {
const tool = this.config.tools[toolName]; if (this.config.tools.hasOwnProperty(toolName)) {
const tool = this.config.tools[toolName];
if (!_.isFunction(tool) && !_.isFunction(tool.class)) { if (!_.isFunction(tool) && !_.isFunction((tool as ToolSettings).class)) {
return Promise.reject(`Tool «${toolName}» must be a constructor function or an object with that function in the «class» property`); throw Error(
`Tool «${toolName}» must be a constructor function or an object with function in the «class» property`,
);
}
} }
} }
} }
@ -188,7 +199,7 @@ export default class Core {
* - make and save instances * - make and save instances
* - configure * - configure
*/ */
init() { public init() {
/** /**
* Make modules instances and save it to the @property this.moduleInstances * Make modules instances and save it to the @property this.moduleInstances
*/ */
@ -200,69 +211,22 @@ export default class Core {
this.configureModules(); this.configureModules();
} }
/**
* Make modules instances and save it to the @property this.moduleInstances
*/
constructModules() {
modules.forEach( Module => {
try {
/**
* We use class name provided by displayName property
*
* On build, Babel will transform all Classes to the Functions so, name will always be 'Function'
* To prevent this, we use 'babel-plugin-class-display-name' plugin
* @see https://www.npmjs.com/package/babel-plugin-class-display-name
*/
this.moduleInstances[Module.displayName] = new Module({
config : this.configuration
});
} catch ( e ) {
console.log('Module %o skipped because %o', Module, e);
}
});
}
/**
* Modules instances configuration:
* - pass other modules to the 'state' property
* - ...
*/
configureModules() {
for(let name in this.moduleInstances) {
/**
* Module does not need self-instance
*/
this.moduleInstances[name].state = this.getModulesDiff( name );
}
}
/**
* Return modules without passed name
*/
getModulesDiff( name ) {
let diff = {};
for(let moduleName in this.moduleInstances) {
/**
* Skip module with passed name
*/
if (moduleName === name) {
continue;
}
diff[moduleName] = this.moduleInstances[moduleName];
}
return diff;
}
/** /**
* Start Editor! * Start Editor!
* *
* Get list of modules that needs to be prepared and return a sequence (Promise) * Get list of modules that needs to be prepared and return a sequence (Promise)
* @return {Promise} * @return {Promise}
*/ */
async start() { public async start() {
const modulesToPrepare = ['Tools', 'UI', 'BlockManager', 'Paste', 'DragNDrop', 'ModificationsObserver', 'BlockSelection']; const modulesToPrepare = [
'Tools',
'UI',
'BlockManager',
'Paste',
'DragNDrop',
'ModificationsObserver',
'BlockSelection',
];
await modulesToPrepare.reduce( await modulesToPrepare.reduce(
(promise, module) => promise.then(async () => { (promise, module) => promise.then(async () => {
@ -275,9 +239,67 @@ export default class Core {
} }
_.log(`Preparing ${module} module`, 'timeEnd'); _.log(`Preparing ${module} module`, 'timeEnd');
}), }),
Promise.resolve() Promise.resolve(),
); );
return this.moduleInstances.Renderer.render(this.config.data.blocks); return this.moduleInstances.Renderer.render(this.config.data.blocks);
} }
};
/**
* Make modules instances and save it to the @property this.moduleInstances
*/
private constructModules(): void {
modules.forEach( (Module) => {
try {
/**
* We use class name provided by displayName property
*
* On build, Babel will transform all Classes to the Functions so, name will always be 'Function'
* To prevent this, we use 'babel-plugin-class-display-name' plugin
* @see https://www.npmjs.com/package/babel-plugin-class-display-name
*/
this.moduleInstances[Module.displayName] = new Module({
config : this.configuration,
});
} catch ( e ) {
console.log('Module %o skipped because %o', Module, e);
}
});
}
/**
* Modules instances configuration:
* - pass other modules to the 'state' property
* - ...
*/
private configureModules(): void {
for (const name in this.moduleInstances) {
if (this.moduleInstances.hasOwnProperty(name)) {
/**
* Module does not need self-instance
*/
this.moduleInstances[name].state = this.getModulesDiff(name);
}
}
}
/**
* Return modules without passed name
* @param {string} name - module for witch modules difference should be calculated
*/
private getModulesDiff(name: string): EditorModules {
const diff = {} as EditorModules;
for (const moduleName in this.moduleInstances) {
/**
* Skip module with passed name
*/
if (moduleName === name) {
continue;
}
diff[moduleName] = this.moduleInstances[moduleName];
}
return diff;
}
}

View file

@ -34,9 +34,9 @@ export default class Dom {
* @param {string} tagName - new Element tag name * @param {string} tagName - new Element tag name
* @param {array|string} classNames - list or name of CSS classname(s) * @param {array|string} classNames - list or name of CSS classname(s)
* @param {Object} attributes - any attributes * @param {Object} attributes - any attributes
* @return {Element} * @return {HTMLElement}
*/ */
public static make(tagName: string, classNames: string|string[] = null, attributes: object = {}): Element { public static make(tagName: string, classNames: string|string[] = null, attributes: object = {}): HTMLElement {
const el = document.createElement(tagName); const el = document.createElement(tagName);
if ( Array.isArray(classNames) ) { if ( Array.isArray(classNames) ) {
@ -87,7 +87,7 @@ export default class Dom {
* @param {Element|DocumentFragment} parent - where to append * @param {Element|DocumentFragment} parent - where to append
* @param {Element|Element[]} elements - element or elements list * @param {Element|Element[]} elements - element or elements list
*/ */
public static append(parent: Element|DocumentFragment, elements: Element|Element[]): void { public static append(parent: Element|DocumentFragment, elements: Element|Element[]|DocumentFragment): void {
if ( Array.isArray(elements) ) { if ( Array.isArray(elements) ) {
elements.forEach( (el) => parent.appendChild(el) ); elements.forEach( (el) => parent.appendChild(el) );
} else { } else {

View file

@ -1,7 +1,5 @@
import InlineTool from '../interfaces/tools/inline-tool'; import $ from '../dom';
import ISanitizerConfig from '../interfaces/sanitizer-config'; import {API, InlineTool, SanitizerConfig} from '../../../types';
declare var $: any;
/** /**
* Bold Tool * Bold Tool
@ -24,10 +22,10 @@ export default class BoldInlineTool implements InlineTool {
* Leave <b> tags * Leave <b> tags
* @return {object} * @return {object}
*/ */
static get sanitize(): ISanitizerConfig { static get sanitize(): SanitizerConfig {
return { return {
b: {}, b: {},
}; } as SanitizerConfig;
} }
/** /**
@ -51,17 +49,11 @@ export default class BoldInlineTool implements InlineTool {
button: undefined, button: undefined,
}; };
/**
* @param {{api: IAPI}} - CodeX Editor API
*/
constructor({api}) {
}
/** /**
* Create button for Inline Toolbar * Create button for Inline Toolbar
*/ */
public render(): HTMLElement { public render(): HTMLElement {
this.nodes.button = document.createElement('button'); this.nodes.button = document.createElement('button') as HTMLButtonElement;
this.nodes.button.type = 'button'; this.nodes.button.type = 'button';
this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier); this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier);
this.nodes.button.appendChild($.svg('bold', 13, 15)); this.nodes.button.appendChild($.svg('bold', 13, 15));

View file

@ -1,7 +1,5 @@
import InlineTool from '../interfaces/tools/inline-tool'; import $ from '../dom';
import ISanitizerConfig from '../interfaces/sanitizer-config'; import {InlineTool, SanitizerConfig} from '../../../types';
declare var $: any;
/** /**
* Italic Tool * Italic Tool
@ -24,10 +22,10 @@ export default class ItalicInlineTool implements InlineTool {
* Leave <i> tags * Leave <i> tags
* @return {object} * @return {object}
*/ */
static get sanitize(): ISanitizerConfig { static get sanitize(): SanitizerConfig {
return { return {
i: {}, i: {},
}; } as SanitizerConfig;
} }
/** /**
@ -51,17 +49,11 @@ export default class ItalicInlineTool implements InlineTool {
button: null, button: null,
}; };
/**
* @param {{api: IAPI}} - CodeX Editor API
*/
constructor({api}) {
}
/** /**
* Create button for Inline Toolbar * Create button for Inline Toolbar
*/ */
public render(): HTMLElement { public render(): HTMLElement {
this.nodes.button = document.createElement('button'); this.nodes.button = document.createElement('button') as HTMLButtonElement;
this.nodes.button.type = 'button'; this.nodes.button.type = 'button';
this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier); this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier);
this.nodes.button.appendChild($.svg('italic', 6, 15)); this.nodes.button.appendChild($.svg('italic', 6, 15));

View file

@ -1,10 +1,9 @@
import InlineTool from '../interfaces/tools/inline-tool';
import SelectionUtils from '../selection'; import SelectionUtils from '../selection';
import ISanitizerConfig from '../interfaces/sanitizer-config';
declare var $: any;
declare var _: any;
import $ from '../dom';
import _ from '../utils';
import {API, InlineTool, SanitizerConfig} from '../../../types';
import {Toolbar} from '../../../types/api';
/** /**
* Link Tool * Link Tool
* *
@ -26,14 +25,14 @@ export default class LinkInlineTool implements InlineTool {
* Leave <a> tags * Leave <a> tags
* @return {object} * @return {object}
*/ */
static get sanitize(): ISanitizerConfig { static get sanitize(): SanitizerConfig {
return { return {
a: { a: {
href: true, href: true,
target: '_blank', target: '_blank',
rel: 'nofollow', rel: 'nofollow',
}, },
}; } as SanitizerConfig;
} }
/** /**
@ -62,7 +61,10 @@ export default class LinkInlineTool implements InlineTool {
/** /**
* Elements * Elements
*/ */
private nodes: {button: HTMLButtonElement, input: HTMLInputElement} = { private nodes: {
button: HTMLButtonElement;
input: HTMLInputElement;
} = {
button: null, button: null,
input: null, input: null,
}; };
@ -80,10 +82,10 @@ export default class LinkInlineTool implements InlineTool {
/** /**
* Available Inline Toolbar methods (open/close) * Available Inline Toolbar methods (open/close)
*/ */
private inlineToolbar: any; private inlineToolbar: Toolbar;
/** /**
* @param {{api: IAPI}} - CodeX Editor API * @param {{api: API}} - CodeX Editor API
*/ */
constructor({api}) { constructor({api}) {
this.inlineToolbar = api.toolbar; this.inlineToolbar = api.toolbar;
@ -94,7 +96,7 @@ export default class LinkInlineTool implements InlineTool {
* Create button for Inline Toolbar * Create button for Inline Toolbar
*/ */
public render(): HTMLElement { public render(): HTMLElement {
this.nodes.button = document.createElement('button'); this.nodes.button = document.createElement('button') as HTMLButtonElement;
this.nodes.button.type = 'button'; this.nodes.button.type = 'button';
this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier); this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier);
this.nodes.button.appendChild($.svg('link', 15, 14)); this.nodes.button.appendChild($.svg('link', 15, 14));
@ -106,10 +108,10 @@ export default class LinkInlineTool implements InlineTool {
* Input for the link * Input for the link
*/ */
public renderActions(): HTMLElement { public renderActions(): HTMLElement {
this.nodes.input = document.createElement('input'); this.nodes.input = document.createElement('input') as HTMLInputElement;
this.nodes.input.placeholder = 'Add a link'; this.nodes.input.placeholder = 'Add a link';
this.nodes.input.classList.add(this.CSS.input); this.nodes.input.classList.add(this.CSS.input);
this.nodes.input.addEventListener('keydown', (event) => { this.nodes.input.addEventListener('keydown', (event: KeyboardEvent) => {
if (event.keyCode === this.ENTER_KEY ) { if (event.keyCode === this.ENTER_KEY ) {
this.enterPressed(event); this.enterPressed(event);
} }

View file

@ -1,230 +0,0 @@
import IInputOutputData from './input-output-data';
import {ConfirmNotifierOptions, NotifierOptions, PromptNotifierOptions} from 'codex-notifier';
/**
* CodeX Editor Public API
*
* @copyright <CodeX Team> 2018
*/
export interface IAPI {
blocks: IBlocksAPI;
caret: ICaretAPI;
events: IEventsAPI;
listener: IListenerAPI;
notifier: INotifierAPI;
sanitizer: ISanitizerAPI;
saver: ISaverAPI;
selection: ISelectionAPI;
styles: IStylesAPI;
toolbar: IToolbarAPI;
}
/**
* Working with Blocks list: moving, removing, etc
*/
export interface IBlocksAPI {
/**
* Clears Blocks list
*/
clear: () => void;
/**
* Fills editor with Blocks data
*/
render: (data: IInputOutputData) => void;
/**
* Removes block
*/
delete: (blockIndex?: number) => void;
/**
* Swap two Blocks by positions
* @param {number} fromIndex - position of first Block
* @param {number} toIndex - position of second Block
*/
swap: (fromIndex: number, toIndex: number) => void;
/**
* Returns block by passed index
*
* @param {Number} index - needed block with index
* @return {object}
*/
getBlockByIndex: (index: number) => object;
/**
* Returns current block index
* @return {number}
*/
getCurrentBlockIndex: () => number;
/**
* Returns Block's count
* @return {number}
*/
getBlocksCount: () => number;
/**
* Stretch Block's content
* @param {number} index - index of Block
* @param {boolean} [status] - true to enable, false to disable
*/
stretchBlock: (index: number, status: boolean) => void;
/**
* Insert new initial typed Block
*/
insertNewBlock: () => void;
}
/**
* Methods for working with Caret
*/
export interface ICaretAPI {
/**
* @todo Add caret methods
*/
}
/**
* Events Module API methods
*/
export interface IEventsAPI {
/**
* Subsribe on events
*/
on: (eventName: string, callback: () => void) => void;
/**
* Trigger subsribed callbacks
*/
emit: (eventName: string, data: object) => void;
/**
* Unsubsribe callback
*/
off: (eventName: string, callback: () => void) => void;
}
/**
* DOM Listener API
*/
export interface IListenerAPI {
/**
* Adds event listener
* @param {HTMLElement} element
* @param {string} eventType
* @param {() => void} handler
* @param useCapture
* @return {boolean}
*/
on: (element: HTMLElement, eventType: string, handler: () => void, useCapture: boolean) => void;
/**
* Remove event listener
* @param {HTMLElement} element
* @param {string} eventType
* @param {() => void} handler
*/
off: (element: HTMLElement, eventType: string, handler: () => void) => void;
}
/**
* Notifier API
*/
export interface INotifierAPI {
/**
* Show web notification
*
* @param {NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions}
*/
show: (options: NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions) => void;
}
/**
* Sanitizer's methods
*/
export interface ISanitizerAPI {
/**
* Clean taint string from disallowed tags and attributes
*
* @param taintString
* @param config
*/
clean: (taintString, config?) => string;
}
/**
* Saver's methods
*/
export interface ISaverAPI {
/**
* Return current blocks
*
* @return {IInputOutputData}
*/
save: () => IInputOutputData;
}
/**
* SelectionUtils's methods
*/
export interface ISelectionAPI {
/**
* Looks ahead to find passed tag from current selection
*
* @param {String} tagName
* @param {String} className
*/
findParentTag: (tagName: string, className: string) => HTMLElement|null;
/**
* Expands selection range to the passed parent node
*
* @param {HTMLElement} node
*/
expandToTag: (node: HTMLElement) => void;
}
export interface IStylesAPI {
block: string;
inlineToolButton: string;
inlineToolButtonActive: string;
input: string;
loader: string;
button: string;
settingsButton: string;
settingsButtonActive: string;
}
/**
* Toolbar's methods
* Basic toolbar methods
*/
export interface IToolbarAPI {
/**
* Opens only toolbar
*/
open: () => void;
/**
* Closes toolbar. If toolbox or toolbar-blockSettings are opened then they will be closed too
*/
close: () => void;
}

View file

@ -1,29 +0,0 @@
export interface IBlockTuneConstructor {
new (data: {api: any, settings: any});
}
/**
* BlockTune interface
*
* All tunes must implement this interface
*/
export interface IBlockTune {
/**
* Settings will be described later
*/
settings?: object;
/**
* Returns tune button that will be appended in default settings area
*/
render(): HTMLElement;
/**
* handle Click event
* @param {MouseEvent} event
* @param {HTMLElement} button
*/
handleClick(event: MouseEvent, button: HTMLElement): void;
}
export default IBlockTune;

View file

@ -1,57 +0,0 @@
import ISanitizerConfig from './sanitizer-config';
import IInputOutputData from './input-output-data';
import IToolSettings from './tools/tool-settings';
import ITool from './tools/tool';
/**
* Editor Instance config
*/
export default interface IEditorConfig {
/**
* Element to append Editor
*/
holderId: string;
/**
* Map of used Tools with or without configuration
*/
tools: {[toolName: string]: ITool|IToolSettings};
/**
* This Tool will be added by default
* Name should be equal a one Tool's key of Editor's Tools
*/
initialBlock: string;
/**
* Blocks list in JSON-format
*/
data?: IInputOutputData;
/**
* First Block placeholder
*/
placeholder?: string;
/**
* Define tags not to be stripped off while pasting
* @see {@link sanitizer}
*/
sanitizer?: ISanitizerConfig;
/**
* Do not show toolbar
*/
hideToolbar?: boolean;
/**
* Editor initialization callback
*/
onReady?(): void;
/**
* Trigger callback if Content has beed changed
*/
onChange?(): void;
}

View file

@ -1,62 +0,0 @@
import BlockEvents from '../modules/block-events';
import InlineToolbar from '../modules/toolbar-inline';
import ListenerAPI from '../modules/api-listener';
import Module from '../__module';
import SanitizerAPI from '../modules/api-sanitizer';
import SaverAPI from '../modules/api-saver';
import SelectionAPI from '../modules/api-selection';
import ToolbarAPI from '../modules/api-toolbar';
import API from '../modules/api';
import StylesAPI from '../modules/api-styles';
import Shortcuts from '../modules/shortcuts';
export default interface IEditor {
API: API;
BlockEvents: BlockEvents;
BlockSettings: Module; // @todo create interface
BlocksAPI: Module; // @todo create interface
Caret: Module; // @todo create interface
Events: Module; // @todo create interface
EventsAPI: Module; // @todo create interface
InlineToolbar: InlineToolbar;
ListenerAPI: ListenerAPI;
Listeners: Module; // @todo create interface
Notifier: Module; // @todo create interface
Renderer: Module; // @todo create interface
Sanitizer: Module; // @todo create interface
SanitizerAPI: SanitizerAPI;
Saver: Module; // @todo create interface
SaverAPI: SaverAPI;
SelectionAPI: SelectionAPI;
Shortcuts: Shortcuts;
StylesAPI: StylesAPI;
Toolbar: Module; // @todo create interface
ToolbarAPI: ToolbarAPI;
Toolbox: Module; // @todo create interface
Tools: Module; // @todo create interface
UI: Module; // @todo create interface
}

View file

@ -1,23 +0,0 @@
import IBlockToolData from './tools/block-tool-data';
/**
* Interface represents input CodeX Editor data
* that passed with initial configuration object as 'data' key
*/
export default interface IInputOutputData {
/**
* Timestamp of saving in milliseconds
*/
readonly time?: number;
/**
* Saved Blocks
*/
readonly blocks: IBlockToolData[];
/**
* Editor's version
*/
readonly version?: string;
}

View file

@ -1,12 +0,0 @@
import IEditorConfig from './editor-config';
/**
* Internal Module's construction parameters
*/
export default interface IModuleConfig {
/**
* Editor's config
*/
config: IEditorConfig;
}

View file

@ -1,40 +0,0 @@
/**
* Shortcuts Interface
*
* implements CodeX-Team shortcuts Module
* @see https://github.com/codex-team/codex.shortcuts
*/
export interface IShortcuts {
/**
* Adds shortcut
* @param {IShortcut} shortcut
*/
add(shortcut: IShortcut): void;
/**
* removes shortcut
* @param {string} shortcut
*/
remove(shortcut: string): void;
}
/**
* Shortcut interface
* Each shortcut must have name and handler
* `name` is a shortcut, like 'CMD+K', 'CMD+B' etc
* `handler` is a callback
*/
export interface IShortcut {
/**
* Shortcut name
* Ex. CMD+I, CMD+B ....
*/
name: string;
/**
* Shortcut handler
*/
handler(event): (event) => void;
}

View file

@ -1,4 +0,0 @@
/**
* Object returned by Tool's {@link IBlockTool#save} method
*/
export default interface IBlockToolData {}

View file

@ -1,10 +0,0 @@
/**
* Abstract interface of all Tools
*/
export default interface ITool {
/**
* Define Tool type as Inline
*/
isInline?: boolean;
}

View file

@ -1,187 +0,0 @@
/**
* Codex Editor Notification Module
*
* @author Codex Team
* @version 1.0
*/
module.exports = (function (notifications) {
let editor = codex.editor;
var queue = [];
var addToQueue = function (settings) {
queue.push(settings);
var index = 0;
while ( index < queue.length && queue.length > 5) {
if (queue[index].type == 'confirm' || queue[index].type == 'prompt') {
index++;
continue;
}
queue[index].close();
queue.splice(index, 1);
}
};
notifications.createHolder = function () {
var holder = editor.draw.node('DIV', 'cdx-notifications-block');
editor.nodes.notifications = document.body.appendChild(holder);
return holder;
};
/**
* Error notificator. Shows block with message
* @protected
*/
notifications.errorThrown = function (errorMsg, event) {
editor.notifications.notification({message: 'This action is not available currently', type: event.type});
};
/**
*
* Appends notification
*
* settings = {
* type - notification type (reserved types: alert, confirm, prompt). Just add class 'cdx-notification-'+type
* message - notification message
* okMsg - confirm button text (default - 'Ok')
* cancelBtn - cancel button text (default - 'Cancel'). Only for confirm and prompt types
* confirm - function-handler for ok button click
* cancel - function-handler for cancel button click. Only for confirm and prompt types
* time - time (in seconds) after which notification will close (default - 10s)
* }
*
* @param settings
*/
notifications.notification = function (constructorSettings) {
/** Private vars and methods */
var notification = null,
cancel = null,
type = null,
confirm = null,
inputField = null;
var confirmHandler = function () {
close();
if (typeof confirm !== 'function' ) {
return;
}
if (type == 'prompt') {
confirm(inputField.value);
return;
}
confirm();
};
var cancelHandler = function () {
close();
if (typeof cancel !== 'function' ) {
return;
}
cancel();
};
/** Public methods */
function create(settings) {
if (!(settings && settings.message)) {
editor.core.log('Can\'t create notification. Message is missed');
return;
}
settings.type = settings.type || 'alert';
settings.time = settings.time*1000 || 10000;
var wrapper = editor.draw.node('DIV', 'cdx-notification'),
message = editor.draw.node('DIV', 'cdx-notification__message'),
input = editor.draw.node('INPUT', 'cdx-notification__input'),
okBtn = editor.draw.node('SPAN', 'cdx-notification__ok-btn'),
cancelBtn = editor.draw.node('SPAN', 'cdx-notification__cancel-btn');
message.textContent = settings.message;
okBtn.textContent = settings.okMsg || 'ОК';
cancelBtn.textContent = settings.cancelMsg || 'Отмена';
editor.listeners.add(okBtn, 'click', confirmHandler);
editor.listeners.add(cancelBtn, 'click', cancelHandler);
wrapper.appendChild(message);
if (settings.type == 'prompt') {
wrapper.appendChild(input);
}
wrapper.appendChild(okBtn);
if (settings.type == 'prompt' || settings.type == 'confirm') {
wrapper.appendChild(cancelBtn);
}
wrapper.classList.add('cdx-notification-' + settings.type);
wrapper.dataset.type = settings.type;
notification = wrapper;
type = settings.type;
confirm = settings.confirm;
cancel = settings.cancel;
inputField = input;
if (settings.type != 'prompt' && settings.type != 'confirm') {
window.setTimeout(close, settings.time);
}
};
/**
* Show notification block
*/
function send() {
editor.nodes.notifications.appendChild(notification);
inputField.focus();
editor.nodes.notifications.classList.add('cdx-notification__notification-appending');
window.setTimeout(function () {
editor.nodes.notifications.classList.remove('cdx-notification__notification-appending');
}, 100);
addToQueue({type: type, close: close});
};
/**
* Remove notification block
*/
function close() {
notification.remove();
};
if (constructorSettings) {
create(constructorSettings);
send();
}
return {
create: create,
send: send,
close: close
};
};
notifications.clear = function () {
editor.nodes.notifications.innerHTML = '';
queue = [];
};
return notifications;
})({});

View file

@ -1,26 +0,0 @@
declare var Module: any;
import {ICaretAPI} from '../interfaces/api';
import IModuleConfig from '../interfaces/module-config';
/**
* @class CaretAPI
* provides with methods to work with caret
*/
export default class CaretAPI extends Module implements ICaretAPI {
/**
* Save Editor config. API provides passed configuration to the Blocks
*/
constructor({config}: IModuleConfig) {
super({config});
}
/**
* Available methods
* @return {ICaretAPI}
*/
get methods(): ICaretAPI {
return {};
}
}

View file

@ -1,33 +0,0 @@
declare var Module: any;
import {ISanitizerAPI} from '../interfaces/api';
import IModuleConfig from '../interfaces/module-config';
/**
* @class API
* Provides CodeX Editor Sanitizer that allows developers to clean their HTML
*/
export default class SanitizerAPI extends Module implements ISanitizerAPI {
/**
* Save Editor config. API provides passed configuration to the Blocks
*/
constructor({config}: IModuleConfig) {
super({config});
}
/**
* Available methods
* @return {ISanitizerAPI}
*/
get methods(): ISanitizerAPI {
return {
clean: (taintString, config) => this.clean(taintString, config),
};
}
public clean(taintString, config) {
return this.Editor.Sanitizer.clean(taintString, config);
}
}

View file

@ -1,36 +0,0 @@
declare var Module: any;
import {ISaverAPI} from '../interfaces/api';
import IInputOutputData from '../interfaces/input-output-data';
import IModuleConfig from '../interfaces/module-config';
/**
* @class SaverAPI
* provides with methods to save data
*/
export default class SaverAPI extends Module implements ISaverAPI {
/**
* Save Editor config. API provides passed configuration to the Blocks
*/
constructor({config}: IModuleConfig) {
super({config});
}
/**
* Available methods
* @return {ISaverAPI}
*/
get methods(): ISaverAPI {
return {
save: () => this.save(),
};
}
/**
* Return Editor's data
*/
public save(): IInputOutputData {
return this.Editor.Saver.save();
}
}

View file

@ -1,49 +0,0 @@
declare var Module: any;
import {ISelectionAPI} from '../interfaces/api';
import IModuleConfig from '../interfaces/module-config';
import Selection from '../selection';
/**
* @class API
* Provides with methods working with SelectionUtils
*/
export default class SelectionAPI extends Module implements ISelectionAPI {
/**
* Save Editor config. API provides passed configuration to the Blocks
*/
constructor({config}: IModuleConfig) {
super({config});
}
/**
* Available methods
* @return {ISelectionAPI}
*/
get methods(): ISelectionAPI {
return {
findParentTag: (tagName: string, className: string) => this.findParentTag(tagName, className),
expandToTag: (node: HTMLElement) => this.expandToTag(node),
};
}
/**
* Looks ahead from selection and find passed tag with class name
* @param {string} tagName - tag to find
* @param {string} className - tag's class name
* @return {HTMLElement|null}
*/
public findParentTag(tagName: string, className: string): HTMLElement|null {
return new Selection().findParentTag(tagName, className);
}
/**
* Expand selection to passed tag
* @param {HTMLElement} node - tag that should contain selection
*/
public expandToTag(node: HTMLElement): void {
new Selection().expandToTag(node);
}
}

View file

@ -1,44 +0,0 @@
declare var Module: any;
import {IToolbarAPI} from '../interfaces/api';
import IModuleConfig from '../interfaces/module-config';
/**
* @class ToolbarsAPI
* provides with methods working with Toolbar
*/
export default class ToolbarAPI extends Module implements IToolbarAPI {
/**
* Save Editor config. API provides passed configuration to the Blocks
*/
constructor({config}: IModuleConfig) {
super({config});
}
/**
* Available methods
* @return {IToolbarAPI}
*/
get methods(): IToolbarAPI {
return {
close: () => this.close(),
open: () => this.open(),
};
}
/**
* Open toolbar
*/
public open(): void {
this.Editor.Toolbar.open();
}
/**
* Close toolbar and all included elements
*/
public close(): void {
this.Editor.Toolbar.close();
}
}

View file

@ -1,36 +1,29 @@
declare var Module: any; import Module from '../../__module';
import { IBlocksAPI } from '../interfaces/api'; import {Blocks} from '../../../../types/api';
import IInputOutputData from '../interfaces/input-output-data'; import {OutputData} from '../../../../types';
import IModuleConfig from '../interfaces/module-config'; import Block from '../../block';
import {ModuleConfig} from '../../../types-internal/module-config';
/** /**
* @class BlocksAPI * @class BlocksAPI
* provides with methods working with Block * provides with methods working with Block
*/ */
export default class BlocksAPI extends Module implements IBlocksAPI { export default class BlocksAPI extends Module {
/**
* Save Editor config. API provides passed configuration to the Blocks
*/
constructor({config}: IModuleConfig) {
super({config});
}
/** /**
* Available methods * Available methods
* @return {IBlocksAPI} * @return {Blocks}
*/ */
get methods(): IBlocksAPI { get methods(): Blocks {
return { return {
clear: () => this.clear(), clear: () => this.clear(),
render: (data: IInputOutputData) => this.render(data), render: (data: OutputData) => this.render(data),
delete: () => this.delete(), delete: () => this.delete(),
swap: (fromIndex: number, toIndex: number) => this.swap(fromIndex, toIndex), swap: (fromIndex: number, toIndex: number) => this.swap(fromIndex, toIndex),
getBlockByIndex: (index: number) => this.getBlockByIndex(index), getBlockByIndex: (index: number) => this.getBlockByIndex(index),
getCurrentBlockIndex: () => this.getCurrentBlockIndex(), getCurrentBlockIndex: () => this.getCurrentBlockIndex(),
getBlocksCount: () => this.getBlocksCount(), getBlocksCount: () => this.getBlocksCount(),
stretchBlock: (index: number, status: boolean) => this.stretchBlock(index, status), stretchBlock: (index: number, status: boolean = true) => this.stretchBlock(index, status),
insertNewBlock: () => this.insertNewBlock(), insertNewBlock: () => this.insertNewBlock(),
}; };
} }
@ -52,13 +45,14 @@ export default class BlocksAPI extends Module implements IBlocksAPI {
} }
/** /**
* Returns Current Block * Returns Block holder by Block index
* @param {Number} index * @param {Number} index
* *
* @return {Object} * @return {HTMLElement}
*/ */
public getBlockByIndex(index: number): object { public getBlockByIndex(index: number): HTMLElement {
return this.Editor.BlockManager.getBlockByIndex(index); const block = this.Editor.BlockManager.getBlockByIndex(index);
return block.holder;
} }
/** /**
@ -112,11 +106,11 @@ export default class BlocksAPI extends Module implements IBlocksAPI {
/** /**
* Fills Editor with Blocks data * Fills Editor with Blocks data
* @param {IInputOutputData} data Saved Editor data * @param {OutputData} data Saved Editor data
*/ */
public render(data: IInputOutputData): void { public render(data: OutputData): Promise<void> {
this.Editor.BlockManager.clear(); this.Editor.BlockManager.clear();
this.Editor.Renderer.render(data.blocks); return this.Editor.Renderer.render(data.blocks);
} }
/** /**
@ -124,14 +118,14 @@ export default class BlocksAPI extends Module implements IBlocksAPI {
* @param {number} index * @param {number} index
* @param {boolean} status - true to enable, false to disable * @param {boolean} status - true to enable, false to disable
*/ */
public stretchBlock(index: number, status: boolean): void { public stretchBlock(index: number, status: boolean = true): void {
const block = this.Editor.BlockManager.getBlockByIndex(index); const block = this.Editor.BlockManager.getBlockByIndex(index);
if (!block) { if (!block) {
return; return;
} }
block.stretched = status !== undefined ? status : true; block.stretched = status;
} }
/** /**

View file

@ -0,0 +1,17 @@
import Module from '../../__module';
import {Caret} from '../../../../types/api';
import {ModuleConfig} from '../../../types-internal/module-config';
/**
* @class CaretAPI
* provides with methods to work with caret
*/
export default class CaretAPI extends Module {
/**
* Available methods
* @return {Caret}
*/
get methods(): Caret {
return {};
}
}

View file

@ -1,26 +1,16 @@
declare var Module: any; import Module from '../../__module';
import {Events} from '../../../../types/api';
import {IEventsAPI} from '../interfaces/api';
import IModuleConfig from '../interfaces/module-config';
/** /**
* @class EventsAPI * @class EventsAPI
* provides with methods working with Toolbar * provides with methods working with Toolbar
*/ */
export default class EventsAPI extends Module implements IEventsAPI { export default class EventsAPI extends Module {
/**
* Save Editor config. API provides passed configuration to the Blocks
*/
constructor({config}: IModuleConfig) {
super({config});
}
/** /**
* Available methods * Available methods
* @return {IEventsAPI} * @return {Events}
*/ */
get methods(): IEventsAPI { get methods(): Events {
return { return {
emit: (eventName: string, data: object) => this.emit(eventName, data), emit: (eventName: string, data: object) => this.emit(eventName, data),
off: (eventName: string, callback: () => void) => this.off(eventName, callback), off: (eventName: string, callback: () => void) => this.off(eventName, callback),

View file

@ -5,37 +5,25 @@
* Each block has an Editor API instance to use provided public methods * Each block has an Editor API instance to use provided public methods
* if you cant to read more about how API works, please see docs * if you cant to read more about how API works, please see docs
*/ */
declare var Module: any; import Module from '../../__module';
declare var $: any; import {API as APIInterfaces} from '../../../../types';
declare var _: any;
import { IAPI } from '../interfaces/api';
/** /**
* @class API * @class API
*/ */
export default class API extends Module { export default class API extends Module {
public get methods(): APIInterfaces {
/**
* Save Editor config. API provides passed configuration to the Blocks
* @param {EditorConfig} config
*/
constructor({config}) {
super({config});
}
public get methods(): IAPI {
return { return {
blocks: this.Editor.BlocksAPI.methods, blocks: this.Editor.BlocksAPI.methods,
caret: this.Editor.CaretAPI.methods, caret: this.Editor.CaretAPI.methods,
events: this.Editor.EventsAPI.methods, events: this.Editor.EventsAPI.methods,
listener: this.Editor.ListenerAPI.methods, listeners: this.Editor.ListenersAPI.methods,
notifier: this.Editor.NotifierAPI.methods, notifier: this.Editor.NotifierAPI.methods,
sanitizer: this.Editor.SanitizerAPI.methods, sanitizer: this.Editor.SanitizerAPI.methods,
saver: this.Editor.SaverAPI.methods, saver: this.Editor.SaverAPI.methods,
selection: this.Editor.SelectionAPI.methods, selection: this.Editor.SelectionAPI.methods,
styles: this.Editor.StylesAPI.classes, styles: this.Editor.StylesAPI.classes,
toolbar: this.Editor.ToolbarAPI.methods, toolbar: this.Editor.ToolbarAPI.methods,
}; } as APIInterfaces;
} }
} }

View file

@ -1,28 +1,18 @@
declare var Module: any; import Module from '../../__module';
import {Listeners} from '../../../../types/api';
import {IListenerAPI} from '../interfaces/api';
import IModuleConfig from '../interfaces/module-config';
/** /**
* @class API * @class ListenersAPI
* Provides with methods working with DOM Listener * Provides with methods working with DOM Listener
*/ */
export default class ListenerAPI extends Module implements IListenerAPI { export default class ListenersAPI extends Module {
/**
* Save Editor config. API provides passed configuration to the Blocks
*/
constructor({config}: IModuleConfig) {
super({config});
}
/** /**
* Available methods * Available methods
* @return {IToolbarAPI} * @return {Listeners}
*/ */
get methods(): IListenerAPI { get methods(): Listeners {
return { return {
on: (element, eventType, handler, useCapture) => this.on(element, eventType, handler, useCapture), on: (element: HTMLElement, eventType, handler, useCapture) => this.on(element, eventType, handler, useCapture),
off: (element, eventType, handler) => this.off(element, eventType, handler), off: (element, eventType, handler) => this.off(element, eventType, handler),
}; };
} }

View file

@ -1,4 +1,5 @@
import Module from '../__module'; import Module from '../../__module';
import {Notifier} from '../../../../types/api';
import {ConfirmNotifierOptions, NotifierOptions, PromptNotifierOptions} from 'codex-notifier'; import {ConfirmNotifierOptions, NotifierOptions, PromptNotifierOptions} from 'codex-notifier';
export default class NotifierAPI extends Module { export default class NotifierAPI extends Module {
@ -6,7 +7,7 @@ export default class NotifierAPI extends Module {
/** /**
* Available methods * Available methods
*/ */
get methods() { get methods(): Notifier {
return { return {
show: (options: NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions) => this.show(options), show: (options: NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions) => this.show(options),
}; };

View file

@ -0,0 +1,23 @@
import Module from '../../__module';
import {Sanitizer} from '../../../../types/api';
/**
* @class SanitizerAPI
* Provides CodeX Editor Sanitizer that allows developers to clean their HTML
*/
export default class SanitizerAPI extends Module {
/**
* Available methods
* @return {Sanitizer}
*/
get methods(): Sanitizer {
return {
clean: (taintString, config) => this.clean(taintString, config),
};
}
public clean(taintString, config) {
return this.Editor.Sanitizer.clean(taintString, config);
}
}

View file

@ -0,0 +1,26 @@
import Module from '../../__module';
import {Saver} from '../../../../types/api';
import {OutputData} from '../../../../types';
/**
* @class SaverAPI
* provides with methods to save data
*/
export default class SaverAPI extends Module {
/**
* Available methods
* @return {Saver}
*/
get methods(): Saver {
return {
save: () => this.save(),
};
}
/**
* Return Editor's data
*/
public save(): Promise<OutputData> {
return this.Editor.Saver.save();
}
}

View file

@ -0,0 +1,39 @@
import Module from '../../__module';
import SelectionUtils from '../../selection';
import {Selection as SelectionAPIInterface} from '../../../../types/api';
/**
* @class SelectionAPI
* Provides with methods working with SelectionUtils
*/
export default class SelectionAPI extends Module {
/**
* Available methods
* @return {SelectionAPIInterface}
*/
get methods(): SelectionAPIInterface {
return {
findParentTag: (tagName: string, className?: string) => this.findParentTag(tagName, className),
expandToTag: (node: HTMLElement) => this.expandToTag(node),
};
}
/**
* Looks ahead from selection and find passed tag with class name
* @param {string} tagName - tag to find
* @param {string} className - tag's class name
* @return {HTMLElement|null}
*/
public findParentTag(tagName: string, className?: string): HTMLElement|null {
return new SelectionUtils().findParentTag(tagName, className);
}
/**
* Expand selection to passed tag
* @param {HTMLElement} node - tag that should contain selection
*/
public expandToTag(node: HTMLElement): void {
new SelectionUtils().expandToTag(node);
}
}

View file

@ -1,22 +1,11 @@
declare var Module: any; import Module from '../../__module';
import {Styles} from '../../../../types/api';
import IModuleConfig from '../interfaces/module-config';
import {IStylesAPI} from '../interfaces/api';
/** /**
* *
*/ */
export default class StylesAPI extends Module { export default class StylesAPI extends Module {
get classes(): Styles {
/**
* Save Editor config
* API provides passed configuration to the Blocks
*/
constructor({config}: IModuleConfig) {
super({config});
}
get classes(): IStylesAPI {
return { return {
/** /**
* Base Block styles * Base Block styles

View file

@ -0,0 +1,34 @@
import Module from '../../__module';
import {Toolbar} from '../../../../types/api';
/**
* @class ToolbarAPI
* provides with methods working with Toolbar
*/
export default class ToolbarAPI extends Module {
/**
* Available methods
* @return {Toolbar}
*/
get methods(): Toolbar {
return {
close: () => this.close(),
open: () => this.open(),
};
}
/**
* Open toolbar
*/
public open(): void {
this.Editor.Toolbar.open();
}
/**
* Close toolbar and all included elements
*/
public close(): void {
this.Editor.Toolbar.close();
}
}

View file

@ -1,18 +1,10 @@
/** /**
* Contains keyboard and mouse events binded on each Block by Block Manager * Contains keyboard and mouse events binded on each Block by Block Manager
*/ */
declare var Module: any; import Module from '../__module';
declare var $: any; import _ from '../utils';
declare var _: any;
export default class BlockEvents extends Module { export default class BlockEvents extends Module {
/**
* @constructor
*/
constructor({config}) {
super({config});
}
/** /**
* All keydowns on Block * All keydowns on Block
* @param {KeyboardEvent} event - keydown * @param {KeyboardEvent} event - keydown
@ -147,7 +139,7 @@ export default class BlockEvents extends Module {
* @param {DragEvent} e * @param {DragEvent} e
*/ */
public dragOver(e: DragEvent) { public dragOver(e: DragEvent) {
const block = this.Editor.BlockManager.getBlockByChildNode(e.target); const block = this.Editor.BlockManager.getBlockByChildNode(e.target as Node);
block.dropTarget = true; block.dropTarget = true;
} }
@ -158,7 +150,7 @@ export default class BlockEvents extends Module {
* @param {DragEvent} e * @param {DragEvent} e
*/ */
public dragLeave(e: DragEvent) { public dragLeave(e: DragEvent) {
const block = this.Editor.BlockManager.getBlockByChildNode(e.target); const block = this.Editor.BlockManager.getBlockByChildNode(e.target as Node);
block.dropTarget = false; block.dropTarget = false;
} }
@ -169,7 +161,7 @@ export default class BlockEvents extends Module {
*/ */
private enter(event: KeyboardEvent): void { private enter(event: KeyboardEvent): void {
const currentBlock = this.Editor.BlockManager.currentBlock, const currentBlock = this.Editor.BlockManager.currentBlock,
tool = this.Editor.Tools.toolsAvailable[currentBlock.name]; tool = this.Editor.Tools.available[currentBlock.name];
/** /**
* Don't handle Enter keydowns when Tool sets enableLineBreaks to true. * Don't handle Enter keydowns when Tool sets enableLineBreaks to true.
@ -228,7 +220,7 @@ export default class BlockEvents extends Module {
private backspace(event: KeyboardEvent): void { private backspace(event: KeyboardEvent): void {
const BM = this.Editor.BlockManager; const BM = this.Editor.BlockManager;
const currentBlock = this.Editor.BlockManager.currentBlock, const currentBlock = this.Editor.BlockManager.currentBlock,
tool = this.Editor.Tools.toolsAvailable[currentBlock.name]; tool = this.Editor.Tools.available[currentBlock.name];
/** /**
* Don't handle Backspaces when Tool sets enableLineBreaks to true. * Don't handle Backspaces when Tool sets enableLineBreaks to true.
@ -297,7 +289,7 @@ export default class BlockEvents extends Module {
BM.mergeBlocks(targetBlock, blockToMerge) BM.mergeBlocks(targetBlock, blockToMerge)
.then( () => { .then( () => {
/** Restore caret position after merge */ /** Restore caret position after merge */
this.Editor.Caret.restoreCaret(targetBlock.pluginsContent); this.Editor.Caret.restoreCaret(targetBlock.pluginsContent as HTMLElement);
targetBlock.pluginsContent.normalize(); targetBlock.pluginsContent.normalize();
this.Editor.Toolbar.close(); this.Editor.Toolbar.close();
}); });

View file

@ -1,652 +0,0 @@
/**
* @class BlockManager
* @classdesc Manage editor`s blocks storage and appearance
*
* @module BlockManager
*
* @version 2.0.0
*/
import Block from '../block';
/**
* @typedef {BlockManager} BlockManager
* @property {Number} currentBlockIndex - Index of current working block
* @property {Proxy} _blocks - Proxy for Blocks instance {@link Blocks}
*/
export default class BlockManager extends Module {
/**
* @constructor
* @param {EditorConfig} config
*/
constructor({config}) {
super({config});
/**
* Proxy for Blocks instance {@link Blocks}
*
* @type {Proxy}
* @private
*/
this._blocks = null;
/**
* Index of current working block
*
* @type {number}
* @private
*/
this.currentBlockIndex = -1;
}
/**
* Should be called after Editor.UI preparation
* Define this._blocks property
*
* @returns {Promise}
*/
prepare() {
return new Promise(resolve => {
let blocks = new Blocks(this.Editor.UI.nodes.redactor);
/**
* We need to use Proxy to overload set/get [] operator.
* So we can use array-like syntax to access blocks
*
* @example
* this._blocks[0] = new Block(...);
*
* block = this._blocks[0];
*
* @todo proxy the enumerate method
*
* @type {Proxy}
* @private
*/
this._blocks = new Proxy(blocks, {
set: Blocks.set,
get: Blocks.get
});
resolve();
});
}
/**
* Creates Block instance by tool name
*
* @param {String} toolName - tools passed in editor config {@link EditorConfig#tools}
* @param {Object} data - constructor params
* @param {Object} settings - block settings
*
* @return {Block}
*/
composeBlock(toolName, data, settings) {
let toolInstance = this.Editor.Tools.construct(toolName, data),
toolClass = this.Editor.Tools.available[toolName],
block = new Block(toolName, toolInstance, toolClass, settings, this.Editor.API.methods);
this.bindEvents(block);
return block;
}
/**
* Bind Events
* @param {Object} block
*/
bindEvents(block) {
this.Editor.Listeners.on(block.holder, 'keydown', (event) => this.Editor.BlockEvents.keydown(event), true);
this.Editor.Listeners.on(block.holder, 'mouseup', (event) => this.Editor.BlockEvents.mouseUp(event));
this.Editor.Listeners.on(block.holder, 'keyup', (event) => this.Editor.BlockEvents.keyup(event));
this.Editor.Listeners.on(block.holder, 'dragover', (event) => this.Editor.BlockEvents.dragOver(event));
this.Editor.Listeners.on(block.holder, 'dragleave', (event) => this.Editor.BlockEvents.dragLeave(event));
}
/**
* Insert new block into _blocks
*
* @param {String} toolName plugin name, by default method inserts initial block type
* @param {Object} data plugin data
* @param {Object} settings - default settings
*
* @return {Block}
*/
insert(toolName = this.config.initialBlock, data = {}, settings = {}) {
// Increment index before construct,
// because developers can use API/Blocks/getCurrentInputIndex on the render() method
const newIndex = ++this.currentBlockIndex;
const block = this.composeBlock(toolName, data, settings);
this._blocks[newIndex] = block;
return block;
}
/**
* Always inserts at the end
* @return {Block}
*/
insertAtEnd() {
/**
* Define new value for current block index
*/
this.currentBlockIndex = this.blocks.length - 1;
/**
* Insert initial typed block
*/
return this.insert();
}
/**
* Merge two blocks
* @param {Block} targetBlock - previous block will be append to this block
* @param {Block} blockToMerge - block that will be merged with target block
*
* @return {Promise} - the sequence that can be continued
*/
mergeBlocks(targetBlock, blockToMerge) {
let blockToMergeIndex = this._blocks.indexOf(blockToMerge);
return Promise.resolve()
.then( () => {
if (blockToMerge.isEmpty) {
return;
}
return blockToMerge.data
.then((blockToMergeInfo) => {
targetBlock.mergeWith(blockToMergeInfo.data);
});
})
.then( () => {
this.removeBlock(blockToMergeIndex);
this.currentBlockIndex = this._blocks.indexOf(targetBlock);
});
}
/**
* Remove block with passed index or remove last
* @param {Number|null} index
*/
removeBlock(index) {
if (!index) {
index = this.currentBlockIndex;
}
this._blocks.remove(index);
/**
* If first Block was removed, insert new Initial Block and set focus on it`s first input
*/
if (!this.blocks.length) {
this.currentBlockIndex = -1;
this.insert();
this.currentBlock.firstInput.focus();
}
}
/**
* Split current Block
* 1. Extract content from Caret position to the Block`s end
* 2. Insert a new Block below current one with extracted content
*
* @return {Block}
*/
split() {
let extractedFragment = this.Editor.Caret.extractFragmentFromCaretPosition(),
wrapper = $.make('div');
wrapper.append(extractedFragment);
/**
* @todo make object in accordance with Tool
*/
let data = {
text: $.isEmpty(wrapper) ? '' : wrapper.innerHTML,
};
/**
* Renew current Block
* @type {Block}
*/
return this.insert(this.config.initialBlock, data);
}
/**
* Replace current working block
*
* @param {String} toolName plugin name
* @param {Object} data plugin data
*
* @return {Block}
*/
replace(toolName, data = {}) {
let block = this.composeBlock(toolName, data);
this._blocks.insert(this.currentBlockIndex, block, true);
return block;
}
/**
* returns last Block
* @return {Block}
*/
get lastBlock() {
return this._blocks[this._blocks.length - 1];
}
/**
* Returns Block by passed index
* @param {Number} index
* @return {Block}
*/
getBlockByIndex(index) {
return this._blocks[index];
}
/**
* Get Block instance by html element
* @param {Node} element
* @returns {Block}
*/
getBlock(element) {
if (!$.isElement(element)) {
element = element.parentNode;
}
let nodes = this._blocks.nodes,
firstLevelBlock = element.closest(`.${Block.CSS.wrapper}`),
index = nodes.indexOf(firstLevelBlock);
if (index >= 0) {
return this._blocks[index];
}
}
/**
* Get current Block instance
*
* @return {Block}
*/
get currentBlock() {
return this._blocks[this.currentBlockIndex];
}
/**
* Returns next Block instance
* @return {Block|null}
*/
get nextBlock() {
let isLastBlock = this.currentBlockIndex === (this._blocks.length - 1);
if (isLastBlock) {
return null;
}
return this._blocks[this.currentBlockIndex + 1];
}
/**
* Return first Block with inputs after current Block
*
* @returns {Block | undefined}
*/
get nextContentfulBlock() {
const nextBlocks = this.blocks.slice(this.currentBlockIndex + 1);
return nextBlocks.find(block => !!block.inputs.length);
}
/**
* Return first Block with inputs before current Block
*
* @returns {Block | undefined}
*/
get previousContentfulBlock() {
const previousBlocks = this.blocks.slice(0, this.currentBlockIndex).reverse();
return previousBlocks.find(block => !!block.inputs.length);
}
/**
* Returns previous Block instance
* @return {Block|null}
*/
get previousBlock() {
let isFirstBlock = this.currentBlockIndex === 0;
if (isFirstBlock) {
return null;
}
return this._blocks[this.currentBlockIndex - 1];
}
/**
* Remove selection from all Blocks then highlight only Current Block
*/
highlightCurrentNode() {
/**
* Remove previous selected Block's state
*/
this.clearFocused();
/**
* Mark current Block as selected
* @type {boolean}
*/
this.currentBlock.focused = true;
}
/**
* Remove selection from all Blocks
*/
clearFocused() {
this.blocks.forEach( block => block.focused = false);
}
/**
* Get array of Block instances
*
* @returns {Block[]} {@link Blocks#array}
*/
get blocks() {
return this._blocks.array;
}
/**
* 1) Find first-level Block from passed child Node
* 2) Mark it as current
*
* @param {Element|Text} childNode - look ahead from this node.
* @param {string} caretPosition - position where to set caret
* @throws Error - when passed Node is not included at the Block
*/
setCurrentBlockByChildNode(childNode, caretPosition = 'default') {
/**
* If node is Text TextNode
*/
if (!$.isElement(childNode)) {
childNode = childNode.parentNode;
}
const parentFirstLevelBlock = childNode.closest(`.${Block.CSS.wrapper}`);
if (parentFirstLevelBlock) {
/**
* Update current Block's index
* @type {number}
*/
this.currentBlockIndex = this._blocks.nodes.indexOf(parentFirstLevelBlock);
this.Editor.Caret.setToInput(childNode, caretPosition);
} else {
throw new Error('Can not find a Block from this child Node');
}
}
/**
* Return block which contents passed node
*
* @param {Node} childNode
* @return {Block}
*/
getBlockByChildNode(childNode) {
/**
* If node is Text TextNode
*/
if (!$.isElement(childNode)) {
childNode = childNode.parentNode;
}
const firstLevelBlock = childNode.closest(`.${Block.CSS.wrapper}`);
return this.blocks.find(block => block.holder === firstLevelBlock);
}
/**
* Swap Blocks Position
* @param {Number} fromIndex
* @param {Number} toIndex
*/
swap(fromIndex, toIndex) {
/** Move up current Block */
this._blocks.swap(fromIndex, toIndex);
/** Now actual block moved up so that current block index decreased */
this.currentBlockIndex = toIndex;
}
/**
* Sets current Block Index -1 which means unknown
* and clear highlightings
*/
dropPointer() {
this.currentBlockIndex = -1;
this.clearFocused();
}
/**
* Clears Editor
* @param {boolean} needAddInitialBlock - 1) in internal calls (for example, in api.blocks.render)
* we don't need to add empty initial block
* 2) in api.blocks.clear we should add empty block
*/
clear(needAddInitialBlock = false) {
this._blocks.removeAll();
this.dropPointer();
if (needAddInitialBlock) {
this.insert(this.config.initialBlock);
}
}
};
/**
* @class Blocks
* @classdesc Class to work with Block instances array
*
* @private
*
* @property {HTMLElement} workingArea editor`s working node
*
*/
class Blocks {
/**
* @constructor
*
* @param {HTMLElement} workingArea editor`s working node
*/
constructor(workingArea) {
this.blocks = [];
this.workingArea = workingArea;
}
/**
* Push back new Block
*
* @param {Block} block
*/
push(block) {
this.blocks.push(block);
this.workingArea.appendChild(block.holder);
}
/**
* Swaps blocks with indexes first and second
* @param {Number} first - first block index
* @param {Number} second - second block index
*/
swap(first, second) {
let secondBlock = this.blocks[second];
/**
* Change in DOM
*/
$.swap(this.blocks[first].holder, secondBlock.holder);
/**
* Change in array
*/
this.blocks[second] = this.blocks[first];
this.blocks[first] = secondBlock;
}
/**
* Insert new Block at passed index
*
* @param {Number} index index to insert Block
* @param {Block} block Block to insert
* @param {Boolean} replace it true, replace block on given index
*/
insert(index, block, replace = false) {
if (!this.length) {
this.push(block);
return;
}
if (index > this.length) {
index = this.length;
}
if (replace) {
this.blocks[index].holder.remove();
}
let deleteCount = replace ? 1 : 0;
this.blocks.splice(index, deleteCount, block);
if (index > 0) {
let previousBlock = this.blocks[index - 1];
previousBlock.holder.insertAdjacentElement('afterend', block.holder);
} else {
let nextBlock = this.blocks[index + 1];
if (nextBlock) {
nextBlock.holder.insertAdjacentElement('beforebegin', block.holder);
} else {
this.workingArea.appendChild(block.holder);
}
}
}
/**
* Remove block
* @param {Number|null} index
*/
remove(index) {
if (isNaN(index)) {
index = this.length - 1;
}
this.blocks[index].holder.remove();
this.blocks.splice(index, 1);
}
/**
* Remove all blocks
*/
removeAll() {
this.workingArea.innerHTML = '';
this.blocks.length = 0;
}
/**
* Insert Block after passed target
*
* @todo decide if this method is necessary
*
* @param {Block} targetBlock target after wich Block should be inserted
* @param {Block} newBlock Block to insert
*/
insertAfter(targetBlock, newBlock) {
let index = this.blocks.indexOf(targetBlock);
this.insert(index + 1, newBlock);
}
/**
* Get Block by index
*
* @param {Number} index Block index
* @returns {Block}
*/
get(index) {
return this.blocks[index];
}
/**
* Return index of passed Block
*
* @param {Block} block
* @returns {Number}
*/
indexOf(block) {
return this.blocks.indexOf(block);
}
/**
* Get length of Block instances array
*
* @returns {Number}
*/
get length() {
return this.blocks.length;
}
/**
* Get Block instances array
*
* @returns {Block[]}
*/
get array() {
return this.blocks;
}
/**
* Get blocks html elements array
*
* @returns {HTMLElement[]}
*/
get nodes() {
return _.array(this.workingArea.children);
}
/**
* Proxy trap to implement array-like setter
*
* @example
* blocks[0] = new Block(...)
*
* @param {Blocks} instance Blocks instance
* @param {Number|String} index block index
* @param {Block} block Block to set
* @returns {Boolean}
*/
static set(instance, index, block) {
if (isNaN(Number(index))) {
return false;
}
instance.insert(index, block);
return true;
}
/**
* Proxy trap to implement array-like getter
*
* @param {Blocks} instance Blocks instance
* @param {Number|String} index Block index
* @returns {Block|*}
*/
static get(instance, index) {
if (isNaN(Number(index))) {
return instance[index];
}
return instance.get(index);
}
}

View file

@ -0,0 +1,437 @@
/**
* @class BlockManager
* @classdesc Manage editor`s blocks storage and appearance
*
* @module BlockManager
*
* @version 2.0.0
*/
import Block from '../block';
import Module from '../__module';
import $ from '../dom';
import Blocks from '../blocks';
import {BlockTool, BlockToolConstructable, BlockToolData, ToolConfig} from '../../../types';
import Caret from './caret';
/**
* @typedef {BlockManager} BlockManager
* @property {Number} currentBlockIndex - Index of current working block
* @property {Proxy} _blocks - Proxy for Blocks instance {@link Blocks}
*/
export default class BlockManager extends Module {
/**
* returns last Block
* @return {Block}
*/
public get lastBlock(): Block {
return this._blocks[this._blocks.length - 1];
}
/**
* Get current Block instance
*
* @return {Block}
*/
public get currentBlock(): Block {
return this._blocks[this.currentBlockIndex];
}
/**
* Returns next Block instance
* @return {Block|null}
*/
public get nextBlock(): Block {
const isLastBlock = this.currentBlockIndex === (this._blocks.length - 1);
if (isLastBlock) {
return null;
}
return this._blocks[this.currentBlockIndex + 1];
}
/**
* Return first Block with inputs after current Block
*
* @returns {Block | undefined}
*/
public get nextContentfulBlock(): Block {
const nextBlocks = this.blocks.slice(this.currentBlockIndex + 1);
return nextBlocks.find((block) => !!block.inputs.length);
}
/**
* Return first Block with inputs before current Block
*
* @returns {Block | undefined}
*/
public get previousContentfulBlock(): Block {
const previousBlocks = this.blocks.slice(0, this.currentBlockIndex).reverse();
return previousBlocks.find((block) => !!block.inputs.length);
}
/**
* Returns previous Block instance
* @return {Block|null}
*/
public get previousBlock(): Block {
const isFirstBlock = this.currentBlockIndex === 0;
if (isFirstBlock) {
return null;
}
return this._blocks[this.currentBlockIndex - 1];
}
/**
* Get array of Block instances
*
* @returns {Block[]} {@link Blocks#array}
*/
public get blocks(): Block[] {
return this._blocks.array;
}
/**
* Index of current working block
*
* @type {number}
*/
public currentBlockIndex: number = -1;
/**
* Proxy for Blocks instance {@link Blocks}
*
* @type {Proxy}
* @private
*/
private _blocks: Blocks = null;
/**
* Should be called after Editor.UI preparation
* Define this._blocks property
*
* @returns {Promise}
*/
public async prepare() {
const blocks = new Blocks(this.Editor.UI.nodes.redactor);
/**
* We need to use Proxy to overload set/get [] operator.
* So we can use array-like syntax to access blocks
*
* @example
* this._blocks[0] = new Block(...);
*
* block = this._blocks[0];
*
* @todo proxy the enumerate method
*
* @type {Proxy}
* @private
*/
this._blocks = new Proxy(blocks, {
set: Blocks.set,
get: Blocks.get,
});
}
/**
* Creates Block instance by tool name
*
* @param {String} toolName - tools passed in editor config {@link EditorConfig#tools}
* @param {Object} data - constructor params
* @param {Object} settings - block settings
*
* @return {Block}
*/
public composeBlock(toolName: string, data: BlockToolData, settings?: ToolConfig): Block {
const toolInstance = this.Editor.Tools.construct(toolName, data) as BlockTool;
const toolClass = this.Editor.Tools.available[toolName] as BlockToolConstructable;
const block = new Block(toolName, toolInstance, toolClass, settings, this.Editor.API.methods);
this.bindEvents(block);
return block;
}
/**
* Insert new block into _blocks
*
* @param {String} toolName plugin name, by default method inserts initial block type
* @param {Object} data plugin data
* @param {Object} settings - default settings
*
* @return {Block}
*/
public insert(
toolName: string = this.config.initialBlock,
data: BlockToolData = {},
settings: ToolConfig = {},
): Block {
// Increment index before construct,
// because developers can use API/Blocks/getCurrentInputIndex on the render() method
const newIndex = ++this.currentBlockIndex;
const block = this.composeBlock(toolName, data, settings);
this._blocks[newIndex] = block;
return block;
}
/**
* Always inserts at the end
* @return {Block}
*/
public insertAtEnd(): Block {
/**
* Define new value for current block index
*/
this.currentBlockIndex = this.blocks.length - 1;
/**
* Insert initial typed block
*/
return this.insert();
}
/**
* Merge two blocks
* @param {Block} targetBlock - previous block will be append to this block
* @param {Block} blockToMerge - block that will be merged with target block
*
* @return {Promise} - the sequence that can be continued
*/
public async mergeBlocks(targetBlock: Block, blockToMerge: Block): Promise<void> {
const blockToMergeIndex = this._blocks.indexOf(blockToMerge);
if (blockToMerge.isEmpty) {
return;
}
const blockToMergeData = await blockToMerge.data;
await targetBlock.mergeWith(blockToMergeData);
this.removeBlock(blockToMergeIndex);
this.currentBlockIndex = this._blocks.indexOf(targetBlock);
}
/**
* Remove block with passed index or remove last
* @param {Number|null} index
*/
public removeBlock(index?: number): void {
if (!index) {
index = this.currentBlockIndex;
}
this._blocks.remove(index);
/**
* If first Block was removed, insert new Initial Block and set focus on it`s first input
*/
if (!this.blocks.length) {
this.currentBlockIndex = -1;
this.insert();
this.currentBlock.firstInput.focus();
}
}
/**
* Split current Block
* 1. Extract content from Caret position to the Block`s end
* 2. Insert a new Block below current one with extracted content
*
* @return {Block}
*/
public split(): Block {
const extractedFragment = this.Editor.Caret.extractFragmentFromCaretPosition();
const wrapper = $.make('div');
wrapper.append(extractedFragment);
/**
* @todo make object in accordance with Tool
*/
const data = {
text: $.isEmpty(wrapper) ? '' : wrapper.innerHTML,
};
/**
* Renew current Block
* @type {Block}
*/
return this.insert(this.config.initialBlock, data);
}
/**
* Replace current working block
*
* @param {String} toolName plugin name
* @param {Object} data plugin data
*
* @return {Block}
*/
public replace(toolName: string, data: BlockToolData = {}): Block {
const block = this.composeBlock(toolName, data);
this._blocks.insert(this.currentBlockIndex, block, true);
return block;
}
/**
* Returns Block by passed index
* @param {Number} index
* @return {Block}
*/
public getBlockByIndex(index): Block {
return this._blocks[index];
}
/**
* Get Block instance by html element
* @param {Node} element
* @returns {Block}
*/
public getBlock(element: HTMLElement): Block {
if (!$.isElement(element)) {
element = element.parentNode as HTMLElement;
}
const nodes = this._blocks.nodes,
firstLevelBlock = element.closest(`.${Block.CSS.wrapper}`),
index = nodes.indexOf(firstLevelBlock as HTMLElement);
if (index >= 0) {
return this._blocks[index];
}
}
/**
* Remove selection from all Blocks then highlight only Current Block
*/
public highlightCurrentNode(): void {
/**
* Remove previous selected Block's state
*/
this.clearFocused();
/**
* Mark current Block as selected
* @type {boolean}
*/
this.currentBlock.focused = true;
}
/**
* Remove selection from all Blocks
*/
public clearFocused(): void {
this.blocks.forEach( (block) => block.focused = false);
}
/**
* 1) Find first-level Block from passed child Node
* 2) Mark it as current
*
* @param {Node} childNode - look ahead from this node.
* @param {string} caretPosition - position where to set caret
* @throws Error - when passed Node is not included at the Block
*/
public setCurrentBlockByChildNode(childNode: Node, caretPosition: string = Caret.positions.DEFAULT): void {
/**
* If node is Text TextNode
*/
if (!$.isElement(childNode)) {
childNode = childNode.parentNode;
}
const parentFirstLevelBlock = (childNode as HTMLElement).closest(`.${Block.CSS.wrapper}`);
if (parentFirstLevelBlock) {
/**
* Update current Block's index
* @type {number}
*/
this.currentBlockIndex = this._blocks.nodes.indexOf(parentFirstLevelBlock as HTMLElement);
this.Editor.Caret.setToInput(childNode as HTMLElement, caretPosition);
} else {
throw new Error('Can not find a Block from this child Node');
}
}
/**
* Return block which contents passed node
*
* @param {Node} childNode
* @return {Block}
*/
public getBlockByChildNode(childNode: Node): Block {
/**
* If node is Text TextNode
*/
if (!$.isElement(childNode)) {
childNode = childNode.parentNode;
}
const firstLevelBlock = (childNode as HTMLElement).closest(`.${Block.CSS.wrapper}`);
return this.blocks.find((block) => block.holder === firstLevelBlock);
}
/**
* Swap Blocks Position
* @param {Number} fromIndex
* @param {Number} toIndex
*/
public swap(fromIndex, toIndex): void {
/** Move up current Block */
this._blocks.swap(fromIndex, toIndex);
/** Now actual block moved up so that current block index decreased */
this.currentBlockIndex = toIndex;
}
/**
* Sets current Block Index -1 which means unknown
* and clear highlightings
*/
public dropPointer(): void {
this.currentBlockIndex = -1;
this.clearFocused();
}
/**
* Clears Editor
* @param {boolean} needAddInitialBlock - 1) in internal calls (for example, in api.blocks.render)
* we don't need to add empty initial block
* 2) in api.blocks.clear we should add empty block
*/
public clear(needAddInitialBlock: boolean = false): void {
this._blocks.removeAll();
this.dropPointer();
if (needAddInitialBlock) {
this.insert(this.config.initialBlock);
}
}
/**
* Bind Events
* @param {Object} block
*/
private bindEvents(block: Block): void {
const {BlockEvents, Listeners} = this.Editor;
Listeners.on(block.holder, 'keydown', (event) => BlockEvents.keydown(event as KeyboardEvent), true);
Listeners.on(block.holder, 'mouseup', (event) => BlockEvents.mouseUp(event));
Listeners.on(block.holder, 'keyup', (event) => BlockEvents.keyup(event));
Listeners.on(block.holder, 'dragover', (event) => BlockEvents.dragOver(event as DragEvent));
Listeners.on(block.holder, 'dragleave', (event) => BlockEvents.dragLeave(event as DragEvent));
}
}

View file

@ -5,9 +5,9 @@
* @module BlockSelection * @module BlockSelection
* @version 1.0.0 * @version 1.0.0
*/ */
declare var Module: any; import Module from '../__module';
declare var _: any; import _ from '../utils';
declare var $: any; import $ from '../dom';
import SelectionUtils from '../selection'; import SelectionUtils from '../selection';

View file

@ -10,26 +10,15 @@
*/ */
import Selection from '../selection'; import Selection from '../selection';
import Module from '../__module';
import Block from '../block';
import $ from '../dom';
import _ from '../utils';
/** /**
* @typedef {Caret} Caret * @typedef {Caret} Caret
*/ */
export default class Caret extends Module { export default class Caret extends Module {
/**
* @constructor
*/
constructor({config}) {
super({config});
}
/**
* Elements styles that can be useful for Caret Module
*/
static get CSS() {
return {
shadowCaret: 'cdx-shadow-caret'
};
};
/** /**
* Allowed caret positions in input * Allowed caret positions in input
@ -37,14 +26,130 @@ export default class Caret extends Module {
* @static * @static
* @returns {{START: string, END: string, DEFAULT: string}} * @returns {{START: string, END: string, DEFAULT: string}}
*/ */
static get positions() { public static get positions(): {START: string, END: string, DEFAULT: string} {
return { return {
START: 'start', START: 'start',
END: 'end', END: 'end',
DEFAULT: 'default' DEFAULT: 'default',
}; };
} }
/**
* Elements styles that can be useful for Caret Module
*/
private static get CSS(): {shadowCaret: string} {
return {
shadowCaret: 'cdx-shadow-caret',
};
}
/**
* Get's deepest first node and checks if offset is zero
* @return {boolean}
*/
public get isAtStart(): boolean {
/**
* Don't handle ranges
*/
if (!Selection.isCollapsed) {
return false;
}
const selection = Selection.get(),
anchorNode = selection.anchorNode,
firstNode = $.getDeepestNode(this.Editor.BlockManager.currentBlock.currentInput);
/** In case lastNode is native input */
if ($.isNativeInput(firstNode)) {
return (firstNode as HTMLInputElement).selectionEnd === 0;
}
/**
* 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
firstLetterPosition = 0;
}
/**
* In case of
* <div contenteditable>
* <p><b></b></p> <-- first (and deepest) node is <b></b>
* |adaddad <-- anchor node
* </div>
*/
if ($.isEmpty(firstNode)) {
const leftSiblings = this.getHigherLevelSiblings(anchorNode as HTMLElement, 'left'),
nothingAtLeft = leftSiblings.every( (node) => $.isEmpty(node) );
if (nothingAtLeft && selection.anchorOffset === firstLetterPosition) {
return true;
}
}
/**
* We use <= comparison for case:
* "| Hello" <--- selection.anchorOffset is 0, but firstLetterPosition is 1
*/
return firstNode === null || anchorNode === firstNode && selection.anchorOffset <= firstLetterPosition;
}
/**
* Get's deepest last node and checks if offset is last node text length
* @return {boolean}
*/
public get isAtEnd(): boolean {
/**
* Don't handle ranges
*/
if (!Selection.isCollapsed) {
return false;
}
const selection = Selection.get(),
anchorNode = selection.anchorNode,
lastNode = $.getDeepestNode(this.Editor.BlockManager.currentBlock.currentInput, true);
/** In case lastNode is native input */
if ($.isNativeInput(lastNode)) {
return (lastNode as HTMLInputElement).selectionEnd === (lastNode as HTMLInputElement).value.length;
}
/**
* In case of
* <div contenteditable>
* adaddad| <-- anchor node
* <p><b></b></p> <-- first (and deepest) node is <b></b>
* </div>
*/
if ($.isEmpty(lastNode)) {
const leftSiblings = this.getHigherLevelSiblings(anchorNode as HTMLElement, 'right'),
nothingAtRight = leftSiblings.every( (node) => $.isEmpty(node) );
if (nothingAtRight && selection.anchorOffset === anchorNode.textContent.length) {
return true;
}
}
/**
* Workaround case:
* hello | <--- anchorOffset will be 5, but textContent.length will be 6.
* Why not regular .trim():
* in case of ' hello |' trim() will also remove space at the beginning, so length will be lower than anchorOffset
*/
const rightTrimmedText = lastNode.textContent.replace(/\s+$/, '');
/**
* We use >= comparison for case:
* "Hello |" <--- selection.anchorOffset is 7, but rightTrimmedText is 6
*/
return anchorNode === lastNode && selection.anchorOffset >= rightTrimmedText.length;
}
/** /**
* Method gets Block instance and puts caret to the text node with offset * Method gets Block instance and puts caret to the text node with offset
* There two ways that method applies caret position: * There two ways that method applies caret position:
@ -52,14 +157,15 @@ export default class Caret extends Module {
* - last found text node: sets at the end of the node. Also, you can customize the behaviour * - last found text node: sets at the end of the node. Also, you can customize the behaviour
* *
* @param {Block} block - Block class * @param {Block} block - Block class
* @param {String} position - position where to set caret. If default - leave default behaviour and apply offset if it's passed * @param {String} position - position where to set caret.
* If default - leave default behaviour and apply offset if it's passed
* @param {Number} offset - caret offset regarding to the text node * @param {Number} offset - caret offset regarding to the text node
*/ */
setToBlock(block, position = Caret.positions.DEFAULT, offset = 0) { public setToBlock(block: Block, position: string = Caret.positions.DEFAULT, offset: number = 0): void {
const {BlockManager} = this.Editor; const {BlockManager} = this.Editor;
let element; let element;
switch(position) { switch (position) {
case Caret.positions.START: case Caret.positions.START:
element = block.firstInput; element = block.firstInput;
break; break;
@ -91,7 +197,7 @@ export default class Caret extends Module {
* @todo try to fix via Promises or use querySelectorAll to not to use timeout * @todo try to fix via Promises or use querySelectorAll to not to use timeout
*/ */
_.delay( () => { _.delay( () => {
this.set(nodeToSet, offset); this.set(nodeToSet as HTMLElement, offset);
}, 20)(); }, 20)();
BlockManager.setCurrentBlockByChildNode(block.holder); BlockManager.setCurrentBlockByChildNode(block.holder);
@ -102,27 +208,28 @@ export default class Caret extends Module {
* Set caret to the current input of current Block. * Set caret to the current input of current Block.
* *
* @param {HTMLElement} input - input where caret should be set * @param {HTMLElement} input - input where caret should be set
* @param {String} position - position of the caret. If default - leave default behaviour and apply offset if it's passed * @param {String} position - position of the caret.
* If default - leave default behaviour and apply offset if it's passed
* @param {number} offset - caret offset regarding to the text node * @param {number} offset - caret offset regarding to the text node
*/ */
setToInput(input, position = Caret.positions.DEFAULT, offset = 0) { public setToInput(input: HTMLElement, position: string = Caret.positions.DEFAULT, offset: number = 0): void {
const {currentBlock} = this.Editor.BlockManager; const {currentBlock} = this.Editor.BlockManager;
const nodeToSet = $.getDeepestNode(input); const nodeToSet = $.getDeepestNode(input);
switch (position) { switch (position) {
case Caret.positions.START: case Caret.positions.START:
this.set(nodeToSet, 0); this.set(nodeToSet as HTMLElement, 0);
break; break;
case Caret.positions.END: case Caret.positions.END:
const contentLength = $.getContentLength(nodeToSet); const contentLength = $.getContentLength(nodeToSet);
this.set(nodeToSet, contentLength); this.set(nodeToSet as HTMLElement, contentLength);
break; break;
default: default:
if (offset) { if (offset) {
this.set(nodeToSet, offset); this.set(nodeToSet as HTMLElement, offset);
} }
} }
@ -131,17 +238,17 @@ export default class Caret extends Module {
/** /**
* Creates Document Range and sets caret to the element with offset * Creates Document Range and sets caret to the element with offset
* @param {Element} element - target node. * @param {HTMLElement} element - target node.
* @param {Number} offset - offset * @param {Number} offset - offset
*/ */
set( element, offset = 0) { public set(element: HTMLElement, offset: number = 0): void {
const range = document.createRange(), const range = document.createRange(),
selection = Selection.get(); selection = Selection.get();
/** if found deepest node is native input */ /** if found deepest node is native input */
if ($.isNativeInput(element)) { if ($.isNativeInput(element)) {
element.focus(); element.focus();
element.selectionStart = element.selectionEnd = offset; (element as HTMLInputElement).selectionStart = (element as HTMLInputElement).selectionEnd = offset;
return; return;
} }
@ -151,23 +258,23 @@ export default class Caret extends Module {
selection.removeAllRanges(); selection.removeAllRanges();
selection.addRange(range); selection.addRange(range);
/** If new cursor position is not visible, scroll to it */ /** If new cursor position is not visible, scroll to it */
const {top, bottom} = range.getBoundingClientRect(); const {top, bottom} = range.getBoundingClientRect();
const {innerHeight} = window; const {innerHeight} = window;
if (top < 0) window.scrollBy(0, top); if (top < 0) { window.scrollBy(0, top); }
if (bottom > innerHeight) window.scrollBy(0, bottom - innerHeight); if (bottom > innerHeight) { window.scrollBy(0, bottom - innerHeight); }
}; }
/** /**
* Set Caret to the last Block * Set Caret to the last Block
* If last block is not empty, append another empty block * If last block is not empty, append another empty block
*/ */
setToTheLastBlock() { public setToTheLastBlock(): void {
let lastBlock = this.Editor.BlockManager.lastBlock; const lastBlock = this.Editor.BlockManager.lastBlock;
if (!lastBlock) return; if (!lastBlock) {
return;
}
/** /**
* If last block is empty and it is an initialBlock, set to that. * If last block is empty and it is an initialBlock, set to that.
@ -185,18 +292,17 @@ export default class Caret extends Module {
/** /**
* Extract content fragment of current Block from Caret position to the end of the Block * Extract content fragment of current Block from Caret position to the end of the Block
*/ */
extractFragmentFromCaretPosition() { public extractFragmentFromCaretPosition(): void|DocumentFragment {
let selection = Selection.get(); const selection = Selection.get();
if (selection.rangeCount) { if (selection.rangeCount) {
const selectRange = selection.getRangeAt(0); const selectRange = selection.getRangeAt(0);
const currentBlockInput = this.Editor.BlockManager.currentBlock.currentInput; const currentBlockInput = this.Editor.BlockManager.currentBlock.currentInput;
selectRange.deleteContents(); selectRange.deleteContents();
if (currentBlockInput) { if (currentBlockInput) {
let range = selectRange.cloneRange(true); const range = selectRange.cloneRange();
range.selectNodeContents(currentBlockInput); range.selectNodeContents(currentBlockInput);
range.setStart(selectRange.endContainer, selectRange.endOffset); range.setStart(selectRange.endContainer, selectRange.endOffset);
@ -205,47 +311,6 @@ export default class Caret extends Module {
} }
} }
/**
* Get all first-level (first child of [contenteditabel]) siblings from passed node
* Then you can check it for emptiness
*
* @example
* <div contenteditable>
* <p></p> |
* <p></p> | left first-level siblings
* <p></p> |
* <blockquote><a><b>adaddad</b><a><blockquote> <-- passed node for example <b>
* <p></p> |
* <p></p> | right first-level siblings
* <p></p> |
* </div>
*
* @return {Element[]}
*/
getHigherLevelSiblings(from, direction ) {
let current = from,
siblings = [];
/**
* Find passed node's firs-level parent (in example - blockquote)
*/
while (current.parentNode && current.parentNode.contentEditable !== 'true') {
current = current.parentNode;
}
let sibling = direction === 'left' ? 'previousSibling' : 'nextSibling';
/**
* Find all left/right siblings
*/
while (current[sibling]) {
current = current[sibling];
siblings.push(current);
}
return siblings;
}
/** /**
* Set's caret to the next Block or Tool`s input * Set's caret to the next Block or Tool`s input
* Before moving caret, we should check if caret position is at the end of Plugins node * Before moving caret, we should check if caret position is at the end of Plugins node
@ -255,7 +320,7 @@ export default class Caret extends Module {
* *
* @return {Boolean} * @return {Boolean}
*/ */
navigateNext(force = false) { public navigateNext(force: boolean = false): boolean {
const {currentBlock, nextContentfulBlock} = this.Editor.BlockManager; const {currentBlock, nextContentfulBlock} = this.Editor.BlockManager;
const {nextInput} = currentBlock; const {nextInput} = currentBlock;
@ -291,9 +356,14 @@ export default class Caret extends Module {
* *
* @return {Boolean} * @return {Boolean}
*/ */
navigatePrevious(force = false) { public navigatePrevious(force: boolean = false): boolean {
const {currentBlock, previousContentfulBlock} = this.Editor.BlockManager; const {currentBlock, previousContentfulBlock} = this.Editor.BlockManager;
const {previousInput} = currentBlock || {};
if (!currentBlock) {
return false;
}
const {previousInput} = currentBlock;
if (!previousContentfulBlock && !previousInput) { if (!previousContentfulBlock && !previousInput) {
return false; return false;
@ -316,121 +386,12 @@ export default class Caret extends Module {
return false; return false;
} }
/**
* Get's deepest first node and checks if offset is zero
* @return {boolean}
*/
get isAtStart() {
/**
* Don't handle ranges
*/
if (!Selection.isCollapsed) {
return false;
}
let selection = Selection.get(),
anchorNode = selection.anchorNode,
firstNode = $.getDeepestNode(this.Editor.BlockManager.currentBlock.currentInput);
/** In case lastNode is native input */
if ($.isNativeInput(firstNode)) {
return firstNode.selectionEnd === 0;
}
/**
* 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
firstLetterPosition = 0;
}
/**
* In case of
* <div contenteditable>
* <p><b></b></p> <-- first (and deepest) node is <b></b>
* |adaddad <-- anchor node
* </div>
*/
if ($.isEmpty(firstNode)) {
let leftSiblings = this.getHigherLevelSiblings(anchorNode, 'left'),
nothingAtLeft = leftSiblings.every( node => $.isEmpty(node) );
if (nothingAtLeft && selection.anchorOffset === firstLetterPosition) {
return true;
}
}
/**
* We use <= comparison for case:
* "| Hello" <--- selection.anchorOffset is 0, but firstLetterPosition is 1
*/
return firstNode === null || anchorNode === firstNode && selection.anchorOffset <= firstLetterPosition;
}
/**
* Get's deepest last node and checks if offset is last node text length
* @return {boolean}
*/
get isAtEnd() {
/**
* Don't handle ranges
*/
if (!Selection.isCollapsed) {
return false;
}
let selection = Selection.get(),
anchorNode = selection.anchorNode,
lastNode = $.getDeepestNode(this.Editor.BlockManager.currentBlock.currentInput, true);
/** In case lastNode is native input */
if ($.isNativeInput(lastNode)) {
return lastNode.selectionEnd === lastNode.value.length;
}
/**
* In case of
* <div contenteditable>
* adaddad| <-- anchor node
* <p><b></b></p> <-- first (and deepest) node is <b></b>
* </div>
*/
if ($.isEmpty(lastNode)) {
let leftSiblings = this.getHigherLevelSiblings(anchorNode, 'right'),
nothingAtRight = leftSiblings.every( node => $.isEmpty(node) );
if (nothingAtRight && selection.anchorOffset === anchorNode.textContent.length) {
return true;
}
}
/**
* Workaround case:
* hello | <--- anchorOffset will be 5, but textContent.length will be 6.
* Why not regular .trim():
* in case of ' hello |' trim() will also remove space at the beginning, so length will be lower than anchorOffset
*/
let rightTrimmedText = lastNode.textContent.replace(/\s+$/, '');
/**
* We use >= comparison for case:
* "Hello |" <--- selection.anchorOffset is 7, but rightTrimmedText is 6
*/
return anchorNode === lastNode && selection.anchorOffset >= rightTrimmedText.length;
}
/** /**
* Inserts shadow element after passed element where caret can be placed * Inserts shadow element after passed element where caret can be placed
* @param {Node} element * @param {Node} element
*/ */
createShadow(element) { public createShadow(element): void {
let shadowCaret = document.createElement('span'); const shadowCaret = document.createElement('span');
shadowCaret.classList.add(Caret.CSS.shadowCaret); shadowCaret.classList.add(Caret.CSS.shadowCaret);
element.insertAdjacentElement('beforeEnd', shadowCaret); element.insertAdjacentElement('beforeEnd', shadowCaret);
@ -438,10 +399,10 @@ export default class Caret extends Module {
/** /**
* Restores caret position * Restores caret position
* @param {Node} element * @param {HTMLElement} element
*/ */
restoreCaret(element) { public restoreCaret(element: HTMLElement): void {
let shadowCaret = element.querySelector(`.${Caret.CSS.shadowCaret}`); const shadowCaret = element.querySelector(`.${Caret.CSS.shadowCaret}`);
if (!shadowCaret) { if (!shadowCaret) {
return; return;
@ -455,15 +416,56 @@ export default class Caret extends Module {
* - select shadowed span * - select shadowed span
* - use extractContent to remove it from DOM * - use extractContent to remove it from DOM
*/ */
let sel = new Selection(); const sel = new Selection();
sel.expandToTag(shadowCaret); sel.expandToTag(shadowCaret as HTMLElement);
setTimeout(() => { setTimeout(() => {
let newRange = document.createRange(); const newRange = document.createRange();
newRange.selectNode(shadowCaret); newRange.selectNode(shadowCaret);
newRange.extractContents(); newRange.extractContents();
}, 50); }, 50);
} }
/**
* Get all first-level (first child of [contenteditabel]) siblings from passed node
* Then you can check it for emptiness
*
* @example
* <div contenteditable>
* <p></p> |
* <p></p> | left first-level siblings
* <p></p> |
* <blockquote><a><b>adaddad</b><a><blockquote> <-- passed node for example <b>
* <p></p> |
* <p></p> | right first-level siblings
* <p></p> |
* </div>
*
* @return {Element[]}
*/
private getHigherLevelSiblings(from: HTMLElement, direction?: string): HTMLElement[] {
let current = from;
const siblings = [];
/**
* Find passed node's firs-level parent (in example - blockquote)
*/
while (current.parentNode && (current.parentNode as HTMLElement).contentEditable !== 'true') {
current = current.parentNode as HTMLElement;
}
const sibling = direction === 'left' ? 'previousSibling' : 'nextSibling';
/**
* Find all left/right siblings
*/
while (current[sibling]) {
current = current[sibling] as HTMLElement;
siblings.push(current);
}
return siblings;
}
} }

View file

@ -1,6 +1,7 @@
import SelectionUtils from '../selection'; import SelectionUtils from '../selection';
declare var Module: any; import Module from '../__module';
import Caret from './caret';
export default class DragNDrop extends Module { export default class DragNDrop extends Module {
@ -67,9 +68,9 @@ export default class DragNDrop extends Module {
* If drop target (error will be thrown) is not part of the Block, set last Block as current. * If drop target (error will be thrown) is not part of the Block, set last Block as current.
*/ */
try { try {
BlockManager.setCurrentBlockByChildNode(dropEvent.target, 'end'); BlockManager.setCurrentBlockByChildNode(dropEvent.target as Node, Caret.positions.END);
} catch (e) { } catch (e) {
BlockManager.setCurrentBlockByChildNode(BlockManager.lastBlock.holder, 'end'); BlockManager.setCurrentBlockByChildNode(BlockManager.lastBlock.holder, Caret.positions.END);
} }
Paste.processDataTransfer(dropEvent.dataTransfer, true); Paste.processDataTransfer(dropEvent.dataTransfer, true);

View file

@ -1,3 +1,5 @@
import Module from '../__module';
/** /**
* @module eventDispatcher * @module eventDispatcher
* *
@ -12,13 +14,12 @@
* @property {Object} subscribers - all subscribers grouped by event name * @property {Object} subscribers - all subscribers grouped by event name
*/ */
export default class Events extends Module { export default class Events extends Module {
/** /**
* @constructor * Object with events` names as key and array of callback functions as value
* @type {{}}
*/ */
constructor({config}) { private subscribers: {[name: string]: Array<(data?: any) => void>} = {};
super({config});
this.subscribers = {};
}
/** /**
* Subscribe any event on callback * Subscribe any event on callback
@ -26,7 +27,7 @@ export default class Events extends Module {
* @param {String} eventName - event name * @param {String} eventName - event name
* @param {Function} callback - subscriber * @param {Function} callback - subscriber
*/ */
on(eventName, callback) { public on(eventName: string, callback: (data: any) => void) {
if (!(eventName in this.subscribers)) { if (!(eventName in this.subscribers)) {
this.subscribers[eventName] = []; this.subscribers[eventName] = [];
} }
@ -41,13 +42,13 @@ export default class Events extends Module {
* @param {String} eventName - event name * @param {String} eventName - event name
* @param {Object} data - subscribers get this data when they were fired * @param {Object} data - subscribers get this data when they were fired
*/ */
emit(eventName, data) { public emit(eventName: string, data?: any): void {
if (!this.subscribers[eventName]) { if (!this.subscribers[eventName]) {
return; return;
} }
this.subscribers[eventName].reduce(function (previousData, currentHandler) { this.subscribers[eventName].reduce((previousData, currentHandler) => {
let newData = currentHandler(previousData); const newData = currentHandler(previousData);
return newData ? newData : previousData; return newData ? newData : previousData;
}, data); }, data);
@ -59,8 +60,8 @@ export default class Events extends Module {
* @param eventName * @param eventName
* @param callback * @param callback
*/ */
off(eventName, callback) { public off(eventName: string, callback: (data: any) => void): void {
for(let i = 0; i < this.subscribers[eventName].length; i++) { for (let i = 0; i < this.subscribers[eventName].length; i++) {
if (this.subscribers[eventName][i] === callback) { if (this.subscribers[eventName][i] === callback) {
delete this.subscribers[eventName][i]; delete this.subscribers[eventName][i];
break; break;
@ -72,7 +73,7 @@ export default class Events extends Module {
* Destroyer * Destroyer
* clears subsribers list * clears subsribers list
*/ */
destroy() { public destroy(): void {
this.subscribers = null; this.subscribers = null;
} }
} }

View file

@ -1,174 +0,0 @@
/**
* Codex Editor Listeners module
*
* @module Listeners
*
* Module-decorator for event listeners assignment
*
* @author Codex Team
* @version 2.0.0
*/
/**
* @typedef {Listeners} Listeners
* @property {Array} allListeners
*/
export default class Listeners extends Module {
/**
* @constructor
* @param {EditorConfig} config
*/
constructor({config}) {
super({config});
this.allListeners = [];
}
/**
* Assigns event listener on element
*
* @param {Element} element - DOM element that needs to be listened
* @param {String} eventType - event type
* @param {Function} handler - method that will be fired on event
* @param {Boolean} useCapture - use event bubbling
*/
on(element, eventType, handler, useCapture = false) {
let assignedEventData = {
element,
eventType,
handler,
useCapture
};
let alreadyExist = this.findOne(element, eventType, handler);
if (alreadyExist) return;
this.allListeners.push(assignedEventData);
element.addEventListener(eventType, handler, useCapture);
}
/**
* Removes event listener from element
*
* @param {Element} element - DOM element that we removing listener
* @param {String} eventType - event type
* @param {Function} handler - remove handler, if element listens several handlers on the same event type
* @param {Boolean} useCapture - use event bubbling
*/
off(element, eventType, handler, useCapture = false) {
let existingListeners = this.findAll(element, eventType, handler);
for (let i = 0; i < existingListeners.length; i++) {
let index = this.allListeners.indexOf(existingListeners[i]);
if (index > 0) {
this.allListeners.splice(index, 1);
}
}
element.removeEventListener(eventType, handler, useCapture);
}
/**
* Search method: looks for listener by passed element
* @param {Element} element - searching element
* @returns {Array} listeners that found on element
*/
findByElement(element) {
let listenersOnElement = [];
for (let i = 0; i < this.allListeners.length; i++) {
let listener = this.allListeners[i];
if (listener.element === element) {
listenersOnElement.push(listener);
}
}
return listenersOnElement;
}
/**
* Search method: looks for listener by passed event type
* @param {String} eventType
* @return {Array} listeners that found on element
*/
findByType(eventType) {
let listenersWithType = [];
for (let i = 0; i < this.allListeners.length; i++) {
let listener = this.allListeners[i];
if (listener.type === eventType) {
listenersWithType.push(listener);
}
}
return listenersWithType;
}
/**
* Search method: looks for listener by passed handler
* @param {Function} handler
* @return {Array} listeners that found on element
*/
findByHandler(handler) {
let listenersWithHandler = [];
for (let i = 0; i < this.allListeners.length; i++) {
let listener = this.allListeners[i];
if (listener.handler === handler) {
listenersWithHandler.push(listener);
}
}
return listenersWithHandler;
}
/**
* @param {Element} element
* @param {String} eventType
* @param {Function} handler
* @return {Element|null}
*/
findOne(element, eventType, handler) {
let foundListeners = this.findAll(element, eventType, handler);
return foundListeners.length > 0 ? foundListeners[0] : null;
}
/**
* @param {Element} element
* @param {String} eventType
* @param {Function} handler
* @return {Array}
*/
findAll(element, eventType, handler) {
let found,
foundByElements = element ? this.findByElement(element) : [];
// foundByEventType = eventType ? this.findByType(eventType) : [],
// foundByHandler = handler ? this.findByHandler(handler) : [];
if (element && eventType && handler) {
found = foundByElements.filter( event => event.eventType === eventType && event.handler === handler );
} else if (element && eventType) {
found = foundByElements.filter( event => event.eventType === eventType);
} else {
found = foundByElements;
}
return found;
}
/**
* Removes all listeners
*/
removeAll() {
this.allListeners.map( (current) => {
current.element.removeEventListener(current.eventType, current.handler);
});
this.allListeners = [];
}
}

View file

@ -0,0 +1,192 @@
import Module from '../__module';
import {EditorConfig} from '../../../types';
/**
* Event listener information
*/
export interface ListenerData {
/**
* Element where to listen to dispatched events
*/
element: EventTarget;
/**
* Event to listen
*/
eventType: string;
/**
* Event handler
*
* @param {Event} event
*/
handler: (event: Event) => void;
/**
* Should event bubbling be used or not
*/
useCapture: boolean;
}
/**
* Codex Editor Listeners module
*
* @module Listeners
*
* Module-decorator for event listeners assignment
*
* @author Codex Team
* @version 2.0.0
*/
/**
* @typedef {Listeners} Listeners
* @property {Array} allListeners
*/
export default class Listeners extends Module {
/**
* Stores all listeners data to find/remove/process it
* @type {ListenerData[]}
*/
private allListeners: ListenerData[] = [];
/**
* Assigns event listener on element
*
* @param {EventTarget} element - DOM element that needs to be listened
* @param {String} eventType - event type
* @param {Function} handler - method that will be fired on event
* @param {Boolean} useCapture - use event bubbling
*/
public on(
element: EventTarget,
eventType: string,
handler: (event: Event) => void,
useCapture: boolean = false,
): void {
const assignedEventData = {
element,
eventType,
handler,
useCapture,
};
const alreadyExist = this.findOne(element, eventType, handler);
if (alreadyExist) { return; }
this.allListeners.push(assignedEventData);
element.addEventListener(eventType, handler, useCapture);
}
/**
* Removes event listener from element
*
* @param {EventTarget} element - DOM element that we removing listener
* @param {String} eventType - event type
* @param {Function} handler - remove handler, if element listens several handlers on the same event type
* @param {Boolean} useCapture - use event bubbling
*/
public off(
element: EventTarget,
eventType: string,
handler: (event: Event) => void,
useCapture: boolean = false,
): void {
const existingListeners = this.findAll(element, eventType, handler);
existingListeners.forEach((listener, i) => {
const index = this.allListeners.indexOf(existingListeners[i]);
if (index > 0) {
this.allListeners.splice(index, 1);
}
});
element.removeEventListener(eventType, handler, useCapture);
}
/**
* @param {EventTarget} element
* @param {String} eventType
* @param {Function} handler
* @return {EventTarget|null}
*/
public findOne(element: EventTarget, eventType: string, handler: (event: Event) => void): ListenerData {
const foundListeners = this.findAll(element, eventType, handler);
return foundListeners.length > 0 ? foundListeners[0] : null;
}
/**
* @param {EventTarget} element
* @param {String} eventType
* @param {Function} handler
* @return {Array}
*/
public findAll(element: EventTarget, eventType: string, handler: (event: Event) => void): ListenerData[] {
let found;
const foundByEventTargets = element ? this.findByEventTarget(element) : [];
if (element && eventType && handler) {
found = foundByEventTargets.filter( (event) => event.eventType === eventType && event.handler === handler );
} else if (element && eventType) {
found = foundByEventTargets.filter( (event) => event.eventType === eventType);
} else {
found = foundByEventTargets;
}
return found;
}
/**
* Removes all listeners
*/
public removeAll(): void {
this.allListeners.map( (current) => {
current.element.removeEventListener(current.eventType, current.handler);
});
this.allListeners = [];
}
/**
* Search method: looks for listener by passed element
* @param {EventTarget} element - searching element
* @returns {Array} listeners that found on element
*/
private findByEventTarget(element: EventTarget): ListenerData[] {
return this.allListeners.filter((listener) => {
if (listener.element === element) {
return listener;
}
});
}
/**
* Search method: looks for listener by passed event type
* @param {String} eventType
* @return {Array} listeners that found on element
*/
private findByType(eventType: string): ListenerData[] {
return this.allListeners.filter((listener) => {
if (listener.eventType === eventType) {
return listener;
}
});
}
/**
* Search method: looks for listener by passed handler
* @param {Function} handler
* @return {Array} listeners that found on element
*/
private findByHandler(handler: (event: Event) => void): ListenerData[] {
return this.allListeners.filter((listener) => {
if (listener.handler === handler) {
return listener;
}
});
}
}

View file

@ -5,10 +5,8 @@
* and gives opportunity to handle outside * and gives opportunity to handle outside
*/ */
import IEditorConfig from '../interfaces/editor-config'; import Module from '../__module';
import _ from '../utils';
declare const Module: any;
declare const _: any;
export default class ModificationsObserver extends Module { export default class ModificationsObserver extends Module {
@ -23,17 +21,9 @@ export default class ModificationsObserver extends Module {
* @type {Function} * @type {Function}
*/ */
private mutationDebouncer = _.debounce( () => { private mutationDebouncer = _.debounce( () => {
this.config.onChange.call(); this.config.onChange();
}, ModificationsObserver.DebounceTimer); }, ModificationsObserver.DebounceTimer);
/**
* Constructor
* @param {IEditorConfig} config
*/
constructor({config}) {
super({config});
}
/** /**
* Clear timeout and set null to mutationDebouncer property * Clear timeout and set null to mutationDebouncer property
*/ */

View file

@ -1,76 +1,117 @@
import IBlockToolData from '../interfaces/tools/block-tool';
import IEditorConfig from '../interfaces/editor-config';
import CaretClass from './caret'; import CaretClass from './caret';
import SelectionUtils from '../selection'; import SelectionUtils from '../selection';
declare const Module: any; import Module from '../__module';
declare const $: any; import $ from '../dom';
declare const _: any; import _ from '../utils';
import {BlockToolData, PasteConfig} from '../../../types';
/** /**
* Tag substitute object. * Tag substitute object.
*
* @param {string} tool - name of related Tool
* @param {Function} handler - callback to handle pasted element
*/ */
interface ITagSubstitute { interface TagSubstitute {
/**
* Name of related Tool
* @type {string}
*/
tool: string; tool: string;
handler: (element: HTMLElement) => IBlockToolData;
/**
* Callback to handle pasted element
*
* @param {HTMLElement} element
* @return {BlockToolData}
*/
handler: (element: HTMLElement) => BlockToolData;
} }
/** /**
* Pattern substitute object. * Pattern substitute object.
*
* @param {string} key - pattern`s key
* @param {RegExp} pattern - pasted pattern
* @param {Function} handler - callback to handle pasted pattern
* @param {string} tool - name of related Tool
*/ */
interface IPatternSubstitute { interface PatternSubstitute {
/**
* Pattern`s key
* @type {string}
*/
key: string; key: string;
/**
* Pattern regexp
* @type {RegExp}
*/
pattern: RegExp; pattern: RegExp;
handler: (text: string, key: string) => IBlockToolData;
/**
* Callback to handle pasted pattern
*
* @param {string} text
* @param {string} key
* @return {BlockToolData}
*/
handler: (text: string, key: string) => BlockToolData;
/**
* Name of related Tool
* @type {string}
*/
tool: string; tool: string;
} }
/** /**
* Files` types substitutions object. * Files` types substitutions object.
*
* @param {string[]} extensions - array of extenstions Tool can handle
* @param {string[]} mimeTypes - array of MIME types Tool can handle
* @param {Function} handler - callback to handle pasted File
*/ */
interface IFilesSubstitution { interface FilesSubstitution {
/**
* Array of file extensions Tool can handle
* @type {string[]}
*/
extensions: string[]; extensions: string[];
/**
* Array of MIME types Tool can handle
* @type {string[]}
*/
mimeTypes: string[]; mimeTypes: string[];
handler: (file: File) => IBlockToolData;
/**
* Callback to handle pasted File
*
* @param {File} file
* @return {BlockToolData}
*/
handler: (file: File) => BlockToolData;
} }
/** /**
* Processed paste data object. * Processed paste data object.
*
* @param {string} tool - name of related Tool
* @param {HTMLElement} content - processed pasted content
* @param {boolean} isBlock - true if content should be inserted as new Block
* @param {Function} handler - callback that returns pasted data in IBlockToolData format
*/ */
interface IPasteData { interface PasteData {
/**
* Name of related Tool
* @type {string}
*/
tool: string; tool: string;
content: HTMLElement;
isBlock: boolean;
handler: (content: HTMLElement|string, patten?: RegExp) => IBlockToolData;
}
/** /**
* Tool onPaste configuration object * Pasted data. Processed and wrapped to HTML element
*/ * @type {HTMLElement}
interface IPasteConfig { */
tags?: string[]; content: HTMLElement;
handler?: (element: HTMLElement) => IBlockToolData;
patterns?: {[key: string]: RegExp}; /**
patternHandler?: (text: string, key: string) => IBlockToolData; * True if content should be inserted as new Block
files?: {extensions?: string[], mimeTypes?: string[]}; * @type {boolean}
fileHandler?: (file: File) => IBlockToolData; */
isBlock: boolean;
/**
* Callback that returns pasted data in BlockToolData format
*
* @param {HTMLElement | string} content
* @param {RegExp} patten
* @return {BlockToolData}
*/
handler: (content: HTMLElement|string, patten?: RegExp) => BlockToolData;
} }
/** /**
@ -89,7 +130,7 @@ export default class Paste extends Module {
/** /**
* Tags` substitutions parameters * Tags` substitutions parameters
*/ */
private toolsTags: {[tag: string]: ITagSubstitute} = {}; private toolsTags: {[tag: string]: TagSubstitute} = {};
/** /**
* Store tags to substitute by tool name * Store tags to substitute by tool name
@ -97,21 +138,13 @@ export default class Paste extends Module {
private tagsByTool: {[tools: string]: string[]} = {}; private tagsByTool: {[tools: string]: string[]} = {};
/** Patterns` substitutions parameters */ /** Patterns` substitutions parameters */
private toolsPatterns: IPatternSubstitute[] = []; private toolsPatterns: PatternSubstitute[] = [];
/** Files` substitutions parameters */ /** Files` substitutions parameters */
private toolsFiles: { private toolsFiles: {
[tool: string]: IFilesSubstitution, [tool: string]: FilesSubstitution,
} = {}; } = {};
/**
* @constructor
* @param {IEditorConfig} config
*/
constructor({config}) {
super({config});
}
/** /**
* Set onPaste callback and collect tools` paste configurations * Set onPaste callback and collect tools` paste configurations
* *
@ -126,6 +159,7 @@ export default class Paste extends Module {
* Handle pasted or dropped data transfer object * Handle pasted or dropped data transfer object
* *
* @param {DataTransfer} dataTransfer - pasted or dropped data transfer object * @param {DataTransfer} dataTransfer - pasted or dropped data transfer object
* @param {boolean} isDragNDrop
*/ */
public async processDataTransfer(dataTransfer: DataTransfer, isDragNDrop = false): Promise<void> { public async processDataTransfer(dataTransfer: DataTransfer, isDragNDrop = false): Promise<void> {
const { Sanitizer } = this.Editor; const { Sanitizer } = this.Editor;
@ -208,9 +242,9 @@ export default class Paste extends Module {
* Get tags to substitute by Tool * Get tags to substitute by Tool
* *
* @param {string} name - Tool name * @param {string} name - Tool name
* @param {IPasteConfig} toolPasteConfig - Tool onPaste configuration * @param {PasteConfig} toolPasteConfig - Tool onPaste configuration
*/ */
private getTagsConfig(name: string, toolPasteConfig: IPasteConfig): void { private getTagsConfig(name: string, toolPasteConfig: PasteConfig): void {
if (this.config.initialBlock === name && !toolPasteConfig.handler) { if (this.config.initialBlock === name && !toolPasteConfig.handler) {
_.log( _.log(
`«${name}» Tool must provide a paste handler.`, `«${name}» Tool must provide a paste handler.`,
@ -256,9 +290,9 @@ export default class Paste extends Module {
* Get files` types and extensions to substitute by Tool * Get files` types and extensions to substitute by Tool
* *
* @param {string} name - Tool name * @param {string} name - Tool name
* @param {IPasteConfig} toolPasteConfig - Tool onPaste configuration * @param {PasteConfig} toolPasteConfig - Tool onPaste configuration
*/ */
private getFilesConfig(name: string, toolPasteConfig: IPasteConfig): void { private getFilesConfig(name: string, toolPasteConfig: PasteConfig): void {
const {fileHandler, files = {}} = toolPasteConfig; const {fileHandler, files = {}} = toolPasteConfig;
let {extensions, mimeTypes} = files; let {extensions, mimeTypes} = files;
@ -304,9 +338,9 @@ export default class Paste extends Module {
* Get RegExp patterns to substitute by Tool * Get RegExp patterns to substitute by Tool
* *
* @param {string} name - Tool name * @param {string} name - Tool name
* @param {IPasteConfig} toolPasteConfig - Tool onPaste configuration * @param {PasteConfig} toolPasteConfig - Tool onPaste configuration
*/ */
private getPatternsConfig(name: string, toolPasteConfig: IPasteConfig): void { private getPatternsConfig(name: string, toolPasteConfig: PasteConfig): void {
if (!toolPasteConfig.patternHandler || _.isEmpty(toolPasteConfig.patterns)) { if (!toolPasteConfig.patternHandler || _.isEmpty(toolPasteConfig.patterns)) {
return; return;
} }
@ -351,7 +385,7 @@ export default class Paste extends Module {
return true; return true;
} }
const block = BlockManager.getBlock(element); const block = BlockManager.getBlock(element as HTMLElement);
return !block; return !block;
} }
@ -362,10 +396,6 @@ export default class Paste extends Module {
* @param {ClipboardEvent} event * @param {ClipboardEvent} event
*/ */
private handlePasteEvent = async (event: ClipboardEvent): Promise<void> => { private handlePasteEvent = async (event: ClipboardEvent): Promise<void> => {
const {
Editor: {Sanitizer, BlockManager, Tools, Caret},
} = this;
/** If target is native input or is not Block, use browser behaviour */ /** If target is native input or is not Block, use browser behaviour */
if ( if (
this.isNativeBehaviour(event.target) && !event.clipboardData.types.includes('Files') this.isNativeBehaviour(event.target) && !event.clipboardData.types.includes('Files')
@ -385,7 +415,7 @@ export default class Paste extends Module {
private async processFiles(items: DataTransferItemList) { private async processFiles(items: DataTransferItemList) {
const {BlockManager} = this.Editor; const {BlockManager} = this.Editor;
let dataToInsert: Array<{type: string, data: IBlockToolData}>; let dataToInsert: Array<{type: string, data: BlockToolData}>;
dataToInsert = await Promise.all( dataToInsert = await Promise.all(
Array Array
@ -483,9 +513,9 @@ export default class Paste extends Module {
* Split HTML string to blocks and return it as array of Block data * Split HTML string to blocks and return it as array of Block data
* *
* @param {string} innerHTML * @param {string} innerHTML
* @returns {IPasteData[]} * @returns {PasteData[]}
*/ */
private processHTML(innerHTML: string): IPasteData[] { private processHTML(innerHTML: string): PasteData[] {
const {Tools, Sanitizer} = this.Editor, const {Tools, Sanitizer} = this.Editor,
initialTool = this.config.initialBlock, initialTool = this.config.initialBlock,
wrapper = $.make('DIV'); wrapper = $.make('DIV');
@ -536,9 +566,9 @@ export default class Paste extends Module {
* Split plain text by new line symbols and return it as array of Block data * Split plain text by new line symbols and return it as array of Block data
* *
* @param {string} plain * @param {string} plain
* @returns {IPasteData[]} * @returns {PasteData[]}
*/ */
private processPlain(plain: string): IPasteData[] { private processPlain(plain: string): PasteData[] {
const {initialBlock} = this.config as {initialBlock: string}, const {initialBlock} = this.config as {initialBlock: string},
{Tools} = this.Editor; {Tools} = this.Editor;
@ -567,9 +597,9 @@ export default class Paste extends Module {
* 2. Insert new block if it is not the same type as current one * 2. Insert new block if it is not the same type as current one
* 3. Just insert text if there is no substitutions * 3. Just insert text if there is no substitutions
* *
* @param {IPasteData} dataToInsert * @param {PasteData} dataToInsert
*/ */
private async processSingleBlock(dataToInsert: IPasteData): Promise<void> { private async processSingleBlock(dataToInsert: PasteData): Promise<void> {
const initialTool = this.config.initialBlock, const initialTool = this.config.initialBlock,
{BlockManager, Caret, Sanitizer} = this.Editor, {BlockManager, Caret, Sanitizer} = this.Editor,
{content, tool} = dataToInsert; {content, tool} = dataToInsert;
@ -607,9 +637,9 @@ export default class Paste extends Module {
* Get patterns` matches * Get patterns` matches
* *
* @param {string} text * @param {string} text
* @returns Promise<{data: IBlockToolData, tool: string}> * @returns Promise<{data: BlockToolData, tool: string}>
*/ */
private async processPattern(text: string): Promise<{data: IBlockToolData, tool: string}> { private async processPattern(text: string): Promise<{data: BlockToolData, tool: string}> {
const pattern = this.toolsPatterns.find((substitute) => { const pattern = this.toolsPatterns.find((substitute) => {
const execResult = substitute.pattern.exec(text); const execResult = substitute.pattern.exec(text);
@ -630,11 +660,11 @@ export default class Paste extends Module {
/** /**
* *
* @param {IPasteData} data * @param {PasteData} data
* @param {Boolean} canReplaceCurrentBlock - if true and is current Block is empty, will replace current Block * @param {Boolean} canReplaceCurrentBlock - if true and is current Block is empty, will replace current Block
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async insertBlock(data: IPasteData, canReplaceCurrentBlock: boolean = false): Promise<void> { private async insertBlock(data: PasteData, canReplaceCurrentBlock: boolean = false): Promise<void> {
const blockData = await data.handler(data.content), const blockData = await data.handler(data.content),
{BlockManager, Caret} = this.Editor, {BlockManager, Caret} = this.Editor,
{currentBlock} = BlockManager; {currentBlock} = BlockManager;
@ -644,9 +674,9 @@ export default class Paste extends Module {
return; return;
} }
const Block = BlockManager.insert(data.tool, blockData); const block = BlockManager.insert(data.tool, blockData);
Caret.setToBlock(Block); Caret.setToBlock(block);
} }
/** /**

View file

@ -1,3 +1,7 @@
import Module from '../__module';
import _, {ChainData} from '../utils';
import {BlockToolData} from '../../../types';
/** /**
* Codex Editor Renderer Module * Codex Editor Renderer Module
* *
@ -7,14 +11,6 @@
* @version 2.0.0 * @version 2.0.0
*/ */
export default class Renderer extends Module { export default class Renderer extends Module {
/**
* @constructor
* @param {EditorConfig} config
*/
constructor({config}) {
super({config});
}
/** /**
* @typedef {Object} RendererBlocks * @typedef {Object} RendererBlocks
* @property {String} type - tool name * @property {String} type - tool name
@ -45,16 +41,10 @@ export default class Renderer extends Module {
* Make plugin blocks from array of plugin`s data * Make plugin blocks from array of plugin`s data
* @param {RendererBlocks[]} blocks * @param {RendererBlocks[]} blocks
*/ */
render(blocks) { public render(blocks: BlockToolData[]): Promise<void> {
let chainData = []; const chainData = blocks.map((block) => ({function: () => this.insertBlock(block)}));
for (let i = 0; i < blocks.length; i++) { return _.sequence(chainData as ChainData[]);
chainData.push({
function: () => this.insertBlock(blocks[i])
});
}
return _.sequence(chainData);
} }
/** /**
@ -63,20 +53,20 @@ export default class Renderer extends Module {
* Insert block to working zone * Insert block to working zone
* *
* @param {Object} item * @param {Object} item
* @returns {Promise.<T>} * @returns {Promise<void>}
* @private * @private
*/ */
insertBlock(item) { public async insertBlock(item): Promise<void> {
let tool = item.type, const tool = item.type;
data = item.data, const data = item.data;
settings = item.settings; const settings = item.settings;
if (tool in this.Editor.Tools.available) { if (tool in this.Editor.Tools.available) {
try { try {
this.Editor.BlockManager.insert(tool, data, settings); this.Editor.BlockManager.insert(tool, data, settings);
} catch (error) { } catch (error) {
_.log(`Block «${tool}» skipped because of plugins error`, 'warn', data); _.log(`Block «${tool}» skipped because of plugins error`, 'warn', data);
Promise.reject(error); throw Error(error);
} }
} else { } else {
/** /**
@ -86,7 +76,5 @@ export default class Renderer extends Module {
*/ */
_.log(`Tool «${tool}» is not found. Check 'tools' property at your initial CodeX Editor config.`, 'warn'); _.log(`Tool «${tool}» is not found. Check 'tools' property at your initial CodeX Editor config.`, 'warn');
} }
return Promise.resolve();
} }
} }

View file

@ -16,7 +16,8 @@
* {@link SanitizerConfig} * {@link SanitizerConfig}
*/ */
import ISanitizerConfig from '../interfaces/sanitizer-config'; import Module from '../__module';
import _ from '../utils';
/** /**
* @typedef {Object} SanitizerConfig * @typedef {Object} SanitizerConfig
@ -34,46 +35,30 @@ import ISanitizerConfig from '../interfaces/sanitizer-config';
* } * }
*/ */
declare const Module: any;
declare const _: any;
import HTMLJanitor from 'html-janitor'; import HTMLJanitor from 'html-janitor';
import IBlockToolData from '../interfaces/tools/block-tool-data'; import {BlockToolData, InlineToolConstructable, SanitizerConfig} from '../../../types';
import IInlineTool from '../interfaces/tools/inline-tool';
export default class Sanitizer extends Module { export default class Sanitizer extends Module {
/** /**
* Memoize tools config * Memoize tools config
*/ */
private configCache: {[toolName: string]: ISanitizerConfig} = {}; private configCache: {[toolName: string]: SanitizerConfig} = {};
/** /**
* Cached inline tools config * Cached inline tools config
*/ */
private inlineToolsConfigCache: ISanitizerConfig | null = null; private inlineToolsConfigCache: SanitizerConfig | null = null;
/**
* Initializes Sanitizer module
* Sets default configuration if custom not exists
*
* @property {HTMLJanitor} this._sanitizerInstance - Sanitizer library
*
* @param {IEditorConfig} config
*/
constructor({config}) {
super({config});
}
/** /**
* Sanitize Blocks * Sanitize Blocks
* *
* Enumerate blocks and clean data * Enumerate blocks and clean data
* *
* @param {{tool, data: IBlockToolData}[]} blocksData[] * @param {{tool, data: BlockToolData}[]} blocksData[]
*/ */
public sanitizeBlocks( public sanitizeBlocks(
blocksData: Array<{tool: string, data: IBlockToolData}>, blocksData: Array<{tool: string, data: BlockToolData}>,
): Array<{tool: string, data: IBlockToolData}> { ): Array<{tool: string, data: BlockToolData}> {
return blocksData.map((block) => { return blocksData.map((block) => {
const toolConfig = this.composeToolConfig(block.tool); const toolConfig = this.composeToolConfig(block.tool);
@ -91,10 +76,10 @@ export default class Sanitizer extends Module {
/** /**
* Method recursively reduces Block's data and cleans with passed rules * Method recursively reduces Block's data and cleans with passed rules
* *
* @param {IBlockToolData|object|*} dataToSanitize - taint string or object/array that contains taint string * @param {BlockToolData|object|*} dataToSanitize - taint string or object/array that contains taint string
* @param {ISanitizerConfig} rules - object with sanitizer rules * @param {SanitizerConfig} rules - object with sanitizer rules
*/ */
public deepSanitize(dataToSanitize: any, rules: ISanitizerConfig): any { public deepSanitize(dataToSanitize: any, rules: SanitizerConfig): any {
/** /**
* BlockData It may contain 3 types: * BlockData It may contain 3 types:
* - Array * - Array
@ -133,7 +118,7 @@ export default class Sanitizer extends Module {
* *
* @return {string} clean HTML * @return {string} clean HTML
*/ */
public clean(taintString: string, customConfig: ISanitizerConfig = {}): string { public clean(taintString: string, customConfig: SanitizerConfig = {} as SanitizerConfig): string {
const sanitizerConfig = { const sanitizerConfig = {
tags: customConfig, tags: customConfig,
@ -150,10 +135,10 @@ export default class Sanitizer extends Module {
* Merge with inline tool config * Merge with inline tool config
* *
* @param {string} toolName * @param {string} toolName
* @param {ISanitizerConfig} toolRules * @param {SanitizerConfig} toolRules
* @return {ISanitizerConfig} * @return {SanitizerConfig}
*/ */
public composeToolConfig(toolName: string): ISanitizerConfig { public composeToolConfig(toolName: string): SanitizerConfig {
/** /**
* If cache is empty, then compose tool config and put it to the cache object * If cache is empty, then compose tool config and put it to the cache object
*/ */
@ -174,7 +159,7 @@ export default class Sanitizer extends Module {
const toolRules = toolClass.sanitize; const toolRules = toolClass.sanitize;
const toolConfig = {}; const toolConfig = {} as SanitizerConfig;
for (const fieldName in toolRules) { for (const fieldName in toolRules) {
if (toolRules.hasOwnProperty(fieldName)) { if (toolRules.hasOwnProperty(fieldName)) {
const rule = toolRules[fieldName]; const rule = toolRules[fieldName];
@ -195,13 +180,13 @@ export default class Sanitizer extends Module {
* When Tool's "inlineToolbar" value is True, get all sanitizer rules from all tools, * When Tool's "inlineToolbar" value is True, get all sanitizer rules from all tools,
* otherwise get only enabled * otherwise get only enabled
*/ */
public getInlineToolsConfig(name: string): ISanitizerConfig { public getInlineToolsConfig(name: string): SanitizerConfig {
const {Tools} = this.Editor; const {Tools} = this.Editor;
const toolsConfig = Tools.getToolSettings(name), const toolsConfig = Tools.getToolSettings(name),
enableInlineTools = toolsConfig.inlineToolbar || []; enableInlineTools = toolsConfig.inlineToolbar || [];
let config = {}; let config = {} as SanitizerConfig;
if (typeof enableInlineTools === 'boolean' && enableInlineTools) { if (typeof enableInlineTools === 'boolean' && enableInlineTools) {
/** /**
@ -212,8 +197,11 @@ export default class Sanitizer extends Module {
/** /**
* getting only enabled * getting only enabled
*/ */
enableInlineTools.map( (inlineToolName) => { (enableInlineTools as string[]).map( (inlineToolName) => {
config = Object.assign(config, Tools.inline[inlineToolName][Tools.apiSettings.SANITIZE_CONFIG]); config = Object.assign(
config,
Tools.inline[inlineToolName][Tools.apiSettings.SANITIZE_CONFIG],
) as SanitizerConfig;
}); });
} }
@ -223,19 +211,19 @@ export default class Sanitizer extends Module {
/** /**
* Return general config for all inline tools * Return general config for all inline tools
*/ */
public getAllInlineToolsConfig(): ISanitizerConfig { public getAllInlineToolsConfig(): SanitizerConfig {
const {Tools} = this.Editor; const {Tools} = this.Editor;
if (this.inlineToolsConfigCache) { if (this.inlineToolsConfigCache) {
return this.inlineToolsConfigCache; return this.inlineToolsConfigCache;
} }
const config: ISanitizerConfig = {}; const config: SanitizerConfig = {} as SanitizerConfig;
Object.entries(Tools.inline) Object.entries(Tools.inline)
.forEach( ([name, inlineTool]: [string, IInlineTool]) => { .forEach( ([name, inlineTool]: [string, InlineToolConstructable]) => {
Object.assign(config, inlineTool[Tools.apiSettings.SANITIZE_CONFIG]); Object.assign(config, inlineTool[Tools.apiSettings.SANITIZE_CONFIG]);
}); });
this.inlineToolsConfigCache = config; this.inlineToolsConfigCache = config;
@ -247,7 +235,7 @@ export default class Sanitizer extends Module {
* @param {array} array - [1, 2, {}, []] * @param {array} array - [1, 2, {}, []]
* @param {object} ruleForItem * @param {object} ruleForItem
*/ */
private cleanArray(array: any[], ruleForItem: ISanitizerConfig): any[] { private cleanArray(array: any[], ruleForItem: SanitizerConfig): any[] {
return array.map( (arrayItem) => this.deepSanitize(arrayItem, ruleForItem)); return array.map( (arrayItem) => this.deepSanitize(arrayItem, ruleForItem));
} }
@ -257,7 +245,7 @@ export default class Sanitizer extends Module {
* @param {object} rules - { b: true } or true|false * @param {object} rules - { b: true } or true|false
* @return {object} * @return {object}
*/ */
private cleanObject(object: any, rules: ISanitizerConfig|{[field: string]: ISanitizerConfig}): any { private cleanObject(object: any, rules: SanitizerConfig|{[field: string]: SanitizerConfig}): any {
const cleanData = {}; const cleanData = {};
for (const fieldName in object) { for (const fieldName in object) {
@ -272,23 +260,23 @@ export default class Sanitizer extends Module {
* - if it is a HTML Janitor rule, call with this rule * - if it is a HTML Janitor rule, call with this rule
* - otherwise, call with parent's config * - otherwise, call with parent's config
*/ */
const ruleForItem = this.isRule(rules[fieldName] as ISanitizerConfig) ? rules[fieldName] : rules; const ruleForItem = this.isRule(rules[fieldName] as SanitizerConfig) ? rules[fieldName] : rules;
cleanData[fieldName] = this.deepSanitize(currentIterationItem, ruleForItem as ISanitizerConfig); cleanData[fieldName] = this.deepSanitize(currentIterationItem, ruleForItem as SanitizerConfig);
} }
return cleanData; return cleanData;
} }
/** /**
* @param {string} taintString * @param {string} taintString
* @param {ISanitizerConfig|boolean} rule * @param {SanitizerConfig|boolean} rule
* @return {string} * @return {string}
*/ */
private cleanOneItem(taintString: string, rule: ISanitizerConfig|boolean): string { private cleanOneItem(taintString: string, rule: SanitizerConfig|boolean): string {
if (typeof rule === 'object') { if (typeof rule === 'object') {
return this.clean(taintString, rule); return this.clean(taintString, rule);
} else if (rule === false) { } else if (rule === false) {
return this.clean(taintString, {}); return this.clean(taintString, {} as SanitizerConfig);
} else { } else {
return taintString; return taintString;
} }
@ -300,7 +288,7 @@ export default class Sanitizer extends Module {
* undefined, null, 0, 1, 2 not a rules * undefined, null, 0, 1, 2 not a rules
* @param config * @param config
*/ */
private isRule(config: ISanitizerConfig): boolean { private isRule(config: SanitizerConfig): boolean {
return typeof config === 'object' || typeof config === 'boolean' || typeof config === 'function'; return typeof config === 'object' || typeof config === 'boolean' || typeof config === 'function';
} }
@ -313,7 +301,7 @@ export default class Sanitizer extends Module {
* *
* @param {SanitizerConfig} config - sanitizer extension * @param {SanitizerConfig} config - sanitizer extension
*/ */
private createHTMLJanitorInstance(config: {tags: ISanitizerConfig}): any { private createHTMLJanitorInstance(config: {tags: SanitizerConfig}): HTMLJanitor|null {
if (config) { if (config) {
return new HTMLJanitor(config); return new HTMLJanitor(config);
} }

View file

@ -5,18 +5,10 @@
* @author Codex Team * @author Codex Team
* @version 2.0.0 * @version 2.0.0
*/ */
import Module from '../__module';
import {OutputData} from '../../../types';
/** declare const VERSION: string;
* @typedef {Object} SavedData
* @property {Date} time - saving proccess time
* @property {Object} blocks - extracted data
* @property {String} version - CodexEditor version
*/
interface SavedData {
time: number;
blocks: object[];
version: string;
}
/** /**
* @classdesc This method reduces all Blocks asyncronically and calls Block's save method to extract data * @classdesc This method reduces all Blocks asyncronically and calls Block's save method to extract data
@ -25,48 +17,31 @@ interface SavedData {
* @property {Element} html - Editor HTML content * @property {Element} html - Editor HTML content
* @property {String} json - Editor JSON output * @property {String} json - Editor JSON output
*/ */
declare const Module: any;
declare const VERSION: string;
export default class Saver extends Module { export default class Saver extends Module {
/**
* @constructor
* @param config
*/
constructor({config}) {
super({config});
this.output = null;
this.blocksData = [];
}
/** /**
* Composes new chain of Promises to fire them alternatelly * Composes new chain of Promises to fire them alternatelly
* @return {SavedData} * @return {OutputData}
*/ */
public save(): Promise<SavedData> { public async save(): Promise<OutputData> {
const blocks = this.Editor.BlockManager.blocks, const blocks = this.Editor.BlockManager.blocks,
chainData = []; chainData = [];
blocks.forEach((block) => { blocks.forEach((block) => {
chainData.push(block.data); chainData.push(block.data);
}); });
return Promise.all(chainData) const extractedData = await Promise.all(chainData);
.then((extractedData) => this.Editor.Sanitizer.sanitizeBlocks(extractedData))
.then((allExtractedData) => this.makeOutput(allExtractedData)) const sanitizedData = await this.Editor.Sanitizer.sanitizeBlocks(extractedData);
.then((outputData) => { return this.makeOutput(sanitizedData);
return outputData;
});
} }
/** /**
* Creates output object with saved data, time and version of editor * Creates output object with saved data, time and version of editor
* @param {Object} allExtractedData * @param {Object} allExtractedData
* @return {SavedData} * @return {OutputData}
*/ */
private makeOutput(allExtractedData): SavedData { private makeOutput(allExtractedData): OutputData {
let totalTime = 0; let totalTime = 0;
const blocks = []; const blocks = [];
@ -87,8 +62,8 @@ export default class Saver extends Module {
return { return {
time: +new Date(), time: +new Date(),
version: VERSION,
blocks, blocks,
version: VERSION,
}; };
} }
} }

View file

@ -1,12 +1,29 @@
import Shortcut from '@codexteam/shortcuts'; import Shortcut from '@codexteam/shortcuts';
import {IShortcut, IShortcuts} from '../interfaces/shortcuts';
import IEditorConfig from '../interfaces/editor-config'; /**
* ShortcutData interface
* Each shortcut must have name and handler
* `name` is a shortcut, like 'CMD+K', 'CMD+B' etc
* `handler` is a callback
*/
export interface ShortcutData {
/**
* Shortcut name
* Ex. CMD+I, CMD+B ....
*/
name: string;
/**
* Shortcut handler
*/
handler(event): void;
}
/** /**
* Contains keyboard and mouse events binded on each Block by Block Manager * Contains keyboard and mouse events binded on each Block by Block Manager
*/ */
declare var Module: any; import Module from '../__module';
/** /**
* @class Shortcut * @class Shortcut
@ -14,27 +31,18 @@ declare var Module: any;
* *
* Internal Shortcuts Module * Internal Shortcuts Module
*/ */
export default class Shortcuts extends Module implements IShortcuts { export default class Shortcuts extends Module {
/** /**
* All registered shortcuts * All registered shortcuts
* @type {IShortcut[]} * @type {Shortcut[]}
*/ */
private registeredShortcuts: IShortcut[]; private registeredShortcuts: Shortcut[] = [];
/**
* @constructor
* @param {IEditorConfig} config
*/
constructor({config}) {
super({config});
this.registeredShortcuts = [];
}
/** /**
* Register shortcut * Register shortcut
* @param {IShortcut} shortcut * @param {ShortcutData} shortcut
*/ */
public add(shortcut: IShortcut): void { public add(shortcut: ShortcutData): void {
const { UI } = this.Editor; const { UI } = this.Editor;
const newShortcut = new Shortcut({ const newShortcut = new Shortcut({
@ -48,9 +56,12 @@ export default class Shortcuts extends Module implements IShortcuts {
/** /**
* Remove shortcut * Remove shortcut
* @param {IShortcut} shortcut * @param {ShortcutData} shortcut
*/ */
public remove(shortcut: string): void { public remove(shortcut: string): void {
// Remove const index = this.registeredShortcuts.findIndex((shc) => shc.name === shortcut);
this.registeredShortcuts[index].remove();
this.registeredShortcuts.splice(index, 1);
} }
} }

View file

@ -1,3 +1,6 @@
import Module from '../../__module';
import $ from '../../dom';
/** /**
* Block Settings * Block Settings
* *
@ -10,24 +13,12 @@
* |________________________| * |________________________|
*/ */
export default class BlockSettings extends Module { export default class BlockSettings extends Module {
/**
* @constructor
*/
constructor({config}) {
super({config});
this.nodes = {
wrapper: null,
toolSettings: null,
defaultSettings: null
};
}
/** /**
* Module Events * Module Events
* @return {{opened: string, closed: string}} * @return {{opened: string, closed: string}}
*/ */
get events() { public get events(): {opened: string, closed: string} {
return { return {
opened: 'block-settings-opened', opened: 'block-settings-opened',
closed: 'block-settings-closed', closed: 'block-settings-closed',
@ -38,7 +29,7 @@ export default class BlockSettings extends Module {
* Block Settings CSS * Block Settings CSS
* @return {{wrapper, wrapperOpened, toolSettings, defaultSettings, button}} * @return {{wrapper, wrapperOpened, toolSettings, defaultSettings, button}}
*/ */
static get CSS() { private static get CSS() {
return { return {
// Settings Panel // Settings Panel
wrapper: 'ce-settings', wrapper: 'ce-settings',
@ -46,10 +37,27 @@ export default class BlockSettings extends Module {
toolSettings: 'ce-settings__plugin-zone', toolSettings: 'ce-settings__plugin-zone',
defaultSettings: 'ce-settings__default-zone', defaultSettings: 'ce-settings__default-zone',
button: 'ce-settings__button' button: 'ce-settings__button',
}; };
} }
/**
* Is Block Settings opened or not
* @returns {boolean}
*/
public get opened(): boolean {
return this.nodes.wrapper.classList.contains(BlockSettings.CSS.wrapperOpened);
}
/**
* Block settings UI HTML elements
*/
public nodes: {[key: string]: HTMLElement} = {
wrapper: null,
toolSettings: null,
defaultSettings: null,
};
/** /**
* Panel with block settings with 2 sections: * Panel with block settings with 2 sections:
* - Tool's Settings * - Tool's Settings
@ -57,7 +65,7 @@ export default class BlockSettings extends Module {
* *
* @return {Element} * @return {Element}
*/ */
make() { public make(): void {
this.nodes.wrapper = $.make('div', BlockSettings.CSS.wrapper); this.nodes.wrapper = $.make('div', BlockSettings.CSS.wrapper);
this.nodes.toolSettings = $.make('div', BlockSettings.CSS.toolSettings); this.nodes.toolSettings = $.make('div', BlockSettings.CSS.toolSettings);
@ -66,34 +74,10 @@ export default class BlockSettings extends Module {
$.append(this.nodes.wrapper, [this.nodes.toolSettings, this.nodes.defaultSettings]); $.append(this.nodes.wrapper, [this.nodes.toolSettings, this.nodes.defaultSettings]);
} }
/**
* Add Tool's settings
*/
addToolSettings() {
if (typeof this.Editor.BlockManager.currentBlock.tool.renderSettings === 'function') {
$.append(this.nodes.toolSettings, this.Editor.BlockManager.currentBlock.tool.renderSettings());
}
}
/**
* Add default settings
*/
addDefaultSettings() {
$.append(this.nodes.defaultSettings, this.Editor.BlockManager.currentBlock.renderTunes());
}
/**
* Is Block Settings opened or not
* @returns {boolean}
*/
get opened() {
return this.nodes.wrapper.classList.contains(BlockSettings.CSS.wrapperOpened);
}
/** /**
* Open Block Settings pane * Open Block Settings pane
*/ */
open() { public open(): void {
this.nodes.wrapper.classList.add(BlockSettings.CSS.wrapperOpened); this.nodes.wrapper.classList.add(BlockSettings.CSS.wrapperOpened);
/** /**
@ -113,7 +97,7 @@ export default class BlockSettings extends Module {
/** /**
* Close Block Settings pane * Close Block Settings pane
*/ */
close() { public close(): void {
this.nodes.wrapper.classList.remove(BlockSettings.CSS.wrapperOpened); this.nodes.wrapper.classList.remove(BlockSettings.CSS.wrapperOpened);
/** Clear settings */ /** Clear settings */
@ -123,4 +107,20 @@ export default class BlockSettings extends Module {
/** Tell to subscribers that block settings is closed */ /** Tell to subscribers that block settings is closed */
this.Editor.Events.emit(this.events.closed); this.Editor.Events.emit(this.events.closed);
} }
/**
* Add Tool's settings
*/
private addToolSettings(): void {
if (typeof this.Editor.BlockManager.currentBlock.tool.renderSettings === 'function') {
$.append(this.nodes.toolSettings, this.Editor.BlockManager.currentBlock.tool.renderSettings());
}
}
/**
* Add default settings
*/
private addDefaultSettings(): void {
$.append(this.nodes.defaultSettings, this.Editor.BlockManager.currentBlock.renderTunes());
}
} }

View file

@ -1,3 +1,6 @@
import Module from '../../__module';
import $ from '../../dom';
/** /**
* *
* «Toolbar» is the node that moves up/down over current block * «Toolbar» is the node that moves up/down over current block
@ -51,31 +54,26 @@
*/ */
export default class Toolbar extends Module { export default class Toolbar extends Module {
/** /**
* @constructor * HTML Elements used for Toolbar UI
*/ */
constructor({config}) { public nodes: {[key: string]: HTMLElement} = {
super({config}); wrapper : null,
content : null,
actions : null,
this.nodes = { // Content Zone
wrapper : null, plusButton : null,
content : null,
actions : null,
// Content Zone // Actions Zone
plusButton : null, blockActionsButtons: null,
settingsToggler : null,
// Actions Zone };
blockActionsButtons: null,
settingsToggler : null,
};
}
/** /**
* CSS styles * CSS styles
* @return {Object} * @return {Object}
* @constructor
*/ */
static get CSS() { private static get CSS() {
return { return {
toolbar: 'ce-toolbar', toolbar: 'ce-toolbar',
content: 'ce-toolbar__content', content: 'ce-toolbar__content',
@ -96,18 +94,17 @@ export default class Toolbar extends Module {
/** /**
* Makes toolbar * Makes toolbar
*/ */
make() { public make(): void {
this.nodes.wrapper = $.make('div', Toolbar.CSS.toolbar); this.nodes.wrapper = $.make('div', Toolbar.CSS.toolbar);
/** /**
* Make Content Zone and Actions Zone * Make Content Zone and Actions Zone
*/ */
['content', 'actions'].forEach( el => { ['content', 'actions'].forEach( (el) => {
this.nodes[el] = $.make('div', Toolbar.CSS[el]); this.nodes[el] = $.make('div', Toolbar.CSS[el]);
$.append(this.nodes.wrapper, this.nodes[el]); $.append(this.nodes.wrapper, this.nodes[el]);
}); });
/** /**
* Fill Content Zone: * Fill Content Zone:
* - Plus Button * - Plus Button
@ -116,8 +113,7 @@ export default class Toolbar extends Module {
this.nodes.plusButton = $.make('div', Toolbar.CSS.plusButton); this.nodes.plusButton = $.make('div', Toolbar.CSS.plusButton);
$.append(this.nodes.plusButton, $.svg('plus', 14, 14)); $.append(this.nodes.plusButton, $.svg('plus', 14, 14));
$.append(this.nodes.content, this.nodes.plusButton); $.append(this.nodes.content, this.nodes.plusButton);
this.nodes.plusButton.addEventListener('click', event => this.plusButtonClicked(event), false); this.Editor.Listeners.on(this.nodes.plusButton, 'click', () => this.plusButtonClicked(), false);
/** /**
* Make a Toolbox * Make a Toolbox
@ -159,14 +155,14 @@ export default class Toolbar extends Module {
* Move Toolbar to the Current Block * Move Toolbar to the Current Block
* @param {Boolean} forceClose - force close Toolbar Settings and Toolbar * @param {Boolean} forceClose - force close Toolbar Settings and Toolbar
*/ */
move(forceClose = true) { public move(forceClose: boolean = true): void {
if (forceClose) { if (forceClose) {
/** Close Toolbox when we move toolbar */ /** Close Toolbox when we move toolbar */
this.Editor.Toolbox.close(); this.Editor.Toolbox.close();
this.Editor.BlockSettings.close(); this.Editor.BlockSettings.close();
} }
let currentBlock = this.Editor.BlockManager.currentBlock.holder; const currentBlock = this.Editor.BlockManager.currentBlock.holder;
/** /**
* If no one Block selected as a Current * If no one Block selected as a Current
@ -192,7 +188,7 @@ export default class Toolbar extends Module {
/** /**
* Open Toolbar with Plus Button * Open Toolbar with Plus Button
*/ */
open() { public open(): void {
/** /**
* Wait Block rendering for correct height computing * Wait Block rendering for correct height computing
*/ */
@ -206,14 +202,14 @@ export default class Toolbar extends Module {
* returns toolbar opened state * returns toolbar opened state
* @return {Boolean} * @return {Boolean}
*/ */
get opened() { public get opened(): boolean {
return this.nodes.wrapper.classList.contains(Toolbar.CSS.toolbarOpened); return this.nodes.wrapper.classList.contains(Toolbar.CSS.toolbarOpened);
} }
/** /**
* Close the Toolbar * Close the Toolbar
*/ */
close() { public close(): void {
this.nodes.wrapper.classList.remove(Toolbar.CSS.toolbarOpened); this.nodes.wrapper.classList.remove(Toolbar.CSS.toolbarOpened);
/** Close components */ /** Close components */
@ -225,7 +221,7 @@ export default class Toolbar extends Module {
* Plus Button public methods * Plus Button public methods
* @return {{hide: function(): void, show: function(): void}} * @return {{hide: function(): void, show: function(): void}}
*/ */
get plusButton() { public get plusButton(): {hide: () => void, show: () => void} {
return { return {
hide: () => this.nodes.plusButton.classList.add(Toolbar.CSS.plusButtonHidden), hide: () => this.nodes.plusButton.classList.add(Toolbar.CSS.plusButtonHidden),
show: () => { show: () => {
@ -233,7 +229,7 @@ export default class Toolbar extends Module {
return; return;
} }
this.nodes.plusButton.classList.remove(Toolbar.CSS.plusButtonHidden); this.nodes.plusButton.classList.remove(Toolbar.CSS.plusButtonHidden);
} },
}; };
} }
@ -241,7 +237,7 @@ export default class Toolbar extends Module {
* Handler for Plus Button * Handler for Plus Button
* @param {MouseEvent} event * @param {MouseEvent} event
*/ */
plusButtonClicked() { private plusButtonClicked(): void {
this.Editor.Toolbox.toggle(); this.Editor.Toolbox.toggle();
} }
@ -249,19 +245,17 @@ export default class Toolbar extends Module {
* Bind events on the Toolbar Elements: * Bind events on the Toolbar Elements:
* - Block Settings * - Block Settings
*/ */
bindEvents() { private bindEvents(): void {
/** /**
* Settings toggler * Settings toggler
*/ */
this.Editor.Listeners.on(this.nodes.settingsToggler, 'click', (event) => { this.Editor.Listeners.on(this.nodes.settingsToggler, 'click', () => this.settingsTogglerClicked());
this.settingsTogglerClicked(event);
});
} }
/** /**
* Clicks on the Block Settings toggler * Clicks on the Block Settings toggler
*/ */
settingsTogglerClicked() { private settingsTogglerClicked(): void {
if (this.Editor.BlockSettings.opened) { if (this.Editor.BlockSettings.opened) {
this.Editor.BlockSettings.close(); this.Editor.BlockSettings.close();
} else { } else {

View file

@ -1,12 +1,12 @@
import ITool from '../interfaces/tools/tool'; import Module from '../../__module';
import $ from '../../dom';
declare var Module: any; import BoldInlineTool from '../../inline-tools/inline-tool-bold';
declare var $: any; import ItalicInlineTool from '../../inline-tools/inline-tool-italic';
import LinkInlineTool from '../../inline-tools/inline-tool-link';
import EditorConfig from '../interfaces/editor-config'; import SelectionUtils from '../../selection';
import InlineTool from '../interfaces/tools/inline-tool'; import _ from '../../utils';
import SelectionUtils from '../selection'; import {InlineTool, InlineToolConstructable, ToolConstructable, ToolSettings} from '../../../../types';
import _ from '../utils';
/** /**
* Inline toolbar with actions that modifies selected text fragment * Inline toolbar with actions that modifies selected text fragment
@ -17,6 +17,34 @@ import _ from '../utils';
*/ */
export default class InlineToolbar extends Module { export default class InlineToolbar extends Module {
/**
* Returns internal inline tools
* Includes Bold, Italic, Link
*/
private get internalTools(): {[name: string]: InlineTool} {
return {
bold: this.Editor.Tools.constructInline(BoldInlineTool),
italic: this.Editor.Tools.constructInline(ItalicInlineTool),
link: this.Editor.Tools.constructInline(LinkInlineTool),
};
}
/**
* Get external tools
* Tools that has isInline is true
*/
private get externalTools(): {[name: string]: InlineTool} {
const result = {};
for (const tool in this.Editor.Tools.inline) {
if (this.Editor.Tools.inline.hasOwnProperty(tool)) {
result[tool] = this.Editor.Tools.constructInline(this.Editor.Tools.inline[tool]);
}
}
return result;
}
/** /**
* CSS styles * CSS styles
*/ */
@ -32,7 +60,7 @@ export default class InlineToolbar extends Module {
/** /**
* Inline Toolbar elements * Inline Toolbar elements
*/ */
private nodes = { private nodes: {wrapper: HTMLElement, buttons: HTMLElement, actions: HTMLElement} = {
wrapper: null, wrapper: null,
buttons: null, buttons: null,
/** /**
@ -52,14 +80,6 @@ export default class InlineToolbar extends Module {
*/ */
private toolsInstances: Map<string, InlineTool>; private toolsInstances: Map<string, InlineTool>;
/**
* @constructor
* @param {EditorConfig} config
*/
constructor({config}) {
super({config});
}
/** /**
* Inline Toolbar Tools * Inline Toolbar Tools
* *
@ -152,10 +172,22 @@ export default class InlineToolbar extends Module {
this.nodes.wrapper.style.top = Math.floor(newCoords.y) + 'px'; this.nodes.wrapper.style.top = Math.floor(newCoords.y) + 'px';
} }
/**
* Hides Inline Toolbar
*/
public close(): void {
this.nodes.wrapper.classList.remove(this.CSS.inlineToolbarShowed);
this.tools.forEach( (toolInstance, toolName) => {
if (typeof toolInstance.clear === 'function') {
toolInstance.clear();
}
});
}
/** /**
* Shows Inline Toolbar * Shows Inline Toolbar
*/ */
private open() { private open(): void {
/** /**
* Filter inline-tools and show only allowed by Block's Tool * Filter inline-tools and show only allowed by Block's Tool
*/ */
@ -169,19 +201,7 @@ export default class InlineToolbar extends Module {
/** /**
* Call 'clear' method for Inline Tools (for example, 'link' want to clear input) * Call 'clear' method for Inline Tools (for example, 'link' want to clear input)
*/ */
this.tools.forEach( (toolInstance, toolName) => { this.tools.forEach( (toolInstance: InlineTool) => {
if (typeof toolInstance.clear === 'function') {
toolInstance.clear();
}
});
}
/**
* Hides Inline Toolbar
*/
private close() {
this.nodes.wrapper.classList.remove(this.CSS.inlineToolbarShowed);
this.tools.forEach( (toolInstance, toolName) => {
if (typeof toolInstance.clear === 'function') { if (typeof toolInstance.clear === 'function') {
toolInstance.clear(); toolInstance.clear();
} }
@ -217,7 +237,7 @@ export default class InlineToolbar extends Module {
} }
// is enabled by current Block's Tool // is enabled by current Block's Tool
const currentBlock = this.Editor.BlockManager.getBlock(currentSelection.anchorNode); const currentBlock = this.Editor.BlockManager.getBlock(currentSelection.anchorNode as HTMLElement);
if (!currentBlock) { if (!currentBlock) {
return false; return false;
@ -233,7 +253,7 @@ export default class InlineToolbar extends Module {
*/ */
private filterTools(): void { private filterTools(): void {
const currentSelection = SelectionUtils.get(), const currentSelection = SelectionUtils.get(),
currentBlock = this.Editor.BlockManager.getBlock(currentSelection.anchorNode); currentBlock = this.Editor.BlockManager.getBlock(currentSelection.anchorNode as HTMLElement);
const toolSettings = this.Editor.Tools.getToolSettings(currentBlock.name), const toolSettings = this.Editor.Tools.getToolSettings(currentBlock.name),
inlineToolbarSettings = toolSettings && toolSettings[this.Editor.Tools.apiSettings.IS_ENABLED_INLINE_TOOLBAR]; inlineToolbarSettings = toolSettings && toolSettings[this.Editor.Tools.apiSettings.IS_ENABLED_INLINE_TOOLBAR];
@ -328,8 +348,14 @@ export default class InlineToolbar extends Module {
*/ */
const internalTools: string[] = Object const internalTools: string[] = Object
.entries(Tools.internalTools) .entries(Tools.internalTools)
.filter(([name, toolClass]: [string, ITool]) => toolClass[Tools.apiSettings.IS_INLINE]) .filter(([name, toolClass]: [string, ToolConstructable|ToolSettings]) => {
.map(([name, toolClass]: [string, ITool]) => name); if (_.isFunction(toolClass)) {
return toolClass[Tools.apiSettings.IS_INLINE];
}
return (toolClass as ToolSettings).class[Tools.apiSettings.IS_INLINE];
})
.map(([name, toolClass]: [string, InlineToolConstructable|ToolSettings]) => name);
/** /**
* 1) For internal tools, check public getter 'shortcut' * 1) For internal tools, check public getter 'shortcut'
@ -349,7 +375,7 @@ export default class InlineToolbar extends Module {
/** /**
* Enable Tool shortcut with Editor Shortcuts Module * Enable Tool shortcut with Editor Shortcuts Module
* @param {InlineTool} tool - Tool instance * @param {InlineTool} tool - Tool instance
* @param {string} shortcut - shortcut according to the Shortcut Module format * @param {string} shortcut - shortcut according to the ShortcutData Module format
*/ */
private enableShortcuts(tool: InlineTool, shortcut: string): void { private enableShortcuts(tool: InlineTool, shortcut: string): void {
this.Editor.Shortcuts.add({ this.Editor.Shortcuts.add({
@ -398,7 +424,7 @@ export default class InlineToolbar extends Module {
* Check Tools` state by selection * Check Tools` state by selection
*/ */
private checkToolsState(): void { private checkToolsState(): void {
this.tools.forEach( (toolInstance, toolName) => { this.tools.forEach( (toolInstance) => {
toolInstance.checkState(SelectionUtils.get()); toolInstance.checkState(SelectionUtils.get());
}); });
} }

View file

@ -1,4 +1,7 @@
import IEditorConfig from '../interfaces/editor-config'; import Module from '../../__module';
import $ from '../../dom';
import _ from '../../utils';
import {BlockToolConstructable} from '../../../../types';
/** /**
* @class Toolbox * @class Toolbox
@ -11,37 +14,41 @@ import IEditorConfig from '../interfaces/editor-config';
* *
*/ */
export default class Toolbox extends Module { export default class Toolbox extends Module {
private static LEAF_DIRECTIONS = {
RIGHT: 'right',
LEFT: 'left',
};
/** /**
* @constructor * Opening state
* @param {IEditorConfig} config * @type {boolean}
*/ */
constructor({config}) { public opened: boolean = false;
super({config});
this.nodes = { /**
toolbox: null, * HTMLElements used for Toolbox UI
buttons: [] */
}; public nodes: {
toolbox: HTMLElement,
buttons: HTMLElement[],
} = {
toolbox: null,
buttons: [],
};
/** /**
* Opening state * Active button index
* @type {boolean} * -1 equals no chosen Tool
*/ * @type {number}
this.opened = false; */
private activeButtonIndex: number = -1;
/** /**
* Active button index * How many tools displayed in Toolbox
* -1 equals no chosen Tool * @type {number}
* @type {number} */
*/ private displayedToolsCount: number = 0;
this.activeButtonIndex = -1;
/**
* How many tools displayed in Toolbox
* @type {number}
*/
this.displayedToolsCount = 0;
}
/** /**
* CSS styles * CSS styles
@ -59,21 +66,157 @@ export default class Toolbox extends Module {
/** /**
* Makes the Toolbox * Makes the Toolbox
*/ */
make() { public make(): void {
this.nodes.toolbox = $.make('div', Toolbox.CSS.toolbox); this.nodes.toolbox = $.make('div', Toolbox.CSS.toolbox);
$.append(this.Editor.Toolbar.nodes.content, this.nodes.toolbox); $.append(this.Editor.Toolbar.nodes.content, this.nodes.toolbox);
this.addTools(); this.addTools();
} }
/**
* Toolbox Tool's button click handler
*
* @param {MouseEvent|KeyboardEvent} event
* @param {string} toolName
*/
public toolButtonActivate(event: MouseEvent|KeyboardEvent, toolName: string): void {
const tool = this.Editor.Tools.toolsClasses[toolName] as BlockToolConstructable;
this.insertNewBlock(tool, toolName);
}
/**
* Open Toolbox with Tools
*/
public open(): void {
if (this.isEmpty) {
return;
}
this.nodes.toolbox.classList.add(Toolbox.CSS.toolboxOpened);
this.opened = true;
}
/**
* Close Toolbox
*/
public close(): void {
this.nodes.toolbox.classList.remove(Toolbox.CSS.toolboxOpened);
this.opened = false;
/** remove active item pointer */
this.activeButtonIndex = -1;
const activeButton = this.nodes.toolbox.querySelector(`.${Toolbox.CSS.toolboxButtonActive}`);
if (activeButton) {
activeButton.classList.remove(Toolbox.CSS.toolboxButtonActive);
}
}
/**
* Close Toolbox
*/
public toggle(): void {
if (!this.opened) {
this.open();
} else {
this.close();
}
}
/**
* Leaf
* flip through the toolbox items
* @param {String} direction - leaf direction, right is default
*/
public leaf(direction: string = Toolbox.LEAF_DIRECTIONS.RIGHT): void {
const childNodes = this.nodes.toolbox.childNodes;
/**
* If activeButtonIndex === -1 then we have no chosen Tool in Toolbox
*/
if (this.activeButtonIndex === -1) {
/**
* Normalize "previous" Tool index depending on direction.
* We need to do this to highlight "first" Tool correctly
*
* Order of Tools: [0] [1] ... [n - 1]
* [0 = n] because of: n % n = 0 % n
*
* Direction 'right': for [0] the [n - 1] is a previous index
* [n - 1] -> [0]
*
* Direction 'left': for [n - 1] the [0] is a previous index
* [n - 1] <- [0]
*
* @type {number}
*/
this.activeButtonIndex = direction === Toolbox.LEAF_DIRECTIONS.RIGHT ? -1 : 0;
} else {
/**
* If we have chosen Tool then remove highlighting
*/
(childNodes[this.activeButtonIndex] as HTMLElement).classList.remove(Toolbox.CSS.toolboxButtonActive);
}
/**
* Count index for next Tool
*/
if (direction === Toolbox.LEAF_DIRECTIONS.RIGHT) {
/**
* If we go right then choose next (+1) Tool
* @type {number}
*/
this.activeButtonIndex = (this.activeButtonIndex + 1) % childNodes.length;
} else {
/**
* If we go left then choose previous (-1) Tool
* Before counting module we need to add length before because of "The JavaScript Modulo Bug"
* @type {number}
*/
this.activeButtonIndex = (childNodes.length + this.activeButtonIndex - 1) % childNodes.length;
}
/**
* Highlight new chosen Tool
*/
(childNodes[this.activeButtonIndex] as HTMLElement).classList.add(Toolbox.CSS.toolboxButtonActive);
}
/**
* get tool name when it is selected
* In case when nothing selection returns null
*
* @return {String|null}
*/
public get getActiveTool(): string {
const childNodes = this.nodes.toolbox.childNodes;
if (this.activeButtonIndex === -1) {
return null;
}
return (childNodes[this.activeButtonIndex] as HTMLElement).title;
}
/**
* Returns True if Toolbox is Empty and nothing to show
* @return {boolean}
*/
public get isEmpty(): boolean {
return this.displayedToolsCount === 0;
}
/** /**
* Iterates available tools and appends them to the Toolbox * Iterates available tools and appends them to the Toolbox
*/ */
addTools() { private addTools(): void {
let tools = this.Editor.Tools.toolsAvailable; const tools = this.Editor.Tools.available;
for (let toolName in tools) { for (const toolName in tools) {
this.addTool(toolName, tools[toolName]); if (tools.hasOwnProperty(toolName)) {
this.addTool(toolName, tools[toolName] as BlockToolConstructable);
}
} }
} }
@ -81,9 +224,9 @@ export default class Toolbox extends Module {
* Append Tool to the Toolbox * Append Tool to the Toolbox
* *
* @param {string} toolName - tool name * @param {string} toolName - tool name
* @param {IBlockTool} tool - tool class * @param {BlockToolConstructable} tool - tool class
*/ */
addTool(toolName, tool) { private addTool(toolName: string, tool: BlockToolConstructable): void {
const api = this.Editor.Tools.apiSettings; const api = this.Editor.Tools.apiSettings;
if (tool[api.IS_DISPLAYED_IN_TOOLBOX] && !tool[api.TOOLBAR_ICON]) { if (tool[api.IS_DISPLAYED_IN_TOOLBOX] && !tool[api.TOOLBAR_ICON]) {
@ -106,8 +249,8 @@ export default class Toolbox extends Module {
return; return;
} }
let button = $.make('li', [ Toolbox.CSS.toolboxButton ], { const button = $.make('li', [ Toolbox.CSS.toolboxButton ], {
title: toolName title: toolName,
}); });
button.innerHTML = tool.toolboxIcon; button.innerHTML = tool.toolboxIcon;
@ -120,7 +263,7 @@ export default class Toolbox extends Module {
/** /**
* Add click listener * Add click listener
*/ */
this.Editor.Listeners.on(button, 'click', (event) => { this.Editor.Listeners.on(button, 'click', (event: KeyboardEvent|MouseEvent) => {
this.toolButtonActivate(event, toolName); this.toolButtonActivate(event, toolName);
}); });
@ -139,28 +282,28 @@ export default class Toolbox extends Module {
/** /**
* Enable shortcut Block Tool implemented shortcut * Enable shortcut Block Tool implemented shortcut
* @param {IBlockTool} tool - Tool class * @param {BlockToolConstructable} tool - Tool class
* @param {String} toolName - Tool name * @param {String} toolName - Tool name
* @param {String} shortcut - shortcut according to the Shortcut Module format * @param {String} shortcut - shortcut according to the ShortcutData Module format
*/ */
enableShortcut(tool, toolName, shortcut) { private enableShortcut(tool: BlockToolConstructable, toolName: string, shortcut: string) {
this.Editor.Shortcuts.add({ this.Editor.Shortcuts.add({
name: shortcut, name: shortcut,
handler: (event) => { handler: (event: KeyboardEvent) => {
event.preventDefault(); event.preventDefault();
this.insertNewBlock(tool, toolName); this.insertNewBlock(tool, toolName);
} },
}); });
} }
/** /**
* Inserts new block * Inserts new block
* Can be called when button clicked on Toolbox or by Shortcut * Can be called when button clicked on Toolbox or by ShortcutData
* *
* @param {IBlockTool} tool - Tool Class * @param {BlockToolConstructable} tool - Tool Class
* @param {String} toolName - Tool name * @param {String} toolName - Tool name
*/ */
insertNewBlock(tool, toolName) { private insertNewBlock(tool: BlockToolConstructable, toolName: string) {
/** /**
* @type {Block} * @type {Block}
*/ */
@ -192,138 +335,4 @@ export default class Toolbox extends Module {
*/ */
this.Editor.Toolbar.close(); this.Editor.Toolbar.close();
} }
/**
* Toolbox Tool's button click handler
*
* @param {MouseEvent|KeyboardEvent} event
* @param {string} toolName
*/
toolButtonActivate(event, toolName) {
const tool = this.Editor.Tools.toolsClasses[toolName];
this.insertNewBlock(tool, toolName);
}
/**
* Open Toolbox with Tools
*/
open() {
if (this.isEmpty) {
return;
}
this.nodes.toolbox.classList.add(Toolbox.CSS.toolboxOpened);
this.opened = true;
}
/**
* Close Toolbox
*/
close() {
this.nodes.toolbox.classList.remove(Toolbox.CSS.toolboxOpened);
this.opened = false;
/** remove active item pointer */
this.activeButtonIndex = -1;
const activeButton = this.nodes.toolbox.querySelector(`.${Toolbox.CSS.toolboxButtonActive}`);
if (activeButton) {
activeButton.classList.remove(Toolbox.CSS.toolboxButtonActive);
}
}
/**
* Close Toolbox
*/
toggle() {
if (!this.opened) {
this.open();
} else {
this.close();
}
}
/**
* Leaf
* flip through the toolbox items
* @param {String} direction - leaf direction, right is default
*/
leaf(direction = 'right') {
const childNodes = this.nodes.toolbox.childNodes;
/**
* If activeButtonIndex === -1 then we have no chosen Tool in Toolbox
*/
if (this.activeButtonIndex === -1) {
/**
* Normalize "previous" Tool index depending on direction.
* We need to do this to highlight "first" Tool correctly
*
* Order of Tools: [0] [1] ... [n - 1]
* [0 = n] because of: n % n = 0 % n
*
* Direction 'right': for [0] the [n - 1] is a previous index
* [n - 1] -> [0]
*
* Direction 'left': for [n - 1] the [0] is a previous index
* [n - 1] <- [0]
*
* @type {number}
*/
this.activeButtonIndex = direction === 'right' ? -1 : 0;
} else {
/**
* If we have chosen Tool then remove highlighting
*/
childNodes[this.activeButtonIndex].classList.remove(Toolbox.CSS.toolboxButtonActive);
}
/**
* Count index for next Tool
*/
if (direction === 'right') {
/**
* If we go right then choose next (+1) Tool
* @type {number}
*/
this.activeButtonIndex = (this.activeButtonIndex + 1) % childNodes.length;
} else {
/**
* If we go left then choose previous (-1) Tool
* Before counting module we need to add length before because of "The JavaScript Modulo Bug"
* @type {number}
*/
this.activeButtonIndex = (childNodes.length + this.activeButtonIndex - 1) % childNodes.length;
}
/**
* Highlight new chosen Tool
*/
childNodes[this.activeButtonIndex].classList.add(Toolbox.CSS.toolboxButtonActive);
}
/**
* get tool name when it is selected
* In case when nothing selection returns null
*
* @return {String|null}
*/
get getActiveTool() {
const childNodes = this.nodes.toolbox.childNodes;
if (this.activeButtonIndex === -1) {
return null;
}
return childNodes[this.activeButtonIndex].title;
}
/**
* Returns True if Toolbox is Empty and nothing to show
* @return {boolean}
*/
get isEmpty() {
return this.displayedToolsCount === 0;
}
} }

View file

@ -1,9 +1,11 @@
import Paragraph from '../tools/paragraph/dist/bundle';
import Module from '../__module';
import _ from '../utils';
import {BlockToolConstructable, ToolConfig, ToolConstructable, ToolSettings} from '../../../types';
import BoldInlineTool from '../inline-tools/inline-tool-bold'; import BoldInlineTool from '../inline-tools/inline-tool-bold';
import ItalicInlineTool from '../inline-tools/inline-tool-italic'; import ItalicInlineTool from '../inline-tools/inline-tool-italic';
import LinkInlineTool from '../inline-tools/inline-tool-link'; import LinkInlineTool from '../inline-tools/inline-tool-link';
const Paragraph = require('../tools/paragraph/dist/bundle');
/** /**
* @module Codex Editor Tools Submodule * @module Codex Editor Tools Submodule
* *
@ -17,7 +19,8 @@ const Paragraph = require('../tools/paragraph/dist/bundle');
* @property {String} iconClassname - this a icon in toolbar * @property {String} iconClassname - this a icon in toolbar
* @property {Boolean} displayInToolbox - will be displayed in toolbox. Default value is TRUE * @property {Boolean} displayInToolbox - will be displayed in toolbox. Default value is TRUE
* @property {Boolean} enableLineBreaks - inserts new block or break lines. Default value is FALSE * @property {Boolean} enableLineBreaks - inserts new block or break lines. Default value is FALSE
* @property {Boolean|String[]} inlineToolbar - Pass `true` to enable the Inline Toolbar with all Tools, all pass an array with specified Tools list | * @property {Boolean|String[]} inlineToolbar - Pass `true` to enable the Inline Toolbar with all Tools,
* all pass an array with specified Tools list
* @property render @todo add description * @property render @todo add description
* @property save @todo add description * @property save @todo add description
* @property settings @todo add description * @property settings @todo add description
@ -49,54 +52,12 @@ const Paragraph = require('../tools/paragraph/dist/bundle');
* @property {EditorConfig} config - Editor config * @property {EditorConfig} config - Editor config
*/ */
export default class Tools extends Module { export default class Tools extends Module {
/**
* @constructor
*
* @param {EditorConfig} config
*/
constructor({config}) {
super({config});
/**
* Map {name: Class, ...} where:
* name block type name in JSON. Got from EditorConfig.tools keys
* @type {Object}
*/
this.toolsClasses = {};
/**
* Tools settings in a map {name: settings, ...}
* @type {Object}
*/
this.toolsSettings = {};
/**
* Available tools list
* {name: Class, ...}
* @type {Object}
*/
this.toolsAvailable = {};
/**
* Tools that rejected a prepare method
* {name: Class, ... }
* @type {Object}
*/
this.toolsUnavailable = {};
/**
* Cache for the prepared inline tools
* @type {null|object}
* @private
*/
this._inlineTools = null;
}
/** /**
* Returns available Tools * Returns available Tools
* @return {Tool[]} * @return {Tool[]}
*/ */
get available() { public get available(): {[name: string]: ToolConstructable} {
return this.toolsAvailable; return this.toolsAvailable;
} }
@ -104,7 +65,7 @@ export default class Tools extends Module {
* Returns unavailable Tools * Returns unavailable Tools
* @return {Tool[]} * @return {Tool[]}
*/ */
get unavailable() { public get unavailable(): {[name: string]: ToolConstructable} {
return this.toolsUnavailable; return this.toolsUnavailable;
} }
@ -112,7 +73,7 @@ export default class Tools extends Module {
* Return Tools for the Inline Toolbar * Return Tools for the Inline Toolbar
* @return {Object} - object of Inline Tool's classes * @return {Object} - object of Inline Tool's classes
*/ */
get inline() { public get inline(): {[name: string]: ToolConstructable} {
if (this._inlineTools) { if (this._inlineTools) {
return this._inlineTools; return this._inlineTools;
} }
@ -126,10 +87,14 @@ export default class Tools extends Module {
* Some Tools validation * Some Tools validation
*/ */
const inlineToolRequiredMethods = ['render', 'surround', 'checkState']; const inlineToolRequiredMethods = ['render', 'surround', 'checkState'];
const notImplementedMethods = inlineToolRequiredMethods.filter( method => !this.constructInline(tool)[method]); const notImplementedMethods = inlineToolRequiredMethods.filter( (method) => !this.constructInline(tool)[method]);
if (notImplementedMethods.length) { if (notImplementedMethods.length) {
_.log(`Incorrect Inline Tool: ${tool.name}. Some of required methods is not implemented %o`, 'warn', notImplementedMethods); _.log(
`Incorrect Inline Tool: ${tool.name}. Some of required methods is not implemented %o`,
'warn',
notImplementedMethods,
);
return false; return false;
} }
@ -154,14 +119,10 @@ export default class Tools extends Module {
/** /**
* Return editor block tools * Return editor block tools
*/ */
get blockTools() { public get blockTools(): {[name: string]: BlockToolConstructable} {
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const tools = Object.entries(this.available).filter( ([name, tool]) => { const tools = Object.entries(this.available).filter( ([name, tool]) => {
if (tool[this.apiSettings.IS_INLINE]) { return !tool[this.apiSettings.IS_INLINE];
return false;
}
return true;
}); });
/** /**
@ -178,7 +139,7 @@ export default class Tools extends Module {
* Constant for available Tools Settings * Constant for available Tools Settings
* @return {object} * @return {object}
*/ */
get apiSettings() { public get apiSettings() {
return { return {
CONFIG: 'config', CONFIG: 'config',
IS_CONTENTLESS: 'contentless', IS_CONTENTLESS: 'contentless',
@ -190,28 +151,87 @@ export default class Tools extends Module {
IS_PASTE_DISALLOWED: 'disallowPaste', IS_PASTE_DISALLOWED: 'disallowPaste',
SHORTCUT: 'shortcut', SHORTCUT: 'shortcut',
TOOLBAR_ICON: 'toolboxIcon', TOOLBAR_ICON: 'toolboxIcon',
SANITIZE_CONFIG: 'sanitize' SANITIZE_CONFIG: 'sanitize',
}; };
} }
/**
* Map {name: Class, ...} where:
* name block type name in JSON. Got from EditorConfig.tools keys
* @type {Object}
*/
public readonly toolsClasses: {[name: string]: ToolConstructable} = {};
/**
* Tools` classes available to use
*/
private readonly toolsAvailable: {[name: string]: ToolConstructable} = {};
/**
* Tools` classes not availbale to use beacause of preparation failure
*/
private readonly toolsUnavailable: {[name: string]: ToolConstructable} = {};
/**
* Tools settings in a map {name: settings, ...}
* @type {Object}
*/
private readonly toolsSettings: {[name: string]: ToolSettings} = {};
/**
* Cache for the prepared inline tools
* @type {null|object}
* @private
*/
private _inlineTools: {[name: string]: ToolConstructable} = {};
/**
* @constructor
*
* @param {EditorConfig} config
*/
constructor({config}) {
super({config});
this.toolsClasses = {};
this.toolsSettings = {};
/**
* Available tools list
* {name: Class, ...}
* @type {Object}
*/
this.toolsAvailable = {};
/**
* Tools that rejected a prepare method
* {name: Class, ... }
* @type {Object}
*/
this.toolsUnavailable = {};
this._inlineTools = null;
}
/** /**
* Creates instances via passed or default configuration * Creates instances via passed or default configuration
* @return {Promise} * @return {Promise}
*/ */
prepare() { public prepare() {
/** /**
* Assign internal tools * Assign internal tools
*/ */
Object.assign(this.config.tools, this.internalTools); Object.assign(this.config.tools, this.internalTools);
if (!this.config.hasOwnProperty('tools') || Object.keys(this.config.tools).length === 0) { if (!this.config.hasOwnProperty('tools') || Object.keys(this.config.tools).length === 0) {
return Promise.reject('Can\'t start without tools'); throw Error('Can\'t start without tools');
} }
/** /**
* Save Tools settings to a map * Save Tools settings to a map
*/ */
for(let toolName in this.config.tools) { for (const toolName in this.config.tools) {
/** /**
* If Tool is an object not a Tool's class then * If Tool is an object not a Tool's class then
* save class and settings separately * save class and settings separately
@ -219,15 +239,15 @@ export default class Tools extends Module {
if (typeof this.config.tools[toolName] === 'object') { if (typeof this.config.tools[toolName] === 'object') {
/** /**
* Save Tool's class from 'class' field * Save Tool's class from 'class' field
* @type {ITool} * @type {Tool}
*/ */
this.toolsClasses[toolName] = this.config.tools[toolName].class; this.toolsClasses[toolName] = (this.config.tools[toolName] as ToolSettings).class;
/** /**
* Save Tool's settings * Save Tool's settings
* @type {IToolSettings} * @type {ToolSettings}
*/ */
this.toolsSettings[toolName] = this.config.tools[toolName]; this.toolsSettings[toolName] = this.config.tools[toolName] as ToolSettings;
/** /**
* Remove Tool's class from settings * Remove Tool's class from settings
@ -236,22 +256,22 @@ export default class Tools extends Module {
} else { } else {
/** /**
* Save Tool's class * Save Tool's class
* @type {ITool} * @type {Tool}
*/ */
this.toolsClasses[toolName] = this.config.tools[toolName]; this.toolsClasses[toolName] = this.config.tools[toolName] as ToolConstructable;
/** /**
* Set empty settings for Block by default * Set empty settings for Block by default
* @type {{}} * @type {{}}
*/ */
this.toolsSettings[toolName] = {}; this.toolsSettings[toolName] = {class: this.config.tools[toolName] as ToolConstructable};
} }
} }
/** /**
* getting classes that has prepare method * getting classes that has prepare method
*/ */
let sequenceData = this.getListOfPrepareFunctions(); const sequenceData = this.getListOfPrepareFunctions();
/** /**
* if sequence data contains nothing then resolve current chain and run other module prepare * if sequence data contains nothing then resolve current chain and run other module prepare
@ -263,53 +283,24 @@ export default class Tools extends Module {
/** /**
* to see how it works {@link Util#sequence} * to see how it works {@link Util#sequence}
*/ */
return _.sequence(sequenceData, (data) => { return _.sequence(sequenceData, (data: any) => {
this.success(data); this.success(data);
}, (data) => { }, (data) => {
this.fallback(data); this.fallback(data);
}); });
} }
/**
* Binds prepare function of plugins with user or default config
* @return {Array} list of functions that needs to be fired sequentially
*/
getListOfPrepareFunctions() {
let toolPreparationList = [];
for(let toolName in this.toolsClasses) {
let toolClass = this.toolsClasses[toolName];
if (typeof toolClass.prepare === 'function') {
toolPreparationList.push({
function : toolClass.prepare,
data : {
toolName,
config: this.toolsSettings[toolName][this.apiSettings.CONFIG]
}
});
} else {
/**
* If Tool hasn't a prepare method, mark it as available
*/
this.toolsAvailable[toolName] = toolClass;
}
}
return toolPreparationList;
}
/** /**
* @param {ChainData.data} data - append tool to available list * @param {ChainData.data} data - append tool to available list
*/ */
success(data) { public success(data) {
this.toolsAvailable[data.toolName] = this.toolsClasses[data.toolName]; this.toolsAvailable[data.toolName] = this.toolsClasses[data.toolName];
} }
/** /**
* @param {ChainData.data} data - append tool to unavailable list * @param {ChainData.data} data - append tool to unavailable list
*/ */
fallback(data) { public fallback(data) {
this.toolsUnavailable[data.toolName] = this.toolsClasses[data.toolName]; this.toolsUnavailable[data.toolName] = this.toolsClasses[data.toolName];
} }
@ -317,10 +308,10 @@ export default class Tools extends Module {
* Return Tool`s instance * Return Tool`s instance
* *
* @param {String} tool tool name * @param {String} tool tool name
* @param {IBlockToolData} data initial data * @param {BlockToolData} data initial data
* @return {IBlockTool} * @return {BlockTool}
*/ */
construct(tool, data) { public construct(tool, data) {
const plugin = this.toolsClasses[tool]; const plugin = this.toolsClasses[tool];
/** /**
@ -329,12 +320,12 @@ export default class Tools extends Module {
const config = this.toolsSettings[tool][this.apiSettings.CONFIG]; const config = this.toolsSettings[tool][this.apiSettings.CONFIG];
/** /**
* @type {{api: IAPI, config: ({}), data: IBlockToolData}} * @type {{api: API, config: ({}), data: BlockToolData}}
*/ */
const constructorOptions = { const constructorOptions = {
api: this.Editor.API.methods, api: this.Editor.API.methods,
config: config || {}, config: config || {},
data: data data,
}; };
return new plugin(constructorOptions); return new plugin(constructorOptions);
@ -343,15 +334,15 @@ export default class Tools extends Module {
/** /**
* Return Inline Tool's instance * Return Inline Tool's instance
* *
* @param {IInlineTool} tool * @param {InlineTool} tool
* @return {IInlineTool} instance * @return {InlineTool} instance
*/ */
constructInline(tool) { public constructInline(tool) {
/** /**
* @type {{api: IAPI}} * @type {{api: API}}
*/ */
const constructorOptions = { const constructorOptions = {
api: this.Editor.API.methods api: this.Editor.API.methods,
}; };
return new tool(constructorOptions); return new tool(constructorOptions);
@ -362,19 +353,56 @@ export default class Tools extends Module {
* @param {Tool} tool - Tool to check * @param {Tool} tool - Tool to check
* @return {Boolean} * @return {Boolean}
*/ */
isInitial(tool) { public isInitial(tool) {
return tool instanceof this.available[this.config.initialBlock]; return tool instanceof this.available[this.config.initialBlock];
} }
/** /**
* Return Tool's config by name * Return Tool's config by name
* @param {string} toolName * @param {string} toolName
* @return {IToolSettings} * @return {ToolSettings}
*/ */
getToolSettings(toolName) { public getToolSettings(toolName) {
return this.toolsSettings[toolName]; return this.toolsSettings[toolName];
} }
/**
* Binds prepare function of plugins with user or default config
* @return {Array} list of functions that needs to be fired sequentially
*/
private getListOfPrepareFunctions(): Array<{
function: (data: {toolName: string, config: ToolConfig}) => void,
data: {toolName: string, config: ToolConfig},
}> {
const toolPreparationList: Array<{
function: (data: {toolName: string, config: ToolConfig}) => void,
data: {toolName: string, config: ToolConfig}}
> = [];
for (const toolName in this.toolsClasses) {
if (this.toolsClasses.hasOwnProperty(toolName)) {
const toolClass = this.toolsClasses[toolName];
if (typeof toolClass.prepare === 'function') {
toolPreparationList.push({
function: toolClass.prepare,
data: {
toolName,
config: this.toolsSettings[toolName][this.apiSettings.CONFIG],
},
});
} else {
/**
* If Tool hasn't a prepare method, mark it as available
*/
this.toolsAvailable[toolName] = toolClass;
}
}
}
return toolPreparationList;
}
/** /**
* Returns internal tools * Returns internal tools
* Includes Bold, Italic, Link and Paragraph * Includes Bold, Italic, Link and Paragraph
@ -386,8 +414,8 @@ export default class Tools extends Module {
link: LinkInlineTool, link: LinkInlineTool,
paragraph: { paragraph: {
class: Paragraph, class: Paragraph,
inlineToolbar: true inlineToolbar: true,
} },
}; };
} }
} }

View file

@ -1,15 +1,19 @@
/**
* Module UI
*
* @type {UI}
*/
/** /**
* Prebuilded sprite of SVG icons * Prebuilded sprite of SVG icons
*/ */
import sprite from '../../../build/sprite.svg'; import sprite from '../../../build/sprite.svg';
/**
* Module UI
*
* @type {UI}
*/
import Module from '../__module';
import $ from '../dom';
import _ from '../utils';
import Selection from '../selection'; import Selection from '../selection';
import {ModuleConfig} from '../../types-internal/module-config';
/** /**
* @class * @class
@ -30,25 +34,31 @@ import Selection from '../selection';
* @property {Element} nodes.redactor - <ce-redactor> * @property {Element} nodes.redactor - <ce-redactor>
*/ */
export default class UI extends Module { export default class UI extends Module {
/**
* @constructor
*
* @param {EditorConfig} config
*/
constructor({config}) {
super({config});
this.nodes = { /**
holder: null, * CodeX Editor UI CSS class names
wrapper: null, * @return {{editorWrapper: string, editorZone: string}}
redactor: null */
private get CSS(): {editorWrapper: string, editorZone: string} {
return {
editorWrapper : 'codex-editor',
editorZone : 'codex-editor__redactor',
}; };
} }
/**
* HTML Elements used for UI
*/
public nodes: {[key: string]: HTMLElement} = {
holder: null,
wrapper: null,
redactor: null,
};
/** /**
* Making main interface * Making main interface
*/ */
async prepare() { public async prepare(): Promise<void> {
await this.make(); await this.make();
/** /**
@ -78,21 +88,17 @@ export default class UI extends Module {
} }
/** /**
* CodeX Editor UI CSS class names * Clean editor`s UI
* @return {{editorWrapper: string, editorZone: string, block: string}}
*/ */
get CSS() { public destroy(): void {
return { this.nodes.holder.innerHTML = '';
editorWrapper : 'codex-editor',
editorZone : 'codex-editor__redactor',
};
} }
/** /**
* Makes CodeX Editor interface * Makes CodeX Editor interface
* @return {Promise<any>} * @return {Promise<void>}
*/ */
async make() { private async make(): Promise<void> {
/** /**
* Element where we need to append CodeX Editor * Element where we need to append CodeX Editor
* @type {Element} * @type {Element}
@ -100,7 +106,7 @@ export default class UI extends Module {
this.nodes.holder = document.getElementById(this.config.holderId); this.nodes.holder = document.getElementById(this.config.holderId);
if (!this.nodes.holder) { if (!this.nodes.holder) {
throw Error("Holder wasn't found by ID: #" + this.config.holderId); throw Error('Holder wasn\'t found by ID: #' + this.config.holderId);
} }
/** /**
@ -116,17 +122,17 @@ export default class UI extends Module {
/** /**
* Appends CSS * Appends CSS
*/ */
loadStyles() { private loadStyles(): void {
/** /**
* Load CSS * Load CSS
*/ */
let styles = require('../../styles/main.css'); const styles = require('../../styles/main.css');
/** /**
* Make tag * Make tag
*/ */
let tag = $.make('style', null, { const tag = $.make('style', null, {
textContent: styles.toString() textContent: styles.toString(),
}); });
/** /**
@ -138,17 +144,22 @@ export default class UI extends Module {
/** /**
* Bind events on the CodeX Editor interface * Bind events on the CodeX Editor interface
*/ */
bindEvents() { private bindEvents(): void {
this.Editor.Listeners.on(this.nodes.redactor, 'click', event => this.redactorClicked(event), false ); this.Editor.Listeners.on(
this.Editor.Listeners.on(document, 'keydown', event => this.documentKeydown(event), true ); this.nodes.redactor,
this.Editor.Listeners.on(document, 'click', event => this.documentClicked(event), false ); 'click',
(event) => this.redactorClicked(event as MouseEvent),
false,
);
this.Editor.Listeners.on(document, 'keydown', (event) => this.documentKeydown(event as KeyboardEvent), true );
this.Editor.Listeners.on(document, 'click', (event) => this.documentClicked(event as MouseEvent), false );
} }
/** /**
* All keydowns on document * All keydowns on document
* @param {Event} event * @param {Event} event
*/ */
documentKeydown(event) { private documentKeydown(event: KeyboardEvent): void {
switch (event.keyCode) { switch (event.keyCode) {
case _.keyCodes.ENTER: case _.keyCodes.ENTER:
this.enterPressed(event); this.enterPressed(event);
@ -163,8 +174,8 @@ export default class UI extends Module {
* Ignore all other document's keydown events * Ignore all other document's keydown events
* @param {KeyboardEvent} event * @param {KeyboardEvent} event
*/ */
defaultBehaviour(event) { private defaultBehaviour(event: KeyboardEvent): void {
const keyDownOnEditor = event.target.closest(`.${this.CSS.editorWrapper}`); const keyDownOnEditor = (event.target as HTMLElement).closest(`.${this.CSS.editorWrapper}`);
/** /**
* Ignore keydowns on document * Ignore keydowns on document
@ -187,8 +198,8 @@ export default class UI extends Module {
* Enter pressed on document * Enter pressed on document
* @param event * @param event
*/ */
enterPressed(event) { private enterPressed(event: KeyboardEvent): void {
let hasPointerToBlock = this.Editor.BlockManager.currentBlockIndex >= 0; const hasPointerToBlock = this.Editor.BlockManager.currentBlockIndex >= 0;
/** /**
* If Caret is not set anywhere, event target on Enter is always Element that we handle * If Caret is not set anywhere, event target on Enter is always Element that we handle
@ -197,7 +208,7 @@ export default class UI extends Module {
* So, BlockManager points some Block and Enter press is on Body * So, BlockManager points some Block and Enter press is on Body
* We can create a new block * We can create a new block
*/ */
if (hasPointerToBlock && event.target.tagName === 'BODY') { if (hasPointerToBlock && (event.target as HTMLElement).tagName === 'BODY') {
/** /**
* Insert initial typed Block * Insert initial typed Block
*/ */
@ -224,13 +235,14 @@ export default class UI extends Module {
* All clicks on document * All clicks on document
* @param {MouseEvent} event - Click * @param {MouseEvent} event - Click
*/ */
documentClicked(event) { private documentClicked(event: MouseEvent): void {
/** /**
* Close Inline Toolbar when nothing selected * Close Inline Toolbar when nothing selected
* Do not fire check on clicks at the Inline Toolbar buttons * Do not fire check on clicks at the Inline Toolbar buttons
*/ */
const clickedOnInlineToolbarButton = event.target.closest(`.${this.Editor.InlineToolbar.CSS.inlineToolbar}`); const target = event.target as HTMLElement;
const clickedInsideofEditor = event.target.closest(`.${this.CSS.editorWrapper}`); const clickedOnInlineToolbarButton = target.closest(`.${this.Editor.InlineToolbar.CSS.inlineToolbar}`);
const clickedInsideofEditor = target.closest(`.${this.CSS.editorWrapper}`);
/** Clear highlightings and pointer on BlockManager */ /** Clear highlightings and pointer on BlockManager */
if (!clickedInsideofEditor && !Selection.isAtEditor) { if (!clickedInsideofEditor && !Selection.isAtEditor) {
@ -274,8 +286,8 @@ export default class UI extends Module {
* @see selectClickedBlock * @see selectClickedBlock
* *
*/ */
redactorClicked(event) { private redactorClicked(event: MouseEvent): void {
const clickedNode = event.target; const clickedNode = event.target as HTMLElement;
/** /**
* Select clicked Block as Current * Select clicked Block as Current
@ -319,7 +331,7 @@ export default class UI extends Module {
* - Block is an initial-block (Text) * - Block is an initial-block (Text)
* - Block is empty * - Block is empty
*/ */
let isInitialBlock = this.Editor.Tools.isInitial(this.Editor.BlockManager.currentBlock.tool), const isInitialBlock = this.Editor.Tools.isInitial(this.Editor.BlockManager.currentBlock.tool),
isEmptyBlock = this.Editor.BlockManager.currentBlock.isEmpty; isEmptyBlock = this.Editor.BlockManager.currentBlock.isEmpty;
if (isInitialBlock && isEmptyBlock) { if (isInitialBlock && isEmptyBlock) {
@ -333,8 +345,8 @@ export default class UI extends Module {
/** /**
* Append prebuilded sprite with SVG icons * Append prebuilded sprite with SVG icons
*/ */
appendSVGSprite() { private appendSVGSprite(): void {
let spriteHolder = $.make('div'); const spriteHolder = $.make('div');
spriteHolder.hidden = true; spriteHolder.hidden = true;
spriteHolder.style.display = 'none'; spriteHolder.style.display = 'none';
@ -342,11 +354,4 @@ export default class UI extends Module {
$.append(this.nodes.wrapper, spriteHolder); $.append(this.nodes.wrapper, spriteHolder);
} }
/**
* Clean editor`s UI
*/
destroy() {
this.nodes.holder.innerHTML = '';
}
} }

View file

@ -9,6 +9,7 @@ interface Element {
oMatchesSelector: (selector: string) => boolean; oMatchesSelector: (selector: string) => boolean;
prepend: (nodes: Node|Node[]|any) => void; prepend: (nodes: Node|Node[]|any) => void;
append: (nodes: Node|Node[]|DocumentFragment|void) => void;
} }
/** /**

View file

@ -29,14 +29,13 @@ interface Document {
/** /**
* Working with selection * Working with selection
* @typedef {Selection} Selection * @typedef {SelectionUtils} SelectionUtils
*/ */
export default class SelectionUtils { export default class SelectionUtils {
/** /**
* Editor styles * Editor styles
* @return {{editorWrapper: string, editorZone: string}} * @return {{editorWrapper: string, editorZone: string}}
* @constructor
*/ */
static get CSS(): {editorWrapper: string, editorZone: string} { static get CSS(): {editorWrapper: string, editorZone: string} {
return { return {

View file

@ -9,8 +9,8 @@ import Dom from './dom';
* @property {Object} data - data that will be passed to the success or fallback * @property {Object} data - data that will be passed to the success or fallback
* @property {Function} function - function's that must be called asynchronically * @property {Function} function - function's that must be called asynchronically
*/ */
interface ChainData { export interface ChainData {
data: any; data?: any;
function: (...args: any[]) => any; function: (...args: any[]) => any;
} }
@ -51,7 +51,7 @@ export default class Util {
* Returns basic keycodes as constants * Returns basic keycodes as constants
* @return {{}} * @return {{}}
*/ */
static get keyCodes(): object { static get keyCodes() {
return { return {
BACKSPACE: 8, BACKSPACE: 8,
TAB: 9, TAB: 9,
@ -79,7 +79,11 @@ export default class Util {
* *
* @return {Promise} * @return {Promise}
*/ */
public static async sequence(chains: ChainData[], success = () => {}, fallback = () => {}): Promise<void> { public static async sequence(
chains: ChainData[],
success: (data: any) => void = () => {},
fallback: (data: any) => void = () => {},
): Promise<void> {
/** /**
* Decorator * Decorator
* *
@ -219,7 +223,7 @@ export default class Util {
* @param {Boolean} immediate - call now * @param {Boolean} immediate - call now
* @return {Function} * @return {Function}
*/ */
public static debounce(func: () => void, wait: number , immediate: boolean): () => void { public static debounce(func: () => void, wait?: number , immediate?: boolean): () => void {
let timeout; let timeout;
return () => { return () => {

65
src/types-internal/editor-modules.d.ts vendored Normal file
View file

@ -0,0 +1,65 @@
import UI from '../components/modules/ui';
import BlockEvents from '../components/modules/blockEvents';
import Listeners from '../components/modules/listeners';
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 Events from '../components/modules/events';
import Shortcuts from '../components/modules/shortcuts';
import Paste from '../components/modules/paste';
import Notifier from '../components/modules/notifier';
import DragNDrop from '../components/modules/dragNDrop';
import ModificationsObserver from '../components/modules/modificationsObserver';
import Renderer from '../components/modules/renderer';
import Sanitizer from '../components/modules/sanitizer';
import Tools from '../components/modules/tools';
import API from '../components/modules/api/index';
import Caret from '../components/modules/caret';
import BlockManager from '../components/modules/blockManager';
import BlocksAPI from '../components/modules/api/blocks';
import CaretAPI from '../components/modules/api/caret';
import EventsAPI from '../components/modules/api/events';
import ListenersAPI from '../components/modules/api/listeners';
import SanitizerAPI from '../components/modules/api/sanitizer';
import ToolbarAPI from '../components/modules/api/toolbar';
import StylesAPI from '../components/modules/api/styles';
import SelectionAPI from '../components/modules/api/selection';
import NotifierAPI from '../components/modules/api/notifier';
import SaverAPI from '../components/modules/api/saver';
import Saver from '../components/modules/saver';
import BlockSelection from '../components/modules/blockSelection';
export interface EditorModules {
UI: UI;
BlockEvents: BlockEvents;
BlockSelection: BlockSelection;
Listeners: Listeners;
Toolbar: Toolbar;
InlineToolbar: InlineToolbar;
Toolbox: Toolbox;
BlockSettings: BlockSettings;
Events: Events;
Shortcuts: Shortcuts;
Paste: Paste;
DragNDrop: DragNDrop;
ModificationsObserver: ModificationsObserver;
Renderer: Renderer;
Sanitizer: Sanitizer;
Tools: Tools;
API: API;
Caret: Caret;
Saver: Saver;
Notifier: Notifier;
BlockManager: BlockManager;
BlocksAPI: BlocksAPI;
CaretAPI: CaretAPI;
EventsAPI: EventsAPI;
ListenersAPI: ListenersAPI;
SanitizerAPI: SanitizerAPI;
SaverAPI: SaverAPI;
SelectionAPI: SelectionAPI;
StylesAPI: StylesAPI;
ToolbarAPI: ToolbarAPI;
NotifierAPI: NotifierAPI;
}

22
src/types-internal/html-janitor.d.ts vendored Normal file
View file

@ -0,0 +1,22 @@
/**
* Declaration for external JS module
* After that we can use it at the TS modules
*/
declare module 'html-janitor' {
interface Config {
tags: {
[key: string]: boolean|{[attr: string]: boolean|string}|(() => any)
};
}
export class HTMLJanitor {
constructor(config: Config);
public clean(taintString: string): string;
}
/**
* Default export
*/
export default HTMLJanitor;
}

8
src/types-internal/module-config.d.ts vendored Normal file
View file

@ -0,0 +1,8 @@
import {EditorConfig} from '../../types/index';
/**
* Describes object passed to Editor modules constructor
*/
export interface ModuleConfig {
config: EditorConfig;
}

7
src/types-internal/svg.d.ts vendored Normal file
View file

@ -0,0 +1,7 @@
/**
* Allow to import .svg from components/modules/ui from TypeScript file
*/
declare module '*.svg' {
const content: string;
export default content;
}

View file

@ -1,5 +1,10 @@
{ {
"extends": "tslint:recommended", "extends": "tslint:recommended",
"linterOptions": {
"exclude": [
"node_modules"
]
},
"rules": { "rules": {
"indent": [true, "spaces", 2], "indent": [true, "spaces", 2],
"interface-name": false, "interface-name": false,
@ -12,6 +17,11 @@
"import-sources-order": "any", "import-sources-order": "any",
"named-imports-order": "case-insensitive" "named-imports-order": "case-insensitive"
}], }],
"no-empty": false "no-empty": false,
"no-namespace": false,
"variable-name": [true, "allow-leading-underscore", "allow-pascal-case"]
},
"globals": {
"require": true
} }
} }

61
types/api/blocks.d.ts vendored Normal file
View file

@ -0,0 +1,61 @@
import {OutputData} from '../data-formats/output-data';
/**
* Describes methods to manipulate with Editor`s blocks
*/
export interface Blocks {
/**
* Remove all blocks from Editor zone
*/
clear(): void;
/**
* Render passed data
* @param {OutputData} data
* @return {Promise<void>}
*/
render(data: OutputData): Promise<void>;
/**
* Removes current Block
*/
delete(): void;
/**
* Swaps two Blocks
* @param {number} fromIndex - block to swap
* @param {number} toIndex - block to swap with
*/
swap(fromIndex: number, toIndex: number): void;
/**
* Returns Block holder by Block index
* @param {number} index
* @returns {HTMLElement}
*/
getBlockByIndex(index: number): HTMLElement;
/**
* Returns current Block index
* @returns {number}
*/
getCurrentBlockIndex(): number;
/**
* Mark Block as stretched
* @param {number} index - Block to mark
* @param {boolean} status - stretch status
*/
stretchBlock(index: number, status?: boolean): void;
/**
* Returns Blocks count
* @return {number}
*/
getBlocksCount(): number;
/**
* Insert new Initial Block after current Block
*/
insertNewBlock(): void;
}

6
types/api/caret.d.ts vendored Normal file
View file

@ -0,0 +1,6 @@
/**
* Describes Editor`s caret API
*
* TODO
*/
export interface Caret {}

28
types/api/events.d.ts vendored Normal file
View file

@ -0,0 +1,28 @@
/**
* Describes Editor`s events API
*/
export interface Events {
/**
* Emits event
*
* @param {string} eventName
* @param {any} data
*/
emit(eventName: string, data: any): void;
/**
* Unsubscribe from event
*
* @param {string} eventName
* @param {(data: any) => void} callback
*/
off(eventName: string, callback: (data?: any) => void): void;
/**
* Subscribe to event
*
* @param {string} eventName
* @param {(data: any) => void} callback
*/
on(eventName: string, callback: (data?: any) => void): void;
}

10
types/api/index.d.ts vendored Normal file
View file

@ -0,0 +1,10 @@
export * from './blocks';
export * from './events';
export * from './listeners';
export * from './sanitizer';
export * from './saver';
export * from './selection';
export * from './styles';
export * from './caret';
export * from './toolbar';
export * from './notifier';

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

@ -0,0 +1,24 @@
/**
* Describes Editor`s listeners API
*/
export interface Listeners {
/**
* Subscribe to event dispatched on passed element
*
* @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;
/**
* Unsubscribe from event dispatched on passed element
*
* @param {Element} element
* @param {string} eventType
* @param {(event: Event) => void}handler
* @param {boolean} useCapture
*/
off(element: Element, eventType: string, handler: (event?: Event) => void, useCapture?: boolean): void;
}

14
types/api/notifier.d.ts vendored Normal file
View file

@ -0,0 +1,14 @@
import {ConfirmNotifierOptions, NotifierOptions, PromptNotifierOptions} from 'codex-notifier';
/**
* Notifier API
*/
export interface Notifier {
/**
* Show web notification
*
* @param {NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions}
*/
show: (options: NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions) => void;
}

14
types/api/sanitizer.d.ts vendored Normal file
View file

@ -0,0 +1,14 @@
import {SanitizerConfig} from '../index';
/**
* Describes Editor`s sanitizer API
*/
export interface Sanitizer {
/**
* Clean taint string with html and returns clean string
*
* @param {string} taintString
* @param {SanitizerConfig} config - configuration for sanitizer
*/
clean(taintString: string, config: SanitizerConfig): string;
}

13
types/api/saver.d.ts vendored Normal file
View file

@ -0,0 +1,13 @@
import {OutputData} from '../data-formats/output-data';
/**
* Describes Editor`s saver API
*/
export interface Saver {
/**
* Saves Editors data and returns promise with it
*
* @returns {Promise<OutputData>}
*/
save(): Promise<OutputData>;
}

18
types/api/selection.d.ts vendored Normal file
View file

@ -0,0 +1,18 @@
/**
* Describes methods for work with Selections
*/
export interface Selection {
/**
* Looks ahead from selection and find passed tag with class name
* @param {string} tagName - tag to find
* @param {string} className - tag's class name
* @return {HTMLElement|null}
*/
findParentTag(tagName: string, className?: string): HTMLElement|null;
/**
* Expand selection to passed tag
* @param {HTMLElement} node - tag that should contain selection
*/
expandToTag(node: HTMLElement): void;
}

44
types/api/styles.d.ts vendored Normal file
View file

@ -0,0 +1,44 @@
/**
* Describes styles API
*/
export interface Styles {
/**
* Main Editor`s block styles
*/
block: string;
/**
* Styles for Inline Toolbar button
*/
inlineToolButton: string;
/**
* Styles for active Inline Toolbar button
*/
inlineToolButtonActive: string;
/**
* Styles for inputs
*/
input: string;
/**
* Loader styles
*/
loader: string;
/**
* Styles for Settings box buttons
*/
settingsButton: string;
/**
* Styles for active Settings box buttons
*/
settingsButtonActive: string;
/**
* Styles for buttons
*/
button: string;
}

14
types/api/toolbar.d.ts vendored Normal file
View file

@ -0,0 +1,14 @@
/**
* Describes Toolbar API methods
*/
export interface Toolbar {
/**
* Closes Toolbar
*/
close(): void;
/**
* Opens Toolbar
*/
open(): void;
}

20
types/block-tunes/block-tune.d.ts vendored Normal file
View file

@ -0,0 +1,20 @@
import {API, ToolConfig} from '../index';
/**
* Describes BLockTune blueprint
*/
export interface BlockTune {
/**
* Returns block tune HTMLElement
*
* @return {HTMLElement}
*/
render(): HTMLElement;
}
/**
* Describes BlockTune class constructor function
*/
export interface BlockTuneConstructable {
new (config: {api: API, settings?: ToolConfig}): BlockTune;
}

1
types/block-tunes/index.d.ts vendored Normal file
View file

@ -0,0 +1 @@
export * from './block-tune';

53
types/configs/editor-config.d.ts vendored Normal file
View file

@ -0,0 +1,53 @@
import {ToolConstructable, ToolSettings} from '../tools';
import {OutputData} from '../index';
import {SanitizerConfig} from './sanitizer-config';
export interface EditorConfig {
/**
* Element where Editor will be append
*/
holderId: string;
/**
* This Tool will be used as default
* Name should be equal to one of Tool`s keys of passed tools
* If not specified, Paragraph Tool will be used
*/
initialBlock?: string;
/**
* First Block placeholder
*/
placeholder?: string;
/**
* Define default sanitizer configuration
* @see {@link sanitizer}
*/
sanitizer?: SanitizerConfig;
/**
* If true, toolbar won't be shown
*/
hideToolbar?: boolean;
/**
* Map of Tools to use
*/
tools?: {[toolName: string]: ToolConstructable|ToolSettings};
/**
* Data to render on Editor start
*/
data?: OutputData;
/**
* Fires when Editor is ready to work
*/
onReady?(): void;
/**
* Fires when something changed in DOM
*/
onChange?(): void;
}

3
types/configs/index.d.ts vendored Normal file
View file

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

50
types/configs/paste-config.d.ts vendored Normal file
View file

@ -0,0 +1,50 @@
import {BlockToolData} from '../index';
/**
* Tool onPaste configuration object
*/
export interface PasteConfig {
/**
* Array of tags Tool can substitute
* @type string[]
*/
tags?: string[];
/**
* Handler to process pasted HTML tag
*
* @param {HTMLElement} element
* @return {BlockToolData}
*/
handler?: (element: HTMLElement) => BlockToolData;
/**
* Object of string patterns Tool can substitute.
* Key is your internal key and value is RegExp
*
* @type {{[key: string]: Regexp}}
*/
patterns?: {[key: string]: RegExp};
/**
* Handler to process pasted patterns
*
* @param {string} text
* @param {string} key
* @return {BlockToolData}
*/
patternHandler?: (text: string, key: string) => BlockToolData;
/**
* Object with arrays of extensions and MIME types Tool can substitute
*/
files?: {extensions?: string[], mimeTypes?: string[]};
/**
* Handler to process pasted files
*
* @param {File} file
* @return {BlockToolData}
*/
fileHandler?: (file: File) => BlockToolData;
}

View file

@ -1,5 +1,4 @@
export default interface ISanitizerConfig { export interface SanitizerConfig {
/** /**
* Tag name and params not to be stripped off * Tag name and params not to be stripped off
* @see {@link https://github.com/guardian/html-janitor} * @see {@link https://github.com/guardian/html-janitor}

21
types/data-formats/output-data.d.ts vendored Normal file
View file

@ -0,0 +1,21 @@
import {BlockToolData} from '../tools';
export interface OutputData {
/**
* Editor's version
*/
version?: string;
/**
* Timestamp of saving in milliseconds
*/
time?: number;
/**
* Saved Blocks
*/
blocks: Array<{
type: string;
data: BlockToolData
}>;
}

69
types/index.d.ts vendored Normal file
View file

@ -0,0 +1,69 @@
/**
* For export type there should be one entry point,
* so we export all types from this file
* ------------------------------------
*/
import {EditorConfig} from './configs';
import {Blocks, Caret, Events, Listeners, Notifier, Sanitizer, Saver, Selection, Styles, Toolbar} from './api';
/**
* Interfaces used for development
*/
export {
Tool,
ToolConstructable,
InlineTool,
InlineToolConstructable,
BlockToolConstructable,
BlockTool,
BlockToolData,
ToolSettings,
ToolConfig,
} from './tools';
export {BlockTune, BlockTuneConstructable} from './block-tunes';
export {EditorConfig, SanitizerConfig, PasteConfig} from './configs';
export {OutputData} from './data-formats/output-data';
/**
* We have a namespace API {@link ./api/index.d.ts} (APIMethods) but we can not use it as interface
* So we should create new interface for exporting API type
*/
export interface API {
blocks: Blocks;
caret: Caret;
events: Events;
listeners: Listeners;
notifier: Notifier;
sanitizer: Sanitizer;
saver: Saver;
selection: Selection;
styles: Styles;
toolbar: Toolbar;
}
/**
* Main Editor class
*/
declare class EditorJS {
public static version: string;
public isReady: Promise<void>;
public blocks: Blocks;
public caret: Caret;
public events: Events;
public listeners: Listeners;
public sanitizer: Sanitizer;
public saver: Saver;
public selection: Selection;
public styles: Styles;
public toolbar: Toolbar;
constructor(configuration?: EditorConfig|string);
public destroy(): void;
}
export as namespace EditorJS;
export default EditorJS;

5
types/tools/block-tool-data.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
/**
* Object returned by Tool's {@link BlockTool#save} method
* Specified by Tool developer, so leave it as object
*/
export type BlockToolData = object;

View file

@ -1,12 +1,58 @@
import IBlockToolData from './tool-settings'; import {PasteConfig, SanitizerConfig} from '../configs';
import ITool from './tool'; import {BlockToolData} from './block-tool-data';
import {Tool, ToolConstructable} from './tool';
import {ToolConfig} from './tool-config';
import {API} from '../index';
/** /**
* Describe Block Tool object * Describe Block Tool object
* @see {@link docs/tools.md} * @see {@link docs/tools.md}
*/ */
export default interface IBlockTool extends ITool { export interface BlockTool extends Tool {
/**
* Sanitizer rules description
*/
sanitize?: SanitizerConfig;
/**
* @constructor
*/
constructor: BlockToolConstructable;
/**
* Return Tool's main block-wrapper
* @return {HTMLElement}
*/
render(): HTMLElement;
/**
* Process Tool's element in DOM and return raw data
* @param {HTMLElement} block - element created by {@link BlockTool#render} function
* @return {BlockToolData}
*/
save(block: HTMLElement): BlockToolData;
/**
* Create Block's settings block
* @return {HTMLElement}
*/
renderSettings?(): HTMLElement;
/**
* Validate Block's data
* @param {BlockToolData} blockData
* @return {boolean}
*/
validate?(blockData: BlockToolData): boolean;
/**
* Method that specified how to merge two Blocks with same type.
* Called by backspace at the beginning of the Block
* @param {BlockToolData} blockData
*/
merge?(blockData: BlockToolData): void;
}
export interface BlockToolConstructable extends ToolConstructable {
/** /**
* Pass `true` if Tool represents decorative empty Block * Pass `true` if Tool represents decorative empty Block
*/ */
@ -28,40 +74,9 @@ export default interface IBlockTool extends ITool {
toolboxIcon?: string; toolboxIcon?: string;
/** /**
* Sanitizer rules description * Paste substitutions configuration
*/ */
sanitizer?: object; onPaste?: PasteConfig;
/** new (config: {api: API, config: ToolConfig, data: BlockToolData}): BlockTool;
* Return Tool's main block-wrapper
* @return {HTMLElement}
*/
render(): HTMLElement;
/**
* Process Tool's element in DOM and return raw data
* @param {HTMLElement} block - element created by {@link IBlockTool#render} function
* @return {IBlockToolData}
*/
save(block: HTMLElement): IBlockToolData;
/**
* Create Block's settings block
* @return {HTMLElement}
*/
renderSettings?(): HTMLElement;
/**
* Validate Block's data
* @param {IBlockToolData} blockData
* @return {boolean}
*/
validate?(blockData: IBlockToolData): boolean;
/**
* Method that specified how to merge two Blocks with same type.
* Called by backspace at the beginning of the Block
* @param {IBlockToolData} blockData
*/
merge?(blockData: IBlockToolData): void;
} }

6
types/tools/index.d.ts vendored Normal file
View file

@ -0,0 +1,6 @@
export * from './block-tool';
export * from './block-tool-data';
export * from './inline-tool';
export * from './tool';
export * from './tool-config';
export * from './tool-settings';

Some files were not shown because too many files have changed in this diff Show more