mirror of
https://github.com/codex-team/editor.js
synced 2024-06-10 18:03:25 +02:00
TypeScript support, Webpack 4, Inline Toolbar beginning (#257)
* Create UI * Support TypeScript Modules * remove tmp files * migrate to 2-spaced tabs * Add TS Linter
This commit is contained in:
parent
d8747e5677
commit
dbb4cd6f8f
9
.editorconfig
Normal file
9
.editorconfig
Normal file
|
@ -0,0 +1,9 @@
|
|||
root = false
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
|
@ -42,7 +42,7 @@
|
|||
"no-nested-ternary": 1,
|
||||
"no-trailing-spaces": 2,
|
||||
"no-mixed-spaces-and-tabs": 2,
|
||||
"padded-blocks": [2, "always"],
|
||||
"padded-blocks": [2, "never"],
|
||||
"space-before-blocks": 1,
|
||||
"space-before-function-paren": [1, {
|
||||
"anonymous": "always",
|
||||
|
@ -53,7 +53,7 @@
|
|||
"markers": ["=", "!"]
|
||||
}],
|
||||
"semi": [2, "always"],
|
||||
"indent": [2, 4, {
|
||||
"indent": [2, 2, {
|
||||
"SwitchCase": 1
|
||||
}],
|
||||
"camelcase": [2, {
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
6291
package-lock.json
generated
6291
package-lock.json
generated
File diff suppressed because it is too large
Load diff
43
package.json
43
package.json
|
@ -4,40 +4,51 @@
|
|||
"description": "Codex Editor. Native JS, based on API and Open Source",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "webpack --progress --display-error-details --display-entrypoints"
|
||||
"build": "rimraf dist && npm run build:dev",
|
||||
"build:dev": "webpack --mode development --progress --display-error-details --display-entrypoints"
|
||||
},
|
||||
"author": "Codex Team",
|
||||
"license": "ISC",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/codex-team/codex.editor.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-loader": "^7.1.2",
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-loader": "^7.1.4",
|
||||
"babel-plugin-add-module-exports": "^0.2.1",
|
||||
"babel-plugin-class-display-name": "^2.1.0",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"css-loader": "^0.28.7",
|
||||
"eslint": "^4.13.1",
|
||||
"eslint-loader": "^1.9.0",
|
||||
"css-loader": "^0.28.11",
|
||||
"eslint": "^4.19.1",
|
||||
"eslint-loader": "^2.0.0",
|
||||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"html-janitor": "^2.0.2",
|
||||
"path": "^0.12.7",
|
||||
"postcss-apply": "^0.8.0",
|
||||
"postcss-apply": "^0.10.0",
|
||||
"postcss-color-function": "^4.0.1",
|
||||
"postcss-color-hex-alpha": "^3.0.0",
|
||||
"postcss-cssnext": "^3.0.2",
|
||||
"postcss-cssnext": "^3.1.0",
|
||||
"postcss-custom-media": "^6.0.0",
|
||||
"postcss-custom-properties": "^6.2.0",
|
||||
"postcss-custom-properties": "^7.0.0",
|
||||
"postcss-custom-selectors": "^4.0.1",
|
||||
"postcss-font-family-system-ui": "^2.1.1",
|
||||
"postcss-font-family-system-ui": "^3.0.0",
|
||||
"postcss-font-variant": "^3.0.0",
|
||||
"postcss-loader": "^2.0.9",
|
||||
"postcss-loader": "^2.1.5",
|
||||
"postcss-media-minmax": "^3.0.0",
|
||||
"postcss-nested": "^2.1.2",
|
||||
"postcss-nested-ancestors": "^1.0.0",
|
||||
"postcss-nesting": "^4.2.1",
|
||||
"postcss-nested": "^3.0.0",
|
||||
"postcss-nested-ancestors": "^2.0.0",
|
||||
"postcss-nesting": "^6.0.0",
|
||||
"postcss-smart-import": "^0.7.6",
|
||||
"webpack": "^3.10.0"
|
||||
"rimraf": "^2.6.2",
|
||||
"ts-loader": "^4.4.1",
|
||||
"tslint": "^5.10.0",
|
||||
"tslint-loader": "^3.6.0",
|
||||
"typescript": "^2.9.1",
|
||||
"webpack": "^4.12.0",
|
||||
"webpack-cli": "^3.0.3"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
|
|
392
src/codex.js
392
src/codex.js
|
@ -85,248 +85,198 @@ let modules = editorModules.map( module => require('./components/modules/' + mod
|
|||
*
|
||||
* @type {CodexEditor}
|
||||
*/
|
||||
module.exports = class CodexEditor {
|
||||
export default class CodexEditor {
|
||||
/** Editor version */
|
||||
static get version() {
|
||||
return VERSION;
|
||||
}
|
||||
|
||||
/** Editor version */
|
||||
static get version() {
|
||||
/**
|
||||
* @param {EditorConfig} config - user configuration
|
||||
*
|
||||
*/
|
||||
constructor(config) {
|
||||
/**
|
||||
* Configuration object
|
||||
* @type {EditorConfig}
|
||||
*/
|
||||
this.config = {};
|
||||
|
||||
return VERSION;
|
||||
/**
|
||||
* @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 = {};
|
||||
|
||||
Promise.resolve()
|
||||
.then(() => {
|
||||
this.configuration = config;
|
||||
})
|
||||
.then(() => this.init())
|
||||
.then(() => this.start())
|
||||
.then(() => {
|
||||
console.log('CodeX Editor is ready!');
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('CodeX Editor does not ready because of %o', error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setting for configuration
|
||||
* @param {EditorConfig} config
|
||||
*/
|
||||
set configuration(config) {
|
||||
/**
|
||||
* Initlai block type
|
||||
* Uses in case when there is no items passed
|
||||
* @type {{type: (*), data: {text: null}}}
|
||||
*/
|
||||
let initialBlock = {
|
||||
type : config.initialBlock,
|
||||
data : {}
|
||||
};
|
||||
|
||||
this.config.holderId = config.holderId;
|
||||
this.config.placeholder = config.placeholder || 'write your story...';
|
||||
this.config.sanitizer = config.sanitizer || {
|
||||
p: true,
|
||||
b: true,
|
||||
a: true
|
||||
};
|
||||
|
||||
this.config.hideToolbar = config.hideToolbar ? config.hideToolbar : false;
|
||||
this.config.tools = config.tools || {};
|
||||
this.config.toolsConfig = config.toolsConfig || {};
|
||||
this.config.data = config.data || {};
|
||||
|
||||
/**
|
||||
* Initialize items to pass data to the Renderer
|
||||
*/
|
||||
if (_.isEmpty(this.config.data)) {
|
||||
this.config.data = {};
|
||||
this.config.data.items = [ initialBlock ];
|
||||
} else {
|
||||
if (!this.config.data.items || this.config.data.items.length === 0) {
|
||||
this.config.data.items = [ initialBlock ];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EditorConfig} config - user configuration
|
||||
*
|
||||
* If initial Block's Tool was not passed, use the first Tool in config.tools
|
||||
*/
|
||||
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
|
||||
*/
|
||||
this.moduleInstances = {};
|
||||
|
||||
Promise.resolve()
|
||||
.then(() => {
|
||||
|
||||
this.configuration = config;
|
||||
|
||||
})
|
||||
.then(() => this.init())
|
||||
.then(() => this.start())
|
||||
.then(() => {
|
||||
|
||||
console.log('CodeX Editor is ready!');
|
||||
|
||||
})
|
||||
.catch(error => {
|
||||
|
||||
console.log('CodeX Editor does not ready because of %o', error);
|
||||
|
||||
});
|
||||
|
||||
if (!config.initialBlock) {
|
||||
for (this.config.initialBlock in this.config.tools) break;
|
||||
} else {
|
||||
this.config.initialBlock = config.initialBlock;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setting for configuration
|
||||
* @param {EditorConfig} config
|
||||
*/
|
||||
set configuration(config) {
|
||||
|
||||
/**
|
||||
* Initlai block type
|
||||
* Uses in case when there is no items passed
|
||||
* @type {{type: (*), data: {text: null}}}
|
||||
*/
|
||||
let initialBlock = {
|
||||
type : config.initialBlock,
|
||||
data : {}
|
||||
};
|
||||
|
||||
this.config.holderId = config.holderId;
|
||||
this.config.placeholder = config.placeholder || 'write your story...';
|
||||
this.config.sanitizer = config.sanitizer || {
|
||||
p: true,
|
||||
b: true,
|
||||
a: true
|
||||
};
|
||||
|
||||
this.config.hideToolbar = config.hideToolbar ? config.hideToolbar : false;
|
||||
this.config.tools = config.tools || {};
|
||||
this.config.toolsConfig = config.toolsConfig || {};
|
||||
this.config.data = config.data || {};
|
||||
|
||||
/**
|
||||
* Initialize items to pass data to the Renderer
|
||||
*/
|
||||
if (_.isEmpty(this.config.data)) {
|
||||
|
||||
this.config.data = {};
|
||||
this.config.data.items = [ initialBlock ];
|
||||
|
||||
} else {
|
||||
|
||||
if (!this.config.data.items || this.config.data.items.length === 0) {
|
||||
|
||||
this.config.data.items = [ initialBlock ];
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* If initial Block's Tool was not passed, use the first Tool in config.tools
|
||||
*/
|
||||
if (!config.initialBlock) {
|
||||
|
||||
for (this.config.initialBlock in this.config.tools) break;
|
||||
|
||||
} else {
|
||||
|
||||
this.config.initialBlock = config.initialBlock;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns private property
|
||||
* @returns {EditorConfig}
|
||||
*/
|
||||
get configuration() {
|
||||
|
||||
return this.config;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes modules:
|
||||
* - make and save instances
|
||||
* - configure
|
||||
*/
|
||||
init() {
|
||||
|
||||
/**
|
||||
* Make modules instances and save it to the @property this.moduleInstances
|
||||
*/
|
||||
this.constructModules();
|
||||
|
||||
/**
|
||||
* Modules configuration
|
||||
*/
|
||||
this.configureModules();
|
||||
|
||||
}
|
||||
/**
|
||||
* Returns private property
|
||||
* @returns {EditorConfig}
|
||||
*/
|
||||
get configuration() {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes modules:
|
||||
* - make and save instances
|
||||
* - configure
|
||||
*/
|
||||
init() {
|
||||
/**
|
||||
* Make modules instances and save it to the @property this.moduleInstances
|
||||
*/
|
||||
constructModules() {
|
||||
this.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 configuration
|
||||
*/
|
||||
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];
|
||||
}
|
||||
|
||||
/**
|
||||
* Modules instances configuration:
|
||||
* - pass other modules to the 'state' property
|
||||
* - ...
|
||||
*/
|
||||
configureModules() {
|
||||
return diff;
|
||||
}
|
||||
|
||||
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}
|
||||
*/
|
||||
start() {
|
||||
|
||||
let prepareDecorator = module => module.prepare();
|
||||
|
||||
return Promise.resolve()
|
||||
.then(prepareDecorator(this.moduleInstances.Tools))
|
||||
.then(prepareDecorator(this.moduleInstances.UI))
|
||||
.then(prepareDecorator(this.moduleInstances.BlockManager))
|
||||
.then(() => {
|
||||
|
||||
return this.moduleInstances.Renderer.render(this.config.data.items);
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
/**
|
||||
* Start Editor!
|
||||
*
|
||||
* Get list of modules that needs to be prepared and return a sequence (Promise)
|
||||
* @return {Promise}
|
||||
*/
|
||||
start() {
|
||||
let prepareDecorator = module => module.prepare();
|
||||
|
||||
return Promise.resolve()
|
||||
.then(prepareDecorator(this.moduleInstances.Tools))
|
||||
.then(prepareDecorator(this.moduleInstances.UI))
|
||||
.then(prepareDecorator(this.moduleInstances.BlockManager))
|
||||
.then(() => {
|
||||
return this.moduleInstances.Renderer.render(this.config.data.items);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// module.exports = (function (editor) {
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
/**
|
||||
* @abstract
|
||||
* @class Module
|
||||
* @classdesc All modules inherits from this class.
|
||||
*
|
||||
* @typedef {Module} Module
|
||||
* @property {Object} config - Editor user settings
|
||||
* @property {Object} Editor - List of Editor modules
|
||||
*/
|
||||
export default class Module {
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*
|
||||
* @param {EditorConfig} config
|
||||
*/
|
||||
constructor({ config } = {}) {
|
||||
|
||||
if (new.target === Module) {
|
||||
|
||||
throw new TypeError('Constructors for abstract class Module are not allowed.');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {EditorConfig}
|
||||
*/
|
||||
this.config = config;
|
||||
|
||||
/**
|
||||
* @type {EditorComponents}
|
||||
*/
|
||||
this.Editor = null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Editor modules setter
|
||||
*
|
||||
* @param Editor
|
||||
* @param Editor.modules {@link CodexEditor#moduleInstances}
|
||||
* @param Editor.config {@link CodexEditor#configuration}
|
||||
*/
|
||||
set state(Editor) {
|
||||
|
||||
this.Editor = Editor;
|
||||
|
||||
}
|
||||
|
||||
}
|
48
src/components/__module.ts
Normal file
48
src/components/__module.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* @abstract
|
||||
* @class Module
|
||||
* @classdesc All modules inherits from this class.
|
||||
*
|
||||
* @typedef {Module} Module
|
||||
* @property {Object} config - Editor user settings
|
||||
* @property {Object} Editor - List of Editor modules
|
||||
*/
|
||||
export default class Module {
|
||||
|
||||
/**
|
||||
* Editor modules list
|
||||
* @type {EditorComponents}
|
||||
*/
|
||||
private Editor: any = null;
|
||||
|
||||
/**
|
||||
* Editor configuration object
|
||||
* @type {EditorConfig}
|
||||
*/
|
||||
private config: any = {};
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*
|
||||
* @param {EditorConfig} config
|
||||
*/
|
||||
constructor({config}) {
|
||||
|
||||
if (new.target === Module) {
|
||||
throw new TypeError('Constructors for abstract class Module are not allowed.');
|
||||
}
|
||||
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Editor modules setter
|
||||
*
|
||||
* @param Editor
|
||||
* @param Editor.modules {@link CodexEditor#moduleInstances}
|
||||
* @param Editor.config {@link CodexEditor#configuration}
|
||||
*/
|
||||
set state(Editor) {
|
||||
this.Editor = Editor;
|
||||
}
|
||||
}
|
|
@ -18,247 +18,201 @@
|
|||
* @property pluginsContent - HTML content that returns by Tool's render function
|
||||
*/
|
||||
export default class Block {
|
||||
/**
|
||||
* @constructor
|
||||
* @param {String} toolName - Tool name that passed on initialization
|
||||
* @param {Object} toolInstance — passed Tool`s instance that rendered the Block
|
||||
*/
|
||||
constructor(toolName, toolInstance) {
|
||||
this.name = toolName;
|
||||
this.tool = toolInstance;
|
||||
this._html = this.compose();
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS classes for the Block
|
||||
* @return {{wrapper: string, content: string}}
|
||||
*/
|
||||
static get CSS() {
|
||||
return {
|
||||
wrapper: 'ce-block',
|
||||
content: 'ce-block__content',
|
||||
selected: 'ce-block--selected'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Make default Block wrappers and put Tool`s content there
|
||||
* @returns {HTMLDivElement}
|
||||
*/
|
||||
compose() {
|
||||
this.wrapper = $.make('div', Block.CSS.wrapper);
|
||||
this.contentNode = $.make('div', Block.CSS.content);
|
||||
this.pluginsContent = this.tool.render();
|
||||
|
||||
this.contentNode.appendChild(this.pluginsContent);
|
||||
this.wrapper.appendChild(this.contentNode);
|
||||
|
||||
return this.wrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls Tool's method
|
||||
*
|
||||
* Method checks tool property {MethodName}. Fires method with passes params If it is instance of Function
|
||||
*
|
||||
* @param {String} methodName
|
||||
* @param {Object} params
|
||||
*/
|
||||
call(methodName, params) {
|
||||
/**
|
||||
* @constructor
|
||||
* @param {String} toolName - Tool name that passed on initialization
|
||||
* @param {Object} toolInstance — passed Tool`s instance that rendered the Block
|
||||
* call Tool's method with the instance context
|
||||
*/
|
||||
constructor(toolName, toolInstance) {
|
||||
|
||||
this.name = toolName;
|
||||
this.tool = toolInstance;
|
||||
this._html = this.compose();
|
||||
|
||||
if (this.tool[methodName] && this.tool[methodName] instanceof Function) {
|
||||
this.tool[methodName].call(this.tool, params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS classes for the Block
|
||||
* @return {{wrapper: string, content: string}}
|
||||
*/
|
||||
static get CSS() {
|
||||
/**
|
||||
* Get Block`s HTML
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
get html() {
|
||||
return this._html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Block's JSON data
|
||||
* @return {Object}
|
||||
*/
|
||||
get data() {
|
||||
return this.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* is block mergeable
|
||||
* We plugin have merge function then we call it mergable
|
||||
* @return {boolean}
|
||||
*/
|
||||
get mergeable() {
|
||||
return typeof this.tool.merge === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* Call plugins merge method
|
||||
* @param {Object} data
|
||||
*/
|
||||
mergeWith(data) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
this.tool.merge(data);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Extracts data from Block
|
||||
* Groups Tool's save processing time
|
||||
* @return {Object}
|
||||
*/
|
||||
save() {
|
||||
let extractedBlock = this.tool.save(this.pluginsContent);
|
||||
|
||||
/** Measuring execution time*/
|
||||
let measuringStart = window.performance.now(),
|
||||
measuringEnd;
|
||||
|
||||
return Promise.resolve(extractedBlock)
|
||||
.then((finishedExtraction) => {
|
||||
/** measure promise execution */
|
||||
measuringEnd = window.performance.now();
|
||||
|
||||
return {
|
||||
wrapper: 'ce-block',
|
||||
content: 'ce-block__content',
|
||||
selected: 'ce-block--selected'
|
||||
tool: this.name,
|
||||
data: finishedExtraction,
|
||||
time : measuringEnd - measuringStart
|
||||
};
|
||||
})
|
||||
.catch(function (error) {
|
||||
_.log(`Saving proccess for ${this.tool.name} tool failed due to the ${error}`, 'log', 'red');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses Tool's validation method to check the correctness of output data
|
||||
* Tool's validation method is optional
|
||||
*
|
||||
* @description Method also can return data if it passed the validation
|
||||
*
|
||||
* @param {Object} data
|
||||
* @returns {Boolean|Object} valid
|
||||
*/
|
||||
validateData(data) {
|
||||
let isValid = true;
|
||||
|
||||
if (this.tool.validate instanceof Function) {
|
||||
isValid = this.tool.validate(data);
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check block for emptiness
|
||||
* @return {Boolean}
|
||||
*/
|
||||
get isEmpty() {
|
||||
/**
|
||||
* Make default Block wrappers and put Tool`s content there
|
||||
* @returns {HTMLDivElement}
|
||||
* Allow Tool to represent decorative contentless blocks: for example "* * *"-tool
|
||||
* That Tools are not empty
|
||||
*/
|
||||
compose() {
|
||||
|
||||
this.wrapper = $.make('div', Block.CSS.wrapper);
|
||||
this.contentNode = $.make('div', Block.CSS.content);
|
||||
this.pluginsContent = this.tool.render();
|
||||
|
||||
this.contentNode.appendChild(this.pluginsContent);
|
||||
this.wrapper.appendChild(this.contentNode);
|
||||
|
||||
return this.wrapper;
|
||||
|
||||
if (this.tool.contentless) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let emptyText = $.isEmpty(this.pluginsContent),
|
||||
emptyMedia = !this.hasMedia;
|
||||
|
||||
return emptyText && emptyMedia;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if block has a media content such as images, iframes and other
|
||||
* @return {Boolean}
|
||||
*/
|
||||
get hasMedia() {
|
||||
/**
|
||||
* Calls Tool's method
|
||||
*
|
||||
* Method checks tool property {MethodName}. Fires method with passes params If it is instance of Function
|
||||
*
|
||||
* @param {String} methodName
|
||||
* @param {Object} params
|
||||
* This tags represents media-content
|
||||
* @type {string[]}
|
||||
*/
|
||||
call(methodName, params) {
|
||||
const mediaTags = [
|
||||
'img',
|
||||
'iframe',
|
||||
'video',
|
||||
'audio',
|
||||
'source',
|
||||
'input',
|
||||
'textarea',
|
||||
'twitterwidget'
|
||||
];
|
||||
|
||||
/**
|
||||
* call Tool's method with the instance context
|
||||
*/
|
||||
if (this.tool[methodName] && this.tool[methodName] instanceof Function) {
|
||||
|
||||
this.tool[methodName].call(this.tool, params);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
return !!this._html.querySelector(mediaTags.join(','));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set selected state
|
||||
* @param {Boolean} state - 'true' to select, 'false' to remove selection
|
||||
*/
|
||||
set selected(state) {
|
||||
/**
|
||||
* Get Block`s HTML
|
||||
* @returns {HTMLElement}
|
||||
* We don't need to mark Block as Selected when it is not empty
|
||||
*/
|
||||
get html() {
|
||||
|
||||
return this._html;
|
||||
|
||||
if (state === true && !this.isEmpty) {
|
||||
this._html.classList.add(Block.CSS.selected);
|
||||
} else {
|
||||
this._html.classList.remove(Block.CSS.selected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Block's JSON data
|
||||
* @return {Object}
|
||||
*/
|
||||
get data() {
|
||||
|
||||
return this.save();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* is block mergeable
|
||||
* We plugin have merge function then we call it mergable
|
||||
* @return {boolean}
|
||||
*/
|
||||
get mergeable() {
|
||||
|
||||
return typeof this.tool.merge === 'function';
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Call plugins merge method
|
||||
* @param {Object} data
|
||||
*/
|
||||
mergeWith(data) {
|
||||
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
|
||||
this.tool.merge(data);
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
/**
|
||||
* Extracts data from Block
|
||||
* Groups Tool's save processing time
|
||||
* @return {Object}
|
||||
*/
|
||||
save() {
|
||||
|
||||
let extractedBlock = this.tool.save(this.pluginsContent);
|
||||
|
||||
/** Measuring execution time*/
|
||||
let measuringStart = window.performance.now(),
|
||||
measuringEnd;
|
||||
|
||||
return Promise.resolve(extractedBlock)
|
||||
.then((finishedExtraction) => {
|
||||
|
||||
/** measure promise execution */
|
||||
measuringEnd = window.performance.now();
|
||||
|
||||
return {
|
||||
tool: this.name,
|
||||
data: finishedExtraction,
|
||||
time : measuringEnd - measuringStart
|
||||
};
|
||||
|
||||
})
|
||||
.catch(function (error) {
|
||||
|
||||
_.log(`Saving proccess for ${this.tool.name} tool failed due to the ${error}`, 'log', 'red');
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses Tool's validation method to check the correctness of output data
|
||||
* Tool's validation method is optional
|
||||
*
|
||||
* @description Method also can return data if it passed the validation
|
||||
*
|
||||
* @param {Object} data
|
||||
* @returns {Boolean|Object} valid
|
||||
*/
|
||||
validateData(data) {
|
||||
|
||||
let isValid = true;
|
||||
|
||||
if (this.tool.validate instanceof Function) {
|
||||
|
||||
isValid = this.tool.validate(data);
|
||||
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
return data;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Check block for emptiness
|
||||
* @return {Boolean}
|
||||
*/
|
||||
get isEmpty() {
|
||||
|
||||
/**
|
||||
* Allow Tool to represent decorative contentless blocks: for example "* * *"-tool
|
||||
* That Tools are not empty
|
||||
*/
|
||||
if (this.tool.contentless) {
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
let emptyText = $.isEmpty(this.pluginsContent),
|
||||
emptyMedia = !this.hasMedia;
|
||||
|
||||
return emptyText && emptyMedia;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if block has a media content such as images, iframes and other
|
||||
* @return {Boolean}
|
||||
*/
|
||||
get hasMedia() {
|
||||
|
||||
/**
|
||||
* This tags represents media-content
|
||||
* @type {string[]}
|
||||
*/
|
||||
const mediaTags = [
|
||||
'img',
|
||||
'iframe',
|
||||
'video',
|
||||
'audio',
|
||||
'source',
|
||||
'input',
|
||||
'textarea',
|
||||
'twitterwidget'
|
||||
];
|
||||
|
||||
return !!this._html.querySelector(mediaTags.join(','));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Set selected state
|
||||
* @param {Boolean} state - 'true' to select, 'false' to remove selection
|
||||
*/
|
||||
set selected(state) {
|
||||
|
||||
/**
|
||||
* We don't need to mark Block as Selected when it is not empty
|
||||
*/
|
||||
if (state === true && !this.isEmpty) {
|
||||
|
||||
this._html.classList.add(Block.CSS.selected);
|
||||
|
||||
} else {
|
||||
|
||||
this._html.classList.remove(Block.CSS.selected);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -2,315 +2,249 @@
|
|||
* DOM manipulations helper
|
||||
*/
|
||||
export default class Dom {
|
||||
|
||||
/**
|
||||
* Check if passed tag has no closed tag
|
||||
* @param {Element} tag
|
||||
* @return {Boolean}
|
||||
*/
|
||||
static isSingleTag(tag) {
|
||||
|
||||
return tag.tagName && ['AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR'].includes(tag.tagName);
|
||||
|
||||
};
|
||||
/**
|
||||
* Check if passed tag has no closed tag
|
||||
* @param {Element} tag
|
||||
* @return {Boolean}
|
||||
*/
|
||||
static isSingleTag(tag) {
|
||||
return tag.tagName && ['AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR'].includes(tag.tagName);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Helper for making Elements with classname and attributes
|
||||
*
|
||||
* @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}
|
||||
*/
|
||||
static make(tagName, classNames = null, attributes = {}) {
|
||||
|
||||
let el = document.createElement(tagName);
|
||||
|
||||
if ( Array.isArray(classNames) ) {
|
||||
|
||||
el.classList.add(...classNames);
|
||||
|
||||
} else if( classNames ) {
|
||||
|
||||
el.classList.add(classNames);
|
||||
|
||||
}
|
||||
|
||||
for (let attrName in attributes) {
|
||||
|
||||
el[attrName] = attributes[attrName];
|
||||
|
||||
}
|
||||
|
||||
return el;
|
||||
/**
|
||||
* Helper for making Elements with classname and attributes
|
||||
*
|
||||
* @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}
|
||||
*/
|
||||
static make(tagName, classNames = null, attributes = {}) {
|
||||
let el = document.createElement(tagName);
|
||||
|
||||
if ( Array.isArray(classNames) ) {
|
||||
el.classList.add(...classNames);
|
||||
} else if( classNames ) {
|
||||
el.classList.add(classNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates Text Node with the passed content
|
||||
* @param {String} content - text content
|
||||
* @return {Text}
|
||||
*/
|
||||
static text(content) {
|
||||
|
||||
return document.createTextNode(content);
|
||||
|
||||
for (let attrName in attributes) {
|
||||
el[attrName] = attributes[attrName];
|
||||
}
|
||||
|
||||
/**
|
||||
* Append one or several elements to the parent
|
||||
*
|
||||
* @param {Element} parent - where to append
|
||||
* @param {Element|Element[]} - element ore elements list
|
||||
*/
|
||||
static append(parent, elements) {
|
||||
return el;
|
||||
}
|
||||
|
||||
if ( Array.isArray(elements) ) {
|
||||
|
||||
elements.forEach( el => parent.appendChild(el) );
|
||||
|
||||
} else {
|
||||
|
||||
parent.appendChild(elements);
|
||||
|
||||
}
|
||||
/**
|
||||
* Creates Text Node with the passed content
|
||||
* @param {String} content - text content
|
||||
* @return {Text}
|
||||
*/
|
||||
static text(content) {
|
||||
return document.createTextNode(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append one or several elements to the parent
|
||||
*
|
||||
* @param {Element} parent - where to append
|
||||
* @param {Element|Element[]} - element ore elements list
|
||||
*/
|
||||
static append(parent, elements) {
|
||||
if ( Array.isArray(elements) ) {
|
||||
elements.forEach( el => parent.appendChild(el) );
|
||||
} else {
|
||||
parent.appendChild(elements);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector Decorator
|
||||
*
|
||||
* Returns first match
|
||||
*
|
||||
* @param {Element} el - element we searching inside. Default - DOM Document
|
||||
* @param {String} selector - searching string
|
||||
*
|
||||
* @returns {Element}
|
||||
*/
|
||||
static find(el = document, selector) {
|
||||
return el.querySelector(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector Decorator.
|
||||
*
|
||||
* Returns all matches
|
||||
*
|
||||
* @param {Element} el - element we searching inside. Default - DOM Document
|
||||
* @param {String} selector - searching string
|
||||
* @returns {NodeList}
|
||||
*/
|
||||
static findAll(el = document, selector) {
|
||||
return el.querySelectorAll(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for deepest node which is Leaf.
|
||||
* Leaf is the vertex that doesn't have any child nodes
|
||||
*
|
||||
* @description Method recursively goes throw the all Node until it finds the Leaf
|
||||
*
|
||||
* @param {Node} node - root Node. From this vertex we start Deep-first search {@link https://en.wikipedia.org/wiki/Depth-first_search}
|
||||
* @param {Boolean} atLast - find last text node
|
||||
* @return {Node} - it can be text Node or Element Node, so that caret will able to work with it
|
||||
*/
|
||||
static getDeepestNode(node, atLast = false) {
|
||||
/**
|
||||
* Selector Decorator
|
||||
*
|
||||
* Returns first match
|
||||
*
|
||||
* @param {Element} el - element we searching inside. Default - DOM Document
|
||||
* @param {String} selector - searching string
|
||||
*
|
||||
* @returns {Element}
|
||||
* Current function have two directions:
|
||||
* - starts from first child and every time gets first or nextSibling in special cases
|
||||
* - starts from last child and gets last or previousSibling
|
||||
* @type {string}
|
||||
*/
|
||||
static find(el = document, selector) {
|
||||
let child = atLast ? 'lastChild' : 'firstChild',
|
||||
sibling = atLast ? 'previousSibling' : 'nextSibling';
|
||||
|
||||
return el.querySelector(selector);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector Decorator.
|
||||
*
|
||||
* Returns all matches
|
||||
*
|
||||
* @param {Element} el - element we searching inside. Default - DOM Document
|
||||
* @param {String} selector - searching string
|
||||
* @returns {NodeList}
|
||||
*/
|
||||
static findAll(el = document, selector) {
|
||||
|
||||
return el.querySelectorAll(selector);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for deepest node which is Leaf.
|
||||
* Leaf is the vertex that doesn't have any child nodes
|
||||
*
|
||||
* @description Method recursively goes throw the all Node until it finds the Leaf
|
||||
*
|
||||
* @param {Node} node - root Node. From this vertex we start Deep-first search {@link https://en.wikipedia.org/wiki/Depth-first_search}
|
||||
* @param {Boolean} atLast - find last text node
|
||||
* @return {Node} - it can be text Node or Element Node, so that caret will able to work with it
|
||||
*/
|
||||
static getDeepestNode(node, atLast = false) {
|
||||
if (node && node.nodeType === Node.ELEMENT_NODE && node[child]) {
|
||||
let nodeChild = node[child];
|
||||
|
||||
/**
|
||||
* special case when child is single tag that can't contain any content
|
||||
*/
|
||||
if (Dom.isSingleTag(nodeChild)) {
|
||||
/**
|
||||
* Current function have two directions:
|
||||
* - starts from first child and every time gets first or nextSibling in special cases
|
||||
* - starts from last child and gets last or previousSibling
|
||||
* @type {string}
|
||||
* 1) We need to check the next sibling. If it is Node Element then continue searching for deepest
|
||||
* from sibling
|
||||
*
|
||||
* 2) If single tag's next sibling is null, then go back to parent and check his sibling
|
||||
* In case of Node Element continue searching
|
||||
*
|
||||
* 3) If none of conditions above happened return parent Node Element
|
||||
*/
|
||||
let child = atLast ? 'lastChild' : 'firstChild',
|
||||
sibling = atLast ? 'previousSibling' : 'nextSibling';
|
||||
|
||||
if (node && node.nodeType === Node.ELEMENT_NODE && node[child]) {
|
||||
|
||||
let nodeChild = node[child];
|
||||
|
||||
/**
|
||||
* special case when child is single tag that can't contain any content
|
||||
*/
|
||||
if (Dom.isSingleTag(nodeChild)) {
|
||||
|
||||
/**
|
||||
* 1) We need to check the next sibling. If it is Node Element then continue searching for deepest
|
||||
* from sibling
|
||||
*
|
||||
* 2) If single tag's next sibling is null, then go back to parent and check his sibling
|
||||
* In case of Node Element continue searching
|
||||
*
|
||||
* 3) If none of conditions above happened return parent Node Element
|
||||
*/
|
||||
if (nodeChild[sibling]) {
|
||||
|
||||
nodeChild = nodeChild[sibling];
|
||||
|
||||
} else if (nodeChild.parentNode[sibling]) {
|
||||
|
||||
nodeChild = nodeChild.parentNode[sibling];
|
||||
|
||||
} else {
|
||||
|
||||
return nodeChild.parentNode;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return this.getDeepestNode(nodeChild, atLast);
|
||||
|
||||
}
|
||||
|
||||
return node;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if object is DOM node
|
||||
*
|
||||
* @param {Object} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static isElement(node) {
|
||||
|
||||
return node && typeof node === 'object' && node.nodeType && node.nodeType === Node.ELEMENT_NODE;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks target if it is native input
|
||||
* @param {Element|String} target - HTML element or string
|
||||
* @return {Boolean}
|
||||
*/
|
||||
static isNativeInput(target) {
|
||||
|
||||
let nativeInputs = [
|
||||
'INPUT',
|
||||
'TEXTAREA'
|
||||
];
|
||||
|
||||
return target ? nativeInputs.includes(target.tagName) : false;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks node if it is empty
|
||||
*
|
||||
* @description Method checks simple Node without any childs for emptiness
|
||||
* If you have Node with 2 or more children id depth, you better use {@link Dom#isEmpty} method
|
||||
*
|
||||
* @param {Node} node
|
||||
* @return {Boolean} true if it is empty
|
||||
*/
|
||||
static isNodeEmpty(node) {
|
||||
|
||||
let nodeText;
|
||||
|
||||
if ( this.isElement(node) && this.isNativeInput(node) ) {
|
||||
|
||||
nodeText = node.value;
|
||||
|
||||
if (nodeChild[sibling]) {
|
||||
nodeChild = nodeChild[sibling];
|
||||
} else if (nodeChild.parentNode[sibling]) {
|
||||
nodeChild = nodeChild.parentNode[sibling];
|
||||
} else {
|
||||
|
||||
nodeText = node.textContent.replace('\u200B', '');
|
||||
|
||||
return nodeChild.parentNode;
|
||||
}
|
||||
}
|
||||
|
||||
return nodeText.trim().length === 0;
|
||||
|
||||
return this.getDeepestNode(nodeChild, atLast);
|
||||
}
|
||||
|
||||
/**
|
||||
* checks node if it is doesn't have any child nodes
|
||||
* @param {Node} node
|
||||
* @return {boolean}
|
||||
*/
|
||||
static isLeaf(node) {
|
||||
return node;
|
||||
}
|
||||
|
||||
if (!node) {
|
||||
/**
|
||||
* Check if object is DOM node
|
||||
*
|
||||
* @param {Object} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static isElement(node) {
|
||||
return node && typeof node === 'object' && node.nodeType && node.nodeType === Node.ELEMENT_NODE;
|
||||
}
|
||||
|
||||
return false;
|
||||
/**
|
||||
* Checks target if it is native input
|
||||
* @param {Element|String} target - HTML element or string
|
||||
* @return {Boolean}
|
||||
*/
|
||||
static isNativeInput(target) {
|
||||
let nativeInputs = [
|
||||
'INPUT',
|
||||
'TEXTAREA'
|
||||
];
|
||||
|
||||
}
|
||||
return target ? nativeInputs.includes(target.tagName) : false;
|
||||
}
|
||||
|
||||
return node.childNodes.length === 0;
|
||||
/**
|
||||
* Checks node if it is empty
|
||||
*
|
||||
* @description Method checks simple Node without any childs for emptiness
|
||||
* If you have Node with 2 or more children id depth, you better use {@link Dom#isEmpty} method
|
||||
*
|
||||
* @param {Node} node
|
||||
* @return {Boolean} true if it is empty
|
||||
*/
|
||||
static isNodeEmpty(node) {
|
||||
let nodeText;
|
||||
|
||||
if ( this.isElement(node) && this.isNativeInput(node) ) {
|
||||
nodeText = node.value;
|
||||
} else {
|
||||
nodeText = node.textContent.replace('\u200B', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* breadth-first search (BFS)
|
||||
* {@link https://en.wikipedia.org/wiki/Breadth-first_search}
|
||||
*
|
||||
* @description Pushes to stack all DOM leafs and checks for emptiness
|
||||
*
|
||||
* @param {Node} node
|
||||
* @return {boolean}
|
||||
*/
|
||||
static isEmpty(node) {
|
||||
return nodeText.trim().length === 0;
|
||||
}
|
||||
|
||||
let treeWalker = [],
|
||||
leafs = [];
|
||||
/**
|
||||
* checks node if it is doesn't have any child nodes
|
||||
* @param {Node} node
|
||||
* @return {boolean}
|
||||
*/
|
||||
static isLeaf(node) {
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!node) {
|
||||
return node.childNodes.length === 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
/**
|
||||
* breadth-first search (BFS)
|
||||
* {@link https://en.wikipedia.org/wiki/Breadth-first_search}
|
||||
*
|
||||
* @description Pushes to stack all DOM leafs and checks for emptiness
|
||||
*
|
||||
* @param {Node} node
|
||||
* @return {boolean}
|
||||
*/
|
||||
static isEmpty(node) {
|
||||
let treeWalker = [],
|
||||
leafs = [];
|
||||
|
||||
}
|
||||
if (!node) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!node.childNodes.length) {
|
||||
if (!node.childNodes.length) {
|
||||
return this.isNodeEmpty(node);
|
||||
}
|
||||
|
||||
return this.isNodeEmpty(node);
|
||||
treeWalker.push(node.firstChild);
|
||||
|
||||
}
|
||||
while ( treeWalker.length > 0 ) {
|
||||
node = treeWalker.shift();
|
||||
|
||||
if (!node) continue;
|
||||
|
||||
if ( this.isLeaf(node) ) {
|
||||
leafs.push(node);
|
||||
} else {
|
||||
treeWalker.push(node.firstChild);
|
||||
}
|
||||
|
||||
while ( treeWalker.length > 0 ) {
|
||||
while ( node && node.nextSibling ) {
|
||||
node = node.nextSibling;
|
||||
|
||||
node = treeWalker.shift();
|
||||
if (!node) continue;
|
||||
|
||||
if (!node) continue;
|
||||
|
||||
if ( this.isLeaf(node) ) {
|
||||
|
||||
leafs.push(node);
|
||||
|
||||
} else {
|
||||
|
||||
treeWalker.push(node.firstChild);
|
||||
|
||||
}
|
||||
|
||||
while ( node && node.nextSibling ) {
|
||||
|
||||
node = node.nextSibling;
|
||||
|
||||
if (!node) continue;
|
||||
|
||||
treeWalker.push(node);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* If one of childs is not empty, checked Node is not empty too
|
||||
*/
|
||||
if (node && !this.isNodeEmpty(node)) {
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return leafs.every( leaf => this.isNodeEmpty(leaf) );
|
||||
treeWalker.push(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* If one of childs is not empty, checked Node is not empty too
|
||||
*/
|
||||
if (node && !this.isNodeEmpty(node)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return leafs.every( leaf => this.isNodeEmpty(leaf) );
|
||||
}
|
||||
};
|
File diff suppressed because it is too large
Load diff
|
@ -15,285 +15,233 @@
|
|||
import Selection from '../Selection';
|
||||
|
||||
export default class Caret extends Module {
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
constructor({config}) {
|
||||
super({config});
|
||||
}
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
constructor({config}) {
|
||||
/**
|
||||
* Method gets Block instance and puts caret to the text node with offset
|
||||
* There two ways that method applies caret position:
|
||||
* - first found text node: sets at the beginning, but you can pass an offset
|
||||
* - last found text node: sets at the end of the node. Also, you can customize the behaviour
|
||||
*
|
||||
* @param {Block} block - Block class
|
||||
* @param {Number} offset - caret offset regarding to the text node
|
||||
* @param {Boolean} atEnd - put caret at the end of the text node or not
|
||||
*/
|
||||
setToBlock(block, offset = 0, atEnd = false) {
|
||||
let element = block.pluginsContent;
|
||||
|
||||
super({config});
|
||||
/** If Element is INPUT */
|
||||
if ($.isNativeInput(element)) {
|
||||
element.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
let nodeToSet = $.getDeepestNode(element, atEnd);
|
||||
|
||||
if (atEnd || offset > nodeToSet.length) {
|
||||
offset = nodeToSet.length;
|
||||
}
|
||||
|
||||
/** if found deepest node is native input */
|
||||
if ($.isNativeInput(nodeToSet)) {
|
||||
nodeToSet.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method gets Block instance and puts caret to the text node with offset
|
||||
* There two ways that method applies caret position:
|
||||
* - first found text node: sets at the beginning, but you can pass an offset
|
||||
* - last found text node: sets at the end of the node. Also, you can customize the behaviour
|
||||
*
|
||||
* @param {Block} block - Block class
|
||||
* @param {Number} offset - caret offset regarding to the text node
|
||||
* @param {Boolean} atEnd - put caret at the end of the text node or not
|
||||
*/
|
||||
setToBlock(block, offset = 0, atEnd = false) {
|
||||
|
||||
let element = block.pluginsContent;
|
||||
|
||||
/** If Element is INPUT */
|
||||
if ($.isNativeInput(element)) {
|
||||
|
||||
element.focus();
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
let nodeToSet = $.getDeepestNode(element, atEnd);
|
||||
|
||||
if (atEnd || offset > nodeToSet.length) {
|
||||
|
||||
offset = nodeToSet.length;
|
||||
|
||||
}
|
||||
|
||||
/** if found deepest node is native input */
|
||||
if ($.isNativeInput(nodeToSet)) {
|
||||
|
||||
nodeToSet.focus();
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo try to fix via Promises or use querySelectorAll to not to use timeout
|
||||
*/
|
||||
_.delay( () => {
|
||||
_.delay( () => {
|
||||
this.set(nodeToSet, offset);
|
||||
}, 20)();
|
||||
|
||||
this.set(nodeToSet, offset);
|
||||
this.Editor.BlockManager.currentNode = block.wrapper;
|
||||
}
|
||||
|
||||
}, 20)();
|
||||
/**
|
||||
* Creates Document Range and sets caret to the element with offset
|
||||
* @param {Element} element - target node.
|
||||
* @param {Number} offset - offset
|
||||
*/
|
||||
set( element, offset = 0) {
|
||||
let range = document.createRange(),
|
||||
selection = Selection.get();
|
||||
|
||||
this.Editor.BlockManager.currentNode = block.wrapper;
|
||||
range.setStart(element, offset);
|
||||
range.setEnd(element, offset);
|
||||
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set Caret to the last Block
|
||||
* If last block is not empty, append another empty block
|
||||
*/
|
||||
setToTheLastBlock() {
|
||||
let lastBlock = this.Editor.BlockManager.lastBlock;
|
||||
|
||||
if (!lastBlock) return;
|
||||
|
||||
/**
|
||||
* If last block is empty and it is an initialBlock, set to that.
|
||||
* Otherwise, append new empty block and set to that
|
||||
*/
|
||||
if (lastBlock.isEmpty) {
|
||||
this.setToBlock(lastBlock);
|
||||
} else {
|
||||
this.Editor.BlockManager.insert(this.config.initialBlock);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract content fragment of current Block from Caret position to the end of the Block
|
||||
*/
|
||||
extractFragmentFromCaretPosition() {
|
||||
let selection = Selection.get();
|
||||
|
||||
if (selection.rangeCount) {
|
||||
let selectRange = selection.getRangeAt(0),
|
||||
blockElem = this.Editor.BlockManager.currentBlock.pluginsContent;
|
||||
|
||||
selectRange.deleteContents();
|
||||
|
||||
if (blockElem) {
|
||||
let range = selectRange.cloneRange(true);
|
||||
|
||||
range.selectNodeContents(blockElem);
|
||||
range.setStart(selectRange.endContainer, selectRange.endOffset);
|
||||
return range.extractContents();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.pluginsContent);
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates Document Range and sets caret to the element with offset
|
||||
* @param {Element} element - target node.
|
||||
* @param {Number} offset - offset
|
||||
*/
|
||||
set( element, offset = 0) {
|
||||
|
||||
let range = document.createRange(),
|
||||
selection = Selection.get();
|
||||
|
||||
range.setStart(element, offset);
|
||||
range.setEnd(element, offset);
|
||||
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Set Caret to the last Block
|
||||
* If last block is not empty, append another empty block
|
||||
*/
|
||||
setToTheLastBlock() {
|
||||
|
||||
let lastBlock = this.Editor.BlockManager.lastBlock;
|
||||
|
||||
if (!lastBlock) return;
|
||||
|
||||
/**
|
||||
* If last block is empty and it is an initialBlock, set to that.
|
||||
* Otherwise, append new empty block and set to that
|
||||
*/
|
||||
if (lastBlock.isEmpty) {
|
||||
|
||||
this.setToBlock(lastBlock);
|
||||
|
||||
} else {
|
||||
|
||||
this.Editor.BlockManager.insert(this.config.initialBlock);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract content fragment of current Block from Caret position to the end of the Block
|
||||
*/
|
||||
extractFragmentFromCaretPosition() {
|
||||
|
||||
let selection = Selection.get();
|
||||
|
||||
if (selection.rangeCount) {
|
||||
|
||||
let selectRange = selection.getRangeAt(0),
|
||||
blockElem = this.Editor.BlockManager.currentBlock.pluginsContent;
|
||||
|
||||
selectRange.deleteContents();
|
||||
|
||||
if (blockElem) {
|
||||
|
||||
let range = selectRange.cloneRange(true);
|
||||
|
||||
range.selectNodeContents(blockElem);
|
||||
range.setStart(selectRange.endContainer, selectRange.endOffset);
|
||||
return range.extractContents();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all first-level (first child of [contenteditabel]) siblings from passed node
|
||||
* Then you can check it for emptiness
|
||||
*
|
||||
* @example
|
||||
* In case of
|
||||
* <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> |
|
||||
* <p><b></b></p> <-- first (and deepest) node is <b></b>
|
||||
* |adaddad <-- anchor node
|
||||
* </div>
|
||||
*
|
||||
* @return {Element[]}
|
||||
*/
|
||||
getHigherLevelSiblings(from, direction ) {
|
||||
if ($.isEmpty(firstNode)) {
|
||||
let leftSiblings = this.getHigherLevelSiblings(anchorNode, 'left'),
|
||||
nothingAtLeft = leftSiblings.every( node => $.isEmpty(node) );
|
||||
|
||||
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;
|
||||
|
||||
if (nothingAtLeft && selection.anchorOffset === firstLetterPosition) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
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.pluginsContent, true);
|
||||
|
||||
/**
|
||||
* Get's deepest first node and checks if offset is zero
|
||||
* @return {boolean}
|
||||
* In case of
|
||||
* <div contenteditable>
|
||||
* adaddad| <-- anchor node
|
||||
* <p><b></b></p> <-- first (and deepest) node is <b></b>
|
||||
* </div>
|
||||
*/
|
||||
get isAtStart() {
|
||||
|
||||
/**
|
||||
* Don't handle ranges
|
||||
*/
|
||||
if (!Selection.isCollapsed) {
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
let selection = Selection.get(),
|
||||
anchorNode = selection.anchorNode,
|
||||
firstNode = $.getDeepestNode(this.Editor.BlockManager.currentBlock.pluginsContent);
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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.pluginsContent, true);
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return anchorNode === lastNode && selection.anchorOffset === lastNode.textContent.length;
|
||||
if ($.isEmpty(lastNode)) {
|
||||
let leftSiblings = this.getHigherLevelSiblings(anchorNode, 'right'),
|
||||
nothingAtRight = leftSiblings.every( node => $.isEmpty(node) );
|
||||
|
||||
if (nothingAtRight && selection.anchorOffset === anchorNode.textContent.length) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return anchorNode === lastNode && selection.anchorOffset === lastNode.textContent.length;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,58 +11,44 @@
|
|||
* @property {Object} subscribers - all subscribers grouped by event name
|
||||
*/
|
||||
export default class Events extends Module {
|
||||
|
||||
/**
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
constructor({config}) {
|
||||
|
||||
super({config});
|
||||
this.subscribers = {};
|
||||
constructor({config}) {
|
||||
super({config});
|
||||
this.subscribers = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} eventName - event name
|
||||
* @param {Function} callback - subscriber
|
||||
*/
|
||||
on(eventName, callback) {
|
||||
if (!(eventName in this.subscribers)) {
|
||||
this.subscribers[eventName] = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} eventName - event name
|
||||
* @param {Function} callback - subscriber
|
||||
*/
|
||||
on(eventName, callback) {
|
||||
// group by events
|
||||
this.subscribers[eventName].push(callback);
|
||||
}
|
||||
|
||||
if (!(eventName in this.subscribers)) {
|
||||
/**
|
||||
* @param {String} eventName - event name
|
||||
* @param {Object} data - subscribers get this data when they were fired
|
||||
*/
|
||||
emit(eventName, data) {
|
||||
this.subscribers[eventName].reduce(function (previousData, currentHandler) {
|
||||
let newData = currentHandler(previousData);
|
||||
|
||||
this.subscribers[eventName] = [];
|
||||
|
||||
}
|
||||
|
||||
// group by events
|
||||
this.subscribers[eventName].push(callback);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} eventName - event name
|
||||
* @param {Object} data - subscribers get this data when they were fired
|
||||
*/
|
||||
emit(eventName, data) {
|
||||
|
||||
this.subscribers[eventName].reduce(function (previousData, currentHandler) {
|
||||
|
||||
let newData = currentHandler(previousData);
|
||||
|
||||
return newData ? newData : previousData;
|
||||
|
||||
}, data);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroyer
|
||||
* clears subsribers list
|
||||
*/
|
||||
destroy() {
|
||||
|
||||
this.subscribers = null;
|
||||
|
||||
}
|
||||
return newData ? newData : previousData;
|
||||
}, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroyer
|
||||
* clears subsribers list
|
||||
*/
|
||||
destroy() {
|
||||
this.subscribers = null;
|
||||
}
|
||||
}
|
|
@ -12,166 +12,138 @@
|
|||
* @typedef {Keyboard} Keyboard
|
||||
*/
|
||||
export default class Keyboard extends Module {
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
constructor({config}) {
|
||||
super({config});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler on Block for keyboard keys at keydown event
|
||||
*
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
blockKeydownsListener(event) {
|
||||
switch(event.keyCode) {
|
||||
case _.keyCodes.BACKSPACE:
|
||||
|
||||
_.log('Backspace key pressed');
|
||||
this.backspacePressed(event);
|
||||
break;
|
||||
|
||||
case _.keyCodes.ENTER:
|
||||
|
||||
_.log('Enter key pressed');
|
||||
this.enterPressed(event);
|
||||
break;
|
||||
|
||||
case _.keyCodes.DOWN:
|
||||
case _.keyCodes.RIGHT:
|
||||
|
||||
_.log('Right/Down key pressed');
|
||||
this.arrowRightAndDownPressed();
|
||||
break;
|
||||
|
||||
case _.keyCodes.UP:
|
||||
case _.keyCodes.LEFT:
|
||||
|
||||
_.log('Left/Up key pressed');
|
||||
this.arrowLeftAndUpPressed();
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle pressing enter key
|
||||
*
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
enterPressed(event) {
|
||||
let currentBlock = this.Editor.BlockManager.currentBlock,
|
||||
toolsConfig = this.config.toolsConfig[currentBlock.name];
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* Don't handle Enter keydowns when Tool sets enableLineBreaks to true.
|
||||
* Uses for Tools like <code> where line breaks should be handled by default behaviour.
|
||||
*/
|
||||
constructor({config}) {
|
||||
|
||||
super({config});
|
||||
|
||||
if (toolsConfig && toolsConfig.enableLineBreaks) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler on Block for keyboard keys at keydown event
|
||||
* Allow to create linebreaks by Shift+Enter
|
||||
*/
|
||||
if (event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Split the Current Block into two blocks
|
||||
*/
|
||||
this.Editor.BlockManager.split();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle backspace keypress on block
|
||||
* @param {KeyboardEvent} event - keydown
|
||||
*/
|
||||
backspacePressed(event) {
|
||||
const BM = this.Editor.BlockManager;
|
||||
|
||||
let isFirstBlock = BM.currentBlockIndex === 0,
|
||||
canMergeBlocks = this.Editor.Caret.isAtStart && !isFirstBlock;
|
||||
|
||||
if (!canMergeBlocks) {
|
||||
return;
|
||||
}
|
||||
|
||||
// preventing browser default behaviour
|
||||
event.preventDefault();
|
||||
|
||||
let targetBlock = BM.getBlockByIndex(BM.currentBlockIndex - 1),
|
||||
blockToMerge = BM.currentBlock;
|
||||
|
||||
/**
|
||||
* Blocks that can be merged:
|
||||
* 1) with the same Name
|
||||
* 2) Tool has 'merge' method
|
||||
*
|
||||
* @param {KeyboardEvent} event
|
||||
* other case will handle as usual ARROW LEFT behaviour
|
||||
*/
|
||||
blockKeydownsListener(event) {
|
||||
|
||||
switch(event.keyCode) {
|
||||
|
||||
case _.keyCodes.BACKSPACE:
|
||||
|
||||
_.log('Backspace key pressed');
|
||||
this.backspacePressed(event);
|
||||
break;
|
||||
|
||||
case _.keyCodes.ENTER:
|
||||
|
||||
_.log('Enter key pressed');
|
||||
this.enterPressed(event);
|
||||
break;
|
||||
|
||||
case _.keyCodes.DOWN:
|
||||
case _.keyCodes.RIGHT:
|
||||
|
||||
_.log('Right/Down key pressed');
|
||||
this.arrowRightAndDownPressed();
|
||||
break;
|
||||
|
||||
case _.keyCodes.UP:
|
||||
case _.keyCodes.LEFT:
|
||||
|
||||
_.log('Left/Up key pressed');
|
||||
this.arrowLeftAndUpPressed();
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
if (blockToMerge.name !== targetBlock.name || !targetBlock.mergeable) {
|
||||
BM.navigatePrevious();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle pressing enter key
|
||||
*
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
enterPressed(event) {
|
||||
let setCaretToTheEnd = !targetBlock.isEmpty ? true : false;
|
||||
|
||||
let currentBlock = this.Editor.BlockManager.currentBlock,
|
||||
toolsConfig = this.config.toolsConfig[currentBlock.name];
|
||||
BM.mergeBlocks(targetBlock, blockToMerge)
|
||||
.then( () => {
|
||||
window.setTimeout( () => {
|
||||
// set caret to the block without offset at the end
|
||||
this.Editor.Caret.setToBlock(BM.currentBlock, 0, setCaretToTheEnd);
|
||||
this.Editor.Toolbar.close();
|
||||
}, 10);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't handle Enter keydowns when Tool sets enableLineBreaks to true.
|
||||
* Uses for Tools like <code> where line breaks should be handled by default behaviour.
|
||||
*/
|
||||
if (toolsConfig && toolsConfig.enableLineBreaks) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow to create linebreaks by Shift+Enter
|
||||
*/
|
||||
if (event.shiftKey) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Split the Current Block into two blocks
|
||||
*/
|
||||
this.Editor.BlockManager.split();
|
||||
event.preventDefault();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle backspace keypress on block
|
||||
* @param {KeyboardEvent} event - keydown
|
||||
*/
|
||||
backspacePressed(event) {
|
||||
|
||||
const BM = this.Editor.BlockManager;
|
||||
|
||||
let isFirstBlock = BM.currentBlockIndex === 0,
|
||||
canMergeBlocks = this.Editor.Caret.isAtStart && !isFirstBlock;
|
||||
|
||||
if (!canMergeBlocks) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
// preventing browser default behaviour
|
||||
event.preventDefault();
|
||||
|
||||
let targetBlock = BM.getBlockByIndex(BM.currentBlockIndex - 1),
|
||||
blockToMerge = BM.currentBlock;
|
||||
|
||||
/**
|
||||
* Blocks that can be merged:
|
||||
* 1) with the same Name
|
||||
* 2) Tool has 'merge' method
|
||||
*
|
||||
* other case will handle as usual ARROW LEFT behaviour
|
||||
*/
|
||||
if (blockToMerge.name !== targetBlock.name || !targetBlock.mergeable) {
|
||||
|
||||
BM.navigatePrevious();
|
||||
|
||||
}
|
||||
|
||||
let setCaretToTheEnd = !targetBlock.isEmpty ? true : false;
|
||||
|
||||
BM.mergeBlocks(targetBlock, blockToMerge)
|
||||
.then( () => {
|
||||
|
||||
window.setTimeout( () => {
|
||||
|
||||
// set caret to the block without offset at the end
|
||||
this.Editor.Caret.setToBlock(BM.currentBlock, 0, setCaretToTheEnd);
|
||||
this.Editor.Toolbar.close();
|
||||
|
||||
}, 10);
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle right and down keyboard keys
|
||||
*/
|
||||
arrowRightAndDownPressed() {
|
||||
|
||||
this.Editor.BlockManager.navigateNext();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle left and up keyboard keys
|
||||
*/
|
||||
arrowLeftAndUpPressed() {
|
||||
|
||||
this.Editor.BlockManager.navigatePrevious();
|
||||
|
||||
}
|
||||
/**
|
||||
* Handle right and down keyboard keys
|
||||
*/
|
||||
arrowRightAndDownPressed() {
|
||||
this.Editor.BlockManager.navigateNext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle left and up keyboard keys
|
||||
*/
|
||||
arrowLeftAndUpPressed() {
|
||||
this.Editor.BlockManager.navigatePrevious();
|
||||
}
|
||||
}
|
|
@ -15,203 +15,164 @@
|
|||
*/
|
||||
|
||||
export default class Listeners extends Module {
|
||||
/**
|
||||
* @constructor
|
||||
* @param {EditorConfig} config
|
||||
*/
|
||||
constructor({config}) {
|
||||
super({config});
|
||||
this.allListeners = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {EditorConfig} config
|
||||
*/
|
||||
constructor({config}) {
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
|
||||
super({config});
|
||||
this.allListeners = [];
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
element.removeEventListener(eventType, handler, useCapture);
|
||||
}
|
||||
|
||||
let assignedEventData = {
|
||||
element,
|
||||
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 = [];
|
||||
|
||||
let alreadyExist = this.findOne(element, eventType, handler);
|
||||
|
||||
if (alreadyExist) return;
|
||||
|
||||
this.allListeners.push(assignedEventData);
|
||||
element.addEventListener(eventType, handler, useCapture);
|
||||
for (let i = 0; i < this.allListeners.length; i++) {
|
||||
let listener = this.allListeners[i];
|
||||
|
||||
if (listener.element === element) {
|
||||
listenersOnElement.push(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return listenersOnElement;
|
||||
}
|
||||
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search method: looks for listener by passed element
|
||||
* @param {Element} element - searching element
|
||||
* @returns {Array} listeners that found on element
|
||||
*/
|
||||
findByElement(element) {
|
||||
return listenersWithType;
|
||||
}
|
||||
|
||||
let listenersOnElement = [];
|
||||
/**
|
||||
* 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.element === element) {
|
||||
|
||||
listenersOnElement.push(listener);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return listenersOnElement;
|
||||
for (let i = 0; i < this.allListeners.length; i++) {
|
||||
let listener = this.allListeners[i];
|
||||
|
||||
if (listener.handler === handler) {
|
||||
listenersWithHandler.push(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search method: looks for listener by passed event type
|
||||
* @param {String} eventType
|
||||
* @return {Array} listeners that found on element
|
||||
*/
|
||||
findByType(eventType) {
|
||||
return listenersWithHandler;
|
||||
}
|
||||
|
||||
let listenersWithType = [];
|
||||
/**
|
||||
* @param {Element} element
|
||||
* @param {String} eventType
|
||||
* @param {Function} handler
|
||||
* @return {Element|null}
|
||||
*/
|
||||
findOne(element, eventType, handler) {
|
||||
let foundListeners = this.findAll(element, eventType, handler);
|
||||
|
||||
for (let i = 0; i < this.allListeners.length; i++) {
|
||||
return foundListeners.length > 0 ? foundListeners[0] : null;
|
||||
}
|
||||
|
||||
let listener = this.allListeners[i];
|
||||
/**
|
||||
* @param {Element} element
|
||||
* @param {String} eventType
|
||||
* @param {Function} handler
|
||||
* @return {Array}
|
||||
*/
|
||||
findAll(element, eventType, handler) {
|
||||
let foundAllListeners,
|
||||
foundByElements = [],
|
||||
foundByEventType = [],
|
||||
foundByHandler = [];
|
||||
|
||||
if (listener.type === eventType) {
|
||||
if (element)
|
||||
foundByElements = this.findByElement(element);
|
||||
|
||||
listenersWithType.push(listener);
|
||||
if (eventType)
|
||||
foundByEventType = this.findByType(eventType);
|
||||
|
||||
}
|
||||
if (handler)
|
||||
foundByHandler = this.findByHandler(handler);
|
||||
|
||||
}
|
||||
foundAllListeners = foundByElements.concat(foundByEventType, foundByHandler);
|
||||
|
||||
return listenersWithType;
|
||||
return foundAllListeners;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 foundAllListeners,
|
||||
foundByElements = [],
|
||||
foundByEventType = [],
|
||||
foundByHandler = [];
|
||||
|
||||
if (element)
|
||||
foundByElements = this.findByElement(element);
|
||||
|
||||
if (eventType)
|
||||
foundByEventType = this.findByType(eventType);
|
||||
|
||||
if (handler)
|
||||
foundByHandler = this.findByHandler(handler);
|
||||
|
||||
foundAllListeners = foundByElements.concat(foundByEventType, foundByHandler);
|
||||
|
||||
return foundAllListeners;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all listeners
|
||||
*/
|
||||
removeAll() {
|
||||
|
||||
this.allListeners.map( (current) => {
|
||||
|
||||
current.element.removeEventListener(current.eventType, current.handler);
|
||||
|
||||
});
|
||||
|
||||
this.allListeners = [];
|
||||
|
||||
}
|
||||
/**
|
||||
* Removes all listeners
|
||||
*/
|
||||
removeAll() {
|
||||
this.allListeners.map( (current) => {
|
||||
current.element.removeEventListener(current.eventType, current.handler);
|
||||
});
|
||||
|
||||
this.allListeners = [];
|
||||
}
|
||||
}
|
|
@ -7,81 +7,71 @@
|
|||
* @version 2.0.0
|
||||
*/
|
||||
export default class Renderer extends Module {
|
||||
/**
|
||||
* @constructor
|
||||
* @param {EditorConfig} config
|
||||
*/
|
||||
constructor({config}) {
|
||||
super({config});
|
||||
}
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {EditorConfig} config
|
||||
*/
|
||||
constructor({config}) {
|
||||
/**
|
||||
* @typedef {Object} RendererItems
|
||||
* @property {String} type - tool name
|
||||
* @property {Object} data - tool data
|
||||
*/
|
||||
|
||||
super({config});
|
||||
/**
|
||||
* @example
|
||||
*
|
||||
* items: [
|
||||
* {
|
||||
* type : 'paragraph',
|
||||
* data : {
|
||||
* text : 'Hello from Codex!'
|
||||
* }
|
||||
* },
|
||||
* {
|
||||
* type : 'paragraph',
|
||||
* data : {
|
||||
* text : 'Leave feedback if you like it!'
|
||||
* }
|
||||
* },
|
||||
* ]
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Make plugin blocks from array of plugin`s data
|
||||
* @param {RendererItems[]} items
|
||||
*/
|
||||
render(items) {
|
||||
let chainData = [];
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
chainData.push({
|
||||
function: () => this.insertBlock(items[i])
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} RendererItems
|
||||
* @property {String} type - tool name
|
||||
* @property {Object} data - tool data
|
||||
*/
|
||||
return _.sequence(chainData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @example
|
||||
*
|
||||
* items: [
|
||||
* {
|
||||
* type : 'paragraph',
|
||||
* data : {
|
||||
* text : 'Hello from Codex!'
|
||||
* }
|
||||
* },
|
||||
* {
|
||||
* type : 'paragraph',
|
||||
* data : {
|
||||
* text : 'Leave feedback if you like it!'
|
||||
* }
|
||||
* },
|
||||
* ]
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* Get plugin instance
|
||||
* Add plugin instance to BlockManager
|
||||
* Insert block to working zone
|
||||
*
|
||||
* @param {Object} item
|
||||
* @returns {Promise.<T>}
|
||||
* @private
|
||||
*/
|
||||
insertBlock(item) {
|
||||
let tool = item.type,
|
||||
data = item.data;
|
||||
|
||||
/**
|
||||
* Make plugin blocks from array of plugin`s data
|
||||
* @param {RendererItems[]} items
|
||||
*/
|
||||
render(items) {
|
||||
|
||||
let chainData = [];
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
|
||||
chainData.push({
|
||||
function: () => this.insertBlock(items[i])
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
return _.sequence(chainData);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plugin instance
|
||||
* Add plugin instance to BlockManager
|
||||
* Insert block to working zone
|
||||
*
|
||||
* @param {Object} item
|
||||
* @returns {Promise.<T>}
|
||||
* @private
|
||||
*/
|
||||
insertBlock(item) {
|
||||
|
||||
let tool = item.type,
|
||||
data = item.data;
|
||||
|
||||
this.Editor.BlockManager.insert(tool, data);
|
||||
|
||||
return Promise.resolve();
|
||||
|
||||
}
|
||||
this.Editor.BlockManager.insert(tool, data);
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
|
@ -33,112 +33,91 @@
|
|||
* }
|
||||
*/
|
||||
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});
|
||||
|
||||
/**
|
||||
* 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}) {
|
||||
// default config
|
||||
this.defaultConfig = null;
|
||||
this._sanitizerInstance = null;
|
||||
|
||||
super({config});
|
||||
/** Custom configuration */
|
||||
this.sanitizerConfig = config.settings ? config.settings.sanitizer : {};
|
||||
|
||||
// default config
|
||||
this.defaultConfig = null;
|
||||
this._sanitizerInstance = null;
|
||||
/** HTML Janitor library */
|
||||
this.sanitizerInstance = require('html-janitor');
|
||||
}
|
||||
|
||||
/** Custom configuration */
|
||||
this.sanitizerConfig = config.settings ? config.settings.sanitizer : {};
|
||||
|
||||
/** 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) {
|
||||
|
||||
this._sanitizerInstance = new library(this.defaultConfig);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets sanitizer configuration. Uses default config if user didn't pass the restriction
|
||||
* @param {SanitizerConfig} config
|
||||
*/
|
||||
set sanitizerConfig(config) {
|
||||
|
||||
if (_.isEmpty(config)) {
|
||||
|
||||
this.defaultConfig = {
|
||||
tags: {
|
||||
p: {},
|
||||
a: {
|
||||
href: true,
|
||||
target: '_blank',
|
||||
rel: 'nofollow'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} else {
|
||||
|
||||
this.defaultConfig = config;
|
||||
/**
|
||||
* 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) {
|
||||
this._sanitizerInstance = new library(this.defaultConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets sanitizer configuration. Uses default config if user didn't pass the restriction
|
||||
* @param {SanitizerConfig} config
|
||||
*/
|
||||
set sanitizerConfig(config) {
|
||||
if (_.isEmpty(config)) {
|
||||
this.defaultConfig = {
|
||||
tags: {
|
||||
p: {},
|
||||
a: {
|
||||
href: true,
|
||||
target: '_blank',
|
||||
rel: 'nofollow'
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
} else {
|
||||
this.defaultConfig = config;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (_.isEmpty(customConfig)) {
|
||||
|
||||
return this._sanitizerInstance.clean(taintString);
|
||||
|
||||
} else {
|
||||
|
||||
return Sanitizer.clean(taintString, customConfig);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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 (_.isEmpty(customConfig)) {
|
||||
return this._sanitizerInstance.clean(taintString);
|
||||
} else {
|
||||
return Sanitizer.clean(taintString, customConfig);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = Sanitizer(customConfig);
|
||||
|
||||
return newInstance.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 = Sanitizer(customConfig);
|
||||
|
||||
return newInstance.clean(taintString);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,77 +22,63 @@
|
|||
*/
|
||||
|
||||
export default class Saver extends Module {
|
||||
/**
|
||||
* @constructor
|
||||
* @param config
|
||||
*/
|
||||
constructor({config}) {
|
||||
super({config});
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param config
|
||||
*/
|
||||
constructor({config}) {
|
||||
this.output = null;
|
||||
this.blocksData = [];
|
||||
}
|
||||
|
||||
super({config});
|
||||
/**
|
||||
* Composes new chain of Promises to fire them alternatelly
|
||||
* @return {SavedData}
|
||||
*/
|
||||
save() {
|
||||
let blocks = this.Editor.BlockManager.blocks,
|
||||
chainData = [];
|
||||
|
||||
this.output = null;
|
||||
this.blocksData = [];
|
||||
blocks.forEach((block) => {
|
||||
chainData.push(block.data);
|
||||
});
|
||||
|
||||
}
|
||||
return Promise.all(chainData)
|
||||
.then((allExtractedData) => this.makeOutput(allExtractedData))
|
||||
.then((outputData) => {
|
||||
return outputData;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Composes new chain of Promises to fire them alternatelly
|
||||
* @return {SavedData}
|
||||
*/
|
||||
save() {
|
||||
/**
|
||||
* Creates output object with saved data, time and version of editor
|
||||
* @param {Object} allExtractedData
|
||||
* @return {SavedData}
|
||||
*/
|
||||
makeOutput(allExtractedData) {
|
||||
let items = [],
|
||||
totalTime = 0;
|
||||
|
||||
let blocks = this.Editor.BlockManager.blocks,
|
||||
chainData = [];
|
||||
console.groupCollapsed('[CodexEditor saving]:');
|
||||
|
||||
blocks.forEach((block) => {
|
||||
allExtractedData.forEach((extraction) => {
|
||||
/** Group process info */
|
||||
console.log(`«${extraction.tool}» saving info`, extraction);
|
||||
totalTime += extraction.time;
|
||||
items.push(extraction.data);
|
||||
});
|
||||
|
||||
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 items = [],
|
||||
totalTime = 0;
|
||||
|
||||
console.groupCollapsed('[CodexEditor saving]:');
|
||||
|
||||
allExtractedData.forEach((extraction, index) => {
|
||||
|
||||
/** Group process info */
|
||||
console.log(`«${extraction.tool}» saving info`, extraction);
|
||||
totalTime += extraction.time;
|
||||
items.push(extraction.data);
|
||||
|
||||
});
|
||||
|
||||
console.log('Total', totalTime);
|
||||
console.groupEnd();
|
||||
|
||||
return {
|
||||
time : +new Date(),
|
||||
items : items,
|
||||
version : VERSION,
|
||||
};
|
||||
|
||||
}
|
||||
console.log('Total', totalTime);
|
||||
console.groupEnd();
|
||||
|
||||
return {
|
||||
time : +new Date(),
|
||||
items : items,
|
||||
version : VERSION,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// module.exports = (function (saver) {
|
||||
|
|
|
@ -10,132 +10,111 @@
|
|||
* |________________________|
|
||||
*/
|
||||
export default class BlockSettings extends Module {
|
||||
constructor({config}) {
|
||||
super({config});
|
||||
|
||||
constructor({config}) {
|
||||
this.nodes = {
|
||||
wrapper: null,
|
||||
toolSettings: null,
|
||||
defaultSettings: null,
|
||||
buttonRemove: null
|
||||
};
|
||||
}
|
||||
|
||||
super({config});
|
||||
/**
|
||||
* Block Settings CSS
|
||||
* @return {{wrapper, wrapperOpened, toolSettings, defaultSettings, button}}
|
||||
*/
|
||||
static get CSS() {
|
||||
return {
|
||||
// Settings Panel
|
||||
wrapper: 'ce-settings',
|
||||
wrapperOpened: 'ce-settings--opened',
|
||||
toolSettings: 'ce-settings__plugin-zone',
|
||||
defaultSettings: 'ce-settings__default-zone',
|
||||
|
||||
this.nodes = {
|
||||
wrapper: null,
|
||||
toolSettings: null,
|
||||
defaultSettings: null,
|
||||
buttonRemove: null
|
||||
};
|
||||
button: 'ce-settings__button'
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Panel with block settings with 2 sections:
|
||||
* - Tool's Settings
|
||||
* - Default Settings [Move, Remove, etc]
|
||||
*
|
||||
* @return {Element}
|
||||
*/
|
||||
make() {
|
||||
this.nodes.wrapper = $.make('div', BlockSettings.CSS.wrapper);
|
||||
|
||||
this.nodes.toolSettings = $.make('div', BlockSettings.CSS.toolSettings);
|
||||
this.nodes.defaultSettings = $.make('div', BlockSettings.CSS.defaultSettings);
|
||||
|
||||
$.append(this.nodes.wrapper, [this.nodes.toolSettings, this.nodes.defaultSettings]);
|
||||
|
||||
/**
|
||||
* Block Settings CSS
|
||||
* @return {{wrapper, wrapperOpened, toolSettings, defaultSettings, button}}
|
||||
* Add default settings that presents for all Blocks
|
||||
*/
|
||||
static get CSS() {
|
||||
this.addDefaultSettings();
|
||||
}
|
||||
|
||||
return {
|
||||
// Settings Panel
|
||||
wrapper: 'ce-settings',
|
||||
wrapperOpened: 'ce-settings--opened',
|
||||
toolSettings: 'ce-settings__plugin-zone',
|
||||
defaultSettings: 'ce-settings__default-zone',
|
||||
/**
|
||||
* Add Tool's settings
|
||||
*/
|
||||
addToolSettings() {
|
||||
console.log('Block Settings: add settings for ',
|
||||
this.Editor.BlockManager.currentBlock
|
||||
);
|
||||
}
|
||||
|
||||
button: 'ce-settings__button'
|
||||
};
|
||||
/**
|
||||
* Add default settings
|
||||
*/
|
||||
addDefaultSettings() {
|
||||
/**
|
||||
* Remove Block Button
|
||||
* --------------------------------------------
|
||||
*/
|
||||
this.nodes.buttonRemove = $.make('div', BlockSettings.CSS.button, {
|
||||
textContent: 'Remove Block'
|
||||
});
|
||||
|
||||
}
|
||||
$.append(this.nodes.defaultSettings, this.nodes.buttonRemove);
|
||||
|
||||
this.Editor.Listeners.on(this.nodes.buttonRemove, 'click', (event) => this.removeBlockButtonClicked(event));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the Remove Block Button
|
||||
*/
|
||||
removeBlockButtonClicked() {
|
||||
console.log('❇️ Remove Block Button clicked');
|
||||
}
|
||||
|
||||
/**
|
||||
* Is Block Settings opened or not
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get opened() {
|
||||
return this.nodes.wrapper.classList.contains(BlockSettings.CSS.wrapperOpened);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open Block Settings pane
|
||||
*/
|
||||
open() {
|
||||
this.nodes.wrapper.classList.add(BlockSettings.CSS.wrapperOpened);
|
||||
|
||||
/**
|
||||
* Panel with block settings with 2 sections:
|
||||
* - Tool's Settings
|
||||
* - Default Settings [Move, Remove, etc]
|
||||
*
|
||||
* @return {Element}
|
||||
* Fill Tool's settings
|
||||
*/
|
||||
make() {
|
||||
|
||||
this.nodes.wrapper = $.make('div', BlockSettings.CSS.wrapper);
|
||||
|
||||
this.nodes.toolSettings = $.make('div', BlockSettings.CSS.toolSettings);
|
||||
this.nodes.defaultSettings = $.make('div', BlockSettings.CSS.defaultSettings);
|
||||
|
||||
$.append(this.nodes.wrapper, [this.nodes.toolSettings, this.nodes.defaultSettings]);
|
||||
|
||||
/**
|
||||
* Add default settings that presents for all Blocks
|
||||
*/
|
||||
this.addDefaultSettings();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Tool's settings
|
||||
*/
|
||||
addToolSettings() {
|
||||
|
||||
console.log('Block Settings: add settings for ',
|
||||
this.Editor.BlockManager.currentBlock
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Add default settings
|
||||
*/
|
||||
addDefaultSettings() {
|
||||
|
||||
/**
|
||||
* Remove Block Button
|
||||
* --------------------------------------------
|
||||
*/
|
||||
this.nodes.buttonRemove = $.make('div', BlockSettings.CSS.button, {
|
||||
textContent: 'Remove Block'
|
||||
});
|
||||
|
||||
$.append(this.nodes.defaultSettings, this.nodes.buttonRemove);
|
||||
|
||||
this.Editor.Listeners.on(this.nodes.buttonRemove, 'click', (event) => this.removeBlockButtonClicked(event));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the Remove Block Button
|
||||
*/
|
||||
removeBlockButtonClicked() {
|
||||
|
||||
console.log('❇️ Remove Block Button clicked');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Is Block Settings opened or not
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get opened() {
|
||||
|
||||
return this.nodes.wrapper.classList.contains(BlockSettings.CSS.wrapperOpened);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Open Block Settings pane
|
||||
*/
|
||||
open() {
|
||||
|
||||
this.nodes.wrapper.classList.add(BlockSettings.CSS.wrapperOpened);
|
||||
|
||||
/**
|
||||
* Fill Tool's settings
|
||||
*/
|
||||
this.addToolSettings();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Close Block Settings pane
|
||||
*/
|
||||
close() {
|
||||
|
||||
this.nodes.wrapper.classList.remove(BlockSettings.CSS.wrapperOpened);
|
||||
|
||||
}
|
||||
this.addToolSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close Block Settings pane
|
||||
*/
|
||||
close() {
|
||||
this.nodes.wrapper.classList.remove(BlockSettings.CSS.wrapperOpened);
|
||||
}
|
||||
}
|
||||
|
|
68
src/components/modules/toolbar-inline.ts
Normal file
68
src/components/modules/toolbar-inline.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
/**
|
||||
* Inline toolbar with actions that modifies selected text fragment
|
||||
*
|
||||
* ________________________
|
||||
* | |
|
||||
* | B i [link] [mark] |
|
||||
* | _______________________|
|
||||
*/
|
||||
declare var Module: any;
|
||||
declare var $: any;
|
||||
|
||||
/**
|
||||
* DOM Elements
|
||||
*/
|
||||
interface InlineToolbarNodes {
|
||||
wrapper?: Element; // main wrapper
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS
|
||||
*/
|
||||
interface InlineToolbarCSS {
|
||||
inlineToolbar: string;
|
||||
}
|
||||
|
||||
export default class InlineToolbar extends Module {
|
||||
|
||||
/**
|
||||
* Inline Toolbar elements
|
||||
*/
|
||||
private nodes: InlineToolbarNodes = {
|
||||
wrapper: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* CSS styles
|
||||
*/
|
||||
private CSS: InlineToolbarCSS = {
|
||||
inlineToolbar: 'ce-inline-toolbar',
|
||||
};
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
constructor({config}) {
|
||||
|
||||
super({config});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Making DOM
|
||||
*/
|
||||
public make() {
|
||||
|
||||
this.nodes.wrapper = $.make('div', this.CSS.inlineToolbar);
|
||||
|
||||
/**
|
||||
* Append Inline Toolbar to the Editor
|
||||
*/
|
||||
$.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper);
|
||||
|
||||
}
|
||||
|
||||
public move() {
|
||||
// moving
|
||||
}
|
||||
}
|
|
@ -9,215 +9,179 @@
|
|||
*
|
||||
*/
|
||||
export default class Toolbox extends Module {
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
constructor({config}) {
|
||||
super({config});
|
||||
|
||||
this.nodes = {
|
||||
toolbox: null,
|
||||
buttons: []
|
||||
};
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* Opening state
|
||||
* @type {boolean}
|
||||
*/
|
||||
constructor({config}) {
|
||||
this.opened = false;
|
||||
}
|
||||
|
||||
super({config});
|
||||
/**
|
||||
* CSS styles
|
||||
* @return {{toolbox: string, toolboxButton: string, toolboxOpened: string}}
|
||||
*/
|
||||
static get CSS() {
|
||||
return {
|
||||
toolbox: 'ce-toolbox',
|
||||
toolboxButton: 'ce-toolbox__button',
|
||||
toolboxOpened: 'ce-toolbox--opened',
|
||||
};
|
||||
}
|
||||
|
||||
this.nodes = {
|
||||
toolbox: null,
|
||||
buttons: []
|
||||
};
|
||||
/**
|
||||
* Makes the Toolbox
|
||||
*/
|
||||
make() {
|
||||
this.nodes.toolbox = $.make('div', Toolbox.CSS.toolbox);
|
||||
$.append(this.Editor.Toolbar.nodes.content, this.nodes.toolbox);
|
||||
|
||||
/**
|
||||
* Opening state
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.opened = false;
|
||||
this.addTools();
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates available tools and appends them to the Toolbox
|
||||
*/
|
||||
addTools() {
|
||||
let tools = this.Editor.Tools.toolsAvailable;
|
||||
|
||||
for (let toolName in tools) {
|
||||
this.addTool(toolName, tools[toolName]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append Tool to the Toolbox
|
||||
*
|
||||
* @param {string} toolName - tool name
|
||||
* @param {Tool} tool - tool class
|
||||
*/
|
||||
addTool(toolName, tool) {
|
||||
if (tool.displayInToolbox && !tool.iconClassName) {
|
||||
_.log('Toolbar icon class name is missed. Tool %o skipped', 'warn', toolName);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS styles
|
||||
* @return {{toolbox: string, toolboxButton: string, toolboxOpened: string}}
|
||||
* @todo Add checkup for the render method
|
||||
*/
|
||||
static get CSS() {
|
||||
// if (typeof tool.render !== 'function') {
|
||||
//
|
||||
// _.log('render method missed. Tool %o skipped', 'warn', tool);
|
||||
// return;
|
||||
//
|
||||
// }
|
||||
|
||||
return {
|
||||
toolbox: 'ce-toolbox',
|
||||
toolboxButton: 'ce-toolbox__button',
|
||||
toolboxOpened: 'ce-toolbox--opened',
|
||||
};
|
||||
/**
|
||||
* Skip tools that pass 'displayInToolbox=false'
|
||||
*/
|
||||
if (!tool.displayInToolbox) {
|
||||
return;
|
||||
}
|
||||
|
||||
let button = $.make('li', [Toolbox.CSS.toolboxButton, tool.iconClassName], {
|
||||
title: toolName
|
||||
});
|
||||
|
||||
/**
|
||||
* Save tool's name in the button data-name
|
||||
*/
|
||||
button.dataset.name = toolName;
|
||||
|
||||
$.append(this.nodes.toolbox, button);
|
||||
|
||||
this.nodes.toolbox.appendChild(button);
|
||||
this.nodes.buttons.push(button);
|
||||
|
||||
/**
|
||||
* @todo add event with module Listeners
|
||||
*/
|
||||
// this.Editor.Listeners.add();
|
||||
button.addEventListener('click', event => {
|
||||
this.buttonClicked(event);
|
||||
}, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toolbox button click listener
|
||||
* 1) if block is empty -> replace
|
||||
* 2) if block is not empty -> add new block below
|
||||
*
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
buttonClicked(event) {
|
||||
let toolButton = event.target,
|
||||
toolName = toolButton.dataset.name,
|
||||
tool = this.Editor.Tools.toolClasses[toolName];
|
||||
|
||||
/**
|
||||
* @type {Block}
|
||||
*/
|
||||
let currentBlock = this.Editor.BlockManager.currentBlock;
|
||||
|
||||
/**
|
||||
* We do replace if:
|
||||
* - block is empty
|
||||
* - block is not irreplaceable
|
||||
* @type {Array}
|
||||
*/
|
||||
if (!tool.irreplaceable && currentBlock.isEmpty) {
|
||||
this.Editor.BlockManager.replace(toolName);
|
||||
} else {
|
||||
this.Editor.BlockManager.insert(toolName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the Toolbox
|
||||
* @todo set caret to the new block
|
||||
*/
|
||||
make() {
|
||||
|
||||
this.nodes.toolbox = $.make('div', Toolbox.CSS.toolbox);
|
||||
$.append(this.Editor.Toolbar.nodes.content, this.nodes.toolbox);
|
||||
// window.setTimeout(function () {
|
||||
|
||||
this.addTools();
|
||||
/** Set caret to current block */
|
||||
// editor.caret.setToBlock(currentInputIndex);
|
||||
|
||||
}
|
||||
// }, 10);
|
||||
|
||||
/**
|
||||
* Iterates available tools and appends them to the Toolbox
|
||||
* Move toolbar when node is changed
|
||||
*/
|
||||
addTools() {
|
||||
this.Editor.Toolbar.move();
|
||||
}
|
||||
|
||||
let tools = this.Editor.Tools.toolsAvailable;
|
||||
/**
|
||||
* Open Toolbox with Tools
|
||||
*/
|
||||
open() {
|
||||
this.nodes.toolbox.classList.add(Toolbox.CSS.toolboxOpened);
|
||||
this.opened = true;
|
||||
}
|
||||
|
||||
for (let toolName in tools) {
|
||||
|
||||
this.addTool(toolName, tools[toolName]);
|
||||
|
||||
}
|
||||
/**
|
||||
* Close Toolbox
|
||||
*/
|
||||
close() {
|
||||
this.nodes.toolbox.classList.remove(Toolbox.CSS.toolboxOpened);
|
||||
this.opened = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close Toolbox
|
||||
*/
|
||||
toggle() {
|
||||
if (!this.opened) {
|
||||
this.open();
|
||||
} else {
|
||||
this.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Append Tool to the Toolbox
|
||||
*
|
||||
* @param {string} toolName - tool name
|
||||
* @param {Tool} tool - tool class
|
||||
*/
|
||||
addTool(toolName, tool) {
|
||||
|
||||
if (tool.displayInToolbox && !tool.iconClassName) {
|
||||
|
||||
_.log('Toolbar icon class name is missed. Tool %o skipped', 'warn', toolName);
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Add checkup for the render method
|
||||
*/
|
||||
// if (typeof tool.render !== 'function') {
|
||||
//
|
||||
// _.log('render method missed. Tool %o skipped', 'warn', tool);
|
||||
// return;
|
||||
//
|
||||
// }
|
||||
|
||||
/**
|
||||
* Skip tools that pass 'displayInToolbox=false'
|
||||
*/
|
||||
if (!tool.displayInToolbox) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
let button = $.make('li', [Toolbox.CSS.toolboxButton, tool.iconClassName], {
|
||||
title: toolName
|
||||
});
|
||||
|
||||
/**
|
||||
* Save tool's name in the button data-name
|
||||
*/
|
||||
button.dataset.name = toolName;
|
||||
|
||||
$.append(this.nodes.toolbox, button);
|
||||
|
||||
this.nodes.toolbox.appendChild(button);
|
||||
this.nodes.buttons.push(button);
|
||||
|
||||
/**
|
||||
* @todo add event with module Listeners
|
||||
*/
|
||||
// this.Editor.Listeners.add();
|
||||
button.addEventListener('click', event => {
|
||||
|
||||
this.buttonClicked(event);
|
||||
|
||||
}, false);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Toolbox button click listener
|
||||
* 1) if block is empty -> replace
|
||||
* 2) if block is not empty -> add new block below
|
||||
*
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
buttonClicked(event) {
|
||||
|
||||
let toolButton = event.target,
|
||||
toolName = toolButton.dataset.name,
|
||||
tool = this.Editor.Tools.toolClasses[toolName];
|
||||
|
||||
/**
|
||||
* @type {Block}
|
||||
*/
|
||||
let currentBlock = this.Editor.BlockManager.currentBlock;
|
||||
|
||||
/**
|
||||
* We do replace if:
|
||||
* - block is empty
|
||||
* - block is not irreplaceable
|
||||
* @type {Array}
|
||||
*/
|
||||
if (!tool.irreplaceable && currentBlock.isEmpty) {
|
||||
|
||||
this.Editor.BlockManager.replace(toolName);
|
||||
|
||||
} else {
|
||||
|
||||
this.Editor.BlockManager.insert(toolName);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo set caret to the new block
|
||||
*/
|
||||
|
||||
// window.setTimeout(function () {
|
||||
|
||||
/** Set caret to current block */
|
||||
// editor.caret.setToBlock(currentInputIndex);
|
||||
|
||||
// }, 10);
|
||||
|
||||
/**
|
||||
* Move toolbar when node is changed
|
||||
*/
|
||||
this.Editor.Toolbar.move();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Open Toolbox with Tools
|
||||
*/
|
||||
open() {
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Close Toolbox
|
||||
*/
|
||||
toggle() {
|
||||
|
||||
if (!this.opened) {
|
||||
|
||||
this.open();
|
||||
|
||||
} else {
|
||||
|
||||
this.close();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -50,225 +50,193 @@
|
|||
* @property {Element} nodes.defaultSettings - Default Settings section of Settings Panel
|
||||
*/
|
||||
export default class Toolbar extends Module {
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
constructor({config}) {
|
||||
super({config});
|
||||
|
||||
this.nodes = {
|
||||
wrapper : null,
|
||||
content : null,
|
||||
actions : null,
|
||||
|
||||
// Content Zone
|
||||
plusButton : null,
|
||||
|
||||
// Actions Zone
|
||||
blockActionsButtons: null,
|
||||
settingsToggler : null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS styles
|
||||
* @return {Object}
|
||||
* @constructor
|
||||
*/
|
||||
static get CSS() {
|
||||
return {
|
||||
toolbar: 'ce-toolbar',
|
||||
content: 'ce-toolbar__content',
|
||||
actions: 'ce-toolbar__actions',
|
||||
|
||||
toolbarOpened: 'ce-toolbar--opened',
|
||||
|
||||
// Content Zone
|
||||
plusButton: 'ce-toolbar__plus',
|
||||
plusButtonHidden: 'ce-toolbar__plus--hidden',
|
||||
|
||||
// Actions Zone
|
||||
blockActionsButtons: 'ce-toolbar__actions-buttons',
|
||||
settingsToggler: 'ce-toolbar__settings-btn',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes toolbar
|
||||
*/
|
||||
make() {
|
||||
this.nodes.wrapper = $.make('div', Toolbar.CSS.toolbar);
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* Make Content Zone and Actions Zone
|
||||
*/
|
||||
constructor({config}) {
|
||||
['content', 'actions'].forEach( el => {
|
||||
this.nodes[el] = $.make('div', Toolbar.CSS[el]);
|
||||
$.append(this.nodes.wrapper, this.nodes[el]);
|
||||
});
|
||||
|
||||
super({config});
|
||||
|
||||
this.nodes = {
|
||||
wrapper : null,
|
||||
content : null,
|
||||
actions : null,
|
||||
/**
|
||||
* Fill Content Zone:
|
||||
* - Plus Button
|
||||
* - Toolbox
|
||||
*/
|
||||
this.nodes.plusButton = $.make('div', Toolbar.CSS.plusButton);
|
||||
$.append(this.nodes.content, this.nodes.plusButton);
|
||||
this.nodes.plusButton.addEventListener('click', event => this.plusButtonClicked(event), false);
|
||||
|
||||
// Content Zone
|
||||
plusButton : null,
|
||||
|
||||
// Actions Zone
|
||||
blockActionsButtons: null,
|
||||
settingsToggler : null,
|
||||
};
|
||||
/**
|
||||
* Make a Toolbox
|
||||
*/
|
||||
this.Editor.Toolbox.make();
|
||||
|
||||
/**
|
||||
* Fill Actions Zone:
|
||||
* - Settings Toggler
|
||||
* - Remove Block Button
|
||||
* - Settings Panel
|
||||
*/
|
||||
this.nodes.blockActionsButtons = $.make('div', Toolbar.CSS.blockActionsButtons);
|
||||
this.nodes.settingsToggler = $.make('span', Toolbar.CSS.settingsToggler);
|
||||
|
||||
$.append(this.nodes.blockActionsButtons, this.nodes.settingsToggler);
|
||||
$.append(this.nodes.actions, this.nodes.blockActionsButtons);
|
||||
|
||||
/**
|
||||
* Make and append Settings Panel
|
||||
*/
|
||||
this.Editor.BlockSettings.make();
|
||||
$.append(this.nodes.actions, this.Editor.BlockSettings.nodes.wrapper);
|
||||
|
||||
/**
|
||||
* Append toolbar to the Editor
|
||||
*/
|
||||
$.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper);
|
||||
|
||||
/**
|
||||
* Bind events on the Toolbar elements
|
||||
*/
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Move Toolbar to the Current Block
|
||||
*/
|
||||
move() {
|
||||
/** Close Toolbox when we move toolbar */
|
||||
this.Editor.Toolbox.close();
|
||||
|
||||
let currentNode = this.Editor.BlockManager.currentNode;
|
||||
|
||||
/**
|
||||
* If no one Block selected as a Current
|
||||
*/
|
||||
if (!currentNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS styles
|
||||
* @return {Object}
|
||||
* @constructor
|
||||
* @todo Compute dynamically on prepare
|
||||
* @type {number}
|
||||
*/
|
||||
static get CSS() {
|
||||
const defaultToolbarHeight = 49;
|
||||
const defaultOffset = 34;
|
||||
|
||||
return {
|
||||
toolbar: 'ce-toolbar',
|
||||
content: 'ce-toolbar__content',
|
||||
actions: 'ce-toolbar__actions',
|
||||
var newYCoordinate = currentNode.offsetTop - (defaultToolbarHeight / 2) + defaultOffset;
|
||||
|
||||
toolbarOpened: 'ce-toolbar--opened',
|
||||
this.nodes.wrapper.style.transform = `translate3D(0, ${Math.floor(newYCoordinate)}px, 0)`;
|
||||
|
||||
// Content Zone
|
||||
plusButton: 'ce-toolbar__plus',
|
||||
plusButtonHidden: 'ce-toolbar__plus--hidden',
|
||||
/** Close trash actions */
|
||||
// editor.toolbar.settings.hideRemoveActions();
|
||||
}
|
||||
|
||||
// Actions Zone
|
||||
blockActionsButtons: 'ce-toolbar__actions-buttons',
|
||||
settingsToggler: 'ce-toolbar__settings-btn',
|
||||
};
|
||||
/**
|
||||
* Open Toolbar with Plus Button
|
||||
*/
|
||||
open() {
|
||||
this.nodes.wrapper.classList.add(Toolbar.CSS.toolbarOpened);
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Close the Toolbar
|
||||
*/
|
||||
close() {
|
||||
this.nodes.wrapper.classList.remove(Toolbar.CSS.toolbarOpened);
|
||||
}
|
||||
|
||||
/**
|
||||
* Plus Button public methods
|
||||
* @return {{hide: function(): void, show: function(): void}}
|
||||
*/
|
||||
get plusButton() {
|
||||
return {
|
||||
hide: () => this.nodes.plusButton.classList.add(Toolbar.CSS.plusButtonHidden),
|
||||
show: () => this.nodes.plusButton.classList.remove(Toolbar.CSS.plusButtonHidden)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for Plus Button
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
plusButtonClicked() {
|
||||
this.Editor.Toolbox.toggle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind events on the Toolbar Elements:
|
||||
* - Block Settings
|
||||
*/
|
||||
bindEvents() {
|
||||
/**
|
||||
* Makes toolbar
|
||||
* Settings toggler
|
||||
*/
|
||||
make() {
|
||||
|
||||
this.nodes.wrapper = $.make('div', Toolbar.CSS.toolbar);
|
||||
|
||||
/**
|
||||
* Make Content Zone and Actions Zone
|
||||
*/
|
||||
['content', 'actions'].forEach( el => {
|
||||
|
||||
this.nodes[el] = $.make('div', Toolbar.CSS[el]);
|
||||
$.append(this.nodes.wrapper, this.nodes[el]);
|
||||
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Fill Content Zone:
|
||||
* - Plus Button
|
||||
* - Toolbox
|
||||
*/
|
||||
this.nodes.plusButton = $.make('div', Toolbar.CSS.plusButton);
|
||||
$.append(this.nodes.content, this.nodes.plusButton);
|
||||
this.nodes.plusButton.addEventListener('click', event => this.plusButtonClicked(event), false);
|
||||
|
||||
|
||||
/**
|
||||
* Make a Toolbox
|
||||
*/
|
||||
this.Editor.Toolbox.make();
|
||||
|
||||
/**
|
||||
* Fill Actions Zone:
|
||||
* - Settings Toggler
|
||||
* - Remove Block Button
|
||||
* - Settings Panel
|
||||
*/
|
||||
this.nodes.blockActionsButtons = $.make('div', Toolbar.CSS.blockActionsButtons);
|
||||
this.nodes.settingsToggler = $.make('span', Toolbar.CSS.settingsToggler);
|
||||
|
||||
$.append(this.nodes.blockActionsButtons, this.nodes.settingsToggler);
|
||||
$.append(this.nodes.actions, this.nodes.blockActionsButtons);
|
||||
|
||||
/**
|
||||
* Make and append Settings Panel
|
||||
*/
|
||||
this.Editor.BlockSettings.make();
|
||||
$.append(this.nodes.actions, this.Editor.BlockSettings.nodes.wrapper);
|
||||
|
||||
/**
|
||||
* Append toolbar to the Editor
|
||||
*/
|
||||
$.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper);
|
||||
|
||||
/**
|
||||
* Bind events on the Toolbar elements
|
||||
*/
|
||||
this.bindEvents();
|
||||
this.Editor.Listeners.on(this.nodes.settingsToggler, 'click', (event) => {
|
||||
this.settingsTogglerClicked(event);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the Block Settings toggler
|
||||
*/
|
||||
settingsTogglerClicked() {
|
||||
if (this.Editor.BlockSettings.opened) {
|
||||
this.Editor.BlockSettings.close();
|
||||
} else {
|
||||
this.Editor.BlockSettings.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* Move Toolbar to the Current Block
|
||||
*/
|
||||
move() {
|
||||
|
||||
/** Close Toolbox when we move toolbar */
|
||||
this.Editor.Toolbox.close();
|
||||
|
||||
let currentNode = this.Editor.BlockManager.currentNode;
|
||||
|
||||
/**
|
||||
* If no one Block selected as a Current
|
||||
*/
|
||||
if (!currentNode) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Compute dynamically on prepare
|
||||
* @type {number}
|
||||
*/
|
||||
const defaultToolbarHeight = 49;
|
||||
const defaultOffset = 34;
|
||||
|
||||
var newYCoordinate = currentNode.offsetTop - (defaultToolbarHeight / 2) + defaultOffset;
|
||||
|
||||
this.nodes.wrapper.style.transform = `translate3D(0, ${Math.floor(newYCoordinate)}px, 0)`;
|
||||
|
||||
/** Close trash actions */
|
||||
// editor.toolbar.settings.hideRemoveActions();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Open Toolbar with Plus Button
|
||||
*/
|
||||
open() {
|
||||
|
||||
this.nodes.wrapper.classList.add(Toolbar.CSS.toolbarOpened);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the Toolbar
|
||||
*/
|
||||
close() {
|
||||
|
||||
this.nodes.wrapper.classList.remove(Toolbar.CSS.toolbarOpened);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Plus Button public methods
|
||||
* @return {{hide: function(): void, show: function(): void}}
|
||||
*/
|
||||
get plusButton() {
|
||||
|
||||
return {
|
||||
hide: () => this.nodes.plusButton.classList.add(Toolbar.CSS.plusButtonHidden),
|
||||
show: () => this.nodes.plusButton.classList.remove(Toolbar.CSS.plusButtonHidden)
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for Plus Button
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
plusButtonClicked() {
|
||||
|
||||
this.Editor.Toolbox.toggle();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind events on the Toolbar Elements:
|
||||
* - Block Settings
|
||||
*/
|
||||
bindEvents() {
|
||||
|
||||
/**
|
||||
* Settings toggler
|
||||
*/
|
||||
this.Editor.Listeners.on(this.nodes.settingsToggler, 'click', (event) => {
|
||||
|
||||
this.settingsTogglerClicked(event);
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the Block Settings toggler
|
||||
*/
|
||||
settingsTogglerClicked() {
|
||||
|
||||
if (this.Editor.BlockSettings.opened) {
|
||||
|
||||
this.Editor.BlockSettings.close();
|
||||
|
||||
} else {
|
||||
|
||||
this.Editor.BlockSettings.open();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -41,212 +41,172 @@
|
|||
* @property {EditorConfig} config - Editor config
|
||||
*/
|
||||
export default class Tools extends Module {
|
||||
/**
|
||||
* Returns available Tools
|
||||
* @return {Tool[]}
|
||||
*/
|
||||
get available() {
|
||||
return this.toolsAvailable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns unavailable Tools
|
||||
* @return {Tool[]}
|
||||
*/
|
||||
get unavailable() {
|
||||
return this.toolsUnavailable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static getter for default Tool config fields
|
||||
*
|
||||
* @usage Tools.defaultConfig.displayInToolbox
|
||||
* @return {ToolConfig}
|
||||
*/
|
||||
static get defaultConfig() {
|
||||
return {
|
||||
iconClassName : '',
|
||||
displayInToolbox : false,
|
||||
enableLineBreaks : false,
|
||||
irreplaceable : false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*
|
||||
* @param {EditorConfig} config
|
||||
*/
|
||||
constructor({config}) {
|
||||
super({config});
|
||||
|
||||
/**
|
||||
* Returns available Tools
|
||||
* @return {Tool[]}
|
||||
* Map {name: Class, ...} where:
|
||||
* name — block type name in JSON. Got from EditorConfig.tools keys
|
||||
* @type {Object}
|
||||
*/
|
||||
get available() {
|
||||
this.toolClasses = {};
|
||||
|
||||
return this.toolsAvailable;
|
||||
/**
|
||||
* Available tools list
|
||||
* {name: Class, ...}
|
||||
* @type {Object}
|
||||
*/
|
||||
this.toolsAvailable = {};
|
||||
|
||||
/**
|
||||
* Tools that rejected a prepare method
|
||||
* {name: Class, ... }
|
||||
* @type {Object}
|
||||
*/
|
||||
this.toolsUnavailable = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates instances via passed or default configuration
|
||||
* @return {Promise}
|
||||
*/
|
||||
prepare() {
|
||||
if (!this.config.hasOwnProperty('tools')) {
|
||||
return Promise.reject("Can't start without tools");
|
||||
}
|
||||
|
||||
for(let toolName in this.config.tools) {
|
||||
this.toolClasses[toolName] = this.config.tools[toolName];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns unavailable Tools
|
||||
* @return {Tool[]}
|
||||
* getting classes that has prepare method
|
||||
*/
|
||||
get unavailable() {
|
||||
|
||||
return this.toolsUnavailable;
|
||||
let sequenceData = this.getListOfPrepareFunctions();
|
||||
|
||||
/**
|
||||
* if sequence data contains nothing then resolve current chain and run other module prepare
|
||||
*/
|
||||
if (sequenceData.length === 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Static getter for default Tool config fields
|
||||
*
|
||||
* @usage Tools.defaultConfig.displayInToolbox
|
||||
* @return {ToolConfig}
|
||||
* to see how it works {@link Util#sequence}
|
||||
*/
|
||||
static get defaultConfig() {
|
||||
return _.sequence(sequenceData, (data) => {
|
||||
this.success(data);
|
||||
}, (data) => {
|
||||
this.fallback(data);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
iconClassName : '',
|
||||
displayInToolbox : false,
|
||||
enableLineBreaks : false,
|
||||
irreplaceable : false
|
||||
};
|
||||
/**
|
||||
* Binds prepare function of plugins with user or default config
|
||||
* @return {Array} list of functions that needs to be fired sequentially
|
||||
*/
|
||||
getListOfPrepareFunctions() {
|
||||
let toolPreparationList = [];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @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.toolClasses = {};
|
||||
|
||||
/**
|
||||
* Available tools list
|
||||
* {name: Class, ...}
|
||||
* @type {Object}
|
||||
*/
|
||||
this.toolsAvailable = {};
|
||||
|
||||
/**
|
||||
* Tools that rejected a prepare method
|
||||
* {name: Class, ... }
|
||||
* @type {Object}
|
||||
*/
|
||||
this.toolsUnavailable = {};
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates instances via passed or default configuration
|
||||
* @return {Promise}
|
||||
*/
|
||||
prepare() {
|
||||
|
||||
if (!this.config.hasOwnProperty('tools')) {
|
||||
|
||||
return Promise.reject("Can't start without tools");
|
||||
|
||||
}
|
||||
|
||||
for(let toolName in this.config.tools) {
|
||||
|
||||
this.toolClasses[toolName] = this.config.tools[toolName];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* getting classes that has prepare method
|
||||
*/
|
||||
let sequenceData = this.getListOfPrepareFunctions();
|
||||
|
||||
/**
|
||||
* if sequence data contains nothing then resolve current chain and run other module prepare
|
||||
*/
|
||||
if (sequenceData.length === 0) {
|
||||
|
||||
return Promise.resolve();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* to see how it works {@link Util#sequence}
|
||||
*/
|
||||
return _.sequence(sequenceData, (data) => {
|
||||
|
||||
this.success(data);
|
||||
|
||||
}, (data) => {
|
||||
|
||||
this.fallback(data);
|
||||
for(let toolName in this.toolClasses) {
|
||||
let toolClass = this.toolClasses[toolName];
|
||||
|
||||
if (typeof toolClass.prepare === 'function') {
|
||||
toolPreparationList.push({
|
||||
function : toolClass.prepare,
|
||||
data : {
|
||||
toolName
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
/**
|
||||
* If Tool hasn't a prepare method, mark it as available
|
||||
*/
|
||||
this.toolsAvailable[toolName] = toolClass;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds prepare function of plugins with user or default config
|
||||
* @return {Array} list of functions that needs to be fired sequentially
|
||||
*/
|
||||
getListOfPrepareFunctions() {
|
||||
return toolPreparationList;
|
||||
}
|
||||
|
||||
let toolPreparationList = [];
|
||||
/**
|
||||
* @param {ChainData.data} data - append tool to available list
|
||||
*/
|
||||
success(data) {
|
||||
this.toolsAvailable[data.toolName] = this.toolClasses[data.toolName];
|
||||
}
|
||||
|
||||
for(let toolName in this.toolClasses) {
|
||||
/**
|
||||
* @param {ChainData.data} data - append tool to unavailable list
|
||||
*/
|
||||
fallback(data) {
|
||||
this.toolsUnavailable[data.toolName] = this.toolClasses[data.toolName];
|
||||
}
|
||||
|
||||
let toolClass = this.toolClasses[toolName];
|
||||
|
||||
if (typeof toolClass.prepare === 'function') {
|
||||
|
||||
toolPreparationList.push({
|
||||
function : toolClass.prepare,
|
||||
data : {
|
||||
toolName
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
/**
|
||||
* If Tool hasn't a prepare method, mark it as available
|
||||
*/
|
||||
this.toolsAvailable[toolName] = toolClass;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return toolPreparationList;
|
||||
/**
|
||||
* Return tool`a instance
|
||||
*
|
||||
* @param {String} tool — tool name
|
||||
* @param {Object} data — initial data
|
||||
*
|
||||
* @todo throw exceptions if tool doesnt exist
|
||||
*
|
||||
*/
|
||||
construct(tool, data) {
|
||||
let plugin = this.toolClasses[tool],
|
||||
config = this.config.toolsConfig[tool];
|
||||
|
||||
if (!config) {
|
||||
config = this.defaultConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ChainData.data} data - append tool to available list
|
||||
*/
|
||||
success(data) {
|
||||
let instance = new plugin(data, config);
|
||||
|
||||
this.toolsAvailable[data.toolName] = this.toolClasses[data.toolName];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ChainData.data} data - append tool to unavailable list
|
||||
*/
|
||||
fallback(data) {
|
||||
|
||||
this.toolsUnavailable[data.toolName] = this.toolClasses[data.toolName];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Return tool`a instance
|
||||
*
|
||||
* @param {String} tool — tool name
|
||||
* @param {Object} data — initial data
|
||||
*
|
||||
* @todo throw exceptions if tool doesnt exist
|
||||
*
|
||||
*/
|
||||
construct(tool, data) {
|
||||
|
||||
let plugin = this.toolClasses[tool],
|
||||
config = this.config.toolsConfig[tool];
|
||||
|
||||
if (!config) {
|
||||
|
||||
config = this.defaultConfig;
|
||||
|
||||
}
|
||||
|
||||
let instance = new plugin(data, config);
|
||||
|
||||
return instance;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if passed Tool is an instance of Initial Block Tool
|
||||
* @param {Tool} tool - Tool to check
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isInitial(tool) {
|
||||
|
||||
return tool instanceof this.available[this.config.initialBlock];
|
||||
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if passed Tool is an instance of Initial Block Tool
|
||||
* @param {Tool} tool - Tool to check
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isInitial(tool) {
|
||||
return tool instanceof this.available[this.config.initialBlock];
|
||||
}
|
||||
}
|
|
@ -6,28 +6,28 @@
|
|||
// let className = {
|
||||
|
||||
/**
|
||||
* @const {string} BLOCK_CLASSNAME - redactor blocks name
|
||||
*/
|
||||
* @const {string} BLOCK_CLASSNAME - redactor blocks name
|
||||
*/
|
||||
// BLOCK_CLASSNAME : 'ce-block',
|
||||
|
||||
/**
|
||||
* @const {String} wrapper for plugins content
|
||||
*/
|
||||
* @const {String} wrapper for plugins content
|
||||
*/
|
||||
// BLOCK_CONTENT : 'ce-block__content',
|
||||
|
||||
/**
|
||||
* @const {String} BLOCK_STRETCHED - makes block stretched
|
||||
*/
|
||||
* @const {String} BLOCK_STRETCHED - makes block stretched
|
||||
*/
|
||||
// BLOCK_STRETCHED : 'ce-block--stretched',
|
||||
|
||||
/**
|
||||
* @const {String} BLOCK_HIGHLIGHTED - adds background
|
||||
*/
|
||||
* @const {String} BLOCK_HIGHLIGHTED - adds background
|
||||
*/
|
||||
// BLOCK_HIGHLIGHTED : 'ce-block--focused',
|
||||
|
||||
/**
|
||||
* @const {String} - for all default settings
|
||||
*/
|
||||
* @const {String} - for all default settings
|
||||
*/
|
||||
// SETTINGS_ITEM : 'ce-settings__item'
|
||||
// };
|
||||
|
||||
|
@ -52,305 +52,279 @@
|
|||
* @property {Element} nodes.redactor - <ce-redactor>
|
||||
*/
|
||||
export default class UI extends Module {
|
||||
/**
|
||||
* @constructor
|
||||
*
|
||||
* @param {EditorConfig} config
|
||||
*/
|
||||
constructor({config}) {
|
||||
super({config});
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*
|
||||
* @param {EditorConfig} config
|
||||
*/
|
||||
constructor({config}) {
|
||||
this.nodes = {
|
||||
holder: null,
|
||||
wrapper: null,
|
||||
redactor: null
|
||||
};
|
||||
}
|
||||
|
||||
super({config});
|
||||
/**
|
||||
* Making main interface
|
||||
*/
|
||||
prepare() {
|
||||
return this.make()
|
||||
/**
|
||||
* Make toolbar
|
||||
*/
|
||||
.then(() => this.Editor.Toolbar.make())
|
||||
/**
|
||||
* Make the Inline toolbar
|
||||
*/
|
||||
.then(() => this.Editor.InlineToolbar.make())
|
||||
/**
|
||||
* Load and append CSS
|
||||
*/
|
||||
.then(() => this.loadStyles())
|
||||
/**
|
||||
* Bind events for the UI elements
|
||||
*/
|
||||
.then(() => this.bindEvents())
|
||||
|
||||
this.nodes = {
|
||||
holder: null,
|
||||
wrapper: null,
|
||||
redactor: null
|
||||
};
|
||||
/** Make container for inline toolbar */
|
||||
// .then(makeInlineToolbar_)
|
||||
|
||||
}
|
||||
/** Add inline toolbar tools */
|
||||
// .then(addInlineToolbarTools_)
|
||||
|
||||
/**
|
||||
* Making main interface
|
||||
*/
|
||||
prepare() {
|
||||
/** Draw wrapper for notifications */
|
||||
// .then(makeNotificationHolder_)
|
||||
|
||||
// this.Editor.Toolbar.make();
|
||||
/** Add eventlisteners to redactor elements */
|
||||
// .then(bindEvents_)
|
||||
|
||||
return this.make()
|
||||
/**
|
||||
* Make toolbar
|
||||
*/
|
||||
.then(() => this.Editor.Toolbar.make())
|
||||
/**
|
||||
* Load and append CSS
|
||||
*/
|
||||
.then(() => this.loadStyles())
|
||||
/**
|
||||
* Bind events for the UI elements
|
||||
*/
|
||||
.then(() => this.bindEvents())
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
|
||||
/** Make container for inline toolbar */
|
||||
// .then(makeInlineToolbar_)
|
||||
// editor.core.log("Can't draw editor interface");
|
||||
});
|
||||
}
|
||||
|
||||
/** Add inline toolbar tools */
|
||||
// .then(addInlineToolbarTools_)
|
||||
|
||||
/** Draw wrapper for notifications */
|
||||
// .then(makeNotificationHolder_)
|
||||
|
||||
/** Add eventlisteners to redactor elements */
|
||||
// .then(bindEvents_)
|
||||
|
||||
.catch(e => {
|
||||
|
||||
console.error(e);
|
||||
|
||||
// editor.core.log("Can't draw editor interface");
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* CodeX Editor UI CSS class names
|
||||
* @return {{editorWrapper: string, editorZone: string, block: string}}
|
||||
*/
|
||||
get CSS() {
|
||||
get CSS() {
|
||||
return {
|
||||
editorWrapper : 'codex-editor',
|
||||
editorZone : 'codex-editor__redactor',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
editorWrapper : 'codex-editor',
|
||||
editorZone : 'codex-editor__redactor',
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Makes CodeX Editor interface
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
make() {
|
||||
make() {
|
||||
return new Promise( (resolve, reject) => {
|
||||
/**
|
||||
* Element where we need to append CodeX Editor
|
||||
* @type {Element}
|
||||
*/
|
||||
this.nodes.holder = document.getElementById(this.config.holderId);
|
||||
|
||||
return new Promise( (resolve, reject) => {
|
||||
if (!this.nodes.holder) {
|
||||
reject(Error("Holder wasn't found by ID: #" + this.config.holderId));
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Element where we need to append CodeX Editor
|
||||
* @type {Element}
|
||||
*/
|
||||
this.nodes.holder = document.getElementById(this.config.holderId);
|
||||
/**
|
||||
* Create and save main UI elements
|
||||
*/
|
||||
this.nodes.wrapper = $.make('div', this.CSS.editorWrapper);
|
||||
this.nodes.redactor = $.make('div', this.CSS.editorZone);
|
||||
|
||||
if (!this.nodes.holder) {
|
||||
this.nodes.wrapper.appendChild(this.nodes.redactor);
|
||||
this.nodes.holder.appendChild(this.nodes.wrapper);
|
||||
|
||||
reject(Error("Holder wasn't found by ID: #" + this.config.holderId));
|
||||
return;
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and save main UI elements
|
||||
*/
|
||||
this.nodes.wrapper = $.make('div', this.CSS.editorWrapper);
|
||||
this.nodes.redactor = $.make('div', this.CSS.editorZone);
|
||||
|
||||
this.nodes.wrapper.appendChild(this.nodes.redactor);
|
||||
this.nodes.holder.appendChild(this.nodes.wrapper);
|
||||
|
||||
resolve();
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
/**
|
||||
* Appends CSS
|
||||
*/
|
||||
loadStyles() {
|
||||
/**
|
||||
* Load CSS
|
||||
*/
|
||||
let styles = require('../../styles/main.css');
|
||||
|
||||
/**
|
||||
* Appends CSS
|
||||
* Make tag
|
||||
*/
|
||||
loadStyles() {
|
||||
|
||||
/**
|
||||
* Load CSS
|
||||
*/
|
||||
let styles = require('../../styles/main.css');
|
||||
|
||||
/**
|
||||
* Make tag
|
||||
*/
|
||||
let tag = $.make('style', null, {
|
||||
textContent: styles.toString()
|
||||
});
|
||||
|
||||
/**
|
||||
* Append styles
|
||||
*/
|
||||
$.append(document.head, tag);
|
||||
|
||||
}
|
||||
let tag = $.make('style', null, {
|
||||
textContent: styles.toString()
|
||||
});
|
||||
|
||||
/**
|
||||
* Bind events on the CodeX Editor interface
|
||||
* Append styles
|
||||
*/
|
||||
bindEvents() {
|
||||
$.append(document.head, tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo bind events with the Listeners module
|
||||
*/
|
||||
this.Editor.Listeners.on(this.nodes.redactor, 'click', event => this.redactorClicked(event), false );
|
||||
/**
|
||||
* Bind events on the CodeX Editor interface
|
||||
*/
|
||||
bindEvents() {
|
||||
/**
|
||||
* @todo bind events with the Listeners module
|
||||
*/
|
||||
this.Editor.Listeners.on(this.nodes.redactor, 'click', event => this.redactorClicked(event), false );
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* All clicks on the redactor zone
|
||||
*
|
||||
* @param {MouseEvent} event
|
||||
*
|
||||
* @description
|
||||
* 1. Save clicked Block as a current {@link BlockManager#currentNode}
|
||||
* it uses for the following:
|
||||
* - add CSS modifier for the selected Block
|
||||
* - on Enter press, we make a new Block under that
|
||||
*
|
||||
* 2. Move and show the Toolbar
|
||||
*
|
||||
* 3. Set a Caret
|
||||
*
|
||||
* 4. By clicks on the Editor's bottom zone:
|
||||
* - if last Block is empty, set a Caret to this
|
||||
* - otherwise, add a new empty Block and set a Caret to that
|
||||
*
|
||||
* 5. Hide the Inline Toolbar
|
||||
*
|
||||
* @see selectClickedBlock
|
||||
*
|
||||
*/
|
||||
redactorClicked(event) {
|
||||
let clickedNode = event.target;
|
||||
|
||||
/**
|
||||
* All clicks on the redactor zone
|
||||
*
|
||||
* @param {MouseEvent} event
|
||||
*
|
||||
* @description
|
||||
* 1. Save clicked Block as a current {@link BlockManager#currentNode}
|
||||
* it uses for the following:
|
||||
* - add CSS modifier for the selected Block
|
||||
* - on Enter press, we make a new Block under that
|
||||
*
|
||||
* 2. Move and show the Toolbar
|
||||
*
|
||||
* 3. Set a Caret
|
||||
*
|
||||
* 4. By clicks on the Editor's bottom zone:
|
||||
* - if last Block is empty, set a Caret to this
|
||||
* - otherwise, add a new empty Block and set a Caret to that
|
||||
*
|
||||
* 5. Hide the Inline Toolbar
|
||||
*
|
||||
* @see selectClickedBlock
|
||||
*
|
||||
* Select clicked Block as Current
|
||||
*/
|
||||
redactorClicked(event) {
|
||||
|
||||
let clickedNode = event.target;
|
||||
|
||||
/**
|
||||
* Select clicked Block as Current
|
||||
*/
|
||||
try {
|
||||
|
||||
this.Editor.BlockManager.setCurrentBlockByChildNode(clickedNode);
|
||||
|
||||
} catch (e) {
|
||||
|
||||
/**
|
||||
* If clicked outside first-level Blocks, set Caret to the last empty Block
|
||||
*/
|
||||
this.Editor.Caret.setToTheLastBlock();
|
||||
|
||||
}
|
||||
try {
|
||||
this.Editor.BlockManager.setCurrentBlockByChildNode(clickedNode);
|
||||
} catch (e) {
|
||||
/**
|
||||
* If clicked outside first-level Blocks, set Caret to the last empty Block
|
||||
*/
|
||||
this.Editor.Caret.setToTheLastBlock();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @todo hide the Inline Toolbar
|
||||
*/
|
||||
// var selectedText = editor.toolbar.inline.getSelectionText(),
|
||||
// firstLevelBlock;
|
||||
/**
|
||||
* @todo hide the Inline Toolbar
|
||||
*/
|
||||
// var selectedText = editor.toolbar.inline.getSelectionText(),
|
||||
// firstLevelBlock;
|
||||
|
||||
/** If selection range took off, then we hide inline toolbar */
|
||||
// if (selectedText.length === 0) {
|
||||
/** If selection range took off, then we hide inline toolbar */
|
||||
// if (selectedText.length === 0) {
|
||||
|
||||
// editor.toolbar.inline.close();
|
||||
// editor.toolbar.inline.close();
|
||||
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
/**
|
||||
*
|
||||
|
||||
/** Update current input index in memory when caret focused into existed input */
|
||||
// if (event.target.contentEditable == 'true') {
|
||||
//
|
||||
// editor.caret.saveCurrentInputIndex();
|
||||
//
|
||||
// }
|
||||
// if (event.target.contentEditable == 'true') {
|
||||
//
|
||||
// editor.caret.saveCurrentInputIndex();
|
||||
//
|
||||
// }
|
||||
|
||||
// if (editor.content.currentNode === null) {
|
||||
//
|
||||
// /**
|
||||
// * If inputs in redactor does not exits, then we put input index 0 not -1
|
||||
// */
|
||||
// var indexOfLastInput = editor.state.inputs.length > 0 ? editor.state.inputs.length - 1 : 0;
|
||||
//
|
||||
// /** If we have any inputs */
|
||||
// if (editor.state.inputs.length) {
|
||||
//
|
||||
// /** getting firstlevel parent of input */
|
||||
// firstLevelBlock = editor.content.getFirstLevelBlock(editor.state.inputs[indexOfLastInput]);
|
||||
//
|
||||
// }
|
||||
//
|
||||
// /** If input is empty, then we set caret to the last input */
|
||||
// if (editor.state.inputs.length && editor.state.inputs[indexOfLastInput].textContent === '' && firstLevelBlock.dataset.tool == editor.settings.initialBlockPlugin) {
|
||||
//
|
||||
// editor.caret.setToBlock(indexOfLastInput);
|
||||
//
|
||||
// } else {
|
||||
//
|
||||
// /** Create new input when caret clicked in redactors area */
|
||||
// var NEW_BLOCK_TYPE = editor.settings.initialBlockPlugin;
|
||||
//
|
||||
// editor.content.insertBlock({
|
||||
// type : NEW_BLOCK_TYPE,
|
||||
// block : editor.tools[NEW_BLOCK_TYPE].render()
|
||||
// });
|
||||
//
|
||||
// /** If there is no inputs except inserted */
|
||||
// if (editor.state.inputs.length === 1) {
|
||||
//
|
||||
// editor.caret.setToBlock(indexOfLastInput);
|
||||
//
|
||||
// } else {
|
||||
//
|
||||
// /** Set caret to this appended input */
|
||||
// editor.caret.setToNextBlock(indexOfLastInput);
|
||||
//
|
||||
// }
|
||||
//
|
||||
// }
|
||||
//
|
||||
// } else {
|
||||
//
|
||||
// /** Close all panels */
|
||||
// editor.toolbar.settings.close();
|
||||
// editor.toolbar.toolbox.close();
|
||||
//
|
||||
// }
|
||||
//
|
||||
/**
|
||||
* Move toolbar and open
|
||||
*/
|
||||
this.Editor.Toolbar.move();
|
||||
this.Editor.Toolbar.open();
|
||||
//
|
||||
// var inputIsEmpty = !editor.content.currentNode.textContent.trim(),
|
||||
// currentNodeType = editor.content.currentNode.dataset.tool,
|
||||
// isInitialType = currentNodeType == editor.settings.initialBlockPlugin;
|
||||
//
|
||||
//
|
||||
// if (editor.content.currentNode === null) {
|
||||
//
|
||||
// /**
|
||||
// * If inputs in redactor does not exits, then we put input index 0 not -1
|
||||
// */
|
||||
// var indexOfLastInput = editor.state.inputs.length > 0 ? editor.state.inputs.length - 1 : 0;
|
||||
//
|
||||
// /** If we have any inputs */
|
||||
// if (editor.state.inputs.length) {
|
||||
//
|
||||
// /** getting firstlevel parent of input */
|
||||
// firstLevelBlock = editor.content.getFirstLevelBlock(editor.state.inputs[indexOfLastInput]);
|
||||
//
|
||||
// }
|
||||
//
|
||||
// /** If input is empty, then we set caret to the last input */
|
||||
// if (editor.state.inputs.length && editor.state.inputs[indexOfLastInput].textContent === '' && firstLevelBlock.dataset.tool == editor.settings.initialBlockPlugin) {
|
||||
//
|
||||
// editor.caret.setToBlock(indexOfLastInput);
|
||||
//
|
||||
// } else {
|
||||
//
|
||||
// /** Create new input when caret clicked in redactors area */
|
||||
// var NEW_BLOCK_TYPE = editor.settings.initialBlockPlugin;
|
||||
//
|
||||
// editor.content.insertBlock({
|
||||
// type : NEW_BLOCK_TYPE,
|
||||
// block : editor.tools[NEW_BLOCK_TYPE].render()
|
||||
// });
|
||||
//
|
||||
// /** If there is no inputs except inserted */
|
||||
// if (editor.state.inputs.length === 1) {
|
||||
//
|
||||
// editor.caret.setToBlock(indexOfLastInput);
|
||||
//
|
||||
// } else {
|
||||
//
|
||||
// /** Set caret to this appended input */
|
||||
// editor.caret.setToNextBlock(indexOfLastInput);
|
||||
//
|
||||
// }
|
||||
//
|
||||
// }
|
||||
//
|
||||
// } else {
|
||||
//
|
||||
// /** Close all panels */
|
||||
// editor.toolbar.settings.close();
|
||||
// editor.toolbar.toolbox.close();
|
||||
//
|
||||
// }
|
||||
//
|
||||
/**
|
||||
* Move toolbar and open
|
||||
*/
|
||||
this.Editor.Toolbar.move();
|
||||
this.Editor.Toolbar.open();
|
||||
//
|
||||
// var inputIsEmpty = !editor.content.currentNode.textContent.trim(),
|
||||
// currentNodeType = editor.content.currentNode.dataset.tool,
|
||||
// isInitialType = currentNodeType == editor.settings.initialBlockPlugin;
|
||||
//
|
||||
//
|
||||
|
||||
/**
|
||||
* Hide the Plus Button
|
||||
* */
|
||||
this.Editor.Toolbar.plusButton.hide();
|
||||
/**
|
||||
* Hide the Plus Button
|
||||
* */
|
||||
this.Editor.Toolbar.plusButton.hide();
|
||||
|
||||
/**
|
||||
* Show the Plus Button if:
|
||||
* - Block is an initial-block (Text)
|
||||
* - Block is empty
|
||||
*/
|
||||
let isInitialBlock = this.Editor.Tools.isInitial(this.Editor.BlockManager.currentBlock.tool),
|
||||
isEmptyBlock = this.Editor.BlockManager.currentBlock.isEmpty;
|
||||
|
||||
if (isInitialBlock && isEmptyBlock) {
|
||||
|
||||
this.Editor.Toolbar.plusButton.show();
|
||||
|
||||
}
|
||||
/**
|
||||
* Show the Plus Button if:
|
||||
* - Block is an initial-block (Text)
|
||||
* - Block is empty
|
||||
*/
|
||||
let isInitialBlock = this.Editor.Tools.isInitial(this.Editor.BlockManager.currentBlock.tool),
|
||||
isEmptyBlock = this.Editor.BlockManager.currentBlock.isEmpty;
|
||||
|
||||
if (isInitialBlock && isEmptyBlock) {
|
||||
this.Editor.Toolbar.plusButton.show();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// /**
|
||||
|
|
|
@ -4,21 +4,17 @@
|
|||
* https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
|
||||
*/
|
||||
if (!Element.prototype.matches)
|
||||
Element.prototype.matches = Element.prototype.msMatchesSelector ||
|
||||
Element.prototype.matches = Element.prototype.msMatchesSelector ||
|
||||
Element.prototype.webkitMatchesSelector;
|
||||
|
||||
if (!Element.prototype.closest)
|
||||
Element.prototype.closest = function (s) {
|
||||
Element.prototype.closest = function (s) {
|
||||
var el = this;
|
||||
|
||||
var el = this;
|
||||
|
||||
if (!document.documentElement.contains(el)) return null;
|
||||
do {
|
||||
|
||||
if (el.matches(s)) return el;
|
||||
el = el.parentElement || el.parentNode;
|
||||
|
||||
} while (el !== null);
|
||||
return null;
|
||||
|
||||
};
|
||||
if (!document.documentElement.contains(el)) return null;
|
||||
do {
|
||||
if (el.matches(s)) return el;
|
||||
el = el.parentElement || el.parentNode;
|
||||
} while (el !== null);
|
||||
return null;
|
||||
};
|
||||
|
|
|
@ -2,64 +2,52 @@
|
|||
* Working with selection
|
||||
*/
|
||||
export default class Selection {
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
constructor() {
|
||||
this.instance = null;
|
||||
this.selection = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
constructor() {
|
||||
/**
|
||||
* Returns window Selection
|
||||
* {@link https://developer.mozilla.org/ru/docs/Web/API/Window/getSelection}
|
||||
* @return {Selection}
|
||||
*/
|
||||
static get() {
|
||||
return window.getSelection();
|
||||
}
|
||||
|
||||
this.instance = null;
|
||||
this.selection = null;
|
||||
/**
|
||||
* Returns selected anchor
|
||||
* {@link https://developer.mozilla.org/ru/docs/Web/API/Selection/anchorNode}
|
||||
* @return {Node|null}
|
||||
*/
|
||||
static getAnchorNode() {
|
||||
let selection = window.getSelection();
|
||||
|
||||
}
|
||||
return selection ? selection.anchorNode : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns window Selection
|
||||
* {@link https://developer.mozilla.org/ru/docs/Web/API/Window/getSelection}
|
||||
* @return {Selection}
|
||||
*/
|
||||
static get() {
|
||||
/**
|
||||
* Returns selection offset according to the anchor node
|
||||
* {@link https://developer.mozilla.org/ru/docs/Web/API/Selection/anchorOffset}
|
||||
* @return {Number|null}
|
||||
*/
|
||||
static getAnchorOffset() {
|
||||
let selection = window.getSelection();
|
||||
|
||||
return window.getSelection();
|
||||
return selection ? selection.anchorOffset : null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns selected anchor
|
||||
* {@link https://developer.mozilla.org/ru/docs/Web/API/Selection/anchorNode}
|
||||
* @return {Node|null}
|
||||
*/
|
||||
static getAnchorNode() {
|
||||
|
||||
let selection = window.getSelection();
|
||||
|
||||
return selection ? selection.anchorNode : null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns selection offset according to the anchor node
|
||||
* {@link https://developer.mozilla.org/ru/docs/Web/API/Selection/anchorOffset}
|
||||
* @return {Number|null}
|
||||
*/
|
||||
static getAnchorOffset() {
|
||||
|
||||
let selection = window.getSelection();
|
||||
|
||||
return selection ? selection.anchorOffset : null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Is current selection range collapsed
|
||||
* @return {boolean|null}
|
||||
*/
|
||||
static get isCollapsed() {
|
||||
|
||||
let selection = window.getSelection();
|
||||
|
||||
return selection ? selection.isCollapsed : null;
|
||||
|
||||
}
|
||||
/**
|
||||
* Is current selection range collapsed
|
||||
* @return {boolean|null}
|
||||
*/
|
||||
static get isCollapsed() {
|
||||
let selection = window.getSelection();
|
||||
|
||||
return selection ? selection.isCollapsed : null;
|
||||
}
|
||||
}
|
|
@ -2,214 +2,170 @@
|
|||
* Codex Editor Util
|
||||
*/
|
||||
export default class Util {
|
||||
/**
|
||||
* Custom logger
|
||||
*
|
||||
* @param {string} msg - message
|
||||
* @param {string} type - logging type 'log'|'warn'|'error'|'info'
|
||||
* @param {*} args - argument to log with a message
|
||||
*/
|
||||
static log(msg, type, args) {
|
||||
type = type || 'log';
|
||||
|
||||
/**
|
||||
* Custom logger
|
||||
*
|
||||
* @param {string} msg - message
|
||||
* @param {string} type - logging type 'log'|'warn'|'error'|'info'
|
||||
* @param {*} args - argument to log with a message
|
||||
*/
|
||||
static log(msg, type, args) {
|
||||
if (!args) {
|
||||
args = msg || 'undefined';
|
||||
msg = '[codex-editor]: %o';
|
||||
} else {
|
||||
msg = '[codex-editor]: ' + msg;
|
||||
}
|
||||
|
||||
type = type || 'log';
|
||||
try{
|
||||
if ( 'console' in window && window.console[ type ] ) {
|
||||
if ( args ) window.console[ type ]( msg, args );
|
||||
else window.console[ type ]( msg );
|
||||
}
|
||||
} catch(e) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
if (!args) {
|
||||
/**
|
||||
* Returns basic keycodes as constants
|
||||
* @return {{}}
|
||||
*/
|
||||
static get keyCodes() {
|
||||
return {
|
||||
BACKSPACE: 8,
|
||||
TAB: 9,
|
||||
ENTER: 13,
|
||||
SHIFT: 16,
|
||||
CTRL: 17,
|
||||
ALT: 18,
|
||||
ESC: 27,
|
||||
SPACE: 32,
|
||||
LEFT: 37,
|
||||
UP: 38,
|
||||
DOWN: 40,
|
||||
RIGHT: 39,
|
||||
DELETE: 46,
|
||||
META: 91
|
||||
};
|
||||
}
|
||||
|
||||
args = msg || 'undefined';
|
||||
msg = '[codex-editor]: %o';
|
||||
|
||||
} else {
|
||||
|
||||
msg = '[codex-editor]: ' + msg;
|
||||
|
||||
}
|
||||
|
||||
try{
|
||||
|
||||
if ( 'console' in window && window.console[ type ] ) {
|
||||
|
||||
if ( args ) window.console[ type ]( msg, args );
|
||||
else window.console[ type ]( msg );
|
||||
/**
|
||||
* @typedef {Object} ChainData
|
||||
* @property {Object} data - data that will be passed to the success or fallback
|
||||
* @property {Function} function - function's that must be called asynchronically
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires a promise sequence asyncronically
|
||||
*
|
||||
* @param {Object[]} chains - list or ChainData's
|
||||
* @param {Function} success - success callback
|
||||
* @param {Function} fallback - callback that fires in case of errors
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
static sequence(chains, success = () => {}, fallback = () => {}) {
|
||||
return new Promise(function (resolve) {
|
||||
/**
|
||||
* pluck each element from queue
|
||||
* First, send resolved Promise as previous value
|
||||
* Each plugins "prepare" method returns a Promise, that's why
|
||||
* reduce current element will not be able to continue while can't get
|
||||
* a resolved Promise
|
||||
*/
|
||||
chains.reduce(function (previousValue, currentValue, iteration) {
|
||||
return previousValue
|
||||
.then(() => waitNextBlock(currentValue, success, fallback))
|
||||
.then(() => {
|
||||
// finished
|
||||
if (iteration === chains.length - 1) {
|
||||
resolve();
|
||||
}
|
||||
|
||||
} catch(e) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
}, Promise.resolve());
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns basic keycodes as constants
|
||||
* @return {{}}
|
||||
*/
|
||||
static get keyCodes() {
|
||||
|
||||
return {
|
||||
BACKSPACE: 8,
|
||||
TAB: 9,
|
||||
ENTER: 13,
|
||||
SHIFT: 16,
|
||||
CTRL: 17,
|
||||
ALT: 18,
|
||||
ESC: 27,
|
||||
SPACE: 32,
|
||||
LEFT: 37,
|
||||
UP: 38,
|
||||
DOWN: 40,
|
||||
RIGHT: 39,
|
||||
DELETE: 46,
|
||||
META: 91
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} ChainData
|
||||
* @property {Object} data - data that will be passed to the success or fallback
|
||||
* @property {Function} function - function's that must be called asynchronically
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires a promise sequence asyncronically
|
||||
* Decorator
|
||||
*
|
||||
* @param {Object[]} chains - list or ChainData's
|
||||
* @param {Function} success - success callback
|
||||
* @param {Function} fallback - callback that fires in case of errors
|
||||
* @param {ChainData} chainData
|
||||
*
|
||||
* @param {Function} successCallback
|
||||
* @param {Function} fallbackCallback
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
static sequence(chains, success = () => {}, fallback = () => {}) {
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
|
||||
/**
|
||||
* pluck each element from queue
|
||||
* First, send resolved Promise as previous value
|
||||
* Each plugins "prepare" method returns a Promise, that's why
|
||||
* reduce current element will not be able to continue while can't get
|
||||
* a resolved Promise
|
||||
*/
|
||||
chains.reduce(function (previousValue, currentValue, iteration) {
|
||||
|
||||
return previousValue
|
||||
.then(() => waitNextBlock(currentValue, success, fallback))
|
||||
.then(() => {
|
||||
|
||||
// finished
|
||||
if (iteration === chains.length - 1) {
|
||||
|
||||
resolve();
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}, Promise.resolve());
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* Decorator
|
||||
*
|
||||
* @param {ChainData} chainData
|
||||
*
|
||||
* @param {Function} successCallback
|
||||
* @param {Function} fallbackCallback
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
function waitNextBlock(chainData, successCallback, fallbackCallback) {
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
|
||||
chainData.function()
|
||||
.then(() => {
|
||||
|
||||
successCallback(chainData.data || {});
|
||||
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(function () {
|
||||
|
||||
fallbackCallback(chainData.data || {});
|
||||
|
||||
// anyway, go ahead even it falls
|
||||
resolve();
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
function waitNextBlock(chainData, successCallback, fallbackCallback) {
|
||||
return new Promise(function (resolve) {
|
||||
chainData.function()
|
||||
.then(() => {
|
||||
successCallback(chainData.data || {});
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(function () {
|
||||
fallbackCallback(chainData.data || {});
|
||||
|
||||
// anyway, go ahead even it falls
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make array from array-like collection
|
||||
*
|
||||
* @param {*} collection
|
||||
*
|
||||
* @return {Array}
|
||||
*/
|
||||
static array(collection) {
|
||||
/**
|
||||
* Make array from array-like collection
|
||||
*
|
||||
* @param {*} collection
|
||||
*
|
||||
* @return {Array}
|
||||
*/
|
||||
static array(collection) {
|
||||
return Array.prototype.slice.call(collection);
|
||||
}
|
||||
|
||||
return Array.prototype.slice.call(collection);
|
||||
/**
|
||||
* Checks if object is empty
|
||||
*
|
||||
* @param {Object} object
|
||||
* @return {boolean}
|
||||
*/
|
||||
static isEmpty(object) {
|
||||
return Object.keys(object).length === 0 && object.constructor === Object;
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Check if passed object is a Promise
|
||||
* @param {*} object - object to check
|
||||
* @return {Boolean}
|
||||
*/
|
||||
static isPromise(object) {
|
||||
return Promise.resolve(object) === object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if object is empty
|
||||
*
|
||||
* @param {Object} object
|
||||
* @return {boolean}
|
||||
*/
|
||||
static isEmpty(object) {
|
||||
/**
|
||||
* Check if passed element is contenteditable
|
||||
* @param element
|
||||
* @return {boolean}
|
||||
*/
|
||||
static isContentEditable(element) {
|
||||
return element.contentEditable === 'true';
|
||||
}
|
||||
|
||||
return Object.keys(object).length === 0 && object.constructor === Object;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if passed object is a Promise
|
||||
* @param {*} object - object to check
|
||||
* @return {Boolean}
|
||||
*/
|
||||
static isPromise(object) {
|
||||
|
||||
return Promise.resolve(object) === object;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if passed element is contenteditable
|
||||
* @param element
|
||||
* @return {boolean}
|
||||
*/
|
||||
static isContentEditable(element) {
|
||||
|
||||
return element.contentEditable === 'true';
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Delays method execution
|
||||
*
|
||||
* @param method
|
||||
* @param timeout
|
||||
*/
|
||||
static delay(method, timeout) {
|
||||
|
||||
return function () {
|
||||
|
||||
let context = this,
|
||||
args = arguments;
|
||||
|
||||
window.setTimeout(() => method.apply(context, args), timeout);
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
/**
|
||||
* Delays method execution
|
||||
*
|
||||
* @param method
|
||||
* @param timeout
|
||||
*/
|
||||
static delay(method, timeout) {
|
||||
return function () {
|
||||
let context = this,
|
||||
args = arguments;
|
||||
|
||||
window.setTimeout(() => method.apply(context, args), timeout);
|
||||
};
|
||||
}
|
||||
};
|
8
src/styles/inline-toolbar.css
Normal file
8
src/styles/inline-toolbar.css
Normal file
|
@ -0,0 +1,8 @@
|
|||
.ce-inline-toolbar {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
@apply --overlay-pane;
|
||||
|
||||
width: 100px;
|
||||
height: 40px;
|
||||
}
|
|
@ -2,5 +2,6 @@
|
|||
@import url('ui.css');
|
||||
@import url('toolbar.css');
|
||||
@import url('toolbox.css');
|
||||
@import url('inline-toolbar.css');
|
||||
@import url('settings.css');
|
||||
@import url('block.css');
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
.ce-toolbox {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
transition: opacity 100ms ease;
|
||||
will-change: opacity;
|
||||
|
|
|
@ -20,4 +20,23 @@
|
|||
*/
|
||||
--toolbar-buttons-size: 34px;
|
||||
|
||||
--overlay-pane: {
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0 8px 23px -6px rgba(21,40,54,0.31), 22px -14px 34px -18px rgba(33,48,73,0.26);
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
left: 50%;
|
||||
margin-left: -7px;
|
||||
transform: rotate(-45deg);
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
6
tsconfig.json
Normal file
6
tsconfig.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"compilerOptions" : {
|
||||
"target": "es6",
|
||||
"declaration": false,
|
||||
}
|
||||
}
|
6
tslint.json
Normal file
6
tslint.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": "tslint:recommended",
|
||||
"rules": {
|
||||
"quotemark": [true, "single"]
|
||||
}
|
||||
}
|
|
@ -20,7 +20,7 @@ const VERSION = process.env.VERSION || pkg.version;
|
|||
* Plugins for bundle
|
||||
* @type {webpack}
|
||||
*/
|
||||
var webpack = require('webpack');
|
||||
var webpack = require('webpack');
|
||||
|
||||
/**
|
||||
* File system
|
||||
|
@ -33,147 +33,170 @@ var fs = require('fs');
|
|||
* Folders and files starting with '_' will be skipped
|
||||
* @type {Array}
|
||||
*/
|
||||
var editorModules = fs.readdirSync('./src/components/modules').filter( name => /.js$/.test(name) && name.substring(0,1) !== '_' );
|
||||
var editorModules = fs.readdirSync('./src/components/modules').filter( name => /.(j|t)s$/.test(name) && name.substring(0,1) !== '_' );
|
||||
|
||||
editorModules.forEach( name => {
|
||||
console.log('Require modules/' + name);
|
||||
console.log('Require modules/' + name);
|
||||
});
|
||||
|
||||
/**
|
||||
* Options for the Babel
|
||||
*/
|
||||
var babelLoader = {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
cacheDirectory: true,
|
||||
presets: [
|
||||
"env"
|
||||
],
|
||||
plugins: [
|
||||
/**
|
||||
* Dont need to use «.default» after «export default Class Ui {}»
|
||||
* @see {@link https://github.com/59naga/babel-plugin-add-module-exports}
|
||||
*/
|
||||
'add-module-exports',
|
||||
/**
|
||||
* Babel transforms some awesome ES6 features to ES5 with extra code, such as Class, JSX.
|
||||
* This plugin makes all generated extra codes to one module which significantly reduces the bundle code size.
|
||||
*
|
||||
* {@link https://github.com/brianZeng/babel-plugin-transform-helper}
|
||||
* @since 11 dec 2017 - removed due to plugin does not supports class inheritance
|
||||
*/
|
||||
// ['babel-plugin-transform-helper', {
|
||||
// helperFilename:'build/__tmp_babel_helpers.js'
|
||||
// }],
|
||||
'class-display-name',
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
module.exports = {
|
||||
|
||||
entry: {
|
||||
'codex-editor': './src/codex'
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'build'),
|
||||
filename: '[name].js',
|
||||
library: [ 'CodexEditor' ]
|
||||
},
|
||||
entry: {
|
||||
'codex-editor': './src/codex'
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'build'),
|
||||
filename: '[name].js',
|
||||
library: [ 'CodexEditor' ]
|
||||
},
|
||||
|
||||
watch: true,
|
||||
watch: true,
|
||||
watchOptions: {
|
||||
aggregateTimeout: 50
|
||||
},
|
||||
|
||||
watchOptions: {
|
||||
aggregateTimeout: 50
|
||||
},
|
||||
devtool: NODE_ENV == 'development' ? 'source-map' : null,
|
||||
|
||||
devtool: NODE_ENV == 'development' ? 'source-map' : null,
|
||||
/**
|
||||
* Tell webpack what directories should be searched when resolving modules.
|
||||
*/
|
||||
resolve : {
|
||||
// fallback: path.join(__dirname, 'node_modules'),
|
||||
modules : [ path.join(__dirname, "src"), "node_modules"],
|
||||
alias: {
|
||||
'utils': path.resolve(__dirname + '/src/components/', './utils'),
|
||||
'dom': path.resolve(__dirname + '/src/components/', './dom'),
|
||||
}
|
||||
},
|
||||
//
|
||||
|
||||
// resolveLoader : {
|
||||
// modules: [ path.resolve(__dirname, "src"), "node_modules" ],
|
||||
// moduleTemplates: ['*-webpack-loader', '*-web-loader', '*-loader', '*'],
|
||||
// extensions: ['.js']
|
||||
// },
|
||||
|
||||
plugins: [
|
||||
|
||||
/** Pass variables into modules */
|
||||
new webpack.DefinePlugin({
|
||||
NODE_ENV: JSON.stringify(NODE_ENV),
|
||||
VERSION: JSON.stringify(VERSION),
|
||||
editorModules: JSON.stringify(editorModules)
|
||||
}),
|
||||
|
||||
/**
|
||||
* Tell webpack what directories should be searched when resolving modules.
|
||||
* Setting up a dynamic requires that we use to autoload Editor Modules from 'components/modules' dir
|
||||
* {@link https://webpack.js.org/plugins/context-replacement-plugin/}
|
||||
*/
|
||||
resolve : {
|
||||
// fallback: path.join(__dirname, 'node_modules'),
|
||||
modules : [ path.join(__dirname, "src"), "node_modules"],
|
||||
alias: {
|
||||
'utils': path.resolve(__dirname + '/src/components/', './utils'),
|
||||
'dom': path.resolve(__dirname + '/src/components/', './dom'),
|
||||
}
|
||||
},
|
||||
//
|
||||
new webpack.ContextReplacementPlugin(
|
||||
/src\/components\/modules/,
|
||||
false, // newContentRecursive=false because we dont need to include folders
|
||||
new RegExp(
|
||||
'[^_]' + // dont match names started with '_'
|
||||
`(${editorModules.join('|')})` + // module names pattern: (events.js|ui.js|...)
|
||||
'$' // at the end of path
|
||||
)
|
||||
),
|
||||
|
||||
// resolveLoader : {
|
||||
// modules: [ path.resolve(__dirname, "src"), "node_modules" ],
|
||||
// moduleTemplates: ['*-webpack-loader', '*-web-loader', '*-loader', '*'],
|
||||
// extensions: ['.js']
|
||||
// },
|
||||
|
||||
plugins: [
|
||||
|
||||
/** Pass variables into modules */
|
||||
new webpack.DefinePlugin({
|
||||
NODE_ENV: JSON.stringify(NODE_ENV),
|
||||
VERSION: JSON.stringify(VERSION),
|
||||
editorModules: JSON.stringify(editorModules)
|
||||
}),
|
||||
|
||||
/**
|
||||
* Setting up a dynamic requires that we use to autoload Editor Modules from 'components/modules' dir
|
||||
* {@link https://webpack.js.org/plugins/context-replacement-plugin/}
|
||||
*/
|
||||
new webpack.ContextReplacementPlugin(
|
||||
/src\/components\/modules/,
|
||||
false, // newContentRecursive=false because we dont need to include folders
|
||||
new RegExp(
|
||||
'[^_]' + // dont match names started with '_'
|
||||
`(${editorModules.join('|')})` + // module names pattern: (events.js|ui.js|...)
|
||||
'$' // at the end of path
|
||||
)
|
||||
),
|
||||
|
||||
/**
|
||||
* Automatically load global visible modules
|
||||
* instead of having to import/require them everywhere.
|
||||
*/
|
||||
new webpack.ProvidePlugin({
|
||||
'_': 'utils',
|
||||
'$': 'dom',
|
||||
'Module': './../__module',
|
||||
}),
|
||||
/**
|
||||
* Automatically load global visible modules
|
||||
* instead of having to import/require them everywhere.
|
||||
*/
|
||||
new webpack.ProvidePlugin({
|
||||
'_': 'utils',
|
||||
'$': 'dom',
|
||||
'Module': './../__module.ts',
|
||||
}),
|
||||
|
||||
|
||||
/** Минифицируем CSS и JS */
|
||||
// new webpack.optimize.UglifyJsPlugin({
|
||||
/** Disable warning messages. Cant disable uglify for 3rd party libs such as html-janitor */
|
||||
// compress: {
|
||||
// warnings: false
|
||||
// }
|
||||
// }),
|
||||
/** Минифицируем CSS и JS */
|
||||
// new webpack.optimize.UglifyJsPlugin({
|
||||
/** Disable warning messages. Cant disable uglify for 3rd party libs such as html-janitor */
|
||||
// compress: {
|
||||
// warnings: false
|
||||
// }
|
||||
// }),
|
||||
|
||||
/** Block biuld if errors found */
|
||||
// new webpack.NoErrorsPlugin(),
|
||||
/** Block biuld if errors found */
|
||||
// new webpack.NoErrorsPlugin(),
|
||||
|
||||
],
|
||||
],
|
||||
|
||||
module : {
|
||||
rules : [
|
||||
{
|
||||
test : /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
use : {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [ __dirname + '/node_modules/babel-preset-es2015' ],
|
||||
plugins: [
|
||||
/**
|
||||
* Dont need to use «.default» after «export default Class Ui {}»
|
||||
* @see {@link https://github.com/59naga/babel-plugin-add-module-exports}
|
||||
*/
|
||||
'add-module-exports',
|
||||
/**
|
||||
* Babel transforms some awesome ES6 features to ES5 with extra code, such as Class, JSX.
|
||||
* This plugin makes all generated extra codes to one module which significantly reduces the bundle code size.
|
||||
*
|
||||
* {@link https://github.com/brianZeng/babel-plugin-transform-helper}
|
||||
* @since 11 dec 2017 - removed due to plugin does not supports class inheritance
|
||||
*/
|
||||
// ['babel-plugin-transform-helper', {
|
||||
// helperFilename:'build/__tmp_babel_helpers.js'
|
||||
// }],
|
||||
'class-display-name',
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test : /\.js$/,
|
||||
use: 'eslint-loader?fix=true',
|
||||
exclude: /(node_modules|build)/ // dont need to look in '/build' to prevent analyse __tmp_babel_helper.js
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
// minimize: 1,
|
||||
importLoaders: 1
|
||||
}
|
||||
},
|
||||
'postcss-loader'
|
||||
]
|
||||
}
|
||||
module : {
|
||||
rules : [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
use: [
|
||||
babelLoader,
|
||||
{
|
||||
loader: 'ts-loader'
|
||||
},
|
||||
{
|
||||
loader: 'tslint-loader',
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
},
|
||||
{
|
||||
test : /\.js$/,
|
||||
use: [
|
||||
babelLoader,
|
||||
{
|
||||
loader: 'eslint-loader?fix=true&esModules=true',
|
||||
}
|
||||
],
|
||||
exclude: /(node_modules|build)/, // dont need to look in '/build' to prevent analyse __tmp_babel_helper.js
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
// minimize: 1,
|
||||
importLoaders: 1
|
||||
}
|
||||
},
|
||||
'postcss-loader'
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
optimization: {
|
||||
minimize: false
|
||||
},
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue