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:
Murod Khaydarov 2017-12-24 23:05:59 +03:00
commit 14b36c2fa0
32 changed files with 2526 additions and 9403 deletions

View file

@ -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
View 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

View file

@ -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'
// }

View file

@ -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,

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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": {}
}

View file

@ -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'
// };
//
// /**

View file

@ -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;
}

View file

@ -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);
}
}
}

View file

@ -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

View file

@ -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;

View file

@ -118,7 +118,7 @@ module.exports = class Content {
*/
getFirstLevelBlock(node) {
if (!$.isNode(node)) {
if (!$.isElement(node)) {
node = node.parentNode;

View file

@ -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');
}
}
}
/**

View 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 += '|';
}
}

View file

@ -15,9 +15,9 @@ export default class Events extends Module {
/**
* @constructor
*/
constructor(config) {
constructor({config}) {
super(config);
super({config});
this.subscribers = {};
}

View file

@ -12,9 +12,9 @@ export default class Renderer extends Module {
* @constructor
* @param {EditorConfig} config
*/
constructor(config) {
constructor({config}) {
super(config);
super({config});
}

View file

@ -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;

View file

@ -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();
}

View 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();
}
}
}

View file

@ -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];
}
}

View file

@ -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 () {
//

View 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;
};

View file

@ -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
View 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;
}
}

View file

@ -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
View 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
View 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;
}
}
}

View file

@ -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;
}
}

View file

@ -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;
}