diff --git a/ce_interface.js b/ce_interface.js index b089129d..3760bc71 100644 --- a/ce_interface.js +++ b/ce_interface.js @@ -3,6 +3,13 @@ * @author Savchenko Peter (vk.com/specc) */ + /* + * TODO + * выделение нескольких блоков и нажатие энтера - вместо замены новой стро + * + * */ + + var ce = function(settings) { this.resultTextarea = document.getElementById("codex_editor"); @@ -25,6 +32,13 @@ var ce = function(settings) { /** Making a wrapper and interface */ this.makeInterface(); + + /* + * Импорт содержимого textarea в редактор + * */ + this.importHtml(); + + /** Bind all events */ this.bindEvents(); @@ -45,7 +59,7 @@ ce.prototype.BUTTONS_TOGGLED_CLASSNANE = 'buttons_toggled'; ce.prototype.toolbarOpened = false; // Key event constants -ce.prototype.key = { TAB: 9, ENTER: 13, BACKSPACE: 8, DELETE: 46, DOWN: 40, SPACE: 32, ESC: 27, CTRL: 17, META: 91, SHIFT: 16, ALT: 18 }; +ce.prototype.key = { TAB: 9, ENTER: 13, BACKSPACE: 8, DELETE: 46, SPACE: 32, ESC: 27, CTRL: 17, META: 91, SHIFT: 16, ALT: 18, LEFT: 37, UP: 38, DOWN: 40, RIGHT: 39 }; /** * Editor interface drawing @@ -55,15 +69,20 @@ ce.prototype.makeInterface = function () { var wrapper = this.make.editorWrapper(), firstNode = this.make.textNode('Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quia nihil repellendus aut cupiditate reprehenderit sapiente magnam nobis doloremque eaque! Sint nobis assumenda nisi ducimus minima illo tenetur, cumque facilis.' ), - toolbar = this.make.toolbar(); + toolbar = this.make.toolbar(), + editableWrapper; this.wrapper = wrapper; + this.editableWrapper = editableWrapper = wrapper.getElementsByClassName("ce_content")[0]; this.toolbar = toolbar; this.toolbarButtons = this.make.toolbarButtons(this.allTools, this.settings.tools); + toolbar.appendChild(this.toolbarButtons); + + /** Add first node and tool bar*/ - wrapper.appendChild(firstNode); + editableWrapper.appendChild(firstNode); wrapper.appendChild(toolbar); /** Insert Editor after initial textarea. Hide textarea */ @@ -73,29 +92,185 @@ ce.prototype.makeInterface = function () { this.focusNode(firstNode); }; + +/* + * Экспорт разметки в итоговый текстареа + * пока по кнопке "экспорт", потом можно сделать на каждое изменение в редакторе (надо ли это?) + * */ +ce.prototype.exportHtml = function () { + this.resultTextarea.innerHTML = this.editableWrapper.innerHTML; + this.resultTextarea.value = this.editableWrapper.innerHTML; + + return false; +}; + + +/** + * Импорт разметки из итоговой текстареа + * пока по кнопке "импорт", потом можно сделать на каждое изменение в редакторе (надо ли это?) + * + * TODO + * 1) удалить лишние узлы, работа с которыми не предполагается в рамках редактора + * 2) удалить скрипты, стили + * 3) поочищать содержимое узлов от мусора - должен остаться только текст, теги форматирования (жирность и тд) и переносы строк (или их тоже убираем?) + * */ +ce.prototype.importHtml = function () { + var node, body, i, nodeType, tmp; + + /* + * Парсим содержимое textarea. + * Создаем новый документ, получаем указатель на контенейр body. + * */ + tmp = new DOMParser().parseFromString( this.resultTextarea.value, "text/html" ); + body = tmp.getElementsByTagName("body")[0]; + + /* + * Обходим корневые узлы. Проставляем им класс и тип узла. + * */ + for(i = 0; i < body.children.length; i++){ + node = body.children.item(i); + + if (!node.classList.contains("node")) + node.classList.add("node"); + + + switch (node.tagName){ + case "P" : + nodeType = "text"; + break; + + case "H1" : + case "H2" : + case "H3" : + case "H4" : + case "H5" : + case "H6" : + nodeType = "header"; + break; + + case "UL" : + nodeType = "list"; + break; + + case "IMG" : + nodeType = "picture"; + break; + + case "CODE" : + nodeType = "code"; + break; + } + + node.dataset["type"] = nodeType; + } + + this.editableWrapper.innerHTML = body.innerHTML; + +}; + + /** * All events binds in one place */ ce.prototype.bindEvents = function () { - var _this = this; + var _this = this, + selectedNodeClass = "selected"; + + /* + * Экспорт разметки в итоговый textarea по нажатию на кнопку "сохранить". + * Кнопка сохранения должна иметь, так же как и textarea, особенный ID. + * */ + document.getElementById("codex_editor_export_btn").addEventListener('click', function () { + _this.exportHtml.apply(_this) + }); + /** All keydowns on Window */ - window.addEventListener('keydown', function (event) { + document.addEventListener('keydown', function (event) { _this.globalKeydownCallback(event); }, false ); + + /** All mouseover on Window */ + document.addEventListener('mouseover', function (event) { + _this.globalMouseOverCallback(event); + }, false ); + + + /** All mouseout on Window */ + document.addEventListener('mouseout', function (event) { + _this.globalMouseOutCallback(event); + }, false ); + }; + +/** + * All window mouseover handles here +*/ +ce.prototype.globalMouseOverCallback = function (event) { + var sender = event.target; + + if (sender.classList.contains("node") && !this.toolbar.isOpened) { + var toolbar = this.toolbar; + + toolbar.style.top = sender.offsetTop + "px"; + + toolbar.classList.add("show"); + } +}; + + +/** + * All window mouseout handles here +*/ +ce.prototype.globalMouseOutCallback = function (event) { + var sender = event.target; + + if (!this.toolbar.isOpened) { + var toolbar = this.toolbar; + + toolbar.classList.remove("show"); + } +}; + + /** * Sets focus to node conteneditable child * todo depending on node type */ ce.prototype.focusNode = function (node) { + node.focus(); - var contentEditable = node.getElementsByClassName('ce_node_content'); - contentEditable.length && contentEditable[0].focus(); + if (typeof window.getSelection != "undefined" && typeof document.createRange != "undefined") { + var range = document.createRange(); + range.selectNodeContents(node); + range.collapse(false); + var sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + } else if (typeof document.body.createTextRange != "undefined") { + var textRange = document.body.createTextRange(); + textRange.moveToElementText(node); + textRange.collapse(false); + textRange.select(); + } +}; + +/* +* Определяет, есть ли выделенный текст +* */ +ce.prototype.isTextSelected = function(){ + return !!window.getSelection().toString() +}; + +/* +* Определяет, относится ли нажатая кнопка к навигационным +* */ +ce.prototype.isNavigationKey = function(keyCode){ + return keyCode == this.key.LEFT || keyCode == this.key.UP || keyCode == this.key.DOWN || keyCode == this.key.RIGHT }; /** @@ -103,6 +278,12 @@ ce.prototype.focusNode = function (node) { */ ce.prototype.globalKeydownCallback = function (event) { + /** + * Обработка клавиш на панеле добавления + */ + this.processToolBarKeyPressed(event); + + // switch (event.keyCode){ case this.key.TAB : this.tabKeyPressed(event); break; // TAB case this.key.ENTER : this.enterKeyPressed(event); break; // Enter @@ -110,6 +291,61 @@ ce.prototype.globalKeydownCallback = function (event) { }; + +/** +* Обрабатывает нажатие клавиш при открытой панеле добавления +*/ +ce.prototype.processToolBarKeyPressed = function(event){ + if (this.toolbar.isOpened) { + + if (this.isNavigationKey(event.which)) { + + this.moveToolBarButtonFocus(event.which == this.key.LEFT || event.which == this.key.UP ); + + event.preventDefault(); + + } else if (event.which == this.key.ENTER) { + // will process later + } else if (event.which != this.key.TAB && event.which != this.key.SHIFT) { + + this.closeToolBar(); + + } + + } +}; + + + +/** +* Closes tool bar (plus btn) +*/ +ce.prototype.closeToolBar = function(){ + var _this = this, + toolbar = this.toolbar; + + toolbar.isOpened = false; + this.focusedToolbarBtn.classList.remove("focused"); + this.focusedToolbarBtn = false; + + // repair buttons animation - just add css class async + setTimeout(function () { + toolbar.classList.remove("show"); + toolbar.classList.remove(_this.BUTTONS_TOGGLED_CLASSNANE); + }); +}; + + + +/** +* Returns node which is currently focused +*/ +ce.prototype.getFocusedNode = function(){ + var sel = window.getSelection(); + return sel.anchorNode.tagName ? sel.anchorNode : sel.focusNode.parentElement; +}; + + /** * */ @@ -118,26 +354,62 @@ ce.prototype.tabKeyPressed = function(event) { // check if currently focused in contenteditable element if ("BODY" == event.target.tagName) return; - var toolbar = event.target.parentNode.nextSibling, - _this = this; + var _this = this; - toolbar.appendChild(this.toolbarButtons); + var toolbar = this.toolbar; - // repair buttons animation - just add css class async - setTimeout(function () { + if (!toolbar.isOpened) { + var sel = window.getSelection(); + var curNode = sel.anchorNode.tagName ? sel.anchorNode : sel.focusNode.parentElement; - if ( !toolbar.className.includes(_this.BUTTONS_TOGGLED_CLASSNANE) ){ - toolbar.className += ' ' + _this.BUTTONS_TOGGLED_CLASSNANE; - _this.toolbarOpened = true; - } else { - toolbar.className = toolbar.className.replace(_this.BUTTONS_TOGGLED_CLASSNANE, ''); - _this.toolbarOpened = false + toolbar.style.top = curNode.offsetTop + "px"; + + if (!toolbar.classList.contains(_this.BUTTONS_TOGGLED_CLASSNANE)) { + // repair buttons animation - just add css class async + setTimeout(function () { + toolbar.classList.add(_this.BUTTONS_TOGGLED_CLASSNANE) + toolbar.isOpened = true; + }); } - }); + } + + // + this.moveToolBarButtonFocus(event.shiftKey); event.preventDefault(); +}; +/** +* Перемещает фокус на следующую кнопку в панеле добавления (плюс) +* */ +ce.prototype.moveToolBarButtonFocus = function(focusPrev){ + var allButtons = this.toolbarButtons; + + var focusedQuery = allButtons.getElementsByClassName("focused"); + var focused; + + if (focusedQuery.length > 0) { + focused = focusedQuery[0]; + + focused.classList.remove("focused"); + + if (focusPrev) focused = focused.previousSibling; + else focused = focused.nextSibling; + + if (!focused) { + if (focusPrev) focused = allButtons.lastChild; + else focused = allButtons.firstChild; + } + + focused.classList.add("focused"); + } else { + focused = allButtons.firstChild; + + focused.classList.add("focused"); + } + + this.focusedToolbarBtn = focused; }; /** @@ -145,33 +417,80 @@ ce.prototype.tabKeyPressed = function(event) { */ ce.prototype.enterKeyPressed = function(event) { - if (event.shiftKey){ - document.execCommand('insertHTML', false, '

'); - } else { - var newNode = this.make.textNode(), - toolbar = this.make.toolbar(); + var _this = this, + curNode = this.getFocusedNode(); - /** Add node */ - this.wrapper.insertBefore(newNode, event.target.parentNode.nextSibling); + /* + * обработка выбранной кнопки тулбара + * */ + if (this.toolbar.isOpened) { - /** Add toolbar to node */ - this.wrapper.insertBefore(toolbar, newNode); + switch ( this.focusedToolbarBtn.dataset["type"] ){ + + case "header": + var header = this.make.headerNode(); + + if (curNode.textContent){ + + header.textContent = curNode.textContent; + curNode.textContent = ""; + + // insert before, if curNode is paragraph or header or some other text-editable node + if (curNode.dataset["type"] == "text"){ + curNode.parentNode.insertBefore(header, curNode); + curNode.remove(); + } + // else insert header node after + else + curNode.parentNode.insertBefore(header, curNode.nextSibling); + + } else { + + curNode.parentNode.insertBefore(header, curNode); + curNode.remove(); + + } + + this.focusNode(header); + + break; + + } + + this.closeToolBar(); + + // TODO do the same by mouse clicking on any toolbar btn + + event.preventDefault(); - /** Set auto focus */ - var contentEditable = newNode.getElementsByClassName('ce_node_content'); - contentEditable.length && contentEditable[0].focus(); } + /* + * Перехват создания нового параграфа при нахождении в заголовке. + * По-умолчанию создается просто div. + * */ + else { - event.preventDefault(); + if (curNode.dataset["type"] == "header" && !this.isTextSelected()) { + var newNode = this.make.textNode(); + + /** Add node */ + this.editableWrapper.insertBefore(newNode, curNode.nextSibling); + + /** Set auto focus */ + setTimeout(function () { + + _this.focusNode(newNode); + }); + + event.preventDefault(); + } + } }; /** * Creates HTML elements */ ce.prototype.make = function () { - - var _this = this; - /** Empty toolbar with toggler */ function toolbar () { @@ -224,19 +543,42 @@ ce.prototype.make = function () { */ function textNode (content){ - var node = document.createElement('div'); + var node = document.createElement('p'); - node.className += 'node'; - node.innerHTML = '

' + (content || '') + '

'; + node.classList.add("node"); + node.dataset["type"] = "text"; + + node.innerHTML = content || ''; + + return node; + } + + /** + * Header node + */ + function headerNode (content){ + + var node = document.createElement('h2'); + + node.classList.add("node"); + node.dataset["type"] = "header"; + + node.innerHTML = content || ''; return node; } function editorWrapper () { - var wrapper = document.createElement('div'); + var wrapper = document.createElement('div'), + editable_wrapper = document.createElement('div'); + + + editable_wrapper.className += 'ce_content'; + editable_wrapper.setAttribute("contenteditable", "true"); wrapper.className += 'codex_editor'; + wrapper.appendChild(editable_wrapper); return wrapper; } @@ -245,29 +587,11 @@ ce.prototype.make = function () { this.toolbar = toolbar; this.toolbarButtons = toolbarButtons; this.toolbarButton = toolbarButton; - this.textNode = textNode; this.editorWrapper = editorWrapper; + this.textNode = textNode; + this.headerNode = headerNode; }; return new ceMake(); -}(); - - - - - -/** -* Polyfilling ECMAScript 6 method String.includes -* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes#Browser_compatibility -*/ -if ( !String.prototype.includes ) { - - String.prototype.includes = function() { - - 'use strict'; - - return String.prototype.indexOf.apply(this, arguments) !== -1; - - }; -} \ No newline at end of file +}(); \ No newline at end of file diff --git a/editor.css b/editor.css index b03455c9..1e1d29a1 100644 --- a/editor.css +++ b/editor.css @@ -85,20 +85,40 @@ .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; - position: relative; + /* margin-left: -42px; */ + /* margin-top: -50px; */ + /* margin-bottom: -25px; */ + position: absolute; + /* visibility: hidden; */ + opacity: 0; + transition: opacity .15s ease-in-out; + top: 0; + left: -35px; + white-space: nowrap; } +.add_buttons.show { + background: #fff; + z-index: 10; + opacity: 1; +} +.add_buttons.show .buttons {display:none;} + +.codex_editor .node.selected + .add_buttons{visibility:visible;} + .add_buttons .buttons { position: absolute; - top: 3px; + top: 1px; + background: wheat; + padding: 2px; + border-radius: 3px; + display: none; } .codex_editor .add_buttons button:hover, .codex_editor .add_buttons .focused{ @@ -113,6 +133,7 @@ .codex_editor .buttons_toggled{ background: #fff; z-index: 10; + opacity: 1; } .codex_editor .buttons_toggled button{ opacity: 1; @@ -125,6 +146,7 @@ font-size: 23px; color: #387ff5; transition: transform 100ms ease-in; + cursor: pointer; } .codex_editor .toggler .buttons { @@ -135,8 +157,21 @@ transform: rotate(45deg); } +.codex_editor .buttons_toggled .buttons{ + display: inline; +} + + + + /** Typography styles */ .codex_editor p{ - margin: 30px 0; + padding: 5px 0; font-size: 17px; + 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 3e57bf74..18040ed8 100644 --- a/example.html +++ b/example.html @@ -10,9 +10,28 @@

CodeX Editor

+ +
+
- +
@@ -58,7 +77,7 @@ /** Document is ready */ ready(function() { window.cEditor = new ce({ - tools : ['header', 'picture'] + tools : ['header'] }); })