From f523879cda9ab2c43a81ad00fe6fd4ebc0a9e50e Mon Sep 17 00:00:00 2001 From: Peter S Date: Fri, 5 Feb 2016 11:25:52 +0300 Subject: [PATCH] Parser and UI modules --- codex-editor.js | 355 +++++++++++++++++++++++++++++++++++++++++++----- editor.css | 122 +++++------------ example.html | 55 ++++---- 3 files changed, 386 insertions(+), 146 deletions(-) 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 @@

-