mirror of
https://github.com/codex-team/editor.js
synced 2024-06-13 03:12:21 +02:00
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:
parent
531f1d253b
commit
bcdfcdadbc
5
.babelrc
5
.babelrc
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"presets": [
|
||||
"presets": [
|
||||
["@babel/preset-env", {
|
||||
"modules": "umd"
|
||||
"modules": "umd",
|
||||
"useBuiltIns": "entry"
|
||||
}]
|
||||
],
|
||||
"plugins": [
|
||||
|
|
|
@ -85,6 +85,8 @@
|
|||
"$": true,
|
||||
"_": true,
|
||||
"setTimeout": true,
|
||||
"process": true,
|
||||
"__dirname": true,
|
||||
"Map": true
|
||||
}
|
||||
}
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -8,3 +8,4 @@ Thumbs.db
|
|||
node_modules/*
|
||||
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
|
14
README.md
14
README.md
|
@ -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).
|
||||
|
@ -64,7 +64,7 @@ or apply Tool's settings, if it provided. For example, set a Heading level or Li
|
|||
|
||||
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
|
||||
|
@ -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>
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -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
|
||||
|
|
|
@ -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
12536
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
223
src/components/blocks.ts
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
import IEditorConfig from './editor-config';
|
||||
|
||||
/**
|
||||
* Internal Module's construction parameters
|
||||
*/
|
||||
export default interface IModuleConfig {
|
||||
|
||||
/**
|
||||
* Editor's config
|
||||
*/
|
||||
config: IEditorConfig;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
/**
|
||||
* Object returned by Tool's {@link IBlockTool#save} method
|
||||
*/
|
||||
export default interface IBlockToolData {}
|
|
@ -1,10 +0,0 @@
|
|||
/**
|
||||
* Abstract interface of all Tools
|
||||
*/
|
||||
export default interface ITool {
|
||||
|
||||
/**
|
||||
* Define Tool type as Inline
|
||||
*/
|
||||
isInline?: boolean;
|
||||
}
|
|
@ -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;
|
||||
})({});
|
|
@ -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 {};
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
17
src/components/modules/api/caret.ts
Normal file
17
src/components/modules/api/caret.ts
Normal 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 {};
|
||||
}
|
||||
}
|
|
@ -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),
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
};
|
||||
}
|
|
@ -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),
|
||||
};
|
23
src/components/modules/api/sanitizer.ts
Normal file
23
src/components/modules/api/sanitizer.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
26
src/components/modules/api/saver.ts
Normal file
26
src/components/modules/api/saver.ts
Normal 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();
|
||||
}
|
||||
}
|
39
src/components/modules/api/selection.ts
Normal file
39
src/components/modules/api/selection.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
34
src/components/modules/api/toolbar.ts
Normal file
34
src/components/modules/api/toolbar.ts
Normal 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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
}
|
437
src/components/modules/blockManager.ts
Normal file
437
src/components/modules/blockManager.ts
Normal 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));
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 = [];
|
||||
}
|
||||
}
|
192
src/components/modules/listeners.ts
Normal file
192
src/components/modules/listeners.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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 {
|
|
@ -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());
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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 = '';
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ interface Element {
|
|||
oMatchesSelector: (selector: string) => boolean;
|
||||
|
||||
prepend: (nodes: Node|Node[]|any) => void;
|
||||
append: (nodes: Node|Node[]|DocumentFragment|void) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
65
src/types-internal/editor-modules.d.ts
vendored
Normal 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
22
src/types-internal/html-janitor.d.ts
vendored
Normal 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
8
src/types-internal/module-config.d.ts
vendored
Normal 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
7
src/types-internal/svg.d.ts
vendored
Normal 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;
|
||||
}
|
12
tslint.json
12
tslint.json
|
@ -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
61
types/api/blocks.d.ts
vendored
Normal 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
6
types/api/caret.d.ts
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
/**
|
||||
* Describes Editor`s caret API
|
||||
*
|
||||
* TODO
|
||||
*/
|
||||
export interface Caret {}
|
28
types/api/events.d.ts
vendored
Normal file
28
types/api/events.d.ts
vendored
Normal 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
10
types/api/index.d.ts
vendored
Normal 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
24
types/api/listeners.d.ts
vendored
Normal 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
14
types/api/notifier.d.ts
vendored
Normal 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
14
types/api/sanitizer.d.ts
vendored
Normal 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
13
types/api/saver.d.ts
vendored
Normal 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
18
types/api/selection.d.ts
vendored
Normal 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
44
types/api/styles.d.ts
vendored
Normal 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
14
types/api/toolbar.d.ts
vendored
Normal 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
20
types/block-tunes/block-tune.d.ts
vendored
Normal 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
1
types/block-tunes/index.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './block-tune';
|
53
types/configs/editor-config.d.ts
vendored
Normal file
53
types/configs/editor-config.d.ts
vendored
Normal 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
3
types/configs/index.d.ts
vendored
Normal 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
50
types/configs/paste-config.d.ts
vendored
Normal 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;
|
||||
}
|
|
@ -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
21
types/data-formats/output-data.d.ts
vendored
Normal 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
69
types/index.d.ts
vendored
Normal 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
5
types/tools/block-tool-data.d.ts
vendored
Normal 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;
|
|
@ -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
6
types/tools/index.d.ts
vendored
Normal 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
Loading…
Reference in a new issue