Sanitizer features (#467)

* Sanitizer features

* move to ts, use sanitizer module to clean taintstring

* Sanitizer is ready

* it works

* Code refactored (#476)

* Use new features for paste handling

* Fix asterix

* Add types and some comments

* Use any type for deepSanitize method

* Make sanitize property static and use apiSettings object

* Use sanitize for single-block pasting

* Fix comment

* little updates

* rename sanitize in inline-tools docs

* Update pattern handling

* Use public getter for available tools

* Fix typo
This commit is contained in:
Murod Khaydarov 2018-10-23 10:34:00 +03:00 committed by GitHub
parent 5188933ee3
commit 3f8c7fbb7b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 902 additions and 858 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -39,6 +39,9 @@ Methods that working with Blocks
```clean(taintString, config)``` - method uses HTMLJanitor to clean taint string.
CodeX Editor provides basic config without attributes, but you can inherit by passing your own config.
If Tool enables inline-tools, we get it's sanitizing rules and merge with your passed custom
rules.
Usage:
```js

View file

@ -2,27 +2,6 @@
The `Sanitizer` module represents a set of methods that clears taint strings.
Uses lightweight npm package with simple API [html-janitor](https://www.npmjs.com/package/html-janitor)
Sanitizer class implements basic Module class that holds User configuration
and default CodeX Editor instances
## Properties
Default Editor Sanitizer configuration according to the html-janitor API
```javascript
defaultConfig
```
Custom User configuration which passed on Editor initialization. Data type must be according to the html-janitor API
```javascript
sanitizerConfig
```
Property that holds an instance used in Module
```javascript
sanitizerInstance
```
## Methods

View file

@ -17,6 +17,7 @@ Also, you can provide optional methods
- `renderActions()` — create additional element below the buttons
- `clear()` — clear Tool's stuff on opening/closing of Inline Toolbar
- `sanitize()` — sanitizer configuration
At the constructor of Tool's class exemplar you will accept an object with the [API](api.md) as a parameter.
@ -102,3 +103,23 @@ Method does not accept any parameters
#### Return value
Method should not return a value.
### static get sanitize()
We recommend to specify the Sanitizer config that corresponds with inline tags that is used by your Tool.
In that case, your config will be merged with sanitizer configuration of Block Tool
that is using the Inline Toolbar with your Tool.
Example:
If your Tool wrapps selected text with `<b>` tag, the sanitizer config should looks like this:
```js
static get sanitize() {
return {
b: {} // {} means clean all attributes. true — leave all attributes
}
}
```
Read more about Sanitizer configuration at the [Tools#sanitize](tools.md#sanitize)

View file

@ -209,7 +209,6 @@ static get onPaste() {
CodeX Editor provides [API](sanitizer.md) to clean taint strings.
Use it manually at the `save()` method or or pass `sanitizer` config to do it automatically.
#### Sanitizer Configuration
The example of sanitizer configuration
@ -279,12 +278,15 @@ save() {
#### Automatic sanitize
If you pass the sanitizer config, CodeX Editor will automatically sanitize your saved data.
If you pass the sanitizer config as static getter, CodeX Editor will automatically sanitize your saved data.
Note that if your Tool is allowed to use the Inline Toolbar, we will get sanitizing rules for each Inline Tool
and merge with your passed config.
You can define rules for each field
```javascript
get sanitize() {
static get sanitize() {
return {
text: {},
items: {
@ -302,7 +304,7 @@ if you want to sanitize everything and get data without any tags, use `{}` or ju
ignore field in case if you want to get pure HTML
```javascript
get sanitize() {
static get sanitize() {
return {
text: {},
items: {}, // this rules will be used for all properties of this object

View file

@ -1,6 +1,6 @@
{
"name": "codex.editor",
"version": "2.1.3",
"version": "2.2.0",
"description": "Codex Editor. Native JS, based on API and Open Source",
"main": "build/codex-editor.js",
"scripts": {

View file

@ -149,6 +149,14 @@ export default class Block {
return this.save();
}
/**
* Returns tool's sanitizer config
* @return {object}
*/
get sanitize(): object {
return this.tool.sanitize;
}
/**
* is block mergeable
* We plugin have merge function then we call it mergable
@ -287,15 +295,7 @@ export default class Block {
* @return {Object}
*/
public async save(): Promise<void|{tool: string, data: any, time: number}> {
let extractedBlock = await this.tool.save(this.pluginsContent);
/**
* if Tool provides custom sanitizer config
* then use this config
*/
if (this.tool.sanitize && typeof this.tool.sanitize === 'object') {
extractedBlock = this.sanitizeBlock(extractedBlock, this.tool.sanitize);
}
const extractedBlock = await this.tool.save(this.pluginsContent);
/**
* Measuring execution time
@ -373,73 +373,6 @@ export default class Block {
return tunesElement;
}
/**
* Method recursively reduces Block's data and cleans with passed rules
*
* @param {Object|string} blockData - taint string or object/array that contains taint string
* @param {Object} rules - object with sanitizer rules
*/
private sanitizeBlock(blockData, rules) {
/**
* Case 1: Block data is Array
* Array's in JS can not be enumerated with for..in because result will be Object not Array
* which conflicts with Consistency
*/
if (Array.isArray(blockData)) {
/**
* Create new "cleanData" array and fill in with sanitizer data
*/
return blockData.map((item) => {
return this.sanitizeBlock(item, rules);
});
} else if (typeof blockData === 'object') {
/**
* Create new "cleanData" object and fill with sanitized objects
*/
const cleanData = {};
/**
* Object's may have 3 cases:
* 1. When Data is Array. Then call again itself and recursively clean arrays items
* 2. When Data is Object that can have object's inside. Do the same, call itself and clean recursively
* 3. When Data is base type (string, int, bool, ...). Check if rule is passed
*/
for (const data in blockData) {
if (Array.isArray(blockData[data]) || typeof blockData[data] === 'object') {
/**
* Case 1 & Case 2
*/
if (rules[data]) {
cleanData[data] = this.sanitizeBlock(blockData[data], rules[data]);
} else if (_.isEmpty(rules)) {
cleanData[data] = this.sanitizeBlock(blockData[data], rules);
} else {
cleanData[data] = blockData[data];
}
} else {
/**
* Case 3.
*/
if (rules[data]) {
cleanData[data] = this.api.sanitizer.clean(blockData[data], rules[data]);
} else {
cleanData[data] = this.api.sanitizer.clean(blockData[data], rules);
}
}
}
return cleanData;
} else {
/**
* In case embedded objects use parent rules
*/
return this.api.sanitizer.clean(blockData, rules);
}
}
/**
* Toggle drop target state
* @param {boolean} state

View file

@ -1,5 +1,5 @@
import InlineTool from '../interfaces/tools/inline-tool';
import SelectionUtils from '../selection';
import ISanitizerConfig from '../interfaces/sanitizer-config';
declare var $: any;
@ -12,6 +12,24 @@ declare var $: any;
*/
export default class BoldInlineTool implements InlineTool {
/**
* Specifies Tool as Inline Toolbar Tool
*
* @return {boolean}
*/
public static isInline = true;
/**
* Sanitizer Rule
* Leave <b> tags
* @return {object}
*/
static get sanitize(): ISanitizerConfig {
return {
b: {},
};
}
/**
* Native Document's command that uses for Bold
*/
@ -29,7 +47,7 @@ export default class BoldInlineTool implements InlineTool {
/**
* Elements
*/
private nodes = {
private nodes: {button: HTMLButtonElement} = {
button: undefined,
};

View file

@ -1,4 +1,5 @@
import InlineTool from '../interfaces/tools/inline-tool';
import ISanitizerConfig from '../interfaces/sanitizer-config';
declare var $: any;
@ -11,6 +12,24 @@ declare var $: any;
*/
export default class ItalicInlineTool implements InlineTool {
/**
* Specifies Tool as Inline Toolbar Tool
*
* @return {boolean}
*/
public static isInline = true;
/**
* Sanitizer Rule
* Leave <i> tags
* @return {object}
*/
static get sanitize(): ISanitizerConfig {
return {
i: {},
};
}
/**
* Native Document's command that uses for Italic
*/
@ -28,7 +47,7 @@ export default class ItalicInlineTool implements InlineTool {
/**
* Elements
*/
private nodes = {
private nodes: {button: HTMLButtonElement} = {
button: null,
};
@ -74,5 +93,4 @@ export default class ItalicInlineTool implements InlineTool {
public get shortcut(): string {
return 'CMD+I';
}
}

View file

@ -1,5 +1,6 @@
import InlineTool from '../interfaces/tools/inline-tool';
import SelectionUtils from '../selection';
import ISanitizerConfig from '../interfaces/sanitizer-config';
declare var $: any;
declare var _: any;
@ -12,6 +13,29 @@ declare var _: any;
* Wrap selected text with <a> tag
*/
export default class LinkInlineTool implements InlineTool {
/**
* Specifies Tool as Inline Toolbar Tool
*
* @return {boolean}
*/
public static isInline = true;
/**
* Sanitizer Rule
* Leave <a> tags
* @return {object}
*/
static get sanitize(): ISanitizerConfig {
return {
a: {
href: true,
target: '_blank',
rel: 'nofollow',
},
};
}
/**
* Native Document's commands for link/unlink
*/
@ -38,7 +62,7 @@ export default class LinkInlineTool implements InlineTool {
/**
* Elements
*/
private nodes = {
private nodes: {button: HTMLButtonElement, input: HTMLInputElement} = {
button: null,
input: null,
};

View file

@ -32,5 +32,5 @@ export default interface ISanitizerConfig {
* }
* }
*/
[key: string]: boolean|object|(() => any);
[key: string]: boolean|{[attr: string]: boolean|string}|(() => any);
}

View file

@ -2,6 +2,7 @@
* Base structure for the Inline Toolbar Tool
*/
import ITool from './tool';
import ISanitizerConfig from '../sanitizer-config';
export default interface IInlineTool extends ITool {
/**
@ -9,6 +10,11 @@ export default interface IInlineTool extends ITool {
*/
shortcut?: string;
/**
* Inline tool sanitize configuration
*/
sanitize?: ISanitizerConfig;
/**
* Returns button for the Inline Toolbar
*/
@ -37,5 +43,4 @@ export default interface IInlineTool extends ITool {
* Function called with Inline Toolbar closing
*/
clear?(): void;
}

View file

@ -152,7 +152,8 @@ export default class Paste extends Module {
return result;
}, {});
const customConfig = Object.assign({}, toolsTags, Sanitizer.defaultConfig.tags);
const customConfig = Object.assign({}, toolsTags, Sanitizer.getAllInlineToolsConfig());
const cleanData = Sanitizer.clean(htmlData, customConfig);
/** If there is no HTML or HTML string is equal to plain one, process it as plain text */
@ -522,7 +523,7 @@ export default class Paste extends Module {
return result;
}, {});
const customConfig = Object.assign({}, toolTags, Sanitizer.defaultConfig.tags);
const customConfig = Object.assign({}, toolTags, Sanitizer.getInlineToolsConfig(tool));
content.innerHTML = Sanitizer.clean(content.innerHTML, customConfig);
@ -570,7 +571,7 @@ export default class Paste extends Module {
*/
private async processSingleBlock(dataToInsert: IPasteData): Promise<void> {
const initialTool = this.config.initialBlock,
{BlockManager, Caret} = this.Editor,
{BlockManager, Caret, Sanitizer} = this.Editor,
{content, tool} = dataToInsert;
if (tool === initialTool && content.textContent.length < Paste.PATTERN_PROCESSING_MAX_LENGTH) {
@ -580,6 +581,10 @@ export default class Paste extends Module {
this.splitBlock();
let insertedBlock;
const sanitizeConfig = Sanitizer.composeToolConfig(tool);
blockData.data = Sanitizer.deepSanitize(blockData.data, sanitizeConfig);
if (BlockManager.currentBlock && BlockManager.currentBlock.isEmpty) {
insertedBlock = BlockManager.replace(blockData.tool, blockData.data);
} else {
@ -590,8 +595,10 @@ export default class Paste extends Module {
}
}
const currentToolSanitizeConfig = Sanitizer.getInlineToolsConfig(BlockManager.currentBlock.name);
/** If there is no pattern substitute - insert string as it is */
document.execCommand('insertHTML', false, content.innerHTML);
document.execCommand('insertHTML', false, Sanitizer.clean(content.innerHTML, currentToolSanitizeConfig));
}
/**

View file

@ -1,150 +0,0 @@
/**
* CodeX Sanitizer
*
* @module Sanitizer
* Clears HTML from taint tags
*
* @version 2.0.0
*
* @example
* Module can be used within two ways:
* 1) When you have an instance
* - this.Editor.Sanitizer.clean(yourTaintString);
* 2) As static method
* - CodexEditor.Sanitizer.clean(yourTaintString, yourCustomConfiguration);
*
* {@link SanitizerConfig}
*/
/**
* @typedef {Object} SanitizerConfig
* @property {Object} tags - define tags restrictions
*
* @example
*
* tags : {
* p: true,
* a: {
* href: true,
* rel: "nofollow",
* target: "_blank"
* }
* }
*/
export default class Sanitizer extends Module {
/**
* Initializes Sanitizer module
* Sets default configuration if custom not exists
*
* @property {SanitizerConfig} this.defaultConfig
* @property {HTMLJanitor} this._sanitizerInstance - Sanitizer library
*
* @param {SanitizerConfig} config
*/
constructor({config}) {
super({config});
this._sanitizerInstance = null;
/** Custom configuration */
this.sanitizerConfig = config.settings ? config.settings.sanitizer : null;
/** HTML Janitor library */
this.sanitizerInstance = require('html-janitor');
}
/**
* If developer uses editor's API, then he can customize sanitize restrictions.
* Or, sanitizing config can be defined globally in editors initialization. That config will be used everywhere
* At least, if there is no config overrides, that API uses Default configuration
*
* @uses https://www.npmjs.com/package/html-janitor
*
* @param {HTMLJanitor} library - sanitizer extension
*/
set sanitizerInstance(library) {
if (this.sanitizerConfig) {
this._sanitizerInstance = new library(this.sanitizerConfig);
}
return this._sanitizerInstance;
}
/**
* Sets sanitizer configuration. Uses default config if user didn't pass the restriction
*/
get defaultConfig() {
return {
tags: {
p: {},
a: {
href: true,
target: '_blank',
rel: 'nofollow'
},
b: {},
i: {}
}
};
}
/**
* Return sanitizer instance
* @return {null|library}
*/
get sanitizerInstance() {
return this._sanitizerInstance;
}
/**
* Cleans string from unwanted tags
* @param {String} taintString - HTML string
* @param {Object} customConfig - custom sanitizer configuration. Method uses default if param is empty
* @return {String} clean HTML
*/
clean(taintString, customConfig) {
if (customConfig && typeof customConfig === 'object') {
/**
* API client can use custom config to manage sanitize process
*/
let newConfig = {
tags: customConfig
};
return Sanitizer.clean(taintString, newConfig);
} else {
/**
* Ignore sanitizing when nothing passed in config
*/
if (!this.sanitizerInstance) {
return taintString;
} else {
return this.sanitizerInstance.clean(taintString);
}
}
}
/**
* Cleans string from unwanted tags
* @static
*
* Method allows to use default config
*
* @param {String} taintString - taint string
* @param {SanitizerConfig} customConfig - allowed tags
*
* @return {String} clean HTML
*/
static clean(taintString, customConfig) {
let newInstance = new Sanitizer({
config: {
settings: {
sanitizer: customConfig
}
}
});
return newInstance.clean(taintString);
}
}

View file

@ -0,0 +1,313 @@
/**
* CodeX Sanitizer
*
* @module Sanitizer
* Clears HTML from taint tags
*
* @version 2.0.0
*
* @example
* Module can be used within two ways:
* 1) When you have an instance
* - this.Editor.Sanitizer.clean(yourTaintString);
* 2) As static method
* - CodexEditor.Sanitizer.clean(yourTaintString, yourCustomConfiguration);
*
* {@link SanitizerConfig}
*/
import ISanitizerConfig from '../interfaces/sanitizer-config';
/**
* @typedef {Object} SanitizerConfig
* @property {Object} tags - define tags restrictions
*
* @example
*
* tags : {
* p: true,
* a: {
* href: true,
* rel: "nofollow",
* target: "_blank"
* }
* }
*/
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';
export default class Sanitizer extends Module {
/**
* Memoize tools config
*/
private configCache: {[toolName: string]: ISanitizerConfig} = {};
/**
* 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});
}
/**
* Sanitize Blocks
*
* Enumerate blocks and clean data
*
* @param {{tool, data: IBlockToolData}[]} blocksData[]
*/
public sanitizeBlocks(
blocksData: Array<{tool: string, data: IBlockToolData}>,
): Array<{tool: string, data: IBlockToolData}> {
return blocksData.map((block) => {
const toolConfig = this.composeToolConfig(block.tool);
block.data = this.deepSanitize(block.data, toolConfig);
return block;
});
}
/**
* 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
*/
public deepSanitize(dataToSanitize: any, rules: ISanitizerConfig): any {
/**
* BlockData It may contain 3 types:
* - Array
* - Object
* - Primitive
*/
if (Array.isArray(dataToSanitize)) {
/**
* Array: call sanitize for each item
*/
return this.cleanArray(dataToSanitize, rules);
} else if (typeof dataToSanitize === 'object') {
/**
* Objects: just clean object deeper.
*/
return this.cleanObject(dataToSanitize, rules);
} else {
/**
* Primitives (number|string|boolean): clean this item
*/
return this.cleanOneItem(dataToSanitize, rules);
}
}
/**
* Cleans string from unwanted tags
* Method allows to use default config
*
* @param {String} taintString - taint string
* @param {SanitizerConfig} customConfig - allowed tags
*
* @return {String} clean HTML
*/
public clean(taintString: string, customConfig: ISanitizerConfig = {}): string {
const sanitizerConfig = {
tags: customConfig,
};
/**
* API client can use custom config to manage sanitize process
*/
const sanitizerInstance = this.createHTMLJanitorInstance(sanitizerConfig);
return sanitizerInstance.clean(taintString);
}
/**
* Merge with inline tool config
*
* @param {string} toolName
* @param {ISanitizerConfig} toolRules
* @return {ISanitizerConfig}
*/
public composeToolConfig(toolName: string): ISanitizerConfig {
/**
* If cache is empty, then compose tool config and put it to the cache object
*/
if (this.configCache[toolName]) {
return this.configCache[toolName];
}
const sanitizeGetter = this.Editor.Tools.apiSettings.SANITIZE_CONFIG;
const toolClass = this.Editor.Tools.available[toolName];
/**
* If Tools doesn't provide sanitizer config or it is empty
*/
if (!toolClass.sanitize || (toolClass[sanitizeGetter] && _.isEmpty(toolClass[sanitizeGetter]))) {
return {};
}
const toolRules = toolClass.sanitize;
const baseConfig = this.getInlineToolsConfig(toolName);
const toolConfig = {};
for (const fieldName in toolRules) {
if (toolRules.hasOwnProperty(fieldName)) {
const rule = toolRules[fieldName];
if (typeof rule === 'object') {
toolConfig[fieldName] = Object.assign({}, baseConfig, rule);
} else {
toolConfig[fieldName] = rule;
}
}
}
this.configCache[toolName] = toolConfig;
return toolConfig;
}
/**
* Returns Sanitizer config
* When Tool's "inlineToolbar" value is True, get all sanitizer rules from all tools,
* otherwise get only enabled
*/
public getInlineToolsConfig(name: string): ISanitizerConfig {
const {Tools} = this.Editor;
const toolsConfig = Tools.getToolSettings(name),
enableInlineTools = toolsConfig.inlineToolbar || [];
let config = {};
if (typeof enableInlineTools === 'boolean' && enableInlineTools) {
/**
* getting all tools sanitizer rule
*/
config = this.getAllInlineToolsConfig();
} else {
/**
* getting only enabled
*/
enableInlineTools.map( (inlineToolName) => {
config = Object.assign(config, Tools.inline[inlineToolName][Tools.apiSettings.SANITIZE_CONFIG]);
});
}
return config;
}
/**
* Return general config for all inline tools
*/
public getAllInlineToolsConfig(): ISanitizerConfig {
const {Tools} = this.Editor;
if (this.inlineToolsConfigCache) {
return this.inlineToolsConfigCache;
}
const config: ISanitizerConfig = {};
Object.entries(Tools.inline)
.forEach( ([name, inlineTool]: [string, IInlineTool]) => {
Object.assign(config, inlineTool[Tools.apiSettings.SANITIZE_CONFIG]);
});
this.inlineToolsConfigCache = config;
return this.inlineToolsConfigCache;
}
/**
* Clean array
* @param {array} array - [1, 2, {}, []]
* @param {object} ruleForItem
*/
private cleanArray(array: any[], ruleForItem: ISanitizerConfig): any[] {
return array.map( (arrayItem) => this.deepSanitize(arrayItem, ruleForItem));
}
/**
* Clean object
* @param {object} object - {level: 0, text: 'adada', items: [1,2,3]}}
* @param {object} rules - { b: true } or true|false
* @return {object}
*/
private cleanObject(object: any, rules: ISanitizerConfig|{[field: string]: ISanitizerConfig}): any {
const cleanData = {};
for (const fieldName in object) {
if (!object.hasOwnProperty(fieldName)) {
continue;
}
const currentIterationItem = object[fieldName];
/**
* Get object from config by field name
* - 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;
cleanData[fieldName] = this.deepSanitize(currentIterationItem, ruleForItem as ISanitizerConfig);
}
return cleanData;
}
/**
* @param {string} taintString
* @param {ISanitizerConfig|boolean} rule
* @return {string}
*/
private cleanOneItem(taintString: string, rule: ISanitizerConfig|boolean): string {
if (typeof rule === 'object') {
return this.clean(taintString, rule);
} else if (rule === false) {
return this.clean(taintString, {});
} else {
return taintString;
}
}
/**
* Check if passed item is a HTML Janitor rule:
* { a : true }, {}, false, true, function(){} correct rules
* undefined, null, 0, 1, 2 not a rules
* @param config
*/
private isRule(config: ISanitizerConfig): boolean {
return typeof config === 'object' || typeof config === 'boolean' || typeof config === 'function';
}
/**
* If developer uses editor's API, then he can customize sanitize restrictions.
* Or, sanitizing config can be defined globally in editors initialization. That config will be used everywhere
* At least, if there is no config overrides, that API uses Default configuration
*
* @uses https://www.npmjs.com/package/html-janitor
*
* @param {SanitizerConfig} config - sanitizer extension
*/
private createHTMLJanitorInstance(config: {tags: ISanitizerConfig}): any {
if (config) {
return new HTMLJanitor(config);
}
return null;
}
}

View file

@ -1,243 +0,0 @@
/**
* Codex Editor Saver
*
* @module Saver
* @author Codex Team
* @version 2.0.0
*/
/**
* @typedef {Object} SavedData
* @property {Date} time - saving proccess time
* @property {Object} items - extracted data
* @property {String} version - CodexEditor version
*/
/**
* @classdesc This method reduces all Blocks asyncronically and calls Block's save method to extract data
*
* @typedef {Saver} Saver
* @property {Element} html - Editor HTML content
* @property {String} json - Editor JSON output
*/
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}
*/
save() {
let blocks = this.Editor.BlockManager.blocks,
chainData = [];
blocks.forEach((block) => {
chainData.push(block.data);
});
return Promise.all(chainData)
.then((allExtractedData) => this.makeOutput(allExtractedData))
.then((outputData) => {
return outputData;
});
}
/**
* Creates output object with saved data, time and version of editor
* @param {Object} allExtractedData
* @return {SavedData}
*/
makeOutput(allExtractedData) {
let blocks = [],
totalTime = 0;
console.groupCollapsed('[CodexEditor saving]:');
allExtractedData.forEach((extraction) => {
/** Group process info */
console.log(`«${extraction.tool}» saving info`, extraction);
totalTime += extraction.time;
blocks.push({
type: extraction.tool,
data: extraction.data
});
});
console.log('Total', totalTime);
console.groupEnd();
return {
time: +new Date(),
blocks: blocks,
version: VERSION,
};
}
}
// module.exports = (function (saver) {
//
// let editor = codex.editor;
//
// /**
// * @public
// * Save blocks
// */
// saver.save = function () {
//
// /** Save html content of redactor to memory */
// editor.state.html = editor.nodes.redactor.innerHTML;
//
// /** Clean jsonOutput state */
// editor.state.jsonOutput = [];
//
// return saveBlocks(editor.nodes.redactor.childNodes);
//
// };
//
// /**
// * @private
// * Save each block data
// *
// * @param blocks
// * @returns {Promise.<TResult>}
// */
// let saveBlocks = function (blocks) {
//
// let data = [];
//
// for(let index = 0; index < blocks.length; index++) {
//
// data.push(getBlockData(blocks[index]));
//
// }
//
// return Promise.all(data)
// .then(makeOutput)
// .catch(editor.core.log);
//
// };
//
// /** Save and validate block data */
// let getBlockData = function (block) {
//
// return saveBlockData(block)
// .then(validateBlockData)
// .catch(editor.core.log);
//
// };
//
// /**
// * @private
// * Call block`s plugin save method and return saved data
// *
// * @param block
// * @returns {Object}
// */
// let saveBlockData = function (block) {
//
// let pluginName = block.dataset.tool;
//
// /** Check for plugin existence */
// if (!editor.tools[pluginName]) {
//
// editor.core.log(`Plugin «${pluginName}» not found`, 'error');
// return {data: null, pluginName: null};
//
// }
//
// /** Check for plugin having save method */
// if (typeof editor.tools[pluginName].save !== 'function') {
//
// editor.core.log(`Plugin «${pluginName}» must have save method`, 'error');
// return {data: null, pluginName: null};
//
// }
//
// /** Result saver */
// let blockContent = block.childNodes[0],
// pluginsContent = blockContent.childNodes[0],
// position = pluginsContent.dataset.inputPosition;
//
// /** If plugin wasn't available then return data from cache */
// if ( editor.tools[pluginName].available === false ) {
//
// return Promise.resolve({data: codex.editor.state.blocks.items[position].data, pluginName});
//
// }
//
// return Promise.resolve(pluginsContent)
// .then(editor.tools[pluginName].save)
// .then(data => Object({data, pluginName}));
//
// };
//
// /**
// * Call plugin`s validate method. Return false if validation failed
// *
// * @param data
// * @param pluginName
// * @returns {Object|Boolean}
// */
// let validateBlockData = function ({data, pluginName}) {
//
// if (!data || !pluginName) {
//
// return false;
//
// }
//
// if (editor.tools[pluginName].validate) {
//
// let result = editor.tools[pluginName].validate(data);
//
// /**
// * Do not allow invalid data
// */
// if (!result) {
//
// return false;
//
// }
//
// }
//
// return {data, pluginName};
//
//
// };
//
// /**
// * Compile article output
// *
// * @param savedData
// * @returns {{time: number, version, items: (*|Array)}}
// */
// let makeOutput = function (savedData) {
//
// savedData = savedData.filter(blockData => blockData);
//
// let items = savedData.map(blockData => Object({type: blockData.pluginName, data: blockData.data}));
//
// editor.state.jsonOutput = items;
//
// return {
// id: editor.state.blocks.id || null,
// time: +new Date(),
// version: editor.version,
// items
// };
//
// };
//
// return saver;
//
// })({});

View file

@ -0,0 +1,94 @@
/**
* Codex Editor Saver
*
* @module Saver
* @author Codex Team
* @version 2.0.0
*/
/**
* @typedef {Object} SavedData
* @property {Date} time - saving proccess time
* @property {Object} blocks - extracted data
* @property {String} version - CodexEditor version
*/
interface SavedData {
time: number;
blocks: object[];
version: string;
}
/**
* @classdesc This method reduces all Blocks asyncronically and calls Block's save method to extract data
*
* @typedef {Saver} Saver
* @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}
*/
public save(): Promise<SavedData> {
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;
});
}
/**
* Creates output object with saved data, time and version of editor
* @param {Object} allExtractedData
* @return {SavedData}
*/
private makeOutput(allExtractedData): SavedData {
let totalTime = 0;
const blocks = [];
console.groupCollapsed('[CodexEditor saving]:');
allExtractedData.forEach((extraction) => {
/** Group process info */
console.log(`«${extraction.tool}» saving info`, extraction);
totalTime += extraction.time;
blocks.push({
type: extraction.tool,
data: extraction.data,
});
});
console.log('Total', totalTime);
console.groupEnd();
return {
time: +new Date(),
version: VERSION,
blocks,
};
}
}

View file

@ -1,9 +1,8 @@
import ITool from '../interfaces/tools/tool';
declare var Module: any;
declare var $: any;
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 EditorConfig from '../interfaces/editor-config';
import InlineTool from '../interfaces/tools/inline-tool';
import SelectionUtils from '../selection';
@ -63,13 +62,12 @@ export default class InlineToolbar extends Module {
/**
* Inline Toolbar Tools
* includes internal and external tools
*
* @returns Map<string, InlineTool>
*/
get tools(): Map<string, InlineTool> {
if (!this.toolsInstances || this.toolsInstances.size === 0) {
const allTools = {...this.internalTools, ...this.externalTools};
const allTools = this.inlineTools;
this.toolsInstances = new Map();
for (const tool in allTools) {
@ -289,6 +287,11 @@ export default class InlineToolbar extends Module {
* Add tool button and activate clicks
*/
private addTool(toolName: string, tool: InlineTool): void {
const {
Listeners,
Tools,
} = this.Editor;
const button = tool.render();
if (!button) {
@ -304,7 +307,7 @@ export default class InlineToolbar extends Module {
this.nodes.actions.appendChild(actions);
}
this.Editor.Listeners.on(button, 'click', (event) => {
Listeners.on(button, 'click', (event) => {
this.toolClicked(tool);
event.preventDefault();
});
@ -313,18 +316,26 @@ export default class InlineToolbar extends Module {
* Enable shortcuts
* Ignore tool that doesn't have shortcut or empty string
*/
const toolSettings = this.Editor.Tools.getToolSettings(toolName);
const toolSettings = Tools.getToolSettings(toolName);
let shortcut = null;
/**
* Get internal inline tools
*/
const internalTools: string[] = Object
.entries(Tools.internalTools)
.filter(([name, toolClass]: [string, ITool]) => toolClass[Tools.apiSettings.IS_INLINE])
.map(([name, toolClass]: [string, ITool]) => name);
/**
* 1) For internal tools, check public getter 'shortcut'
* 2) For external tools, check tool's settings
*/
if (this.internalTools[toolName]) {
shortcut = this.internalTools[toolName].shortcut;
} else if (toolSettings && toolSettings[this.Editor.Tools.apiSettings.SHORTCUT]) {
shortcut = toolSettings[this.Editor.Tools.apiSettings.SHORTCUT];
if (internalTools.includes(toolName)) {
shortcut = this.inlineTools[toolName].shortcut;
} else if (toolSettings && toolSettings[Tools.apiSettings.SHORTCUT]) {
shortcut = toolSettings[Tools.apiSettings.SHORTCUT];
}
if (shortcut) {
@ -390,22 +401,10 @@ 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
* Get inline tools tools
* Tools that has isInline is true
*/
private get externalTools(): {[name: string]: InlineTool} {
private get inlineTools(): {[name: string]: InlineTool} {
const result = {};
for (const tool in this.Editor.Tools.inline) {

View file

@ -1,3 +1,7 @@
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');
/**
@ -186,6 +190,7 @@ export default class Tools extends Module {
IS_PASTE_DISALLOWED: 'disallowPaste',
SHORTCUT: 'shortcut',
TOOLBAR_ICON: 'toolboxIcon',
SANITIZE_CONFIG: 'sanitize'
};
}
@ -194,10 +199,10 @@ export default class Tools extends Module {
* @return {Promise}
*/
prepare() {
this.config.tools.paragraph = {
class: Paragraph,
inlineToolbar: true
};
/**
* 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');
@ -369,4 +374,20 @@ export default class Tools extends Module {
getToolSettings(toolName) {
return this.toolsSettings[toolName];
}
/**
* Returns internal tools
* Includes Bold, Italic, Link and Paragraph
*/
get internalTools() {
return {
bold: BoldInlineTool,
italic: ItalicInlineTool,
link: LinkInlineTool,
paragraph: {
class: Paragraph,
inlineToolbar: true
}
};
}
}