mirror of
https://github.com/codex-team/editor.js
synced 2026-03-16 23:55:49 +01:00
Merge branch 'rewriting-version2.0' into renderer-improvements
# Conflicts: # build/codex-editor.js # build/codex-editor.js.map # src/codex.js
This commit is contained in:
commit
14b36c2fa0
32 changed files with 2526 additions and 9403 deletions
|
|
@ -27,7 +27,10 @@
|
|||
"objectsInArrays": true,
|
||||
"arraysInArrays": true
|
||||
}],
|
||||
"quotes": [2, "single", "avoid-escape"],
|
||||
"quotes": [2, "single", {
|
||||
"avoidEscape": true,
|
||||
"allowTemplateLiterals": true
|
||||
}],
|
||||
"eqeqeq": 0,
|
||||
"brace-style": [2, "1tbs"],
|
||||
"comma-spacing": [2, {
|
||||
|
|
@ -75,7 +78,9 @@
|
|||
"RegExp": true,
|
||||
"Module": true,
|
||||
"Node": true,
|
||||
"Element": true,
|
||||
"Proxy": true,
|
||||
"Symbol": true,
|
||||
"$": true,
|
||||
"_": true
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
24
docs/tools.md
Normal file
24
docs/tools.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# CodeX Editor Tools
|
||||
|
||||
CodeX Editor is a block-oriented editor. It means that entry composed with the list of `Blocks` of different types: `Texts`, `Headers`, `Images`, `Quotes` etc.
|
||||
|
||||
`Tool` — is a class that provide custom `Block` type. All Tools represented by `Plugins`.
|
||||
|
||||
## Tool class structure
|
||||
|
||||
### Constructor
|
||||
|
||||
### Save
|
||||
|
||||
### Render
|
||||
|
||||
### Available settings
|
||||
|
||||
| Name | Type | Default Value | Description |
|
||||
| -- | -- | -- | -- |
|
||||
| `displayInToolbox` | _Boolean_ | `false` | Pass `true` to display this `Tool` in the Editor's `Toolbox` |
|
||||
| `iconClassName` | _String_ | — | CSS class name for the `Toolbox` icon. Used when `displayInToolbox` is `true` |
|
||||
| `irreplaceable` | _Boolean_ | `false` | By default, **empty** `Blocks` can be **replaced** by other `Blocks` with the `Toolbox`. Some tools with media-content may prefer another behaviour. Pass `true` and `Toolbox` will add a new block below yours. |
|
||||
| `contentless` | _Boolean_ | `false` | Pass `true` for Tool which represents decorative empty `Blocks` |
|
||||
|
||||
### Sanitize
|
||||
|
|
@ -9,11 +9,6 @@
|
|||
font-size: 14px;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
#codex-editor {
|
||||
margin: 0 auto;
|
||||
max-width: 800px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -22,8 +17,8 @@
|
|||
|
||||
</body>
|
||||
|
||||
<script src="plugins/paragraph/paragraph.js?v=100"></script>
|
||||
<link rel="stylesheet" href="plugins/paragraph/paragraph.css">
|
||||
<script src="plugins/text/text.js?v=100"></script>
|
||||
<link rel="stylesheet" href="plugins/text/text.css">
|
||||
|
||||
<!--<script src="plugins/header/header.js"></script>-->
|
||||
<!--<link rel="stylesheet" href="plugins/header/header.css">-->
|
||||
|
|
@ -65,9 +60,9 @@
|
|||
codex.editor = 1;
|
||||
var editor = new CodexEditor({
|
||||
holderId : 'codex-editor',
|
||||
initialBlock : 'paragraph',
|
||||
initialBlock : 'text',
|
||||
tools: {
|
||||
paragraph: Paragraph,
|
||||
text: Text,
|
||||
},
|
||||
toolsConfig: {
|
||||
quote: {
|
||||
|
|
@ -79,13 +74,13 @@
|
|||
data: {
|
||||
items: [
|
||||
{
|
||||
type : 'paragraph',
|
||||
type : 'text',
|
||||
data : {
|
||||
text : 'Привет от CodeX'
|
||||
}
|
||||
},
|
||||
{
|
||||
type : 'paragraph',
|
||||
type : 'text',
|
||||
data : {
|
||||
text : 'Пишите нам на team@ifmo.su'
|
||||
}
|
||||
|
|
@ -100,17 +95,17 @@
|
|||
// });
|
||||
// codex.editor.start({
|
||||
// holderId : "codex-editor",
|
||||
// initialBlockPlugin : 'paragraph',
|
||||
// initialBlockPlugin : 'text',
|
||||
// // placeholder: 'Прошлой ночью мне приснилось...',
|
||||
// hideToolbar: false,
|
||||
// tools : {
|
||||
// paragraph: {
|
||||
// type: 'paragraph',
|
||||
// iconClassname: 'ce-icon-paragraph',
|
||||
// render: paragraph.render,
|
||||
// validate: paragraph.validate,
|
||||
// save: paragraph.save,
|
||||
// destroy: paragraph.destroy,
|
||||
// text: {
|
||||
// type: 'text',
|
||||
// iconClassname: 'ce-icon-text',
|
||||
// render: text.render,
|
||||
// validate: text.validate,
|
||||
// save: text.save,
|
||||
// destroy: text.destroy,
|
||||
// allowedToPaste: true,
|
||||
// showInlineToolbar: true,
|
||||
// allowRenderOnPaste: true
|
||||
|
|
@ -272,7 +267,7 @@
|
|||
// }
|
||||
// },
|
||||
// {
|
||||
// type : 'paragraph',
|
||||
// type : 'text',
|
||||
// data : {
|
||||
// text : 'Пишите нам на team@ifmo.su'
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@
|
|||
* Empty paragraph placeholder
|
||||
*/
|
||||
|
||||
.ce-paragraph {
|
||||
padding: 0.7em 0 !important;
|
||||
line-height: 1.7em;
|
||||
.ce-text {
|
||||
padding: 15px 0 !important;
|
||||
line-height: 1.6em;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ce-paragraph:empty::before,
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* @class Paragraph
|
||||
* @class Text
|
||||
* @classdesc Paragraph plugin for CodexEditor
|
||||
*
|
||||
* @author CodeX Team (team@ifmo.su)
|
||||
|
|
@ -8,33 +8,44 @@
|
|||
* @version 2.0.0
|
||||
*
|
||||
*
|
||||
* @typedef {Object} ParagraphData
|
||||
* @property {String} text — HTML content to insert to paragraph element
|
||||
* @typedef {Object} TextData
|
||||
* @property {String} text — HTML content to insert to text element
|
||||
*
|
||||
*/
|
||||
|
||||
class Paragraph {
|
||||
class Text {
|
||||
|
||||
/**
|
||||
* Get the name of the plugin
|
||||
* Pass true to display this tool in the Editor's Toolbox
|
||||
*
|
||||
* @returns {string} The plugin name
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static get name() {
|
||||
static get displayInToolbox() {
|
||||
|
||||
return 'paragraph';
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for the Toolbox icon
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
static get iconClassName() {
|
||||
|
||||
return 'cdx-text-icon';
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Render plugin`s html and set initial content
|
||||
*
|
||||
* @param {ParagraphData} data — initial plugin content
|
||||
* @param {TextData} data — initial plugin content
|
||||
*/
|
||||
constructor(data = {}) {
|
||||
|
||||
this._CSS = {
|
||||
wrapper: 'ce-paragraph'
|
||||
wrapper: 'ce-text'
|
||||
};
|
||||
|
||||
this._data = {};
|
||||
|
|
@ -65,7 +76,7 @@ class Paragraph {
|
|||
/**
|
||||
* Check if saved text is empty
|
||||
*
|
||||
* @param {ParagraphData} savedData — data received from plugins`s element
|
||||
* @param {TextData} savedData — data received from plugins`s element
|
||||
* @returns {boolean} false if saved text is empty, true otherwise
|
||||
*/
|
||||
validate(savedData) {
|
||||
|
|
@ -96,7 +107,7 @@ class Paragraph {
|
|||
*
|
||||
* @todo sanitize data while saving
|
||||
*
|
||||
* @returns {ParagraphData} Current data
|
||||
* @returns {TextData} Current data
|
||||
*/
|
||||
get data() {
|
||||
|
||||
|
|
@ -111,7 +122,7 @@ class Paragraph {
|
|||
/**
|
||||
* Set new data for plugin
|
||||
*
|
||||
* @param {ParagraphData} data — data to set
|
||||
* @param {TextData} data — data to set
|
||||
*/
|
||||
set data(data) {
|
||||
|
||||
8688
package-lock.json
generated
8688
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -17,7 +17,7 @@
|
|||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"css-loader": "^0.28.7",
|
||||
"eslint": "^4.11.0",
|
||||
"eslint": "^4.13.1",
|
||||
"eslint-loader": "^1.9.0",
|
||||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"html-janitor": "^2.0.2",
|
||||
|
|
@ -36,8 +36,8 @@
|
|||
"postcss-nested": "^2.1.2",
|
||||
"postcss-nested-ancestors": "^1.0.0",
|
||||
"postcss-nesting": "^4.2.1",
|
||||
"postcss-smart-import": "^0.7.5",
|
||||
"webpack": "^3.8.1"
|
||||
"postcss-smart-import": "^0.7.6",
|
||||
"webpack": "^3.10.0"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
|
|
|
|||
131
src/codex.js
131
src/codex.js
|
|
@ -45,15 +45,30 @@
|
|||
|
||||
/**
|
||||
* @typedef {Object} EditorConfig
|
||||
* @property {String} holderId - Element to append Editor
|
||||
* @property {String} initialBlock - Tool name which will be initial
|
||||
* @property {Object} tools - list of tools. The object value must be function (constructor) so that CodexEditor could make an instance
|
||||
* @property {@link Tools#ToolsConfig} toolsConfig - tools configuration
|
||||
* @property {Array} data - Blocks list in JSON-format
|
||||
* @property {String} holderId - Element to append Editor
|
||||
* @property {Array} data - Blocks list in JSON-format
|
||||
* @property {Object} tools - Map for used Tools in format { name : Class, ... }
|
||||
* @property {String} initialBlock - This Tool will be added by default
|
||||
* @property {String} placeholder - First Block placeholder
|
||||
* @property {Object} sanitizer - @todo fill desc
|
||||
* @property {Boolean} hideToolbar - @todo fill desc
|
||||
* @property {Object} toolsConfig - tools configuration {@link Tools#ToolsConfig}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Dynamically imported utils
|
||||
*
|
||||
* @typedef {Dom} $ - {@link components/dom.js}
|
||||
* @typedef {Util} _ - {@link components/utils.js}
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Apply polyfills
|
||||
*/
|
||||
import 'components/polyfills';
|
||||
|
||||
/**
|
||||
* Require Editor modules places in components/modules dir
|
||||
*/
|
||||
|
|
@ -87,11 +102,19 @@ module.exports = class CodexEditor {
|
|||
|
||||
/**
|
||||
* Configuration object
|
||||
* @type {EditorConfig}
|
||||
*/
|
||||
this.config = {};
|
||||
|
||||
/**
|
||||
* Editor Components
|
||||
* @typedef {Object} EditorComponents
|
||||
* @property {BlockManager} BlockManager
|
||||
* @property {Tools} Tools
|
||||
* @property {Events} Events
|
||||
* @property {UI} UI
|
||||
* @property {Toolbar} Toolbar
|
||||
* @property {Toolbox} Toolbox
|
||||
* @property {Renderer} Renderer
|
||||
*/
|
||||
this.moduleInstances = {};
|
||||
|
||||
|
|
@ -110,7 +133,7 @@ module.exports = class CodexEditor {
|
|||
})
|
||||
.catch(error => {
|
||||
|
||||
console.log('CodeX Editor does not ready beecause of %o', error);
|
||||
console.log('CodeX Editor does not ready because of %o', error);
|
||||
|
||||
});
|
||||
|
||||
|
|
@ -118,9 +141,9 @@ module.exports = class CodexEditor {
|
|||
|
||||
/**
|
||||
* Setting for configuration
|
||||
* @param {Object} config
|
||||
* @param {EditorConfig} config
|
||||
*/
|
||||
set configuration(config = {}) {
|
||||
set configuration(config) {
|
||||
|
||||
/**
|
||||
* Initlai block type
|
||||
|
|
@ -165,11 +188,24 @@ module.exports = class CodexEditor {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {{}|*}
|
||||
* @returns {EditorConfig}
|
||||
*/
|
||||
get configuration() {
|
||||
|
||||
|
|
@ -277,77 +313,20 @@ module.exports = class CodexEditor {
|
|||
*/
|
||||
start() {
|
||||
|
||||
let modulePreparationList = this.modulePreparationList();
|
||||
let prepareDecorator = module => module.prepare();
|
||||
|
||||
return _.sequence(modulePreparationList)
|
||||
.catch(function (error) {
|
||||
return Promise.resolve()
|
||||
.then(prepareDecorator(this.moduleInstances.Tools))
|
||||
.then(prepareDecorator(this.moduleInstances.UI))
|
||||
.then(prepareDecorator(this.moduleInstances.BlockManager))
|
||||
.then(() => {
|
||||
|
||||
_.log('Error occured', error);
|
||||
return this.moduleInstances.Renderer.render(this.config.data.items);
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a sequence with modules that needs to be prepared consistently
|
||||
* @return {[*,*,*,*]}
|
||||
*/
|
||||
modulePreparationList() {
|
||||
|
||||
/**
|
||||
* Chain that will be passed alternately
|
||||
* Returns {@link utils#ChainData}
|
||||
*/
|
||||
let chainData = [
|
||||
{
|
||||
/**
|
||||
* First: Call UI module preparation method
|
||||
*/
|
||||
function: () => {
|
||||
|
||||
return this.moduleInstances.UI.prepare();
|
||||
|
||||
}
|
||||
},
|
||||
{
|
||||
/**
|
||||
* Second: Call Tools module preparation method
|
||||
*/
|
||||
function: () => {
|
||||
|
||||
return this.moduleInstances.Tools.prepare();
|
||||
|
||||
}
|
||||
},
|
||||
{
|
||||
/**
|
||||
* Third: Call BlockManager module preparation method
|
||||
*/
|
||||
function: () => {
|
||||
|
||||
return this.moduleInstances.BlockManager.prepare();
|
||||
|
||||
}
|
||||
},
|
||||
{
|
||||
/**
|
||||
* Fourth: Render data
|
||||
*
|
||||
* If no items was passed by user, then use default block which is 'paragraph' or passed initialBlock
|
||||
* {@link EditorConfig#initialBlock}
|
||||
*/
|
||||
function: () => {
|
||||
|
||||
return this.moduleInstances.Renderer.render(this.config.data.items);
|
||||
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return chainData;
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// module.exports = (function (editor) {
|
||||
|
|
@ -384,11 +363,11 @@ module.exports = class CodexEditor {
|
|||
// * holds initial settings
|
||||
// */
|
||||
// editor.settings = {
|
||||
// tools : ['paragraph', 'header', 'picture', 'list', 'quote', 'code', 'twitter', 'instagram', 'smile'],
|
||||
// tools : ['text', 'header', 'picture', 'list', 'quote', 'code', 'twitter', 'instagram', 'smile'],
|
||||
// holderId : 'codex-editor',
|
||||
//
|
||||
// // Type of block showing on empty editor
|
||||
// initialBlockPlugin: 'paragraph'
|
||||
// initialBlockPlugin: 'text'
|
||||
// };
|
||||
//
|
||||
// /**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* @abstract
|
||||
* @class Module
|
||||
* @classdesc All modules inherites from this class.
|
||||
* @classdesc All modules inherits from this class.
|
||||
*
|
||||
* @typedef {Module} Module
|
||||
* @property {Object} config - Editor user settings
|
||||
|
|
@ -22,7 +22,14 @@ export default class Module {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {EditorConfig}
|
||||
*/
|
||||
this.config = config;
|
||||
|
||||
/**
|
||||
* @type {EditorComponents}
|
||||
*/
|
||||
this.Editor = null;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,15 +20,24 @@ export default class Block {
|
|||
|
||||
this.tool = tool;
|
||||
|
||||
this.CSS = {
|
||||
wrapper: 'ce-block',
|
||||
content: 'ce-block__content'
|
||||
};
|
||||
|
||||
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
|
||||
*
|
||||
|
|
@ -37,8 +46,8 @@ export default class Block {
|
|||
*/
|
||||
compose() {
|
||||
|
||||
let wrapper = $.make('div', this.CSS.wrapper),
|
||||
content = $.make('div', this.CSS.content);
|
||||
let wrapper = $.make('div', Block.CSS.wrapper),
|
||||
content = $.make('div', Block.CSS.content);
|
||||
|
||||
content.appendChild(this.tool.html);
|
||||
wrapper.appendChild(content);
|
||||
|
|
@ -58,4 +67,74 @@ export default class Block {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = this._html.textContent.trim().length === 0,
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -99,42 +99,6 @@ export default class Core {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* Core custom logger
|
||||
*
|
||||
* @param msg
|
||||
* @param type
|
||||
* @param args
|
||||
*/
|
||||
log(msg, type, args) {
|
||||
|
||||
type = type || 'log';
|
||||
|
||||
if (!args) {
|
||||
|
||||
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 );
|
||||
|
||||
}
|
||||
|
||||
} catch(e) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Native Ajax
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ export default class Dom {
|
|||
* @param {Object} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static isNode(node) {
|
||||
static isElement(node) {
|
||||
|
||||
return node && typeof node === 'object' && node.nodeType && node.nodeType === Node.ELEMENT_NODE;
|
||||
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ module.exports = class Content {
|
|||
*/
|
||||
getFirstLevelBlock(node) {
|
||||
|
||||
if (!$.isNode(node)) {
|
||||
if (!$.isElement(node)) {
|
||||
|
||||
node = node.parentNode;
|
||||
|
||||
|
|
|
|||
|
|
@ -7,15 +7,20 @@
|
|||
|
||||
import Block from '../block';
|
||||
|
||||
/**
|
||||
* @typedef {BlockManager} BlockManager
|
||||
* @property {Number} currentBlockIndex - Index of current working block
|
||||
* @property {Proxy} _blocks - Proxy for Blocks instance {@link Blocks}
|
||||
*/
|
||||
export default class BlockManager extends Module {
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {EditorConfig} config
|
||||
*/
|
||||
constructor(config) {
|
||||
constructor({config}) {
|
||||
|
||||
super(config);
|
||||
super({config});
|
||||
|
||||
/**
|
||||
* Proxy for Blocks instance {@link Blocks}
|
||||
|
|
@ -78,13 +83,17 @@ export default class BlockManager extends Module {
|
|||
* @param {String} toolName — plugin name
|
||||
* @param {Object} data — plugin data
|
||||
*/
|
||||
insert(toolName, data) {
|
||||
insert(toolName, data = {}) {
|
||||
|
||||
let toolInstance = this.Editor.Tools.construct(toolName, data),
|
||||
block = new Block(toolInstance);
|
||||
|
||||
this._blocks[++this.currentBlockIndex] = block;
|
||||
|
||||
/**
|
||||
* @todo fire Tool's appendCallback
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -93,13 +102,17 @@ export default class BlockManager extends Module {
|
|||
* @param {String} toolName — plugin name
|
||||
* @param {Object} data — plugin data
|
||||
*/
|
||||
replace(toolName, data) {
|
||||
replace(toolName, data = {}) {
|
||||
|
||||
let toolInstance = this.Editor.Tools.construct(toolName, data),
|
||||
block = new Block(toolInstance);
|
||||
|
||||
this._blocks.insert(this.currentBlockIndex, block, true);
|
||||
|
||||
/**
|
||||
* @todo fire Tool's appendCallback
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -156,8 +169,23 @@ export default class BlockManager extends Module {
|
|||
|
||||
let nodes = this._blocks.nodes;
|
||||
|
||||
/**
|
||||
* Update current Block's index
|
||||
* @type {number}
|
||||
*/
|
||||
this.currentBlockIndex = nodes.indexOf(element);
|
||||
|
||||
/**
|
||||
* Remove previous selected Block's state
|
||||
*/
|
||||
this._blocks.array.forEach( block => block.selected = false);
|
||||
|
||||
/**
|
||||
* Mark current Block as selected
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.currentBlock.selected = true;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -171,6 +199,38 @@ export default class BlockManager extends Module {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* 1) Find first-level Block from passed child Node
|
||||
* 2) Mark it as current
|
||||
*
|
||||
* @param {Element|Text} childNode - look ahead from this node.
|
||||
* @throws Error - when passed Node is not included at the Block
|
||||
*/
|
||||
setCurrentBlockByChildNode(childNode) {
|
||||
|
||||
/**
|
||||
* If node is Text TextNode
|
||||
*/
|
||||
if (!$.isElement(childNode)) {
|
||||
|
||||
childNode = childNode.parentNode;
|
||||
|
||||
}
|
||||
|
||||
let parentFirstLevelBlock = childNode.closest(`.${Block.CSS.wrapper}`);
|
||||
|
||||
if (parentFirstLevelBlock) {
|
||||
|
||||
this.currentNode = parentFirstLevelBlock;
|
||||
|
||||
} else {
|
||||
|
||||
throw new Error('Can not find a Block from this child Node');
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
109
src/components/modules/caret.js
Normal file
109
src/components/modules/caret.js
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
/**
|
||||
* @class Caret
|
||||
* @classdesc Contains methods for working Caret
|
||||
*
|
||||
* @typedef {Caret} Caret
|
||||
*/
|
||||
export default class Caret extends Module {
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
constructor({config}) {
|
||||
|
||||
super({config});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Caret to the last Block
|
||||
*
|
||||
* If last block is not empty, append another empty block
|
||||
*/
|
||||
setToTheLastBlock() {
|
||||
|
||||
let blocks = this.Editor.BlockManager.blocks,
|
||||
lastBlock;
|
||||
|
||||
if (blocks.length) {
|
||||
|
||||
lastBlock = blocks[blocks.length - 1];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.set(lastBlock.html);
|
||||
|
||||
} else {
|
||||
|
||||
this.Editor.BlockManager.insert(this.config.initialBlock);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
// * 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);
|
||||
//
|
||||
// }
|
||||
//
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Set caret to the passed Node
|
||||
* @param {Element} node - content-editable Element
|
||||
*/
|
||||
set(node) {
|
||||
|
||||
/**
|
||||
* @todo add working with Selection
|
||||
* tmp: work with textContent
|
||||
*/
|
||||
|
||||
node.textContent += '|';
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -15,9 +15,9 @@ export default class Events extends Module {
|
|||
/**
|
||||
* @constructor
|
||||
*/
|
||||
constructor(config) {
|
||||
constructor({config}) {
|
||||
|
||||
super(config);
|
||||
super({config});
|
||||
this.subscribers = {};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ export default class Renderer extends Module {
|
|||
* @constructor
|
||||
* @param {EditorConfig} config
|
||||
*/
|
||||
constructor(config) {
|
||||
constructor({config}) {
|
||||
|
||||
super(config);
|
||||
super({config});
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,9 +43,9 @@ export default class Sanitizer extends Module {
|
|||
*
|
||||
* @param {SanitizerConfig} config
|
||||
*/
|
||||
constructor(config) {
|
||||
constructor({config}) {
|
||||
|
||||
super(config);
|
||||
super({config});
|
||||
|
||||
// default config
|
||||
this.defaultConfig = null;
|
||||
|
|
|
|||
|
|
@ -54,9 +54,9 @@ export default class Toolbar extends Module {
|
|||
/**
|
||||
* @constructor
|
||||
*/
|
||||
constructor(config) {
|
||||
constructor({config}) {
|
||||
|
||||
super(config);
|
||||
super({config});
|
||||
|
||||
this.nodes = {
|
||||
wrapper : null,
|
||||
|
|
@ -65,7 +65,6 @@ export default class Toolbar extends Module {
|
|||
|
||||
// Content Zone
|
||||
plusButton : null,
|
||||
toolbox : null,
|
||||
|
||||
// Actions Zone
|
||||
settingsToggler : null,
|
||||
|
|
@ -77,14 +76,25 @@ export default class Toolbar extends Module {
|
|||
defaultSettings: null,
|
||||
};
|
||||
|
||||
this.CSS = {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
toolbox: 'ce-toolbar__toolbox',
|
||||
plusButton: 'ce-toolbar__plus',
|
||||
plusButtonHidden: 'ce-toolbar__plus--hidden',
|
||||
|
||||
// Actions Zone
|
||||
settingsToggler: 'ce-toolbar__settings-btn',
|
||||
|
|
@ -103,14 +113,14 @@ export default class Toolbar extends Module {
|
|||
*/
|
||||
make() {
|
||||
|
||||
this.nodes.wrapper = $.make('div', this.CSS.toolbar);
|
||||
this.nodes.wrapper = $.make('div', Toolbar.CSS.toolbar);
|
||||
|
||||
/**
|
||||
* Make Content Zone and Actions Zone
|
||||
*/
|
||||
['content', 'actions'].forEach( el => {
|
||||
|
||||
this.nodes[el] = $.make('div', this.CSS[el]);
|
||||
this.nodes[el] = $.make('div', Toolbar.CSS[el]);
|
||||
$.append(this.nodes.wrapper, this.nodes[el]);
|
||||
|
||||
});
|
||||
|
|
@ -121,12 +131,15 @@ export default class Toolbar extends Module {
|
|||
* - Plus Button
|
||||
* - Toolbox
|
||||
*/
|
||||
['plusButton', 'toolbox'].forEach( el => {
|
||||
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);
|
||||
|
||||
this.nodes[el] = $.make('div', this.CSS[el]);
|
||||
$.append(this.nodes.content, this.nodes[el]);
|
||||
|
||||
});
|
||||
/**
|
||||
* Make a Toolbox
|
||||
*/
|
||||
this.Editor.Toolbox.make();
|
||||
|
||||
/**
|
||||
* Fill Actions Zone:
|
||||
|
|
@ -134,7 +147,7 @@ export default class Toolbar extends Module {
|
|||
* - Remove Block Button
|
||||
* - Settings Panel
|
||||
*/
|
||||
this.nodes.settingsToggler = $.make('span', this.CSS.settingsToggler);
|
||||
this.nodes.settingsToggler = $.make('span', Toolbar.CSS.settingsToggler);
|
||||
this.nodes.removeBlockButton = this.makeRemoveBlockButton();
|
||||
|
||||
$.append(this.nodes.actions, [this.nodes.settingsToggler, this.nodes.removeBlockButton]);
|
||||
|
|
@ -158,10 +171,10 @@ export default class Toolbar extends Module {
|
|||
*/
|
||||
makeBlockSettingsPanel() {
|
||||
|
||||
this.nodes.settings = $.make('div', this.CSS.settings);
|
||||
this.nodes.settings = $.make('div', Toolbar.CSS.settings);
|
||||
|
||||
this.nodes.pluginSettings = $.make('div', this.CSS.pluginSettings);
|
||||
this.nodes.defaultSettings = $.make('div', this.CSS.defaultSettings);
|
||||
this.nodes.pluginSettings = $.make('div', Toolbar.CSS.pluginSettings);
|
||||
this.nodes.defaultSettings = $.make('div', Toolbar.CSS.defaultSettings);
|
||||
|
||||
$.append(this.nodes.settings, [this.nodes.pluginSettings, this.nodes.defaultSettings]);
|
||||
$.append(this.nodes.actions, this.nodes.settings);
|
||||
|
|
@ -178,7 +191,83 @@ export default class Toolbar extends Module {
|
|||
* @todo add confirmation panel and handlers
|
||||
* @see {@link settings#makeRemoveBlockButton}
|
||||
*/
|
||||
return $.make('span', this.CSS.removeBlockButton);
|
||||
return $.make('span', Toolbar.CSS.removeBlockButton);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(event) {
|
||||
|
||||
this.Editor.Toolbox.toggle();
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
223
src/components/modules/toolbox.js
Normal file
223
src/components/modules/toolbox.js
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
/**
|
||||
* @class Toolbox
|
||||
* @classdesc Holder for Tools
|
||||
*
|
||||
* @typedef {Toolbox} Toolbox
|
||||
* @property {Boolean} opened - opening state
|
||||
* @property {Object} nodes - Toolbox nodes
|
||||
* @property {Object} CSS - CSS class names
|
||||
*
|
||||
*/
|
||||
export default class Toolbox extends Module {
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
constructor({config}) {
|
||||
|
||||
super({config});
|
||||
|
||||
this.nodes = {
|
||||
toolbox: null,
|
||||
buttons: []
|
||||
};
|
||||
|
||||
/**
|
||||
* Opening state
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.opened = false;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS styles
|
||||
* @return {{toolbox: string, toolboxButton: string, toolboxOpened: string}}
|
||||
*/
|
||||
static get CSS() {
|
||||
|
||||
return {
|
||||
toolbox: 'ce-toolbox',
|
||||
toolboxButton: 'ce-toolbox__button',
|
||||
toolboxOpened: 'ce-toolbox--opened',
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the Toolbox
|
||||
*/
|
||||
make() {
|
||||
|
||||
this.nodes.toolbox = $.make('div', Toolbox.CSS.toolbox);
|
||||
$.append(this.Editor.Toolbar.nodes.content, this.nodes.toolbox);
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -5,10 +5,9 @@
|
|||
*/
|
||||
|
||||
/**
|
||||
* Load user defined tools
|
||||
* Tools must contain the following important objects:
|
||||
* Each Tool must contain the following important objects:
|
||||
*
|
||||
* @typedef {Object} ToolsConfig
|
||||
* @typedef {Object} ToolConfig {@link docs/tools.md}
|
||||
* @property {String} iconClassname - this a icon in toolbar
|
||||
* @property {Boolean} displayInToolbox - will be displayed in toolbox. Default value is TRUE
|
||||
* @property {Boolean} enableLineBreaks - inserts new block or break lines. Default value is FALSE
|
||||
|
|
@ -19,15 +18,28 @@
|
|||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Tool} Tool
|
||||
* @property {String} name - name of this module
|
||||
* @property {Object[]} toolInstances - list of tool instances
|
||||
* @property {Tools[]} available - available Tools
|
||||
* @property {Tools[]} unavailable - unavailable Tools
|
||||
* @typedef {Function} Tool {@link docs/tools.md}
|
||||
* @property {Boolean} displayInToolbox - By default, tools won't be added in the Toolbox. Pass true to add.
|
||||
* @property {String} iconClassName - CSS class name for the Toolbox button
|
||||
* @property {Boolean} irreplaceable - Toolbox behaviour: replace or add new block below
|
||||
* @property render
|
||||
* @property save
|
||||
* @property settings
|
||||
* @property validate
|
||||
*
|
||||
* @todo update according to current API
|
||||
* @todo describe Tool in the {@link docs/tools.md}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class properties:
|
||||
*
|
||||
* @typedef {Tools} Tools
|
||||
* @property {Tools[]} toolsAvailable - available Tools
|
||||
* @property {Tools[]} toolsUnavailable - unavailable Tools
|
||||
* @property {Object} toolsClasses - all classes
|
||||
* @property {EditorConfig} config - Editor config
|
||||
*/
|
||||
|
||||
export default class Tools extends Module {
|
||||
|
||||
/**
|
||||
|
|
@ -51,15 +63,18 @@ export default class Tools extends Module {
|
|||
}
|
||||
|
||||
/**
|
||||
* If config wasn't passed by user
|
||||
* @return {ToolsConfig}
|
||||
* Static getter for default Tool config fields
|
||||
*
|
||||
* @usage Tools.defaultConfig.displayInToolbox
|
||||
* @return {ToolConfig}
|
||||
*/
|
||||
get defaultConfig() {
|
||||
static get defaultConfig() {
|
||||
|
||||
return {
|
||||
iconClassName : 'default-icon',
|
||||
iconClassName : '',
|
||||
displayInToolbox : false,
|
||||
enableLineBreaks : false
|
||||
enableLineBreaks : false,
|
||||
irreplaceable : false
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -67,21 +82,38 @@ export default class Tools extends Module {
|
|||
/**
|
||||
* @constructor
|
||||
*
|
||||
* @param {ToolsConfig} config
|
||||
* @param {EditorConfig} config
|
||||
*/
|
||||
constructor(config) {
|
||||
constructor({config}) {
|
||||
|
||||
super(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 {boolean}
|
||||
* @return {Promise}
|
||||
*/
|
||||
prepare() {
|
||||
|
||||
|
|
@ -128,7 +160,7 @@ export default class Tools extends Module {
|
|||
|
||||
/**
|
||||
* Binds prepare function of plugins with user or default config
|
||||
* @return {Array} list of functions that needs to be fired sequently
|
||||
* @return {Array} list of functions that needs to be fired sequentially
|
||||
*/
|
||||
getListOfPrepareFunctions() {
|
||||
|
||||
|
|
@ -147,6 +179,13 @@ export default class Tools extends Module {
|
|||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
/**
|
||||
* If Tool hasn't a prepare method, mark it as available
|
||||
*/
|
||||
this.toolsAvailable[toolName] = toolClass;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -173,16 +212,6 @@ export default class Tools extends Module {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all tools
|
||||
* @return {Array}
|
||||
*/
|
||||
getTools() {
|
||||
|
||||
return this.toolInstances;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Return tool`a instance
|
||||
*
|
||||
|
|
@ -209,4 +238,15 @@ export default class Tools extends Module {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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];
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -31,10 +31,7 @@
|
|||
// SETTINGS_ITEM : 'ce-settings__item'
|
||||
// };
|
||||
|
||||
let CSS = {
|
||||
editorWrapper : 'codex-editor',
|
||||
editorZone : 'ce-redactor'
|
||||
};
|
||||
import Block from '../block';
|
||||
|
||||
/**
|
||||
* @class
|
||||
|
|
@ -54,7 +51,6 @@ let CSS = {
|
|||
* @property {Element} nodes.wrapper - <codex-editor>
|
||||
* @property {Element} nodes.redactor - <ce-redactor>
|
||||
*/
|
||||
|
||||
export default class UI extends Module {
|
||||
|
||||
/**
|
||||
|
|
@ -62,9 +58,9 @@ export default class UI extends Module {
|
|||
*
|
||||
* @param {EditorConfig} config
|
||||
*/
|
||||
constructor(config) {
|
||||
constructor({config}) {
|
||||
|
||||
super(config);
|
||||
super({config});
|
||||
|
||||
this.nodes = {
|
||||
holder: null,
|
||||
|
|
@ -79,45 +75,19 @@ export default class UI extends Module {
|
|||
*/
|
||||
prepare() {
|
||||
|
||||
return new Promise( (resolve, reject) => {
|
||||
|
||||
/**
|
||||
* Element where we need to append CodeX Editor
|
||||
* @type {Element}
|
||||
*/
|
||||
this.nodes.holder = document.getElementById(this.config.holderId);
|
||||
|
||||
if (!this.nodes.holder) {
|
||||
|
||||
reject(Error("Holder wasn't found by ID: #" + this.config.holderId));
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and save main UI elements
|
||||
*/
|
||||
this.nodes.wrapper = $.make('div', CSS.editorWrapper);
|
||||
this.nodes.redactor = $.make('div', CSS.editorZone);
|
||||
|
||||
this.nodes.wrapper.appendChild(this.nodes.redactor);
|
||||
this.nodes.holder.appendChild(this.nodes.wrapper);
|
||||
|
||||
return this.make()
|
||||
/**
|
||||
* Make toolbar
|
||||
*/
|
||||
this.Editor.Toolbar.make();
|
||||
.then(() => this.Editor.Toolbar.make())
|
||||
/**
|
||||
* Load and append CSS
|
||||
*/
|
||||
this.loadStyles();
|
||||
|
||||
resolve();
|
||||
|
||||
})
|
||||
|
||||
/** Add toolbox tools */
|
||||
// .then(addTools_)
|
||||
.then(() => this.loadStyles())
|
||||
/**
|
||||
* Bind events for the UI elements
|
||||
*/
|
||||
.then(() => this.bindEvents())
|
||||
|
||||
/** Make container for inline toolbar */
|
||||
// .then(makeInlineToolbar_)
|
||||
|
|
@ -141,6 +111,58 @@ export default class UI extends Module {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* CodeX Editor UI CSS class names
|
||||
* @return {{editorWrapper: string, editorZone: string, block: string}}
|
||||
*/
|
||||
get CSS() {
|
||||
|
||||
return {
|
||||
editorWrapper : 'codex-editor',
|
||||
editorZone : 'codex-editor__redactor',
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes CodeX Editor interface
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
make() {
|
||||
|
||||
return new Promise( (resolve, reject) => {
|
||||
|
||||
/**
|
||||
* Element where we need to append CodeX Editor
|
||||
* @type {Element}
|
||||
*/
|
||||
this.nodes.holder = document.getElementById(this.config.holderId);
|
||||
|
||||
if (!this.nodes.holder) {
|
||||
|
||||
reject(Error("Holder wasn't found by ID: #" + this.config.holderId));
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
|
||||
/**
|
||||
|
|
@ -162,6 +184,174 @@ export default class UI extends Module {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind events on the CodeX Editor interface
|
||||
*/
|
||||
bindEvents() {
|
||||
|
||||
/**
|
||||
* @todo bind events with the Listeners module
|
||||
*/
|
||||
this.nodes.redactor.addEventListener('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;
|
||||
|
||||
/**
|
||||
* Select clicked Block as Current
|
||||
*/
|
||||
try {
|
||||
|
||||
this.Editor.BlockManager.setCurrentBlockByChildNode(clickedNode);
|
||||
|
||||
/**
|
||||
* If clicked outside first-level Blocks, set Caret to the last empty Block
|
||||
*/
|
||||
|
||||
} catch (e) {
|
||||
|
||||
this.Editor.Caret.setToTheLastBlock();
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
|
||||
// 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 (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();
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// /**
|
||||
|
|
@ -192,54 +382,6 @@ export default class UI extends Module {
|
|||
//
|
||||
// };
|
||||
//
|
||||
// /**
|
||||
// * @private
|
||||
// * Append tools passed in editor.tools
|
||||
// */
|
||||
// var addTools_ = function () {
|
||||
//
|
||||
// var tool,
|
||||
// toolName,
|
||||
// toolButton;
|
||||
//
|
||||
// for ( toolName in editor.settings.tools ) {
|
||||
//
|
||||
// tool = editor.settings.tools[toolName];
|
||||
//
|
||||
// editor.tools[toolName] = tool;
|
||||
//
|
||||
// if (!tool.iconClassname && tool.displayInToolbox) {
|
||||
//
|
||||
// editor.core.log('Toolbar icon classname missed. Tool %o skipped', 'warn', toolName);
|
||||
// continue;
|
||||
//
|
||||
// }
|
||||
//
|
||||
// if (typeof tool.render != 'function') {
|
||||
//
|
||||
// editor.core.log('render method missed. Tool %o skipped', 'warn', toolName);
|
||||
// continue;
|
||||
//
|
||||
// }
|
||||
//
|
||||
// if (!tool.displayInToolbox) {
|
||||
//
|
||||
// continue;
|
||||
//
|
||||
// } else {
|
||||
//
|
||||
// /** if tools is for toolbox */
|
||||
// toolButton = editor.draw.toolbarButton(toolName, tool.iconClassname);
|
||||
//
|
||||
// editor.nodes.toolbox.appendChild(toolButton);
|
||||
//
|
||||
// editor.nodes.toolbarButtons[toolName] = toolButton;
|
||||
//
|
||||
// }
|
||||
//
|
||||
// }
|
||||
//
|
||||
// };
|
||||
//
|
||||
// var addInlineToolbarTools_ = function () {
|
||||
//
|
||||
|
|
|
|||
24
src/components/polyfills.js
Normal file
24
src/components/polyfills.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* Element.closest()
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
|
||||
*/
|
||||
if (!Element.prototype.matches)
|
||||
Element.prototype.matches = Element.prototype.msMatchesSelector ||
|
||||
Element.prototype.webkitMatchesSelector;
|
||||
|
||||
if (!Element.prototype.closest)
|
||||
Element.prototype.closest = function (s) {
|
||||
|
||||
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;
|
||||
|
||||
};
|
||||
|
|
@ -3,6 +3,43 @@
|
|||
*/
|
||||
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';
|
||||
|
||||
if (!args) {
|
||||
|
||||
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 );
|
||||
|
||||
}
|
||||
|
||||
} catch(e) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} ChainData
|
||||
* @property {Object} data - data that will be passed to the success or fallback
|
||||
|
|
|
|||
12
src/styles/block.css
Normal file
12
src/styles/block.css
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
.ce-block {
|
||||
border: 1px dotted #ccc;
|
||||
margin: 2px 0;
|
||||
|
||||
&--selected {
|
||||
background-color: var(--bg-light);
|
||||
}
|
||||
&__content {
|
||||
max-width: var(--content-width);
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +1,5 @@
|
|||
@import url('variables.css');
|
||||
@import url('ui.css');
|
||||
@import url('toolbar.css');
|
||||
@import url('toolbox.css');
|
||||
@import url('block.css');
|
||||
|
|
|
|||
44
src/styles/toolbar.css
Normal file
44
src/styles/toolbar.css
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
.ce-toolbar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 100ms ease;
|
||||
will-change: opacity, transform;
|
||||
|
||||
&--opened {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
&__content {
|
||||
max-width: var(--content-width);
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
&__plus {
|
||||
position: absolute;
|
||||
left: calc(-var(--toolbar-buttons-size) - 10px);
|
||||
display: inline-block;
|
||||
background-color: var(--bg-light);
|
||||
width: var(--toolbar-buttons-size);
|
||||
height: var(--toolbar-buttons-size);
|
||||
line-height: 34px;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
|
||||
&::after {
|
||||
content: '+';
|
||||
font-size: 26px;
|
||||
display: block;
|
||||
margin-top: -2px;
|
||||
margin-right: -2px;
|
||||
}
|
||||
|
||||
&--hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/styles/toolbox.css
Normal file
34
src/styles/toolbox.css
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
.ce-toolbox {
|
||||
visibility: hidden;
|
||||
transition: opacity 100ms ease;
|
||||
will-change: opacity;
|
||||
|
||||
&--opened {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
&__button {
|
||||
display: inline-block;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
background: var(--bg-light);
|
||||
width: var(--toolbar-buttons-size);
|
||||
height: var(--toolbar-buttons-size);
|
||||
border-radius: 30px;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
line-height: var(--toolbar-buttons-size);
|
||||
|
||||
&::before {
|
||||
content: attr(title);
|
||||
font-size: 22px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 1em;
|
||||
font-variant-caps: all-small-caps;
|
||||
padding-left: 11.5px;
|
||||
margin-top: -1px;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,17 @@
|
|||
/**
|
||||
* Editor wrapper
|
||||
*/
|
||||
.codex-editor{
|
||||
.codex-editor {
|
||||
position: relative;
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__redactor {
|
||||
padding-bottom: 300px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,4 +5,14 @@
|
|||
*/
|
||||
--bg-light: #eff2f5;
|
||||
|
||||
/**
|
||||
* Block content width
|
||||
*/
|
||||
--content-width: 650px;
|
||||
|
||||
/**
|
||||
* Toolbar Plus Button and Toolbox buttons height and width
|
||||
*/
|
||||
--toolbar-buttons-size: 34px;
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue