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", {
"modules": "umd"
"modules": "umd",
"useBuiltIns": "entry"
}]
],
"plugins": [

View file

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

1
.gitignore vendored
View file

@ -8,3 +8,4 @@ Thumbs.db
node_modules/*
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` 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 [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.
- `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` — Improved zero-configuration initialization.
- and many little improvements.
- and many little improvements.
## 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 create a Block Tool Plugin](docs/tools.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.
@ -38,8 +38,8 @@ Sorry if we missed something. You can join a [Telegram-chat](//t.me/codex_editor
## Basics
CodeX Editor is a Block-Styled editor. Blocks is a structural units, of which the Entry is composed.
For example, `Paragraph`, `Heading`, `Image`, `Video`, `List` are Blocks. Each Block is represented by a Plugin.
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 Plugin.
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).
@ -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)
- Select text fragment and apply a style or insert a link from the Inline Toolbar
![](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.
![](https://capella.pics/01a55381-46cd-47c7-b92e-34765434f2ca.jpg)
![](https://capella.pics/01a55381-46cd-47c7-b92e-34765434f2ca.jpg)
## 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
`SHIFT+TAB` | Leaf back a Toolbox. | While Toolbox is opened
@ -72,7 +72,7 @@ Action | Shortcut | Restrictions
`CMD+B` | Bold style | On selection
`CMD+I` | Italic style | 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:
```js
@ -104,7 +104,7 @@ There are few steps to run CodeX Editor on your site.
## 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.
@ -120,7 +120,7 @@ Install the package via NPM or Yarn
npm i codex.editor --save-dev
```
Include module at your application
Include module in your application
```javascript
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 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
<div id="codex-editor"></div>
@ -217,7 +217,7 @@ editor.saver.save()
.then((savedData) => {
console.log(savedData);
});
```
```
## 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 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.
```html

View file

@ -18,7 +18,7 @@ Each Tool's instance called with an params object.
| config | `object` | Special configuration params passed in «config» |
| data | `object` | Data to be rendered in this Tool |
[iapi-link]: ../src/components/interfaces/api.ts
[iapi-link]: ../src/types-internal/api.ts
#### 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",
"version": "2.4.3",
"version": "2.5.0",
"description": "Codex Editor. Native JS, based on API and Open Source",
"main": "build/codex-editor.js",
"types": "./types/index.d.ts",
"scripts": {
"build": "rimraf dist && yarn svg && yarn build:dev",
"svg": "svg-sprite-generate -d src/assets/ -o build/sprite.svg",
@ -22,6 +23,8 @@
"@babel/register": "^7.0.0",
"@babel/runtime": "^7.1.2",
"@codexteam/shortcuts": "^1.0.0",
"@types/webpack": "^4.4.16",
"@types/webpack-env": "^1.13.6",
"babel-loader": "^8.0.4",
"babel-plugin-add-module-exports": "^1.0.0",
"babel-plugin-class-display-name": "^2.1.0",
@ -51,11 +54,11 @@
"rimraf": "^2.6.2",
"stylelint": "^9.3.0",
"svg-sprite-generator": "0.0.7",
"ts-loader": "^4.4.2",
"ts-loader": "^5.3.0",
"tslint": "^5.11.0",
"tslint-loader": "^3.6.0",
"typescript": "^2.9.2",
"webpack": "^4.16.2",
"webpack": "4.20.2",
"webpack-cli": "^3.1.0"
}
}

View file

@ -1,10 +1,14 @@
'use strict';
import {EditorConfig} from '../types';
declare const VERSION: string;
/**
* Apply polyfills
*/
import '@babel/register';
if (!window || !window._babelPolyfill) require('@babel/polyfill');
import 'components/polyfills';
import Core from './components/core';
@ -17,17 +21,28 @@ import Core from './components/core';
* @author CodeX-Team <https://ifmo.su>
*/
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 */
static get version() {
static get version(): string {
return VERSION;
}
/**
* @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
*/
@ -48,7 +63,7 @@ export default class CodexEditor {
/**
* We need to export isReady promise in the constructor
* as it can be used before other API methods are exported
* @type {Promise<any | never>}
* @type {Promise<void>}
*/
this.isReady = editor.isReady.then(() => {
this.exportAPI(editor);
@ -61,7 +76,7 @@ export default class CodexEditor {
*
* @param editor
*/
exportAPI(editor) {
public exportAPI(editor: Core): void {
const fieldsToExport = [ 'configuration' ];
const destroy = () => {
editor.moduleInstances.Listeners.removeAll();
@ -70,13 +85,15 @@ export default class CodexEditor {
editor = null;
for (const field in this) {
delete this[field];
if (this.hasOwnProperty(field)) {
delete this[field];
}
}
Object.setPrototypeOf(this, null);
};
fieldsToExport.forEach(field => {
fieldsToExport.forEach((field) => {
this[field] = editor[field];
});
@ -84,6 +101,6 @@ export default class CodexEditor {
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 IEditorConfig from './interfaces/editor-config';
import IModuleConfig from './interfaces/module-config';
import {EditorModules} from '../types-internal/editor-modules';
import {EditorConfig} from '../../types';
import {ModuleConfig} from '../types-internal/module-config';
/**
* @abstract
@ -9,27 +9,27 @@ import IModuleConfig from './interfaces/module-config';
*
* @typedef {Module} Module
* @property {Object} config - Editor user settings
* @property {IEditorConfig} Editor - List of Editor modules
* @property {EditorModules} Editor - List of Editor modules
*/
export default class Module {
/**
* Editor modules list
* @type {IEditor}
* @type {EditorModules}
*/
protected Editor: IEditor;
protected Editor: EditorModules;
/**
* Editor configuration object
* @type {IEditorConfig}
* @type {EditorConfig}
*/
protected config: IEditorConfig;
protected config: EditorConfig;
/**
* @constructor
* @param {IModuleConfig}
* @param {EditorConfig}
*/
constructor({config}: IModuleConfig) {
constructor({config}: ModuleConfig) {
if (new.target === Module) {
throw new TypeError('Constructors for abstract class Module are not allowed.');
}
@ -39,9 +39,9 @@ export default class Module {
/**
* Editor modules setter
* @param {IEditor} Editor
* @param {EditorModules} Editor
*/
set state(Editor) {
set state(Editor: EditorModules) {
this.Editor = Editor;
}
}

View file

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

View file

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

View file

@ -4,18 +4,16 @@
*
* @copyright <CodeX Team> 2018
*/
import IBlockTune from '../interfaces/block-tune';
import $ from '../dom';
import {API, BlockTune} from '../../../types';
declare var $: any;
declare var _: any;
export default class MoveUpTune implements IBlockTune {
export default class MoveUpTune implements BlockTune {
/**
* Property that contains CodeX Editor API methods
* @see {api.md}
*/
private readonly api: any;
private readonly api: API;
/**
* Styles
@ -30,7 +28,7 @@ export default class MoveUpTune implements IBlockTune {
/**
* MoveUpTune constructor
*
* @param {Object} api
* @param {{api: API}} api
*/
public constructor({api}) {
this.api = api;
@ -43,7 +41,12 @@ export default class MoveUpTune implements IBlockTune {
public render(): HTMLElement {
const moveUpButton = $.make('div', [this.CSS.button, this.CSS.wrapper], {});
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;
}
@ -65,8 +68,8 @@ export default class MoveUpTune implements IBlockTune {
return;
}
const currentBlockElement = this.api.blocks.getBlockByIndex(currentBlockIndex).holder,
previousBlockElement = this.api.blocks.getBlockByIndex(currentBlockIndex - 1).holder;
const currentBlockElement = this.api.blocks.getBlockByIndex(currentBlockIndex);
const previousBlockElement = this.api.blocks.getBlockByIndex(currentBlockIndex - 1);
/**
* 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 './utils';
type Tool = any;
/**
* @class Block
* @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
*
*/
@ -17,7 +25,6 @@ type Tool = any;
import MoveUpTune from './block-tunes/block-tune-move-up';
import DeleteTune from './block-tunes/block-tune-delete';
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
@ -154,7 +161,7 @@ export default class Block {
* Returns tool's sanitizer config
* @return {object}
*/
get sanitize(): object {
get sanitize(): SanitizerConfig {
return this.tool.sanitize;
}
@ -251,13 +258,45 @@ export default class Block {
this.holder.classList.toggle(Block.CSS.wrapperStretched, state);
}
/**
* Block Tool`s name
*/
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 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;
/**
@ -268,7 +307,13 @@ export default class Block {
* @param {Object} settings - default settings
* @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.tool = toolInstance;
this.class = toolClass;
@ -277,7 +322,7 @@ export default class Block {
this.holder = this.compose();
/**
* @type {IBlockTune[]}
* @type {BlockTune[]}
*/
this.tunes = this.makeTunes();
}
@ -303,19 +348,16 @@ export default class Block {
* Call plugins merge method
* @param {Object} data
*/
public mergeWith(data: object): Promise<void> {
return Promise.resolve()
.then(() => {
this.tool.merge(data);
});
public async mergeWith(data: BlockToolData): Promise<void> {
await this.tool.merge(data);
}
/**
* Extracts data from Block
* Groups Tool's save processing time
* @return {Object}
*/
public async save(): Promise<void|{tool: string, data: any, time: number}> {
const extractedBlock = await this.tool.save(this.pluginsContent);
public async save(): Promise<void|{tool: string, data: BlockToolData, time: number}> {
const extractedBlock = await this.tool.save(this.pluginsContent as HTMLElement);
/**
* Measuring execution time
@ -334,8 +376,8 @@ export default class Block {
time : measuringEnd - measuringStart,
};
})
.catch(function(error) {
_.log(`Saving proccess for ${this.tool.name} tool failed due to the ${error}`, 'log', 'red');
.catch((error) => {
_.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
* @returns {Boolean|Object} valid
*/
public validateData(data: object): object|false {
public validateData(data: BlockToolData): BlockToolData|false {
let isValid = true;
if (this.tool.validate instanceof Function) {
@ -365,13 +407,13 @@ export default class Block {
/**
* Make an array with default settings
* Each block has default tune instance that have states
* @return {IBlockTune[]}
* @return {BlockTune[]}
*/
public makeTunes(): IBlockTune[] {
public makeTunes(): BlockTune[] {
const tunesList = [MoveUpTune, DeleteTune, MoveDownTune];
// 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({
api: this.api,
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
*/
/**
* Dynamically imported utils
*
* @typedef {Dom} $ - {@link components/dom.js}
* @typedef {Util} _ - {@link components/utils.js}
*/
/**
* Require Editor modules places in components/modules dir
*/
// eslint-disable-next-line
let modules = editorModules.map( module => require('./modules/' + module ));
const contextRequire = require.context('./modules', true);
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
@ -26,31 +36,27 @@ let modules = editorModules.map( module => require('./modules/' + module ));
* @type {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
*
*/
constructor(config) {
/**
* 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 = {};
constructor(config?: EditorConfig|string) {
/**
* Ready promise. Resolved if CodeX Editor is ready to work, rejected otherwise
*/
@ -62,13 +68,13 @@ export default class Core {
});
Promise.resolve()
.then(() => {
.then(async () => {
this.configuration = config;
})
.then(() => this.validate())
.then(() => this.init())
.then(() => this.start())
.then(() => {
await this.validate();
await this.init();
await this.start();
_.log('I\'m ready! (ノ◕ヮ◕)ノ*:・゚✧');
setTimeout(() => {
@ -78,7 +84,7 @@ export default class Core {
onReady();
}, 500);
})
.catch(error => {
.catch((error) => {
_.log(`CodeX Editor does not ready because of ${error}`, 'error');
/**
@ -90,52 +96,53 @@ export default class Core {
/**
* 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
*/
if (typeof config === 'string' || typeof config === 'undefined') {
config = {
holderId: config
this.config = {
holderId: config || 'codex-editor',
};
} else {
this.config = config;
}
/**
* 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
* Uses in case when there is no blocks passed
* @type {{type: (*), data: {text: null}}}
*/
let initialBlockData = {
const initialBlockData = {
type : this.config.initialBlock,
data : {}
data : {},
};
this.config.holderId = config.holderId || 'codex-editor';
this.config.placeholder = config.placeholder || 'write your story...';
this.config.sanitizer = config.sanitizer || {
this.config.placeholder = this.config.placeholder || 'write your story...';
this.config.sanitizer = this.config.sanitizer || {
p: true,
b: true,
a: true
};
a: true,
} as SanitizerConfig;
this.config.hideToolbar = config.hideToolbar ? config.hideToolbar : false;
this.config.tools = config.tools || {};
this.config.data = config.data || {};
this.config.onReady = config.onReady || function () {};
this.config.onChange = config.onChange || function () {};
this.config.hideToolbar = this.config.hideToolbar ? this.config.hideToolbar : false;
this.config.tools = this.config.tools || {};
this.config.data = this.config.data || {} as OutputData;
this.config.onReady = this.config.onReady || (() => {});
this.config.onChange = this.config.onChange || (() => {});
/**
* Initialize Blocks to pass data to the Renderer
*/
if (_.isEmpty(this.config.data)) {
this.config.data = {};
this.config.data = {} as OutputData;
this.config.data.blocks = [ initialBlockData ];
} else {
if (!this.config.data.blocks || this.config.data.blocks.length === 0) {
@ -148,37 +155,41 @@ export default class Core {
* Returns private property
* @returns {EditorConfig}
*/
get configuration() {
get configuration(): EditorConfig|string {
return this.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
*/
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
*/
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
*/
for (let toolName in this.config.tools) {
const tool = this.config.tools[toolName];
for (const toolName in this.config.tools) {
if (this.config.tools.hasOwnProperty(toolName)) {
const tool = this.config.tools[toolName];
if (!_.isFunction(tool) && !_.isFunction(tool.class)) {
return Promise.reject(`Tool «${toolName}» must be a constructor function or an object with that function in the «class» property`);
if (!_.isFunction(tool) && !_.isFunction((tool as ToolSettings).class)) {
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
* - configure
*/
init() {
public init() {
/**
* Make modules instances and save it to the @property this.moduleInstances
*/
@ -200,69 +211,22 @@ export default class Core {
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!
*
* Get list of modules that needs to be prepared and return a sequence (Promise)
* @return {Promise}
*/
async start() {
const modulesToPrepare = ['Tools', 'UI', 'BlockManager', 'Paste', 'DragNDrop', 'ModificationsObserver', 'BlockSelection'];
public async start() {
const modulesToPrepare = [
'Tools',
'UI',
'BlockManager',
'Paste',
'DragNDrop',
'ModificationsObserver',
'BlockSelection',
];
await modulesToPrepare.reduce(
(promise, module) => promise.then(async () => {
@ -275,9 +239,67 @@ export default class Core {
}
_.log(`Preparing ${module} module`, 'timeEnd');
}),
Promise.resolve()
Promise.resolve(),
);
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 {array|string} classNames - list or name of CSS classname(s)
* @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);
if ( Array.isArray(classNames) ) {
@ -87,7 +87,7 @@ export default class Dom {
* @param {Element|DocumentFragment} parent - where to append
* @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) ) {
elements.forEach( (el) => parent.appendChild(el) );
} else {

View file

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

View file

@ -1,7 +1,5 @@
import InlineTool from '../interfaces/tools/inline-tool';
import ISanitizerConfig from '../interfaces/sanitizer-config';
declare var $: any;
import $ from '../dom';
import {InlineTool, SanitizerConfig} from '../../../types';
/**
* Italic Tool
@ -24,10 +22,10 @@ export default class ItalicInlineTool implements InlineTool {
* Leave <i> tags
* @return {object}
*/
static get sanitize(): ISanitizerConfig {
static get sanitize(): SanitizerConfig {
return {
i: {},
};
} as SanitizerConfig;
}
/**
@ -51,17 +49,11 @@ export default class ItalicInlineTool implements InlineTool {
button: null,
};
/**
* @param {{api: IAPI}} - CodeX Editor API
*/
constructor({api}) {
}
/**
* Create button for Inline Toolbar
*/
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.classList.add(this.CSS.button, this.CSS.buttonModifier);
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 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
*
@ -26,14 +25,14 @@ export default class LinkInlineTool implements InlineTool {
* Leave <a> tags
* @return {object}
*/
static get sanitize(): ISanitizerConfig {
static get sanitize(): SanitizerConfig {
return {
a: {
href: true,
target: '_blank',
rel: 'nofollow',
},
};
} as SanitizerConfig;
}
/**
@ -62,7 +61,10 @@ export default class LinkInlineTool implements InlineTool {
/**
* Elements
*/
private nodes: {button: HTMLButtonElement, input: HTMLInputElement} = {
private nodes: {
button: HTMLButtonElement;
input: HTMLInputElement;
} = {
button: null,
input: null,
};
@ -80,10 +82,10 @@ export default class LinkInlineTool implements InlineTool {
/**
* 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}) {
this.inlineToolbar = api.toolbar;
@ -94,7 +96,7 @@ export default class LinkInlineTool implements InlineTool {
* Create button for Inline Toolbar
*/
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.classList.add(this.CSS.button, this.CSS.buttonModifier);
this.nodes.button.appendChild($.svg('link', 15, 14));
@ -106,10 +108,10 @@ export default class LinkInlineTool implements InlineTool {
* Input for the link
*/
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.classList.add(this.CSS.input);
this.nodes.input.addEventListener('keydown', (event) => {
this.nodes.input.addEventListener('keydown', (event: KeyboardEvent) => {
if (event.keyCode === this.ENTER_KEY ) {
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 IInputOutputData from '../interfaces/input-output-data';
import IModuleConfig from '../interfaces/module-config';
import {Blocks} from '../../../../types/api';
import {OutputData} from '../../../../types';
import Block from '../../block';
import {ModuleConfig} from '../../../types-internal/module-config';
/**
* @class BlocksAPI
* provides with methods working with Block
*/
export default class BlocksAPI extends Module implements IBlocksAPI {
/**
* Save Editor config. API provides passed configuration to the Blocks
*/
constructor({config}: IModuleConfig) {
super({config});
}
export default class BlocksAPI extends Module {
/**
* Available methods
* @return {IBlocksAPI}
* @return {Blocks}
*/
get methods(): IBlocksAPI {
get methods(): Blocks {
return {
clear: () => this.clear(),
render: (data: IInputOutputData) => this.render(data),
render: (data: OutputData) => this.render(data),
delete: () => this.delete(),
swap: (fromIndex: number, toIndex: number) => this.swap(fromIndex, toIndex),
getBlockByIndex: (index: number) => this.getBlockByIndex(index),
getCurrentBlockIndex: () => this.getCurrentBlockIndex(),
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(),
};
}
@ -52,13 +45,14 @@ export default class BlocksAPI extends Module implements IBlocksAPI {
}
/**
* Returns Current Block
* Returns Block holder by Block index
* @param {Number} index
*
* @return {Object}
* @return {HTMLElement}
*/
public getBlockByIndex(index: number): object {
return this.Editor.BlockManager.getBlockByIndex(index);
public getBlockByIndex(index: number): HTMLElement {
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
* @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.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 {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);
if (!block) {
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 {IEventsAPI} from '../interfaces/api';
import IModuleConfig from '../interfaces/module-config';
import Module from '../../__module';
import {Events} from '../../../../types/api';
/**
* @class EventsAPI
* provides with methods working with Toolbar
*/
export default class EventsAPI extends Module implements IEventsAPI {
/**
* Save Editor config. API provides passed configuration to the Blocks
*/
constructor({config}: IModuleConfig) {
super({config});
}
export default class EventsAPI extends Module {
/**
* Available methods
* @return {IEventsAPI}
* @return {Events}
*/
get methods(): IEventsAPI {
get methods(): Events {
return {
emit: (eventName: string, data: object) => this.emit(eventName, data),
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
* if you cant to read more about how API works, please see docs
*/
declare var Module: any;
declare var $: any;
declare var _: any;
import { IAPI } from '../interfaces/api';
import Module from '../../__module';
import {API as APIInterfaces} from '../../../../types';
/**
* @class API
*/
export default class API extends Module {
/**
* Save Editor config. API provides passed configuration to the Blocks
* @param {EditorConfig} config
*/
constructor({config}) {
super({config});
}
public get methods(): IAPI {
public get methods(): APIInterfaces {
return {
blocks: this.Editor.BlocksAPI.methods,
caret: this.Editor.CaretAPI.methods,
events: this.Editor.EventsAPI.methods,
listener: this.Editor.ListenerAPI.methods,
listeners: this.Editor.ListenersAPI.methods,
notifier: this.Editor.NotifierAPI.methods,
sanitizer: this.Editor.SanitizerAPI.methods,
saver: this.Editor.SaverAPI.methods,
selection: this.Editor.SelectionAPI.methods,
styles: this.Editor.StylesAPI.classes,
toolbar: this.Editor.ToolbarAPI.methods,
};
} as APIInterfaces;
}
}

View file

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

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';
export default class NotifierAPI extends Module {
@ -6,7 +7,7 @@ export default class NotifierAPI extends Module {
/**
* Available methods
*/
get methods() {
get methods(): Notifier {
return {
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 IModuleConfig from '../interfaces/module-config';
import {IStylesAPI} from '../interfaces/api';
import Module from '../../__module';
import {Styles} from '../../../../types/api';
/**
*
*/
export default class StylesAPI extends Module {
/**
* Save Editor config
* API provides passed configuration to the Blocks
*/
constructor({config}: IModuleConfig) {
super({config});
}
get classes(): IStylesAPI {
get classes(): Styles {
return {
/**
* 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
*/
declare var Module: any;
declare var $: any;
declare var _: any;
import Module from '../__module';
import _ from '../utils';
export default class BlockEvents extends Module {
/**
* @constructor
*/
constructor({config}) {
super({config});
}
/**
* All keydowns on Block
* @param {KeyboardEvent} event - keydown
@ -147,7 +139,7 @@ export default class BlockEvents extends Module {
* @param {DragEvent} e
*/
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;
}
@ -158,7 +150,7 @@ export default class BlockEvents extends Module {
* @param {DragEvent} e
*/
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;
}
@ -169,7 +161,7 @@ export default class BlockEvents extends Module {
*/
private enter(event: KeyboardEvent): void {
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.
@ -228,7 +220,7 @@ export default class BlockEvents extends Module {
private backspace(event: KeyboardEvent): void {
const BM = this.Editor.BlockManager;
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.
@ -297,7 +289,7 @@ export default class BlockEvents extends Module {
BM.mergeBlocks(targetBlock, blockToMerge)
.then( () => {
/** Restore caret position after merge */
this.Editor.Caret.restoreCaret(targetBlock.pluginsContent);
this.Editor.Caret.restoreCaret(targetBlock.pluginsContent as HTMLElement);
targetBlock.pluginsContent.normalize();
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
* @version 1.0.0
*/
declare var Module: any;
declare var _: any;
declare var $: any;
import Module from '../__module';
import _ from '../utils';
import $ from '../dom';
import SelectionUtils from '../selection';

View file

@ -10,26 +10,15 @@
*/
import Selection from '../selection';
import Module from '../__module';
import Block from '../block';
import $ from '../dom';
import _ from '../utils';
/**
* @typedef {Caret} Caret
*/
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
@ -37,14 +26,130 @@ export default class Caret extends Module {
* @static
* @returns {{START: string, END: string, DEFAULT: string}}
*/
static get positions() {
public static get positions(): {START: string, END: string, DEFAULT: string} {
return {
START: 'start',
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
* 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
*
* @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
*/
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;
let element;
switch(position) {
switch (position) {
case Caret.positions.START:
element = block.firstInput;
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
*/
_.delay( () => {
this.set(nodeToSet, offset);
this.set(nodeToSet as HTMLElement, offset);
}, 20)();
BlockManager.setCurrentBlockByChildNode(block.holder);
@ -102,27 +208,28 @@ export default class Caret extends Module {
* Set caret to the current input of current Block.
*
* @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
*/
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 nodeToSet = $.getDeepestNode(input);
switch (position) {
case Caret.positions.START:
this.set(nodeToSet, 0);
this.set(nodeToSet as HTMLElement, 0);
break;
case Caret.positions.END:
const contentLength = $.getContentLength(nodeToSet);
this.set(nodeToSet, contentLength);
this.set(nodeToSet as HTMLElement, contentLength);
break;
default:
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
* @param {Element} element - target node.
* @param {HTMLElement} element - target node.
* @param {Number} offset - offset
*/
set( element, offset = 0) {
public set(element: HTMLElement, offset: number = 0): void {
const range = document.createRange(),
selection = Selection.get();
/** if found deepest node is native input */
if ($.isNativeInput(element)) {
element.focus();
element.selectionStart = element.selectionEnd = offset;
(element as HTMLInputElement).selectionStart = (element as HTMLInputElement).selectionEnd = offset;
return;
}
@ -151,23 +258,23 @@ export default class Caret extends Module {
selection.removeAllRanges();
selection.addRange(range);
/** If new cursor position is not visible, scroll to it */
const {top, bottom} = range.getBoundingClientRect();
const {innerHeight} = window;
if (top < 0) window.scrollBy(0, top);
if (bottom > innerHeight) window.scrollBy(0, bottom - innerHeight);
};
if (top < 0) { window.scrollBy(0, top); }
if (bottom > innerHeight) { window.scrollBy(0, bottom - innerHeight); }
}
/**
* Set Caret to the last Block
* If last block is not empty, append another empty block
*/
setToTheLastBlock() {
let lastBlock = this.Editor.BlockManager.lastBlock;
public setToTheLastBlock(): void {
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.
@ -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
*/
extractFragmentFromCaretPosition() {
let selection = Selection.get();
public extractFragmentFromCaretPosition(): void|DocumentFragment {
const selection = Selection.get();
if (selection.rangeCount) {
const selectRange = selection.getRangeAt(0);
const currentBlockInput = this.Editor.BlockManager.currentBlock.currentInput;
selectRange.deleteContents();
if (currentBlockInput) {
let range = selectRange.cloneRange(true);
const range = selectRange.cloneRange();
range.selectNodeContents(currentBlockInput);
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
* 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}
*/
navigateNext(force = false) {
public navigateNext(force: boolean = false): boolean {
const {currentBlock, nextContentfulBlock} = this.Editor.BlockManager;
const {nextInput} = currentBlock;
@ -291,9 +356,14 @@ export default class Caret extends Module {
*
* @return {Boolean}
*/
navigatePrevious(force = false) {
public navigatePrevious(force: boolean = false): boolean {
const {currentBlock, previousContentfulBlock} = this.Editor.BlockManager;
const {previousInput} = currentBlock || {};
if (!currentBlock) {
return false;
}
const {previousInput} = currentBlock;
if (!previousContentfulBlock && !previousInput) {
return false;
@ -316,121 +386,12 @@ export default class Caret extends Module {
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
* @param {Node} element
*/
createShadow(element) {
let shadowCaret = document.createElement('span');
public createShadow(element): void {
const shadowCaret = document.createElement('span');
shadowCaret.classList.add(Caret.CSS.shadowCaret);
element.insertAdjacentElement('beforeEnd', shadowCaret);
@ -438,10 +399,10 @@ export default class Caret extends Module {
/**
* Restores caret position
* @param {Node} element
* @param {HTMLElement} element
*/
restoreCaret(element) {
let shadowCaret = element.querySelector(`.${Caret.CSS.shadowCaret}`);
public restoreCaret(element: HTMLElement): void {
const shadowCaret = element.querySelector(`.${Caret.CSS.shadowCaret}`);
if (!shadowCaret) {
return;
@ -455,15 +416,56 @@ export default class Caret extends Module {
* - select shadowed span
* - use extractContent to remove it from DOM
*/
let sel = new Selection();
const sel = new Selection();
sel.expandToTag(shadowCaret);
sel.expandToTag(shadowCaret as HTMLElement);
setTimeout(() => {
let newRange = document.createRange();
const newRange = document.createRange();
newRange.selectNode(shadowCaret);
newRange.extractContents();
}, 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';
declare var Module: any;
import Module from '../__module';
import Caret from './caret';
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.
*/
try {
BlockManager.setCurrentBlockByChildNode(dropEvent.target, 'end');
BlockManager.setCurrentBlockByChildNode(dropEvent.target as Node, Caret.positions.END);
} catch (e) {
BlockManager.setCurrentBlockByChildNode(BlockManager.lastBlock.holder, 'end');
BlockManager.setCurrentBlockByChildNode(BlockManager.lastBlock.holder, Caret.positions.END);
}
Paste.processDataTransfer(dropEvent.dataTransfer, true);

View file

@ -1,3 +1,5 @@
import Module from '../__module';
/**
* @module eventDispatcher
*
@ -12,13 +14,12 @@
* @property {Object} subscribers - all subscribers grouped by event name
*/
export default class Events extends Module {
/**
* @constructor
* Object with events` names as key and array of callback functions as value
* @type {{}}
*/
constructor({config}) {
super({config});
this.subscribers = {};
}
private subscribers: {[name: string]: Array<(data?: any) => void>} = {};
/**
* Subscribe any event on callback
@ -26,7 +27,7 @@ export default class Events extends Module {
* @param {String} eventName - event name
* @param {Function} callback - subscriber
*/
on(eventName, callback) {
public on(eventName: string, callback: (data: any) => void) {
if (!(eventName in this.subscribers)) {
this.subscribers[eventName] = [];
}
@ -41,13 +42,13 @@ export default class Events extends Module {
* @param {String} eventName - event name
* @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]) {
return;
}
this.subscribers[eventName].reduce(function (previousData, currentHandler) {
let newData = currentHandler(previousData);
this.subscribers[eventName].reduce((previousData, currentHandler) => {
const newData = currentHandler(previousData);
return newData ? newData : previousData;
}, data);
@ -59,8 +60,8 @@ export default class Events extends Module {
* @param eventName
* @param callback
*/
off(eventName, callback) {
for(let i = 0; i < this.subscribers[eventName].length; i++) {
public off(eventName: string, callback: (data: any) => void): void {
for (let i = 0; i < this.subscribers[eventName].length; i++) {
if (this.subscribers[eventName][i] === callback) {
delete this.subscribers[eventName][i];
break;
@ -72,7 +73,7 @@ export default class Events extends Module {
* Destroyer
* clears subsribers list
*/
destroy() {
public destroy(): void {
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
*/
import IEditorConfig from '../interfaces/editor-config';
declare const Module: any;
declare const _: any;
import Module from '../__module';
import _ from '../utils';
export default class ModificationsObserver extends Module {
@ -23,17 +21,9 @@ export default class ModificationsObserver extends Module {
* @type {Function}
*/
private mutationDebouncer = _.debounce( () => {
this.config.onChange.call();
this.config.onChange();
}, ModificationsObserver.DebounceTimer);
/**
* Constructor
* @param {IEditorConfig} config
*/
constructor({config}) {
super({config});
}
/**
* 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 SelectionUtils from '../selection';
declare const Module: any;
declare const $: any;
declare const _: any;
import Module from '../__module';
import $ from '../dom';
import _ from '../utils';
import {BlockToolData, PasteConfig} from '../../../types';
/**
* 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;
handler: (element: HTMLElement) => IBlockToolData;
/**
* Callback to handle pasted element
*
* @param {HTMLElement} element
* @return {BlockToolData}
*/
handler: (element: HTMLElement) => BlockToolData;
}
/**
* 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;
/**
* Pattern regexp
* @type {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;
}
/**
* 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[];
/**
* Array of MIME types Tool can handle
* @type {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.
*
* @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;
content: HTMLElement;
isBlock: boolean;
handler: (content: HTMLElement|string, patten?: RegExp) => IBlockToolData;
}
/**
* Tool onPaste configuration object
*/
interface IPasteConfig {
tags?: string[];
handler?: (element: HTMLElement) => IBlockToolData;
patterns?: {[key: string]: RegExp};
patternHandler?: (text: string, key: string) => IBlockToolData;
files?: {extensions?: string[], mimeTypes?: string[]};
fileHandler?: (file: File) => IBlockToolData;
/**
* Pasted data. Processed and wrapped to HTML element
* @type {HTMLElement}
*/
content: HTMLElement;
/**
* True if content should be inserted as new Block
* @type {boolean}
*/
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
*/
private toolsTags: {[tag: string]: ITagSubstitute} = {};
private toolsTags: {[tag: string]: TagSubstitute} = {};
/**
* Store tags to substitute by tool name
@ -97,21 +138,13 @@ export default class Paste extends Module {
private tagsByTool: {[tools: string]: string[]} = {};
/** Patterns` substitutions parameters */
private toolsPatterns: IPatternSubstitute[] = [];
private toolsPatterns: PatternSubstitute[] = [];
/** Files` substitutions parameters */
private toolsFiles: {
[tool: string]: IFilesSubstitution,
[tool: string]: FilesSubstitution,
} = {};
/**
* @constructor
* @param {IEditorConfig} config
*/
constructor({config}) {
super({config});
}
/**
* 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
*
* @param {DataTransfer} dataTransfer - pasted or dropped data transfer object
* @param {boolean} isDragNDrop
*/
public async processDataTransfer(dataTransfer: DataTransfer, isDragNDrop = false): Promise<void> {
const { Sanitizer } = this.Editor;
@ -208,9 +242,9 @@ export default class Paste extends Module {
* Get tags to substitute by Tool
*
* @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) {
_.log(
`«${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
*
* @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;
let {extensions, mimeTypes} = files;
@ -304,9 +338,9 @@ export default class Paste extends Module {
* Get RegExp patterns to substitute by Tool
*
* @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)) {
return;
}
@ -351,7 +385,7 @@ export default class Paste extends Module {
return true;
}
const block = BlockManager.getBlock(element);
const block = BlockManager.getBlock(element as HTMLElement);
return !block;
}
@ -362,10 +396,6 @@ export default class Paste extends Module {
* @param {ClipboardEvent} event
*/
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 (
this.isNativeBehaviour(event.target) && !event.clipboardData.types.includes('Files')
@ -385,7 +415,7 @@ export default class Paste extends Module {
private async processFiles(items: DataTransferItemList) {
const {BlockManager} = this.Editor;
let dataToInsert: Array<{type: string, data: IBlockToolData}>;
let dataToInsert: Array<{type: string, data: BlockToolData}>;
dataToInsert = await Promise.all(
Array
@ -483,9 +513,9 @@ export default class Paste extends Module {
* Split HTML string to blocks and return it as array of Block data
*
* @param {string} innerHTML
* @returns {IPasteData[]}
* @returns {PasteData[]}
*/
private processHTML(innerHTML: string): IPasteData[] {
private processHTML(innerHTML: string): PasteData[] {
const {Tools, Sanitizer} = this.Editor,
initialTool = this.config.initialBlock,
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
*
* @param {string} plain
* @returns {IPasteData[]}
* @returns {PasteData[]}
*/
private processPlain(plain: string): IPasteData[] {
private processPlain(plain: string): PasteData[] {
const {initialBlock} = this.config as {initialBlock: string},
{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
* 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,
{BlockManager, Caret, Sanitizer} = this.Editor,
{content, tool} = dataToInsert;
@ -607,9 +637,9 @@ export default class Paste extends Module {
* Get patterns` matches
*
* @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 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
* @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),
{BlockManager, Caret} = this.Editor,
{currentBlock} = BlockManager;
@ -644,9 +674,9 @@ export default class Paste extends Module {
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
*
@ -7,14 +11,6 @@
* @version 2.0.0
*/
export default class Renderer extends Module {
/**
* @constructor
* @param {EditorConfig} config
*/
constructor({config}) {
super({config});
}
/**
* @typedef {Object} RendererBlocks
* @property {String} type - tool name
@ -45,16 +41,10 @@ export default class Renderer extends Module {
* Make plugin blocks from array of plugin`s data
* @param {RendererBlocks[]} blocks
*/
render(blocks) {
let chainData = [];
public render(blocks: BlockToolData[]): Promise<void> {
const chainData = blocks.map((block) => ({function: () => this.insertBlock(block)}));
for (let i = 0; i < blocks.length; i++) {
chainData.push({
function: () => this.insertBlock(blocks[i])
});
}
return _.sequence(chainData);
return _.sequence(chainData as ChainData[]);
}
/**
@ -63,20 +53,20 @@ export default class Renderer extends Module {
* Insert block to working zone
*
* @param {Object} item
* @returns {Promise.<T>}
* @returns {Promise<void>}
* @private
*/
insertBlock(item) {
let tool = item.type,
data = item.data,
settings = item.settings;
public async insertBlock(item): Promise<void> {
const tool = item.type;
const data = item.data;
const settings = item.settings;
if (tool in this.Editor.Tools.available) {
try {
this.Editor.BlockManager.insert(tool, data, settings);
} catch (error) {
_.log(`Block «${tool}» skipped because of plugins error`, 'warn', data);
Promise.reject(error);
throw Error(error);
}
} 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');
}
return Promise.resolve();
}
}

View file

@ -16,7 +16,8 @@
* {@link SanitizerConfig}
*/
import ISanitizerConfig from '../interfaces/sanitizer-config';
import Module from '../__module';
import _ from '../utils';
/**
* @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 IBlockToolData from '../interfaces/tools/block-tool-data';
import IInlineTool from '../interfaces/tools/inline-tool';
import {BlockToolData, InlineToolConstructable, SanitizerConfig} from '../../../types';
export default class Sanitizer extends Module {
/**
* Memoize tools config
*/
private configCache: {[toolName: string]: ISanitizerConfig} = {};
private configCache: {[toolName: string]: SanitizerConfig} = {};
/**
* Cached inline tools config
*/
private inlineToolsConfigCache: ISanitizerConfig | 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});
}
private inlineToolsConfigCache: SanitizerConfig | null = null;
/**
* Sanitize Blocks
*
* Enumerate blocks and clean data
*
* @param {{tool, data: IBlockToolData}[]} blocksData[]
* @param {{tool, data: BlockToolData}[]} blocksData[]
*/
public sanitizeBlocks(
blocksData: Array<{tool: string, data: IBlockToolData}>,
): Array<{tool: string, data: IBlockToolData}> {
blocksData: Array<{tool: string, data: BlockToolData}>,
): Array<{tool: string, data: BlockToolData}> {
return blocksData.map((block) => {
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
*
* @param {IBlockToolData|object|*} dataToSanitize - taint string or object/array that contains taint string
* @param {ISanitizerConfig} rules - object with sanitizer rules
* @param {BlockToolData|object|*} dataToSanitize - taint string or object/array that contains taint string
* @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:
* - Array
@ -133,7 +118,7 @@ export default class Sanitizer extends Module {
*
* @return {string} clean HTML
*/
public clean(taintString: string, customConfig: ISanitizerConfig = {}): string {
public clean(taintString: string, customConfig: SanitizerConfig = {} as SanitizerConfig): string {
const sanitizerConfig = {
tags: customConfig,
@ -150,10 +135,10 @@ export default class Sanitizer extends Module {
* Merge with inline tool config
*
* @param {string} toolName
* @param {ISanitizerConfig} toolRules
* @return {ISanitizerConfig}
* @param {SanitizerConfig} toolRules
* @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
*/
@ -174,7 +159,7 @@ export default class Sanitizer extends Module {
const toolRules = toolClass.sanitize;
const toolConfig = {};
const toolConfig = {} as SanitizerConfig;
for (const fieldName in toolRules) {
if (toolRules.hasOwnProperty(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,
* otherwise get only enabled
*/
public getInlineToolsConfig(name: string): ISanitizerConfig {
public getInlineToolsConfig(name: string): SanitizerConfig {
const {Tools} = this.Editor;
const toolsConfig = Tools.getToolSettings(name),
enableInlineTools = toolsConfig.inlineToolbar || [];
let config = {};
let config = {} as SanitizerConfig;
if (typeof enableInlineTools === 'boolean' && enableInlineTools) {
/**
@ -212,8 +197,11 @@ export default class Sanitizer extends Module {
/**
* getting only enabled
*/
enableInlineTools.map( (inlineToolName) => {
config = Object.assign(config, Tools.inline[inlineToolName][Tools.apiSettings.SANITIZE_CONFIG]);
(enableInlineTools as string[]).map( (inlineToolName) => {
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
*/
public getAllInlineToolsConfig(): ISanitizerConfig {
public getAllInlineToolsConfig(): SanitizerConfig {
const {Tools} = this.Editor;
if (this.inlineToolsConfigCache) {
return this.inlineToolsConfigCache;
}
const config: ISanitizerConfig = {};
const config: SanitizerConfig = {} as SanitizerConfig;
Object.entries(Tools.inline)
.forEach( ([name, inlineTool]: [string, IInlineTool]) => {
Object.assign(config, inlineTool[Tools.apiSettings.SANITIZE_CONFIG]);
});
.forEach( ([name, inlineTool]: [string, InlineToolConstructable]) => {
Object.assign(config, inlineTool[Tools.apiSettings.SANITIZE_CONFIG]);
});
this.inlineToolsConfigCache = config;
@ -247,7 +235,7 @@ export default class Sanitizer extends Module {
* @param {array} array - [1, 2, {}, []]
* @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));
}
@ -257,7 +245,7 @@ export default class Sanitizer extends Module {
* @param {object} rules - { b: true } or true|false
* @return {object}
*/
private cleanObject(object: any, rules: ISanitizerConfig|{[field: string]: ISanitizerConfig}): any {
private cleanObject(object: any, rules: SanitizerConfig|{[field: string]: SanitizerConfig}): any {
const cleanData = {};
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
* - 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;
}
/**
* @param {string} taintString
* @param {ISanitizerConfig|boolean} rule
* @param {SanitizerConfig|boolean} rule
* @return {string}
*/
private cleanOneItem(taintString: string, rule: ISanitizerConfig|boolean): string {
private cleanOneItem(taintString: string, rule: SanitizerConfig|boolean): string {
if (typeof rule === 'object') {
return this.clean(taintString, rule);
} else if (rule === false) {
return this.clean(taintString, {});
return this.clean(taintString, {} as SanitizerConfig);
} else {
return taintString;
}
@ -300,7 +288,7 @@ export default class Sanitizer extends Module {
* undefined, null, 0, 1, 2 not a rules
* @param config
*/
private isRule(config: ISanitizerConfig): boolean {
private isRule(config: SanitizerConfig): boolean {
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
*/
private createHTMLJanitorInstance(config: {tags: ISanitizerConfig}): any {
private createHTMLJanitorInstance(config: {tags: SanitizerConfig}): HTMLJanitor|null {
if (config) {
return new HTMLJanitor(config);
}

View file

@ -5,18 +5,10 @@
* @author Codex Team
* @version 2.0.0
*/
import Module from '../__module';
import {OutputData} from '../../../types';
/**
* @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;
}
declare const VERSION: string;
/**
* @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 {String} json - Editor JSON output
*/
declare const Module: any;
declare const VERSION: string;
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
* @return {SavedData}
* @return {OutputData}
*/
public save(): Promise<SavedData> {
const blocks = this.Editor.BlockManager.blocks,
public async save(): Promise<OutputData> {
const blocks = this.Editor.BlockManager.blocks,
chainData = [];
blocks.forEach((block) => {
chainData.push(block.data);
});
return Promise.all(chainData)
.then((extractedData) => this.Editor.Sanitizer.sanitizeBlocks(extractedData))
.then((allExtractedData) => this.makeOutput(allExtractedData))
.then((outputData) => {
return outputData;
});
const extractedData = await Promise.all(chainData);
const sanitizedData = await this.Editor.Sanitizer.sanitizeBlocks(extractedData);
return this.makeOutput(sanitizedData);
}
/**
* Creates output object with saved data, time and version of editor
* @param {Object} allExtractedData
* @return {SavedData}
* @return {OutputData}
*/
private makeOutput(allExtractedData): SavedData {
private makeOutput(allExtractedData): OutputData {
let totalTime = 0;
const blocks = [];
@ -87,8 +62,8 @@ export default class Saver extends Module {
return {
time: +new Date(),
version: VERSION,
blocks,
version: VERSION,
};
}
}

View file

@ -1,12 +1,29 @@
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
*/
declare var Module: any;
import Module from '../__module';
/**
* @class Shortcut
@ -14,27 +31,18 @@ declare var Module: any;
*
* Internal Shortcuts Module
*/
export default class Shortcuts extends Module implements IShortcuts {
export default class Shortcuts extends Module {
/**
* All registered shortcuts
* @type {IShortcut[]}
* @type {Shortcut[]}
*/
private registeredShortcuts: IShortcut[];
/**
* @constructor
* @param {IEditorConfig} config
*/
constructor({config}) {
super({config});
this.registeredShortcuts = [];
}
private registeredShortcuts: Shortcut[] = [];
/**
* Register shortcut
* @param {IShortcut} shortcut
* @param {ShortcutData} shortcut
*/
public add(shortcut: IShortcut): void {
public add(shortcut: ShortcutData): void {
const { UI } = this.Editor;
const newShortcut = new Shortcut({
@ -48,9 +56,12 @@ export default class Shortcuts extends Module implements IShortcuts {
/**
* Remove shortcut
* @param {IShortcut} shortcut
* @param {ShortcutData} shortcut
*/
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
*
@ -10,24 +13,12 @@
* |________________________|
*/
export default class BlockSettings extends Module {
/**
* @constructor
*/
constructor({config}) {
super({config});
this.nodes = {
wrapper: null,
toolSettings: null,
defaultSettings: null
};
}
/**
* Module Events
* @return {{opened: string, closed: string}}
*/
get events() {
public get events(): {opened: string, closed: string} {
return {
opened: 'block-settings-opened',
closed: 'block-settings-closed',
@ -38,7 +29,7 @@ export default class BlockSettings extends Module {
* Block Settings CSS
* @return {{wrapper, wrapperOpened, toolSettings, defaultSettings, button}}
*/
static get CSS() {
private static get CSS() {
return {
// Settings Panel
wrapper: 'ce-settings',
@ -46,10 +37,27 @@ export default class BlockSettings extends Module {
toolSettings: 'ce-settings__plugin-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:
* - Tool's Settings
@ -57,7 +65,7 @@ export default class BlockSettings extends Module {
*
* @return {Element}
*/
make() {
public make(): void {
this.nodes.wrapper = $.make('div', BlockSettings.CSS.wrapper);
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]);
}
/**
* 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() {
public open(): void {
this.nodes.wrapper.classList.add(BlockSettings.CSS.wrapperOpened);
/**
@ -113,7 +97,7 @@ export default class BlockSettings extends Module {
/**
* Close Block Settings pane
*/
close() {
public close(): void {
this.nodes.wrapper.classList.remove(BlockSettings.CSS.wrapperOpened);
/** Clear settings */
@ -123,4 +107,20 @@ export default class BlockSettings extends Module {
/** Tell to subscribers that block settings is 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
@ -51,31 +54,26 @@
*/
export default class Toolbar extends Module {
/**
* @constructor
* HTML Elements used for Toolbar UI
*/
constructor({config}) {
super({config});
public nodes: {[key: string]: HTMLElement} = {
wrapper : null,
content : null,
actions : null,
this.nodes = {
wrapper : null,
content : null,
actions : null,
// Content Zone
plusButton : null,
// Content Zone
plusButton : null,
// Actions Zone
blockActionsButtons: null,
settingsToggler : null,
};
}
// Actions Zone
blockActionsButtons: null,
settingsToggler : null,
};
/**
* CSS styles
* @return {Object}
* @constructor
*/
static get CSS() {
private static get CSS() {
return {
toolbar: 'ce-toolbar',
content: 'ce-toolbar__content',
@ -96,18 +94,17 @@ export default class Toolbar extends Module {
/**
* Makes toolbar
*/
make() {
public make(): void {
this.nodes.wrapper = $.make('div', Toolbar.CSS.toolbar);
/**
* Make Content Zone and Actions Zone
*/
['content', 'actions'].forEach( el => {
['content', 'actions'].forEach( (el) => {
this.nodes[el] = $.make('div', Toolbar.CSS[el]);
$.append(this.nodes.wrapper, this.nodes[el]);
});
/**
* Fill Content Zone:
* - Plus Button
@ -116,8 +113,7 @@ export default class Toolbar extends Module {
this.nodes.plusButton = $.make('div', Toolbar.CSS.plusButton);
$.append(this.nodes.plusButton, $.svg('plus', 14, 14));
$.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
@ -159,14 +155,14 @@ export default class Toolbar extends Module {
* Move Toolbar to the Current Block
* @param {Boolean} forceClose - force close Toolbar Settings and Toolbar
*/
move(forceClose = true) {
public move(forceClose: boolean = true): void {
if (forceClose) {
/** Close Toolbox when we move toolbar */
this.Editor.Toolbox.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
@ -192,7 +188,7 @@ export default class Toolbar extends Module {
/**
* Open Toolbar with Plus Button
*/
open() {
public open(): void {
/**
* Wait Block rendering for correct height computing
*/
@ -206,14 +202,14 @@ export default class Toolbar extends Module {
* returns toolbar opened state
* @return {Boolean}
*/
get opened() {
public get opened(): boolean {
return this.nodes.wrapper.classList.contains(Toolbar.CSS.toolbarOpened);
}
/**
* Close the Toolbar
*/
close() {
public close(): void {
this.nodes.wrapper.classList.remove(Toolbar.CSS.toolbarOpened);
/** Close components */
@ -225,7 +221,7 @@ export default class Toolbar extends Module {
* Plus Button public methods
* @return {{hide: function(): void, show: function(): void}}
*/
get plusButton() {
public get plusButton(): {hide: () => void, show: () => void} {
return {
hide: () => this.nodes.plusButton.classList.add(Toolbar.CSS.plusButtonHidden),
show: () => {
@ -233,7 +229,7 @@ export default class Toolbar extends Module {
return;
}
this.nodes.plusButton.classList.remove(Toolbar.CSS.plusButtonHidden);
}
},
};
}
@ -241,7 +237,7 @@ export default class Toolbar extends Module {
* Handler for Plus Button
* @param {MouseEvent} event
*/
plusButtonClicked() {
private plusButtonClicked(): void {
this.Editor.Toolbox.toggle();
}
@ -249,19 +245,17 @@ export default class Toolbar extends Module {
* Bind events on the Toolbar Elements:
* - Block Settings
*/
bindEvents() {
private bindEvents(): void {
/**
* Settings toggler
*/
this.Editor.Listeners.on(this.nodes.settingsToggler, 'click', (event) => {
this.settingsTogglerClicked(event);
});
this.Editor.Listeners.on(this.nodes.settingsToggler, 'click', () => this.settingsTogglerClicked());
}
/**
* Clicks on the Block Settings toggler
*/
settingsTogglerClicked() {
private settingsTogglerClicked(): void {
if (this.Editor.BlockSettings.opened) {
this.Editor.BlockSettings.close();
} else {

View file

@ -1,12 +1,12 @@
import ITool from '../interfaces/tools/tool';
import Module from '../../__module';
import $ from '../../dom';
declare var Module: any;
declare var $: any;
import EditorConfig from '../interfaces/editor-config';
import InlineTool from '../interfaces/tools/inline-tool';
import SelectionUtils from '../selection';
import _ from '../utils';
import BoldInlineTool from '../../inline-tools/inline-tool-bold';
import ItalicInlineTool from '../../inline-tools/inline-tool-italic';
import LinkInlineTool from '../../inline-tools/inline-tool-link';
import SelectionUtils from '../../selection';
import _ from '../../utils';
import {InlineTool, InlineToolConstructable, ToolConstructable, ToolSettings} from '../../../../types';
/**
* Inline toolbar with actions that modifies selected text fragment
@ -17,6 +17,34 @@ import _ from '../utils';
*/
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
*/
@ -32,7 +60,7 @@ export default class InlineToolbar extends Module {
/**
* Inline Toolbar elements
*/
private nodes = {
private nodes: {wrapper: HTMLElement, buttons: HTMLElement, actions: HTMLElement} = {
wrapper: null,
buttons: null,
/**
@ -52,14 +80,6 @@ export default class InlineToolbar extends Module {
*/
private toolsInstances: Map<string, InlineTool>;
/**
* @constructor
* @param {EditorConfig} config
*/
constructor({config}) {
super({config});
}
/**
* Inline Toolbar Tools
*
@ -152,10 +172,22 @@ export default class InlineToolbar extends Module {
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
*/
private open() {
private open(): void {
/**
* 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)
*/
this.tools.forEach( (toolInstance, toolName) => {
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) => {
this.tools.forEach( (toolInstance: InlineTool) => {
if (typeof toolInstance.clear === 'function') {
toolInstance.clear();
}
@ -217,7 +237,7 @@ export default class InlineToolbar extends Module {
}
// 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) {
return false;
@ -233,7 +253,7 @@ export default class InlineToolbar extends Module {
*/
private filterTools(): void {
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),
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
.entries(Tools.internalTools)
.filter(([name, toolClass]: [string, ITool]) => toolClass[Tools.apiSettings.IS_INLINE])
.map(([name, toolClass]: [string, ITool]) => name);
.filter(([name, toolClass]: [string, ToolConstructable|ToolSettings]) => {
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'
@ -349,7 +375,7 @@ export default class InlineToolbar extends Module {
/**
* Enable Tool shortcut with Editor Shortcuts Module
* @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 {
this.Editor.Shortcuts.add({
@ -398,7 +424,7 @@ export default class InlineToolbar extends Module {
* Check Tools` state by selection
*/
private checkToolsState(): void {
this.tools.forEach( (toolInstance, toolName) => {
this.tools.forEach( (toolInstance) => {
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
@ -11,37 +14,41 @@ import IEditorConfig from '../interfaces/editor-config';
*
*/
export default class Toolbox extends Module {
private static LEAF_DIRECTIONS = {
RIGHT: 'right',
LEFT: 'left',
};
/**
* @constructor
* @param {IEditorConfig} config
* Opening state
* @type {boolean}
*/
constructor({config}) {
super({config});
public opened: boolean = false;
this.nodes = {
toolbox: null,
buttons: []
};
/**
* HTMLElements used for Toolbox UI
*/
public nodes: {
toolbox: HTMLElement,
buttons: HTMLElement[],
} = {
toolbox: null,
buttons: [],
};
/**
* Opening state
* @type {boolean}
*/
this.opened = false;
/**
* Active button index
* -1 equals no chosen Tool
* @type {number}
*/
private activeButtonIndex: number = -1;
/**
* Active button index
* -1 equals no chosen Tool
* @type {number}
*/
this.activeButtonIndex = -1;
/**
* How many tools displayed in Toolbox
* @type {number}
*/
this.displayedToolsCount = 0;
}
/**
* How many tools displayed in Toolbox
* @type {number}
*/
private displayedToolsCount: number = 0;
/**
* CSS styles
@ -59,21 +66,157 @@ export default class Toolbox extends Module {
/**
* Makes the Toolbox
*/
make() {
public make(): void {
this.nodes.toolbox = $.make('div', Toolbox.CSS.toolbox);
$.append(this.Editor.Toolbar.nodes.content, this.nodes.toolbox);
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
*/
addTools() {
let tools = this.Editor.Tools.toolsAvailable;
private addTools(): void {
const tools = this.Editor.Tools.available;
for (let toolName in tools) {
this.addTool(toolName, tools[toolName]);
for (const toolName in tools) {
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
*
* @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;
if (tool[api.IS_DISPLAYED_IN_TOOLBOX] && !tool[api.TOOLBAR_ICON]) {
@ -106,8 +249,8 @@ export default class Toolbox extends Module {
return;
}
let button = $.make('li', [ Toolbox.CSS.toolboxButton ], {
title: toolName
const button = $.make('li', [ Toolbox.CSS.toolboxButton ], {
title: toolName,
});
button.innerHTML = tool.toolboxIcon;
@ -120,7 +263,7 @@ export default class Toolbox extends Module {
/**
* Add click listener
*/
this.Editor.Listeners.on(button, 'click', (event) => {
this.Editor.Listeners.on(button, 'click', (event: KeyboardEvent|MouseEvent) => {
this.toolButtonActivate(event, toolName);
});
@ -139,28 +282,28 @@ export default class Toolbox extends Module {
/**
* Enable shortcut Block Tool implemented shortcut
* @param {IBlockTool} tool - Tool class
* @param {BlockToolConstructable} tool - Tool class
* @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({
name: shortcut,
handler: (event) => {
handler: (event: KeyboardEvent) => {
event.preventDefault();
this.insertNewBlock(tool, toolName);
}
},
});
}
/**
* 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
*/
insertNewBlock(tool, toolName) {
private insertNewBlock(tool: BlockToolConstructable, toolName: string) {
/**
* @type {Block}
*/
@ -192,138 +335,4 @@ export default class Toolbox extends Module {
*/
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 ItalicInlineTool from '../inline-tools/inline-tool-italic';
import LinkInlineTool from '../inline-tools/inline-tool-link';
const Paragraph = require('../tools/paragraph/dist/bundle');
/**
* @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 {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|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 save @todo add description
* @property settings @todo add description
@ -49,54 +52,12 @@ const Paragraph = require('../tools/paragraph/dist/bundle');
* @property {EditorConfig} config - Editor config
*/
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
* @return {Tool[]}
*/
get available() {
public get available(): {[name: string]: ToolConstructable} {
return this.toolsAvailable;
}
@ -104,7 +65,7 @@ export default class Tools extends Module {
* Returns unavailable Tools
* @return {Tool[]}
*/
get unavailable() {
public get unavailable(): {[name: string]: ToolConstructable} {
return this.toolsUnavailable;
}
@ -112,7 +73,7 @@ export default class Tools extends Module {
* Return Tools for the Inline Toolbar
* @return {Object} - object of Inline Tool's classes
*/
get inline() {
public get inline(): {[name: string]: ToolConstructable} {
if (this._inlineTools) {
return this._inlineTools;
}
@ -126,10 +87,14 @@ export default class Tools extends Module {
* Some Tools validation
*/
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) {
_.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;
}
@ -154,14 +119,10 @@ export default class Tools extends Module {
/**
* Return editor block tools
*/
get blockTools() {
public get blockTools(): {[name: string]: BlockToolConstructable} {
// eslint-disable-next-line no-unused-vars
const tools = Object.entries(this.available).filter( ([name, tool]) => {
if (tool[this.apiSettings.IS_INLINE]) {
return false;
}
return true;
return !tool[this.apiSettings.IS_INLINE];
});
/**
@ -178,7 +139,7 @@ export default class Tools extends Module {
* Constant for available Tools Settings
* @return {object}
*/
get apiSettings() {
public get apiSettings() {
return {
CONFIG: 'config',
IS_CONTENTLESS: 'contentless',
@ -190,28 +151,87 @@ export default class Tools extends Module {
IS_PASTE_DISALLOWED: 'disallowPaste',
SHORTCUT: 'shortcut',
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
* @return {Promise}
*/
prepare() {
public prepare() {
/**
* Assign internal tools
*/
Object.assign(this.config.tools, this.internalTools);
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
*/
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
* save class and settings separately
@ -219,15 +239,15 @@ export default class Tools extends Module {
if (typeof this.config.tools[toolName] === 'object') {
/**
* 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
* @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
@ -236,22 +256,22 @@ export default class Tools extends Module {
} else {
/**
* 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
* @type {{}}
*/
this.toolsSettings[toolName] = {};
this.toolsSettings[toolName] = {class: this.config.tools[toolName] as ToolConstructable};
}
}
/**
* 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
@ -263,53 +283,24 @@ export default class Tools extends Module {
/**
* to see how it works {@link Util#sequence}
*/
return _.sequence(sequenceData, (data) => {
return _.sequence(sequenceData, (data: any) => {
this.success(data);
}, (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
*/
success(data) {
public success(data) {
this.toolsAvailable[data.toolName] = this.toolsClasses[data.toolName];
}
/**
* @param {ChainData.data} data - append tool to unavailable list
*/
fallback(data) {
public fallback(data) {
this.toolsUnavailable[data.toolName] = this.toolsClasses[data.toolName];
}
@ -317,10 +308,10 @@ export default class Tools extends Module {
* Return Tool`s instance
*
* @param {String} tool tool name
* @param {IBlockToolData} data initial data
* @return {IBlockTool}
* @param {BlockToolData} data initial data
* @return {BlockTool}
*/
construct(tool, data) {
public construct(tool, data) {
const plugin = this.toolsClasses[tool];
/**
@ -329,12 +320,12 @@ export default class Tools extends Module {
const config = this.toolsSettings[tool][this.apiSettings.CONFIG];
/**
* @type {{api: IAPI, config: ({}), data: IBlockToolData}}
* @type {{api: API, config: ({}), data: BlockToolData}}
*/
const constructorOptions = {
api: this.Editor.API.methods,
config: config || {},
data: data
data,
};
return new plugin(constructorOptions);
@ -343,15 +334,15 @@ export default class Tools extends Module {
/**
* Return Inline Tool's instance
*
* @param {IInlineTool} tool
* @return {IInlineTool} instance
* @param {InlineTool} tool
* @return {InlineTool} instance
*/
constructInline(tool) {
public constructInline(tool) {
/**
* @type {{api: IAPI}}
* @type {{api: API}}
*/
const constructorOptions = {
api: this.Editor.API.methods
api: this.Editor.API.methods,
};
return new tool(constructorOptions);
@ -362,19 +353,56 @@ export default class Tools extends Module {
* @param {Tool} tool - Tool to check
* @return {Boolean}
*/
isInitial(tool) {
public isInitial(tool) {
return tool instanceof this.available[this.config.initialBlock];
}
/**
* Return Tool's config by name
* @param {string} toolName
* @return {IToolSettings}
* @return {ToolSettings}
*/
getToolSettings(toolName) {
public getToolSettings(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
* Includes Bold, Italic, Link and Paragraph
@ -386,8 +414,8 @@ export default class Tools extends Module {
link: LinkInlineTool,
paragraph: {
class: Paragraph,
inlineToolbar: true
}
inlineToolbar: true,
},
};
}
}

View file

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

View file

@ -29,14 +29,13 @@ interface Document {
/**
* Working with selection
* @typedef {Selection} Selection
* @typedef {SelectionUtils} SelectionUtils
*/
export default class SelectionUtils {
/**
* Editor styles
* @return {{editorWrapper: string, editorZone: string}}
* @constructor
*/
static get CSS(): {editorWrapper: string, editorZone: string} {
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 {Function} function - function's that must be called asynchronically
*/
interface ChainData {
data: any;
export interface ChainData {
data?: any;
function: (...args: any[]) => any;
}
@ -51,7 +51,7 @@ export default class Util {
* Returns basic keycodes as constants
* @return {{}}
*/
static get keyCodes(): object {
static get keyCodes() {
return {
BACKSPACE: 8,
TAB: 9,
@ -79,7 +79,11 @@ export default class Util {
*
* @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
*
@ -219,7 +223,7 @@ export default class Util {
* @param {Boolean} immediate - call now
* @return {Function}
*/
public static debounce(func: () => void, wait: number , immediate: boolean): () => void {
public static debounce(func: () => void, wait?: number , immediate?: boolean): () => void {
let timeout;
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",
"linterOptions": {
"exclude": [
"node_modules"
]
},
"rules": {
"indent": [true, "spaces", 2],
"interface-name": false,
@ -12,6 +17,11 @@
"import-sources-order": "any",
"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
* @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 ITool from './tool';
import {PasteConfig, SanitizerConfig} from '../configs';
import {BlockToolData} from './block-tool-data';
import {Tool, ToolConstructable} from './tool';
import {ToolConfig} from './tool-config';
import {API} from '../index';
/**
* Describe Block Tool object
* @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
*/
@ -28,40 +74,9 @@ export default interface IBlockTool extends ITool {
toolboxIcon?: string;
/**
* Sanitizer rules description
* Paste substitutions configuration
*/
sanitizer?: object;
onPaste?: PasteConfig;
/**
* 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;
new (config: {api: API, config: ToolConfig, data: BlockToolData}): BlockTool;
}

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