diff --git a/codex-editor.js b/codex-editor.js index b2eefdbf..e10e2d68 100644 --- a/codex-editor.js +++ b/codex-editor.js @@ -7,15 +7,26 @@ var cEditor = (function (cEditor) { // Default settings cEditor.settings = { - tools : ['header', 'picture', 'list', 'quote', 'code', 'twitter', 'instagram', 'smile'], - textareaId : 'codex-editor' + tools : ['header', 'picture', 'list', 'quote', 'code', 'twitter', 'instagram', 'smile'], + textareaId : 'codex-editor', + + // First-level tags viewing as separated blocks. Other'll be inserted as child + blockTags : ['P','BLOCKQUOTE','UL','CODE','OL','H1','H2','H3','H4','H5','H6'] }; // Static nodes cEditor.nodes = { textarea : null, - editor : null, - toolbar : null + wrapper : null, + toolbar : null, + toolbarButtons : {}, // {type : DomEl, ... } + redactor : null + } + + // Current editor state + cEditor.state = { + html : '', + blocks : [] } /** @@ -31,9 +42,10 @@ var cEditor = (function (cEditor) { // Prepare editor settings this.core.prepare(userSettings) - // If all ok, make UI, parse content and bind events + // If all ok, make UI, bind events and parse initial-content .then(this.ui.make) .then(this.ui.bindEvents) + .then(this.parser.parseTextareaContent) .catch(function (error) { cEditor.core.log('Initialization failed with error: %o', 'warn', error); }) @@ -50,7 +62,8 @@ var cEditor = (function (cEditor) { * Methods: * - init * - log -* - el +* - insertAfter +* - isDomNode */ cEditor.core = { @@ -104,26 +117,26 @@ cEditor.core = { }, /** - * Returns element by selector - * @todo Not using now. Check for necessity + * Helper for insert one element after another */ - el : function (selector, parent) { + insertAfter : function (target, element) { + target.parentNode.insertBefore(element, target.nextSibling); + }, - var el = null; + /** + * DOM node types map + */ + nodeTypes : { + TAG : 1, + TEXT : 3, + COMMENT : 8 + }, - parent = parent || document; - - if ( selector.substring(0,1) == '#' ){ - el = parent.getElementById(selector.substring(1)); - if ( typeof el != undefined ) - return el; - } else { - el = parent.querySelectorAll(selector); - if ( el.length !== 0 ) - return el; - } - - return el; + /** + * Check object for DOM node + */ + isDomNode : function (el) { + return el && typeof el === 'object' && el.nodeType && el.nodeType == this.nodeTypes.TAG; } } @@ -135,20 +148,40 @@ cEditor.ui = { */ make : function () { - cEditor.core.log('ui.make fired', 'info'); + var wrapper, + toolbar, + tool, + redactor; - // Making toolbar ... + /** Make editor wrapper */ + wrapper = cEditor.draw.wrapper(); - // Making 'plus' button ... + /** Append editor wrapper after initial textarea */ + cEditor.core.insertAfter(cEditor.nodes.textarea, wrapper); - }, - /** - * Parses input string to HTML editor content - */ - parseContent : function () { + /** Make toolbar and content-editable redactor */ + toolbar = cEditor.draw.toolbar(); + redactor = cEditor.draw.redactor(); - cEditor.core.log('ui.parseContent fired', 'info'); + wrapper.appendChild(toolbar); + wrapper.appendChild(redactor); + + /** Make toolbar buttons */ + cEditor.settings.tools.forEach(function(type) { + + tool = cEditor.draw.toolbarButton(type); + toolbar.appendChild(tool); + + /** Save tools to static nodes */ + cEditor.nodes.toolbarButtons[type] = tool; + + }); + + /** Save created ui-elements to static nodes state */ + cEditor.nodes.wrapper = wrapper; + cEditor.nodes.toolbar = toolbar; + cEditor.nodes.redactor = redactor; }, @@ -161,4 +194,262 @@ cEditor.ui = { } +} + +/** +* Content parsing module +*/ +cEditor.parser = { + + /** + * Asynchronously parses textarea input string to HTML editor blocks + */ + parseTextareaContent : function () { + + var initialContent = cEditor.nodes.textarea.value; + + if ( initialContent.trim().length === 0 ) return true; + + + cEditor.parser + + /** Get child nodes async-aware */ + .getNodesFromString(initialContent) + + /** Then append nodes to the redactor */ + .then(cEditor.parser.appendNodesToRedactor) + + /** Write log if something goes wrong */ + .catch(function(error) { + cEditor.core.log('Error while parsing content: %o', 'warn', error); + }) + + }, + + /** + * Parses string to nodeList + * @param string inputString + * @return Primise -> nodeList + */ + getNodesFromString : function (inputString) { + + return Promise.resolve().then(function() { + + var contentHolder = document.createElement('div'); + + contentHolder.innerHTML = inputString; + + /** + * Returning childNodes will include: + * - Elements (html-tags), + * - Texts (empty-spaces or non-wrapped strings ) + * - Comments and other + */ + return contentHolder.childNodes; + + }); + }, + + /** + * Appends nodes to the redactor + * @param nodeList nodes - list for nodes to append + */ + appendNodesToRedactor : function(nodes) { + + /** + * Sequence of one-by-one nodes appending + * Uses to save blocks order after async-handler + */ + var nodeSequence = Promise.resolve(); + + + for (var index = 0; index < nodes.length ; index++ ) { + + /** Add node to sequence at specified index */ + cEditor.parser.appendNodeAtIndex(nodeSequence, nodes, index); + + } + + }, + + /** + * Append node at specified index + */ + appendNodeAtIndex : function (nodeSequence, nodes, index) { + + /** We need to append node to sequence */ + nodeSequence + + /** first, get node async-aware */ + .then(function() { + + return cEditor.parser.getNodeAsync(nodes , index); + + }) + + /** + * second, compose editor-block from node + * and append it to redactor + */ + .then(function(node){ + + var block = cEditor.parser.createBlockByDomNode(node); + + if ( cEditor.core.isDomNode(block) ) { + + /** Append block to the redactor */ + cEditor.nodes.redactor.appendChild(block); + + /** Save block to the cEditor.state array */ + cEditor.state.blocks.push(block); + }; + + }) + + /** Log if something wrong with node */ + .catch(function(error) { + cEditor.core.log('Node skipped while parsing because %o', 'warn', error); + }); + + }, + + /** + * Asynchronously returns node from nodeList by index + * @return Promise to node + */ + getNodeAsync : function (nodeList, index) { + + return Promise.resolve().then(function() { + + return nodeList.item(index); + + }); + }, + + /** + * Creates editor block by DOM node + * + * First-level blocks (see cEditor.settings.blockTags) saves as-is, + * other wrapps with
-tag + * + * @param DOMnode node + * @return First-level node (paragraph) + */ + createBlockByDomNode : function (node) { + + /** First level nodes already appears as blocks */ + if ( cEditor.parser.isFirstLevelBlock(node) ){ + return node; + } + + /** Other nodes wraps into parent block (paragraph-tag) */ + var parentBlock, + nodeContent = node.textContent.trim(), + isPlainTextNode = node.nodeType != cEditor.core.nodeTypes.TAG; + + + /** Skip empty textNodes with space-symbols */ + if (isPlainTextNode && !nodeContent.length) return null; + + /** Make
tag */
+ parentBlock = cEditor.draw.block('P');
+
+ if (isPlainTextNode){
+ parentBlock.textContent = nodeContent.replace(/(\s){2,}/, '$1'); // remove double spaces
+ } else {
+ parentBlock.appendChild(node);
+ }
+
+ return parentBlock;
+
+ },
+
+ /**
+ * Check DOM node for display style: separated block or child-view
+ */
+ isFirstLevelBlock : function (node) {
+
+ return node.nodeType == cEditor.core.nodeTypes.TAG &&
+ cEditor.settings.blockTags.indexOf(node.tagName) !== -1;
+
+ }
+
+};
+
+/**
+* Creates HTML elements
+*/
+cEditor.draw = {
+
+ /**
+ * Base editor wrapper
+ */
+ wrapper : function () {
+
+ var wrapper = document.createElement('div');
+
+ wrapper.className += 'ce_wrapper';
+
+ return wrapper;
+
+ },
+
+ /**
+ * Content-editable holder
+ */
+ redactor : function () {
+
+ var redactor = document.createElement('div');
+
+ redactor.className += 'ce_redactor';
+ redactor.contentEditable = true;
+
+ return redactor;
+
+ },
+
+ /**
+ * Empty toolbar with toggler
+ */
+ toolbar : function () {
+
+ var bar = document.createElement('div');
+
+ bar.className += 'ce_toolbar';
+
+ /** Toggler button*/
+ bar.innerHTML = '' +
+ ''+
+ '';
+ return bar;
+ },
+
+ /**
+ * Toolbar button
+ */
+ toolbarButton : function (type) {
+
+ var button = document.createElement("li");
+
+ button.dataset.type = type;
+ button.innerHTML = '';
+
+ return button;
+
+ },
+
+ /**
+ * Redactor block
+ */
+ block : function (tagName, content) {
+
+ var node = document.createElement(tagName);
+
+ node.innerHTML = content || '';
+
+ return node;
+
+ }
+
+
}
\ No newline at end of file
diff --git a/editor.css b/editor.css
index b5ec98f2..9c3836b4 100644
--- a/editor.css
+++ b/editor.css
@@ -70,108 +70,48 @@
/* EDITOR */
-.hidden {display: none !important;}
-.hidden_file {position: absolute; opacity: 0; z-index: -1; left:-99999px; }
-
-.codex_editor button{
- border: 0;
- outline: none;
- background: transparent;
-}
-.codex_editor [contenteditable]{
+.ce_redactor {
+ position: relative;
outline: none;
}
-.codex_editor .node{
- position: relative;
- z-index: 5;
- min-height: 20px;
-}
-
-.codex_editor .add_buttons{
- color: #3b4352;
- font-size: 16px;
- /* margin-left: -42px; */
- /* margin-top: -50px; */
- /* margin-bottom: -25px; */
+.ce_toolbar{
position: absolute;
- /* visibility: hidden; */
- opacity: 0;
- transition: opacity .15s ease-in-out;
- top: 0;
- left: -35px;
- white-space: nowrap;
-}
+ z-index: 2;
+
+ margin-left: -45px;
+ transform: translateY(100px);
+
+ overflow: hidden;
-.add_buttons.show {
background: #fff;
- z-index: 10;
- opacity: 1;
+ border: 1px solid #e3e7ee;
+ border-radius: 2px;
+ box-shadow: 0 2px 11px rgba(27,39,54,.11);
+ color: #2e394b;
}
-.add_buttons.show .buttons {display:none;}
-
-.codex_editor .node.selected + .add_buttons{visibility:visible;}
-
-.add_buttons .buttons {
- position: absolute;
- top: 1px;
- background: wheat;
- padding: 2px;
- border-radius: 3px;
- display: none;
-}
-.codex_editor .add_buttons button:hover,
-.codex_editor .add_buttons .focused{
- color: #3770ef;
-}
-.codex_editor .add_buttons button{
- transition: all 150ms ease-in;
- transform: translate3d(-50px, 0 , 0);
- opacity: 0;
- font-size: 14px;
-}
-.codex_editor .buttons_toggled{
- background: #fff;
- z-index: 10;
- opacity: 1;
-}
-.codex_editor .buttons_toggled button{
- opacity: 1;
- transform: translate3d(0,0,0);
-}
-
-
-.codex_editor .toggler{
- display: inline-block;
- font-size: 23px;
- color: #387ff5;
- transition: transform 100ms ease-in;
- cursor: pointer;
-}
-
-.codex_editor .toggler .buttons {
- position: relative;
- top: -2px;
-}
-.codex_editor .buttons_toggled .toggler{
- transform: rotate(45deg);
-}
-
-.codex_editor .buttons_toggled .buttons{
- display: inline;
-}
-
-
+ .ce_toolbar .toggler{
+ color: #3e6dd6
+ }
+ .ce_toolbar .toggler,
+ .ce_toolbar li
+ {
+ display: inline-block;
+ padding: 12px;
+ cursor: pointer;
+ font-size: 14px;
+ }
+ .ce_toolbar .selected,
+ .ce_toolbar li:hover
+ {
+ background: #3e6dd6;
+ color: #e2edff;
+ }
/** Typography styles */
-.codex_editor p{
+.ce_redactor p{
padding: 5px 0;
font-size: 1em;
margin: 0;
-}
-
-.codex_editor {position: relative;}
-
-.codex_editor .ce_content {
}
\ No newline at end of file
diff --git a/example.html b/example.html
index 8f6fc422..36a8f1e8 100644
--- a/example.html
+++ b/example.html
@@ -22,8 +22,10 @@
На днях я получил очередной проект по разработке личного кабинета.
Как обычно, я открыл консоль, чтобы посмотреть историю проекта, ветки и все ли правки закомичены (от слова commit - фиксировать). Однако ничего из этого я не узнал — проект не содержал .git репозитория.
Эта ситуация в очередной раз заставила задуматься о том, как много разработчиков до сих пор не понимают необходимость контролировать изменения в файлах с исходным кодом. А многие и вовсе не знают что это такое, и как этим пользоваться.
Основные преимущества:
+