/** * CodeX Editor * https://ifmo.su/editor * @author CodeX team team@ifmo.su */ var cEditor = (function (cEditor) { // Default settings cEditor.settings = { tools : ['paragraph', '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, wrapper : null, toolbar : null, showSettingsButton : null, blockSettings : null, toolbarButtons : {}, // { type : DomEl, ... } redactor : null, } // Current editor state cEditor.state = { html : '', blocks : [] } /** * Initialization * @uses Promise cEditor.core.prepare * @param {} userSettings are : * - tools [], * - textareaId String * ... */ cEditor.start = function (userSettings) { // Prepare editor settings this.core.prepare(userSettings) // If all ok, make UI, bind events and parse initial-content .then(this.ui.make) .then(this.ui.addTools) .then(this.ui.bindEvents) // .then(this.parser.parseTextareaContent) .then(this.renderer.makeBlocksFromData) .catch(function (error) { cEditor.core.log('Initialization failed with error: %o', 'warn', error); }) }; return cEditor; })({}); /** * Redactor core methods * Methods: * - init * - log * - insertAfter * - isDomNode */ cEditor.core = { /** * Editor preparing method * @return Promise */ prepare : function (userSettings) { return new Promise(function(resolve, reject){ if ( userSettings ) { cEditor.settings.tools = userSettings.tools || cEditor.settings.tools; } if (userSettings.data) { cEditor.state.blocks = userSettings.data; } cEditor.nodes.textarea = document.getElementById(userSettings.textareaId || cEditor.settings.textareaId); if (typeof cEditor.nodes.textarea == undefined || cEditor.nodes.textarea == null) { reject(Error("Textarea wasn't found by ID: #" + userSettings.textareaId)); } else { resolve(); } }); }, /** * Logging method * @param type = ['log', 'info', 'warn'] */ log : function (msg, type, arg) { type = type || 'log'; if (!arg) { arg = msg || 'undefined'; msg = '[codex-editor]: %o'; } else { msg = '[codex-editor]: ' + msg; } try{ if ( 'console' in window && console[ type ] ){ if ( arg ) console[ type ]( msg , arg ); else console[ type ]( msg ); } }catch(e){} }, /** * Helper for insert one element after another */ insertAfter : function (target, element) { target.parentNode.insertBefore(element, target.nextSibling); }, /** * Readable DOM-node types map */ nodeTypes : { TAG : 1, TEXT : 3, COMMENT : 8 }, /** * Readable keys map */ keys : { BACKSPACE: 8, TAB: 9, ENTER: 13, SHIFT: 16, CTRL: 17, ALT: 18, ESC: 27, SPACE: 32, LEFT: 37, UP: 38, DOWN: 40, RIGHT: 39, DELETE: 46, META: 91 }, /** * Check object for DOM node */ isDomNode : function (el) { return el && typeof el === 'object' && el.nodeType && el.nodeType == this.nodeTypes.TAG; } } /** * Methods for parsing JSON reactor data to HTML blocks */ cEditor.renderer = { /** * Asyncronously parses input JSON to redactor blocks */ makeBlocksFromData : function (argument) { Promise.resolve() /** First, get JSON from state */ .then(function() { return cEditor.state.blocks; }) /** Then, start to iterate they */ .then(cEditor.renderer.appendBlocks) /** Write log if something goes wrong */ .catch(function(error) { cEditor.core.log('Error while parsing JSON: %o', 'error', error); }); }, /** * Parses JSON to blocks * @param {object} data * @return Primise -> nodeList */ appendBlocks : function (data) { var blocks = data.items; /** * Sequence of one-by-one blocks appending * Uses to save blocks order after async-handler */ var nodeSequence = Promise.resolve(); for (var index = 0; index < blocks.length ; index++ ) { /** Add node to sequence at specified index */ cEditor.renderer.appendNodeAtIndex(nodeSequence, blocks, index); } }, /** * Append node at specified index */ appendNodeAtIndex : function (nodeSequence, blocks, index) { /** We need to append node to sequence */ nodeSequence /** first, get node async-aware */ .then(function() { return cEditor.renderer.getNodeAsync(blocks , index); }) /** * second, compose editor-block from JSON object */ .then(cEditor.renderer.createBlockFromData) /** * now insert block to redactor */ .then(function(blockData){ /** * blockData has 'block' and 'type' information */ cEditor.content.insertBlock(blockData.block, blockData.type); /** Pass created block to next step */ return blockData.block; }) /** * add handlers to new block */ .then(cEditor.ui.addBlockHandlers) /** Log if something wrong with node */ .catch(function(error) { cEditor.core.log('Node skipped while parsing because %o', 'error', error); }); }, /** * Asynchronously returns block data from blocksList by index * @return Promise to node */ getNodeAsync : function (blocksList, index) { return Promise.resolve().then(function() { return blocksList[index]; }); }, /** * Creates editor block by JSON-data * * @uses render method of each plugin * * @param {object} blockData looks like * { header : { * text: '', * type: 'H3', ... * } * } * @return {object} with type and Element */ createBlockFromData : function (blockData) { /** Get first key of object that stores plugin name */ for (var pluginName in blockData) break; /** Check for plugin existance */ if (!cEditor.tools[pluginName]) { throw Error(`Plugin «${pluginName}» not found`); } /** Check for plugin having render method */ if (typeof cEditor.tools[pluginName].render != 'function') { throw Error(`Plugin «${pluginName}» must have «render» method`); } /** Fire the render method with data */ var block = cEditor.tools[pluginName].render(blockData[pluginName]); /** Retrun type and block */ return { type : pluginName, block : block } }, } /** * Methods for saving HTML blocks to JSON object */ cEditor.saver = { /** * Saves blocks */ saveBlocks : function (argument) { console.info('saver saveBlocks'); } } cEditor.ui = { /** Blocks name. */ BLOCK_CLASSNAME : 'ce_block', /** * Making main interface */ make : function () { var wrapper, toolbar, redactor, blockSettings, showSettingsButton; /** Make editor wrapper */ wrapper = cEditor.draw.wrapper(); /** Append editor wrapper after initial textarea */ cEditor.core.insertAfter(cEditor.nodes.textarea, wrapper); /** Make toolbar and content-editable redactor */ toolbar = cEditor.draw.toolbar(); showSettingsButton = cEditor.draw.settingsButton(); blockSettings = cEditor.draw.blockSettings(); redactor = cEditor.draw.redactor(); toolbar.appendChild(showSettingsButton); toolbar.appendChild(blockSettings); wrapper.appendChild(toolbar); wrapper.appendChild(redactor); /** Save created ui-elements to static nodes state */ cEditor.nodes.wrapper = wrapper; cEditor.nodes.toolbar = toolbar; cEditor.nodes.blockSettings = blockSettings; cEditor.nodes.showSettingsButton = showSettingsButton; cEditor.nodes.redactor = redactor; }, /** * Append tools passed in cEditor.tools */ addTools : function () { var tool, tool_button; /** Make toolbar buttons */ for (var name in cEditor.tools){ tool = cEditor.tools[name]; if (!tool.iconClassname) { cEditor.core.log('Toolbar icon classname missed. Tool %o skipped', 'warn', name); continue; } tool_button = cEditor.draw.toolbarButton(name, tool.iconClassname); cEditor.nodes.toolbar.appendChild(tool_button); /** Save tools to static nodes */ cEditor.nodes.toolbarButtons[name] = tool_button; } }, /** * Bind editor UI events */ bindEvents : function () { cEditor.core.log('ui.bindEvents fired', 'info'); /** All keydowns on Document */ document.addEventListener('keydown', function (event) { cEditor.callback.globalKeydown(event); }, false ); /** All keydowns on Document */ document.addEventListener('keyup', function (event) { cEditor.callback.globalKeyup(event); }, false ); /** Mouse click to radactor */ cEditor.nodes.redactor.addEventListener('click', function (event) { cEditor.callback.redactorClicked(event); cEditor.caret.save(); }, false ); /** Clicks to SETTINGS button in toolbar */ cEditor.nodes.showSettingsButton.addEventListener('click', function (event) { cEditor.callback.showSettingsButtonClicked(event); }, false ); /** * @deprecated; * Any redactor changes: keyboard input, mouse cut/paste, drag-n-drop text */ cEditor.nodes.redactor.addEventListener('input', function (event) { /** Saving caret in every modifications */ cEditor.caret.save(); cEditor.callback.redactorInputEvent(event); }, false ); /** Bind click listeners on toolbar buttons */ for (button in cEditor.nodes.toolbarButtons){ cEditor.nodes.toolbarButtons[button].addEventListener('click', function (event) { cEditor.callback.toolbarButtonClicked(event, this); }, false); }; }, addBlockHandlers : function(block) { if (!block) return; block.addEventListener('keydown', function(event) { cEditor.callback.blockKeydown(event, block); }, false); block.addEventListener('paste', function (event) { cEditor.callback.blockPaste(event, block); }, false); } }; cEditor.callback = { redactorSyncTimeout : null, globalKeydown : function(event){ switch (event.keyCode){ case cEditor.core.keys.TAB : this.tabKeyPressed(event); break; case cEditor.core.keys.ENTER : this.enterKeyPressed(event); break; case cEditor.core.keys.ESC : this.escapeKeyPressed(event); break; } }, globalKeyup : function(event){ switch (event.keyCode){ case cEditor.core.keys.UP : case cEditor.core.keys.LEFT : case cEditor.core.keys.RIGHT : case cEditor.core.keys.DOWN : this.arrowKeyPressed(event); break; } }, tabKeyPressed : function(event){ if ( !cEditor.toolbar.opened ) { cEditor.toolbar.open(); } else { cEditor.toolbar.leaf(); } event.preventDefault(); }, enterKeyPressed : function(event){ cEditor.content.workingNodeChanged(); if ( cEditor.toolbar.opened && event.target == cEditor.content.currentNode) { event.preventDefault(); cEditor.toolbar.toolClicked(event); cEditor.toolbar.close(); }; }, escapeKeyPressed : function(event){ cEditor.toolbar.close(); event.preventDefault(); }, arrowKeyPressed : function(event){ cEditor.content.workingNodeChanged(); /* Closing toolbar */ cEditor.toolbar.close(); cEditor.toolbar.move(); }, redactorClicked : function (event) { cEditor.content.workingNodeChanged(); cEditor.toolbar.move(); cEditor.toolbar.open(); cEditor.toolbar.settings.close(); }, /** * Toolbar button click handler * @param this - cursor to the button */ toolbarButtonClicked : function (event, button) { cEditor.toolbar.current = button.dataset.type; cEditor.toolbar.toolClicked(event); cEditor.toolbar.close(); }, redactorInputEvent : function (event) { /** * Clear previous sync-timeout */ if (this.redactorSyncTimeout){ clearTimeout(this.redactorSyncTimeout); } /** * Start waiting to input finish and sync redactor */ this.redactorSyncTimeout = setTimeout(function() { cEditor.content.sync(); }, 500); }, /** * Block handlers for KeyDown events */ blockKeydown : function(event, block) { switch (event.keyCode){ case cEditor.core.keys.DOWN: case cEditor.core.keys.RIGHT: cEditor.callback.blockRightOrDownArrowPressed(block); break; case cEditor.core.keys.ENTER: cEditor.callback.enterPressed(block); break; case cEditor.core.keys.BACKSPACE: cEditor.callback.backspacePressed(block); break; case cEditor.core.keys.UP: case cEditor.core.keys.LEFT: cEditor.callback.blockLeftOrUpArrowPressed(block); break; } }, /** * RIGHT or DOWN keydowns on block */ blockRightOrDownArrowPressed : function (block) { var selection = window.getSelection(), focusedNode = selection.anchorNode, focusedNodeHolder; /** Check for caret existance */ if (!focusedNode){ return false; } /** Saving caret after keydown event happend */ cEditor.caret.save(); /** Looking for closest (parent) contentEditable element of focused node */ while (focusedNode.className != cEditor.ui.BLOCK_CLASSNAME) { focusedNodeHolder = focusedNode.parentNode; focusedNode = focusedNodeHolder; } /** Founded contentEditable element doesn't have childs */ if (focusedNode.childNodes.length === 0) { cEditor.caret.setToNextBlock(block); return; } /** * Find deepest child node * Iterate child nodes and find LAST DEEPEST node * We need to check caret positon (it must be at the end) * @param focusedNodeIndex is index of childnode by length * @param focusedTextNode is Text node founded by DFS algorithm */ var focusedTextNode = '', focusedNodeIndex = cEditor.caret.focusedNodeIndex + 1; if (focusedNodeHolder.childNodes){ /** Looking from the END of node */ focusedTextNode = cEditor.content.getDeepestTextNodeFromPosition(focusedNodeHolder, focusedNodeHolder.childNodes.length); } /** * Stop transition when caret is not at the end of Text node * When we click "DOWN" or "RIGHT", caret moves to the end of node. * We should check caret position before we transmit/switch the block. */ if ( block.childNodes.length != focusedNodeIndex || focusedTextNode.length != selection.anchorOffset) { return false; } cEditor.caret.setToNextBlock(block); }, /** * LEFT or UP keydowns on block */ blockLeftOrUpArrowPressed : function (block) { var selection = window.getSelection(), focusedNode = selection.anchorNode, focusedNodeHolder; /** Check for caret existance */ if (!focusedNode){ return false; } /** Saving caret after keydown event happend */ cEditor.caret.save(); /** Looking for parent contentEditable block */ while (focusedNode.className != cEditor.ui.BLOCK_CLASSNAME) { focusedNodeHolder = focusedNode.parentNode; focusedNode = focusedNodeHolder; } /** * Find deepest child node * Iterate child nodes and find First DEEPEST node * We need it to check caret positon (it must be at the begining) */ focusedNodeHolder = focusedNodeHolder || focusedNode; if (focusedNodeHolder.childNodes.length !== 0) { var focusedTextNode = ''; if (focusedNodeHolder.childNodes){ /** Looking from the first child */ focusedTextNode = cEditor.content.getDeepestTextNodeFromPosition(focusedNodeHolder, 0); } } /** * When we click "UP" or "LEFT", caret behaviour is as default. * We should check caret position before we transmit/switch the block. */ if ( selection.anchorOffset !== 0) { return false; } /** * We can't switch block till caret is not at the begining of first node and has zero offset */ if ( (cEditor.caret.offset !== 0 || cEditor.caret.focusedNodeIndex !== 0) && focusedNodeHolder.childNodes.length !== 0 ) { return; } cEditor.caret.setToPreviousBlock(block); }, enterPressed: function (block) { var selection = window.getSelection(), currentNode = selection.anchorNode, parentOfFocusedNode = currentNode.parentNode; /** * We add new block with contentEditable property if enter key is pressed. * First we check, if caret is at the end of last node and offset is legth of text node * focusedNodeIndex + 1, because that we compare non-arrays index. */ if ( currentNode.length === cEditor.caret.offset && parentOfFocusedNode.childNodes.length == cEditor.caret.focusedNodeIndex + 1) { /** Prevent
creation */ // event.preventDefault(); /** Create new Block and append it after current */ var newBlock = cEditor.draw.block('p'); newBlock.contentEditable = "true"; newBlock.classList.add(cEditor.ui.BLOCK_CLASSNAME); /** Add event listeners (Keydown) for new created block */ cEditor.ui.addBlockHandlers(newBlock); cEditor.core.insertAfter(block, newBlock); /** set focus to the current (created) block */ cEditor.caret.setToNextBlock(block); cEditor.toolbar.move(); } }, backspacePressed: function (block) { if (block.textContent.trim()) return; cEditor.caret.setToPreviousBlock(block); block.remove(); cEditor.toolbar.move(); event.preventDefault(); }, blockPaste: function(event, block) { var clipboardData, pastedData, nodeContent; event.preventDefault(); clipboardData = event.clipboardData || window.clipboardData; pastedData = clipboardData.getData('Text'); nodeContent = document.createTextNode(pastedData); block.appendChild(nodeContent); }, /** * Clicks on block settings button */ showSettingsButtonClicked : function(){ /** * Get type of current block * It uses to append settings from tool.settings property. * ... * Type is stored in data-type attribute on block */ var currentToolType = cEditor.content.currentNode.dataset.type; cEditor.toolbar.settings.toggle(currentToolType); } }; cEditor.content = { currentNode : null, /** * Synchronizes redactor with original textarea */ sync : function () { cEditor.core.log('syncing...'); /** * Save redactor content to cEditor.state */ cEditor.state.html = cEditor.nodes.redactor.innerHTML; /** * Put it to the textarea */ cEditor.nodes.textarea.value = cEditor.state.html; }, getNodeFocused : function() { var selection = window.getSelection(), focused; if (selection.anchorNode != null) { if ( selection.anchorNode.nodeType == cEditor.core.nodeTypes.TAG ) { focused = selection.anchorNode; } else { focused = selection.focusNode.parentElement; } } if ( !cEditor.parser.isFirstLevelBlock(focused) ) { focused = focused.parentElement; } if (focused != cEditor.nodes.redactor){ return focused; } return null; }, /** * Trigger this event when working node changed */ workingNodeChanged : function (setCurrent) { this.currentNode = setCurrent || this.getNodeFocused(); }, /** * Replaces one redactor block with another * @param {Element} targetBlock - block to replace. Mostly currentNode. * @param {Element} newBlock * @param {string} newBlockType - type of new block; we need to store it to data-attribute * * [!] Function does not saves old block content. * You can get it manually and pass with newBlock.innerHTML */ replaceBlock : function function_name(targetBlock, newBlock, newBlockType) { if (!targetBlock || !newBlock || !newBlockType){ cEditor.core.log('replaceBlock: missed params'); return; } /** Add redactor block classname to new block */ newBlock.classList.add(cEditor.ui.BLOCK_CLASSNAME); /** Store block type */ newBlock.dataset.type = newBlockType; /** Replacing */ cEditor.nodes.redactor.replaceChild(newBlock, targetBlock); /** * Set new node as current */ cEditor.content.workingNodeChanged(newBlock); /** * Setting caret * @todo is it necessary? */ cEditor.caret.set(newBlock); }, insertBlock : function(newBlock, blockType) { var workingNode = cEditor.content.currentNode; newBlock.classList.add(cEditor.ui.BLOCK_CLASSNAME); newBlock.dataset.type = blockType; /** Content-editable blocks. We can set focus to such blocks and hang listeners */ switch (blockType) { case 'header' : case 'paragraph': case 'quote' : case 'list' : cEditor.ui.addBlockHandlers(newBlock); break; } if (workingNode) { cEditor.core.insertAfter(workingNode, newBlock); } else { /** * If redactor is empty, append as first child */ cEditor.nodes.redactor.appendChild(newBlock); /** * Set new node as current */ cEditor.content.workingNodeChanged(newBlock); } }, /** * @deprecated with replaceBlock() */ switchBlock : function (targetBlock, newBlockTagname) { if (!targetBlock || !newBlockTagname) return; var nodeToReplace; /** * First-level nodes replaces as-is, * otherwise we need to replace parent node */ if (cEditor.parser.isFirstLevelBlock(targetBlock)) { nodeToReplace = targetBlock; } else { nodeToReplace = targetBlock.parentNode; } /** * Make new node with original content */ var nodeCreated = cEditor.draw.block(newBlockTagname, targetBlock.innerHTML); /** Mark node as redactor block */ nodeCreated.contentEditable = "true"; nodeCreated.classList.add(cEditor.ui.BLOCK_CLASSNAME); /** * If it is a first-level node, replace as-is. */ if (cEditor.parser.isFirstLevelBlock(nodeCreated)) { cEditor.nodes.redactor.replaceChild(nodeCreated, nodeToReplace); /** * Set new node as current */ cEditor.content.workingNodeChanged(nodeCreated); /** * Setting caret */ cEditor.caret.set(nodeCreated); /** Add event listeners for created node */ cEditor.ui.addBlockHandlers(nodeCreated); return; } /** * If it is not a first-level node, for example LI or IMG * we need to wrap it in block-tag (or
-tag * * @param DOMnode node * @return First-level node (paragraph) */ createBlockByDomNode : function (node) { /** First level nodes already appears as blocks */ if ( cEditor.parser.isFirstLevelBlock(node) ){ /** Save plugin type in data-type */ node = this.storeBlockType(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); } /** Save plugin type in data-type */ parentBlock = this.storeBlockType(parentBlock); return parentBlock; }, /** * It's a crutch * - - - - - - - * We need block type stored as data-attr * Now supports only simple blocks : P, HEADER, QUOTE, CODE * Remove it after updating parser module for the block-oriented structure: * - each block must have stored type * @param {Element} node */ storeBlockType : function (node) { switch (node.tagName) { case 'P' : node.dataset.type = 'paragraph'; break; case 'H1': case 'H2': case 'H3': case 'H4': case 'H5': case 'H6': node.dataset.type = 'header'; break; case 'BLOCKQUOTE': node.dataset.type = 'quote'; break; case 'CODE': node.dataset.type = 'code'; break; } return node; }, /** * 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; } }; cEditor.tools = { }; /** * 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'; return redactor; }, /** * Empty toolbar with toggler */ toolbar : function () { var bar = document.createElement('div'); bar.className += 'ce_toolbar'; return bar; }, /** * Block settings panel */ blockSettings : function () { var settings = document.createElement('div'); settings.className += 'ce_block_settings'; return settings; }, /** * Settings button in toolbar */ settingsButton : function () { var toggler = document.createElement('span'); toggler.className = 'toggler'; /** Toggler button*/ toggler.innerHTML = ''; return toggler; }, /** * Toolbar button */ toolbarButton : function (type, classname) { 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; } } /** * Paragraph Plugin\ * Creates P tag and adds content to this tag */ var paragraphTool = { /** * Make initial header block * @param {object} JSON to with block data * @return {Element} element to append */ makeBlockToAppend : function (data) { var tag = document.createElement('P'); if (data && data.text) { tag.innerHTML = data.text; } tag.contentEditable = true; return tag; }, /** * Method to render HTML block from JSON */ render : function (data) { return paragraphTool.makeBlockToAppend(data); }, /** * Method to extract JSON data from HTML block */ save : function (block){ var data = { text : null }; data.text = blockData.textContent; return data; }, }; /** * Now plugin is ready. * Add it to redactor tools */ cEditor.tools.paragraph = { type : 'paragraph', iconClassname : 'ce-icon-paragraph', append : paragraphTool.makeBlockToAppend(), appendCallback : null, settings : null, render : paragraphTool.render, save : paragraphTool.save }; /** * Example of making plugin * H e a d e r */ var headerTool = { /** * Make initial header block * @param {object} JSON to with block data * @return {Element} element to append */ makeBlockToAppend : function (data) { var availableTypes = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6'], tag; if (data && data.type && availableTypes.includes(data.type)) { tag = document.createElement( data.type ); /** * Save header type in data-attr. * We need it in save method to extract type from HTML to JSON */ tag.dataset.headerData = data.type; } else { tag = document.createElement( 'H2' ); } if (data && data.text) { tag.textContent = data.text; } tag.contentEditable = true; return tag; }, /** * Method to render HTML block from JSON */ render : function (data) { return headerTool.makeBlockToAppend(data); }, /** * Method to extract JSON data from HTML block */ save : function (block){ var data = { type : null, text : null }; data.type = blockData.dataset.headerData; data.text = blockData.textContent; return data; }, /** * Block appending callback */ appendCallback : function (argument) { console.log('header appended...'); }, /** * Settings panel content * - - - - - - - - - - - - - * | настройки H1 H2 H3 | * - - - - - - - - - - - - - * @return {Element} element contains all settings */ makeSettings : function () { var holder = document.createElement('DIV'), caption = document.createElement('SPAN'), types = { H2: 'Заголовок раздела', H3: 'Подзаголовок', H4: 'Заголовок 3-его уровня' }, selectTypeButton; /** Add holder classname */ holder.className = 'ce_plugin_header--settings' /** Add settings helper caption */ caption.textContent = 'Настройки заголовка'; caption.className = 'ce_plugin_header--caption'; holder.appendChild(caption); /** Now add type selectors */ for (var type in types){ selectTypeButton = document.createElement('SPAN'); selectTypeButton.textContent = types[type]; selectTypeButton.className = 'ce_plugin_header--select_button'; this.addSelectTypeClickListener(selectTypeButton, type); holder.appendChild(selectTypeButton); } return holder; }, /** * Binds click event to passed button */ addSelectTypeClickListener : function (el, type) { el.addEventListener('click', function () { headerTool.selectTypeClicked(type); }, false); }, /** * Replaces old header with new type * @params {string} type - new header tagName: H1—H6 */ selectTypeClicked : function (type) { var old_header, new_header; /** Now current header stored as a currentNode */ old_header = cEditor.content.currentNode; /** Making new header */ new_header = document.createElement(type); new_header.innerHTML = old_header.innerHTML; new_header.contentEditable = true; cEditor.content.replaceBlock(old_header, new_header, 'header'); /** Add listeners for Arrow keys*/ cEditor.ui.addBlockHandlers(new_header); /** Close settings after replacing */ cEditor.toolbar.settings.close(); }, }; /** * Now plugin is ready. * Add it to redactor tools */ cEditor.tools.header = { type : 'header', iconClassname : 'ce-icon-header', append : headerTool.makeBlockToAppend(), appendCallback : headerTool.appendCallback, settings : headerTool.makeSettings(), render : headerTool.render, save : headerTool.save }; /** QUOTE PLUGIN **/ var quoteTools = { /** * Make Quote from JSON datasets */ makeBlockToAppend : function(data) { var tag; if (data && data.type) { switch (data.type) { case 'simple': tag = quoteTools.makeSimpleQuote(data); break; case 'withCaption': tag = quoteTools.makeQuoteWithCaption(data); break; case 'withPhoto': tag = quoteTools.makeQuoteWithPhoto(data); break; } } else { tag = document.createElement('BLOCKQUOTE'); tag.contentEditable = 'true'; tag.id = 'ce_quote--text'; } return tag; }, render : function(data) { return quoteTools.makeBlockToAppend(data); }, save : function(block) { /** * Extracts JSON quote data from HTML block * @param {Text} text, {Text} author, {Object} photo */ parsedblock = quoteTools.parseBlockQuote(block); var data = { type : 'quote', style : parsedblock.style, text : parsedblock.quote, author : parsedblock.author, photo : parsedblock.photo, }; return data; }, makeSettings : function(data) { var holder = document.createElement('DIV'), caption = document.createElement('SPAN'), types = { simple : 'Простая цитата', withCaption : 'Цитата с подписью', withPhoto : 'Цитата с фото и ФИО' }, selectTypeButton; /** Add holder classname */ holder.className = 'ce_plugin_quote--settings' /** Add settings helper caption */ caption.textContent = 'Настройки цитат'; caption.className = 'ce_plugin_quote--caption'; holder.appendChild(caption); /** Now add type selectors */ for (var type in types){ selectTypeButton = document.createElement('SPAN'); selectTypeButton.textContent = types[type]; selectTypeButton.className = 'ce_plugin_quote--select_button'; var quoteStyle = quoteTools.selectTypeQuoteStyle(type); quoteTools.addSelectTypeClickListener(selectTypeButton, quoteStyle); holder.appendChild(selectTypeButton); } return holder; }, selectTypeQuoteStyle : function(type) { /** * Choose Quote style to replace */ switch (type) { case 'simple': quoteStyleFunction = quoteTools.makeSimpleQuote; break; case 'withCaption': quoteStyleFunction = quoteTools.makeQuoteWithCaption; break; case 'withPhoto': quoteStyleFunction = quoteTools.makeQuoteWithPhoto; break; } return quoteStyleFunction; }, addSelectTypeClickListener : function(el, quoteStyle) { el.addEventListener('click', function () { /** * Parsing currentNode to JSON. */ var parsedOldQuote = quoteTools.parseBlockQuote(), newStyledQuote = quoteStyle(parsedOldQuote); cEditor.content.replaceBlock(cEditor.content.currentNode, newStyledQuote, 'quote'); cEditor.ui.addBlockHandlers(newStyledQuote); }, false); }, makeSimpleQuote : function(data) { var blockquote = document.createElement('BLOCKQUOTE'); blockquote.innerHTML = data.text || ''; blockquote.classList.add('quoteStyle-simple--blockquote'); blockquote.dataset.quoteStyle = 'simple'; blockquote.id = 'ce_quote--text'; blockquote.contentEditable = 'true'; return blockquote; }, makeQuoteWithCaption : function(data) { var block = document.createElement('BLOCKQUOTE'), quote = document.createElement('DIV'), author = document.createElement('DIV'); /* Creating ContentEditable block for quote */ quote.contentEditable = 'true'; quote.classList.add('quoteStyle-withCaption--blockquote'); quote.innerHTML = data.text; quote.id = 'ce_quote--text'; /* Block for author of quote */ author.contentEditable = 'true'; author.classList.add('quoteStyle-withCaption--author'); author.id = 'ce_quote--author'; author.textContent = data.author; /* Appending created tags */ block.dataset.quoteStyle = 'withCaption'; block.appendChild(quote); block.appendChild(author); return block; }, makeQuoteWithPhoto : function(data) { var block = document.createElement('BLOCKQUOTE'), photo = document.createElement('IMG'), author = document.createElement('DIV'), position = document.createElement('DIV'), quote = document.createElement('DIV'); /* Author of quote — photo */ photo.classList.add('quoteStyle-withPhoto--photo') photo.src = "../img/01.jpg"; /* Author name */ author.contentEditable = 'true'; author.classList.add('quoteStyle-withPhoto--author'); author.id = 'ce_quote--author'; author.textContent = data.author; /* Author's position and job */ position.contentEditable = 'true'; position.classList.add('quoteStyle-withPhoto--position'); position.id = 'ce_quote--position'; position.textContent = data.position; var header = document.createElement('DIV'); header.appendChild(author); header.appendChild(position); /* Quote */ quote.contentEditable = 'true'; quote.classList.add('quoteStyle-withPhoto--quote'); quote.id = 'ce_quote--text'; quote.innerHTML = data.text; block.dataset.quoteStyle = 'withPhoto'; block.appendChild(photo); block.appendChild(header); block.appendChild(quote); return block; }, parseBlockQuote : function(block) { var currentNode = block || cEditor.content.currentNode, photo = currentNode.getElementsByTagName('img')[0], quote = currentNode.querySelector('#ce_quote--text'), author = currentNode.querySelector('#ce_quote--author'), position = currentNode.querySelector('#ce_quote--position'); // this.style = currentNode.dataset.quoteStyle; console.log(currentNode); if ( position ) position = position.textContent; if ( author ) author = author.textContent; if ( quote ) quote = quote.textContent; if ( photo ) photo = photo.src; var data = { text : quote, author : author, position : position, photo : photo, }; return data; }, }; cEditor.tools.quote = { type : 'quote', iconClassname : 'ce-icon-quote', append : quoteTools.makeBlockToAppend(), appendCallback : null, settings : quoteTools.makeSettings(), render : quoteTools.render, save : quoteTools.save, };