+ *
+ * @return {Element[]}
+ */
+
+ }, {
+ key: 'getHigherLevelSiblings',
+ value: function getHigherLevelSiblings(from, direction) {
+ var current = from,
+ siblings = [];
+
+ /**
+ * Find passed node's firs-level parent (in example - blockquote)
+ */
+ while (current.parentNode && current.parentNode.contentEditable !== 'true') {
+ current = current.parentNode;
+ }
+
+ var sibling = direction === 'left' ? 'previousSibling' : 'nextSibling';
+
+ /**
+ * Find all left/right siblings
+ */
+ while (current[sibling]) {
+ current = current[sibling];
+ siblings.push(current);
+ }
+
+ return siblings;
+ }
+
+ /**
+ * Set's caret to the next Block
+ * Before moving caret, we should check if caret position is at the end of Plugins node
+ * Using {@link Dom#getDeepestNode} to get a last node and match with current selection
+ *
+ * @param {Boolean} force - force navigation even if caret is not at the end
+ *
+ * @return {Boolean}
+ */
+
+ }, {
+ key: 'navigateNext',
+ value: function navigateNext() {
+ var force = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
+
+ var nextBlock = this.Editor.BlockManager.nextBlock;
+
+ if (!nextBlock) {
+ return false;
+ }
+
+ if (force || this.isAtEnd) {
+ this.setToBlock(nextBlock);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Set's caret to the previous Block
+ * Before moving caret, we should check if caret position is start of the Plugins node
+ * Using {@link Dom#getDeepestNode} to get a last node and match with current selection
+ *
+ * @param {Boolean} force - force navigation even if caret is not at the start
+ *
+ * @return {Boolean}
+ */
+
+ }, {
+ key: 'navigatePrevious',
+ value: function navigatePrevious() {
+ var force = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
+
+ var previousBlock = this.Editor.BlockManager.previousBlock;
+
+ if (!previousBlock) {
+ return false;
+ }
+
+ if (force || this.isAtStart) {
+ this.setToBlock(previousBlock, 0, true);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Get's deepest first node and checks if offset is zero
+ * @return {boolean}
+ */
+
+ }, {
+ key: 'createShadow',
+
+
+ /**
+ * Inserts shadow element after passed element where caret can be placed
+ * @param {Node} element
+ */
+ value: function createShadow(element) {
+ var shadowCaret = document.createElement('span');
+
+ shadowCaret.classList.add(Caret.CSS.shadowCaret);
+ element.insertAdjacentElement('beforeEnd', shadowCaret);
+ }
+
+ /**
+ * Restores caret position
+ * @param {Node} element
+ */
+
+ }, {
+ key: 'restoreCaret',
+ value: function restoreCaret(element) {
+ var shadowCaret = element.querySelector('.' + Caret.CSS.shadowCaret);
+
+ if (!shadowCaret) {
+ return;
+ }
+
+ /**
+ * After we set the caret to the required place
+ * we need to clear shadow caret
+ *
+ * - make new range
+ * - select shadowed span
+ * - use extractContent to remove it from DOM
+ */
+ var sel = new _selection2.default();
+
+ sel.expandToTag(shadowCaret);
+
+ setTimeout(function () {
+ var newRange = document.createRange();
+
+ newRange.selectNode(shadowCaret);
+ newRange.extractContents();
+ }, 50);
+ }
+ }, {
+ key: 'isAtStart',
+ get: function get() {
+ /**
+ * Don't handle ranges
+ */
+ if (!_selection2.default.isCollapsed) {
+ return false;
+ }
+
+ var selection = _selection2.default.get(),
+ anchorNode = selection.anchorNode,
+ firstNode = $.getDeepestNode(this.Editor.BlockManager.currentBlock.pluginsContent);
+
+ /**
+ * Workaround case when caret in the text like " |Hello!"
+ * selection.anchorOffset is 1, but real caret visible position is 0
+ * @type {number}
+ */
+ var firstLetterPosition = anchorNode.textContent.search(/\S/);
+
+ if (firstLetterPosition === -1) {
+ // empty text
+ firstLetterPosition = 0;
+ }
+
+ /**
+ * In case of
+ *
+ *
<-- first (and deepest) node is
+ * |adaddad <-- anchor node
+ *
+ */
+ if ($.isEmpty(firstNode)) {
+ var leftSiblings = this.getHigherLevelSiblings(anchorNode, 'left'),
+ nothingAtLeft = leftSiblings.every(function (node) {
+ return $.isEmpty(node);
+ });
+
+ if (nothingAtLeft && selection.anchorOffset === firstLetterPosition) {
+ return true;
+ }
+ }
+
+ /**
+ * We use <= comparison for case:
+ * "| Hello" <--- selection.anchorOffset is 0, but firstLetterPosition is 1
+ */
+ return firstNode === null || anchorNode === firstNode && selection.anchorOffset <= firstLetterPosition;
+ }
+
+ /**
+ * Get's deepest last node and checks if offset is last node text length
+ * @return {boolean}
+ */
+
+ }, {
+ key: 'isAtEnd',
+ get: function get() {
+ /**
+ * Don't handle ranges
+ */
+ if (!_selection2.default.isCollapsed) {
+ return false;
+ }
+
+ var selection = _selection2.default.get(),
+ anchorNode = selection.anchorNode,
+ lastNode = $.getDeepestNode(this.Editor.BlockManager.currentBlock.pluginsContent, true);
+
+ /**
+ * In case of
+ *
\r\n *\r\n * @return {Element[]}\r\n */\r\n getHigherLevelSiblings(from, direction ) {\r\n let current = from,\r\n siblings = [];\r\n\r\n /**\r\n * Find passed node's firs-level parent (in example - blockquote)\r\n */\r\n while (current.parentNode && current.parentNode.contentEditable !== 'true') {\r\n current = current.parentNode;\r\n }\r\n\r\n let sibling = direction === 'left' ? 'previousSibling' : 'nextSibling';\r\n\r\n /**\r\n * Find all left/right siblings\r\n */\r\n while (current[sibling]) {\r\n current = current[sibling];\r\n siblings.push(current);\r\n }\r\n\r\n return siblings;\r\n }\r\n\r\n /**\r\n * Set's caret to the next Block\r\n * Before moving caret, we should check if caret position is at the end of Plugins node\r\n * Using {@link Dom#getDeepestNode} to get a last node and match with current selection\r\n *\r\n * @param {Boolean} force - force navigation even if caret is not at the end\r\n *\r\n * @return {Boolean}\r\n */\r\n navigateNext(force = false) {\r\n let nextBlock = this.Editor.BlockManager.nextBlock;\r\n\r\n if (!nextBlock) {\r\n return false;\r\n }\r\n\r\n if (force || this.isAtEnd) {\r\n this.setToBlock(nextBlock);\r\n return true;\r\n }\r\n\r\n return false;\r\n }\r\n\r\n /**\r\n * Set's caret to the previous Block\r\n * Before moving caret, we should check if caret position is start of the Plugins node\r\n * Using {@link Dom#getDeepestNode} to get a last node and match with current selection\r\n *\r\n * @param {Boolean} force - force navigation even if caret is not at the start\r\n *\r\n * @return {Boolean}\r\n */\r\n navigatePrevious(force = false) {\r\n let previousBlock = this.Editor.BlockManager.previousBlock;\r\n\r\n if (!previousBlock) {\r\n return false;\r\n }\r\n\r\n if (force || this.isAtStart) {\r\n this.setToBlock( previousBlock, 0, true );\r\n return true;\r\n }\r\n\r\n return false;\r\n }\r\n\r\n /**\r\n * Get's deepest first node and checks if offset is zero\r\n * @return {boolean}\r\n */\r\n get isAtStart() {\r\n /**\r\n * Don't handle ranges\r\n */\r\n if (!Selection.isCollapsed) {\r\n return false;\r\n }\r\n\r\n let selection = Selection.get(),\r\n anchorNode = selection.anchorNode,\r\n firstNode = $.getDeepestNode(this.Editor.BlockManager.currentBlock.pluginsContent);\r\n\r\n /**\r\n * Workaround case when caret in the text like \" |Hello!\"\r\n * selection.anchorOffset is 1, but real caret visible position is 0\r\n * @type {number}\r\n */\r\n let firstLetterPosition = anchorNode.textContent.search(/\\S/);\r\n\r\n if (firstLetterPosition === -1) { // empty text\r\n firstLetterPosition = 0;\r\n }\r\n\r\n /**\r\n * In case of\r\n *
\r\n *
<-- first (and deepest) node is \r\n * |adaddad <-- anchor node\r\n *
\r\n */\r\n if ($.isEmpty(firstNode)) {\r\n let leftSiblings = this.getHigherLevelSiblings(anchorNode, 'left'),\r\n nothingAtLeft = leftSiblings.every( node => $.isEmpty(node) );\r\n\r\n\r\n\r\n if (nothingAtLeft && selection.anchorOffset === firstLetterPosition) {\r\n return true;\r\n }\r\n }\r\n\r\n /**\r\n * We use <= comparison for case:\r\n * \"| Hello\" <--- selection.anchorOffset is 0, but firstLetterPosition is 1\r\n */\r\n return firstNode === null || anchorNode === firstNode && selection.anchorOffset <= firstLetterPosition;\r\n }\r\n\r\n /**\r\n * Get's deepest last node and checks if offset is last node text length\r\n * @return {boolean}\r\n */\r\n get isAtEnd() {\r\n /**\r\n * Don't handle ranges\r\n */\r\n if (!Selection.isCollapsed) {\r\n return false;\r\n }\r\n\r\n let selection = Selection.get(),\r\n anchorNode = selection.anchorNode,\r\n lastNode = $.getDeepestNode(this.Editor.BlockManager.currentBlock.pluginsContent, true);\r\n\r\n /**\r\n * In case of\r\n *
\r\n * adaddad| <-- anchor node\r\n *
<-- first (and deepest) node is \r\n *
\r\n */\r\n if ($.isEmpty(lastNode)) {\r\n let leftSiblings = this.getHigherLevelSiblings(anchorNode, 'right'),\r\n nothingAtRight = leftSiblings.every( node => $.isEmpty(node) );\r\n\r\n if (nothingAtRight && selection.anchorOffset === anchorNode.textContent.length) {\r\n return true;\r\n }\r\n }\r\n\r\n /**\r\n * Workaround case:\r\n * hello | <--- anchorOffset will be 5, but textContent.length will be 6.\r\n * Why not regular .trim():\r\n * in case of ' hello |' trim() will also remove space at the beginning, so length will be lower than anchorOffset\r\n */\r\n let rightTrimmedText = lastNode.textContent.replace(/\\s+$/, '');\r\n\r\n /**\r\n * We use >= comparison for case:\r\n * \"Hello |\" <--- selection.anchorOffset is 7, but rightTrimmedText is 6\r\n */\r\n return anchorNode === lastNode && selection.anchorOffset >= rightTrimmedText.length;\r\n }\r\n\r\n /**\r\n * Inserts shadow element after passed element where caret can be placed\r\n * @param {Node} element\r\n */\r\n createShadow(element) {\r\n let shadowCaret = document.createElement('span');\r\n\r\n shadowCaret.classList.add(Caret.CSS.shadowCaret);\r\n element.insertAdjacentElement('beforeEnd', shadowCaret);\r\n }\r\n\r\n /**\r\n * Restores caret position\r\n * @param {Node} element\r\n */\r\n restoreCaret(element) {\r\n let shadowCaret = element.querySelector(`.${Caret.CSS.shadowCaret}`);\r\n\r\n if (!shadowCaret) {\r\n return;\r\n }\r\n\r\n /**\r\n * After we set the caret to the required place\r\n * we need to clear shadow caret\r\n *\r\n * - make new range\r\n * - select shadowed span\r\n * - use extractContent to remove it from DOM\r\n */\r\n let sel = new Selection();\r\n\r\n sel.expandToTag(shadowCaret);\r\n\r\n setTimeout(() => {\r\n let newRange = document.createRange();\r\n\r\n newRange.selectNode(shadowCaret);\r\n newRange.extractContents();\r\n }, 50);\r\n }\r\n}\r\n","/**\r\n * @module eventDispatcher\r\n *\r\n * Has two important methods:\r\n * - {Function} on - appends subscriber to the event. If event doesn't exist - creates new one\r\n * - {Function} emit - fires all subscribers with data\r\n * - {Function off - unsubsribes callback\r\n *\r\n * @version 1.0.0\r\n *\r\n * @typedef {Events} Events\r\n * @property {Object} subscribers - all subscribers grouped by event name\r\n */\r\nexport default class Events extends Module {\r\n /**\r\n * @constructor\r\n */\r\n constructor({config}) {\r\n super({config});\r\n this.subscribers = {};\r\n }\r\n\r\n /**\r\n * Subscribe any event on callback\r\n *\r\n * @param {String} eventName - event name\r\n * @param {Function} callback - subscriber\r\n */\r\n on(eventName, callback) {\r\n if (!(eventName in this.subscribers)) {\r\n this.subscribers[eventName] = [];\r\n }\r\n\r\n // group by events\r\n this.subscribers[eventName].push(callback);\r\n }\r\n\r\n /**\r\n * Emit callbacks with passed data\r\n *\r\n * @param {String} eventName - event name\r\n * @param {Object} data - subscribers get this data when they were fired\r\n */\r\n emit(eventName, data) {\r\n if (!this.subscribers[eventName]) {\r\n return;\r\n }\r\n\r\n this.subscribers[eventName].reduce(function (previousData, currentHandler) {\r\n let newData = currentHandler(previousData);\r\n\r\n return newData ? newData : previousData;\r\n }, data);\r\n }\r\n\r\n /**\r\n * Unsubsribe callback from event\r\n *\r\n * @param eventName\r\n * @param callback\r\n */\r\n off(eventName, callback) {\r\n for(let i = 0; i < this.subscribers[eventName].length; i++) {\r\n if (this.subscribers[eventName][i] === callback) {\r\n delete this.subscribers[eventName][i];\r\n break;\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Destroyer\r\n * clears subsribers list\r\n */\r\n destroy() {\r\n this.subscribers = null;\r\n }\r\n}\r\n","/**\r\n * Codex Editor Listeners module\r\n *\r\n * @module Listeners\r\n *\r\n * Module-decorator for event listeners assignment\r\n *\r\n * @author Codex Team\r\n * @version 2.0.0\r\n */\r\n\r\n/**\r\n * @typedef {Listeners} Listeners\r\n * @property {Array} allListeners\r\n */\r\nexport default class Listeners extends Module {\r\n /**\r\n * @constructor\r\n * @param {EditorConfig} config\r\n */\r\n constructor({config}) {\r\n super({config});\r\n this.allListeners = [];\r\n }\r\n\r\n /**\r\n * Assigns event listener on element\r\n *\r\n * @param {Element} element - DOM element that needs to be listened\r\n * @param {String} eventType - event type\r\n * @param {Function} handler - method that will be fired on event\r\n * @param {Boolean} useCapture - use event bubbling\r\n */\r\n on(element, eventType, handler, useCapture = false) {\r\n let assignedEventData = {\r\n element,\r\n eventType,\r\n handler,\r\n useCapture\r\n };\r\n\r\n let alreadyExist = this.findOne(element, eventType, handler);\r\n\r\n if (alreadyExist) return;\r\n\r\n this.allListeners.push(assignedEventData);\r\n element.addEventListener(eventType, handler, useCapture);\r\n }\r\n\r\n /**\r\n * Removes event listener from element\r\n *\r\n * @param {Element} element - DOM element that we removing listener\r\n * @param {String} eventType - event type\r\n * @param {Function} handler - remove handler, if element listens several handlers on the same event type\r\n * @param {Boolean} useCapture - use event bubbling\r\n */\r\n off(element, eventType, handler, useCapture = false) {\r\n let existingListeners = this.findAll(element, eventType, handler);\r\n\r\n for (let i = 0; i < existingListeners.length; i++) {\r\n let index = this.allListeners.indexOf(existingListeners[i]);\r\n\r\n if (index > 0) {\r\n this.allListeners.splice(index, 1);\r\n }\r\n }\r\n\r\n element.removeEventListener(eventType, handler, useCapture);\r\n }\r\n\r\n /**\r\n * Search method: looks for listener by passed element\r\n * @param {Element} element - searching element\r\n * @returns {Array} listeners that found on element\r\n */\r\n findByElement(element) {\r\n let listenersOnElement = [];\r\n\r\n for (let i = 0; i < this.allListeners.length; i++) {\r\n let listener = this.allListeners[i];\r\n\r\n if (listener.element === element) {\r\n listenersOnElement.push(listener);\r\n }\r\n }\r\n\r\n return listenersOnElement;\r\n }\r\n\r\n /**\r\n * Search method: looks for listener by passed event type\r\n * @param {String} eventType\r\n * @return {Array} listeners that found on element\r\n */\r\n findByType(eventType) {\r\n let listenersWithType = [];\r\n\r\n for (let i = 0; i < this.allListeners.length; i++) {\r\n let listener = this.allListeners[i];\r\n\r\n if (listener.type === eventType) {\r\n listenersWithType.push(listener);\r\n }\r\n }\r\n\r\n return listenersWithType;\r\n }\r\n\r\n /**\r\n * Search method: looks for listener by passed handler\r\n * @param {Function} handler\r\n * @return {Array} listeners that found on element\r\n */\r\n findByHandler(handler) {\r\n let listenersWithHandler = [];\r\n\r\n for (let i = 0; i < this.allListeners.length; i++) {\r\n let listener = this.allListeners[i];\r\n\r\n if (listener.handler === handler) {\r\n listenersWithHandler.push(listener);\r\n }\r\n }\r\n\r\n return listenersWithHandler;\r\n }\r\n\r\n /**\r\n * @param {Element} element\r\n * @param {String} eventType\r\n * @param {Function} handler\r\n * @return {Element|null}\r\n */\r\n findOne(element, eventType, handler) {\r\n let foundListeners = this.findAll(element, eventType, handler);\r\n\r\n return foundListeners.length > 0 ? foundListeners[0] : null;\r\n }\r\n\r\n /**\r\n * @param {Element} element\r\n * @param {String} eventType\r\n * @param {Function} handler\r\n * @return {Array}\r\n */\r\n findAll(element, eventType, handler) {\r\n let found,\r\n foundByElements = element ? this.findByElement(element) : [];\r\n // foundByEventType = eventType ? this.findByType(eventType) : [],\r\n // foundByHandler = handler ? this.findByHandler(handler) : [];\r\n\r\n if (element && eventType && handler) {\r\n found = foundByElements.filter( event => event.eventType === eventType && event.handler === handler );\r\n } else if (element && eventType) {\r\n found = foundByElements.filter( event => event.eventType === eventType);\r\n } else {\r\n found = foundByElements;\r\n }\r\n\r\n return found;\r\n }\r\n\r\n /**\r\n * Removes all listeners\r\n */\r\n removeAll() {\r\n this.allListeners.map( (current) => {\r\n current.element.removeEventListener(current.eventType, current.handler);\r\n });\r\n\r\n this.allListeners = [];\r\n }\r\n}\r\n","/**\n * @class Paste\n * @classdesc Contains methods to handle paste on editor\n *\n * @module Paste\n *\n * @version 2.0.0\n */\n\nimport {IBlockToolData} from '../interfaces/block-tool';\n\ndeclare const Module: any;\ndeclare const $: any;\ndeclare const _: any;\n\n/**\n * Tag substitute object.\n *\n * @param {string} tool - name of related Tool\n * @param {Function} handler - callback to handle pasted element\n */\ninterface ITagSubstitute {\n tool: string;\n handler: (element: HTMLElement) => IBlockToolData;\n}\n\n/**\n * Pattern substitute object.\n *\n * @param {string} key - pattern`s key\n * @param {RegExp} pattern - pasted pattern\n * @param {Function} handler - callback to handle pasted pattern\n * @param {string} tool - name of related Tool\n */\ninterface IPatternSubstitute {\n key: string;\n pattern: RegExp;\n handler: (text: string, key: string) => IBlockToolData;\n tool: string;\n}\n\n/**\n * Processed paste data object.\n *\n * @param {string} tool - name of related Tool\n * @param {HTMLElement} content - processed pasted content\n * @param {boolean} isBlock - true if content should be inserted as new Block\n * @param {Function} handler - callback that returns pasted data in IBlockToolData format\n */\ninterface IPasteData {\n tool: string;\n content: HTMLElement;\n isBlock: boolean;\n handler: (content: HTMLElement|string, patten?: RegExp) => IBlockToolData;\n}\n\nexport default class Paste extends Module {\n\n /** If string`s length is greater than this number we don't check paste patterns */\n public static readonly PATTERN_PROCESSING_MAX_LENGTH = 450;\n\n /**\n * Tags` substitutions parameters\n */\n private toolsTags: {[tag: string]: ITagSubstitute} = {};\n\n /** Patterns` substitutions parameters */\n private toolsPatterns: IPatternSubstitute[] = [];\n\n /**\n * @constructor\n */\n constructor({config}) {\n super({config});\n }\n\n public async prepare(): Promise {\n this.setCallback();\n this.processTools();\n }\n\n /**\n * Set onPaste callback handler\n */\n private setCallback(): void {\n const {Listeners, UI} = this.Editor;\n Listeners.on(UI.nodes.redactor, 'paste', this.processPastedData);\n }\n\n /**\n * Get and process tool`s paste configs\n */\n private processTools(): void {\n const tools = this.Editor.Tools.blockTools;\n Object.entries(tools).forEach(this.processTool);\n }\n\n /**\n * Process paste config for each tools\n *\n * @param {string} tool\n */\n private processTool = ([name, tool]) => {\n\n const toolPasteConfig = tool.onPaste || {};\n\n if (!toolPasteConfig.handler) {\n _.log(\n `\"${name}\" Tool MUST provide paste handler.`,\n 'warn',\n );\n }\n\n if (typeof toolPasteConfig.handler !== 'function') {\n _.log(\n `Paste handler for \"${name}\" Tool should be a function.`,\n 'warn',\n );\n } else {\n const tags = toolPasteConfig.tags || [];\n\n tags.forEach((tag) => {\n if (this.toolsTags.hasOwnProperty(tag)) {\n _.log(\n `Paste handler for \"${name}\" Tool on \"${tag}\" tag is skipped ` +\n `because it is already used by \"${this.toolsTags[tag].tool}\" Tool.`,\n 'warn',\n );\n return;\n }\n\n this.toolsTags[tag] = {\n handler: toolPasteConfig.handler,\n tool: name,\n };\n });\n }\n\n if (!toolPasteConfig.patternHandler || _.isEmpty(toolPasteConfig.patterns)) {\n return;\n }\n\n if (typeof toolPasteConfig.patternHandler !== 'function') {\n _.log(\n `Pattern parser for \"${name}\" Tool should be a function.`,\n 'warn',\n );\n } else {\n Object.entries(toolPasteConfig.patterns).forEach(([key, pattern]: [string, RegExp]) => {\n /** Still need to validate pattern as it provided by user */\n if (!(pattern instanceof RegExp)) {\n _.log(\n `Pattern ${pattern} for \"${tool}\" Tool is skipped because it should be a Regexp instance.`,\n 'warn',\n );\n }\n\n this.toolsPatterns.push({\n key,\n pattern,\n handler: toolPasteConfig.patternHandler,\n tool: name,\n });\n });\n }\n }\n\n /**\n * Check if browser behavior suits better\n *\n * @param {EventTarget} element - element where content has been pasted\n * @returns {boolean}\n */\n private isNativeBehaviour(element: EventTarget): boolean {\n const {Editor: {BlockManager}} = this;\n\n if ( $.isNativeInput(element) ) {\n return true;\n }\n\n const block = BlockManager.getBlock(element);\n\n return !block;\n }\n\n /**\n * Get pasted data, process it and insert into editor\n *\n * @param {ClipboardEvent} event\n */\n private processPastedData = async (event: ClipboardEvent): Promise => {\n const {\n Editor: {Tools, Sanitizer, BlockManager, Caret},\n config: {toolsConfig},\n } = this;\n\n /** If target is native input or is not Block, use browser behaviour */\n if (this.isNativeBehaviour(event.target)) {\n return;\n }\n\n event.preventDefault();\n\n const block = BlockManager.getBlock(event.target);\n const toolConfig = toolsConfig[block.name];\n\n /** If paste is dissalowed in block do nothing */\n if (toolConfig && toolConfig[Tools.apiSettings.IS_PASTE_DISALLOWED]) {\n return;\n }\n\n const htmlData = event.clipboardData.getData('text/html'),\n plainData = event.clipboardData.getData('text/plain');\n\n /** Add all block tags and tags can be substituted to sanitizer configuration */\n const blockTags = $.blockElements.reduce((result, tag) => {\n result[tag.toLowerCase()] = {};\n\n return result;\n }, {});\n const toolsTags = Object.keys(this.toolsTags).reduce((result, tag) => {\n result[tag.toLowerCase()] = {};\n\n return result;\n }, {});\n\n const customConfig = {tags: Object.assign({}, blockTags, toolsTags, Sanitizer.defaultConfig.tags)};\n const cleanData = Sanitizer.clean(htmlData, customConfig);\n\n let dataToInsert = [];\n\n /** If there is no HTML or HTML string is equal to plain one, process it as plain text */\n if (!cleanData.trim() || cleanData.trim() === plainData || !$.isHTMLString(cleanData)) {\n dataToInsert = this.processPlain(plainData);\n } else {\n dataToInsert = this.processHTML(htmlData);\n }\n\n if (dataToInsert.length === 1 && !dataToInsert[0].isBlock) {\n this.processSingleBlock(dataToInsert.pop());\n return;\n }\n\n this.splitBlock();\n\n await Promise.all(dataToInsert.map(\n async (data, i) => await this.insertBlock(data, i === 0),\n ));\n\n Caret.setToBlock(BlockManager.currentBlock, 0, true);\n }\n\n /**\n * Process paste to single Block:\n * 1. Find patterns` matches\n * 2. Insert new block if it is not the same type as current one\n * 3. Just insert text if there is no substitutions\n *\n * @param {IPasteData} dataToInsert\n */\n private async processSingleBlock(dataToInsert: IPasteData): Promise {\n const initialTool = this.config.initialBlock;\n const {BlockManager} = this.Editor;\n const {content, tool} = dataToInsert;\n\n if (tool === initialTool && content.textContent.length < Paste.PATTERN_PROCESSING_MAX_LENGTH) {\n const blockData = await this.processPattern(content.textContent);\n\n if (blockData) {\n this.splitBlock();\n\n if (BlockManager.currentBlock.isEmpty) {\n BlockManager.replace(blockData.tool, blockData.data);\n } else {\n BlockManager.insert(blockData.tool, blockData.data);\n }\n return;\n }\n }\n\n /** If there is no pattern substitute - insert string as it is */\n document.execCommand('insertHTML', false, content.innerHTML);\n }\n\n /**\n * Get patterns` matches\n *\n * @param {string} text\n * @returns Promise<{data: IBlockToolData, tool: string}>\n */\n private async processPattern(text: string): Promise<{data: IBlockToolData, tool: string}> {\n const pattern = this.toolsPatterns.find((substitute) => {\n const execResult = substitute.pattern.exec(text);\n\n if (!execResult) {\n return false;\n }\n\n return text === execResult.shift();\n });\n\n const data = pattern && await pattern.handler(text, pattern.key);\n\n return data && {\n data,\n tool: pattern.tool,\n };\n }\n\n /**\n *\n * @param {IPasteData} data\n * @param {Boolean} canReplaceCurrentBlock - if true and is current Block is empty, will replace current Block\n * @returns {Promise}\n */\n private async insertBlock(data: IPasteData, canReplaceCurrentBlock: boolean = false): Promise {\n const blockData = await data.handler(data.content);\n const {BlockManager} = this.Editor;\n const {currentBlock} = BlockManager;\n\n if (canReplaceCurrentBlock && currentBlock.isEmpty) {\n BlockManager.replace(data.tool, blockData);\n return;\n }\n\n BlockManager.insert(data.tool, blockData);\n }\n\n /**\n * Split current block if paste isn't in the end of the block\n */\n private splitBlock() {\n const {BlockManager, Caret} = this.Editor;\n\n /** If we paste into middle of the current block:\n * 1. Split\n * 2. Navigate to the first part\n */\n if (!BlockManager.currentBlock.isEmpty && !Caret.isAtEnd) {\n BlockManager.split();\n BlockManager.currentBlockIndex--;\n }\n }\n\n /**\n * Split HTML string to blocks and return it as array of Block data\n *\n * @param {string} innerHTML\n * @returns {IPasteData[]}\n */\n private processHTML(innerHTML: string): IPasteData[] {\n const {Tools, Sanitizer} = this.Editor;\n const initialTool = this.config.initialBlock;\n const wrapper = $.make('DIV');\n\n wrapper.innerHTML = innerHTML;\n\n const nodes = this.getNodes(wrapper);\n\n return nodes\n .map((node) => {\n let content, tool = initialTool, isBlock = false;\n\n switch (node.nodeType) {\n /** If node is a document fragment, use temp wrapper to get innerHTML */\n case Node.DOCUMENT_FRAGMENT_NODE:\n content = $.make('div');\n content.appendChild(node);\n content.innerHTML = Sanitizer.clean(content.innerHTML);\n break;\n\n /** If node is an element, then there might be a substitution */\n case Node.ELEMENT_NODE:\n content = node as HTMLElement;\n isBlock = true;\n content.innerHTML = Sanitizer.clean(content.innerHTML);\n\n if (this.toolsTags[content.tagName]) {\n tool = this.toolsTags[content.tagName].tool;\n }\n break;\n }\n\n const handler = Tools.blockTools[tool].onPaste.handler;\n\n return {content, isBlock, handler, tool};\n })\n .filter((data) => !$.isNodeEmpty(data.content));\n }\n\n /**\n * Split plain text by new line symbols and return it as array of Block data\n *\n * @param {string} plain\n * @returns {IPasteData[]}\n */\n private processPlain(plain: string): IPasteData[] {\n const {initialBlock} = this.config as {initialBlock: string};\n const {Tools} = this.Editor;\n\n if (!plain) {\n return [];\n }\n const tool = initialBlock;\n const handler = Tools.blockTools[tool].onPaste.handler;\n\n return plain.split('\\n\\n').map((text) => {\n const content = $.make('div');\n\n content.innerHTML = plain;\n return {content, tool, isBlock: false, handler};\n });\n }\n\n /**\n * Recursively divide HTML string to two types of nodes:\n * 1. Block element\n * 2. Document Fragments contained text and markup tags like a, b, i etc.\n *\n * @param {Node} wrapper\n * @returns {Node[]}\n */\n private getNodes(wrapper: Node): Node[] {\n const children = Array.from(wrapper.childNodes);\n const tags = Object.keys(this.toolsTags);\n\n const reducer = (nodes: Node[], node: Node): Node[] => {\n if ($.isEmpty(node)) {\n return nodes;\n }\n\n const lastNode = nodes[nodes.length - 1];\n let destNode: Node = new DocumentFragment();\n\n if (lastNode && $.isFragment(lastNode)) {\n destNode = nodes.pop();\n }\n\n switch (node.nodeType) {\n /**\n * If node is HTML element:\n * 1. Check if it is inline element\n * 2. Check if it contains another block or substitutable elements\n */\n case Node.ELEMENT_NODE:\n const element = node as HTMLElement;\n /** Append inline elements to previous fragment */\n if (\n !$.blockElements.includes(element.tagName.toLowerCase()) &&\n !tags.includes(element.tagName.toLowerCase())\n ) {\n destNode.appendChild(element);\n return [...nodes, destNode];\n }\n\n if (\n (\n tags.includes(element.tagName.toLowerCase()) &&\n Array.from(element.children).every(({tagName}) => !tags.includes(tagName.toLowerCase()))\n ) || (\n $.blockElements.includes(element.tagName.toLowerCase()) &&\n Array.from(element.children).every(\n ({tagName}) => !$.blockElements.includes(tagName.toLowerCase()),\n )\n )\n ) {\n return [...nodes, element];\n }\n break;\n\n /**\n * If node is text node, wrap it with DocumentFragment\n */\n case Node.TEXT_NODE:\n destNode.appendChild(node);\n return [...nodes, destNode];\n\n default:\n return [...nodes, destNode];\n }\n\n return [...nodes, ...Array.from(node.childNodes).reduce(reducer, [])];\n };\n\n return children.reduce(reducer, []);\n }\n}\n","/**\r\n * Codex Editor Renderer Module\r\n *\r\n * @module Renderer\r\n * @author CodeX Team\r\n *\r\n * @version 2.0.0\r\n */\r\nexport default class Renderer extends Module {\r\n /**\r\n * @constructor\r\n * @param {EditorConfig} config\r\n */\r\n constructor({config}) {\r\n super({config});\r\n }\r\n\r\n /**\r\n * @typedef {Object} RendererItems\r\n * @property {String} type - tool name\r\n * @property {Object} data - tool data\r\n */\r\n\r\n /**\r\n * @example\r\n *\r\n * items: [\r\n * {\r\n * type : 'paragraph',\r\n * data : {\r\n * text : 'Hello from Codex!'\r\n * }\r\n * },\r\n * {\r\n * type : 'paragraph',\r\n * data : {\r\n * text : 'Leave feedback if you like it!'\r\n * }\r\n * },\r\n * ]\r\n *\r\n */\r\n\r\n /**\r\n * Make plugin blocks from array of plugin`s data\r\n * @param {RendererItems[]} items\r\n */\r\n render(items) {\r\n let chainData = [];\r\n\r\n for (let i = 0; i < items.length; i++) {\r\n chainData.push({\r\n function: () => this.insertBlock(items[i])\r\n });\r\n }\r\n\r\n return _.sequence(chainData);\r\n }\r\n\r\n /**\r\n * Get plugin instance\r\n * Add plugin instance to BlockManager\r\n * Insert block to working zone\r\n *\r\n * @param {Object} item\r\n * @returns {Promise.}\r\n * @private\r\n */\r\n insertBlock(item) {\r\n let tool = item.type,\r\n data = item.data,\r\n settings = item.settings;\r\n\r\n if (tool in this.Editor.Tools.available) {\r\n this.Editor.BlockManager.insert(tool, data, settings);\r\n } else {\r\n /**\r\n * @todo show warning notification message\r\n *\r\n * `${tool} blocks was skipped.`\r\n */\r\n\r\n _.log(`Tool «${tool}» is not found. Check 'tools' property at your initial CodeX Editor config.`, 'warn');\r\n }\r\n\r\n return Promise.resolve();\r\n }\r\n}\r\n","/**\r\n * CodeX Sanitizer\r\n *\r\n * @module Sanitizer\r\n * Clears HTML from taint tags\r\n *\r\n * @version 2.0.0\r\n *\r\n * @example\r\n * Module can be used within two ways:\r\n * 1) When you have an instance\r\n * - this.Editor.Sanitizer.clean(yourTaintString);\r\n * 2) As static method\r\n * - CodexEditor.Sanitizer.clean(yourTaintString, yourCustomConfiguration);\r\n *\r\n * {@link SanitizerConfig}\r\n */\r\n\r\n\r\n/**\r\n * @typedef {Object} SanitizerConfig\r\n * @property {Object} tags - define tags restrictions\r\n *\r\n * @example\r\n *\r\n * tags : {\r\n * p: true,\r\n * a: {\r\n * href: true,\r\n * rel: \"nofollow\",\r\n * target: \"_blank\"\r\n * }\r\n * }\r\n */\r\nexport default class Sanitizer extends Module {\r\n /**\r\n * Initializes Sanitizer module\r\n * Sets default configuration if custom not exists\r\n *\r\n * @property {SanitizerConfig} this.defaultConfig\r\n * @property {HTMLJanitor} this._sanitizerInstance - Sanitizer library\r\n *\r\n * @param {SanitizerConfig} config\r\n */\r\n constructor({config}) {\r\n super({config});\r\n\r\n // default config\r\n this.defaultConfig = null;\r\n this._sanitizerInstance = null;\r\n\r\n /** Custom configuration */\r\n this.sanitizerConfig = config.settings ? config.settings.sanitizer : {};\r\n\r\n /** HTML Janitor library */\r\n this.sanitizerInstance = require('html-janitor');\r\n }\r\n\r\n /**\r\n * If developer uses editor's API, then he can customize sanitize restrictions.\r\n * Or, sanitizing config can be defined globally in editors initialization. That config will be used everywhere\r\n * At least, if there is no config overrides, that API uses Default configuration\r\n *\r\n * @uses https://www.npmjs.com/package/html-janitor\r\n *\r\n * @param {HTMLJanitor} library - sanitizer extension\r\n */\r\n set sanitizerInstance(library) {\r\n this._sanitizerInstance = new library(this.defaultConfig);\r\n }\r\n\r\n /**\r\n * Sets sanitizer configuration. Uses default config if user didn't pass the restriction\r\n * @param {SanitizerConfig} config\r\n */\r\n set sanitizerConfig(config) {\r\n if (_.isEmpty(config)) {\r\n this.defaultConfig = {\r\n tags: {\r\n p: {},\r\n a: {\r\n href: true,\r\n target: '_blank',\r\n rel: 'nofollow'\r\n },\r\n b: {},\r\n i: {}\r\n }\r\n };\r\n } else {\r\n this.defaultConfig = config;\r\n }\r\n }\r\n\r\n /**\r\n * Cleans string from unwanted tags\r\n * @param {String} taintString - HTML string\r\n * @param {Object} customConfig - custom sanitizer configuration. Method uses default if param is empty\r\n * @return {String} clean HTML\r\n */\r\n clean(taintString, customConfig = {}) {\r\n if (_.isEmpty(customConfig)) {\r\n return this._sanitizerInstance.clean(taintString);\r\n } else {\r\n return Sanitizer.clean(taintString, customConfig);\r\n }\r\n }\r\n\r\n /**\r\n * Cleans string from unwanted tags\r\n * @static\r\n *\r\n * Method allows to use default config\r\n *\r\n * @param {String} taintString - taint string\r\n * @param {SanitizerConfig} customConfig - allowed tags\r\n *\r\n * @return {String} clean HTML\r\n */\r\n static clean(taintString, customConfig) {\r\n let newInstance = new Sanitizer({\r\n config: {\r\n settings: {\r\n sanitizer: customConfig\r\n }\r\n }\r\n });\r\n\r\n return newInstance.clean(taintString);\r\n }\r\n}\r\n","/**\r\n * Codex Editor Saver\r\n *\r\n * @module Saver\r\n * @author Codex Team\r\n * @version 2.0.0\r\n */\r\n\r\n/**\r\n * @typedef {Object} SavedData\r\n * @property {Date} time - saving proccess time\r\n * @property {Object} items - extracted data\r\n * @property {String} version - CodexEditor version\r\n */\r\n\r\n/**\r\n * @classdesc This method reduces all Blocks asyncronically and calls Block's save method to extract data\r\n *\r\n * @typedef {Saver} Saver\r\n * @property {Element} html - Editor HTML content\r\n * @property {String} json - Editor JSON output\r\n */\r\nexport default class Saver extends Module {\r\n /**\r\n * @constructor\r\n * @param config\r\n */\r\n constructor({config}) {\r\n super({config});\r\n\r\n this.output = null;\r\n this.blocksData = [];\r\n }\r\n\r\n /**\r\n * Composes new chain of Promises to fire them alternatelly\r\n * @return {SavedData}\r\n */\r\n save() {\r\n let blocks = this.Editor.BlockManager.blocks,\r\n chainData = [];\r\n\r\n blocks.forEach((block) => {\r\n chainData.push(block.data);\r\n });\r\n\r\n return Promise.all(chainData)\r\n .then((allExtractedData) => this.makeOutput(allExtractedData))\r\n .then((outputData) => {\r\n return outputData;\r\n });\r\n }\r\n\r\n /**\r\n * Creates output object with saved data, time and version of editor\r\n * @param {Object} allExtractedData\r\n * @return {SavedData}\r\n */\r\n makeOutput(allExtractedData) {\r\n let items = [],\r\n totalTime = 0;\r\n\r\n console.groupCollapsed('[CodexEditor saving]:');\r\n\r\n allExtractedData.forEach((extraction) => {\r\n /** Group process info */\r\n console.log(`«${extraction.tool}» saving info`, extraction);\r\n totalTime += extraction.time;\r\n items.push({\r\n type: extraction.tool,\r\n data: extraction.data\r\n });\r\n });\r\n\r\n console.log('Total', totalTime);\r\n console.groupEnd();\r\n\r\n return {\r\n time : +new Date(),\r\n items : items,\r\n version : VERSION,\r\n };\r\n }\r\n}\r\n\r\n// module.exports = (function (saver) {\r\n//\r\n// let editor = codex.editor;\r\n//\r\n// /**\r\n// * @public\r\n// * Save blocks\r\n// */\r\n// saver.save = function () {\r\n//\r\n// /** Save html content of redactor to memory */\r\n// editor.state.html = editor.nodes.redactor.innerHTML;\r\n//\r\n// /** Clean jsonOutput state */\r\n// editor.state.jsonOutput = [];\r\n//\r\n// return saveBlocks(editor.nodes.redactor.childNodes);\r\n//\r\n// };\r\n//\r\n// /**\r\n// * @private\r\n// * Save each block data\r\n// *\r\n// * @param blocks\r\n// * @returns {Promise.}\r\n// */\r\n// let saveBlocks = function (blocks) {\r\n//\r\n// let data = [];\r\n//\r\n// for(let index = 0; index < blocks.length; index++) {\r\n//\r\n// data.push(getBlockData(blocks[index]));\r\n//\r\n// }\r\n//\r\n// return Promise.all(data)\r\n// .then(makeOutput)\r\n// .catch(editor.core.log);\r\n//\r\n// };\r\n//\r\n// /** Save and validate block data */\r\n// let getBlockData = function (block) {\r\n//\r\n// return saveBlockData(block)\r\n// .then(validateBlockData)\r\n// .catch(editor.core.log);\r\n//\r\n// };\r\n//\r\n// /**\r\n// * @private\r\n// * Call block`s plugin save method and return saved data\r\n// *\r\n// * @param block\r\n// * @returns {Object}\r\n// */\r\n// let saveBlockData = function (block) {\r\n//\r\n// let pluginName = block.dataset.tool;\r\n//\r\n// /** Check for plugin existence */\r\n// if (!editor.tools[pluginName]) {\r\n//\r\n// editor.core.log(`Plugin «${pluginName}» not found`, 'error');\r\n// return {data: null, pluginName: null};\r\n//\r\n// }\r\n//\r\n// /** Check for plugin having save method */\r\n// if (typeof editor.tools[pluginName].save !== 'function') {\r\n//\r\n// editor.core.log(`Plugin «${pluginName}» must have save method`, 'error');\r\n// return {data: null, pluginName: null};\r\n//\r\n// }\r\n//\r\n// /** Result saver */\r\n// let blockContent = block.childNodes[0],\r\n// pluginsContent = blockContent.childNodes[0],\r\n// position = pluginsContent.dataset.inputPosition;\r\n//\r\n// /** If plugin wasn't available then return data from cache */\r\n// if ( editor.tools[pluginName].available === false ) {\r\n//\r\n// return Promise.resolve({data: codex.editor.state.blocks.items[position].data, pluginName});\r\n//\r\n// }\r\n//\r\n// return Promise.resolve(pluginsContent)\r\n// .then(editor.tools[pluginName].save)\r\n// .then(data => Object({data, pluginName}));\r\n//\r\n// };\r\n//\r\n// /**\r\n// * Call plugin`s validate method. Return false if validation failed\r\n// *\r\n// * @param data\r\n// * @param pluginName\r\n// * @returns {Object|Boolean}\r\n// */\r\n// let validateBlockData = function ({data, pluginName}) {\r\n//\r\n// if (!data || !pluginName) {\r\n//\r\n// return false;\r\n//\r\n// }\r\n//\r\n// if (editor.tools[pluginName].validate) {\r\n//\r\n// let result = editor.tools[pluginName].validate(data);\r\n//\r\n// /**\r\n// * Do not allow invalid data\r\n// */\r\n// if (!result) {\r\n//\r\n// return false;\r\n//\r\n// }\r\n//\r\n// }\r\n//\r\n// return {data, pluginName};\r\n//\r\n//\r\n// };\r\n//\r\n// /**\r\n// * Compile article output\r\n// *\r\n// * @param savedData\r\n// * @returns {{time: number, version, items: (*|Array)}}\r\n// */\r\n// let makeOutput = function (savedData) {\r\n//\r\n// savedData = savedData.filter(blockData => blockData);\r\n//\r\n// let items = savedData.map(blockData => Object({type: blockData.pluginName, data: blockData.data}));\r\n//\r\n// editor.state.jsonOutput = items;\r\n//\r\n// return {\r\n// id: editor.state.blocks.id || null,\r\n// time: +new Date(),\r\n// version: editor.version,\r\n// items\r\n// };\r\n//\r\n// };\r\n//\r\n// return saver;\r\n//\r\n// })({});\r\n","/**\r\n * Block Settings\r\n *\r\n * ____ Settings Panel ____\r\n * | ...................... |\r\n * | . Tool Settings . |\r\n * | ...................... |\r\n * | . Default Settings . |\r\n * | ...................... |\r\n * |________________________|\r\n */\r\nexport default class BlockSettings extends Module {\r\n /**\r\n * @constructor\r\n */\r\n constructor({config}) {\r\n super({config});\r\n\r\n this.nodes = {\r\n wrapper: null,\r\n toolSettings: null,\r\n defaultSettings: null\r\n };\r\n }\r\n\r\n /**\r\n * Module Events\r\n * @return {{opened: string, closed: string}}\r\n */\r\n get events() {\r\n return {\r\n opened: 'block-settings-opened',\r\n closed: 'block-settings-closed',\r\n };\r\n }\r\n\r\n /**\r\n * Block Settings CSS\r\n * @return {{wrapper, wrapperOpened, toolSettings, defaultSettings, button}}\r\n */\r\n static get CSS() {\r\n return {\r\n // Settings Panel\r\n wrapper: 'ce-settings',\r\n wrapperOpened: 'ce-settings--opened',\r\n toolSettings: 'ce-settings__plugin-zone',\r\n defaultSettings: 'ce-settings__default-zone',\r\n\r\n button: 'ce-settings__button'\r\n };\r\n }\r\n\r\n /**\r\n * Panel with block settings with 2 sections:\r\n * - Tool's Settings\r\n * - Default Settings [Move, Remove, etc]\r\n *\r\n * @return {Element}\r\n */\r\n make() {\r\n this.nodes.wrapper = $.make('div', BlockSettings.CSS.wrapper);\r\n\r\n this.nodes.toolSettings = $.make('div', BlockSettings.CSS.toolSettings);\r\n this.nodes.defaultSettings = $.make('div', BlockSettings.CSS.defaultSettings);\r\n\r\n $.append(this.nodes.wrapper, [this.nodes.toolSettings, this.nodes.defaultSettings]);\r\n }\r\n\r\n /**\r\n * Add Tool's settings\r\n */\r\n addToolSettings() {\r\n if (typeof this.Editor.BlockManager.currentBlock.tool.renderSettings === 'function') {\r\n $.append(this.nodes.toolSettings, this.Editor.BlockManager.currentBlock.tool.renderSettings());\r\n }\r\n }\r\n\r\n /**\r\n * Add default settings\r\n */\r\n addDefaultSettings() {\r\n $.append(this.nodes.defaultSettings, this.Editor.BlockManager.currentBlock.renderTunes());\r\n }\r\n\r\n /**\r\n * Is Block Settings opened or not\r\n * @returns {boolean}\r\n */\r\n get opened() {\r\n return this.nodes.wrapper.classList.contains(BlockSettings.CSS.wrapperOpened);\r\n }\r\n\r\n /**\r\n * Open Block Settings pane\r\n */\r\n open() {\r\n this.nodes.wrapper.classList.add(BlockSettings.CSS.wrapperOpened);\r\n\r\n /**\r\n * Fill Tool's settings\r\n */\r\n this.addToolSettings();\r\n\r\n /**\r\n * Add default settings that presents for all Blocks\r\n */\r\n this.addDefaultSettings();\r\n\r\n /** Tell to subscribers that block settings is opened */\r\n this.Editor.Events.emit(this.events.opened);\r\n }\r\n\r\n /**\r\n * Close Block Settings pane\r\n */\r\n close() {\r\n this.nodes.wrapper.classList.remove(BlockSettings.CSS.wrapperOpened);\r\n\r\n /** Clear settings */\r\n this.nodes.toolSettings.innerHTML = '';\r\n this.nodes.defaultSettings.innerHTML = '';\r\n\r\n /** Tell to subscribers that block settings is closed */\r\n this.Editor.Events.emit(this.events.closed);\r\n }\r\n}\r\n","/**\r\n * Inline toolbar with actions that modifies selected text fragment\r\n *\r\n * |¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯|\r\n * | B i [link] [mark] |\r\n * | _______________________|\r\n */\r\ndeclare var Module: any;\r\ndeclare var $: any;\r\ndeclare var _: any;\r\nimport BoldInlineTool from '../inline-tools/inline-tool-bold';\r\nimport ItalicInlineTool from '../inline-tools/inline-tool-italic';\r\nimport LinkInlineTool from '../inline-tools/inline-tool-link';\r\nimport InlineTool from '../interfaces/inline-tool';\r\nimport Selection from '../selection';\r\n\r\nexport default class InlineToolbar extends Module {\r\n\r\n /**\r\n * CSS styles\r\n */\r\n public CSS = {\r\n inlineToolbar: 'ce-inline-toolbar',\r\n inlineToolbarShowed: 'ce-inline-toolbar--showed',\r\n buttonsWrapper: 'ce-inline-toolbar__buttons',\r\n actionsWrapper: 'ce-inline-toolbar__actions',\r\n };\r\n\r\n /**\r\n * Inline Toolbar elements\r\n */\r\n private nodes = {\r\n wrapper: null,\r\n buttons: null,\r\n /**\r\n * Zone below the buttons where Tools can create additional actions by 'renderActions()' method\r\n * For example, input for the 'link' tool or textarea for the 'comment' tool\r\n */\r\n actions: null,\r\n };\r\n\r\n /**\r\n * Margin above/below the Toolbar\r\n */\r\n private readonly toolbarVerticalMargin: number = 20;\r\n\r\n /**\r\n * Tools instances\r\n */\r\n private toolsInstances: InlineTool[];\r\n\r\n /**\r\n * @constructor\r\n */\r\n constructor({config}) {\r\n super({config});\r\n }\r\n\r\n /**\r\n * Inline Toolbar Tools\r\n * @todo Merge internal tools with external\r\n */\r\n get tools(): InlineTool[] {\r\n if (!this.toolsInstances) {\r\n this.toolsInstances = [\r\n new BoldInlineTool(this.Editor.API.methods),\r\n new ItalicInlineTool(this.Editor.API.methods),\r\n new LinkInlineTool(this.Editor.API.methods),\r\n ...this.Editor.Tools.inline.map( (Tool) => new Tool(this.Editor.API.methods) ),\r\n ];\r\n }\r\n return this.toolsInstances;\r\n }\r\n\r\n /**\r\n * Making DOM\r\n */\r\n public make() {\r\n\r\n this.nodes.wrapper = $.make('div', this.CSS.inlineToolbar);\r\n this.nodes.buttons = $.make('div', this.CSS.buttonsWrapper);\r\n this.nodes.actions = $.make('div', this.CSS.actionsWrapper);\r\n\r\n /**\r\n * Append Inline Toolbar to the Editor\r\n */\r\n $.append(this.nodes.wrapper, [this.nodes.buttons, this.nodes.actions]);\r\n $.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper);\r\n\r\n /**\r\n * Append Inline Toolbar Tools\r\n */\r\n this.addTools();\r\n\r\n }\r\n\r\n /**\r\n *\r\n *\r\n * Moving / appearance\r\n * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\r\n *\r\n */\r\n\r\n /**\r\n * Shows Inline Toolbar by keyup/mouseup\r\n * @param {KeyboardEvent|MouseEvent} event\r\n */\r\n public handleShowingEvent(event): void {\r\n if (!this.allowedToShow(event)) {\r\n this.close();\r\n return;\r\n }\r\n\r\n this.move();\r\n this.open();\r\n\r\n /** Check Tools state for selected fragment */\r\n this.checkToolsState();\r\n }\r\n\r\n /**\r\n * Move Toolbar to the selected text\r\n */\r\n public move(): void {\r\n\r\n const selectionRect = Selection.rect;\r\n const wrapperOffset = this.Editor.UI.nodes.wrapper.getBoundingClientRect();\r\n\r\n const newCoords = {\r\n x: selectionRect.x - wrapperOffset.left,\r\n y: selectionRect.y\r\n + selectionRect.height\r\n // + window.scrollY\r\n - wrapperOffset.top\r\n + this.toolbarVerticalMargin,\r\n };\r\n\r\n /**\r\n * If we know selections width, place InlineToolbar to center\r\n */\r\n if (selectionRect.width) {\r\n newCoords.x += Math.floor(selectionRect.width / 2);\r\n }\r\n\r\n this.nodes.wrapper.style.left = Math.floor(newCoords.x) + 'px';\r\n this.nodes.wrapper.style.top = Math.floor(newCoords.y) + 'px';\r\n }\r\n\r\n /**\r\n * Shows Inline Toolbar\r\n */\r\n private open() {\r\n this.nodes.wrapper.classList.add(this.CSS.inlineToolbarShowed);\r\n\r\n this.tools.forEach( (tool) => {\r\n if (typeof tool.clear === 'function') {\r\n tool.clear();\r\n }\r\n });\r\n }\r\n\r\n /**\r\n * Hides Inline Toolbar\r\n */\r\n private close() {\r\n this.nodes.wrapper.classList.remove(this.CSS.inlineToolbarShowed);\r\n\r\n this.tools.forEach( (tool) => {\r\n if (typeof tool.clear === 'function') {\r\n tool.clear();\r\n }\r\n });\r\n }\r\n\r\n /**\r\n * Need to show Inline Toolbar or not\r\n * @param {KeyboardEvent|MouseEvent} event\r\n */\r\n private allowedToShow(event): boolean {\r\n /**\r\n * Tags conflicts with window.selection function.\r\n * Ex. IMG tag returns null (Firefox) or Redactors wrapper (Chrome)\r\n */\r\n const tagsConflictsWithSelection = ['IMG', 'INPUT'];\r\n if (event && tagsConflictsWithSelection.includes(event.target.tagName)) {\r\n return false;\r\n }\r\n\r\n const currentSelection = Selection.get(),\r\n selectedText = Selection.text;\r\n\r\n // old browsers\r\n if (!currentSelection || !currentSelection.anchorNode) {\r\n return false;\r\n }\r\n\r\n // empty selection\r\n if (currentSelection.isCollapsed || selectedText.length < 1) {\r\n return false;\r\n }\r\n\r\n // is enabled by current Block's Tool\r\n const currentBlock = this.Editor.BlockManager.getBlock(currentSelection.anchorNode);\r\n\r\n if (!currentBlock) {\r\n return false;\r\n }\r\n\r\n const toolConfig = this.config.toolsConfig[currentBlock.name];\r\n\r\n return toolConfig && toolConfig[this.Editor.Tools.apiSettings.IS_ENABLED_INLINE_TOOLBAR];\r\n }\r\n\r\n /**\r\n *\r\n *\r\n * Working with Tools\r\n * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\r\n *\r\n */\r\n\r\n /**\r\n * Fill Inline Toolbar with Tools\r\n */\r\n private addTools(): void {\r\n this.tools.forEach( (tool) => {\r\n this.addTool(tool);\r\n });\r\n }\r\n\r\n /**\r\n * Add tool button and activate clicks\r\n * @param {InlineTool} tool - Tool's instance\r\n */\r\n private addTool(tool: InlineTool): void {\r\n const button = tool.render();\r\n\r\n if (!button) {\r\n _.log('Render method must return an instance of Node', 'warn', tool);\r\n return;\r\n }\r\n\r\n this.nodes.buttons.appendChild(button);\r\n\r\n if (typeof tool.renderActions === 'function') {\r\n const actions = tool.renderActions();\r\n this.nodes.actions.appendChild(actions);\r\n }\r\n\r\n this.Editor.Listeners.on(button, 'click', () => {\r\n this.toolClicked(tool);\r\n });\r\n }\r\n\r\n /**\r\n * Inline Tool button clicks\r\n * @param {InlineTool} tool - Tool's instance\r\n */\r\n private toolClicked(tool: InlineTool): void {\r\n const range = Selection.range;\r\n\r\n tool.surround(range);\r\n this.checkToolsState();\r\n\r\n }\r\n\r\n /**\r\n * Check Tools` state by selection\r\n */\r\n private checkToolsState(): void {\r\n this.tools.forEach( (tool) => {\r\n tool.checkState(Selection.get());\r\n });\r\n }\r\n}\r\n","/**\r\n * @class Toolbox\r\n * @classdesc Holder for Tools\r\n *\r\n * @typedef {Toolbox} Toolbox\r\n * @property {Boolean} opened - opening state\r\n * @property {Object} nodes - Toolbox nodes\r\n * @property {Object} CSS - CSS class names\r\n *\r\n */\r\nexport default class Toolbox extends Module {\r\n /**\r\n * @constructor\r\n */\r\n constructor({config}) {\r\n super({config});\r\n\r\n this.nodes = {\r\n toolbox: null,\r\n buttons: []\r\n };\r\n\r\n /**\r\n * Opening state\r\n * @type {boolean}\r\n */\r\n this.opened = false;\r\n }\r\n\r\n /**\r\n * CSS styles\r\n * @return {{toolbox: string, toolboxButton: string, toolboxOpened: string}}\r\n */\r\n static get CSS() {\r\n return {\r\n toolbox: 'ce-toolbox',\r\n toolboxButton: 'ce-toolbox__button',\r\n toolboxOpened: 'ce-toolbox--opened',\r\n };\r\n }\r\n\r\n /**\r\n * Makes the Toolbox\r\n */\r\n make() {\r\n this.nodes.toolbox = $.make('div', Toolbox.CSS.toolbox);\r\n $.append(this.Editor.Toolbar.nodes.content, this.nodes.toolbox);\r\n\r\n this.addTools();\r\n }\r\n\r\n /**\r\n * Iterates available tools and appends them to the Toolbox\r\n */\r\n addTools() {\r\n let tools = this.Editor.Tools.toolsAvailable;\r\n\r\n for (let toolName in tools) {\r\n this.addTool(toolName, tools[toolName]);\r\n }\r\n }\r\n\r\n /**\r\n * Append Tool to the Toolbox\r\n *\r\n * @param {string} toolName - tool name\r\n * @param {IBlockTool} tool - tool class\r\n */\r\n addTool(toolName, tool) {\r\n const api = this.Editor.Tools.apiSettings;\r\n\r\n if (tool[api.IS_DISPLAYED_IN_TOOLBOX] && !tool[api.TOOLBAR_ICON_CLASS]) {\r\n _.log('Toolbar icon class name is missed. Tool %o skipped', 'warn', toolName);\r\n return;\r\n }\r\n\r\n /**\r\n * @todo Add checkup for the render method\r\n */\r\n // if (typeof tool.render !== 'function') {\r\n //\r\n // _.log('render method missed. Tool %o skipped', 'warn', tool);\r\n // return;\r\n //\r\n // }\r\n\r\n /**\r\n * Skip tools that pass 'displayInToolbox=false'\r\n */\r\n if (!tool[api.IS_DISPLAYED_IN_TOOLBOX]) {\r\n return;\r\n }\r\n\r\n let button = $.make('li', [Toolbox.CSS.toolboxButton, tool[api.TOOLBAR_ICON_CLASS]], {\r\n title: toolName\r\n });\r\n\r\n button.innerHTML = tool.toolboxIcon;\r\n\r\n /**\r\n * Save tool's name in the button data-name\r\n */\r\n button.dataset.name = toolName;\r\n\r\n $.append(this.nodes.toolbox, button);\r\n\r\n this.nodes.toolbox.appendChild(button);\r\n this.nodes.buttons.push(button);\r\n\r\n /**\r\n * @todo add event with module Listeners\r\n */\r\n // this.Editor.Listeners.add();\r\n button.addEventListener('click', event => {\r\n this.buttonClicked(event);\r\n }, false);\r\n }\r\n\r\n /**\r\n * Toolbox button click listener\r\n * 1) if block is empty -> replace\r\n * 2) if block is not empty -> add new block below\r\n *\r\n * @param {MouseEvent} event\r\n */\r\n buttonClicked(event) {\r\n let toolButton = event.target;\r\n\r\n toolButton = toolButton.closest(`.${Toolbox.CSS.toolboxButton}`);\r\n\r\n if (!toolButton) {\r\n return;\r\n }\r\n\r\n let toolName = toolButton.dataset.name,\r\n tool = this.Editor.Tools.toolClasses[toolName];\r\n\r\n /**\r\n * @type {Block}\r\n */\r\n let currentBlock = this.Editor.BlockManager.currentBlock;\r\n\r\n /**\r\n * We do replace if:\r\n * - block is empty\r\n * - block is not irreplaceable\r\n * @type {Array}\r\n */\r\n if (!tool[this.Editor.Tools.apiSettings.IS_IRREPLACEBLE_TOOL] && currentBlock.isEmpty) {\r\n this.Editor.BlockManager.replace(toolName);\r\n } else {\r\n this.Editor.BlockManager.insert(toolName);\r\n }\r\n\r\n /**\r\n * @todo set caret to the new block\r\n */\r\n\r\n // window.setTimeout(function () {\r\n\r\n /** Set caret to current block */\r\n // editor.caret.setToBlock(currentInputIndex);\r\n\r\n // }, 10);\r\n\r\n /**\r\n * Move toolbar when node is changed\r\n */\r\n this.Editor.Toolbar.move();\r\n }\r\n\r\n /**\r\n * Open Toolbox with Tools\r\n */\r\n open() {\r\n this.nodes.toolbox.classList.add(Toolbox.CSS.toolboxOpened);\r\n this.opened = true;\r\n }\r\n\r\n /**\r\n * Close Toolbox\r\n */\r\n close() {\r\n this.nodes.toolbox.classList.remove(Toolbox.CSS.toolboxOpened);\r\n this.opened = false;\r\n }\r\n\r\n /**\r\n * Close Toolbox\r\n */\r\n toggle() {\r\n if (!this.opened) {\r\n this.open();\r\n } else {\r\n this.close();\r\n }\r\n }\r\n}\r\n","/**\r\n *\r\n * «Toolbar» is the node that moves up/down over current block\r\n *\r\n * ______________________________________ Toolbar ____________________________________________\r\n * | |\r\n * | ..................... Content .................... ......... Block Actions .......... |\r\n * | . . . . |\r\n * | . . . [Open Settings] . |\r\n * | . [Plus Button] [Toolbox: {Tool1}, {Tool2}] . . . |\r\n * | . . . [Settings Panel] . |\r\n * | .................................................. .................................. |\r\n * | |\r\n * |___________________________________________________________________________________________|\r\n *\r\n *\r\n * Toolbox — its an Element contains tools buttons. Can be shown by Plus Button.\r\n *\r\n * _______________ Toolbox _______________\r\n * | |\r\n * | [Header] [Image] [List] [Quote] ... |\r\n * |_______________________________________|\r\n *\r\n *\r\n * Settings Panel — is an Element with block settings:\r\n *\r\n * ____ Settings Panel ____\r\n * | ...................... |\r\n * | . Tool Settings . |\r\n * | ...................... |\r\n * | . Default Settings . |\r\n * | ...................... |\r\n * |________________________|\r\n *\r\n *\r\n * @class\r\n * @classdesc Toolbar module\r\n *\r\n * @typedef {Toolbar} Toolbar\r\n * @property {Object} nodes\r\n * @property {Element} nodes.wrapper - Toolbar main element\r\n * @property {Element} nodes.content - Zone with Plus button and toolbox.\r\n * @property {Element} nodes.actions - Zone with Block Settings and Remove Button\r\n * @property {Element} nodes.blockActionsButtons - Zone with Block Buttons: [Settings]\r\n * @property {Element} nodes.plusButton - Button that opens or closes Toolbox\r\n * @property {Element} nodes.toolbox - Container for tools\r\n * @property {Element} nodes.settingsToggler - open/close Settings Panel button\r\n * @property {Element} nodes.settings - Settings Panel\r\n * @property {Element} nodes.pluginSettings - Plugin Settings section of Settings Panel\r\n * @property {Element} nodes.defaultSettings - Default Settings section of Settings Panel\r\n */\r\nexport default class Toolbar extends Module {\r\n /**\r\n * @constructor\r\n */\r\n constructor({config}) {\r\n super({config});\r\n\r\n this.nodes = {\r\n wrapper : null,\r\n content : null,\r\n actions : null,\r\n\r\n // Content Zone\r\n plusButton : null,\r\n\r\n // Actions Zone\r\n blockActionsButtons: null,\r\n settingsToggler : null,\r\n };\r\n }\r\n\r\n /**\r\n * CSS styles\r\n * @return {Object}\r\n * @constructor\r\n */\r\n static get CSS() {\r\n return {\r\n toolbar: 'ce-toolbar',\r\n content: 'ce-toolbar__content',\r\n actions: 'ce-toolbar__actions',\r\n\r\n toolbarOpened: 'ce-toolbar--opened',\r\n\r\n // Content Zone\r\n plusButton: 'ce-toolbar__plus',\r\n plusButtonHidden: 'ce-toolbar__plus--hidden',\r\n\r\n // Actions Zone\r\n blockActionsButtons: 'ce-toolbar__actions-buttons',\r\n settingsToggler: 'ce-toolbar__settings-btn',\r\n };\r\n }\r\n\r\n /**\r\n * Makes toolbar\r\n */\r\n make() {\r\n this.nodes.wrapper = $.make('div', Toolbar.CSS.toolbar);\r\n\r\n /**\r\n * Make Content Zone and Actions Zone\r\n */\r\n ['content', 'actions'].forEach( el => {\r\n this.nodes[el] = $.make('div', Toolbar.CSS[el]);\r\n $.append(this.nodes.wrapper, this.nodes[el]);\r\n });\r\n\r\n\r\n /**\r\n * Fill Content Zone:\r\n * - Plus Button\r\n * - Toolbox\r\n */\r\n this.nodes.plusButton = $.make('div', Toolbar.CSS.plusButton);\r\n $.append(this.nodes.plusButton, $.svg('plus', 14, 14));\r\n $.append(this.nodes.content, this.nodes.plusButton);\r\n this.nodes.plusButton.addEventListener('click', event => this.plusButtonClicked(event), false);\r\n\r\n\r\n /**\r\n * Make a Toolbox\r\n */\r\n this.Editor.Toolbox.make();\r\n\r\n /**\r\n * Fill Actions Zone:\r\n * - Settings Toggler\r\n * - Remove Block Button\r\n * - Settings Panel\r\n */\r\n this.nodes.blockActionsButtons = $.make('div', Toolbar.CSS.blockActionsButtons);\r\n this.nodes.settingsToggler = $.make('span', Toolbar.CSS.settingsToggler);\r\n const settingsIcon = $.svg('dots', 18, 4);\r\n\r\n $.append(this.nodes.settingsToggler, settingsIcon);\r\n $.append(this.nodes.blockActionsButtons, this.nodes.settingsToggler);\r\n $.append(this.nodes.actions, this.nodes.blockActionsButtons);\r\n\r\n /**\r\n * Make and append Settings Panel\r\n */\r\n this.Editor.BlockSettings.make();\r\n $.append(this.nodes.actions, this.Editor.BlockSettings.nodes.wrapper);\r\n\r\n /**\r\n * Append toolbar to the Editor\r\n */\r\n $.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper);\r\n\r\n /**\r\n * Bind events on the Toolbar elements\r\n */\r\n this.bindEvents();\r\n }\r\n\r\n /**\r\n * Move Toolbar to the Current Block\r\n * @param {Boolean} forceClose - force close Toolbar Settings and Toolbar\r\n */\r\n move(forceClose = true) {\r\n if (forceClose) {\r\n /** Close Toolbox when we move toolbar */\r\n this.Editor.Toolbox.close();\r\n this.Editor.BlockSettings.close();\r\n }\r\n\r\n let currentNode = this.Editor.BlockManager.currentNode;\r\n\r\n /**\r\n * If no one Block selected as a Current\r\n */\r\n if (!currentNode) {\r\n return;\r\n }\r\n\r\n /**\r\n * @todo Compute dynamically on prepare\r\n * @type {number}\r\n */\r\n const defaultToolbarHeight = 49;\r\n const defaultOffset = 34;\r\n\r\n var newYCoordinate = currentNode.offsetTop - (defaultToolbarHeight / 2) + defaultOffset;\r\n\r\n this.nodes.wrapper.style.transform = `translate3D(0, ${Math.floor(newYCoordinate)}px, 0)`;\r\n }\r\n\r\n /**\r\n * Open Toolbar with Plus Button\r\n */\r\n open() {\r\n this.nodes.wrapper.classList.add(Toolbar.CSS.toolbarOpened);\r\n }\r\n\r\n /**\r\n * Close the Toolbar\r\n */\r\n close() {\r\n this.nodes.wrapper.classList.remove(Toolbar.CSS.toolbarOpened);\r\n }\r\n\r\n /**\r\n * Plus Button public methods\r\n * @return {{hide: function(): void, show: function(): void}}\r\n */\r\n get plusButton() {\r\n return {\r\n hide: () => this.nodes.plusButton.classList.add(Toolbar.CSS.plusButtonHidden),\r\n show: () => this.nodes.plusButton.classList.remove(Toolbar.CSS.plusButtonHidden)\r\n };\r\n }\r\n\r\n /**\r\n * Handler for Plus Button\r\n * @param {MouseEvent} event\r\n */\r\n plusButtonClicked() {\r\n this.Editor.Toolbox.toggle();\r\n }\r\n\r\n /**\r\n * Bind events on the Toolbar Elements:\r\n * - Block Settings\r\n */\r\n bindEvents() {\r\n /**\r\n * Settings toggler\r\n */\r\n this.Editor.Listeners.on(this.nodes.settingsToggler, 'click', (event) => {\r\n this.settingsTogglerClicked(event);\r\n });\r\n }\r\n\r\n /**\r\n * Clicks on the Block Settings toggler\r\n */\r\n settingsTogglerClicked() {\r\n if (this.Editor.BlockSettings.opened) {\r\n this.Editor.BlockSettings.close();\r\n } else {\r\n this.Editor.BlockSettings.open();\r\n }\r\n }\r\n}\r\n","/**\r\n * Inline toolbar\r\n *\r\n * Contains from tools:\r\n * Bold, Italic, Underline and Anchor\r\n *\r\n * @author Codex Team\r\n * @version 1.0\r\n */\r\n\r\nmodule.exports = (function (inline) {\r\n let editor = codex.editor;\r\n\r\n inline.buttonsOpened = null;\r\n inline.actionsOpened = null;\r\n inline.wrappersOffset = null;\r\n\r\n /**\r\n * saving selection that need for execCommand for styling\r\n *\r\n */\r\n inline.storedSelection = null;\r\n\r\n /**\r\n * @protected\r\n *\r\n * Open inline toobar\r\n */\r\n inline.show = function () {\r\n var currentNode = editor.content.currentNode,\r\n tool = currentNode.dataset.tool,\r\n plugin;\r\n\r\n /**\r\n * tool allowed to open inline toolbar\r\n */\r\n plugin = editor.tools[tool];\r\n\r\n if (!plugin.showInlineToolbar)\r\n return;\r\n\r\n var selectedText = inline.getSelectionText(),\r\n toolbar = editor.nodes.inlineToolbar.wrapper;\r\n\r\n if (selectedText.length > 0) {\r\n /** Move toolbar and open */\r\n editor.toolbar.inline.move();\r\n\r\n /** Open inline toolbar */\r\n toolbar.classList.add('opened');\r\n\r\n /** show buttons of inline toolbar */\r\n editor.toolbar.inline.showButtons();\r\n }\r\n };\r\n\r\n /**\r\n * @protected\r\n *\r\n * Closes inline toolbar\r\n */\r\n inline.close = function () {\r\n var toolbar = editor.nodes.inlineToolbar.wrapper;\r\n\r\n toolbar.classList.remove('opened');\r\n };\r\n\r\n /**\r\n * @private\r\n *\r\n * Moving toolbar\r\n */\r\n inline.move = function () {\r\n if (!this.wrappersOffset) {\r\n this.wrappersOffset = this.getWrappersOffset();\r\n }\r\n\r\n var coords = this.getSelectionCoords(),\r\n defaultOffset = 0,\r\n toolbar = editor.nodes.inlineToolbar.wrapper,\r\n newCoordinateX,\r\n newCoordinateY;\r\n\r\n if (toolbar.offsetHeight === 0) {\r\n defaultOffset = 40;\r\n }\r\n\r\n newCoordinateX = coords.x - this.wrappersOffset.left;\r\n newCoordinateY = coords.y + window.scrollY - this.wrappersOffset.top - defaultOffset - toolbar.offsetHeight;\r\n\r\n toolbar.style.transform = `translate3D(${Math.floor(newCoordinateX)}px, ${Math.floor(newCoordinateY)}px, 0)`;\r\n\r\n /** Close everything */\r\n editor.toolbar.inline.closeButtons();\r\n editor.toolbar.inline.closeAction();\r\n };\r\n\r\n /**\r\n * @private\r\n *\r\n * Tool Clicked\r\n */\r\n\r\n inline.toolClicked = function (event, type) {\r\n /**\r\n * For simple tools we use default browser function\r\n * For more complicated tools, we should write our own behavior\r\n */\r\n switch (type) {\r\n case 'createLink' : editor.toolbar.inline.createLinkAction(event, type); break;\r\n default : editor.toolbar.inline.defaultToolAction(type); break;\r\n }\r\n\r\n /**\r\n * highlight buttons\r\n * after making some action\r\n */\r\n editor.nodes.inlineToolbar.buttons.childNodes.forEach(editor.toolbar.inline.hightlight);\r\n };\r\n\r\n /**\r\n * @private\r\n *\r\n * Saving wrappers offset in DOM\r\n */\r\n inline.getWrappersOffset = function () {\r\n var wrapper = editor.nodes.wrapper,\r\n offset = this.getOffset(wrapper);\r\n\r\n this.wrappersOffset = offset;\r\n return offset;\r\n };\r\n\r\n /**\r\n * @private\r\n *\r\n * Calculates offset of DOM element\r\n *\r\n * @param el\r\n * @returns {{top: number, left: number}}\r\n */\r\n inline.getOffset = function ( el ) {\r\n var _x = 0;\r\n var _y = 0;\r\n\r\n while( el && !isNaN( el.offsetLeft ) && !isNaN( el.offsetTop ) ) {\r\n _x += (el.offsetLeft + el.clientLeft);\r\n _y += (el.offsetTop + el.clientTop);\r\n el = el.offsetParent;\r\n }\r\n return { top: _y, left: _x };\r\n };\r\n\r\n /**\r\n * @private\r\n *\r\n * Calculates position of selected text\r\n * @returns {{x: number, y: number}}\r\n */\r\n inline.getSelectionCoords = function () {\r\n var sel = document.selection, range;\r\n var x = 0, y = 0;\r\n\r\n if (sel) {\r\n if (sel.type != 'Control') {\r\n range = sel.createRange();\r\n range.collapse(true);\r\n x = range.boundingLeft;\r\n y = range.boundingTop;\r\n }\r\n } else if (window.getSelection) {\r\n sel = window.getSelection();\r\n\r\n if (sel.rangeCount) {\r\n range = sel.getRangeAt(0).cloneRange();\r\n if (range.getClientRects) {\r\n range.collapse(true);\r\n var rect = range.getClientRects()[0];\r\n\r\n if (!rect) {\r\n return;\r\n }\r\n\r\n x = rect.left;\r\n y = rect.top;\r\n }\r\n }\r\n }\r\n return { x: x, y: y };\r\n };\r\n\r\n /**\r\n * @private\r\n *\r\n * Returns selected text as String\r\n * @returns {string}\r\n */\r\n inline.getSelectionText = function () {\r\n var selectedText = '';\r\n\r\n // all modern browsers and IE9+\r\n if (window.getSelection) {\r\n selectedText = window.getSelection().toString();\r\n }\r\n\r\n return selectedText;\r\n };\r\n\r\n /** Opens buttons block */\r\n inline.showButtons = function () {\r\n var buttons = editor.nodes.inlineToolbar.buttons;\r\n\r\n buttons.classList.add('opened');\r\n\r\n editor.toolbar.inline.buttonsOpened = true;\r\n\r\n /** highlight buttons */\r\n editor.nodes.inlineToolbar.buttons.childNodes.forEach(editor.toolbar.inline.hightlight);\r\n };\r\n\r\n /** Makes buttons disappear */\r\n inline.closeButtons = function () {\r\n var buttons = editor.nodes.inlineToolbar.buttons;\r\n\r\n buttons.classList.remove('opened');\r\n\r\n editor.toolbar.inline.buttonsOpened = false;\r\n };\r\n\r\n /** Open buttons defined action if exist */\r\n inline.showActions = function () {\r\n var action = editor.nodes.inlineToolbar.actions;\r\n\r\n action.classList.add('opened');\r\n\r\n editor.toolbar.inline.actionsOpened = true;\r\n };\r\n\r\n /** Close actions block */\r\n inline.closeAction = function () {\r\n var action = editor.nodes.inlineToolbar.actions;\r\n\r\n action.innerHTML = '';\r\n action.classList.remove('opened');\r\n editor.toolbar.inline.actionsOpened = false;\r\n };\r\n\r\n\r\n /**\r\n * Callback for keydowns in inline toolbar \"Insert link...\" input\r\n */\r\n let inlineToolbarAnchorInputKeydown_ = function (event) {\r\n if (event.keyCode != editor.core.keys.ENTER) {\r\n return;\r\n }\r\n\r\n let editable = editor.content.currentNode,\r\n storedSelection = editor.toolbar.inline.storedSelection;\r\n\r\n editor.toolbar.inline.restoreSelection(editable, storedSelection);\r\n editor.toolbar.inline.setAnchor(this.value);\r\n\r\n /**\r\n * Preventing events that will be able to happen\r\n */\r\n event.preventDefault();\r\n event.stopImmediatePropagation();\r\n\r\n editor.toolbar.inline.clearRange();\r\n };\r\n\r\n /** Action for link creation or for setting anchor */\r\n inline.createLinkAction = function (event) {\r\n var isActive = this.isLinkActive();\r\n\r\n var editable = editor.content.currentNode,\r\n storedSelection = editor.toolbar.inline.saveSelection(editable);\r\n\r\n /** Save globally selection */\r\n editor.toolbar.inline.storedSelection = storedSelection;\r\n\r\n if (isActive) {\r\n /**\r\n * Changing stored selection. if we want to remove anchor from word\r\n * we should remove anchor from whole word, not only selected part.\r\n * The solution is than we get the length of current link\r\n * Change start position to - end of selection minus length of anchor\r\n */\r\n editor.toolbar.inline.restoreSelection(editable, storedSelection);\r\n\r\n editor.toolbar.inline.defaultToolAction('unlink');\r\n } else {\r\n /** Create input and close buttons */\r\n var action = editor.draw.inputForLink();\r\n\r\n editor.nodes.inlineToolbar.actions.appendChild(action);\r\n\r\n editor.toolbar.inline.closeButtons();\r\n editor.toolbar.inline.showActions();\r\n\r\n /**\r\n * focus to input\r\n * Solution: https://developer.mozilla.org/ru/docs/Web/API/HTMLElement/focus\r\n * Prevents event after showing input and when we need to focus an input which is in unexisted form\r\n */\r\n action.focus();\r\n event.preventDefault();\r\n\r\n /** Callback to link action */\r\n editor.listeners.add(action, 'keydown', inlineToolbarAnchorInputKeydown_, false);\r\n }\r\n };\r\n\r\n inline.isLinkActive = function () {\r\n var isActive = false;\r\n\r\n editor.nodes.inlineToolbar.buttons.childNodes.forEach(function (tool) {\r\n var dataType = tool.dataset.type;\r\n\r\n if (dataType == 'link' && tool.classList.contains('hightlighted')) {\r\n isActive = true;\r\n }\r\n });\r\n\r\n return isActive;\r\n };\r\n\r\n /** default action behavior of tool */\r\n inline.defaultToolAction = function (type) {\r\n document.execCommand(type, false, null);\r\n };\r\n\r\n /**\r\n * @private\r\n *\r\n * Sets URL\r\n *\r\n * @param {String} url - URL\r\n */\r\n inline.setAnchor = function (url) {\r\n document.execCommand('createLink', false, url);\r\n\r\n /** Close after URL inserting */\r\n editor.toolbar.inline.closeAction();\r\n };\r\n\r\n /**\r\n * @private\r\n *\r\n * Saves selection\r\n */\r\n inline.saveSelection = function (containerEl) {\r\n var range = window.getSelection().getRangeAt(0),\r\n preSelectionRange = range.cloneRange(),\r\n start;\r\n\r\n preSelectionRange.selectNodeContents(containerEl);\r\n preSelectionRange.setEnd(range.startContainer, range.startOffset);\r\n\r\n start = preSelectionRange.toString().length;\r\n\r\n return {\r\n start: start,\r\n end: start + range.toString().length\r\n };\r\n };\r\n\r\n /**\r\n * @private\r\n *\r\n * Sets to previous selection (Range)\r\n *\r\n * @param {Element} containerEl - editable element where we restore range\r\n * @param {Object} savedSel - range basic information to restore\r\n */\r\n inline.restoreSelection = function (containerEl, savedSel) {\r\n var range = document.createRange(),\r\n charIndex = 0;\r\n\r\n range.setStart(containerEl, 0);\r\n range.collapse(true);\r\n\r\n var nodeStack = [ containerEl ],\r\n node,\r\n foundStart = false,\r\n stop = false,\r\n nextCharIndex;\r\n\r\n while (!stop && (node = nodeStack.pop())) {\r\n if (node.nodeType == 3) {\r\n nextCharIndex = charIndex + node.length;\r\n\r\n if (!foundStart && savedSel.start >= charIndex && savedSel.start <= nextCharIndex) {\r\n range.setStart(node, savedSel.start - charIndex);\r\n foundStart = true;\r\n }\r\n if (foundStart && savedSel.end >= charIndex && savedSel.end <= nextCharIndex) {\r\n range.setEnd(node, savedSel.end - charIndex);\r\n stop = true;\r\n }\r\n charIndex = nextCharIndex;\r\n } else {\r\n var i = node.childNodes.length;\r\n\r\n while (i--) {\r\n nodeStack.push(node.childNodes[i]);\r\n }\r\n }\r\n }\r\n\r\n var sel = window.getSelection();\r\n\r\n sel.removeAllRanges();\r\n sel.addRange(range);\r\n };\r\n\r\n /**\r\n * @private\r\n *\r\n * Removes all ranges from window selection\r\n */\r\n inline.clearRange = function () {\r\n var selection = window.getSelection();\r\n\r\n selection.removeAllRanges();\r\n };\r\n\r\n /**\r\n * @private\r\n *\r\n * sets or removes hightlight\r\n */\r\n inline.hightlight = function (tool) {\r\n var dataType = tool.dataset.type;\r\n\r\n if (document.queryCommandState(dataType)) {\r\n editor.toolbar.inline.setButtonHighlighted(tool);\r\n } else {\r\n editor.toolbar.inline.removeButtonsHighLight(tool);\r\n }\r\n\r\n /**\r\n *\r\n * hightlight for anchors\r\n */\r\n var selection = window.getSelection(),\r\n tag = selection.anchorNode.parentNode;\r\n\r\n if (tag.tagName == 'A' && dataType == 'link') {\r\n editor.toolbar.inline.setButtonHighlighted(tool);\r\n }\r\n };\r\n\r\n /**\r\n * @private\r\n *\r\n * Mark button if text is already executed\r\n */\r\n inline.setButtonHighlighted = function (button) {\r\n button.classList.add('hightlighted');\r\n\r\n /** At link tool we also change icon */\r\n if (button.dataset.type == 'link') {\r\n var icon = button.childNodes[0];\r\n\r\n icon.classList.remove('ce-icon-link');\r\n icon.classList.add('ce-icon-unlink');\r\n }\r\n };\r\n\r\n /**\r\n * @private\r\n *\r\n * Removes hightlight\r\n */\r\n inline.removeButtonsHighLight = function (button) {\r\n button.classList.remove('hightlighted');\r\n\r\n /** At link tool we also change icon */\r\n if (button.dataset.type == 'link') {\r\n var icon = button.childNodes[0];\r\n\r\n icon.classList.remove('ce-icon-unlink');\r\n icon.classList.add('ce-icon-link');\r\n }\r\n };\r\n\r\n\r\n return inline;\r\n})({});","/**\r\n * Toolbar settings\r\n *\r\n * @version 1.0.5\r\n */\r\n\r\nmodule.exports = (function (settings) {\r\n let editor = codex.editor;\r\n\r\n settings.opened = false;\r\n\r\n settings.setting = null;\r\n settings.actions = null;\r\n\r\n /**\r\n * Append and open settings\r\n */\r\n settings.open = function (toolType) {\r\n /**\r\n * Append settings content\r\n * It's stored in tool.settings\r\n */\r\n if ( !editor.tools[toolType] || !editor.tools[toolType].renderSettings ) {\r\n return;\r\n }\r\n\r\n /**\r\n * Draw settings block\r\n */\r\n var settingsBlock = editor.tools[toolType].renderSettings();\r\n\r\n editor.nodes.pluginSettings.appendChild(settingsBlock);\r\n\r\n\r\n /** Open settings block */\r\n editor.nodes.blockSettings.classList.add('opened');\r\n this.opened = true;\r\n };\r\n\r\n /**\r\n * Close and clear settings\r\n */\r\n settings.close = function () {\r\n editor.nodes.blockSettings.classList.remove('opened');\r\n editor.nodes.pluginSettings.innerHTML = '';\r\n\r\n this.opened = false;\r\n };\r\n\r\n /**\r\n * @param {string} toolType - plugin type\r\n */\r\n settings.toggle = function ( toolType ) {\r\n if ( !this.opened ) {\r\n this.open(toolType);\r\n } else {\r\n this.close();\r\n }\r\n };\r\n\r\n /**\r\n * Here we will draw buttons and add listeners to components\r\n */\r\n settings.makeRemoveBlockButton = function () {\r\n var removeBlockWrapper = editor.draw.node('SPAN', 'ce-toolbar__remove-btn', {}),\r\n settingButton = editor.draw.node('SPAN', 'ce-toolbar__remove-setting', { innerHTML : '' }),\r\n actionWrapper = editor.draw.node('DIV', 'ce-toolbar__remove-confirmation', {}),\r\n confirmAction = editor.draw.node('DIV', 'ce-toolbar__remove-confirm', { textContent : 'Удалить блок' }),\r\n cancelAction = editor.draw.node('DIV', 'ce-toolbar__remove-cancel', { textContent : 'Отмена' });\r\n\r\n editor.listeners.add(settingButton, 'click', editor.toolbar.settings.removeButtonClicked, false);\r\n\r\n editor.listeners.add(confirmAction, 'click', editor.toolbar.settings.confirmRemovingRequest, false);\r\n\r\n editor.listeners.add(cancelAction, 'click', editor.toolbar.settings.cancelRemovingRequest, false);\r\n\r\n actionWrapper.appendChild(confirmAction);\r\n actionWrapper.appendChild(cancelAction);\r\n\r\n removeBlockWrapper.appendChild(settingButton);\r\n removeBlockWrapper.appendChild(actionWrapper);\r\n\r\n /** Save setting */\r\n editor.toolbar.settings.setting = settingButton;\r\n editor.toolbar.settings.actions = actionWrapper;\r\n\r\n return removeBlockWrapper;\r\n };\r\n\r\n settings.removeButtonClicked = function () {\r\n var action = editor.toolbar.settings.actions;\r\n\r\n if (action.classList.contains('opened')) {\r\n editor.toolbar.settings.hideRemoveActions();\r\n } else {\r\n editor.toolbar.settings.showRemoveActions();\r\n }\r\n\r\n editor.toolbar.toolbox.close();\r\n editor.toolbar.settings.close();\r\n };\r\n\r\n settings.cancelRemovingRequest = function () {\r\n editor.toolbar.settings.actions.classList.remove('opened');\r\n };\r\n\r\n settings.confirmRemovingRequest = function () {\r\n var currentBlock = editor.content.currentNode,\r\n firstLevelBlocksCount;\r\n\r\n currentBlock.remove();\r\n\r\n firstLevelBlocksCount = editor.nodes.redactor.childNodes.length;\r\n\r\n /**\r\n * If all blocks are removed\r\n */\r\n if (firstLevelBlocksCount === 0) {\r\n /** update currentNode variable */\r\n editor.content.currentNode = null;\r\n\r\n /** Inserting new empty initial block */\r\n editor.ui.addInitialBlock();\r\n }\r\n\r\n editor.ui.saveInputs();\r\n\r\n editor.toolbar.close();\r\n };\r\n\r\n settings.showRemoveActions = function () {\r\n editor.toolbar.settings.actions.classList.add('opened');\r\n };\r\n\r\n settings.hideRemoveActions = function () {\r\n editor.toolbar.settings.actions.classList.remove('opened');\r\n };\r\n\r\n return settings;\r\n})({});\r\n","/**\r\n * Codex Editor toolbar module\r\n *\r\n * Contains:\r\n * - Inline toolbox\r\n * - Toolbox within plus button\r\n * - Settings section\r\n *\r\n * @author Codex Team\r\n * @version 1.0\r\n */\r\n\r\nmodule.exports = (function (toolbar) {\r\n let editor = codex.editor;\r\n\r\n toolbar.settings = require('./settings');\r\n toolbar.inline = require('./inline');\r\n toolbar.toolbox = require('./toolbox');\r\n\r\n /**\r\n * Margin between focused node and toolbar\r\n */\r\n toolbar.defaultToolbarHeight = 49;\r\n\r\n toolbar.defaultOffset = 34;\r\n\r\n toolbar.opened = false;\r\n\r\n toolbar.current = null;\r\n\r\n /**\r\n * @protected\r\n */\r\n toolbar.open = function () {\r\n if (editor.hideToolbar) {\r\n return;\r\n }\r\n\r\n let toolType = editor.content.currentNode.dataset.tool;\r\n\r\n if (!editor.tools[toolType] || !editor.tools[toolType].renderSettings ) {\r\n editor.nodes.showSettingsButton.classList.add('hide');\r\n } else {\r\n editor.nodes.showSettingsButton.classList.remove('hide');\r\n }\r\n\r\n editor.nodes.toolbar.classList.add('opened');\r\n this.opened = true;\r\n };\r\n\r\n /**\r\n * @protected\r\n */\r\n toolbar.close = function () {\r\n editor.nodes.toolbar.classList.remove('opened');\r\n\r\n toolbar.opened = false;\r\n toolbar.current = null;\r\n\r\n for (var button in editor.nodes.toolbarButtons) {\r\n editor.nodes.toolbarButtons[button].classList.remove('selected');\r\n }\r\n\r\n /** Close toolbox when toolbar is not displayed */\r\n editor.toolbar.toolbox.close();\r\n editor.toolbar.settings.close();\r\n };\r\n\r\n toolbar.toggle = function () {\r\n if ( !this.opened ) {\r\n this.open();\r\n } else {\r\n this.close();\r\n }\r\n };\r\n\r\n toolbar.hidePlusButton = function () {\r\n editor.nodes.plusButton.classList.add('hide');\r\n };\r\n\r\n toolbar.showPlusButton = function () {\r\n editor.nodes.plusButton.classList.remove('hide');\r\n };\r\n\r\n /**\r\n * Moving toolbar to the specified node\r\n */\r\n toolbar.move = function () {\r\n /** Close Toolbox when we move toolbar */\r\n editor.toolbar.toolbox.close();\r\n\r\n if (!editor.content.currentNode) {\r\n return;\r\n }\r\n\r\n var newYCoordinate = editor.content.currentNode.offsetTop - (editor.toolbar.defaultToolbarHeight / 2) + editor.toolbar.defaultOffset;\r\n\r\n editor.nodes.toolbar.style.transform = `translate3D(0, ${Math.floor(newYCoordinate)}px, 0)`;\r\n\r\n /** Close trash actions */\r\n editor.toolbar.settings.hideRemoveActions();\r\n };\r\n\r\n return toolbar;\r\n})({});\r\n","/**\r\n * Codex Editor toolbox\r\n *\r\n * All tools be able to appended here\r\n *\r\n * @author Codex Team\r\n * @version 1.0\r\n */\r\n\r\nmodule.exports = (function (toolbox) {\r\n let editor = codex.editor;\r\n\r\n toolbox.opened = false;\r\n toolbox.openedOnBlock = null;\r\n\r\n /** Shows toolbox */\r\n toolbox.open = function () {\r\n /** Close setting if toolbox is opened */\r\n if (editor.toolbar.settings.opened) {\r\n editor.toolbar.settings.close();\r\n }\r\n\r\n /** Add 'toolbar-opened' class for current block **/\r\n toolbox.openedOnBlock = editor.content.currentNode;\r\n toolbox.openedOnBlock.classList.add('toolbar-opened');\r\n\r\n /** display toolbox */\r\n editor.nodes.toolbox.classList.add('opened');\r\n\r\n /** Animate plus button */\r\n editor.nodes.plusButton.classList.add('clicked');\r\n\r\n /** toolbox state */\r\n editor.toolbar.toolbox.opened = true;\r\n };\r\n\r\n /** Closes toolbox */\r\n toolbox.close = function () {\r\n /** Remove 'toolbar-opened' class from current block **/\r\n if (toolbox.openedOnBlock) toolbox.openedOnBlock.classList.remove('toolbar-opened');\r\n toolbox.openedOnBlock = null;\r\n\r\n /** Makes toolbox disappear */\r\n editor.nodes.toolbox.classList.remove('opened');\r\n\r\n /** Rotate plus button */\r\n editor.nodes.plusButton.classList.remove('clicked');\r\n\r\n /** toolbox state */\r\n editor.toolbar.toolbox.opened = false;\r\n\r\n editor.toolbar.current = null;\r\n };\r\n\r\n toolbox.leaf = function () {\r\n let currentTool = editor.toolbar.current,\r\n tools = Object.keys(editor.tools),\r\n barButtons = editor.nodes.toolbarButtons,\r\n nextToolIndex = 0,\r\n toolToSelect,\r\n visibleTool,\r\n tool;\r\n\r\n if ( !currentTool ) {\r\n /** Get first tool from object*/\r\n for(tool in editor.tools) {\r\n if (editor.tools[tool].displayInToolbox) {\r\n break;\r\n }\r\n\r\n nextToolIndex ++;\r\n }\r\n } else {\r\n nextToolIndex = (tools.indexOf(currentTool) + 1) % tools.length;\r\n visibleTool = tools[nextToolIndex];\r\n\r\n while (!editor.tools[visibleTool].displayInToolbox) {\r\n nextToolIndex = (nextToolIndex + 1) % tools.length;\r\n visibleTool = tools[nextToolIndex];\r\n }\r\n }\r\n\r\n toolToSelect = tools[nextToolIndex];\r\n\r\n for ( var button in barButtons ) {\r\n barButtons[button].classList.remove('selected');\r\n }\r\n\r\n barButtons[toolToSelect].classList.add('selected');\r\n editor.toolbar.current = toolToSelect;\r\n };\r\n\r\n /**\r\n * Transforming selected node type into selected toolbar element type\r\n * @param {event} event\r\n */\r\n toolbox.toolClicked = function (event) {\r\n /**\r\n * UNREPLACEBLE_TOOLS this types of tools are forbidden to replace even they are empty\r\n */\r\n var UNREPLACEBLE_TOOLS = ['image', 'link', 'list', 'instagram', 'twitter', 'embed'],\r\n tool = editor.tools[editor.toolbar.current],\r\n workingNode = editor.content.currentNode,\r\n currentInputIndex = editor.caret.inputIndex,\r\n newBlockContent,\r\n appendCallback,\r\n blockData;\r\n\r\n /** Make block from plugin */\r\n newBlockContent = tool.render();\r\n\r\n /** information about block */\r\n blockData = {\r\n block : newBlockContent,\r\n type : tool.type,\r\n stretched : false\r\n };\r\n\r\n if (\r\n workingNode &&\r\n UNREPLACEBLE_TOOLS.indexOf(workingNode.dataset.tool) === -1 &&\r\n workingNode.textContent.trim() === ''\r\n ) {\r\n /** Replace current block */\r\n editor.content.switchBlock(workingNode, newBlockContent, tool.type);\r\n } else {\r\n /** Insert new Block from plugin */\r\n editor.content.insertBlock(blockData);\r\n\r\n /** increase input index */\r\n currentInputIndex++;\r\n }\r\n\r\n /** Fire tool append callback */\r\n appendCallback = tool.appendCallback;\r\n\r\n if (appendCallback && typeof appendCallback == 'function') {\r\n appendCallback.call(event);\r\n }\r\n\r\n window.setTimeout(function () {\r\n /** Set caret to current block */\r\n editor.caret.setToBlock(currentInputIndex);\r\n }, 10);\r\n\r\n\r\n /**\r\n * Changing current Node\r\n */\r\n editor.content.workingNodeChanged();\r\n\r\n /**\r\n * Move toolbar when node is changed\r\n */\r\n editor.toolbar.move();\r\n };\r\n\r\n return toolbox;\r\n})({});","/**\r\n * @module Codex Editor Tools Submodule\r\n *\r\n * Creates Instances from Plugins and binds external config to the instances\r\n */\r\n\r\n/**\r\n * Each Tool must contain the following important objects:\r\n *\r\n * @typedef {Object} ToolConfig {@link docs/tools.md}\r\n * @property {String} iconClassname - this a icon in toolbar\r\n * @property {Boolean} displayInToolbox - will be displayed in toolbox. Default value is TRUE\r\n * @property {Boolean} enableLineBreaks - inserts new block or break lines. Default value is FALSE\r\n * @property {Boolean|String[]} inlineToolbar - Pass `true` to enable the Inline Toolbar with all Tools, all pass an array with specified Tools list |\r\n * @property render @todo add description\r\n * @property save @todo add description\r\n * @property settings @todo add description\r\n * @property validate - method that validates output data before saving\r\n */\r\n\r\n/**\r\n * @typedef {Function} Tool {@link docs/tools.md}\r\n * @property {Boolean} displayInToolbox - By default, tools won't be added in the Toolbox. Pass true to add.\r\n * @property {String} iconClassName - CSS class name for the Toolbox button\r\n * @property {Boolean} irreplaceable - Toolbox behaviour: replace or add new block below\r\n * @property render\r\n * @property save\r\n * @property settings\r\n * @property validate\r\n *\r\n * @todo update according to current API\r\n * @todo describe Tool in the {@link docs/tools.md}\r\n */\r\n\r\n/**\r\n * Class properties:\r\n *\r\n * @typedef {Tools} Tools\r\n * @property {Tools[]} toolsAvailable - available Tools\r\n * @property {Tools[]} toolsUnavailable - unavailable Tools\r\n * @property {Object} toolsClasses - all classes\r\n * @property {EditorConfig} config - Editor config\r\n */\r\nexport default class Tools extends Module {\r\n /**\r\n * Returns available Tools\r\n * @return {Tool[]}\r\n */\r\n get available() {\r\n return this.toolsAvailable;\r\n }\r\n\r\n /**\r\n * Returns unavailable Tools\r\n * @return {Tool[]}\r\n */\r\n get unavailable() {\r\n return this.toolsUnavailable;\r\n }\r\n\r\n /**\r\n * Return Tools for the Inline Toolbar\r\n * @return {Array} - array of Inline Tool's classes\r\n */\r\n get inline() {\r\n return Object.values(this.available).filter( tool => {\r\n if (!tool[this.apiSettings.IS_INLINE]) {\r\n return false;\r\n }\r\n\r\n /**\r\n * Some Tools validation\r\n */\r\n const inlineToolRequiredMethods = ['render', 'surround', 'checkState'];\r\n const notImplementedMethods = inlineToolRequiredMethods.filter( method => !new tool()[method] );\r\n\r\n if (notImplementedMethods.length) {\r\n _.log(`Incorrect Inline Tool: ${tool.name}. Some of required methods is not implemented %o`, 'warn', notImplementedMethods);\r\n return false;\r\n }\r\n\r\n return true;\r\n });\r\n }\r\n\r\n /**\r\n * Return editor block tools\r\n */\r\n get blockTools() {\r\n // eslint-disable-next-line no-unused-vars\r\n const tools = Object.entries(this.available).filter( ([name, tool]) => {\r\n if (tool[this.apiSettings.IS_INLINE]) {\r\n return false;\r\n }\r\n\r\n return true;\r\n });\r\n\r\n const result = {};\r\n\r\n tools.forEach(([name, tool]) => result[name] = tool);\r\n\r\n return result;\r\n }\r\n\r\n /**\r\n * Constant for available Tools Settings\r\n * @return {object}\r\n */\r\n get apiSettings() {\r\n return {\r\n IS_INLINE: 'isInline',\r\n TOOLBAR_ICON_CLASS: 'iconClassName',\r\n IS_DISPLAYED_IN_TOOLBOX: 'displayInToolbox',\r\n IS_ENABLED_LINE_BREAKS: 'enableLineBreaks',\r\n IS_IRREPLACEBLE_TOOL: 'irreplaceable',\r\n IS_ENABLED_INLINE_TOOLBAR: 'inlineToolbar',\r\n IS_PASTE_DISALLOWED: 'disallowPaste'\r\n };\r\n }\r\n\r\n /**\r\n * Static getter for default Tool config fields\r\n * @return {ToolConfig}\r\n */\r\n get defaultConfig() {\r\n return {\r\n [this.apiSettings.TOOLBAR_ICON_CLASS] : false,\r\n [this.apiSettings.IS_DISPLAYED_IN_TOOLBOX] : false,\r\n [this.apiSettings.IS_ENABLED_LINE_BREAKS] : false,\r\n [this.apiSettings.IS_IRREPLACEBLE_TOOL] : false,\r\n [this.apiSettings.IS_ENABLED_INLINE_TOOLBAR]: false,\r\n };\r\n }\r\n\r\n /**\r\n * @constructor\r\n *\r\n * @param {EditorConfig} config\r\n */\r\n constructor({config}) {\r\n super({config});\r\n\r\n /**\r\n * Map {name: Class, ...} where:\r\n * name — block type name in JSON. Got from EditorConfig.tools keys\r\n * @type {Object}\r\n */\r\n this.toolClasses = {};\r\n\r\n /**\r\n * Available tools list\r\n * {name: Class, ...}\r\n * @type {Object}\r\n */\r\n this.toolsAvailable = {};\r\n\r\n /**\r\n * Tools that rejected a prepare method\r\n * {name: Class, ... }\r\n * @type {Object}\r\n */\r\n this.toolsUnavailable = {};\r\n }\r\n\r\n /**\r\n * Creates instances via passed or default configuration\r\n * @return {Promise}\r\n */\r\n prepare() {\r\n if (!this.config.hasOwnProperty('tools')) {\r\n return Promise.reject(\"Can't start without tools\");\r\n }\r\n\r\n for(let toolName in this.config.tools) {\r\n this.toolClasses[toolName] = this.config.tools[toolName];\r\n }\r\n\r\n /**\r\n * getting classes that has prepare method\r\n */\r\n let sequenceData = this.getListOfPrepareFunctions();\r\n\r\n /**\r\n * if sequence data contains nothing then resolve current chain and run other module prepare\r\n */\r\n if (sequenceData.length === 0) {\r\n return Promise.resolve();\r\n }\r\n\r\n /**\r\n * to see how it works {@link Util#sequence}\r\n */\r\n return _.sequence(sequenceData, (data) => {\r\n this.success(data);\r\n }, (data) => {\r\n this.fallback(data);\r\n });\r\n }\r\n\r\n /**\r\n * Binds prepare function of plugins with user or default config\r\n * @return {Array} list of functions that needs to be fired sequentially\r\n */\r\n getListOfPrepareFunctions() {\r\n let toolPreparationList = [];\r\n\r\n for(let toolName in this.toolClasses) {\r\n let toolClass = this.toolClasses[toolName];\r\n\r\n if (typeof toolClass.prepare === 'function') {\r\n toolPreparationList.push({\r\n function : toolClass.prepare,\r\n data : {\r\n toolName\r\n }\r\n });\r\n } else {\r\n /**\r\n * If Tool hasn't a prepare method, mark it as available\r\n */\r\n this.toolsAvailable[toolName] = toolClass;\r\n }\r\n }\r\n\r\n return toolPreparationList;\r\n }\r\n\r\n /**\r\n * @param {ChainData.data} data - append tool to available list\r\n */\r\n success(data) {\r\n this.toolsAvailable[data.toolName] = this.toolClasses[data.toolName];\r\n }\r\n\r\n /**\r\n * @param {ChainData.data} data - append tool to unavailable list\r\n */\r\n fallback(data) {\r\n this.toolsUnavailable[data.toolName] = this.toolClasses[data.toolName];\r\n }\r\n\r\n /**\r\n * Return tool`a instance\r\n *\r\n * @param {String} tool — tool name\r\n * @param {Object} data — initial data\r\n *\r\n * @todo throw exceptions if tool doesnt exist\r\n *\r\n */\r\n construct(tool, data) {\r\n let plugin = this.toolClasses[tool],\r\n config = this.config.toolsConfig[tool];\r\n\r\n let instance = new plugin(data, config || {});\r\n\r\n return instance;\r\n }\r\n\r\n /**\r\n * Check if passed Tool is an instance of Initial Block Tool\r\n * @param {Tool} tool - Tool to check\r\n * @return {Boolean}\r\n */\r\n isInitial(tool) {\r\n return tool instanceof this.available[this.config.initialBlock];\r\n }\r\n}\r\n","/**\r\n * Module UI\r\n *\r\n * @type {UI}\r\n */\r\n\r\n/**\r\n * Prebuilded sprite of SVG icons\r\n */\r\nimport sprite from '../../../build/sprite.svg';\r\nimport Selection from '../selection';\r\n\r\n/**\r\n * @class\r\n *\r\n * @classdesc Makes CodeX Editor UI:\r\n * \r\n * \r\n * \r\n * \r\n * \r\n *\r\n * @typedef {UI} UI\r\n * @property {EditorConfig} config - editor configuration {@link CodexEditor#configuration}\r\n * @property {Object} Editor - available editor modules {@link CodexEditor#moduleInstances}\r\n * @property {Object} nodes -\r\n * @property {Element} nodes.holder - element where we need to append redactor\r\n * @property {Element} nodes.wrapper - \r\n * @property {Element} nodes.redactor - \r\n */\r\nexport default class UI extends Module {\r\n /**\r\n * @constructor\r\n *\r\n * @param {EditorConfig} config\r\n */\r\n constructor({config}) {\r\n super({config});\r\n\r\n this.nodes = {\r\n holder: null,\r\n wrapper: null,\r\n redactor: null\r\n };\r\n }\r\n\r\n /**\r\n * Making main interface\r\n */\r\n async prepare() {\r\n await this.make();\r\n\r\n /**\r\n * Append SVG sprite\r\n */\r\n await this.appendSVGSprite();\r\n\r\n /**\r\n * Make toolbar\r\n */\r\n await this.Editor.Toolbar.make();\r\n\r\n /**\r\n * Make the Inline toolbar\r\n */\r\n await this.Editor.InlineToolbar.make();\r\n\r\n /**\r\n * Load and append CSS\r\n */\r\n await this.loadStyles();\r\n\r\n /**\r\n * Bind events for the UI elements\r\n */\r\n await this.bindEvents();\r\n }\r\n\r\n /**\r\n * CodeX Editor UI CSS class names\r\n * @return {{editorWrapper: string, editorZone: string, block: string}}\r\n */\r\n get CSS() {\r\n return {\r\n editorWrapper : 'codex-editor',\r\n editorZone : 'codex-editor__redactor',\r\n };\r\n }\r\n\r\n /**\r\n * Makes CodeX Editor interface\r\n * @return {Promise}\r\n */\r\n async make() {\r\n /**\r\n * Element where we need to append CodeX Editor\r\n * @type {Element}\r\n */\r\n this.nodes.holder = document.getElementById(this.config.holderId);\r\n\r\n if (!this.nodes.holder) {\r\n throw Error(\"Holder wasn't found by ID: #\" + this.config.holderId);\r\n }\r\n\r\n /**\r\n * Create and save main UI elements\r\n */\r\n this.nodes.wrapper = $.make('div', this.CSS.editorWrapper);\r\n this.nodes.redactor = $.make('div', this.CSS.editorZone);\r\n\r\n this.nodes.wrapper.appendChild(this.nodes.redactor);\r\n this.nodes.holder.appendChild(this.nodes.wrapper);\r\n }\r\n\r\n /**\r\n * Appends CSS\r\n */\r\n loadStyles() {\r\n /**\r\n * Load CSS\r\n */\r\n let styles = require('../../styles/main.css');\r\n\r\n /**\r\n * Make tag\r\n */\r\n let tag = $.make('style', null, {\r\n textContent: styles.toString()\r\n });\r\n\r\n /**\r\n * Append styles\r\n */\r\n $.append(document.head, tag);\r\n }\r\n\r\n /**\r\n * Bind events on the CodeX Editor interface\r\n */\r\n bindEvents() {\r\n this.Editor.Listeners.on(this.nodes.redactor, 'click', event => this.redactorClicked(event), false );\r\n this.Editor.Listeners.on(document, 'keydown', event => this.documentKeydown(event), true );\r\n this.Editor.Listeners.on(document, 'click', event => this.documentClicked(event), false );\r\n }\r\n\r\n /**\r\n * All keydowns on document\r\n * @param event\r\n */\r\n documentKeydown(event) {\r\n switch (event.keyCode) {\r\n case _.keyCodes.ENTER:\r\n this.enterPressed(event);\r\n break;\r\n\r\n default:\r\n this.defaultBehaviour(event);\r\n break;\r\n }\r\n }\r\n\r\n /**\r\n * Ignore all other document's keydown events\r\n * @param {KeyboardEvent} event\r\n */\r\n defaultBehaviour(event) {\r\n const keyDownOnEditor = event.target.closest(`.${this.CSS.editorWrapper}`);\r\n\r\n /**\r\n * Ignore keydowns on document\r\n * clear pointer and close toolbar\r\n */\r\n if (!keyDownOnEditor) {\r\n /**\r\n * Remove all highlights and remove caret\r\n */\r\n this.Editor.BlockManager.dropPointer();\r\n\r\n /**\r\n * Close Toolbar\r\n */\r\n this.Editor.Toolbar.close();\r\n }\r\n }\r\n\r\n /**\r\n * Enter pressed on document\r\n * @param event\r\n */\r\n enterPressed(event) {\r\n let hasPointerToBlock = this.Editor.BlockManager.currentBlockIndex >= 0;\r\n\r\n /**\r\n * If Selection is out of Editor and document has some selection\r\n */\r\n if (!Selection.isAtEditor && Selection.anchorNode) {\r\n return;\r\n }\r\n\r\n /**\r\n * If there is no selection (caret is not placed) and BlockManager points some to Block\r\n */\r\n if (hasPointerToBlock && !Selection.anchorNode) {\r\n /**\r\n * Insert initial typed Block\r\n */\r\n this.Editor.BlockManager.insert();\r\n this.Editor.BlockManager.highlightCurrentNode();\r\n\r\n /**\r\n * Move toolbar and show plus button because new Block is empty\r\n */\r\n this.Editor.Toolbar.move();\r\n this.Editor.Toolbar.plusButton.show();\r\n }\r\n }\r\n\r\n /**\r\n * All clicks on document\r\n * @param {MouseEvent} event - Click\r\n */\r\n documentClicked(event) {\r\n /**\r\n * Close Inline Toolbar when nothing selected\r\n * Do not fire check on clicks at the Inline Toolbar buttons\r\n */\r\n const clickedOnInlineToolbarButton = event.target.closest(`.${this.Editor.InlineToolbar.CSS.inlineToolbar}`);\r\n const clickedInsideofEditor = event.target.closest(`.${this.CSS.editorWrapper}`);\r\n\r\n /** Clear highlightings and pointer on BlockManager */\r\n if (!clickedInsideofEditor) {\r\n this.Editor.BlockManager.dropPointer();\r\n this.Editor.Toolbar.close();\r\n }\r\n\r\n if (!clickedOnInlineToolbarButton) {\r\n this.Editor.InlineToolbar.handleShowingEvent(event);\r\n }\r\n }\r\n\r\n /**\r\n * All clicks on the redactor zone\r\n *\r\n * @param {MouseEvent} event\r\n *\r\n * @description\r\n * 1. Save clicked Block as a current {@link BlockManager#currentNode}\r\n * it uses for the following:\r\n * - add CSS modifier for the selected Block\r\n * - on Enter press, we make a new Block under that\r\n *\r\n * 2. Move and show the Toolbar\r\n *\r\n * 3. Set a Caret\r\n *\r\n * 4. By clicks on the Editor's bottom zone:\r\n * - if last Block is empty, set a Caret to this\r\n * - otherwise, add a new empty Block and set a Caret to that\r\n *\r\n * 5. Hide the Inline Toolbar\r\n *\r\n * @see selectClickedBlock\r\n *\r\n */\r\n redactorClicked(event) {\r\n let clickedNode = event.target;\r\n\r\n /**\r\n * Select clicked Block as Current\r\n */\r\n try {\r\n /**\r\n * Renew Current Block\r\n */\r\n this.Editor.BlockManager.setCurrentBlockByChildNode(clickedNode);\r\n\r\n /**\r\n * Highlight Current Node\r\n */\r\n this.Editor.BlockManager.highlightCurrentNode();\r\n } catch (e) {\r\n /**\r\n * If clicked outside first-level Blocks, set Caret to the last empty Block\r\n */\r\n this.Editor.Caret.setToTheLastBlock();\r\n }\r\n\r\n /**\r\n * Move toolbar and open\r\n */\r\n this.Editor.Toolbar.move();\r\n this.Editor.Toolbar.open();\r\n\r\n /**\r\n * Hide the Plus Button\r\n * */\r\n this.Editor.Toolbar.plusButton.hide();\r\n\r\n /**\r\n * Show the Plus Button if:\r\n * - Block is an initial-block (Text)\r\n * - Block is empty\r\n */\r\n let isInitialBlock = this.Editor.Tools.isInitial(this.Editor.BlockManager.currentBlock.tool),\r\n isEmptyBlock = this.Editor.BlockManager.currentBlock.isEmpty;\r\n\r\n if (isInitialBlock && isEmptyBlock) {\r\n this.Editor.Toolbar.plusButton.show();\r\n }\r\n }\r\n\r\n /**\r\n * Append prebuilded sprite with SVG icons\r\n */\r\n appendSVGSprite() {\r\n let spriteHolder = $.make('div');\r\n\r\n spriteHolder.innerHTML = sprite;\r\n\r\n $.append(this.nodes.wrapper, spriteHolder);\r\n }\r\n}\r\n\r\n// /**\r\n// * Codex Editor UI module\r\n// *\r\n// * @author Codex Team\r\n// * @version 1.2.0\r\n// */\r\n//\r\n// module.exports = (function (ui) {\r\n//\r\n// let editor = codex.editor;\r\n//\r\n// /**\r\n// * Basic editor classnames\r\n// */\r\n// ui.prepare = function () {\r\n//\r\n\r\n//\r\n// };\r\n//\r\n// /** Draw notifications holder */\r\n// var makeNotificationHolder_ = function () {\r\n//\r\n// /** Append block with notifications to the document */\r\n// editor.nodes.notifications = editor.notifications.createHolder();\r\n//\r\n// };\r\n//\r\n//\r\n// var addInlineToolbarTools_ = function () {\r\n//\r\n// var tools = {\r\n//\r\n// bold: {\r\n// icon : 'ce-icon-bold',\r\n// command : 'bold'\r\n// },\r\n//\r\n// italic: {\r\n// icon : 'ce-icon-italic',\r\n// command : 'italic'\r\n// },\r\n//\r\n// link: {\r\n// icon : 'ce-icon-link',\r\n// command : 'createLink'\r\n// }\r\n// };\r\n//\r\n// var toolButton,\r\n// tool;\r\n//\r\n// for(var name in tools) {\r\n//\r\n// tool = tools[name];\r\n//\r\n// toolButton = editor.draw.toolbarButtonInline(name, tool.icon);\r\n//\r\n// editor.nodes.inlineToolbar.buttons.appendChild(toolButton);\r\n// /**\r\n// * Add callbacks to this buttons\r\n// */\r\n// editor.ui.setInlineToolbarButtonBehaviour(toolButton, tool.command);\r\n//\r\n// }\r\n//\r\n// };\r\n//\r\n// /**\r\n// * @private\r\n// * Bind editor UI events\r\n// */\r\n// var bindEvents_ = function () {\r\n//\r\n// editor.core.log('ui.bindEvents fired', 'info');\r\n//\r\n// // window.addEventListener('error', function (errorMsg, url, lineNumber) {\r\n// // editor.notifications.errorThrown(errorMsg, event);\r\n// // }, false );\r\n//\r\n// /** All keydowns on Document */\r\n// editor.listeners.add(document, 'keydown', editor.callback.globalKeydown, false);\r\n//\r\n// /** All keydowns on Redactor zone */\r\n// editor.listeners.add(editor.nodes.redactor, 'keydown', editor.callback.redactorKeyDown, false);\r\n//\r\n// /** All keydowns on Document */\r\n// editor.listeners.add(document, 'keyup', editor.callback.globalKeyup, false );\r\n//\r\n// /**\r\n// * Mouse click to radactor\r\n// */\r\n// editor.listeners.add(editor.nodes.redactor, 'click', editor.callback.redactorClicked, false );\r\n//\r\n// /**\r\n// * Clicks to the Plus button\r\n// */\r\n// editor.listeners.add(editor.nodes.plusButton, 'click', editor.callback.plusButtonClicked, false);\r\n//\r\n// /**\r\n// * Clicks to SETTINGS button in toolbar\r\n// */\r\n// editor.listeners.add(editor.nodes.showSettingsButton, 'click', editor.callback.showSettingsButtonClicked, false );\r\n//\r\n// /** Bind click listeners on toolbar buttons */\r\n// for (var button in editor.nodes.toolbarButtons) {\r\n//\r\n// editor.listeners.add(editor.nodes.toolbarButtons[button], 'click', editor.callback.toolbarButtonClicked, false);\r\n//\r\n// }\r\n//\r\n// };\r\n//\r\n// ui.addBlockHandlers = function (block) {\r\n//\r\n// if (!block) return;\r\n//\r\n// /**\r\n// * Block keydowns\r\n// */\r\n// editor.listeners.add(block, 'keydown', editor.callback.blockKeydown, false);\r\n//\r\n// /**\r\n// * Pasting content from another source\r\n// * We have two type of sanitization\r\n// * First - uses deep-first search algorithm to get sub nodes,\r\n// * sanitizes whole Block_content and replaces cleared nodes\r\n// * This method is deprecated\r\n// * Method is used in editor.callback.blockPaste(event)\r\n// *\r\n// * Secont - uses Mutation observer.\r\n// * Observer \"observe\" DOM changes and send changings to callback.\r\n// * Callback gets changed node, not whole Block_content.\r\n// * Inserted or changed node, which we've gotten have been cleared and replaced with diry node\r\n// *\r\n// * Method is used in editor.callback.blockPasteViaSanitize(event)\r\n// *\r\n// * @uses html-janitor\r\n// * @example editor.callback.blockPasteViaSanitize(event), the second method.\r\n// *\r\n// */\r\n// editor.listeners.add(block, 'paste', editor.paste.blockPasteCallback, false);\r\n//\r\n// /**\r\n// * Show inline toolbar for selected text\r\n// */\r\n// editor.listeners.add(block, 'mouseup', editor.toolbar.inline.show, false);\r\n// editor.listeners.add(block, 'keyup', editor.toolbar.inline.show, false);\r\n//\r\n// };\r\n//\r\n// /** getting all contenteditable elements */\r\n// ui.saveInputs = function () {\r\n//\r\n// var redactor = editor.nodes.redactor;\r\n//\r\n// editor.state.inputs = [];\r\n//\r\n// /** Save all inputs in global variable state */\r\n// var inputs = redactor.querySelectorAll('[contenteditable], input, textarea');\r\n//\r\n// Array.prototype.map.call(inputs, function (current) {\r\n//\r\n// if (!current.type || current.type == 'text' || current.type == 'textarea') {\r\n//\r\n// editor.state.inputs.push(current);\r\n//\r\n// }\r\n//\r\n// });\r\n//\r\n// };\r\n//\r\n// /**\r\n// * Adds first initial block on empty redactor\r\n// */\r\n// ui.addInitialBlock = function () {\r\n//\r\n// var initialBlockType = editor.settings.initialBlockPlugin,\r\n// initialBlock;\r\n//\r\n// if ( !editor.tools[initialBlockType] ) {\r\n//\r\n// editor.core.log('Plugin %o was not implemented and can\\'t be used as initial block', 'warn', initialBlockType);\r\n// return;\r\n//\r\n// }\r\n//\r\n// initialBlock = editor.tools[initialBlockType].render();\r\n//\r\n// initialBlock.setAttribute('data-placeholder', editor.settings.placeholder);\r\n//\r\n// editor.content.insertBlock({\r\n// type : initialBlockType,\r\n// block : initialBlock\r\n// });\r\n//\r\n// editor.content.workingNodeChanged(initialBlock);\r\n//\r\n// };\r\n//\r\n// ui.setInlineToolbarButtonBehaviour = function (button, type) {\r\n//\r\n// editor.listeners.add(button, 'mousedown', function (event) {\r\n//\r\n// editor.toolbar.inline.toolClicked(event, type);\r\n//\r\n// }, false);\r\n//\r\n// };\r\n//\r\n// return ui;\r\n//\r\n// })({});\r\n","/**\r\n * Element.closest()\r\n *\r\n * https://developer.mozilla.org/en-US/docs/Web/API/Element/closest\r\n */\r\nif (!Element.prototype.matches)\r\n Element.prototype.matches = Element.prototype.msMatchesSelector ||\r\n Element.prototype.webkitMatchesSelector;\r\n\r\nif (!Element.prototype.closest)\r\n Element.prototype.closest = function (s) {\r\n var el = this;\r\n\r\n if (!document.documentElement.contains(el)) return null;\r\n do {\r\n if (el.matches(s)) return el;\r\n el = el.parentElement || el.parentNode;\r\n } while (el !== null);\r\n return null;\r\n };\r\n","/**\r\n * Working with selection\r\n * @typedef {Selection} Selection\r\n */\r\nexport default class Selection {\r\n /**\r\n * @constructor\r\n */\r\n constructor() {\r\n this.instance = null;\r\n this.selection = null;\r\n\r\n /**\r\n * This property can store Selection's range for restoring later\r\n * @type {Range|null}\r\n */\r\n this.savedSelectionRange = null;\r\n }\r\n\r\n /**\r\n * Editor styles\r\n * @return {{editorWrapper: string, editorZone: string}}\r\n * @constructor\r\n */\r\n static get CSS() {\r\n return {\r\n editorWrapper : 'codex-editor',\r\n editorZone : 'codex-editor__redactor',\r\n };\r\n }\r\n\r\n /**\r\n * Returns window Selection\r\n * {@link https://developer.mozilla.org/ru/docs/Web/API/Window/getSelection}\r\n * @return {Selection}\r\n */\r\n static get() {\r\n return window.getSelection();\r\n }\r\n\r\n /**\r\n * Returns selected anchor\r\n * {@link https://developer.mozilla.org/ru/docs/Web/API/Selection/anchorNode}\r\n * @return {Node|null}\r\n */\r\n static get anchorNode() {\r\n const selection = window.getSelection();\r\n\r\n return selection ? selection.anchorNode : null;\r\n }\r\n\r\n /**\r\n * Returns selection offset according to the anchor node\r\n * {@link https://developer.mozilla.org/ru/docs/Web/API/Selection/anchorOffset}\r\n * @return {Number|null}\r\n */\r\n static get anchorOffset() {\r\n const selection = window.getSelection();\r\n\r\n return selection ? selection.anchorOffset : null;\r\n }\r\n\r\n /**\r\n * Is current selection range collapsed\r\n * @return {boolean|null}\r\n */\r\n static get isCollapsed() {\r\n const selection = window.getSelection();\r\n\r\n return selection ? selection.isCollapsed : null;\r\n }\r\n\r\n /**\r\n * Check current selection if it is at Editor's zone\r\n * @return {boolean}\r\n */\r\n static get isAtEditor() {\r\n let selection = Selection.get(),\r\n selectedNode,\r\n editorZone = false;\r\n\r\n /**\r\n * Something selected on document\r\n */\r\n selectedNode = selection.anchorNode || selection.focusNode;\r\n\r\n if (selectedNode && selectedNode.nodeType === Node.TEXT_NODE) {\r\n selectedNode = selectedNode.parentNode;\r\n }\r\n\r\n if (selectedNode) {\r\n editorZone = selectedNode.closest(`.${Selection.CSS.editorZone}`);\r\n }\r\n\r\n /**\r\n * Selection is not out of Editor because Editor's wrapper was found\r\n */\r\n return editorZone && editorZone.nodeType === Node.ELEMENT_NODE;\r\n }\r\n\r\n /**\r\n * Return first range\r\n * @return {Range|null}\r\n */\r\n static get range() {\r\n const selection = window.getSelection();\r\n\r\n return selection && selection.rangeCount ? selection.getRangeAt(0) : null;\r\n }\r\n\r\n /**\r\n * Calculates position and size of selected text\r\n * @return {{x, y, width, height, top?, left?, bottom?, right?}}\r\n */\r\n static get rect() {\r\n let sel = document.selection, range;\r\n let rect = {\r\n x: 0,\r\n y: 0,\r\n width: 0,\r\n height: 0\r\n };\r\n\r\n if (sel && sel.type !== 'Control') {\r\n range = sel.createRange();\r\n rect.x = range.boundingLeft;\r\n rect.y = range.boundingTop;\r\n rect.width = range.boundingWidth;\r\n rect.height = range.boundingHeight;\r\n\r\n return rect;\r\n }\r\n\r\n if (!window.getSelection) {\r\n _.log('Method window.getSelection is not supported', 'warn');\r\n return rect;\r\n }\r\n\r\n sel = window.getSelection();\r\n\r\n if (!sel.rangeCount) {\r\n _.log('Method Selection.rangeCount() is not supported', 'warn');\r\n return rect;\r\n }\r\n\r\n range = sel.getRangeAt(0).cloneRange();\r\n\r\n if (range.getBoundingClientRect) {\r\n rect = range.getBoundingClientRect();\r\n }\r\n // Fall back to inserting a temporary element\r\n if (rect.x === 0 && rect.y === 0) {\r\n let span = document.createElement('span');\r\n\r\n if (span.getBoundingClientRect) {\r\n // Ensure span has dimensions and position by\r\n // adding a zero-width space character\r\n span.appendChild( document.createTextNode('\\u200b') );\r\n range.insertNode(span);\r\n rect = span.getBoundingClientRect();\r\n\r\n let spanParent = span.parentNode;\r\n\r\n spanParent.removeChild(span);\r\n\r\n // Glue any broken text nodes back together\r\n spanParent.normalize();\r\n }\r\n }\r\n\r\n return rect;\r\n }\r\n\r\n /**\r\n * Returns selected text as String\r\n * @returns {string}\r\n */\r\n static get text() {\r\n return window.getSelection ? window.getSelection().toString() : '';\r\n };\r\n\r\n /**\r\n * Save Selection's range\r\n */\r\n save() {\r\n this.savedSelectionRange = Selection.range;\r\n }\r\n\r\n /**\r\n * Restore saved Selection's range\r\n */\r\n restore() {\r\n if (!this.savedSelectionRange) {\r\n return;\r\n }\r\n\r\n const sel = window.getSelection();\r\n\r\n sel.removeAllRanges();\r\n sel.addRange(this.savedSelectionRange);\r\n }\r\n\r\n /**\r\n * Clears saved selection\r\n */\r\n clearSaved() {\r\n this.savedSelectionRange = null;\r\n }\r\n\r\n /**\r\n * Looks ahead to find passed tag from current selection\r\n *\r\n * @param {String} tagName - tag to found\r\n * @param {String} [className] - tag's class name\r\n * @param {Number} [searchDepth] - count of tags that can be included. For better performance.\r\n * @return {HTMLElement|null}\r\n */\r\n findParentTag(tagName, className, searchDepth = 10) {\r\n let selection = window.getSelection(),\r\n parentTag = null;\r\n\r\n /**\r\n * If selection is missing or no anchorNode or focusNode were found then return null\r\n */\r\n if (!selection || !selection.anchorNode || !selection.focusNode) {\r\n return null;\r\n }\r\n\r\n /**\r\n * Define Nodes for start and end of selection\r\n */\r\n let boundNodes = [\r\n /** the Node in which the selection begins */\r\n selection.anchorNode,\r\n /** the Node in which the selection ends */\r\n selection.focusNode\r\n ];\r\n\r\n /**\r\n * For each selection parent Nodes we try to find target tag [with target class name]\r\n * It would be saved in parentTag variable\r\n */\r\n boundNodes.forEach(parent => {\r\n /** Reset tags limit */\r\n let searchDepthIterable = searchDepth;\r\n\r\n while (searchDepthIterable > 0 && parent.parentNode) {\r\n /**\r\n * Check tag's name\r\n */\r\n if (parent.tagName === tagName) {\r\n /**\r\n * Optional additional check for class-name matching\r\n */\r\n if (className && parent.classList && !parent.classList.contains(className)) {\r\n continue;\r\n }\r\n\r\n /**\r\n * If we have found required tag with class then save the result and go out from cycle\r\n */\r\n parentTag = parent;\r\n break;\r\n }\r\n\r\n /**\r\n * Target tag was not found. Go up to the parent and check it\r\n */\r\n parent = parent.parentNode;\r\n searchDepthIterable--;\r\n }\r\n });\r\n\r\n /**\r\n * Return found tag or null\r\n */\r\n return parentTag;\r\n }\r\n\r\n /**\r\n * Expands selection range to the passed parent node\r\n *\r\n * @param {HTMLElement} node\r\n */\r\n expandToTag(node) {\r\n let selection = window.getSelection();\r\n\r\n selection.removeAllRanges();\r\n let range = document.createRange();\r\n\r\n range.selectNodeContents(node);\r\n selection.addRange(range);\r\n }\r\n}\r\n","/**\r\n * Codex Editor Util\r\n */\r\nexport default class Util {\r\n /**\r\n * Custom logger\r\n *\r\n * @param {string} msg - message\r\n * @param {string} type - logging type 'log'|'warn'|'error'|'info'\r\n * @param {*} args - argument to log with a message\r\n */\r\n static log(msg, type, args) {\r\n type = type || 'log';\r\n\r\n if (!args) {\r\n if (['time', 'timeEnd'].includes(type)) {\r\n msg = `[codex-editor]: ${msg}`;\r\n } else {\r\n args = msg || 'undefined';\r\n msg = '[codex-editor]: %o';\r\n }\r\n } else {\r\n msg = '[codex-editor]: ' + msg;\r\n }\r\n\r\n try{\r\n if ( 'console' in window && window.console[ type ] ) {\r\n if ( args ) window.console[ type ]( msg, args );\r\n else window.console[ type ]( msg );\r\n }\r\n } catch(e) {\r\n // do nothing\r\n }\r\n }\r\n\r\n /**\r\n * Returns basic keycodes as constants\r\n * @return {{}}\r\n */\r\n static get keyCodes() {\r\n return {\r\n BACKSPACE: 8,\r\n TAB: 9,\r\n ENTER: 13,\r\n SHIFT: 16,\r\n CTRL: 17,\r\n ALT: 18,\r\n ESC: 27,\r\n SPACE: 32,\r\n LEFT: 37,\r\n UP: 38,\r\n DOWN: 40,\r\n RIGHT: 39,\r\n DELETE: 46,\r\n META: 91\r\n };\r\n }\r\n\r\n /**\r\n * @typedef {Object} ChainData\r\n * @property {Object} data - data that will be passed to the success or fallback\r\n * @property {Function} function - function's that must be called asynchronically\r\n */\r\n\r\n /**\r\n * Fires a promise sequence asyncronically\r\n *\r\n * @param {Object[]} chains - list or ChainData's\r\n * @param {Function} success - success callback\r\n * @param {Function} fallback - callback that fires in case of errors\r\n *\r\n * @return {Promise}\r\n */\r\n static sequence(chains, success = () => {}, fallback = () => {}) {\r\n return new Promise(function (resolve) {\r\n /**\r\n * pluck each element from queue\r\n * First, send resolved Promise as previous value\r\n * Each plugins \"prepare\" method returns a Promise, that's why\r\n * reduce current element will not be able to continue while can't get\r\n * a resolved Promise\r\n */\r\n chains.reduce(function (previousValue, currentValue, iteration) {\r\n return previousValue\r\n .then(() => waitNextBlock(currentValue, success, fallback))\r\n .then(() => {\r\n // finished\r\n if (iteration === chains.length - 1) {\r\n resolve();\r\n }\r\n });\r\n }, Promise.resolve());\r\n });\r\n\r\n /**\r\n * Decorator\r\n *\r\n * @param {ChainData} chainData\r\n *\r\n * @param {Function} successCallback\r\n * @param {Function} fallbackCallback\r\n *\r\n * @return {Promise}\r\n */\r\n function waitNextBlock(chainData, successCallback, fallbackCallback) {\r\n return new Promise(function (resolve) {\r\n chainData.function()\r\n .then(() => {\r\n successCallback(chainData.data || {});\r\n })\r\n .then(resolve)\r\n .catch(function () {\r\n fallbackCallback(chainData.data || {});\r\n\r\n // anyway, go ahead even it falls\r\n resolve();\r\n });\r\n });\r\n }\r\n }\r\n\r\n /**\r\n * Make array from array-like collection\r\n *\r\n * @param {*} collection\r\n *\r\n * @return {Array}\r\n */\r\n static array(collection) {\r\n return Array.prototype.slice.call(collection);\r\n }\r\n\r\n /**\r\n * Checks if object is empty\r\n *\r\n * @param {Object} object\r\n * @return {boolean}\r\n */\r\n static isEmpty(object) {\r\n return Object.keys(object).length === 0 && object.constructor === Object;\r\n }\r\n\r\n /**\r\n * Check if passed object is a Promise\r\n * @param {*} object - object to check\r\n * @return {Boolean}\r\n */\r\n static isPromise(object) {\r\n return Promise.resolve(object) === object;\r\n }\r\n\r\n /**\r\n * Check if passed element is contenteditable\r\n * @param element\r\n * @return {boolean}\r\n */\r\n static isContentEditable(element) {\r\n return element.contentEditable === 'true';\r\n }\r\n\r\n /**\r\n * Delays method execution\r\n *\r\n * @param method\r\n * @param timeout\r\n */\r\n static delay(method, timeout) {\r\n return function () {\r\n let context = this,\r\n args = arguments;\r\n\r\n window.setTimeout(() => method.apply(context, args), timeout);\r\n };\r\n }\r\n};\r\n","exports = module.exports = require(\"../../node_modules/css-loader/lib/css-base.js\")(false);\n// imports\n\n\n// module\nexports.push([module.id, \":root {\\r\\n /**\\r\\n * Selection color\\r\\n */\\r\\n --selectionColor: rgba(61,166,239,0.63);\\r\\n\\r\\n /**\\r\\n * Toolbar buttons\\r\\n */\\r\\n --bg-light: #eff2f5;\\r\\n\\r\\n /**\\r\\n * All gray texts: placeholders, settings\\r\\n */\\r\\n --grayText: #707684;\\r\\n\\r\\n /** Blue icons */\\r\\n --color-active-icon: #388AE5;\\r\\n\\r\\n /**\\r\\n * Block content width\\r\\n */\\r\\n --content-width: 650px;\\r\\n\\r\\n /**\\r\\n * Toolbar buttons height and width\\r\\n */\\r\\n --toolbar-buttons-size: 34px;\\r\\n\\r\\n /**\\r\\n * Toolbar Plus Button and Toolbox buttons height and width\\r\\n */\\r\\n --toolbox-buttons-size: 20px;\\r\\n\\r\\n /**\\r\\n * Confirm deletion bg\\r\\n */\\r\\n --color-confirm: #E24A4A;\\r\\n}\\r\\n/**\\r\\n* Editor wrapper\\r\\n*/\\r\\n.codex-editor {\\r\\n position: relative;\\r\\n box-sizing: border-box;\\r\\n}\\r\\n.codex-editor .hide {\\r\\n display: none;\\r\\n }\\r\\n.codex-editor__redactor {\\r\\n padding-bottom: 300px;\\r\\n }\\r\\n.codex-editor svg {\\r\\n fill: currentColor;\\r\\n vertical-align: middle;\\r\\n max-height: 100%;\\r\\n }\\r\\n/**\\r\\n * Set color for native selection\\r\\n */\\r\\n::-moz-selection{\\r\\n background-color: rgba(61,166,239,0.63);\\r\\n background-color: var(--selectionColor);\\r\\n}\\r\\n::selection{\\r\\n background-color: rgba(61,166,239,0.63);\\r\\n background-color: var(--selectionColor);\\r\\n}\\r\\n/**\\r\\n * Add placeholder to content editable elements with data attribute\\r\\n * data-placeholder=\\\"Hello world!\\\"\\r\\n */\\r\\n[contentEditable=true][data-placeholder]:empty:not(:focus):before{\\r\\n content: attr(data-placeholder);\\r\\n color: #707684;\\r\\n color: var(--grayText);\\r\\n}\\r\\n.ce-toolbar {\\r\\n position: absolute;\\r\\n left: 0;\\r\\n right: 0;\\r\\n top: 0;\\r\\n /*opacity: 0;*/\\r\\n /*visibility: hidden;*/\\r\\n transition: opacity 100ms ease;\\r\\n will-change: opacity, transform;\\r\\n display: none;\\r\\n}\\r\\n.ce-toolbar--opened {\\r\\n display: block;\\r\\n /*opacity: 1;*/\\r\\n /*visibility: visible;*/\\r\\n }\\r\\n.ce-toolbar__content {\\r\\n max-width: 650px;\\r\\n max-width: var(--content-width);\\r\\n margin: 0 auto;\\r\\n position: relative;\\r\\n }\\r\\n.ce-toolbar__plus {\\r\\n color: #707684;\\r\\n color: var(--grayText);\\r\\n cursor: pointer;\\r\\n display: inline-block;\\r\\n width: 20px;\\r\\n width: var(--toolbox-buttons-size);\\r\\n height: 20px;\\r\\n height: var(--toolbox-buttons-size);\\r\\n /*line-height: var(--toolbox-buttons-size);*/\\r\\n display: -ms-flexbox;\\r\\n display: flex;\\r\\n -ms-flex-pack: center;\\r\\n justify-content: center;\\r\\n -ms-flex-align: center;\\r\\n align-items: center\\r\\n }\\r\\n.ce-toolbar__plus:not(:last-of-type){\\r\\n margin-right: 3px;\\r\\n }\\r\\n.ce-toolbar__plus:hover {\\r\\n color: #388AE5;\\r\\n color: var(--color-active-icon);\\r\\n }\\r\\n.ce-toolbar__plus {\\r\\n\\r\\n position: absolute;\\r\\n left: calc(calc(20px + 10px) * -1);\\r\\n left: calc(calc(var(--toolbox-buttons-size) + 10px) * -1);\\r\\n }\\r\\n.ce-toolbar__plus--hidden {\\r\\n display: none;\\r\\n }\\r\\n/**\\r\\n * Block actions Zone\\r\\n * -------------------------\\r\\n */\\r\\n.ce-toolbar__actions {\\r\\n position: absolute;\\r\\n right: 0;\\r\\n top: 0;\\r\\n padding-right: 16px;\\r\\n }\\r\\n.ce-toolbar__actions-buttons {\\r\\n text-align: right;\\r\\n }\\r\\n.ce-toolbar__settings-btn {\\r\\n display: inline-block;\\r\\n width: 24px;\\r\\n height: 24px;\\r\\n color: #707684;\\r\\n color: var(--grayText);\\r\\n cursor: pointer;\\r\\n }\\r\\n.ce-toolbox {\\r\\n position: absolute;\\r\\n visibility: hidden;\\r\\n transition: opacity 100ms ease;\\r\\n will-change: opacity;\\r\\n}\\r\\n.ce-toolbox--opened {\\r\\n opacity: 1;\\r\\n visibility: visible;\\r\\n }\\r\\n.ce-toolbox__button {\\r\\n color: #707684;\\r\\n color: var(--grayText);\\r\\n cursor: pointer;\\r\\n display: inline-block;\\r\\n width: 20px;\\r\\n width: var(--toolbox-buttons-size);\\r\\n height: 20px;\\r\\n height: var(--toolbox-buttons-size);\\r\\n /*line-height: var(--toolbox-buttons-size);*/\\r\\n display: -ms-flexbox;\\r\\n display: flex;\\r\\n -ms-flex-pack: center;\\r\\n justify-content: center;\\r\\n -ms-flex-align: center;\\r\\n align-items: center;\\r\\n }\\r\\n.ce-toolbox__button:not(:last-of-type){\\r\\n margin-right: 3px;\\r\\n }\\r\\n.ce-toolbox__button:hover {\\r\\n color: #388AE5;\\r\\n color: var(--color-active-icon);\\r\\n }\\r\\n.ce-inline-toolbar {\\r\\n position: absolute;\\r\\n background-color: #FFFFFF;\\r\\n box-shadow: 0 8px 23px -6px rgba(21,40,54,0.31), 22px -14px 34px -18px rgba(33,48,73,0.26);\\r\\n border-radius: 4px;\\r\\n z-index: 2\\r\\n}\\r\\n.ce-inline-toolbar::before {\\r\\n content: '';\\r\\n width: 15px;\\r\\n height: 15px;\\r\\n position: absolute;\\r\\n top: -7px;\\r\\n left: 50%;\\r\\n margin-left: -7px;\\r\\n transform: rotate(-45deg);\\r\\n background-color: #fff;\\r\\n z-index: -1;\\r\\n }\\r\\n.ce-inline-toolbar {\\r\\n padding: 6px;\\r\\n transform: translateX(-50%);\\r\\n display: none;\\r\\n box-shadow: 0 6px 12px -6px rgba(131, 147, 173, 0.46),\\r\\n 5px -12px 34px -13px rgba(97, 105, 134, 0.6),\\r\\n 0 26px 52px 3px rgba(147, 165, 186, 0.24);\\r\\n}\\r\\n.ce-inline-toolbar--showed {\\r\\n display: block;\\r\\n }\\r\\n.ce-inline-tool {\\r\\n display: inline-block;\\r\\n width: 34px;\\r\\n height: 34px;\\r\\n line-height: 34px;\\r\\n text-align: center;\\r\\n border-radius: 3px;\\r\\n cursor: pointer;\\r\\n border: 0;\\r\\n outline: none;\\r\\n background-color: transparent;\\r\\n vertical-align: bottom;\\r\\n color: #707684;\\r\\n color: var(--grayText)\\r\\n}\\r\\n.ce-inline-tool:not(:last-of-type){\\r\\n margin-right: 5px;\\r\\n }\\r\\n.ce-inline-tool:hover {\\r\\n background-color: #eff2f5;\\r\\n background-color: var(--bg-light);\\r\\n }\\r\\n.ce-inline-tool {\\r\\n line-height: normal;\\r\\n}\\r\\n.ce-inline-tool--active {\\r\\n color: #388AE5;\\r\\n color: var(--color-active-icon);\\r\\n }\\r\\n.ce-inline-tool--link .icon {\\r\\n margin-top: -2px;\\r\\n }\\r\\n.ce-inline-tool--link .icon--unlink {\\r\\n display: none;\\r\\n }\\r\\n.ce-inline-tool--unlink .icon--link {\\r\\n display: none;\\r\\n }\\r\\n.ce-inline-tool--unlink .icon--unlink {\\r\\n display: inline-block;\\r\\n }\\r\\n.ce-inline-tool-input {\\r\\n background-color: #eff2f5;\\r\\n background-color: var(--bg-light);\\r\\n outline: none;\\r\\n border: 0;\\r\\n border-radius: 3px;\\r\\n margin: 6px 0 0;\\r\\n font-size: 13px;\\r\\n padding: 8px;\\r\\n width: 100%;\\r\\n box-sizing: border-box;\\r\\n display: none\\r\\n }\\r\\n.ce-inline-tool-input::-webkit-input-placeholder {\\r\\n color: #707684;\\r\\n color: var(--grayText);\\r\\n }\\r\\n.ce-inline-tool-input:-ms-input-placeholder {\\r\\n color: #707684;\\r\\n color: var(--grayText);\\r\\n }\\r\\n.ce-inline-tool-input::placeholder {\\r\\n color: #707684;\\r\\n color: var(--grayText);\\r\\n }\\r\\n.ce-inline-tool-input--showed {\\r\\n display: block;\\r\\n }\\r\\n.ce-settings {\\r\\n position: absolute;\\r\\n background-color: #FFFFFF;\\r\\n box-shadow: 0 8px 23px -6px rgba(21,40,54,0.31), 22px -14px 34px -18px rgba(33,48,73,0.26);\\r\\n border-radius: 4px;\\r\\n z-index: 2\\r\\n}\\r\\n.ce-settings::before {\\r\\n content: '';\\r\\n width: 15px;\\r\\n height: 15px;\\r\\n position: absolute;\\r\\n top: -7px;\\r\\n left: 50%;\\r\\n margin-left: -7px;\\r\\n transform: rotate(-45deg);\\r\\n background-color: #fff;\\r\\n z-index: -1;\\r\\n }\\r\\n.ce-settings {\\r\\n right: 5px;\\r\\n top: 35px;\\r\\n min-width: 124px\\r\\n}\\r\\n.ce-settings::before{\\r\\n left: auto;\\r\\n right: 12px;\\r\\n }\\r\\n.ce-settings {\\r\\n\\r\\n display: none;\\r\\n}\\r\\n.ce-settings--opened {\\r\\n display: block;\\r\\n }\\r\\n.ce-settings__plugin-zone:not(:empty){\\r\\n padding: 6px 6px 0;\\r\\n }\\r\\n.ce-settings__default-zone:not(:empty){\\r\\n padding: 6px;\\r\\n }\\r\\n.ce-settings__button {\\r\\n display: inline-block;\\r\\n width: 34px;\\r\\n height: 34px;\\r\\n line-height: 34px;\\r\\n text-align: center;\\r\\n border-radius: 3px;\\r\\n cursor: pointer;\\r\\n border: 0;\\r\\n outline: none;\\r\\n background-color: transparent;\\r\\n vertical-align: bottom;\\r\\n color: #707684;\\r\\n color: var(--grayText)\\r\\n }\\r\\n.ce-settings__button:not(:last-of-type){\\r\\n margin-right: 5px;\\r\\n }\\r\\n.ce-settings__button:hover {\\r\\n background-color: #eff2f5;\\r\\n background-color: var(--bg-light);\\r\\n }\\r\\n.ce-settings__button--active {\\r\\n color: #388AE5;\\r\\n color: var(--color-active-icon);\\r\\n }\\r\\n.ce-settings__button--disabled {\\r\\n cursor: not-allowed !important;\\r\\n opacity: .3;\\r\\n }\\r\\n.ce-settings__button--selected {\\r\\n color: #388AE5;\\r\\n color: var(--color-active-icon);\\r\\n }\\r\\n.ce-settings__button--delete {\\r\\n transition: background-color 300ms ease;\\r\\n will-change: background-color;\\r\\n }\\r\\n.ce-settings__button--delete .icon {\\r\\n transition: transform 200ms ease-out;\\r\\n will-change: transform;\\r\\n }\\r\\n.ce-settings__button--confirm {\\r\\n background-color: #E24A4A;\\r\\n background-color: var(--color-confirm);\\r\\n color: #fff\\r\\n }\\r\\n.ce-settings__button--confirm:hover {\\r\\n background-color: rgb(213, 74, 74) !important;\\r\\n background-color: rgb(213, 74, 74) !important;\\r\\n }\\r\\n.ce-settings__button--confirm .icon {\\r\\n transform: rotate(90deg);\\r\\n }\\r\\n.ce-block:first-of-type {\\r\\n margin-top: 0;\\r\\n }\\r\\n.ce-block--selected {\\r\\n background-image: linear-gradient(17deg, rgba(243, 248, 255, 0.03) 63.45%, rgba(207, 214, 229, 0.27) 98%);\\r\\n border-radius: 3px;\\r\\n }\\r\\n.ce-block__content {\\r\\n max-width: 650px;\\r\\n max-width: var(--content-width);\\r\\n margin: 0 auto;\\r\\n }\\r\\n.wobble {\\r\\n animation-name: wobble;\\r\\n animation-duration: 400ms;\\r\\n}\\r\\n/**\\r\\n * @author Nick Pettit - https://github.com/nickpettit/glide\\r\\n */\\r\\n@keyframes wobble {\\r\\n from {\\r\\n transform: translate3d(0, 0, 0);\\r\\n }\\r\\n\\r\\n 15% {\\r\\n transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -5deg);\\r\\n }\\r\\n\\r\\n 30% {\\r\\n transform: translate3d(2%, 0, 0) rotate3d(0, 0, 1, 3deg);\\r\\n }\\r\\n\\r\\n 45% {\\r\\n transform: translate3d(-3%, 0, 0) rotate3d(0, 0, 1, -3deg);\\r\\n }\\r\\n\\r\\n 60% {\\r\\n transform: translate3d(2%, 0, 0) rotate3d(0, 0, 1, 2deg);\\r\\n }\\r\\n\\r\\n 75% {\\r\\n transform: translate3d(-1%, 0, 0) rotate3d(0, 0, 1, -1deg);\\r\\n }\\r\\n\\r\\n to {\\r\\n transform: translate3d(0, 0, 0);\\r\\n }\\r\\n}\\r\\n\", \"\"]);\n\n// exports\n"],"sourceRoot":""}
\ No newline at end of file
diff --git a/build/sprite.svg b/build/sprite.svg
new file mode 100644
index 00000000..db2abc22
--- /dev/null
+++ b/build/sprite.svg
@@ -0,0 +1,42 @@
+
+
\ No newline at end of file
diff --git a/codex-editor.js b/codex-editor.js
deleted file mode 100644
index ea31f8af..00000000
--- a/codex-editor.js
+++ /dev/null
@@ -1,3 +0,0 @@
-var codex=codex||{};codex.editor=function(e){function t(o){if(n[o])return n[o].exports;var r=n[o]={exports:{},id:o,loaded:!1};return e[o].call(r.exports,r,r.exports,t),r.loaded=!0,r.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){"use strict";e.exports=function(e){e.version="1.7.8",e.scriptPrefix="cdx-script-";var t=function(){e.core=n(1),e.tools=n(2),e.ui=n(3),e.transport=n(4),e.renderer=n(5),e.saver=n(6),e.content=n(7),e.toolbar=n(8),e.callback=n(12),e.draw=n(13),e.caret=n(14),e.notifications=n(15),e.parser=n(16),e.sanitizer=n(17),e.listeners=n(19),e.destroyer=n(20),e.paste=n(21)};return e.settings={tools:["paragraph","header","picture","list","quote","code","twitter","instagram","smile"],holderId:"codex-editor",initialBlockPlugin:"paragraph"},e.nodes={holder:null,wrapper:null,toolbar:null,inlineToolbar:{wrapper:null,buttons:null,actions:null},toolbox:null,notifications:null,plusButton:null,showSettingsButton:null,showTrashButton:null,blockSettings:null,pluginSettings:null,defaultSettings:null,toolbarButtons:{},redactor:null},e.state={jsonOutput:[],blocks:[],inputs:[]},e.tools={},e.start=function(n){t(),e.core.prepare(n).then(e.ui.prepare).then(e.tools.prepare).then(e.sanitizer.prepare).then(e.paste.prepare).then(e.transport.prepare).then(e.renderer.makeBlocksFromData).then(e.ui.saveInputs).catch(function(t){e.core.log("Initialization failed with error: %o","warn",t)})},e}({})},function(e,t){"use strict";var n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};e.exports=function(e){var t=codex.editor;e.prepare=function(e){return new Promise(function(o,r){e&&(t.settings.tools=e.tools||t.settings.tools),e.data&&(t.state.blocks=e.data),e.initialBlockPlugin&&(t.settings.initialBlockPlugin=e.initialBlockPlugin),e.sanitizer&&(t.settings.sanitizer=e.sanitizer),t.hideToolbar=e.hideToolbar,t.settings.placeholder=e.placeholder||"",t.nodes.holder=document.getElementById(e.holderId||t.settings.holderId),void 0===n(t.nodes.holder)||null===t.nodes.holder?r(Error("Holder wasn't found by ID: #"+e.holderId)):o()})},e.log=function(e,t,n){t=t||"log",n?e="[codex-editor]: "+e:(n=e||"undefined",e="[codex-editor]: %o");try{"console"in window&&window.console[t]&&(n?window.console[t](e,n):window.console[t](e))}catch(e){}},e.insertAfter=function(e,t){e.parentNode.insertBefore(t,e.nextSibling)},e.nodeTypes={TAG:1,TEXT:3,COMMENT:8,DOCUMENT_FRAGMENT:11},e.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},e.isDomNode=function(e){return e&&"object"===("undefined"==typeof e?"undefined":n(e))&&e.nodeType&&e.nodeType==this.nodeTypes.TAG},e.isEmpty=function(e){return 0===Object.keys(e).length},e.ajax=function(e){if(e&&e.url){var t,n,r,i=window.XMLHttpRequest?new XMLHttpRequest:new ActiveXObject("Microsoft.XMLHTTP");if(e.async=!0,e.type=e.type||"GET",e.data=e.data||"",e["content-type"]=e["content-type"]||"application/json; charset=utf-8","GET"==e.type&&e.data)e.url=/\?/.test(e.url)?e.url+"&"+e.data:e.url+"?"+e.data;else{t="";for(r in e.data)t+=r+"="+encodeURIComponent(e.data[r])+"&"}e.withCredentials&&(i.withCredentials=!0);var a=void 0,s=void 0;if("function"!=typeof e.beforeSend||(s=e.beforeSend.call(),s!==!1))return i.open(e.type,e.url,e.async),n=o(e.data),n||("POST"!==e.type?i.setRequestHeader("Content-type",e["content-type"]):i.setRequestHeader("Content-type","application/x-www-form-urlencoded")),i.setRequestHeader("X-Requested-With","XMLHttpRequest"),a=s||i,"function"==typeof e.progress&&(i.upload.onprogress=e.progress.bind(a)),i.onreadystatechange=function(){4===i.readyState&&(200===i.status?"function"==typeof e.success&&e.success.call(a,i.responseText):"function"==typeof e.error&&e.error.call(a,i.responseText,i.status))},n?i.send(e.data):i.send(t),i}},e.importScript=function(e,n){return new Promise(function(o,r){var i=void 0;n?document.getElementById(t.scriptPrefix+n)&&o(e):r("Instance name is missed"),i=document.createElement("SCRIPT"),i.async=!0,i.defer=!0,i.id=t.scriptPrefix+n,i.onload=function(){o(e)},i.onerror=function(){r(e)},i.src=e,document.head.appendChild(i)})};var o=function(e){return e instanceof FormData};return e.isNativeInput=function(e){var t=["INPUT","TEXTAREA"];return t.indexOf(e.tagName)!=-1},e.isBlockEmpty=function(e){var t=["IMG","IFRAME"],n=e.querySelectorAll("textarea, input"),o=!0,r=!e.textContent.trim();return Array.prototype.forEach.call(n,function(e){"textarea"!=e.type&&"text"!=e.type||(o=o&&!e.value.trim())}),r&&o&&!t.includes(e.tagName)},e}({})},function(e,t){"use strict";e.exports=function(){function e(){return new Promise(function(e,o){Promise.resolve().then(function(){var t=[],o=n.tools;for(var r in o){var i=o[r];i.prepare&&"function"!=typeof i.prepare||!i.prepare||t.push(i)}return t.length||e(),t}).then(t).then(function(){n.core.log("Plugins loaded","info"),e()}).catch(function(e){o(e)})})}function t(e){return new Promise(function(t){e.reduce(function(r,i,a){return r.then(function(){return new Promise(function(r){o(i).then(r).then(function(){i.available=!0}).catch(function(e){n.core.log("Plugin «"+i.type+"» was not loaded. Preparation failed because %o","warn",e),i.available=!1,i.loadingMessage=e,r()}).then(function(){a==e.length-1&&t()})})})},Promise.resolve())})}var n=codex.editor,o=function(e){return e.prepare(e.config||{})};return{prepare:e}}()},function(e,t){"use strict";e.exports=function(e){var t=codex.editor;e.className={BLOCK_CLASSNAME:"ce-block",BLOCK_CONTENT:"ce-block__content",BLOCK_STRETCHED:"ce-block--stretched",BLOCK_HIGHLIGHTED:"ce-block--focused",SETTINGS_ITEM:"ce-settings__item"},e.prepare=function(){return new Promise(function(e){var n=t.draw.wrapper(),r=t.draw.redactor(),i=o();n.appendChild(i),n.appendChild(r),t.nodes.wrapper=n,t.nodes.redactor=r,t.nodes.holder.appendChild(n),e()}).then(s).then(n).then(l).then(a).then(c).catch(function(){t.core.log("Can't draw editor interface")})};var n=function(){var e=t.draw.inlineToolbar();t.nodes.inlineToolbar.wrapper=e,t.nodes.inlineToolbar.buttons=t.draw.inlineToolbarButtons(),t.nodes.inlineToolbar.actions=t.draw.inlineToolbarActions(),t.nodes.inlineToolbar.wrapper.appendChild(t.nodes.inlineToolbar.buttons),t.nodes.inlineToolbar.wrapper.appendChild(t.nodes.inlineToolbar.actions),t.nodes.wrapper.appendChild(t.nodes.inlineToolbar.wrapper)},o=function(){var e=t.draw.toolbar(),n=i(),o=r();return e.appendChild(n),e.appendChild(o),t.nodes.toolbar=e,e},r=function(){var e=t.draw.toolbarContent(),n=t.draw.toolbox(),o=t.draw.plusButton();return e.appendChild(o),e.appendChild(n),t.nodes.toolbox=n,t.nodes.plusButton=o,e},i=function(){var e=t.draw.blockSettings(),n=t.draw.blockButtons(),o=t.draw.defaultSettings(),r=t.draw.settingsButton(),i=t.toolbar.settings.makeRemoveBlockButton(),a=t.draw.pluginsSettings();return e.appendChild(a),e.appendChild(o),n.appendChild(r),n.appendChild(i),n.appendChild(e),t.nodes.blockSettings=e,t.nodes.pluginSettings=a,t.nodes.defaultSettings=o,t.nodes.showSettingsButton=r,t.nodes.showTrashButton=i,n},a=function(){t.nodes.notifications=t.notifications.createHolder()},s=function(){var e,n,o;for(n in t.settings.tools)e=t.settings.tools[n],t.tools[n]=e,e.iconClassname||!e.displayInToolbox?"function"==typeof e.render?e.displayInToolbox&&(o=t.draw.toolbarButton(n,e.iconClassname),t.nodes.toolbox.appendChild(o),t.nodes.toolbarButtons[n]=o):t.core.log("render method missed. Tool %o skipped","warn",n):t.core.log("Toolbar icon classname missed. Tool %o skipped","warn",n)},l=function(){var e,n,o={bold:{icon:"ce-icon-bold",command:"bold"},italic:{icon:"ce-icon-italic",command:"italic"},link:{icon:"ce-icon-link",command:"createLink"}};for(var r in o)n=o[r],e=t.draw.toolbarButtonInline(r,n.icon),t.nodes.inlineToolbar.buttons.appendChild(e),t.ui.setInlineToolbarButtonBehaviour(e,n.command)},c=function(){t.core.log("ui.bindEvents fired","info"),t.listeners.add(document,"keydown",t.callback.globalKeydown,!1),t.listeners.add(t.nodes.redactor,"keydown",t.callback.redactorKeyDown,!1),t.listeners.add(document,"keyup",t.callback.globalKeyup,!1),t.listeners.add(t.nodes.redactor,"click",t.callback.redactorClicked,!1),t.listeners.add(t.nodes.plusButton,"click",t.callback.plusButtonClicked,!1),t.listeners.add(t.nodes.showSettingsButton,"click",t.callback.showSettingsButtonClicked,!1);for(var e in t.nodes.toolbarButtons)t.listeners.add(t.nodes.toolbarButtons[e],"click",t.callback.toolbarButtonClicked,!1)};return e.addBlockHandlers=function(e){e&&(t.listeners.add(e,"keydown",t.callback.blockKeydown,!1),t.listeners.add(e,"paste",t.paste.blockPasteCallback,!1),t.listeners.add(e,"mouseup",t.toolbar.inline.show,!1),t.listeners.add(e,"keyup",t.toolbar.inline.show,!1))},e.saveInputs=function(){var e=t.nodes.redactor;t.state.inputs=[];var n=e.querySelectorAll("[contenteditable], input, textarea");Array.prototype.map.call(n,function(e){e.type&&"text"!=e.type&&"textarea"!=e.type||t.state.inputs.push(e)})},e.addInitialBlock=function(){var e,n=t.settings.initialBlockPlugin;return t.tools[n]?(e=t.tools[n].render(),e.setAttribute("data-placeholder",t.settings.placeholder),t.content.insertBlock({type:n,block:e}),void t.content.workingNodeChanged(e)):void t.core.log("Plugin %o was not implemented and can't be used as initial block","warn",n)},e.setInlineToolbarButtonBehaviour=function(e,n){t.listeners.add(e,"mousedown",function(e){t.toolbar.inline.toolClicked(e,n)},!1)},e}({})},function(e,t){"use strict";e.exports=function(e){var t=codex.editor,n=null;return e.input=null,e.arguments=null,e.prepare=function(){var e=t.draw.node("INPUT","",{type:"file"});t.listeners.add(e,"change",t.transport.fileSelected),t.transport.input=e},e.clearInput=function(){e.input=null,e.prepare()},e.fileSelected=function(){var o,r=this,i=r.files,a=new FormData;if(t.transport.arguments.multiple===!0)for(o=0;o"+e.split("\n\n").join("
")+"
":""};return e.getEditableParent=function(e){for(;e&&"true"!=e.contentEditable;)e=e.parentNode;return e},e.clear=function(e){t.nodes.redactor.innerHTML="",t.content.sync(),t.ui.saveInputs(),e?t.state.blocks={}:t.state.blocks&&(t.state.blocks.items=[]),t.content.currentNode=null},e.load=function(e){var n=Object.assign({},t.state.blocks);t.content.clear(),Object.keys(n).length?n.items?(n.items=n.items.concat(e.items),t.state.blocks=n):(n.items=e.items,t.state.blocks=n):t.state.blocks=e,t.renderer.makeBlocksFromData()},e}({})},function(e,t,n){"use strict";e.exports=function(e){var t=codex.editor;return e.settings=n(9),e.inline=n(10),e.toolbox=n(11),e.defaultToolbarHeight=49,e.defaultOffset=34,e.opened=!1,e.current=null,e.open=function(){if(!t.hideToolbar){var e=t.content.currentNode.dataset.tool;t.tools[e]&&t.tools[e].makeSettings?t.nodes.showSettingsButton.classList.remove("hide"):t.nodes.showSettingsButton.classList.add("hide"),t.nodes.toolbar.classList.add("opened"),this.opened=!0}},e.close=function(){t.nodes.toolbar.classList.remove("opened"),e.opened=!1,e.current=null;for(var n in t.nodes.toolbarButtons)t.nodes.toolbarButtons[n].classList.remove("selected");t.toolbar.toolbox.close(),t.toolbar.settings.close()},e.toggle=function(){this.opened?this.close():this.open()},e.hidePlusButton=function(){t.nodes.plusButton.classList.add("hide")},e.showPlusButton=function(){t.nodes.plusButton.classList.remove("hide")},e.move=function(){if(t.toolbar.toolbox.close(),t.content.currentNode){var e=t.content.currentNode.offsetTop-t.toolbar.defaultToolbarHeight/2+t.toolbar.defaultOffset;t.nodes.toolbar.style.transform="translate3D(0, "+Math.floor(e)+"px, 0)",t.toolbar.settings.hideRemoveActions()}},e}({})},function(e,t){"use strict";e.exports=function(e){var t=codex.editor;return e.opened=!1,e.setting=null,e.actions=null,e.open=function(e){if(t.tools[e]&&t.tools[e].makeSettings){var n=t.tools[e].makeSettings();t.nodes.pluginSettings.appendChild(n),t.nodes.blockSettings.classList.add("opened"),this.opened=!0}},e.close=function(){t.nodes.blockSettings.classList.remove("opened"),t.nodes.pluginSettings.innerHTML="",this.opened=!1},e.toggle=function(e){this.opened?this.close():this.open(e)},e.makeRemoveBlockButton=function(){var e=t.draw.node("SPAN","ce-toolbar__remove-btn",{}),n=t.draw.node("SPAN","ce-toolbar__remove-setting",{innerHTML:''}),o=t.draw.node("DIV","ce-toolbar__remove-confirmation",{}),r=t.draw.node("DIV","ce-toolbar__remove-confirm",{textContent:"Удалить блок"}),i=t.draw.node("DIV","ce-toolbar__remove-cancel",{textContent:"Отмена"});return t.listeners.add(n,"click",t.toolbar.settings.removeButtonClicked,!1),t.listeners.add(r,"click",t.toolbar.settings.confirmRemovingRequest,!1),t.listeners.add(i,"click",t.toolbar.settings.cancelRemovingRequest,!1),o.appendChild(r),o.appendChild(i),e.appendChild(n),e.appendChild(o),t.toolbar.settings.setting=n,t.toolbar.settings.actions=o,e},e.removeButtonClicked=function(){var e=t.toolbar.settings.actions;e.classList.contains("opened")?t.toolbar.settings.hideRemoveActions():t.toolbar.settings.showRemoveActions(),t.toolbar.toolbox.close(),t.toolbar.settings.close()},e.cancelRemovingRequest=function(){t.toolbar.settings.actions.classList.remove("opened")},e.confirmRemovingRequest=function(){var e,n=t.content.currentNode;n.remove(),e=t.nodes.redactor.childNodes.length,0===e&&(t.content.currentNode=null,t.ui.addInitialBlock()),t.ui.saveInputs(),t.toolbar.close()},e.showRemoveActions=function(){t.toolbar.settings.actions.classList.add("opened")},e.hideRemoveActions=function(){t.toolbar.settings.actions.classList.remove("opened")},e}({})},function(e,t){"use strict";e.exports=function(e){var t=codex.editor;e.buttonsOpened=null,e.actionsOpened=null,e.wrappersOffset=null,e.storedSelection=null,e.show=function(){var n,o=t.content.currentNode,r=o.dataset.tool;if(n=t.tools[r],n.showInlineToolbar){var i=e.getSelectionText(),a=t.nodes.inlineToolbar.wrapper;i.length>0&&(t.toolbar.inline.move(),a.classList.add("opened"),t.toolbar.inline.showButtons())}},e.close=function(){var e=t.nodes.inlineToolbar.wrapper;e.classList.remove("opened")},e.move=function(){this.wrappersOffset||(this.wrappersOffset=this.getWrappersOffset());var e,n,o=this.getSelectionCoords(),r=0,i=t.nodes.inlineToolbar.wrapper;0===i.offsetHeight&&(r=40),e=o.x-this.wrappersOffset.left,n=o.y+window.scrollY-this.wrappersOffset.top-r-i.offsetHeight,i.style.transform="translate3D("+Math.floor(e)+"px, "+Math.floor(n)+"px, 0)",t.toolbar.inline.closeButtons(),t.toolbar.inline.closeAction()},e.toolClicked=function(e,n){switch(n){case"createLink":t.toolbar.inline.createLinkAction(e,n);break;default:t.toolbar.inline.defaultToolAction(n)}t.nodes.inlineToolbar.buttons.childNodes.forEach(t.toolbar.inline.hightlight)},e.getWrappersOffset=function(){var e=t.nodes.wrapper,n=this.getOffset(e);return this.wrappersOffset=n,n},e.getOffset=function(e){for(var t=0,n=0;e&&!isNaN(e.offsetLeft)&&!isNaN(e.offsetTop);)t+=e.offsetLeft+e.clientLeft,n+=e.offsetTop+e.clientTop,e=e.offsetParent;return{top:n,left:t}},e.getSelectionCoords=function(){var e,t=document.selection,n=0,o=0;if(t)"Control"!=t.type&&(e=t.createRange(),e.collapse(!0),n=e.boundingLeft,o=e.boundingTop);else if(window.getSelection&&(t=window.getSelection(),t.rangeCount&&(e=t.getRangeAt(0).cloneRange(),e.getClientRects))){e.collapse(!0);var r=e.getClientRects()[0];if(!r)return;n=r.left,o=r.top}return{x:n,y:o}},e.getSelectionText=function(){var e="";return window.getSelection&&(e=window.getSelection().toString()),e},e.showButtons=function(){var e=t.nodes.inlineToolbar.buttons;e.classList.add("opened"),t.toolbar.inline.buttonsOpened=!0,t.nodes.inlineToolbar.buttons.childNodes.forEach(t.toolbar.inline.hightlight)},e.closeButtons=function(){var e=t.nodes.inlineToolbar.buttons;e.classList.remove("opened"),t.toolbar.inline.buttonsOpened=!1},e.showActions=function(){var e=t.nodes.inlineToolbar.actions;e.classList.add("opened"),t.toolbar.inline.actionsOpened=!0},e.closeAction=function(){var e=t.nodes.inlineToolbar.actions;e.innerHTML="",e.classList.remove("opened"),t.toolbar.inline.actionsOpened=!1};var n=function(e){if(e.keyCode==t.core.keys.ENTER){var n=t.content.currentNode,o=t.toolbar.inline.storedSelection;t.toolbar.inline.restoreSelection(n,o),t.toolbar.inline.setAnchor(this.value),e.preventDefault(),e.stopImmediatePropagation(),t.toolbar.inline.clearRange()}};return e.createLinkAction=function(e){var o=this.isLinkActive(),r=t.content.currentNode,i=t.toolbar.inline.saveSelection(r);if(t.toolbar.inline.storedSelection=i,o)t.toolbar.inline.restoreSelection(r,i),t.toolbar.inline.defaultToolAction("unlink");else{var a=t.draw.inputForLink();t.nodes.inlineToolbar.actions.appendChild(a),t.toolbar.inline.closeButtons(),t.toolbar.inline.showActions(),a.focus(),e.preventDefault(),t.listeners.add(a,"keydown",n,!1)}},e.isLinkActive=function(){var e=!1;return t.nodes.inlineToolbar.buttons.childNodes.forEach(function(t){var n=t.dataset.type;"link"==n&&t.classList.contains("hightlighted")&&(e=!0)}),e},e.defaultToolAction=function(e){document.execCommand(e,!1,null)},e.setAnchor=function(e){document.execCommand("createLink",!1,e),t.toolbar.inline.closeAction()},e.saveSelection=function(e){var t,n=window.getSelection().getRangeAt(0),o=n.cloneRange();return o.selectNodeContents(e),o.setEnd(n.startContainer,n.startOffset),t=o.toString().length,{start:t,end:t+n.toString().length}},e.restoreSelection=function(e,t){var n=document.createRange(),o=0;n.setStart(e,0),n.collapse(!0);for(var r,i,a=[e],s=!1,l=!1;!l&&(r=a.pop());)if(3==r.nodeType)i=o+r.length,!s&&t.start>=o&&t.start<=i&&(n.setStart(r,t.start-o),s=!0),s&&t.end>=o&&t.end<=i&&(n.setEnd(r,t.end-o),l=!0),o=i;else for(var c=r.childNodes.length;c--;)a.push(r.childNodes[c]);var d=window.getSelection();d.removeAllRanges(),d.addRange(n)},e.clearRange=function(){var e=window.getSelection();e.removeAllRanges()},e.hightlight=function(e){var n=e.dataset.type;document.queryCommandState(n)?t.toolbar.inline.setButtonHighlighted(e):t.toolbar.inline.removeButtonsHighLight(e);var o=window.getSelection(),r=o.anchorNode.parentNode;"A"==r.tagName&&"link"==n&&t.toolbar.inline.setButtonHighlighted(e)},e.setButtonHighlighted=function(e){if(e.classList.add("hightlighted"),"link"==e.dataset.type){var t=e.childNodes[0];t.classList.remove("ce-icon-link"),t.classList.add("ce-icon-unlink")}},e.removeButtonsHighLight=function(e){if(e.classList.remove("hightlighted"),"link"==e.dataset.type){var t=e.childNodes[0];t.classList.remove("ce-icon-unlink"),t.classList.add("ce-icon-link")}},e}({})},function(e,t){"use strict";e.exports=function(e){var t=codex.editor;return e.opened=!1,e.openedOnBlock=null,e.open=function(){t.toolbar.settings.opened&&t.toolbar.settings.close(),e.openedOnBlock=t.content.currentNode,e.openedOnBlock.classList.add("toolbar-opened"),t.nodes.toolbox.classList.add("opened"),t.nodes.plusButton.classList.add("clicked"),t.toolbar.toolbox.opened=!0},e.close=function(){e.openedOnBlock&&e.openedOnBlock.classList.remove("toolbar-opened"),e.openedOnBlock=null,t.nodes.toolbox.classList.remove("opened"),t.nodes.plusButton.classList.remove("clicked"),t.toolbar.toolbox.opened=!1,t.toolbar.current=null},e.leaf=function(){var e=t.toolbar.current,n=Object.keys(t.tools),o=t.nodes.toolbarButtons,r=0,i=void 0,a=void 0,s=void 0;if(e)for(r=(n.indexOf(e)+1)%n.length,a=n[r];!t.tools[a].displayInToolbox;)r=(r+1)%n.length,a=n[r];else for(s in t.tools){if(t.tools[s].displayInToolbox)break;r++}i=n[r];for(var l in o)o[l].classList.remove("selected");o[i].classList.add("selected"),t.toolbar.current=i},e.toolClicked=function(e){var n,o,r,i=["image","link","list","instagram","twitter","embed"],a=t.tools[t.toolbar.current],s=t.content.currentNode,l=t.caret.inputIndex;n=a.render(),r={block:n,type:a.type,stretched:!1},s&&i.indexOf(s.dataset.tool)===-1&&""===s.textContent.trim()?t.content.switchBlock(s,n,a.type):(t.content.insertBlock(r),l++),o=a.appendCallback,o&&"function"==typeof o&&o.call(e),window.setTimeout(function(){t.caret.setToBlock(l)},10),t.content.workingNodeChanged(),t.toolbar.move()},e}({})},function(e,t){"use strict";e.exports=function(e){var t=codex.editor;e.globalKeydown=function(e){switch(e.keyCode){case t.core.keys.ENTER:o(e)}},e.redactorKeyDown=function(e){switch(e.keyCode){case t.core.keys.TAB:n(e);break;case t.core.keys.ENTER:i(e);break;case t.core.keys.ESC:a(e);break;default:l(e)}},e.globalKeyup=function(e){switch(e.keyCode){case t.core.keys.UP:case t.core.keys.LEFT:case t.core.keys.RIGHT:case t.core.keys.DOWN:s(e)}};var n=function(e){e.preventDefault(),t.core.isBlockEmpty(t.content.currentNode)&&(t.toolbar.opened||t.toolbar.open(),t.toolbar.opened&&!t.toolbar.toolbox.opened?t.toolbar.toolbox.open():t.toolbar.toolbox.leaf())},o=function(){t.content.editorAreaHightlighted&&(t.caret.inputIndex=-1,r())},r=function(){var e=t.settings.initialBlockPlugin;t.content.insertBlock({type:e,block:t.tools[e].render()},!0),t.toolbar.move(),t.toolbar.open()},i=function(e){"true"==e.target.contentEditable&&t.caret.saveCurrentInputIndex();var n=t.caret.getCurrentInputIndex()||0,o=t.content.currentNode,r=o.dataset.tool,i=t.toolbar.opened&&t.toolbar.current&&e.target==t.state.inputs[n],a=t.tools[r].enableLineBreaks,s=t.settings.initialBlockPlugin;if(i)return e.preventDefault(),t.toolbar.toolbox.toolClicked(e),t.toolbar.close(),e.stopPropagation(),void e.stopImmediatePropagation();if(e.shiftKey||a)return e.stopPropagation(),void e.stopImmediatePropagation();var l=window.getSelection(),c=l.anchorNode,d=t.caret.position.atTheEnd(),u=!1;if(e.shiftKey&&!a)return t.callback.enterPressedOnBlock(t.content.currentBlock,e),void e.preventDefault();if(u=c&&"true"!=c.parentNode.contentEditable,c.nodeType!=t.core.nodeTypes.TEXT||u||d){var p=t.content.isLastNode(c);p&&d&&(e.preventDefault(),e.stopPropagation(),e.stopImmediatePropagation(),t.core.log("ENTER clicked in last textNode. Create new BLOCK"),t.content.insertBlock({type:s,block:t.tools[s].render()},!0),t.toolbar.move(),t.toolbar.open(),t.toolbar.showPlusButton())}else e.preventDefault(),t.core.log("Splitting Text node..."),t.content.splitBlock(n),t.state.inputs[n+1].textContent.trim()||t.toolbar.showPlusButton();t.ui.saveInputs()},a=function(e){t.toolbar.close(),t.toolbar.toolbox.close(),e.preventDefault()},s=function(e){t.content.workingNodeChanged(),t.toolbar.close(),t.toolbar.move()},l=function(){t.toolbar.close(),t.toolbar.inline.actionsOpened||(t.toolbar.inline.close(),t.content.clearMark())};e.redactorClicked=function(e){c(),t.content.workingNodeChanged(e.target),t.ui.saveInputs();var n,o=t.toolbar.inline.getSelectionText();if(0===o.length&&t.toolbar.inline.close(),"true"==e.target.contentEditable&&t.caret.saveCurrentInputIndex(),null===t.content.currentNode){var r=t.state.inputs.length>0?t.state.inputs.length-1:0;if(t.state.inputs.length&&(n=t.content.getFirstLevelBlock(t.state.inputs[r])),t.state.inputs.length&&""===t.state.inputs[r].textContent&&n.dataset.tool==t.settings.initialBlockPlugin)t.caret.setToBlock(r);else{var i=t.settings.initialBlockPlugin;t.content.insertBlock({type:i,block:t.tools[i].render()}),1===t.state.inputs.length?t.caret.setToBlock(r):t.caret.setToNextBlock(r)}}else t.toolbar.settings.close(),t.toolbar.toolbox.close();t.toolbar.move(),t.toolbar.open();var a=!t.content.currentNode.textContent.trim(),s=t.content.currentNode.dataset.tool,l=s==t.settings.initialBlockPlugin;t.toolbar.hidePlusButton(),a||t.content.markBlock(),l&&a&&t.toolbar.showPlusButton()};var c=function(){var e=window.getSelection(),n=e.anchorNode,o=!1;if(0===e.rangeCount)t.content.editorAreaHightlighted=!0;else{for(t.core.isDomNode(n)||(n=n.parentNode),"true"==n.contentEditable&&(o=!0);"true"!=n.contentEditable&&(n=n.parentNode,"true"==n.contentEditable&&(o=!0),n!=document.body););t.content.editorAreaHightlighted=!o}};e.toolbarButtonClicked=function(e){var n=this;t.toolbar.current=n.dataset.type,t.toolbar.toolbox.toolClicked(e),t.toolbar.close()},e.plusButtonClicked=function(){t.nodes.toolbox.classList.contains("opened")?t.toolbar.toolbox.close():t.toolbar.toolbox.open()},e.blockKeydown=function(e){var n=e.target;switch(e.keyCode){case t.core.keys.DOWN:case t.core.keys.RIGHT:d(e);break;case t.core.keys.BACKSPACE:p(n,e);break;case t.core.keys.UP:case t.core.keys.LEFT:u(e)}};var d=function(e){var n,o=window.getSelection(),r=t.state.inputs,i=o.anchorNode;if(!i)return!1;for(;"true"!=i.contentEditable;)n=i.parentNode,i=n;for(var a=0;i!=r[a];)a++;if(!i.textContent)return void t.caret.setToNextBlock(a);var s,l,c=!1,d=!1;return s=i.childNodes[i.childNodes.length-1],l=t.core.isDomNode(s)?t.content.getDeepestTextNodeFromPosition(s,s.childNodes.length):s,c=o.anchorNode==l,d=l.length==o.anchorOffset,
-c&&d?void t.caret.setToNextBlock(a):(t.core.log("arrow [down|right] : caret does not reached the end"),!1)},u=function(e){var n,o=window.getSelection(),r=t.state.inputs,i=o.anchorNode;if(!i)return!1;if(0!==o.anchorOffset)return!1;for(;"true"!=i.contentEditable;)n=i.parentNode,i=n;for(var a=0;i!=r[a];)a++;var s,l,c=!1,d=!1;return i.textContent?(s=i.childNodes[0],l=t.core.isDomNode(s)?t.content.getDeepestTextNodeFromPosition(s,0):s,c=o.anchorNode==l,d=0===o.anchorOffset,void(c&&d&&t.caret.setToPreviousBlock(a))):void t.caret.setToPreviousBlock(a)},p=function(e,n){var o,r,i,a=t.caret.getCurrentInputIndex();if(t.core.isNativeInput(n.target)){if(""!=n.target.value.trim())return;e.remove()}if(e.textContent.trim()){if(o=t.content.getRange(),r=o.endOffset-o.startOffset,!t.caret.position.atStart()||r||!t.state.inputs[a-1])return;t.content.mergeBlocks(a)}r||e.remove(),i=t.nodes.redactor.childNodes.length,0===i?(t.content.currentNode=null,t.ui.addInitialBlock(),t.ui.saveInputs(),window.setTimeout(function(){t.caret.setToPreviousBlock(1)},10)):0!==t.caret.inputIndex?t.caret.setToPreviousBlock(t.caret.inputIndex):t.caret.setToNextBlock(t.caret.inputIndex),t.toolbar.move(),t.toolbar.opened||t.toolbar.open(),t.ui.saveInputs(),n.preventDefault()};return e.showSettingsButtonClicked=function(e){var n=t.content.currentNode.dataset.tool;t.toolbar.settings.toggle(n),t.toolbar.toolbox.close(),t.toolbar.settings.hideRemoveActions()},e}({})},function(e,t){"use strict";e.exports=function(e){return e.wrapper=function(){var e=document.createElement("div");return e.className+="codex-editor",e},e.redactor=function(){var e=document.createElement("div");return e.className+="ce-redactor",e},e.ceBlock=function(){var e=document.createElement("DIV");return e.className+="ce_block",e},e.toolbar=function(){var e=document.createElement("div");return e.className+="ce-toolbar",e},e.toolbarContent=function(){var e=document.createElement("DIV");return e.classList.add("ce-toolbar__content"),e},e.inlineToolbar=function(){var e=document.createElement("DIV");return e.className+="ce-toolbar-inline",e},e.inlineToolbarButtons=function(){var e=document.createElement("DIV");return e.className+="ce-toolbar-inline__buttons",e},e.inlineToolbarActions=function(){var e=document.createElement("DIV");return e.className+="ce-toolbar-inline__actions",e},e.inputForLink=function(){var e=document.createElement("INPUT");return e.type="input",e.className+="inputForLink",e.placeholder="Вставьте ссылку ...",e.setAttribute("form","defaultForm"),e.setAttribute("autofocus","autofocus"),e},e.blockButtons=function(){var e=document.createElement("div");return e.className+="ce-toolbar__actions",e},e.blockSettings=function(){var e=document.createElement("div");return e.className+="ce-settings",e},e.defaultSettings=function(){var e=document.createElement("div");return e.classList.add("ce-settings_default"),e},e.pluginsSettings=function(){var e=document.createElement("div");return e.classList.add("ce-settings_plugin"),e},e.plusButton=function(){var e=document.createElement("span");return e.className="ce-toolbar__plus",e},e.settingsButton=function(){var e=document.createElement("span");return e.className="ce-toolbar__settings-btn",e.innerHTML='',e},e.toolbox=function(){var e=document.createElement("div");return e.className="ce-toolbar__tools",e},e.toolbarButton=function(e,t){var n=document.createElement("li"),o=document.createElement("i"),r=document.createElement("span");return n.dataset.type=e,n.setAttribute("title",e),o.classList.add(t),r.classList.add("ce_toolbar_tools--title"),n.appendChild(o),n.appendChild(r),n},e.toolbarButtonInline=function(e,t){var n=document.createElement("BUTTON"),o=document.createElement("I");return n.type="button",n.dataset.type=e,o.classList.add(t),n.appendChild(o),n},e.block=function(e,t){var n=document.createElement(e);return n.innerHTML=t||"",n},e.node=function(e,t,n){var o=document.createElement(e);if(t&&(o.className=t),n)for(var r in n)o[r]=n[r];return o},e.unavailableBlock=function(){var e=document.createElement("DIV");return e.classList.add("cdx-unavailable-block"),e},e}({})},function(e,t){"use strict";e.exports=function(e){var t=codex.editor;return e.inputIndex=null,e.offset=null,e.focusedNodeIndex=null,e.set=function(n,o,r){r=r||e.offset||0,o=o||e.focusedNodeIndex||0;var i,a=n.childNodes;if(i=0===a.length?n:a[o],"true"!=n.contentEditable)return void n.focus();t.core.isDomNode(i)&&(i=t.content.getDeepestTextNodeFromPosition(i,i.childNodes.length));var s=document.createRange(),l=window.getSelection();window.setTimeout(function(){s.setStart(i,r),s.setEnd(i,r),l.removeAllRanges(),l.addRange(s),t.caret.saveCurrentInputIndex()},20)},e.saveCurrentInputIndex=function(){var n,o=window.getSelection(),r=t.state.inputs,i=o.anchorNode;if(i){for(;"true"!=i.contentEditable;)n=i.parentNode,i=n;for(var a=0;i!=r[a];)a++;e.inputIndex=a}},e.getCurrentInputIndex=function(){return e.inputIndex},e.setToNextBlock=function(e){var n=t.state.inputs,o=n[e+1];if(!o)return void t.core.log("We are reached the end");if(!o.childNodes.length){var r=document.createTextNode("");o.appendChild(r)}t.caret.inputIndex=e+1,t.caret.set(o,0,0),t.content.workingNodeChanged(o)},e.setToBlock=function(e){var n=t.state.inputs,o=n[e];if(o){if(!o.childNodes.length){var r=document.createTextNode("");o.appendChild(r)}t.caret.inputIndex=e,t.caret.set(o,0,0),t.content.workingNodeChanged(o)}},e.setToPreviousBlock=function(e){e=e||0;var n,o,r,i=t.state.inputs,a=i[e-1];return a?(n=t.content.getDeepestTextNodeFromPosition(a,a.childNodes.length),o=n.length,a.childNodes.length||(r=document.createTextNode(""),a.appendChild(r)),t.caret.inputIndex=e-1,t.caret.set(a,a.childNodes.length-1,o),void t.content.workingNodeChanged(i[e-1])):void t.core.log("We are reached first node")},e.position={atStart:function(){var e=window.getSelection(),n=e.anchorOffset,o=e.anchorNode,r=t.content.getFirstLevelBlock(o),i=r.childNodes[0];t.core.isDomNode(o)||(o=o.parentNode);var a=o===i.childNodes[0],s=0===n;return a&&s},atTheEnd:function(){var e=window.getSelection(),t=e.anchorOffset,n=e.anchorNode;return!n||!n.length||t===n.length}},e.insertNode=function(e){var n,o,r=e;e.nodeType==t.core.nodeTypes.DOCUMENT_FRAGMENT&&(r=e.lastChild),n=window.getSelection(),o=n.getRangeAt(0),o.deleteContents(),o.insertNode(e),o.setStartAfter(r),o.collapse(!0),n.removeAllRanges(),n.addRange(o)},e}({})},function(e,t){"use strict";e.exports=function(e){var t=codex.editor,n=[],o=function(e){n.push(e);for(var t=0;t5;)"confirm"!=n[t].type&&"prompt"!=n[t].type?(n[t].close(),n.splice(t,1)):t++};return e.createHolder=function(){var e=t.draw.node("DIV","cdx-notifications-block");return t.nodes.notifications=document.body.appendChild(e),e},e.errorThrown=function(e,n){t.notifications.notification({message:"This action is not available currently",type:n.type})},e.notification=function(e){function n(e){if(!e||!e.message)return void t.core.log("Can't create notification. Message is missed");e.type=e.type||"alert",e.time=1e3*e.time||1e4;var n=t.draw.node("DIV","cdx-notification"),o=t.draw.node("DIV","cdx-notification__message"),r=t.draw.node("INPUT","cdx-notification__input"),f=t.draw.node("SPAN","cdx-notification__ok-btn"),g=t.draw.node("SPAN","cdx-notification__cancel-btn");o.textContent=e.message,f.textContent=e.okMsg||"ОК",g.textContent=e.cancelMsg||"Отмена",t.listeners.add(f,"click",u),t.listeners.add(g,"click",p),n.appendChild(o),"prompt"==e.type&&n.appendChild(r),n.appendChild(f),"prompt"!=e.type&&"confirm"!=e.type||n.appendChild(g),n.classList.add("cdx-notification-"+e.type),n.dataset.type=e.type,a=n,l=e.type,c=e.confirm,s=e.cancel,d=r,"prompt"!=e.type&&"confirm"!=e.type&&window.setTimeout(i,e.time)}function r(){t.nodes.notifications.appendChild(a),d.focus(),t.nodes.notifications.classList.add("cdx-notification__notification-appending"),window.setTimeout(function(){t.nodes.notifications.classList.remove("cdx-notification__notification-appending")},100),o({type:l,close:i})}function i(){a.remove()}var a=null,s=null,l=null,c=null,d=null,u=function(){if(i(),"function"==typeof c)return"prompt"==l?void c(d.value):void c()},p=function(){i(),"function"==typeof s&&s()};return e&&(n(e),r()),{create:n,send:r,close:i}},e.clear=function(){t.nodes.notifications.innerHTML="",n=[]},e}({})},function(e,t){"use strict";e.exports=function(e){var t=codex.editor;return e.insertPastedContent=function(e,n){t.content.insertBlock({type:e.type,block:e.render({text:n.innerHTML})})},e.isFirstLevelBlock=function(e){return e.nodeType==t.core.nodeTypes.TAG&&e.classList.contains(t.ui.className.BLOCK_CLASSNAME)},e}({})},function(e,t,n){"use strict";e.exports=function(e){var t=n(18),o=codex.editor;e.prepare=function(){o.settings.sanitizer&&!o.core.isEmpty(o.settings.sanitizer)&&(r.CUSTOM=o.settings.sanitizer)};var r={CUSTOM:null,BASIC:{tags:{p:{},a:{href:!0,target:"_blank",rel:"nofollow"}}}};e.Config=r;var i=function(e){var n=e||r.CUSTOM||r.BASIC;return new t(n)};return e.clean=function(e,t){var n=i(t);return n.clean(e)},e}({})},function(e,t,n){var o,r;!function(i,a){o=a,r="function"==typeof o?o.call(t,n,t,e):o,!(void 0!==r&&(e.exports=r))}(this,function(){function e(e){var t=e.tags,n=Object.keys(t),o=n.map(function(e){return typeof t[e]}).every(function(e){return"object"===e||"boolean"===e||"function"===e});if(!o)throw new Error("The configuration was invalid");this.config=e}function t(e){return s.indexOf(e.nodeName)!==-1}function n(e){return l.indexOf(e.nodeName)!==-1}function o(e){return document.createTreeWalker(e,NodeFilter.SHOW_TEXT|NodeFilter.SHOW_ELEMENT|NodeFilter.SHOW_COMMENT,null,!1)}function r(e,t,n){return"function"==typeof e.tags[t]?e.tags[t](n):e.tags[t]}function i(e,t){return"undefined"==typeof t||"boolean"==typeof t&&!t}function a(e,t,n){var o=e.name.toLowerCase();return t!==!0&&("function"==typeof t[o]?!t[o](e.value,n):"undefined"==typeof t[o]||(t[o]===!1||"string"==typeof t[o]&&t[o]!==e.value))}var s=["P","LI","TD","TH","DIV","H1","H2","H3","H4","H5","H6","PRE"],l=["A","B","STRONG","I","EM","SUB","SUP","U","STRIKE"];return e.prototype.clean=function(e){var t=document.createElement("div");return t.innerHTML=e,this._sanitize(t),t.innerHTML},e.prototype._sanitize=function(e){var s=o(e),l=s.firstChild();if(l)do if(!l._sanitized)if(l.nodeType!==Node.TEXT_NODE){if(l.nodeType===Node.COMMENT_NODE){e.removeChild(l),this._sanitize(e);break}var c,d=n(l);d&&(c=Array.prototype.some.call(l.childNodes,t));var u=!!e.parentNode,p=t(e)&&t(l)&&u,f=l.nodeName.toLowerCase(),g=r(this.config,f,l),h=d&&c;if(h||i(l,g)||!this.config.keepNestedBlockElements&&p){if("SCRIPT"!==l.nodeName&&"STYLE"!==l.nodeName)for(;l.childNodes.length>0;)e.insertBefore(l.childNodes[0],l);e.removeChild(l),this._sanitize(e);break}for(var v=0;v0&&t.splice(s,1)}},e.removeAll=function(){t.map(function(t){e.remove(t.element,t.type,t.handler)})},e.get=function(t,n,o){return e.search.all(t,n,o)},e}({})},function(e,t){"use strict";var n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};e.exports=function(e){var t=codex.editor;return e.removeNodes=function(){t.nodes.wrapper.remove(),t.nodes.notifications.remove()},e.destroyPlugins=function(){for(var e in t.tools)"function"==typeof t.tools[e].destroy&&t.tools[e].destroy()},e.destroyScripts=function(){for(var e=document.getElementsByTagName("SCRIPT"),n=0;n nodeList\n\t */\n\t renderer.appendBlocks = function (data) {\n\t\n\t var blocks = data.items;\n\t\n\t /**\n\t * Sequence of one-by-one blocks appending\n\t * Uses to save blocks order after async-handler\n\t */\n\t var nodeSequence = Promise.resolve();\n\t\n\t for (var index = 0; index < blocks.length; index++) {\n\t\n\t /** Add node to sequence at specified index */\n\t editor.renderer.appendNodeAtIndex(nodeSequence, blocks, index);\n\t }\n\t };\n\t\n\t /**\n\t * Append node at specified index\n\t */\n\t renderer.appendNodeAtIndex = function (nodeSequence, blocks, index) {\n\t\n\t /** We need to append node to sequence */\n\t nodeSequence\n\t\n\t /** first, get node async-aware */\n\t .then(function () {\n\t\n\t return editor.renderer.getNodeAsync(blocks, index);\n\t })\n\t\n\t /**\n\t * second, compose editor-block from JSON object\n\t */\n\t .then(editor.renderer.createBlockFromData)\n\t\n\t /**\n\t * now insert block to redactor\n\t */\n\t .then(function (blockData) {\n\t\n\t /**\n\t * blockData has 'block', 'type' and 'stretched' information\n\t */\n\t editor.content.insertBlock(blockData);\n\t\n\t /** Pass created block to next step */\n\t return blockData.block;\n\t })\n\t\n\t /** Log if something wrong with node */\n\t .catch(function (error) {\n\t\n\t editor.core.log('Node skipped while parsing because %o', 'error', error);\n\t });\n\t };\n\t\n\t /**\n\t * Asynchronously returns block data from blocksList by index\n\t * @return Promise to node\n\t */\n\t renderer.getNodeAsync = function (blocksList, index) {\n\t\n\t return Promise.resolve().then(function () {\n\t\n\t return {\n\t tool: blocksList[index],\n\t position: index\n\t };\n\t });\n\t };\n\t\n\t /**\n\t * Creates editor block by JSON-data\n\t *\n\t * @uses render method of each plugin\n\t *\n\t * @param {Object} toolData.tool\n\t * { header : {\n\t * text: '',\n\t * type: 'H3', ...\n\t * }\n\t * }\n\t * @param {Number} toolData.position - index in input-blocks array\n\t * @return {Object} with type and Element\n\t */\n\t renderer.createBlockFromData = function (toolData) {\n\t\n\t /** New parser */\n\t var block,\n\t tool = toolData.tool,\n\t pluginName = tool.type;\n\t\n\t /** Get first key of object that stores plugin name */\n\t // for (var pluginName in blockData) break;\n\t\n\t /** Check for plugin existance */\n\t if (!editor.tools[pluginName]) {\n\t\n\t throw Error('Plugin \\xAB' + pluginName + '\\xBB not found');\n\t }\n\t\n\t /** Check for plugin having render method */\n\t if (typeof editor.tools[pluginName].render != 'function') {\n\t\n\t throw Error('Plugin \\xAB' + pluginName + '\\xBB must have \\xABrender\\xBB method');\n\t }\n\t\n\t if (editor.tools[pluginName].available === false) {\n\t\n\t block = editor.draw.unavailableBlock();\n\t\n\t block.innerHTML = editor.tools[pluginName].loadingMessage;\n\t\n\t /**\n\t * Saver will extract data from initial block data by position in array\n\t */\n\t block.dataset.inputPosition = toolData.position;\n\t } else {\n\t\n\t /** New Parser */\n\t block = editor.tools[pluginName].render(tool.data);\n\t }\n\t\n\t /** is first-level block stretched */\n\t var stretched = editor.tools[pluginName].isStretched || false;\n\t\n\t /** Retrun type and block */\n\t return {\n\t type: pluginName,\n\t block: block,\n\t stretched: stretched\n\t };\n\t };\n\t\n\t return renderer;\n\t}({});\n\n/***/ }),\n/* 6 */\n/***/ (function(module, exports) {\n\n\t'use strict';\n\t\n\t/**\n\t * Codex Editor Saver\n\t *\n\t * @author Codex Team\n\t * @version 1.1.0\n\t */\n\t\n\tmodule.exports = function (saver) {\n\t\n\t var editor = codex.editor;\n\t\n\t /**\n\t * @public\n\t * Save blocks\n\t */\n\t saver.save = function () {\n\t\n\t /** Save html content of redactor to memory */\n\t editor.state.html = editor.nodes.redactor.innerHTML;\n\t\n\t /** Clean jsonOutput state */\n\t editor.state.jsonOutput = [];\n\t\n\t return saveBlocks(editor.nodes.redactor.childNodes);\n\t };\n\t\n\t /**\n\t * @private\n\t * Save each block data\n\t *\n\t * @param blocks\n\t * @returns {Promise.}\n\t */\n\t var saveBlocks = function saveBlocks(blocks) {\n\t\n\t var data = [];\n\t\n\t for (var index = 0; index < blocks.length; index++) {\n\t\n\t data.push(getBlockData(blocks[index]));\n\t }\n\t\n\t return Promise.all(data).then(makeOutput).catch(editor.core.log);\n\t };\n\t\n\t /** Save and validate block data */\n\t var getBlockData = function getBlockData(block) {\n\t\n\t return saveBlockData(block).then(validateBlockData).catch(editor.core.log);\n\t };\n\t\n\t /**\n\t * @private\n\t * Call block`s plugin save method and return saved data\n\t *\n\t * @param block\n\t * @returns {Object}\n\t */\n\t var saveBlockData = function saveBlockData(block) {\n\t\n\t var pluginName = block.dataset.tool;\n\t\n\t /** Check for plugin existence */\n\t if (!editor.tools[pluginName]) {\n\t\n\t editor.core.log('Plugin \\xAB' + pluginName + '\\xBB not found', 'error');\n\t return { data: null, pluginName: null };\n\t }\n\t\n\t /** Check for plugin having save method */\n\t if (typeof editor.tools[pluginName].save !== 'function') {\n\t\n\t editor.core.log('Plugin \\xAB' + pluginName + '\\xBB must have save method', 'error');\n\t return { data: null, pluginName: null };\n\t }\n\t\n\t /** Result saver */\n\t var blockContent = block.childNodes[0],\n\t pluginsContent = blockContent.childNodes[0],\n\t position = pluginsContent.dataset.inputPosition;\n\t\n\t /** If plugin wasn't available then return data from cache */\n\t if (editor.tools[pluginName].available === false) {\n\t\n\t return Promise.resolve({ data: codex.editor.state.blocks.items[position].data, pluginName: pluginName });\n\t }\n\t\n\t return Promise.resolve(pluginsContent).then(editor.tools[pluginName].save).then(function (data) {\n\t return Object({ data: data, pluginName: pluginName });\n\t });\n\t };\n\t\n\t /**\n\t * Call plugin`s validate method. Return false if validation failed\n\t *\n\t * @param data\n\t * @param pluginName\n\t * @returns {Object|Boolean}\n\t */\n\t var validateBlockData = function validateBlockData(_ref) {\n\t var data = _ref.data,\n\t pluginName = _ref.pluginName;\n\t\n\t\n\t if (!data || !pluginName) {\n\t\n\t return false;\n\t }\n\t\n\t if (editor.tools[pluginName].validate) {\n\t\n\t var result = editor.tools[pluginName].validate(data);\n\t\n\t /**\n\t * Do not allow invalid data\n\t */\n\t if (!result) {\n\t\n\t return false;\n\t }\n\t }\n\t\n\t return { data: data, pluginName: pluginName };\n\t };\n\t\n\t /**\n\t * Compile article output\n\t *\n\t * @param savedData\n\t * @returns {{time: number, version, items: (*|Array)}}\n\t */\n\t var makeOutput = function makeOutput(savedData) {\n\t\n\t savedData = savedData.filter(function (blockData) {\n\t return blockData;\n\t });\n\t\n\t var items = savedData.map(function (blockData) {\n\t return Object({ type: blockData.pluginName, data: blockData.data });\n\t });\n\t\n\t editor.state.jsonOutput = items;\n\t\n\t return {\n\t id: editor.state.blocks.id || null,\n\t time: +new Date(),\n\t version: editor.version,\n\t items: items\n\t };\n\t };\n\t\n\t return saver;\n\t}({});\n\n/***/ }),\n/* 7 */\n/***/ (function(module, exports) {\n\n\t'use strict';\n\t\n\t/**\n\t * Codex Editor Content Module\n\t * Works with DOM\n\t *\n\t * @module Codex Editor content module\n\t *\n\t * @author Codex Team\n\t * @version 1.3.13\n\t *\n\t * @description Module works with Elements that have been appended to the main DOM\n\t */\n\t\n\tmodule.exports = function (content) {\n\t\n\t var editor = codex.editor;\n\t\n\t /**\n\t * Links to current active block\n\t * @type {null | Element}\n\t */\n\t content.currentNode = null;\n\t\n\t /**\n\t * clicked in redactor area\n\t * @type {null | Boolean}\n\t */\n\t content.editorAreaHightlighted = null;\n\t\n\t /**\n\t * @deprecated\n\t * Synchronizes redactor with original textarea\n\t */\n\t content.sync = function () {\n\t\n\t editor.core.log('syncing...');\n\t\n\t /**\n\t * Save redactor content to editor.state\n\t */\n\t editor.state.html = editor.nodes.redactor.innerHTML;\n\t };\n\t\n\t /**\n\t * Appends background to the block\n\t *\n\t * @description add CSS class to highlight visually first-level block area\n\t */\n\t content.markBlock = function () {\n\t\n\t editor.content.currentNode.classList.add(editor.ui.className.BLOCK_HIGHLIGHTED);\n\t };\n\t\n\t /**\n\t * Clear background\n\t *\n\t * @description clears styles that highlights block\n\t */\n\t content.clearMark = function () {\n\t\n\t if (editor.content.currentNode) {\n\t\n\t editor.content.currentNode.classList.remove(editor.ui.className.BLOCK_HIGHLIGHTED);\n\t }\n\t };\n\t\n\t /**\n\t * Finds first-level block\n\t *\n\t * @param {Element} node - selected or clicked in redactors area node\n\t * @protected\n\t *\n\t * @description looks for first-level block.\n\t * gets parent while node is not first-level\n\t */\n\t content.getFirstLevelBlock = function (node) {\n\t\n\t if (!editor.core.isDomNode(node)) {\n\t\n\t node = node.parentNode;\n\t }\n\t\n\t if (node === editor.nodes.redactor || node === document.body) {\n\t\n\t return null;\n\t } else {\n\t\n\t while (!node.classList.contains(editor.ui.className.BLOCK_CLASSNAME)) {\n\t\n\t node = node.parentNode;\n\t }\n\t\n\t return node;\n\t }\n\t };\n\t\n\t /**\n\t * Trigger this event when working node changed\n\t * @param {Element} targetNode - first-level of this node will be current\n\t * @protected\n\t *\n\t * @description If targetNode is first-level then we set it as current else we look for parents to find first-level\n\t */\n\t content.workingNodeChanged = function (targetNode) {\n\t\n\t /** Clear background from previous marked block before we change */\n\t editor.content.clearMark();\n\t\n\t if (!targetNode) {\n\t\n\t return;\n\t }\n\t\n\t content.currentNode = content.getFirstLevelBlock(targetNode);\n\t };\n\t\n\t /**\n\t * Replaces one redactor block with another\n\t * @protected\n\t * @param {Element} targetBlock - block to replace. Mostly currentNode.\n\t * @param {Element} newBlock\n\t * @param {string} newBlockType - type of new block; we need to store it to data-attribute\n\t *\n\t * [!] Function does not saves old block content.\n\t * You can get it manually and pass with newBlock.innerHTML\n\t */\n\t content.replaceBlock = function (targetBlock, newBlock) {\n\t\n\t if (!targetBlock || !newBlock) {\n\t\n\t editor.core.log('replaceBlock: missed params');\n\t return;\n\t }\n\t\n\t /** If target-block is not a frist-level block, then we iterate parents to find it */\n\t while (!targetBlock.classList.contains(editor.ui.className.BLOCK_CLASSNAME)) {\n\t\n\t targetBlock = targetBlock.parentNode;\n\t }\n\t\n\t /** Replacing */\n\t editor.nodes.redactor.replaceChild(newBlock, targetBlock);\n\t\n\t /**\n\t * Set new node as current\n\t */\n\t editor.content.workingNodeChanged(newBlock);\n\t\n\t /**\n\t * Add block handlers\n\t */\n\t editor.ui.addBlockHandlers(newBlock);\n\t\n\t /**\n\t * Save changes\n\t */\n\t editor.ui.saveInputs();\n\t };\n\t\n\t /**\n\t * @protected\n\t *\n\t * Inserts new block to redactor\n\t * Wrapps block into a DIV with BLOCK_CLASSNAME class\n\t *\n\t * @param blockData {object}\n\t * @param blockData.block {Element} element with block content\n\t * @param blockData.type {string} block plugin\n\t * @param needPlaceCaret {bool} pass true to set caret in new block\n\t *\n\t */\n\t content.insertBlock = function (blockData, needPlaceCaret) {\n\t\n\t var workingBlock = editor.content.currentNode,\n\t newBlockContent = blockData.block,\n\t blockType = blockData.type,\n\t isStretched = blockData.stretched;\n\t\n\t var newBlock = composeNewBlock_(newBlockContent, blockType, isStretched);\n\t\n\t if (workingBlock) {\n\t\n\t editor.core.insertAfter(workingBlock, newBlock);\n\t } else {\n\t\n\t /**\n\t * If redactor is empty, append as first child\n\t */\n\t editor.nodes.redactor.appendChild(newBlock);\n\t }\n\t\n\t /**\n\t * Block handler\n\t */\n\t editor.ui.addBlockHandlers(newBlock);\n\t\n\t /**\n\t * Set new node as current\n\t */\n\t editor.content.workingNodeChanged(newBlock);\n\t\n\t /**\n\t * Save changes\n\t */\n\t editor.ui.saveInputs();\n\t\n\t if (needPlaceCaret) {\n\t\n\t /**\n\t * If we don't know input index then we set default value -1\n\t */\n\t var currentInputIndex = editor.caret.getCurrentInputIndex() || -1;\n\t\n\t if (currentInputIndex == -1) {\n\t\n\t var editableElement = newBlock.querySelector('[contenteditable]'),\n\t emptyText = document.createTextNode('');\n\t\n\t editableElement.appendChild(emptyText);\n\t editor.caret.set(editableElement, 0, 0);\n\t\n\t editor.toolbar.move();\n\t editor.toolbar.showPlusButton();\n\t } else {\n\t\n\t if (currentInputIndex === editor.state.inputs.length - 1) return;\n\t\n\t /** Timeout for browsers execution */\n\t window.setTimeout(function () {\n\t\n\t /** Setting to the new input */\n\t editor.caret.setToNextBlock(currentInputIndex);\n\t editor.toolbar.move();\n\t editor.toolbar.open();\n\t }, 10);\n\t }\n\t }\n\t\n\t /**\n\t * Block is inserted, wait for new click that defined focusing on editors area\n\t * @type {boolean}\n\t */\n\t content.editorAreaHightlighted = false;\n\t };\n\t\n\t /**\n\t * Replaces blocks with saving content\n\t * @protected\n\t * @param {Element} noteToReplace\n\t * @param {Element} newNode\n\t * @param {Element} blockType\n\t */\n\t content.switchBlock = function (blockToReplace, newBlock, tool) {\n\t\n\t tool = tool || editor.content.currentNode.dataset.tool;\n\t var newBlockComposed = composeNewBlock_(newBlock, tool);\n\t\n\t /** Replacing */\n\t editor.content.replaceBlock(blockToReplace, newBlockComposed);\n\t\n\t /** Save new Inputs when block is changed */\n\t editor.ui.saveInputs();\n\t };\n\t\n\t /**\n\t * Iterates between child noted and looking for #text node on deepest level\n\t * @protected\n\t *\n\t * @param {Element} block - node where find\n\t * @param {int} postiton - starting postion\n\t * Example: childNodex.length to find from the end\n\t * or 0 to find from the start\n\t * @return {Text} block\n\t * @uses DFS\n\t */\n\t content.getDeepestTextNodeFromPosition = function (block, position) {\n\t\n\t /**\n\t * Clear Block from empty and useless spaces with trim.\n\t * Such nodes we should remove\n\t */\n\t var blockChilds = block.childNodes,\n\t index,\n\t node,\n\t text;\n\t\n\t for (index = 0; index < blockChilds.length; index++) {\n\t\n\t node = blockChilds[index];\n\t\n\t if (node.nodeType == editor.core.nodeTypes.TEXT) {\n\t\n\t text = node.textContent.trim();\n\t\n\t /** Text is empty. We should remove this child from node before we start DFS\n\t * decrease the quantity of childs.\n\t */\n\t if (text === '') {\n\t\n\t block.removeChild(node);\n\t position--;\n\t }\n\t }\n\t }\n\t\n\t if (block.childNodes.length === 0) {\n\t\n\t return document.createTextNode('');\n\t }\n\t\n\t /** Setting default position when we deleted all empty nodes */\n\t if (position < 0) position = 1;\n\t\n\t var lookingFromStart = false;\n\t\n\t /** For looking from START */\n\t if (position === 0) {\n\t\n\t lookingFromStart = true;\n\t position = 1;\n\t }\n\t\n\t while (position) {\n\t\n\t /** initial verticle of node. */\n\t if (lookingFromStart) {\n\t\n\t block = block.childNodes[0];\n\t } else {\n\t\n\t block = block.childNodes[position - 1];\n\t }\n\t\n\t if (block.nodeType == editor.core.nodeTypes.TAG) {\n\t\n\t position = block.childNodes.length;\n\t } else if (block.nodeType == editor.core.nodeTypes.TEXT) {\n\t\n\t position = 0;\n\t }\n\t }\n\t\n\t return block;\n\t };\n\t\n\t /**\n\t * @private\n\t * @param {Element} block - current plugins render\n\t * @param {String} tool - plugins name\n\t * @param {Boolean} isStretched - make stretched block or not\n\t *\n\t * @description adds necessary information to wrap new created block by first-level holder\n\t */\n\t var composeNewBlock_ = function composeNewBlock_(block, tool, isStretched) {\n\t\n\t var newBlock = editor.draw.node('DIV', editor.ui.className.BLOCK_CLASSNAME, {}),\n\t blockContent = editor.draw.node('DIV', editor.ui.className.BLOCK_CONTENT, {});\n\t\n\t blockContent.appendChild(block);\n\t newBlock.appendChild(blockContent);\n\t\n\t if (isStretched) {\n\t\n\t blockContent.classList.add(editor.ui.className.BLOCK_STRETCHED);\n\t }\n\t\n\t newBlock.dataset.tool = tool;\n\t return newBlock;\n\t };\n\t\n\t /**\n\t * Returns Range object of current selection\n\t * @protected\n\t */\n\t content.getRange = function () {\n\t\n\t var selection = window.getSelection().getRangeAt(0);\n\t\n\t return selection;\n\t };\n\t\n\t /**\n\t * Divides block in two blocks (after and before caret)\n\t *\n\t * @protected\n\t * @param {int} inputIndex - target input index\n\t *\n\t * @description splits current input content to the separate blocks\n\t * When enter is pressed among the words, that text will be splited.\n\t */\n\t content.splitBlock = function (inputIndex) {\n\t\n\t var selection = window.getSelection(),\n\t anchorNode = selection.anchorNode,\n\t anchorNodeText = anchorNode.textContent,\n\t caretOffset = selection.anchorOffset,\n\t textBeforeCaret,\n\t textNodeBeforeCaret,\n\t textAfterCaret,\n\t textNodeAfterCaret;\n\t\n\t var currentBlock = editor.content.currentNode.querySelector('[contentEditable]');\n\t\n\t textBeforeCaret = anchorNodeText.substring(0, caretOffset);\n\t textAfterCaret = anchorNodeText.substring(caretOffset);\n\t\n\t textNodeBeforeCaret = document.createTextNode(textBeforeCaret);\n\t\n\t if (textAfterCaret) {\n\t\n\t textNodeAfterCaret = document.createTextNode(textAfterCaret);\n\t }\n\t\n\t var previousChilds = [],\n\t nextChilds = [],\n\t reachedCurrent = false;\n\t\n\t if (textNodeAfterCaret) {\n\t\n\t nextChilds.push(textNodeAfterCaret);\n\t }\n\t\n\t for (var i = 0, child; !!(child = currentBlock.childNodes[i]); i++) {\n\t\n\t if (child != anchorNode) {\n\t\n\t if (!reachedCurrent) {\n\t\n\t previousChilds.push(child);\n\t } else {\n\t\n\t nextChilds.push(child);\n\t }\n\t } else {\n\t\n\t reachedCurrent = true;\n\t }\n\t }\n\t\n\t /** Clear current input */\n\t editor.state.inputs[inputIndex].innerHTML = '';\n\t\n\t /**\n\t * Append all childs founded before anchorNode\n\t */\n\t var previousChildsLength = previousChilds.length;\n\t\n\t for (i = 0; i < previousChildsLength; i++) {\n\t\n\t editor.state.inputs[inputIndex].appendChild(previousChilds[i]);\n\t }\n\t\n\t editor.state.inputs[inputIndex].appendChild(textNodeBeforeCaret);\n\t\n\t /**\n\t * Append text node which is after caret\n\t */\n\t var nextChildsLength = nextChilds.length,\n\t newNode = document.createElement('div');\n\t\n\t for (i = 0; i < nextChildsLength; i++) {\n\t\n\t newNode.appendChild(nextChilds[i]);\n\t }\n\t\n\t newNode = newNode.innerHTML;\n\t\n\t /** This type of block creates when enter is pressed */\n\t var NEW_BLOCK_TYPE = editor.settings.initialBlockPlugin;\n\t\n\t /**\n\t * Make new paragraph with text after caret\n\t */\n\t editor.content.insertBlock({\n\t type: NEW_BLOCK_TYPE,\n\t block: editor.tools[NEW_BLOCK_TYPE].render({\n\t text: newNode\n\t })\n\t }, true);\n\t };\n\t\n\t /**\n\t * Merges two blocks — current and target\n\t * If target index is not exist, then previous will be as target\n\t *\n\t * @protected\n\t * @param {int} currentInputIndex\n\t * @param {int} targetInputIndex\n\t *\n\t * @description gets two inputs indexes and merges into one\n\t */\n\t content.mergeBlocks = function (currentInputIndex, targetInputIndex) {\n\t\n\t /** If current input index is zero, then prevent method execution */\n\t if (currentInputIndex === 0) {\n\t\n\t return;\n\t }\n\t\n\t var targetInput,\n\t currentInputContent = editor.state.inputs[currentInputIndex].innerHTML;\n\t\n\t if (!targetInputIndex) {\n\t\n\t targetInput = editor.state.inputs[currentInputIndex - 1];\n\t } else {\n\t\n\t targetInput = editor.state.inputs[targetInputIndex];\n\t }\n\t\n\t targetInput.innerHTML += currentInputContent;\n\t };\n\t\n\t /**\n\t * Iterates all right siblings and parents, which has right siblings\n\t * while it does not reached the first-level block\n\t *\n\t * @param {Element} node\n\t * @return {boolean}\n\t */\n\t content.isLastNode = function (node) {\n\t\n\t // console.log('погнали перебор родителей');\n\t\n\t var allChecked = false;\n\t\n\t while (!allChecked) {\n\t\n\t // console.log('Смотрим на %o', node);\n\t // console.log('Проверим, пустые ли соседи справа');\n\t\n\t if (!allSiblingsEmpty_(node)) {\n\t\n\t // console.log('Есть непустые соседи. Узел не последний. Выходим.');\n\t return false;\n\t }\n\t\n\t node = node.parentNode;\n\t\n\t /**\n\t * Проверяем родителей до тех пор, пока не найдем блок первого уровня\n\t */\n\t if (node.classList.contains(editor.ui.className.BLOCK_CONTENT)) {\n\t\n\t allChecked = true;\n\t }\n\t }\n\t\n\t return true;\n\t };\n\t\n\t /**\n\t * Checks if all element right siblings is empty\n\t * @param node\n\t */\n\t var allSiblingsEmpty_ = function allSiblingsEmpty_(node) {\n\t\n\t /**\n\t * Нужно убедиться, что после пустого соседа ничего нет\n\t */\n\t var sibling = node.nextSibling;\n\t\n\t while (sibling) {\n\t\n\t if (sibling.textContent.length) {\n\t\n\t return false;\n\t }\n\t\n\t sibling = sibling.nextSibling;\n\t }\n\t\n\t return true;\n\t };\n\t\n\t /**\n\t * @public\n\t *\n\t * @param {string} htmlData - html content as string\n\t * @param {string} plainData - plain text\n\t * @return {string} - html content as string\n\t */\n\t content.wrapTextWithParagraphs = function (htmlData, plainData) {\n\t\n\t if (!htmlData.trim()) {\n\t\n\t return wrapPlainTextWithParagraphs(plainData);\n\t }\n\t\n\t var wrapper = document.createElement('DIV'),\n\t newWrapper = document.createElement('DIV'),\n\t i,\n\t paragraph,\n\t firstLevelBlocks = ['DIV', 'P'],\n\t blockTyped,\n\t node;\n\t\n\t /**\n\t * Make HTML Element to Wrap Text\n\t * It allows us to work with input data as HTML content\n\t */\n\t wrapper.innerHTML = htmlData;\n\t paragraph = document.createElement('P');\n\t\n\t for (i = 0; i < wrapper.childNodes.length; i++) {\n\t\n\t node = wrapper.childNodes[i];\n\t\n\t blockTyped = firstLevelBlocks.indexOf(node.tagName) != -1;\n\t\n\t /**\n\t * If node is first-levet\n\t * we add this node to our new wrapper\n\t */\n\t if (blockTyped) {\n\t\n\t /**\n\t * If we had splitted inline nodes to paragraph before\n\t */\n\t if (paragraph.childNodes.length) {\n\t\n\t newWrapper.appendChild(paragraph.cloneNode(true));\n\t\n\t /** empty paragraph */\n\t paragraph = null;\n\t paragraph = document.createElement('P');\n\t }\n\t\n\t newWrapper.appendChild(node.cloneNode(true));\n\t } else {\n\t\n\t /** Collect all inline nodes to one as paragraph */\n\t paragraph.appendChild(node.cloneNode(true));\n\t\n\t /** if node is last we should append this node to paragraph and paragraph to new wrapper */\n\t if (i == wrapper.childNodes.length - 1) {\n\t\n\t newWrapper.appendChild(paragraph.cloneNode(true));\n\t }\n\t }\n\t }\n\t\n\t return newWrapper.innerHTML;\n\t };\n\t\n\t /**\n\t * Splits strings on new line and wraps paragraphs with
tag\n\t * @param plainText\n\t * @returns {string}\n\t */\n\t var wrapPlainTextWithParagraphs = function wrapPlainTextWithParagraphs(plainText) {\n\t\n\t if (!plainText) return '';\n\t\n\t return '
' + plainText.split('\\n\\n').join('
') + '
';\n\t };\n\t\n\t /**\n\t * Finds closest Contenteditable parent from Element\n\t * @param {Element} node element looking from\n\t * @return {Element} node contenteditable\n\t */\n\t content.getEditableParent = function (node) {\n\t\n\t while (node && node.contentEditable != 'true') {\n\t\n\t node = node.parentNode;\n\t }\n\t\n\t return node;\n\t };\n\t\n\t /**\n\t * Clear editors content\n\t *\n\t * @param {Boolean} all — if true, delete all article data (content, id, etc.)\n\t */\n\t content.clear = function (all) {\n\t\n\t editor.nodes.redactor.innerHTML = '';\n\t editor.content.sync();\n\t editor.ui.saveInputs();\n\t if (all) {\n\t\n\t editor.state.blocks = {};\n\t } else if (editor.state.blocks) {\n\t\n\t editor.state.blocks.items = [];\n\t }\n\t\n\t editor.content.currentNode = null;\n\t };\n\t\n\t /**\n\t *\n\t * Load new data to editor\n\t * If editor is not empty, just append articleData.items\n\t *\n\t * @param articleData.items\n\t */\n\t content.load = function (articleData) {\n\t\n\t var currentContent = Object.assign({}, editor.state.blocks);\n\t\n\t editor.content.clear();\n\t\n\t if (!Object.keys(currentContent).length) {\n\t\n\t editor.state.blocks = articleData;\n\t } else if (!currentContent.items) {\n\t\n\t currentContent.items = articleData.items;\n\t editor.state.blocks = currentContent;\n\t } else {\n\t\n\t currentContent.items = currentContent.items.concat(articleData.items);\n\t editor.state.blocks = currentContent;\n\t }\n\t\n\t editor.renderer.makeBlocksFromData();\n\t };\n\t\n\t return content;\n\t}({});\n\n/***/ }),\n/* 8 */\n/***/ (function(module, exports, __webpack_require__) {\n\n\t'use strict';\n\t\n\t/**\n\t * Codex Editor toolbar module\n\t *\n\t * Contains:\n\t * - Inline toolbox\n\t * - Toolbox within plus button\n\t * - Settings section\n\t *\n\t * @author Codex Team\n\t * @version 1.0\n\t */\n\t\n\tmodule.exports = function (toolbar) {\n\t\n\t var editor = codex.editor;\n\t\n\t toolbar.settings = __webpack_require__(9);\n\t toolbar.inline = __webpack_require__(10);\n\t toolbar.toolbox = __webpack_require__(11);\n\t\n\t /**\n\t * Margin between focused node and toolbar\n\t */\n\t toolbar.defaultToolbarHeight = 49;\n\t\n\t toolbar.defaultOffset = 34;\n\t\n\t toolbar.opened = false;\n\t\n\t toolbar.current = null;\n\t\n\t /**\n\t * @protected\n\t */\n\t toolbar.open = function () {\n\t\n\t if (editor.hideToolbar) {\n\t\n\t return;\n\t }\n\t\n\t var toolType = editor.content.currentNode.dataset.tool;\n\t\n\t if (!editor.tools[toolType] || !editor.tools[toolType].makeSettings) {\n\t\n\t editor.nodes.showSettingsButton.classList.add('hide');\n\t } else {\n\t\n\t editor.nodes.showSettingsButton.classList.remove('hide');\n\t }\n\t\n\t editor.nodes.toolbar.classList.add('opened');\n\t this.opened = true;\n\t };\n\t\n\t /**\n\t * @protected\n\t */\n\t toolbar.close = function () {\n\t\n\t editor.nodes.toolbar.classList.remove('opened');\n\t\n\t toolbar.opened = false;\n\t toolbar.current = null;\n\t\n\t for (var button in editor.nodes.toolbarButtons) {\n\t\n\t editor.nodes.toolbarButtons[button].classList.remove('selected');\n\t }\n\t\n\t /** Close toolbox when toolbar is not displayed */\n\t editor.toolbar.toolbox.close();\n\t editor.toolbar.settings.close();\n\t };\n\t\n\t toolbar.toggle = function () {\n\t\n\t if (!this.opened) {\n\t\n\t this.open();\n\t } else {\n\t\n\t this.close();\n\t }\n\t };\n\t\n\t toolbar.hidePlusButton = function () {\n\t\n\t editor.nodes.plusButton.classList.add('hide');\n\t };\n\t\n\t toolbar.showPlusButton = function () {\n\t\n\t editor.nodes.plusButton.classList.remove('hide');\n\t };\n\t\n\t /**\n\t * Moving toolbar to the specified node\n\t */\n\t toolbar.move = function () {\n\t\n\t /** Close Toolbox when we move toolbar */\n\t editor.toolbar.toolbox.close();\n\t\n\t if (!editor.content.currentNode) {\n\t\n\t return;\n\t }\n\t\n\t var newYCoordinate = editor.content.currentNode.offsetTop - editor.toolbar.defaultToolbarHeight / 2 + editor.toolbar.defaultOffset;\n\t\n\t editor.nodes.toolbar.style.transform = 'translate3D(0, ' + Math.floor(newYCoordinate) + 'px, 0)';\n\t\n\t /** Close trash actions */\n\t editor.toolbar.settings.hideRemoveActions();\n\t };\n\t\n\t return toolbar;\n\t}({});\n\n/***/ }),\n/* 9 */\n/***/ (function(module, exports) {\n\n\t'use strict';\n\t\n\t/**\n\t * Toolbar settings\n\t *\n\t * @version 1.0.5\n\t */\n\t\n\tmodule.exports = function (settings) {\n\t\n\t var editor = codex.editor;\n\t\n\t settings.opened = false;\n\t\n\t settings.setting = null;\n\t settings.actions = null;\n\t\n\t /**\n\t * Append and open settings\n\t */\n\t settings.open = function (toolType) {\n\t\n\t /**\n\t * Append settings content\n\t * It's stored in tool.settings\n\t */\n\t if (!editor.tools[toolType] || !editor.tools[toolType].makeSettings) {\n\t\n\t return;\n\t }\n\t\n\t /**\n\t * Draw settings block\n\t */\n\t var settingsBlock = editor.tools[toolType].makeSettings();\n\t\n\t editor.nodes.pluginSettings.appendChild(settingsBlock);\n\t\n\t /** Open settings block */\n\t editor.nodes.blockSettings.classList.add('opened');\n\t this.opened = true;\n\t };\n\t\n\t /**\n\t * Close and clear settings\n\t */\n\t settings.close = function () {\n\t\n\t editor.nodes.blockSettings.classList.remove('opened');\n\t editor.nodes.pluginSettings.innerHTML = '';\n\t\n\t this.opened = false;\n\t };\n\t\n\t /**\n\t * @param {string} toolType - plugin type\n\t */\n\t settings.toggle = function (toolType) {\n\t\n\t if (!this.opened) {\n\t\n\t this.open(toolType);\n\t } else {\n\t\n\t this.close();\n\t }\n\t };\n\t\n\t /**\n\t * Here we will draw buttons and add listeners to components\n\t */\n\t settings.makeRemoveBlockButton = function () {\n\t\n\t var removeBlockWrapper = editor.draw.node('SPAN', 'ce-toolbar__remove-btn', {}),\n\t settingButton = editor.draw.node('SPAN', 'ce-toolbar__remove-setting', { innerHTML: '' }),\n\t actionWrapper = editor.draw.node('DIV', 'ce-toolbar__remove-confirmation', {}),\n\t confirmAction = editor.draw.node('DIV', 'ce-toolbar__remove-confirm', { textContent: 'Удалить блок' }),\n\t cancelAction = editor.draw.node('DIV', 'ce-toolbar__remove-cancel', { textContent: 'Отмена' });\n\t\n\t editor.listeners.add(settingButton, 'click', editor.toolbar.settings.removeButtonClicked, false);\n\t\n\t editor.listeners.add(confirmAction, 'click', editor.toolbar.settings.confirmRemovingRequest, false);\n\t\n\t editor.listeners.add(cancelAction, 'click', editor.toolbar.settings.cancelRemovingRequest, false);\n\t\n\t actionWrapper.appendChild(confirmAction);\n\t actionWrapper.appendChild(cancelAction);\n\t\n\t removeBlockWrapper.appendChild(settingButton);\n\t removeBlockWrapper.appendChild(actionWrapper);\n\t\n\t /** Save setting */\n\t editor.toolbar.settings.setting = settingButton;\n\t editor.toolbar.settings.actions = actionWrapper;\n\t\n\t return removeBlockWrapper;\n\t };\n\t\n\t settings.removeButtonClicked = function () {\n\t\n\t var action = editor.toolbar.settings.actions;\n\t\n\t if (action.classList.contains('opened')) {\n\t\n\t editor.toolbar.settings.hideRemoveActions();\n\t } else {\n\t\n\t editor.toolbar.settings.showRemoveActions();\n\t }\n\t\n\t editor.toolbar.toolbox.close();\n\t editor.toolbar.settings.close();\n\t };\n\t\n\t settings.cancelRemovingRequest = function () {\n\t\n\t editor.toolbar.settings.actions.classList.remove('opened');\n\t };\n\t\n\t settings.confirmRemovingRequest = function () {\n\t\n\t var currentBlock = editor.content.currentNode,\n\t firstLevelBlocksCount;\n\t\n\t currentBlock.remove();\n\t\n\t firstLevelBlocksCount = editor.nodes.redactor.childNodes.length;\n\t\n\t /**\n\t * If all blocks are removed\n\t */\n\t if (firstLevelBlocksCount === 0) {\n\t\n\t /** update currentNode variable */\n\t editor.content.currentNode = null;\n\t\n\t /** Inserting new empty initial block */\n\t editor.ui.addInitialBlock();\n\t }\n\t\n\t editor.ui.saveInputs();\n\t\n\t editor.toolbar.close();\n\t };\n\t\n\t settings.showRemoveActions = function () {\n\t\n\t editor.toolbar.settings.actions.classList.add('opened');\n\t };\n\t\n\t settings.hideRemoveActions = function () {\n\t\n\t editor.toolbar.settings.actions.classList.remove('opened');\n\t };\n\t\n\t return settings;\n\t}({});\n\n/***/ }),\n/* 10 */\n/***/ (function(module, exports) {\n\n\t'use strict';\n\t\n\t/**\n\t * Inline toolbar\n\t *\n\t * Contains from tools:\n\t * Bold, Italic, Underline and Anchor\n\t *\n\t * @author Codex Team\n\t * @version 1.0\n\t */\n\t\n\tmodule.exports = function (inline) {\n\t\n\t var editor = codex.editor;\n\t\n\t inline.buttonsOpened = null;\n\t inline.actionsOpened = null;\n\t inline.wrappersOffset = null;\n\t\n\t /**\n\t * saving selection that need for execCommand for styling\n\t *\n\t */\n\t inline.storedSelection = null;\n\t\n\t /**\n\t * @protected\n\t *\n\t * Open inline toobar\n\t */\n\t inline.show = function () {\n\t\n\t var currentNode = editor.content.currentNode,\n\t tool = currentNode.dataset.tool,\n\t plugin;\n\t\n\t /**\n\t * tool allowed to open inline toolbar\n\t */\n\t plugin = editor.tools[tool];\n\t\n\t if (!plugin.showInlineToolbar) return;\n\t\n\t var selectedText = inline.getSelectionText(),\n\t toolbar = editor.nodes.inlineToolbar.wrapper;\n\t\n\t if (selectedText.length > 0) {\n\t\n\t /** Move toolbar and open */\n\t editor.toolbar.inline.move();\n\t\n\t /** Open inline toolbar */\n\t toolbar.classList.add('opened');\n\t\n\t /** show buttons of inline toolbar */\n\t editor.toolbar.inline.showButtons();\n\t }\n\t };\n\t\n\t /**\n\t * @protected\n\t *\n\t * Closes inline toolbar\n\t */\n\t inline.close = function () {\n\t\n\t var toolbar = editor.nodes.inlineToolbar.wrapper;\n\t\n\t toolbar.classList.remove('opened');\n\t };\n\t\n\t /**\n\t * @private\n\t *\n\t * Moving toolbar\n\t */\n\t inline.move = function () {\n\t\n\t if (!this.wrappersOffset) {\n\t\n\t this.wrappersOffset = this.getWrappersOffset();\n\t }\n\t\n\t var coords = this.getSelectionCoords(),\n\t defaultOffset = 0,\n\t toolbar = editor.nodes.inlineToolbar.wrapper,\n\t newCoordinateX,\n\t newCoordinateY;\n\t\n\t if (toolbar.offsetHeight === 0) {\n\t\n\t defaultOffset = 40;\n\t }\n\t\n\t newCoordinateX = coords.x - this.wrappersOffset.left;\n\t newCoordinateY = coords.y + window.scrollY - this.wrappersOffset.top - defaultOffset - toolbar.offsetHeight;\n\t\n\t toolbar.style.transform = 'translate3D(' + Math.floor(newCoordinateX) + 'px, ' + Math.floor(newCoordinateY) + 'px, 0)';\n\t\n\t /** Close everything */\n\t editor.toolbar.inline.closeButtons();\n\t editor.toolbar.inline.closeAction();\n\t };\n\t\n\t /**\n\t * @private\n\t *\n\t * Tool Clicked\n\t */\n\t\n\t inline.toolClicked = function (event, type) {\n\t\n\t /**\n\t * For simple tools we use default browser function\n\t * For more complicated tools, we should write our own behavior\n\t */\n\t switch (type) {\n\t case 'createLink':\n\t editor.toolbar.inline.createLinkAction(event, type);break;\n\t default:\n\t editor.toolbar.inline.defaultToolAction(type);break;\n\t }\n\t\n\t /**\n\t * highlight buttons\n\t * after making some action\n\t */\n\t editor.nodes.inlineToolbar.buttons.childNodes.forEach(editor.toolbar.inline.hightlight);\n\t };\n\t\n\t /**\n\t * @private\n\t *\n\t * Saving wrappers offset in DOM\n\t */\n\t inline.getWrappersOffset = function () {\n\t\n\t var wrapper = editor.nodes.wrapper,\n\t offset = this.getOffset(wrapper);\n\t\n\t this.wrappersOffset = offset;\n\t return offset;\n\t };\n\t\n\t /**\n\t * @private\n\t *\n\t * Calculates offset of DOM element\n\t *\n\t * @param el\n\t * @returns {{top: number, left: number}}\n\t */\n\t inline.getOffset = function (el) {\n\t\n\t var _x = 0;\n\t var _y = 0;\n\t\n\t while (el && !isNaN(el.offsetLeft) && !isNaN(el.offsetTop)) {\n\t\n\t _x += el.offsetLeft + el.clientLeft;\n\t _y += el.offsetTop + el.clientTop;\n\t el = el.offsetParent;\n\t }\n\t return { top: _y, left: _x };\n\t };\n\t\n\t /**\n\t * @private\n\t *\n\t * Calculates position of selected text\n\t * @returns {{x: number, y: number}}\n\t */\n\t inline.getSelectionCoords = function () {\n\t\n\t var sel = document.selection,\n\t range;\n\t var x = 0,\n\t y = 0;\n\t\n\t if (sel) {\n\t\n\t if (sel.type != 'Control') {\n\t\n\t range = sel.createRange();\n\t range.collapse(true);\n\t x = range.boundingLeft;\n\t y = range.boundingTop;\n\t }\n\t } else if (window.getSelection) {\n\t\n\t sel = window.getSelection();\n\t\n\t if (sel.rangeCount) {\n\t\n\t range = sel.getRangeAt(0).cloneRange();\n\t if (range.getClientRects) {\n\t\n\t range.collapse(true);\n\t var rect = range.getClientRects()[0];\n\t\n\t if (!rect) {\n\t\n\t return;\n\t }\n\t\n\t x = rect.left;\n\t y = rect.top;\n\t }\n\t }\n\t }\n\t return { x: x, y: y };\n\t };\n\t\n\t /**\n\t * @private\n\t *\n\t * Returns selected text as String\n\t * @returns {string}\n\t */\n\t inline.getSelectionText = function () {\n\t\n\t var selectedText = '';\n\t\n\t // all modern browsers and IE9+\n\t if (window.getSelection) {\n\t\n\t selectedText = window.getSelection().toString();\n\t }\n\t\n\t return selectedText;\n\t };\n\t\n\t /** Opens buttons block */\n\t inline.showButtons = function () {\n\t\n\t var buttons = editor.nodes.inlineToolbar.buttons;\n\t\n\t buttons.classList.add('opened');\n\t\n\t editor.toolbar.inline.buttonsOpened = true;\n\t\n\t /** highlight buttons */\n\t editor.nodes.inlineToolbar.buttons.childNodes.forEach(editor.toolbar.inline.hightlight);\n\t };\n\t\n\t /** Makes buttons disappear */\n\t inline.closeButtons = function () {\n\t\n\t var buttons = editor.nodes.inlineToolbar.buttons;\n\t\n\t buttons.classList.remove('opened');\n\t\n\t editor.toolbar.inline.buttonsOpened = false;\n\t };\n\t\n\t /** Open buttons defined action if exist */\n\t inline.showActions = function () {\n\t\n\t var action = editor.nodes.inlineToolbar.actions;\n\t\n\t action.classList.add('opened');\n\t\n\t editor.toolbar.inline.actionsOpened = true;\n\t };\n\t\n\t /** Close actions block */\n\t inline.closeAction = function () {\n\t\n\t var action = editor.nodes.inlineToolbar.actions;\n\t\n\t action.innerHTML = '';\n\t action.classList.remove('opened');\n\t editor.toolbar.inline.actionsOpened = false;\n\t };\n\t\n\t /**\n\t * Callback for keydowns in inline toolbar \"Insert link...\" input\n\t */\n\t var inlineToolbarAnchorInputKeydown_ = function inlineToolbarAnchorInputKeydown_(event) {\n\t\n\t if (event.keyCode != editor.core.keys.ENTER) {\n\t\n\t return;\n\t }\n\t\n\t var editable = editor.content.currentNode,\n\t storedSelection = editor.toolbar.inline.storedSelection;\n\t\n\t editor.toolbar.inline.restoreSelection(editable, storedSelection);\n\t editor.toolbar.inline.setAnchor(this.value);\n\t\n\t /**\n\t * Preventing events that will be able to happen\n\t */\n\t event.preventDefault();\n\t event.stopImmediatePropagation();\n\t\n\t editor.toolbar.inline.clearRange();\n\t };\n\t\n\t /** Action for link creation or for setting anchor */\n\t inline.createLinkAction = function (event) {\n\t\n\t var isActive = this.isLinkActive();\n\t\n\t var editable = editor.content.currentNode,\n\t storedSelection = editor.toolbar.inline.saveSelection(editable);\n\t\n\t /** Save globally selection */\n\t editor.toolbar.inline.storedSelection = storedSelection;\n\t\n\t if (isActive) {\n\t\n\t /**\n\t * Changing stored selection. if we want to remove anchor from word\n\t * we should remove anchor from whole word, not only selected part.\n\t * The solution is than we get the length of current link\n\t * Change start position to - end of selection minus length of anchor\n\t */\n\t editor.toolbar.inline.restoreSelection(editable, storedSelection);\n\t\n\t editor.toolbar.inline.defaultToolAction('unlink');\n\t } else {\n\t\n\t /** Create input and close buttons */\n\t var action = editor.draw.inputForLink();\n\t\n\t editor.nodes.inlineToolbar.actions.appendChild(action);\n\t\n\t editor.toolbar.inline.closeButtons();\n\t editor.toolbar.inline.showActions();\n\t\n\t /**\n\t * focus to input\n\t * Solution: https://developer.mozilla.org/ru/docs/Web/API/HTMLElement/focus\n\t * Prevents event after showing input and when we need to focus an input which is in unexisted form\n\t */\n\t action.focus();\n\t event.preventDefault();\n\t\n\t /** Callback to link action */\n\t editor.listeners.add(action, 'keydown', inlineToolbarAnchorInputKeydown_, false);\n\t }\n\t };\n\t\n\t inline.isLinkActive = function () {\n\t\n\t var isActive = false;\n\t\n\t editor.nodes.inlineToolbar.buttons.childNodes.forEach(function (tool) {\n\t\n\t var dataType = tool.dataset.type;\n\t\n\t if (dataType == 'link' && tool.classList.contains('hightlighted')) {\n\t\n\t isActive = true;\n\t }\n\t });\n\t\n\t return isActive;\n\t };\n\t\n\t /** default action behavior of tool */\n\t inline.defaultToolAction = function (type) {\n\t\n\t document.execCommand(type, false, null);\n\t };\n\t\n\t /**\n\t * @private\n\t *\n\t * Sets URL\n\t *\n\t * @param {String} url - URL\n\t */\n\t inline.setAnchor = function (url) {\n\t\n\t document.execCommand('createLink', false, url);\n\t\n\t /** Close after URL inserting */\n\t editor.toolbar.inline.closeAction();\n\t };\n\t\n\t /**\n\t * @private\n\t *\n\t * Saves selection\n\t */\n\t inline.saveSelection = function (containerEl) {\n\t\n\t var range = window.getSelection().getRangeAt(0),\n\t preSelectionRange = range.cloneRange(),\n\t start;\n\t\n\t preSelectionRange.selectNodeContents(containerEl);\n\t preSelectionRange.setEnd(range.startContainer, range.startOffset);\n\t\n\t start = preSelectionRange.toString().length;\n\t\n\t return {\n\t start: start,\n\t end: start + range.toString().length\n\t };\n\t };\n\t\n\t /**\n\t * @private\n\t *\n\t * Sets to previous selection (Range)\n\t *\n\t * @param {Element} containerEl - editable element where we restore range\n\t * @param {Object} savedSel - range basic information to restore\n\t */\n\t inline.restoreSelection = function (containerEl, savedSel) {\n\t\n\t var range = document.createRange(),\n\t charIndex = 0;\n\t\n\t range.setStart(containerEl, 0);\n\t range.collapse(true);\n\t\n\t var nodeStack = [containerEl],\n\t node,\n\t foundStart = false,\n\t stop = false,\n\t nextCharIndex;\n\t\n\t while (!stop && (node = nodeStack.pop())) {\n\t\n\t if (node.nodeType == 3) {\n\t\n\t nextCharIndex = charIndex + node.length;\n\t\n\t if (!foundStart && savedSel.start >= charIndex && savedSel.start <= nextCharIndex) {\n\t\n\t range.setStart(node, savedSel.start - charIndex);\n\t foundStart = true;\n\t }\n\t if (foundStart && savedSel.end >= charIndex && savedSel.end <= nextCharIndex) {\n\t\n\t range.setEnd(node, savedSel.end - charIndex);\n\t stop = true;\n\t }\n\t charIndex = nextCharIndex;\n\t } else {\n\t\n\t var i = node.childNodes.length;\n\t\n\t while (i--) {\n\t\n\t nodeStack.push(node.childNodes[i]);\n\t }\n\t }\n\t }\n\t\n\t var sel = window.getSelection();\n\t\n\t sel.removeAllRanges();\n\t sel.addRange(range);\n\t };\n\t\n\t /**\n\t * @private\n\t *\n\t * Removes all ranges from window selection\n\t */\n\t inline.clearRange = function () {\n\t\n\t var selection = window.getSelection();\n\t\n\t selection.removeAllRanges();\n\t };\n\t\n\t /**\n\t * @private\n\t *\n\t * sets or removes hightlight\n\t */\n\t inline.hightlight = function (tool) {\n\t\n\t var dataType = tool.dataset.type;\n\t\n\t if (document.queryCommandState(dataType)) {\n\t\n\t editor.toolbar.inline.setButtonHighlighted(tool);\n\t } else {\n\t\n\t editor.toolbar.inline.removeButtonsHighLight(tool);\n\t }\n\t\n\t /**\n\t *\n\t * hightlight for anchors\n\t */\n\t var selection = window.getSelection(),\n\t tag = selection.anchorNode.parentNode;\n\t\n\t if (tag.tagName == 'A' && dataType == 'link') {\n\t\n\t editor.toolbar.inline.setButtonHighlighted(tool);\n\t }\n\t };\n\t\n\t /**\n\t * @private\n\t *\n\t * Mark button if text is already executed\n\t */\n\t inline.setButtonHighlighted = function (button) {\n\t\n\t button.classList.add('hightlighted');\n\t\n\t /** At link tool we also change icon */\n\t if (button.dataset.type == 'link') {\n\t\n\t var icon = button.childNodes[0];\n\t\n\t icon.classList.remove('ce-icon-link');\n\t icon.classList.add('ce-icon-unlink');\n\t }\n\t };\n\t\n\t /**\n\t * @private\n\t *\n\t * Removes hightlight\n\t */\n\t inline.removeButtonsHighLight = function (button) {\n\t\n\t button.classList.remove('hightlighted');\n\t\n\t /** At link tool we also change icon */\n\t if (button.dataset.type == 'link') {\n\t\n\t var icon = button.childNodes[0];\n\t\n\t icon.classList.remove('ce-icon-unlink');\n\t icon.classList.add('ce-icon-link');\n\t }\n\t };\n\t\n\t return inline;\n\t}({});\n\n/***/ }),\n/* 11 */\n/***/ (function(module, exports) {\n\n\t'use strict';\n\t\n\t/**\n\t * Codex Editor toolbox\n\t *\n\t * All tools be able to appended here\n\t *\n\t * @author Codex Team\n\t * @version 1.0\n\t */\n\t\n\tmodule.exports = function (toolbox) {\n\t\n\t var editor = codex.editor;\n\t\n\t toolbox.opened = false;\n\t toolbox.openedOnBlock = null;\n\t\n\t /** Shows toolbox */\n\t toolbox.open = function () {\n\t\n\t /** Close setting if toolbox is opened */\n\t if (editor.toolbar.settings.opened) {\n\t\n\t editor.toolbar.settings.close();\n\t }\n\t\n\t /** Add 'toolbar-opened' class for current block **/\n\t toolbox.openedOnBlock = editor.content.currentNode;\n\t toolbox.openedOnBlock.classList.add('toolbar-opened');\n\t\n\t /** display toolbox */\n\t editor.nodes.toolbox.classList.add('opened');\n\t\n\t /** Animate plus button */\n\t editor.nodes.plusButton.classList.add('clicked');\n\t\n\t /** toolbox state */\n\t editor.toolbar.toolbox.opened = true;\n\t };\n\t\n\t /** Closes toolbox */\n\t toolbox.close = function () {\n\t\n\t /** Remove 'toolbar-opened' class from current block **/\n\t if (toolbox.openedOnBlock) toolbox.openedOnBlock.classList.remove('toolbar-opened');\n\t toolbox.openedOnBlock = null;\n\t\n\t /** Makes toolbox disappear */\n\t editor.nodes.toolbox.classList.remove('opened');\n\t\n\t /** Rotate plus button */\n\t editor.nodes.plusButton.classList.remove('clicked');\n\t\n\t /** toolbox state */\n\t editor.toolbar.toolbox.opened = false;\n\t\n\t editor.toolbar.current = null;\n\t };\n\t\n\t toolbox.leaf = function () {\n\t\n\t var currentTool = editor.toolbar.current,\n\t tools = Object.keys(editor.tools),\n\t barButtons = editor.nodes.toolbarButtons,\n\t nextToolIndex = 0,\n\t toolToSelect = void 0,\n\t visibleTool = void 0,\n\t tool = void 0;\n\t\n\t if (!currentTool) {\n\t\n\t /** Get first tool from object*/\n\t for (tool in editor.tools) {\n\t\n\t if (editor.tools[tool].displayInToolbox) {\n\t\n\t break;\n\t }\n\t\n\t nextToolIndex++;\n\t }\n\t } else {\n\t\n\t nextToolIndex = (tools.indexOf(currentTool) + 1) % tools.length;\n\t visibleTool = tools[nextToolIndex];\n\t\n\t while (!editor.tools[visibleTool].displayInToolbox) {\n\t\n\t nextToolIndex = (nextToolIndex + 1) % tools.length;\n\t visibleTool = tools[nextToolIndex];\n\t }\n\t }\n\t\n\t toolToSelect = tools[nextToolIndex];\n\t\n\t for (var button in barButtons) {\n\t\n\t barButtons[button].classList.remove('selected');\n\t }\n\t\n\t barButtons[toolToSelect].classList.add('selected');\n\t editor.toolbar.current = toolToSelect;\n\t };\n\t\n\t /**\n\t * Transforming selected node type into selected toolbar element type\n\t * @param {event} event\n\t */\n\t toolbox.toolClicked = function (event) {\n\t\n\t /**\n\t * UNREPLACEBLE_TOOLS this types of tools are forbidden to replace even they are empty\n\t */\n\t var UNREPLACEBLE_TOOLS = ['image', 'link', 'list', 'instagram', 'twitter', 'embed'],\n\t tool = editor.tools[editor.toolbar.current],\n\t workingNode = editor.content.currentNode,\n\t currentInputIndex = editor.caret.inputIndex,\n\t newBlockContent,\n\t appendCallback,\n\t blockData;\n\t\n\t /** Make block from plugin */\n\t newBlockContent = tool.render();\n\t\n\t /** information about block */\n\t blockData = {\n\t block: newBlockContent,\n\t type: tool.type,\n\t stretched: false\n\t };\n\t\n\t if (workingNode && UNREPLACEBLE_TOOLS.indexOf(workingNode.dataset.tool) === -1 && workingNode.textContent.trim() === '') {\n\t\n\t /** Replace current block */\n\t editor.content.switchBlock(workingNode, newBlockContent, tool.type);\n\t } else {\n\t\n\t /** Insert new Block from plugin */\n\t editor.content.insertBlock(blockData);\n\t\n\t /** increase input index */\n\t currentInputIndex++;\n\t }\n\t\n\t /** Fire tool append callback */\n\t appendCallback = tool.appendCallback;\n\t\n\t if (appendCallback && typeof appendCallback == 'function') {\n\t\n\t appendCallback.call(event);\n\t }\n\t\n\t window.setTimeout(function () {\n\t\n\t /** Set caret to current block */\n\t editor.caret.setToBlock(currentInputIndex);\n\t }, 10);\n\t\n\t /**\n\t * Changing current Node\n\t */\n\t editor.content.workingNodeChanged();\n\t\n\t /**\n\t * Move toolbar when node is changed\n\t */\n\t editor.toolbar.move();\n\t };\n\t\n\t return toolbox;\n\t}({});\n\n/***/ }),\n/* 12 */\n/***/ (function(module, exports) {\n\n\t'use strict';\n\t\n\t/**\n\t * @module Codex Editor Callbacks module\n\t * @description Module works with editor added Elements\n\t *\n\t * @author Codex Team\n\t * @version 1.4.0\n\t */\n\t\n\tmodule.exports = function (callbacks) {\n\t\n\t var editor = codex.editor;\n\t\n\t /**\n\t * used by UI module\n\t * @description Routes all keydowns on document\n\t * @param {Object} event\n\t */\n\t callbacks.globalKeydown = function (event) {\n\t\n\t switch (event.keyCode) {\n\t case editor.core.keys.ENTER:\n\t enterKeyPressed_(event);break;\n\t }\n\t };\n\t\n\t /**\n\t * used by UI module\n\t * @description Routes all keydowns on redactors area\n\t * @param {Object} event\n\t */\n\t callbacks.redactorKeyDown = function (event) {\n\t\n\t switch (event.keyCode) {\n\t case editor.core.keys.TAB:\n\t tabKeyPressedOnRedactorsZone_(event);break;\n\t case editor.core.keys.ENTER:\n\t enterKeyPressedOnRedactorsZone_(event);break;\n\t case editor.core.keys.ESC:\n\t escapeKeyPressedOnRedactorsZone_(event);break;\n\t default:\n\t defaultKeyPressedOnRedactorsZone_(event);break;\n\t }\n\t };\n\t\n\t /**\n\t * used by UI module\n\t * @description Routes all keyup events\n\t * @param {Object} event\n\t */\n\t callbacks.globalKeyup = function (event) {\n\t\n\t switch (event.keyCode) {\n\t case editor.core.keys.UP:\n\t case editor.core.keys.LEFT:\n\t case editor.core.keys.RIGHT:\n\t case editor.core.keys.DOWN:\n\t arrowKeyPressed_(event);break;\n\t }\n\t };\n\t\n\t /**\n\t * @param {Object} event\n\t * @private\n\t *\n\t * Handles behaviour when tab pressed\n\t * @description if Content is empty show toolbox (if it is closed) or leaf tools\n\t * uses Toolbars toolbox module to handle the situation\n\t */\n\t var tabKeyPressedOnRedactorsZone_ = function tabKeyPressedOnRedactorsZone_(event) {\n\t\n\t /**\n\t * Wait for solution. Would like to know the behaviour\n\t * @todo Add spaces\n\t */\n\t event.preventDefault();\n\t\n\t if (!editor.core.isBlockEmpty(editor.content.currentNode)) {\n\t\n\t return;\n\t }\n\t\n\t if (!editor.toolbar.opened) {\n\t\n\t editor.toolbar.open();\n\t }\n\t\n\t if (editor.toolbar.opened && !editor.toolbar.toolbox.opened) {\n\t\n\t editor.toolbar.toolbox.open();\n\t } else {\n\t\n\t editor.toolbar.toolbox.leaf();\n\t }\n\t };\n\t\n\t /**\n\t * Handles global EnterKey Press\n\t * @see enterPressedOnBlock_\n\t * @param {Object} event\n\t */\n\t var enterKeyPressed_ = function enterKeyPressed_() {\n\t\n\t if (editor.content.editorAreaHightlighted) {\n\t\n\t /**\n\t * it means that we lose input index, saved index before is not correct\n\t * therefore we need to set caret when we insert new block\n\t */\n\t editor.caret.inputIndex = -1;\n\t\n\t enterPressedOnBlock_();\n\t }\n\t };\n\t\n\t /**\n\t * Callback for enter key pressing in first-level block area\n\t *\n\t * @param {Event} event\n\t * @private\n\t *\n\t * @description Inserts new block with initial type from settings\n\t */\n\t var enterPressedOnBlock_ = function enterPressedOnBlock_() {\n\t\n\t var NEW_BLOCK_TYPE = editor.settings.initialBlockPlugin;\n\t\n\t editor.content.insertBlock({\n\t type: NEW_BLOCK_TYPE,\n\t block: editor.tools[NEW_BLOCK_TYPE].render()\n\t }, true);\n\t\n\t editor.toolbar.move();\n\t editor.toolbar.open();\n\t };\n\t\n\t /**\n\t * ENTER key handler\n\t *\n\t * @param {Object} event\n\t * @private\n\t *\n\t * @description Makes new block with initial type from settings\n\t */\n\t var enterKeyPressedOnRedactorsZone_ = function enterKeyPressedOnRedactorsZone_(event) {\n\t\n\t if (event.target.contentEditable == 'true') {\n\t\n\t /** Update input index */\n\t editor.caret.saveCurrentInputIndex();\n\t }\n\t\n\t var currentInputIndex = editor.caret.getCurrentInputIndex() || 0,\n\t workingNode = editor.content.currentNode,\n\t tool = workingNode.dataset.tool,\n\t isEnterPressedOnToolbar = editor.toolbar.opened && editor.toolbar.current && event.target == editor.state.inputs[currentInputIndex];\n\t\n\t /** The list of tools which needs the default browser behaviour */\n\t var enableLineBreaks = editor.tools[tool].enableLineBreaks;\n\t\n\t /** This type of block creates when enter is pressed */\n\t var NEW_BLOCK_TYPE = editor.settings.initialBlockPlugin;\n\t\n\t /**\n\t * When toolbar is opened, select tool instead of making new paragraph\n\t */\n\t if (isEnterPressedOnToolbar) {\n\t\n\t event.preventDefault();\n\t\n\t editor.toolbar.toolbox.toolClicked(event);\n\t\n\t editor.toolbar.close();\n\t\n\t /**\n\t * Stop other listeners callback executions\n\t */\n\t event.stopPropagation();\n\t event.stopImmediatePropagation();\n\t\n\t return;\n\t }\n\t\n\t /**\n\t * Allow paragraph lineBreaks with shift enter\n\t * Or if shiftkey pressed and enter and enabledLineBreaks, the let new block creation\n\t */\n\t if (event.shiftKey || enableLineBreaks) {\n\t\n\t event.stopPropagation();\n\t event.stopImmediatePropagation();\n\t return;\n\t }\n\t\n\t var currentSelection = window.getSelection(),\n\t currentSelectedNode = currentSelection.anchorNode,\n\t caretAtTheEndOfText = editor.caret.position.atTheEnd(),\n\t isTextNodeHasParentBetweenContenteditable = false;\n\t\n\t /**\n\t * Allow making new
in same block by SHIFT+ENTER and forbids to prevent default browser behaviour\n\t */\n\t if (event.shiftKey && !enableLineBreaks) {\n\t\n\t editor.callback.enterPressedOnBlock(editor.content.currentBlock, event);\n\t event.preventDefault();\n\t return;\n\t }\n\t\n\t /**\n\t * Workaround situation when caret at the Text node that has some wrapper Elements\n\t * Split block cant handle this.\n\t * We need to save default behavior\n\t */\n\t isTextNodeHasParentBetweenContenteditable = currentSelectedNode && currentSelectedNode.parentNode.contentEditable != 'true';\n\t\n\t /**\n\t * Split blocks when input has several nodes and caret placed in textNode\n\t */\n\t if (currentSelectedNode.nodeType == editor.core.nodeTypes.TEXT && !isTextNodeHasParentBetweenContenteditable && !caretAtTheEndOfText) {\n\t\n\t event.preventDefault();\n\t\n\t editor.core.log('Splitting Text node...');\n\t\n\t editor.content.splitBlock(currentInputIndex);\n\t\n\t /** Show plus button when next input after split is empty*/\n\t if (!editor.state.inputs[currentInputIndex + 1].textContent.trim()) {\n\t\n\t editor.toolbar.showPlusButton();\n\t }\n\t } else {\n\t\n\t var islastNode = editor.content.isLastNode(currentSelectedNode);\n\t\n\t if (islastNode && caretAtTheEndOfText) {\n\t\n\t event.preventDefault();\n\t event.stopPropagation();\n\t event.stopImmediatePropagation();\n\t\n\t editor.core.log('ENTER clicked in last textNode. Create new BLOCK');\n\t\n\t editor.content.insertBlock({\n\t type: NEW_BLOCK_TYPE,\n\t block: editor.tools[NEW_BLOCK_TYPE].render()\n\t }, true);\n\t\n\t editor.toolbar.move();\n\t editor.toolbar.open();\n\t\n\t /** Show plus button with empty block */\n\t editor.toolbar.showPlusButton();\n\t }\n\t }\n\t\n\t /** get all inputs after new appending block */\n\t editor.ui.saveInputs();\n\t };\n\t\n\t /**\n\t * Escape behaviour\n\t * @param event\n\t * @private\n\t *\n\t * @description Closes toolbox and toolbar. Prevents default behaviour\n\t */\n\t var escapeKeyPressedOnRedactorsZone_ = function escapeKeyPressedOnRedactorsZone_(event) {\n\t\n\t /** Close all toolbar */\n\t editor.toolbar.close();\n\t\n\t /** Close toolbox */\n\t editor.toolbar.toolbox.close();\n\t\n\t event.preventDefault();\n\t };\n\t\n\t /**\n\t * @param {Event} event\n\t * @private\n\t *\n\t * closes and moves toolbar\n\t */\n\t var arrowKeyPressed_ = function arrowKeyPressed_(event) {\n\t\n\t editor.content.workingNodeChanged();\n\t\n\t /* Closing toolbar */\n\t editor.toolbar.close();\n\t editor.toolbar.move();\n\t };\n\t\n\t /**\n\t * @private\n\t * @param {Event} event\n\t *\n\t * @description Closes all opened bars from toolbar.\n\t * If block is mark, clears highlightning\n\t */\n\t var defaultKeyPressedOnRedactorsZone_ = function defaultKeyPressedOnRedactorsZone_() {\n\t\n\t editor.toolbar.close();\n\t\n\t if (!editor.toolbar.inline.actionsOpened) {\n\t\n\t editor.toolbar.inline.close();\n\t editor.content.clearMark();\n\t }\n\t };\n\t\n\t /**\n\t * Handler when clicked on redactors area\n\t *\n\t * @protected\n\t * @param event\n\t *\n\t * @description Detects clicked area. If it is first-level block area, marks as detected and\n\t * on next enter press will be inserted new block\n\t * Otherwise, save carets position (input index) and put caret to the editable zone.\n\t *\n\t * @see detectWhenClickedOnFirstLevelBlockArea_\n\t *\n\t */\n\t callbacks.redactorClicked = function (event) {\n\t\n\t detectWhenClickedOnFirstLevelBlockArea_();\n\t\n\t editor.content.workingNodeChanged(event.target);\n\t editor.ui.saveInputs();\n\t\n\t var selectedText = editor.toolbar.inline.getSelectionText(),\n\t firstLevelBlock;\n\t\n\t /** If selection range took off, then we hide inline toolbar */\n\t if (selectedText.length === 0) {\n\t\n\t editor.toolbar.inline.close();\n\t }\n\t\n\t /** Update current input index in memory when caret focused into existed input */\n\t if (event.target.contentEditable == 'true') {\n\t\n\t editor.caret.saveCurrentInputIndex();\n\t }\n\t\n\t if (editor.content.currentNode === null) {\n\t\n\t /**\n\t * If inputs in redactor does not exits, then we put input index 0 not -1\n\t */\n\t var indexOfLastInput = editor.state.inputs.length > 0 ? editor.state.inputs.length - 1 : 0;\n\t\n\t /** If we have any inputs */\n\t if (editor.state.inputs.length) {\n\t\n\t /** getting firstlevel parent of input */\n\t firstLevelBlock = editor.content.getFirstLevelBlock(editor.state.inputs[indexOfLastInput]);\n\t }\n\t\n\t /** If input is empty, then we set caret to the last input */\n\t if (editor.state.inputs.length && editor.state.inputs[indexOfLastInput].textContent === '' && firstLevelBlock.dataset.tool == editor.settings.initialBlockPlugin) {\n\t\n\t editor.caret.setToBlock(indexOfLastInput);\n\t } else {\n\t\n\t /** Create new input when caret clicked in redactors area */\n\t var NEW_BLOCK_TYPE = editor.settings.initialBlockPlugin;\n\t\n\t editor.content.insertBlock({\n\t type: NEW_BLOCK_TYPE,\n\t block: editor.tools[NEW_BLOCK_TYPE].render()\n\t });\n\t\n\t /** If there is no inputs except inserted */\n\t if (editor.state.inputs.length === 1) {\n\t\n\t editor.caret.setToBlock(indexOfLastInput);\n\t } else {\n\t\n\t /** Set caret to this appended input */\n\t editor.caret.setToNextBlock(indexOfLastInput);\n\t }\n\t }\n\t } else {\n\t\n\t /** Close all panels */\n\t editor.toolbar.settings.close();\n\t editor.toolbar.toolbox.close();\n\t }\n\t\n\t /**\n\t * Move toolbar and open\n\t */\n\t editor.toolbar.move();\n\t editor.toolbar.open();\n\t\n\t var inputIsEmpty = !editor.content.currentNode.textContent.trim(),\n\t currentNodeType = editor.content.currentNode.dataset.tool,\n\t isInitialType = currentNodeType == editor.settings.initialBlockPlugin;\n\t\n\t /** Hide plus buttons */\n\t editor.toolbar.hidePlusButton();\n\t\n\t if (!inputIsEmpty) {\n\t\n\t /** Mark current block */\n\t editor.content.markBlock();\n\t }\n\t\n\t if (isInitialType && inputIsEmpty) {\n\t\n\t /** Show plus button */\n\t editor.toolbar.showPlusButton();\n\t }\n\t };\n\t\n\t /**\n\t * This method allows to define, is caret in contenteditable element or not.\n\t *\n\t * @private\n\t *\n\t * @description Otherwise, if we get TEXT node from range container, that will means we have input index.\n\t * In this case we use default browsers behaviour (if plugin allows that) or overwritten action.\n\t * Therefore, to be sure that we've clicked first-level block area, we should have currentNode, which always\n\t * specifies to the first-level block. Other cases we just ignore.\n\t */\n\t var detectWhenClickedOnFirstLevelBlockArea_ = function detectWhenClickedOnFirstLevelBlockArea_() {\n\t\n\t var selection = window.getSelection(),\n\t anchorNode = selection.anchorNode,\n\t flag = false;\n\t\n\t if (selection.rangeCount === 0) {\n\t\n\t editor.content.editorAreaHightlighted = true;\n\t } else {\n\t\n\t if (!editor.core.isDomNode(anchorNode)) {\n\t\n\t anchorNode = anchorNode.parentNode;\n\t }\n\t\n\t /** Already founded, without loop */\n\t if (anchorNode.contentEditable == 'true') {\n\t\n\t flag = true;\n\t }\n\t\n\t while (anchorNode.contentEditable != 'true') {\n\t\n\t anchorNode = anchorNode.parentNode;\n\t\n\t if (anchorNode.contentEditable == 'true') {\n\t\n\t flag = true;\n\t }\n\t\n\t if (anchorNode == document.body) {\n\t\n\t break;\n\t }\n\t }\n\t\n\t /** If editable element founded, flag is \"TRUE\", Therefore we return \"FALSE\" */\n\t editor.content.editorAreaHightlighted = !flag;\n\t }\n\t };\n\t\n\t /**\n\t * Toolbar button click handler\n\t *\n\t * @param {Object} event - cursor to the button\n\t * @protected\n\t *\n\t * @description gets current tool and calls render method\n\t */\n\t callbacks.toolbarButtonClicked = function (event) {\n\t\n\t var button = this;\n\t\n\t editor.toolbar.current = button.dataset.type;\n\t\n\t editor.toolbar.toolbox.toolClicked(event);\n\t editor.toolbar.close();\n\t };\n\t\n\t /**\n\t * Show or Hide toolbox when plus button is clicked\n\t */\n\t callbacks.plusButtonClicked = function () {\n\t\n\t if (!editor.nodes.toolbox.classList.contains('opened')) {\n\t\n\t editor.toolbar.toolbox.open();\n\t } else {\n\t\n\t editor.toolbar.toolbox.close();\n\t }\n\t };\n\t\n\t /**\n\t * Block handlers for KeyDown events\n\t *\n\t * @protected\n\t * @param {Object} event\n\t *\n\t * Handles keydowns on block\n\t * @see blockRightOrDownArrowPressed_\n\t * @see backspacePressed_\n\t * @see blockLeftOrUpArrowPressed_\n\t */\n\t callbacks.blockKeydown = function (event) {\n\t\n\t var block = event.target; // event.target is input\n\t\n\t switch (event.keyCode) {\n\t\n\t case editor.core.keys.DOWN:\n\t case editor.core.keys.RIGHT:\n\t blockRightOrDownArrowPressed_(event);\n\t break;\n\t\n\t case editor.core.keys.BACKSPACE:\n\t backspacePressed_(block, event);\n\t break;\n\t\n\t case editor.core.keys.UP:\n\t case editor.core.keys.LEFT:\n\t blockLeftOrUpArrowPressed_(event);\n\t break;\n\t\n\t }\n\t };\n\t\n\t /**\n\t * RIGHT or DOWN keydowns on block\n\t *\n\t * @param {Object} event\n\t * @private\n\t *\n\t * @description watches the selection and gets closest editable element.\n\t * Uses method getDeepestTextNodeFromPosition to get the last node of next block\n\t * Sets caret if it is contenteditable\n\t */\n\t var blockRightOrDownArrowPressed_ = function blockRightOrDownArrowPressed_(event) {\n\t\n\t var selection = window.getSelection(),\n\t inputs = editor.state.inputs,\n\t focusedNode = selection.anchorNode,\n\t focusedNodeHolder;\n\t\n\t /** Check for caret existance */\n\t if (!focusedNode) {\n\t\n\t return false;\n\t }\n\t\n\t /** Looking for closest (parent) contentEditable element of focused node */\n\t while (focusedNode.contentEditable != 'true') {\n\t\n\t focusedNodeHolder = focusedNode.parentNode;\n\t focusedNode = focusedNodeHolder;\n\t }\n\t\n\t /** Input index in DOM level */\n\t var editableElementIndex = 0;\n\t\n\t while (focusedNode != inputs[editableElementIndex]) {\n\t\n\t editableElementIndex++;\n\t }\n\t\n\t /**\n\t * Founded contentEditable element doesn't have childs\n\t * Or maybe New created block\n\t */\n\t if (!focusedNode.textContent) {\n\t\n\t editor.caret.setToNextBlock(editableElementIndex);\n\t return;\n\t }\n\t\n\t /**\n\t * Do nothing when caret doesn not reaches the end of last child\n\t */\n\t var caretInLastChild = false,\n\t caretAtTheEndOfText = false;\n\t\n\t var lastChild, deepestTextnode;\n\t\n\t lastChild = focusedNode.childNodes[focusedNode.childNodes.length - 1];\n\t\n\t if (editor.core.isDomNode(lastChild)) {\n\t\n\t deepestTextnode = editor.content.getDeepestTextNodeFromPosition(lastChild, lastChild.childNodes.length);\n\t } else {\n\t\n\t deepestTextnode = lastChild;\n\t }\n\t\n\t caretInLastChild = selection.anchorNode == deepestTextnode;\n\t caretAtTheEndOfText = deepestTextnode.length == selection.anchorOffset;\n\t\n\t if (!caretInLastChild || !caretAtTheEndOfText) {\n\t\n\t editor.core.log('arrow [down|right] : caret does not reached the end');\n\t return false;\n\t }\n\t\n\t editor.caret.setToNextBlock(editableElementIndex);\n\t };\n\t\n\t /**\n\t * LEFT or UP keydowns on block\n\t *\n\t * @param {Object} event\n\t * @private\n\t *\n\t * watches the selection and gets closest editable element.\n\t * Uses method getDeepestTextNodeFromPosition to get the last node of previous block\n\t * Sets caret if it is contenteditable\n\t *\n\t */\n\t var blockLeftOrUpArrowPressed_ = function blockLeftOrUpArrowPressed_(event) {\n\t\n\t var selection = window.getSelection(),\n\t inputs = editor.state.inputs,\n\t focusedNode = selection.anchorNode,\n\t focusedNodeHolder;\n\t\n\t /** Check for caret existance */\n\t if (!focusedNode) {\n\t\n\t return false;\n\t }\n\t\n\t /**\n\t * LEFT or UP not at the beginning\n\t */\n\t if (selection.anchorOffset !== 0) {\n\t\n\t return false;\n\t }\n\t\n\t /** Looking for parent contentEditable block */\n\t while (focusedNode.contentEditable != 'true') {\n\t\n\t focusedNodeHolder = focusedNode.parentNode;\n\t focusedNode = focusedNodeHolder;\n\t }\n\t\n\t /** Input index in DOM level */\n\t var editableElementIndex = 0;\n\t\n\t while (focusedNode != inputs[editableElementIndex]) {\n\t\n\t editableElementIndex++;\n\t }\n\t\n\t /**\n\t * Do nothing if caret is not at the beginning of first child\n\t */\n\t var caretInFirstChild = false,\n\t caretAtTheBeginning = false;\n\t\n\t var firstChild, deepestTextnode;\n\t\n\t /**\n\t * Founded contentEditable element doesn't have childs\n\t * Or maybe New created block\n\t */\n\t if (!focusedNode.textContent) {\n\t\n\t editor.caret.setToPreviousBlock(editableElementIndex);\n\t return;\n\t }\n\t\n\t firstChild = focusedNode.childNodes[0];\n\t\n\t if (editor.core.isDomNode(firstChild)) {\n\t\n\t deepestTextnode = editor.content.getDeepestTextNodeFromPosition(firstChild, 0);\n\t } else {\n\t\n\t deepestTextnode = firstChild;\n\t }\n\t\n\t caretInFirstChild = selection.anchorNode == deepestTextnode;\n\t caretAtTheBeginning = selection.anchorOffset === 0;\n\t\n\t if (caretInFirstChild && caretAtTheBeginning) {\n\t\n\t editor.caret.setToPreviousBlock(editableElementIndex);\n\t }\n\t };\n\t\n\t /**\n\t * Handles backspace keydown\n\t *\n\t * @param {Element} block\n\t * @param {Object} event\n\t * @private\n\t *\n\t * @description if block is empty, delete the block and set caret to the previous block\n\t * If block is not empty, try to merge two blocks - current and previous\n\t * But it we try'n to remove first block, then we should set caret to the next block, not previous.\n\t * If we removed the last block, create new one\n\t */\n\t var backspacePressed_ = function backspacePressed_(block, event) {\n\t\n\t var currentInputIndex = editor.caret.getCurrentInputIndex(),\n\t range,\n\t selectionLength,\n\t firstLevelBlocksCount;\n\t\n\t if (editor.core.isNativeInput(event.target)) {\n\t\n\t /** If input value is empty - remove block */\n\t if (event.target.value.trim() == '') {\n\t\n\t block.remove();\n\t } else {\n\t\n\t return;\n\t }\n\t }\n\t\n\t if (block.textContent.trim()) {\n\t\n\t range = editor.content.getRange();\n\t selectionLength = range.endOffset - range.startOffset;\n\t\n\t if (editor.caret.position.atStart() && !selectionLength && editor.state.inputs[currentInputIndex - 1]) {\n\t\n\t editor.content.mergeBlocks(currentInputIndex);\n\t } else {\n\t\n\t return;\n\t }\n\t }\n\t\n\t if (!selectionLength) {\n\t\n\t block.remove();\n\t }\n\t\n\t firstLevelBlocksCount = editor.nodes.redactor.childNodes.length;\n\t\n\t /**\n\t * If all blocks are removed\n\t */\n\t if (firstLevelBlocksCount === 0) {\n\t\n\t /** update currentNode variable */\n\t editor.content.currentNode = null;\n\t\n\t /** Inserting new empty initial block */\n\t editor.ui.addInitialBlock();\n\t\n\t /** Updating inputs state after deleting last block */\n\t editor.ui.saveInputs();\n\t\n\t /** Set to current appended block */\n\t window.setTimeout(function () {\n\t\n\t editor.caret.setToPreviousBlock(1);\n\t }, 10);\n\t } else {\n\t\n\t if (editor.caret.inputIndex !== 0) {\n\t\n\t /** Target block is not first */\n\t editor.caret.setToPreviousBlock(editor.caret.inputIndex);\n\t } else {\n\t\n\t /** If we try to delete first block */\n\t editor.caret.setToNextBlock(editor.caret.inputIndex);\n\t }\n\t }\n\t\n\t editor.toolbar.move();\n\t\n\t if (!editor.toolbar.opened) {\n\t\n\t editor.toolbar.open();\n\t }\n\t\n\t /** Updating inputs state */\n\t editor.ui.saveInputs();\n\t\n\t /** Prevent default browser behaviour */\n\t event.preventDefault();\n\t };\n\t\n\t /**\n\t * used by UI module\n\t * Clicks on block settings button\n\t *\n\t * @param {Object} event\n\t * @protected\n\t * @description Opens toolbar settings\n\t */\n\t callbacks.showSettingsButtonClicked = function (event) {\n\t\n\t /**\n\t * Get type of current block\n\t * It uses to append settings from tool.settings property.\n\t * ...\n\t * Type is stored in data-type attribute on block\n\t */\n\t var currentToolType = editor.content.currentNode.dataset.tool;\n\t\n\t editor.toolbar.settings.toggle(currentToolType);\n\t\n\t /** Close toolbox when settings button is active */\n\t editor.toolbar.toolbox.close();\n\t editor.toolbar.settings.hideRemoveActions();\n\t };\n\t\n\t return callbacks;\n\t}({});\n\n/***/ }),\n/* 13 */\n/***/ (function(module, exports) {\n\n\t'use strict';\n\t\n\t/**\n\t * Codex Editor Draw module\n\t *\n\t * @author Codex Team\n\t * @version 1.0.\n\t */\n\t\n\tmodule.exports = function (draw) {\n\t\n\t /**\n\t * Base editor wrapper\n\t */\n\t draw.wrapper = function () {\n\t\n\t var wrapper = document.createElement('div');\n\t\n\t wrapper.className += 'codex-editor';\n\t\n\t return wrapper;\n\t };\n\t\n\t /**\n\t * Content-editable holder\n\t */\n\t draw.redactor = function () {\n\t\n\t var redactor = document.createElement('div');\n\t\n\t redactor.className += 'ce-redactor';\n\t\n\t return redactor;\n\t };\n\t\n\t draw.ceBlock = function () {\n\t\n\t var block = document.createElement('DIV');\n\t\n\t block.className += 'ce_block';\n\t\n\t return block;\n\t };\n\t\n\t /**\n\t * Empty toolbar with toggler\n\t */\n\t draw.toolbar = function () {\n\t\n\t var bar = document.createElement('div');\n\t\n\t bar.className += 'ce-toolbar';\n\t\n\t return bar;\n\t };\n\t\n\t draw.toolbarContent = function () {\n\t\n\t var wrapper = document.createElement('DIV');\n\t\n\t wrapper.classList.add('ce-toolbar__content');\n\t\n\t return wrapper;\n\t };\n\t\n\t /**\n\t * Inline toolbar\n\t */\n\t draw.inlineToolbar = function () {\n\t\n\t var bar = document.createElement('DIV');\n\t\n\t bar.className += 'ce-toolbar-inline';\n\t\n\t return bar;\n\t };\n\t\n\t /**\n\t * Wrapper for inline toobar buttons\n\t */\n\t draw.inlineToolbarButtons = function () {\n\t\n\t var wrapper = document.createElement('DIV');\n\t\n\t wrapper.className += 'ce-toolbar-inline__buttons';\n\t\n\t return wrapper;\n\t };\n\t\n\t /**\n\t * For some actions\n\t */\n\t draw.inlineToolbarActions = function () {\n\t\n\t var wrapper = document.createElement('DIV');\n\t\n\t wrapper.className += 'ce-toolbar-inline__actions';\n\t\n\t return wrapper;\n\t };\n\t\n\t draw.inputForLink = function () {\n\t\n\t var input = document.createElement('INPUT');\n\t\n\t input.type = 'input';\n\t input.className += 'inputForLink';\n\t input.placeholder = 'Вставьте ссылку ...';\n\t input.setAttribute('form', 'defaultForm');\n\t\n\t input.setAttribute('autofocus', 'autofocus');\n\t\n\t return input;\n\t };\n\t\n\t /**\n\t * @todo Desc\n\t */\n\t draw.blockButtons = function () {\n\t\n\t var block = document.createElement('div');\n\t\n\t block.className += 'ce-toolbar__actions';\n\t\n\t return block;\n\t };\n\t\n\t /**\n\t * Block settings panel\n\t */\n\t draw.blockSettings = function () {\n\t\n\t var settings = document.createElement('div');\n\t\n\t settings.className += 'ce-settings';\n\t\n\t return settings;\n\t };\n\t\n\t draw.defaultSettings = function () {\n\t\n\t var div = document.createElement('div');\n\t\n\t div.classList.add('ce-settings_default');\n\t\n\t return div;\n\t };\n\t\n\t draw.pluginsSettings = function () {\n\t\n\t var div = document.createElement('div');\n\t\n\t div.classList.add('ce-settings_plugin');\n\t\n\t return div;\n\t };\n\t\n\t draw.plusButton = function () {\n\t\n\t var button = document.createElement('span');\n\t\n\t button.className = 'ce-toolbar__plus';\n\t // button.innerHTML = '';\n\t\n\t return button;\n\t };\n\t\n\t /**\n\t * Settings button in toolbar\n\t */\n\t draw.settingsButton = function () {\n\t\n\t var toggler = document.createElement('span');\n\t\n\t toggler.className = 'ce-toolbar__settings-btn';\n\t\n\t /** Toggler button*/\n\t toggler.innerHTML = '';\n\t\n\t return toggler;\n\t };\n\t\n\t /**\n\t * Redactor tools wrapper\n\t */\n\t\n\t draw.toolbox = function () {\n\t\n\t var wrapper = document.createElement('div');\n\t\n\t wrapper.className = 'ce-toolbar__tools';\n\t\n\t return wrapper;\n\t };\n\t\n\t /**\n\t * @protected\n\t *\n\t * Draws tool buttons for toolbox\n\t *\n\t * @param {String} type\n\t * @param {String} classname\n\t * @returns {Element}\n\t */\n\t draw.toolbarButton = function (type, classname) {\n\t\n\t var button = document.createElement('li'),\n\t toolIcon = document.createElement('i'),\n\t toolTitle = document.createElement('span');\n\t\n\t button.dataset.type = type;\n\t button.setAttribute('title', type);\n\t\n\t toolIcon.classList.add(classname);\n\t toolTitle.classList.add('ce_toolbar_tools--title');\n\t\n\t button.appendChild(toolIcon);\n\t button.appendChild(toolTitle);\n\t\n\t return button;\n\t };\n\t\n\t /**\n\t * @protected\n\t *\n\t * Draws tools for inline toolbar\n\t *\n\t * @param {String} type\n\t * @param {String} classname\n\t */\n\t draw.toolbarButtonInline = function (type, classname) {\n\t\n\t var button = document.createElement('BUTTON'),\n\t toolIcon = document.createElement('I');\n\t\n\t button.type = 'button';\n\t button.dataset.type = type;\n\t toolIcon.classList.add(classname);\n\t\n\t button.appendChild(toolIcon);\n\t\n\t return button;\n\t };\n\t\n\t /**\n\t * Redactor block\n\t */\n\t draw.block = function (tagName, content) {\n\t\n\t var node = document.createElement(tagName);\n\t\n\t node.innerHTML = content || '';\n\t\n\t return node;\n\t };\n\t\n\t /**\n\t * Creates Node with passed tagName and className\n\t * @param {string} tagName\n\t * @param {string} className\n\t * @param {object} properties - allow to assign properties\n\t */\n\t draw.node = function (tagName, className, properties) {\n\t\n\t var el = document.createElement(tagName);\n\t\n\t if (className) el.className = className;\n\t\n\t if (properties) {\n\t\n\t for (var name in properties) {\n\t\n\t el[name] = properties[name];\n\t }\n\t }\n\t\n\t return el;\n\t };\n\t\n\t /**\n\t * Unavailable plugin block\n\t */\n\t draw.unavailableBlock = function () {\n\t\n\t var wrapper = document.createElement('DIV');\n\t\n\t wrapper.classList.add('cdx-unavailable-block');\n\t\n\t return wrapper;\n\t };\n\t\n\t return draw;\n\t}({});\n\n/***/ }),\n/* 14 */\n/***/ (function(module, exports) {\n\n\t'use strict';\n\t\n\t/**\n\t * Codex Editor Caret Module\n\t *\n\t * @author Codex Team\n\t * @version 1.0\n\t */\n\t\n\tmodule.exports = function (caret) {\n\t\n\t var editor = codex.editor;\n\t\n\t /**\n\t * @var {int} InputIndex - editable element in DOM\n\t */\n\t caret.inputIndex = null;\n\t\n\t /**\n\t * @var {int} offset - caret position in a text node.\n\t */\n\t caret.offset = null;\n\t\n\t /**\n\t * @var {int} focusedNodeIndex - we get index of child node from first-level block\n\t */\n\t caret.focusedNodeIndex = null;\n\t\n\t /**\n\t * Creates Document Range and sets caret to the element.\n\t * @protected\n\t * @uses caret.save — if you need to save caret position\n\t * @param {Element} el - Changed Node.\n\t */\n\t caret.set = function (el, index, offset) {\n\t\n\t offset = offset || caret.offset || 0;\n\t index = index || caret.focusedNodeIndex || 0;\n\t\n\t var childs = el.childNodes,\n\t nodeToSet;\n\t\n\t if (childs.length === 0) {\n\t\n\t nodeToSet = el;\n\t } else {\n\t\n\t nodeToSet = childs[index];\n\t }\n\t\n\t /** If Element is INPUT */\n\t if (el.contentEditable != 'true') {\n\t\n\t el.focus();\n\t return;\n\t }\n\t\n\t if (editor.core.isDomNode(nodeToSet)) {\n\t\n\t nodeToSet = editor.content.getDeepestTextNodeFromPosition(nodeToSet, nodeToSet.childNodes.length);\n\t }\n\t\n\t var range = document.createRange(),\n\t selection = window.getSelection();\n\t\n\t window.setTimeout(function () {\n\t\n\t range.setStart(nodeToSet, offset);\n\t range.setEnd(nodeToSet, offset);\n\t\n\t selection.removeAllRanges();\n\t selection.addRange(range);\n\t\n\t editor.caret.saveCurrentInputIndex();\n\t }, 20);\n\t };\n\t\n\t /**\n\t * @protected\n\t * Updates index of input and saves it in caret object\n\t */\n\t caret.saveCurrentInputIndex = function () {\n\t\n\t /** Index of Input that we paste sanitized content */\n\t var selection = window.getSelection(),\n\t inputs = editor.state.inputs,\n\t focusedNode = selection.anchorNode,\n\t focusedNodeHolder;\n\t\n\t if (!focusedNode) {\n\t\n\t return;\n\t }\n\t\n\t /** Looking for parent contentEditable block */\n\t while (focusedNode.contentEditable != 'true') {\n\t\n\t focusedNodeHolder = focusedNode.parentNode;\n\t focusedNode = focusedNodeHolder;\n\t }\n\t\n\t /** Input index in DOM level */\n\t var editableElementIndex = 0;\n\t\n\t while (focusedNode != inputs[editableElementIndex]) {\n\t\n\t editableElementIndex++;\n\t }\n\t\n\t caret.inputIndex = editableElementIndex;\n\t };\n\t\n\t /**\n\t * Returns current input index (caret object)\n\t */\n\t caret.getCurrentInputIndex = function () {\n\t\n\t return caret.inputIndex;\n\t };\n\t\n\t /**\n\t * @param {int} index - index of first-level block after that we set caret into next input\n\t */\n\t caret.setToNextBlock = function (index) {\n\t\n\t var inputs = editor.state.inputs,\n\t nextInput = inputs[index + 1];\n\t\n\t if (!nextInput) {\n\t\n\t editor.core.log('We are reached the end');\n\t return;\n\t }\n\t\n\t /**\n\t * When new Block created or deleted content of input\n\t * We should add some text node to set caret\n\t */\n\t if (!nextInput.childNodes.length) {\n\t\n\t var emptyTextElement = document.createTextNode('');\n\t\n\t nextInput.appendChild(emptyTextElement);\n\t }\n\t\n\t editor.caret.inputIndex = index + 1;\n\t editor.caret.set(nextInput, 0, 0);\n\t editor.content.workingNodeChanged(nextInput);\n\t };\n\t\n\t /**\n\t * @param {int} index - index of target input.\n\t * Sets caret to input with this index\n\t */\n\t caret.setToBlock = function (index) {\n\t\n\t var inputs = editor.state.inputs,\n\t targetInput = inputs[index];\n\t\n\t if (!targetInput) {\n\t\n\t return;\n\t }\n\t\n\t /**\n\t * When new Block created or deleted content of input\n\t * We should add some text node to set caret\n\t */\n\t if (!targetInput.childNodes.length) {\n\t\n\t var emptyTextElement = document.createTextNode('');\n\t\n\t targetInput.appendChild(emptyTextElement);\n\t }\n\t\n\t editor.caret.inputIndex = index;\n\t editor.caret.set(targetInput, 0, 0);\n\t editor.content.workingNodeChanged(targetInput);\n\t };\n\t\n\t /**\n\t * @param {int} index - index of input\n\t */\n\t caret.setToPreviousBlock = function (index) {\n\t\n\t index = index || 0;\n\t\n\t var inputs = editor.state.inputs,\n\t previousInput = inputs[index - 1],\n\t lastChildNode,\n\t lengthOfLastChildNode,\n\t emptyTextElement;\n\t\n\t if (!previousInput) {\n\t\n\t editor.core.log('We are reached first node');\n\t return;\n\t }\n\t\n\t lastChildNode = editor.content.getDeepestTextNodeFromPosition(previousInput, previousInput.childNodes.length);\n\t lengthOfLastChildNode = lastChildNode.length;\n\t\n\t /**\n\t * When new Block created or deleted content of input\n\t * We should add some text node to set caret\n\t */\n\t if (!previousInput.childNodes.length) {\n\t\n\t emptyTextElement = document.createTextNode('');\n\t previousInput.appendChild(emptyTextElement);\n\t }\n\t editor.caret.inputIndex = index - 1;\n\t editor.caret.set(previousInput, previousInput.childNodes.length - 1, lengthOfLastChildNode);\n\t editor.content.workingNodeChanged(inputs[index - 1]);\n\t };\n\t\n\t caret.position = {\n\t\n\t atStart: function atStart() {\n\t\n\t var selection = window.getSelection(),\n\t anchorOffset = selection.anchorOffset,\n\t anchorNode = selection.anchorNode,\n\t firstLevelBlock = editor.content.getFirstLevelBlock(anchorNode),\n\t pluginsRender = firstLevelBlock.childNodes[0];\n\t\n\t if (!editor.core.isDomNode(anchorNode)) {\n\t\n\t anchorNode = anchorNode.parentNode;\n\t }\n\t\n\t var isFirstNode = anchorNode === pluginsRender.childNodes[0],\n\t isOffsetZero = anchorOffset === 0;\n\t\n\t return isFirstNode && isOffsetZero;\n\t },\n\t\n\t atTheEnd: function atTheEnd() {\n\t\n\t var selection = window.getSelection(),\n\t anchorOffset = selection.anchorOffset,\n\t anchorNode = selection.anchorNode;\n\t\n\t /** Caret is at the end of input */\n\t return !anchorNode || !anchorNode.length || anchorOffset === anchorNode.length;\n\t }\n\t };\n\t\n\t /**\n\t * Inserts node at the caret location\n\t * @param {HTMLElement|DocumentFragment} node\n\t */\n\t caret.insertNode = function (node) {\n\t\n\t var selection,\n\t range,\n\t lastNode = node;\n\t\n\t if (node.nodeType == editor.core.nodeTypes.DOCUMENT_FRAGMENT) {\n\t\n\t lastNode = node.lastChild;\n\t }\n\t\n\t selection = window.getSelection();\n\t\n\t range = selection.getRangeAt(0);\n\t range.deleteContents();\n\t\n\t range.insertNode(node);\n\t\n\t range.setStartAfter(lastNode);\n\t range.collapse(true);\n\t\n\t selection.removeAllRanges();\n\t selection.addRange(range);\n\t };\n\t\n\t return caret;\n\t}({});\n\n/***/ }),\n/* 15 */\n/***/ (function(module, exports) {\n\n\t'use strict';\n\t\n\t/**\n\t * Codex Editor Notification Module\n\t *\n\t * @author Codex Team\n\t * @version 1.0\n\t */\n\t\n\tmodule.exports = function (notifications) {\n\t\n\t var editor = codex.editor;\n\t\n\t var queue = [];\n\t\n\t var addToQueue = function addToQueue(settings) {\n\t\n\t queue.push(settings);\n\t\n\t var index = 0;\n\t\n\t while (index < queue.length && queue.length > 5) {\n\t\n\t if (queue[index].type == 'confirm' || queue[index].type == 'prompt') {\n\t\n\t index++;\n\t continue;\n\t }\n\t\n\t queue[index].close();\n\t queue.splice(index, 1);\n\t }\n\t };\n\t\n\t notifications.createHolder = function () {\n\t\n\t var holder = editor.draw.node('DIV', 'cdx-notifications-block');\n\t\n\t editor.nodes.notifications = document.body.appendChild(holder);\n\t\n\t return holder;\n\t };\n\t\n\t /**\n\t * Error notificator. Shows block with message\n\t * @protected\n\t */\n\t notifications.errorThrown = function (errorMsg, event) {\n\t\n\t editor.notifications.notification({ message: 'This action is not available currently', type: event.type });\n\t };\n\t\n\t /**\n\t *\n\t * Appends notification\n\t *\n\t * settings = {\n\t * type - notification type (reserved types: alert, confirm, prompt). Just add class 'cdx-notification-'+type\n\t * message - notification message\n\t * okMsg - confirm button text (default - 'Ok')\n\t * cancelBtn - cancel button text (default - 'Cancel'). Only for confirm and prompt types\n\t * confirm - function-handler for ok button click\n\t * cancel - function-handler for cancel button click. Only for confirm and prompt types\n\t * time - time (in seconds) after which notification will close (default - 10s)\n\t * }\n\t *\n\t * @param settings\n\t */\n\t notifications.notification = function (constructorSettings) {\n\t\n\t /** Private vars and methods */\n\t var notification = null,\n\t cancel = null,\n\t type = null,\n\t confirm = null,\n\t inputField = null;\n\t\n\t var confirmHandler = function confirmHandler() {\n\t\n\t close();\n\t\n\t if (typeof confirm !== 'function') {\n\t\n\t return;\n\t }\n\t\n\t if (type == 'prompt') {\n\t\n\t confirm(inputField.value);\n\t return;\n\t }\n\t\n\t confirm();\n\t };\n\t\n\t var cancelHandler = function cancelHandler() {\n\t\n\t close();\n\t\n\t if (typeof cancel !== 'function') {\n\t\n\t return;\n\t }\n\t\n\t cancel();\n\t };\n\t\n\t /** Public methods */\n\t function create(settings) {\n\t\n\t if (!(settings && settings.message)) {\n\t\n\t editor.core.log('Can\\'t create notification. Message is missed');\n\t return;\n\t }\n\t\n\t settings.type = settings.type || 'alert';\n\t settings.time = settings.time * 1000 || 10000;\n\t\n\t var wrapper = editor.draw.node('DIV', 'cdx-notification'),\n\t message = editor.draw.node('DIV', 'cdx-notification__message'),\n\t input = editor.draw.node('INPUT', 'cdx-notification__input'),\n\t okBtn = editor.draw.node('SPAN', 'cdx-notification__ok-btn'),\n\t cancelBtn = editor.draw.node('SPAN', 'cdx-notification__cancel-btn');\n\t\n\t message.textContent = settings.message;\n\t okBtn.textContent = settings.okMsg || 'ОК';\n\t cancelBtn.textContent = settings.cancelMsg || 'Отмена';\n\t\n\t editor.listeners.add(okBtn, 'click', confirmHandler);\n\t editor.listeners.add(cancelBtn, 'click', cancelHandler);\n\t\n\t wrapper.appendChild(message);\n\t\n\t if (settings.type == 'prompt') {\n\t\n\t wrapper.appendChild(input);\n\t }\n\t\n\t wrapper.appendChild(okBtn);\n\t\n\t if (settings.type == 'prompt' || settings.type == 'confirm') {\n\t\n\t wrapper.appendChild(cancelBtn);\n\t }\n\t\n\t wrapper.classList.add('cdx-notification-' + settings.type);\n\t wrapper.dataset.type = settings.type;\n\t\n\t notification = wrapper;\n\t type = settings.type;\n\t confirm = settings.confirm;\n\t cancel = settings.cancel;\n\t inputField = input;\n\t\n\t if (settings.type != 'prompt' && settings.type != 'confirm') {\n\t\n\t window.setTimeout(close, settings.time);\n\t }\n\t };\n\t\n\t /**\n\t * Show notification block\n\t */\n\t function send() {\n\t\n\t editor.nodes.notifications.appendChild(notification);\n\t inputField.focus();\n\t\n\t editor.nodes.notifications.classList.add('cdx-notification__notification-appending');\n\t\n\t window.setTimeout(function () {\n\t\n\t editor.nodes.notifications.classList.remove('cdx-notification__notification-appending');\n\t }, 100);\n\t\n\t addToQueue({ type: type, close: close });\n\t };\n\t\n\t /**\n\t * Remove notification block\n\t */\n\t function close() {\n\t\n\t notification.remove();\n\t };\n\t\n\t if (constructorSettings) {\n\t\n\t create(constructorSettings);\n\t send();\n\t }\n\t\n\t return {\n\t create: create,\n\t send: send,\n\t close: close\n\t };\n\t };\n\t\n\t notifications.clear = function () {\n\t\n\t editor.nodes.notifications.innerHTML = '';\n\t queue = [];\n\t };\n\t\n\t return notifications;\n\t}({});\n\n/***/ }),\n/* 16 */\n/***/ (function(module, exports) {\n\n\t\"use strict\";\n\t\n\t/**\n\t * Codex Editor Parser Module\n\t *\n\t * @author Codex Team\n\t * @version 1.1\n\t */\n\t\n\tmodule.exports = function (parser) {\n\t\n\t var editor = codex.editor;\n\t\n\t /** inserting text */\n\t parser.insertPastedContent = function (blockType, tag) {\n\t\n\t editor.content.insertBlock({\n\t type: blockType.type,\n\t block: blockType.render({\n\t text: tag.innerHTML\n\t })\n\t });\n\t };\n\t\n\t /**\n\t * Check DOM node for display style: separated block or child-view\n\t */\n\t parser.isFirstLevelBlock = function (node) {\n\t\n\t return node.nodeType == editor.core.nodeTypes.TAG && node.classList.contains(editor.ui.className.BLOCK_CLASSNAME);\n\t };\n\t\n\t return parser;\n\t}({});\n\n/***/ }),\n/* 17 */\n/***/ (function(module, exports, __webpack_require__) {\n\n\t'use strict';\n\t\n\t/**\n\t * Codex Sanitizer\n\t */\n\t\n\tmodule.exports = function (sanitizer) {\n\t\n\t /** HTML Janitor library */\n\t var janitor = __webpack_require__(18);\n\t\n\t /** Codex Editor */\n\t var editor = codex.editor;\n\t\n\t sanitizer.prepare = function () {\n\t\n\t if (editor.settings.sanitizer && !editor.core.isEmpty(editor.settings.sanitizer)) {\n\t\n\t Config.CUSTOM = editor.settings.sanitizer;\n\t }\n\t };\n\t\n\t /**\n\t * Basic config\n\t */\n\t var Config = {\n\t\n\t /** User configuration */\n\t CUSTOM: null,\n\t\n\t BASIC: {\n\t\n\t tags: {\n\t p: {},\n\t a: {\n\t href: true,\n\t target: '_blank',\n\t rel: 'nofollow'\n\t }\n\t }\n\t }\n\t };\n\t\n\t sanitizer.Config = Config;\n\t\n\t /**\n\t *\n\t * @param userCustomConfig\n\t * @returns {*}\n\t * @private\n\t *\n\t * @description If developer uses editor's API, then he can customize sane restrictions.\n\t * Or, sane config can be defined globally in editors initialization. That config will be used everywhere\n\t * At least, if there is no config overrides, that API uses BASIC Default configation\n\t */\n\t var init_ = function init_(userCustomConfig) {\n\t\n\t var configuration = userCustomConfig || Config.CUSTOM || Config.BASIC;\n\t\n\t return new janitor(configuration);\n\t };\n\t\n\t /**\n\t * Cleans string from unwanted tags\n\t * @protected\n\t * @param {String} dirtyString - taint string\n\t * @param {Object} customConfig - allowed tags\n\t */\n\t sanitizer.clean = function (dirtyString, customConfig) {\n\t\n\t var janitorInstance = init_(customConfig);\n\t\n\t return janitorInstance.clean(dirtyString);\n\t };\n\t\n\t return sanitizer;\n\t}({});\n\n/***/ }),\n/* 18 */\n/***/ (function(module, exports, __webpack_require__) {\n\n\tvar __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;(function (root, factory) {\n\t if (true) {\n\t !(__WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.call(exports, __webpack_require__, exports, module)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));\n\t } else if (typeof exports === 'object') {\n\t module.exports = factory();\n\t } else {\n\t root.HTMLJanitor = factory();\n\t }\n\t}(this, function () {\n\t\n\t /**\n\t * @param {Object} config.tags Dictionary of allowed tags.\n\t * @param {boolean} config.keepNestedBlockElements Default false.\n\t */\n\t function HTMLJanitor(config) {\n\t\n\t var tagDefinitions = config['tags'];\n\t var tags = Object.keys(tagDefinitions);\n\t\n\t var validConfigValues = tags\n\t .map(function(k) { return typeof tagDefinitions[k]; })\n\t .every(function(type) { return type === 'object' || type === 'boolean' || type === 'function'; });\n\t\n\t if(!validConfigValues) {\n\t throw new Error(\"The configuration was invalid\");\n\t }\n\t\n\t this.config = config;\n\t }\n\t\n\t // TODO: not exhaustive?\n\t var blockElementNames = ['P', 'LI', 'TD', 'TH', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'PRE'];\n\t function isBlockElement(node) {\n\t return blockElementNames.indexOf(node.nodeName) !== -1;\n\t }\n\t\n\t var inlineElementNames = ['A', 'B', 'STRONG', 'I', 'EM', 'SUB', 'SUP', 'U', 'STRIKE'];\n\t function isInlineElement(node) {\n\t return inlineElementNames.indexOf(node.nodeName) !== -1;\n\t }\n\t\n\t HTMLJanitor.prototype.clean = function (html) {\n\t var sandbox = document.createElement('div');\n\t sandbox.innerHTML = html;\n\t\n\t this._sanitize(sandbox);\n\t\n\t return sandbox.innerHTML;\n\t };\n\t\n\t HTMLJanitor.prototype._sanitize = function (parentNode) {\n\t var treeWalker = createTreeWalker(parentNode);\n\t var node = treeWalker.firstChild();\n\t if (!node) { return; }\n\t\n\t do {\n\t // Ignore nodes that have already been sanitized\n\t if (node._sanitized) {\n\t continue;\n\t }\n\t\n\t if (node.nodeType === Node.TEXT_NODE) {\n\t // If this text node is just whitespace and the previous or next element\n\t // sibling is a block element, remove it\n\t // N.B.: This heuristic could change. Very specific to a bug with\n\t // `contenteditable` in Firefox: http://jsbin.com/EyuKase/1/edit?js,output\n\t // FIXME: make this an option?\n\t if (node.data.trim() === ''\n\t && ((node.previousElementSibling && isBlockElement(node.previousElementSibling))\n\t || (node.nextElementSibling && isBlockElement(node.nextElementSibling)))) {\n\t parentNode.removeChild(node);\n\t this._sanitize(parentNode);\n\t break;\n\t } else {\n\t continue;\n\t }\n\t }\n\t\n\t // Remove all comments\n\t if (node.nodeType === Node.COMMENT_NODE) {\n\t parentNode.removeChild(node);\n\t this._sanitize(parentNode);\n\t break;\n\t }\n\t\n\t var isInline = isInlineElement(node);\n\t var containsBlockElement;\n\t if (isInline) {\n\t containsBlockElement = Array.prototype.some.call(node.childNodes, isBlockElement);\n\t }\n\t\n\t // Block elements should not be nested (e.g.
...); if\n\t // they are, we want to unwrap the inner block element.\n\t var isNotTopContainer = !! parentNode.parentNode;\n\t var isNestedBlockElement =\n\t isBlockElement(parentNode) &&\n\t isBlockElement(node) &&\n\t isNotTopContainer;\n\t\n\t var nodeName = node.nodeName.toLowerCase();\n\t\n\t var allowedAttrs = getAllowedAttrs(this.config, nodeName, node);\n\t\n\t var isInvalid = isInline && containsBlockElement;\n\t\n\t // Drop tag entirely according to the whitelist *and* if the markup\n\t // is invalid.\n\t if (isInvalid || shouldRejectNode(node, allowedAttrs)\n\t || (!this.config.keepNestedBlockElements && isNestedBlockElement)) {\n\t // Do not keep the inner text of SCRIPT/STYLE elements.\n\t if (! (node.nodeName === 'SCRIPT' || node.nodeName === 'STYLE')) {\n\t while (node.childNodes.length > 0) {\n\t parentNode.insertBefore(node.childNodes[0], node);\n\t }\n\t }\n\t parentNode.removeChild(node);\n\t\n\t this._sanitize(parentNode);\n\t break;\n\t }\n\t\n\t // Sanitize attributes\n\t for (var a = 0; a < node.attributes.length; a += 1) {\n\t var attr = node.attributes[a];\n\t\n\t if (shouldRejectAttr(attr, allowedAttrs, node)) {\n\t node.removeAttribute(attr.name);\n\t // Shift the array to continue looping.\n\t a = a - 1;\n\t }\n\t }\n\t\n\t // Sanitize children\n\t this._sanitize(node);\n\t\n\t // Mark node as sanitized so it's ignored in future runs\n\t node._sanitized = true;\n\t } while ((node = treeWalker.nextSibling()));\n\t };\n\t\n\t function createTreeWalker(node) {\n\t return document.createTreeWalker(node,\n\t NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT,\n\t null, false);\n\t }\n\t\n\t function getAllowedAttrs(config, nodeName, node){\n\t if (typeof config.tags[nodeName] === 'function') {\n\t return config.tags[nodeName](node);\n\t } else {\n\t return config.tags[nodeName];\n\t }\n\t }\n\t\n\t function shouldRejectNode(node, allowedAttrs){\n\t if (typeof allowedAttrs === 'undefined') {\n\t return true;\n\t } else if (typeof allowedAttrs === 'boolean') {\n\t return !allowedAttrs;\n\t }\n\t\n\t return false;\n\t }\n\t\n\t function shouldRejectAttr(attr, allowedAttrs, node){\n\t var attrName = attr.name.toLowerCase();\n\t\n\t if (allowedAttrs === true){\n\t return false;\n\t } else if (typeof allowedAttrs[attrName] === 'function'){\n\t return !allowedAttrs[attrName](attr.value, node);\n\t } else if (typeof allowedAttrs[attrName] === 'undefined'){\n\t return true;\n\t } else if (allowedAttrs[attrName] === false) {\n\t return true;\n\t } else if (typeof allowedAttrs[attrName] === 'string') {\n\t return (allowedAttrs[attrName] !== attr.value);\n\t }\n\t\n\t return false;\n\t }\n\t\n\t return HTMLJanitor;\n\t\n\t}));\n\n\n/***/ }),\n/* 19 */\n/***/ (function(module, exports) {\n\n\t\"use strict\";\n\t\n\t/**\n\t * Codex Editor Listeners module\n\t *\n\t * @author Codex Team\n\t * @version 1.0\n\t */\n\t\n\t/**\n\t * Module-decorator for event listeners assignment\n\t */\n\tmodule.exports = function (listeners) {\n\t\n\t var allListeners = [];\n\t\n\t /**\n\t * Search methods\n\t *\n\t * byElement, byType and byHandler returns array of suitable listeners\n\t * one and all takes element, eventType, and handler and returns first (all) suitable listener\n\t *\n\t */\n\t listeners.search = function () {\n\t\n\t var byElement = function byElement(element, context) {\n\t\n\t var listenersOnElement = [];\n\t\n\t context = context || allListeners;\n\t\n\t for (var i = 0; i < context.length; i++) {\n\t\n\t var listener = context[i];\n\t\n\t if (listener.element === element) {\n\t\n\t listenersOnElement.push(listener);\n\t }\n\t }\n\t\n\t return listenersOnElement;\n\t };\n\t\n\t var byType = function byType(eventType, context) {\n\t\n\t var listenersWithType = [];\n\t\n\t context = context || allListeners;\n\t\n\t for (var i = 0; i < context.length; i++) {\n\t\n\t var listener = context[i];\n\t\n\t if (listener.type === eventType) {\n\t\n\t listenersWithType.push(listener);\n\t }\n\t }\n\t\n\t return listenersWithType;\n\t };\n\t\n\t var byHandler = function byHandler(handler, context) {\n\t\n\t var listenersWithHandler = [];\n\t\n\t context = context || allListeners;\n\t\n\t for (var i = 0; i < context.length; i++) {\n\t\n\t var listener = context[i];\n\t\n\t if (listener.handler === handler) {\n\t\n\t listenersWithHandler.push(listener);\n\t }\n\t }\n\t\n\t return listenersWithHandler;\n\t };\n\t\n\t var one = function one(element, eventType, handler) {\n\t\n\t var result = allListeners;\n\t\n\t if (element) result = byElement(element, result);\n\t\n\t if (eventType) result = byType(eventType, result);\n\t\n\t if (handler) result = byHandler(handler, result);\n\t\n\t return result[0];\n\t };\n\t\n\t var all = function all(element, eventType, handler) {\n\t\n\t var result = allListeners;\n\t\n\t if (element) result = byElement(element, result);\n\t\n\t if (eventType) result = byType(eventType, result);\n\t\n\t if (handler) result = byHandler(handler, result);\n\t\n\t return result;\n\t };\n\t\n\t return {\n\t byElement: byElement,\n\t byType: byType,\n\t byHandler: byHandler,\n\t one: one,\n\t all: all\n\t };\n\t }();\n\t\n\t listeners.add = function (element, eventType, handler, isCapture) {\n\t\n\t element.addEventListener(eventType, handler, isCapture);\n\t\n\t var data = {\n\t element: element,\n\t type: eventType,\n\t handler: handler\n\t };\n\t\n\t var alreadyAddedListener = listeners.search.one(element, eventType, handler);\n\t\n\t if (!alreadyAddedListener) {\n\t\n\t allListeners.push(data);\n\t }\n\t };\n\t\n\t listeners.remove = function (element, eventType, handler) {\n\t\n\t element.removeEventListener(eventType, handler);\n\t\n\t var existingListeners = listeners.search.all(element, eventType, handler);\n\t\n\t for (var i = 0; i < existingListeners.length; i++) {\n\t\n\t var index = allListeners.indexOf(existingListeners[i]);\n\t\n\t if (index > 0) {\n\t\n\t allListeners.splice(index, 1);\n\t }\n\t }\n\t };\n\t\n\t listeners.removeAll = function () {\n\t\n\t allListeners.map(function (current) {\n\t\n\t listeners.remove(current.element, current.type, current.handler);\n\t });\n\t };\n\t\n\t listeners.get = function (element, eventType, handler) {\n\t\n\t return listeners.search.all(element, eventType, handler);\n\t };\n\t\n\t return listeners;\n\t}({});\n\n/***/ }),\n/* 20 */\n/***/ (function(module, exports) {\n\n\t'use strict';\n\t\n\tvar _typeof = typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; };\n\t\n\t/**\n\t * Codex Editor Destroyer module\n\t *\n\t * @auhor Codex Team\n\t * @version 1.0\n\t */\n\t\n\tmodule.exports = function (destroyer) {\n\t\n\t var editor = codex.editor;\n\t\n\t destroyer.removeNodes = function () {\n\t\n\t editor.nodes.wrapper.remove();\n\t editor.nodes.notifications.remove();\n\t };\n\t\n\t destroyer.destroyPlugins = function () {\n\t\n\t for (var tool in editor.tools) {\n\t\n\t if (typeof editor.tools[tool].destroy === 'function') {\n\t\n\t editor.tools[tool].destroy();\n\t }\n\t }\n\t };\n\t\n\t destroyer.destroyScripts = function () {\n\t\n\t var scripts = document.getElementsByTagName('SCRIPT');\n\t\n\t for (var i = 0; i < scripts.length; i++) {\n\t\n\t if (scripts[i].id.indexOf(editor.scriptPrefix) + 1) {\n\t\n\t scripts[i].remove();\n\t i--;\n\t }\n\t }\n\t };\n\t\n\t /**\n\t * Delete editor data from webpage.\n\t * You should send settings argument with boolean flags:\n\t * @param settings.ui- remove redactor event listeners and DOM nodes\n\t * @param settings.scripts - remove redactor scripts from DOM\n\t * @param settings.plugins - remove plugin's objects\n\t * @param settings.core - remove editor core. You can remove core only if UI and scripts flags is true\n\t * }\n\t *\n\t */\n\t destroyer.destroy = function (settings) {\n\t\n\t if (!settings || (typeof settings === 'undefined' ? 'undefined' : _typeof(settings)) !== 'object') {\n\t\n\t return;\n\t }\n\t\n\t if (settings.ui) {\n\t\n\t destroyer.removeNodes();\n\t editor.listeners.removeAll();\n\t }\n\t\n\t if (settings.scripts) {\n\t\n\t destroyer.destroyScripts();\n\t }\n\t\n\t if (settings.plugins) {\n\t\n\t destroyer.destroyPlugins();\n\t }\n\t\n\t if (settings.ui && settings.scripts && settings.core) {\n\t\n\t delete codex.editor;\n\t }\n\t };\n\t\n\t return destroyer;\n\t}({});\n\n/***/ }),\n/* 21 */\n/***/ (function(module, exports) {\n\n\t'use strict';\n\t\n\t/**\n\t * Codex Editor Paste module\n\t *\n\t * @author Codex Team\n\t * @version 1.1.1\n\t */\n\t\n\tmodule.exports = function (paste) {\n\t\n\t var editor = codex.editor;\n\t\n\t var patterns = [];\n\t\n\t paste.prepare = function () {\n\t\n\t var tools = editor.tools;\n\t\n\t for (var tool in tools) {\n\t\n\t if (!tools[tool].renderOnPastePatterns || !Array.isArray(tools[tool].renderOnPastePatterns)) {\n\t\n\t continue;\n\t }\n\t\n\t tools[tool].renderOnPastePatterns.map(function (pattern) {\n\t\n\t patterns.push(pattern);\n\t });\n\t }\n\t\n\t return Promise.resolve();\n\t };\n\t\n\t /**\n\t * Saves data\n\t * @param event\n\t */\n\t paste.pasted = function (event) {\n\t\n\t var clipBoardData = event.clipboardData || window.clipboardData,\n\t content = clipBoardData.getData('Text');\n\t\n\t var result = analize(content);\n\t\n\t if (result) {\n\t\n\t event.preventDefault();\n\t event.stopImmediatePropagation();\n\t }\n\t\n\t return result;\n\t };\n\t\n\t /**\n\t * Analizes pated string and calls necessary method\n\t */\n\t\n\t var analize = function analize(string) {\n\t\n\t var result = false,\n\t content = editor.content.currentNode,\n\t plugin = content.dataset.tool;\n\t\n\t patterns.map(function (pattern) {\n\t\n\t var execArray = pattern.regex.exec(string),\n\t match = execArray && execArray[0];\n\t\n\t if (match && match === string.trim()) {\n\t\n\t /** current block is not empty */\n\t if (content.textContent.trim() && plugin == editor.settings.initialBlockPlugin) {\n\t\n\t pasteToNewBlock_();\n\t }\n\t\n\t pattern.callback(string, pattern);\n\t result = true;\n\t }\n\t });\n\t\n\t return result;\n\t };\n\t\n\t var pasteToNewBlock_ = function pasteToNewBlock_() {\n\t\n\t /** Create new initial block */\n\t editor.content.insertBlock({\n\t\n\t type: editor.settings.initialBlockPlugin,\n\t block: editor.tools[editor.settings.initialBlockPlugin].render({\n\t text: ''\n\t })\n\t\n\t }, false);\n\t };\n\t\n\t /**\n\t * This method prevents default behaviour.\n\t *\n\t * @param {Object} event\n\t * @protected\n\t *\n\t * @description We get from clipboard pasted data, sanitize, make a fragment that contains of this sanitized nodes.\n\t * Firstly, we need to memorize the caret position. We can do that by getting the range of selection.\n\t * After all, we insert clear fragment into caret placed position. Then, we should move the caret to the last node\n\t */\n\t paste.blockPasteCallback = function (event) {\n\t\n\t if (!needsToHandlePasteEvent(event.target)) {\n\t\n\t return;\n\t }\n\t\n\t /** Prevent default behaviour */\n\t event.preventDefault();\n\t\n\t /** get html pasted data - dirty data */\n\t var htmlData = event.clipboardData.getData('text/html'),\n\t plainData = event.clipboardData.getData('text/plain');\n\t\n\t /** Temporary DIV that is used to work with text's paragraphs as DOM-elements*/\n\t var paragraphs = editor.draw.node('DIV', '', {}),\n\t cleanData,\n\t wrappedData;\n\t\n\t /** Create fragment, that we paste to range after proccesing */\n\t cleanData = editor.sanitizer.clean(htmlData);\n\t\n\t /**\n\t * We wrap pasted text with
tags to split it logically\n\t * @type {string}\n\t */\n\t wrappedData = editor.content.wrapTextWithParagraphs(cleanData, plainData);\n\t paragraphs.innerHTML = wrappedData;\n\t\n\t /**\n\t * If there only one paragraph, just insert in at the caret location\n\t */\n\t if (paragraphs.childNodes.length == 1) {\n\t\n\t emulateUserAgentBehaviour(paragraphs.firstChild);\n\t return;\n\t }\n\t\n\t insertPastedParagraphs(paragraphs.childNodes);\n\t };\n\t\n\t /**\n\t * Checks if we should handle paste event on block\n\t * @param block\n\t *\n\t * @return {boolean}\n\t */\n\t var needsToHandlePasteEvent = function needsToHandlePasteEvent(block) {\n\t\n\t /** If area is input or textarea then allow default behaviour */\n\t if (editor.core.isNativeInput(block)) {\n\t\n\t return false;\n\t }\n\t\n\t var editableParent = editor.content.getEditableParent(block);\n\t\n\t /** Allow paste when event target placed in Editable element */\n\t if (!editableParent) {\n\t\n\t return false;\n\t }\n\t\n\t return true;\n\t };\n\t\n\t /**\n\t * Inserts new initial plugin blocks with data in paragraphs\n\t *\n\t * @param {Array} paragraphs - array of paragraphs (
) whit content, that should be inserted\n\t */\n\t var insertPastedParagraphs = function insertPastedParagraphs(paragraphs) {\n\t\n\t var NEW_BLOCK_TYPE = editor.settings.initialBlockPlugin,\n\t currentNode = editor.content.currentNode;\n\t\n\t paragraphs.forEach(function (paragraph) {\n\t\n\t /** Don't allow empty paragraphs */\n\t if (editor.core.isBlockEmpty(paragraph)) {\n\t\n\t return;\n\t }\n\t\n\t editor.content.insertBlock({\n\t type: NEW_BLOCK_TYPE,\n\t block: editor.tools[NEW_BLOCK_TYPE].render({\n\t text: paragraph.innerHTML\n\t })\n\t });\n\t\n\t editor.caret.inputIndex++;\n\t });\n\t\n\t editor.caret.setToPreviousBlock(editor.caret.getCurrentInputIndex() + 1);\n\t\n\t /**\n\t * If there was no data in working node, remove it\n\t */\n\t if (editor.core.isBlockEmpty(currentNode)) {\n\t\n\t currentNode.remove();\n\t editor.ui.saveInputs();\n\t }\n\t };\n\t\n\t /**\n\t * Inserts node content at the caret position\n\t *\n\t * @param {Node} node - DOM node (could be DocumentFragment), that should be inserted at the caret location\n\t */\n\t var emulateUserAgentBehaviour = function emulateUserAgentBehaviour(node) {\n\t\n\t var newNode;\n\t\n\t if (node.childElementCount) {\n\t\n\t newNode = document.createDocumentFragment();\n\t\n\t node.childNodes.forEach(function (current) {\n\t\n\t if (!editor.core.isDomNode(current) && current.data.trim() === '') {\n\t\n\t return;\n\t }\n\t\n\t newNode.appendChild(current.cloneNode(true));\n\t });\n\t } else {\n\t\n\t newNode = document.createTextNode(node.textContent);\n\t }\n\t\n\t editor.caret.insertNode(newNode);\n\t };\n\t\n\t return paste;\n\t}({});\n\n/***/ })\n/******/ ]);\n\n\n// WEBPACK FOOTER //\n// codex-editor.js"," \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId])\n \t\t\treturn installedModules[moduleId].exports;\n\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\texports: {},\n \t\t\tid: moduleId,\n \t\t\tloaded: false\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.loaded = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(0);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap e0115ab21dbe1447c691","/**\n *\n * Codex Editor\n *\n * @author Codex Team\n */\n\nmodule.exports = (function (editor) {\n\n 'use strict';\n\n editor.version = VERSION;\n editor.scriptPrefix = 'cdx-script-';\n\n var init = function () {\n\n editor.core = require('./modules/core');\n editor.tools = require('./modules/tools');\n editor.ui = require('./modules/ui');\n editor.transport = require('./modules/transport');\n editor.renderer = require('./modules/renderer');\n editor.saver = require('./modules/saver');\n editor.content = require('./modules/content');\n editor.toolbar = require('./modules/toolbar/toolbar');\n editor.callback = require('./modules/callbacks');\n editor.draw = require('./modules/draw');\n editor.caret = require('./modules/caret');\n editor.notifications = require('./modules/notifications');\n editor.parser = require('./modules/parser');\n editor.sanitizer = require('./modules/sanitizer');\n editor.listeners = require('./modules/listeners');\n editor.destroyer = require('./modules/destroyer');\n editor.paste = require('./modules/paste');\n\n };\n\n /**\n * @public\n * holds initial settings\n */\n editor.settings = {\n tools : ['paragraph', 'header', 'picture', 'list', 'quote', 'code', 'twitter', 'instagram', 'smile'],\n holderId : 'codex-editor',\n\n // Type of block showing on empty editor\n initialBlockPlugin: 'paragraph'\n };\n\n /**\n * public\n *\n * Static nodes\n */\n editor.nodes = {\n holder : null,\n wrapper : null,\n toolbar : null,\n inlineToolbar : {\n wrapper : null,\n buttons : null,\n actions : null\n },\n toolbox : null,\n notifications : null,\n plusButton : null,\n showSettingsButton: null,\n showTrashButton : null,\n blockSettings : null,\n pluginSettings : null,\n defaultSettings : null,\n toolbarButtons : {}, // { type : DomEl, ... }\n redactor : null\n };\n\n /**\n * @public\n *\n * Output state\n */\n editor.state = {\n jsonOutput : [],\n blocks : [],\n inputs : []\n };\n\n /**\n * @public\n * Editor plugins\n */\n editor.tools = {};\n\n /**\n * Initialization\n * @uses Promise cEditor.core.prepare\n * @param {Object} userSettings\n * @param {Array} userSettings.tools list of plugins\n * @param {String} userSettings.holderId Element's id to append editor\n *\n * Load user defined tools\n * Tools must contain this important objects :\n * @param {String} type - this is a type of plugin. It can be used as plugin name\n * @param {String} iconClassname - this a icon in toolbar\n * @param {Object} make - what should plugin do, when it is clicked\n * @param {Object} appendCallback - callback after clicking\n * @param {Element} settings - what settings does it have\n * @param {Object} render - plugin get JSON, and should return HTML\n * @param {Object} save - plugin gets HTML content, returns JSON\n * @param {Boolean} displayInToolbox - will be displayed in toolbox. Default value is TRUE\n * @param {Boolean} enableLineBreaks - inserts new block or break lines. Default value is FALSE\n *\n * @example\n * - type : 'header',\n * - iconClassname : 'ce-icon-header',\n * - make : headerTool.make,\n * - appendCallback : headerTool.appendCallback,\n * - settings : headerTool.makeSettings(),\n * - render : headerTool.render,\n * - save : headerTool.save,\n * - displayInToolbox : true,\n * - enableLineBreaks : false\n */\n editor.start = function (userSettings) {\n\n init();\n\n editor.core.prepare(userSettings)\n\n // If all ok, make UI, bind events and parse initial-content\n .then(editor.ui.prepare)\n .then(editor.tools.prepare)\n .then(editor.sanitizer.prepare)\n .then(editor.paste.prepare)\n .then(editor.transport.prepare)\n .then(editor.renderer.makeBlocksFromData)\n .then(editor.ui.saveInputs)\n .catch(function (error) {\n\n editor.core.log('Initialization failed with error: %o', 'warn', error);\n\n });\n\n };\n\n return editor;\n\n})({});\n\n\n\n// WEBPACK FOOTER //\n// ./codex.js","/**\n * Codex Editor Core\n *\n * @author Codex Team\n * @version 1.1.3\n */\n\nmodule.exports = (function (core) {\n\n let editor = codex.editor;\n\n /**\n * @public\n *\n * Editor preparing method\n * @return Promise\n */\n core.prepare = function (userSettings) {\n\n return new Promise(function (resolve, reject) {\n\n if ( userSettings ) {\n\n editor.settings.tools = userSettings.tools || editor.settings.tools;\n\n }\n\n if (userSettings.data) {\n\n editor.state.blocks = userSettings.data;\n\n }\n\n if (userSettings.initialBlockPlugin) {\n\n editor.settings.initialBlockPlugin = userSettings.initialBlockPlugin;\n\n }\n\n if (userSettings.sanitizer) {\n\n editor.settings.sanitizer = userSettings.sanitizer;\n\n }\n\n editor.hideToolbar = userSettings.hideToolbar;\n\n editor.settings.placeholder = userSettings.placeholder || '';\n\n editor.nodes.holder = document.getElementById(userSettings.holderId || editor.settings.holderId);\n\n if (typeof editor.nodes.holder === undefined || editor.nodes.holder === null) {\n\n reject(Error(\"Holder wasn't found by ID: #\" + userSettings.holderId));\n\n } else {\n\n resolve();\n\n }\n\n });\n\n };\n\n /**\n * Logging method\n * @param type = ['log', 'info', 'warn']\n */\n core.log = function (msg, type, arg) {\n\n type = type || 'log';\n\n if (!arg) {\n\n arg = msg || 'undefined';\n msg = '[codex-editor]: %o';\n\n } else {\n\n msg = '[codex-editor]: ' + msg;\n\n }\n\n try{\n\n if ( 'console' in window && window.console[ type ] ) {\n\n if ( arg ) window.console[ type ]( msg, arg );\n else window.console[ type ]( msg );\n\n }\n\n }catch(e) {}\n\n };\n\n /**\n * @protected\n *\n * Helper for insert one element after another\n */\n core.insertAfter = function (target, element) {\n\n target.parentNode.insertBefore(element, target.nextSibling);\n\n };\n\n /**\n * @const\n *\n * Readable DOM-node types map\n */\n core.nodeTypes = {\n TAG : 1,\n TEXT : 3,\n COMMENT : 8,\n DOCUMENT_FRAGMENT: 11\n };\n\n /**\n * @const\n * Readable keys map\n */\n core.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 };\n\n /**\n * @protected\n *\n * Check object for DOM node\n */\n core.isDomNode = function (el) {\n\n return el && typeof el === 'object' && el.nodeType && el.nodeType == this.nodeTypes.TAG;\n\n };\n\n /**\n * Checks passed object for emptiness\n * @require ES5 - Object.keys\n * @param {object}\n */\n core.isEmpty = function ( obj ) {\n\n return Object.keys(obj).length === 0;\n\n };\n\n /**\n * Native Ajax\n * @param {String} settings.url - request URL\n * @param {function} settings.beforeSend - returned value will be passed as context to the Success, Error and Progress callbacks\n * @param {function} settings.success\n * @param {function} settings.progress\n */\n core.ajax = function (settings) {\n\n if (!settings || !settings.url) {\n\n return;\n\n }\n\n var XMLHTTP = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP'),\n encodedString,\n isFormData,\n prop;\n\n\n settings.async = true;\n settings.type = settings.type || 'GET';\n settings.data = settings.data || '';\n settings['content-type'] = settings['content-type'] || 'application/json; charset=utf-8';\n\n if (settings.type == 'GET' && settings.data) {\n\n settings.url = /\\?/.test(settings.url) ? settings.url + '&' + settings.data : settings.url + '?' + settings.data;\n\n } else {\n\n encodedString = '';\n for(prop in settings.data) {\n\n encodedString += (prop + '=' + encodeURIComponent(settings.data[prop]) + '&');\n\n }\n\n }\n\n if (settings.withCredentials) {\n\n XMLHTTP.withCredentials = true;\n\n }\n\n /**\n * Value returned in beforeSend funtion will be passed as context to the other response callbacks\n * If beforeSend returns false, AJAX will be blocked\n */\n let responseContext,\n beforeSendResult;\n\n if (typeof settings.beforeSend === 'function') {\n\n beforeSendResult = settings.beforeSend.call();\n\n if (beforeSendResult === false) {\n\n return;\n\n }\n\n }\n\n XMLHTTP.open( settings.type, settings.url, settings.async );\n\n /**\n * If we send FormData, we need no content-type header\n */\n isFormData = isFormData_(settings.data);\n\n if (!isFormData) {\n\n if (settings.type !== 'POST') {\n\n XMLHTTP.setRequestHeader('Content-type', settings['content-type']);\n\n } else {\n\n XMLHTTP.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');\n\n }\n\n }\n\n XMLHTTP.setRequestHeader('X-Requested-With', 'XMLHttpRequest');\n\n responseContext = beforeSendResult || XMLHTTP;\n\n if (typeof settings.progress === 'function') {\n\n XMLHTTP.upload.onprogress = settings.progress.bind(responseContext);\n\n }\n\n XMLHTTP.onreadystatechange = function () {\n\n if (XMLHTTP.readyState === 4) {\n\n if (XMLHTTP.status === 200) {\n\n if (typeof settings.success === 'function') {\n\n settings.success.call(responseContext, XMLHTTP.responseText);\n\n }\n\n } else {\n\n if (typeof settings.error === 'function') {\n\n settings.error.call(responseContext, XMLHTTP.responseText, XMLHTTP.status);\n\n }\n\n }\n\n }\n\n };\n\n if (isFormData) {\n\n // Sending FormData\n XMLHTTP.send(settings.data);\n\n } else {\n\n // POST requests\n XMLHTTP.send(encodedString);\n\n }\n\n return XMLHTTP;\n\n };\n\n /**\n * Appends script to head of document\n * @return Promise\n */\n core.importScript = function (scriptPath, instanceName) {\n\n return new Promise(function (resolve, reject) {\n\n let script;\n\n /** Script is already loaded */\n if ( !instanceName ) {\n\n reject('Instance name is missed');\n\n } else if ( document.getElementById(editor.scriptPrefix + instanceName) ) {\n\n resolve(scriptPath);\n\n }\n\n script = document.createElement('SCRIPT');\n script.async = true;\n script.defer = true;\n script.id = editor.scriptPrefix + instanceName;\n\n script.onload = function () {\n\n resolve(scriptPath);\n\n };\n\n script.onerror = function () {\n\n reject(scriptPath);\n\n };\n\n script.src = scriptPath;\n document.head.appendChild(script);\n\n });\n\n };\n\n /**\n * Function for checking is it FormData object to send.\n * @param {Object} object to check\n * @return boolean\n */\n var isFormData_ = function (object) {\n\n return object instanceof FormData;\n\n };\n\n /**\n * Check block\n * @param target\n * @description Checks target is it native input\n */\n core.isNativeInput = function (target) {\n\n var nativeInputAreas = ['INPUT', 'TEXTAREA'];\n\n return nativeInputAreas.indexOf(target.tagName) != -1;\n\n };\n\n /**\n * Check if block is empty\n * We should check block textContent, child native inputs and some exceptions like IMG and IFRAME\n *\n * @param block\n * @returns {boolean}\n */\n core.isBlockEmpty = function (block) {\n\n const EXCEPTION_TAGS = ['IMG', 'IFRAME'];\n\n var nativeInputs = block.querySelectorAll('textarea, input'),\n nativeInputsAreEmpty = true,\n textContentIsEmpty = !block.textContent.trim();\n\n Array.prototype.forEach.call(nativeInputs, function (input) {\n\n if (input.type == 'textarea' || input.type == 'text') {\n\n nativeInputsAreEmpty = nativeInputsAreEmpty && !input.value.trim();\n\n }\n\n });\n\n return textContentIsEmpty && nativeInputsAreEmpty && !EXCEPTION_TAGS.includes(block.tagName);\n\n };\n\n\n return core;\n\n})({});\n\n\n\n// WEBPACK FOOTER //\n// ./modules/core.js","/**\n* Module working with plugins\n*/\nmodule.exports = (function () {\n\n let editor = codex.editor;\n\n /**\n * Initialize plugins before using\n * Ex. Load scripts or call some internal methods\n * @return Promise\n */\n function prepare() {\n\n return new Promise(function (resolve_, reject_) {\n\n Promise.resolve()\n\n /**\n * Compose a sequence of plugins that requires preparation\n */\n .then(function () {\n\n let pluginsRequiresPreparation = [],\n allPlugins = editor.tools;\n\n for ( let pluginName in allPlugins ) {\n\n let plugin = allPlugins[pluginName];\n\n if (plugin.prepare && typeof plugin.prepare != 'function' || !plugin.prepare) {\n\n continue;\n\n }\n\n pluginsRequiresPreparation.push(plugin);\n\n }\n\n /**\n * If no one passed plugins requires preparation, finish prepare() and go ahead\n */\n if (!pluginsRequiresPreparation.length) {\n\n resolve_();\n\n }\n\n return pluginsRequiresPreparation;\n\n })\n\n /** Wait plugins while they prepares */\n .then(waitAllPluginsPreparation_)\n\n .then(function () {\n\n editor.core.log('Plugins loaded', 'info');\n resolve_();\n\n }).catch(function (error) {\n\n reject_(error);\n\n });\n\n });\n\n }\n\n /**\n * @param {array} plugins - list of tools that requires preparation\n * @return {Promise} resolved while all plugins will be ready or failed\n */\n function waitAllPluginsPreparation_(plugins) {\n\n /**\n * @calls allPluginsProcessed__ when all plugins prepared or failed\n */\n return new Promise (function (allPluginsProcessed__) {\n\n /**\n * pluck each element from queue\n * First, send resolved Promise as previous value\n * Each plugins \"prepare\" method returns a Promise, that's why\n * reduce current element will not be able to continue while can't get\n * a resolved Promise\n *\n * If last plugin is \"prepared\" then go to the next stage of initialization\n */\n plugins.reduce(function (previousValue, plugin, iteration) {\n\n return previousValue.then(function () {\n\n /**\n * Wait till plugins prepared\n * @calls pluginIsReady__ when plugin is ready or failed\n */\n return new Promise ( function (pluginIsReady__) {\n\n callPluginsPrepareMethod_( plugin )\n\n .then( pluginIsReady__ )\n .then( function () {\n\n plugin.available = true;\n\n })\n\n .catch(function (error) {\n\n editor.core.log(`Plugin «${plugin.type}» was not loaded. Preparation failed because %o`, 'warn', error);\n plugin.available = false;\n plugin.loadingMessage = error;\n\n /** Go ahead even some plugin has problems */\n pluginIsReady__();\n\n })\n\n .then(function () {\n\n /** If last plugin has problems then just ignore and continue */\n if (iteration == plugins.length - 1) {\n\n allPluginsProcessed__();\n\n }\n\n });\n\n });\n\n });\n\n }, Promise.resolve() );\n\n });\n\n }\n\n var callPluginsPrepareMethod_ = function (plugin) {\n\n return plugin.prepare( plugin.config || {} );\n\n };\n\n return {\n prepare: prepare\n };\n\n}());\n\n\n// WEBPACK FOOTER //\n// ./modules/tools.js","/**\n * Codex Editor UI module\n *\n * @author Codex Team\n * @version 1.2.0\n */\n\nmodule.exports = (function (ui) {\n\n let editor = codex.editor;\n\n /**\n * Basic editor classnames\n */\n ui.className = {\n\n /**\n * @const {string} BLOCK_CLASSNAME - redactor blocks name\n */\n BLOCK_CLASSNAME : 'ce-block',\n\n /**\n * @const {String} wrapper for plugins content\n */\n BLOCK_CONTENT : 'ce-block__content',\n\n /**\n * @const {String} BLOCK_STRETCHED - makes block stretched\n */\n BLOCK_STRETCHED : 'ce-block--stretched',\n\n /**\n * @const {String} BLOCK_HIGHLIGHTED - adds background\n */\n BLOCK_HIGHLIGHTED : 'ce-block--focused',\n\n /**\n * @const {String} - for all default settings\n */\n SETTINGS_ITEM : 'ce-settings__item'\n\n };\n\n /**\n * @protected\n *\n * Making main interface\n */\n ui.prepare = function () {\n\n return new Promise(function (resolve) {\n\n let wrapper = editor.draw.wrapper(),\n redactor = editor.draw.redactor(),\n toolbar = makeToolBar_();\n\n wrapper.appendChild(toolbar);\n wrapper.appendChild(redactor);\n\n /** Save created ui-elements to static nodes state */\n editor.nodes.wrapper = wrapper;\n editor.nodes.redactor = redactor;\n\n /** Append editor wrapper with redactor zone into holder */\n editor.nodes.holder.appendChild(wrapper);\n\n resolve();\n\n })\n\n /** Add toolbox tools */\n .then(addTools_)\n\n /** Make container for inline toolbar */\n .then(makeInlineToolbar_)\n\n /** Add inline toolbar tools */\n .then(addInlineToolbarTools_)\n\n /** Draw wrapper for notifications */\n .then(makeNotificationHolder_)\n\n /** Add eventlisteners to redactor elements */\n .then(bindEvents_)\n\n .catch( function () {\n\n editor.core.log(\"Can't draw editor interface\");\n\n });\n\n };\n\n /**\n * @private\n * Draws inline toolbar zone\n */\n var makeInlineToolbar_ = function () {\n\n var container = editor.draw.inlineToolbar();\n\n /** Append to redactor new inline block */\n editor.nodes.inlineToolbar.wrapper = container;\n\n /** Draw toolbar buttons */\n editor.nodes.inlineToolbar.buttons = editor.draw.inlineToolbarButtons();\n\n /** Buttons action or settings */\n editor.nodes.inlineToolbar.actions = editor.draw.inlineToolbarActions();\n\n /** Append to inline toolbar buttons as part of it */\n editor.nodes.inlineToolbar.wrapper.appendChild(editor.nodes.inlineToolbar.buttons);\n editor.nodes.inlineToolbar.wrapper.appendChild(editor.nodes.inlineToolbar.actions);\n\n editor.nodes.wrapper.appendChild(editor.nodes.inlineToolbar.wrapper);\n\n };\n\n var makeToolBar_ = function () {\n\n let toolbar = editor.draw.toolbar(),\n blockButtons = makeToolbarSettings_(),\n toolbarContent = makeToolbarContent_();\n\n /** Appending first-level block buttons */\n toolbar.appendChild(blockButtons);\n\n /** Append toolbarContent to toolbar */\n toolbar.appendChild(toolbarContent);\n\n /** Make toolbar global */\n editor.nodes.toolbar = toolbar;\n\n return toolbar;\n\n };\n\n var makeToolbarContent_ = function () {\n\n let toolbarContent = editor.draw.toolbarContent(),\n toolbox = editor.draw.toolbox(),\n plusButton = editor.draw.plusButton();\n\n /** Append plus button */\n toolbarContent.appendChild(plusButton);\n\n /** Appending toolbar tools */\n toolbarContent.appendChild(toolbox);\n\n /** Make Toolbox and plusButton global */\n editor.nodes.toolbox = toolbox;\n editor.nodes.plusButton = plusButton;\n\n return toolbarContent;\n\n };\n\n var makeToolbarSettings_ = function () {\n\n let blockSettings = editor.draw.blockSettings(),\n blockButtons = editor.draw.blockButtons(),\n defaultSettings = editor.draw.defaultSettings(),\n showSettingsButton = editor.draw.settingsButton(),\n showTrashButton = editor.toolbar.settings.makeRemoveBlockButton(),\n pluginSettings = editor.draw.pluginsSettings();\n\n /** Add default and plugins settings */\n blockSettings.appendChild(pluginSettings);\n blockSettings.appendChild(defaultSettings);\n\n /**\n * Make blocks buttons\n * This block contains settings button and remove block button\n */\n blockButtons.appendChild(showSettingsButton);\n blockButtons.appendChild(showTrashButton);\n blockButtons.appendChild(blockSettings);\n\n /** Make BlockSettings, PluginSettings, DefaultSettings global */\n editor.nodes.blockSettings = blockSettings;\n editor.nodes.pluginSettings = pluginSettings;\n editor.nodes.defaultSettings = defaultSettings;\n editor.nodes.showSettingsButton = showSettingsButton;\n editor.nodes.showTrashButton = showTrashButton;\n\n return blockButtons;\n\n };\n\n /** Draw notifications holder */\n var makeNotificationHolder_ = function () {\n\n /** Append block with notifications to the document */\n editor.nodes.notifications = editor.notifications.createHolder();\n\n };\n\n /**\n * @private\n * Append tools passed in editor.tools\n */\n var addTools_ = function () {\n\n var tool,\n toolName,\n toolButton;\n\n for ( toolName in editor.settings.tools ) {\n\n tool = editor.settings.tools[toolName];\n\n editor.tools[toolName] = tool;\n\n if (!tool.iconClassname && tool.displayInToolbox) {\n\n editor.core.log('Toolbar icon classname missed. Tool %o skipped', 'warn', toolName);\n continue;\n\n }\n\n if (typeof tool.render != 'function') {\n\n editor.core.log('render method missed. Tool %o skipped', 'warn', toolName);\n continue;\n\n }\n\n if (!tool.displayInToolbox) {\n\n continue;\n\n } else {\n\n /** if tools is for toolbox */\n toolButton = editor.draw.toolbarButton(toolName, tool.iconClassname);\n\n editor.nodes.toolbox.appendChild(toolButton);\n\n editor.nodes.toolbarButtons[toolName] = toolButton;\n\n }\n\n }\n\n };\n\n var addInlineToolbarTools_ = function () {\n\n var tools = {\n\n bold: {\n icon : 'ce-icon-bold',\n command : 'bold'\n },\n\n italic: {\n icon : 'ce-icon-italic',\n command : 'italic'\n },\n\n link: {\n icon : 'ce-icon-link',\n command : 'createLink'\n }\n };\n\n var toolButton,\n tool;\n\n for(var name in tools) {\n\n tool = tools[name];\n\n toolButton = editor.draw.toolbarButtonInline(name, tool.icon);\n\n editor.nodes.inlineToolbar.buttons.appendChild(toolButton);\n /**\n * Add callbacks to this buttons\n */\n editor.ui.setInlineToolbarButtonBehaviour(toolButton, tool.command);\n\n }\n\n };\n\n /**\n * @private\n * Bind editor UI events\n */\n var bindEvents_ = function () {\n\n editor.core.log('ui.bindEvents fired', 'info');\n\n // window.addEventListener('error', function (errorMsg, url, lineNumber) {\n // editor.notifications.errorThrown(errorMsg, event);\n // }, false );\n\n /** All keydowns on Document */\n editor.listeners.add(document, 'keydown', editor.callback.globalKeydown, false);\n\n /** All keydowns on Redactor zone */\n editor.listeners.add(editor.nodes.redactor, 'keydown', editor.callback.redactorKeyDown, false);\n\n /** All keydowns on Document */\n editor.listeners.add(document, 'keyup', editor.callback.globalKeyup, false );\n\n /**\n * Mouse click to radactor\n */\n editor.listeners.add(editor.nodes.redactor, 'click', editor.callback.redactorClicked, false );\n\n /**\n * Clicks to the Plus button\n */\n editor.listeners.add(editor.nodes.plusButton, 'click', editor.callback.plusButtonClicked, false);\n\n /**\n * Clicks to SETTINGS button in toolbar\n */\n editor.listeners.add(editor.nodes.showSettingsButton, 'click', editor.callback.showSettingsButtonClicked, false );\n\n /** Bind click listeners on toolbar buttons */\n for (var button in editor.nodes.toolbarButtons) {\n\n editor.listeners.add(editor.nodes.toolbarButtons[button], 'click', editor.callback.toolbarButtonClicked, false);\n\n }\n\n };\n\n ui.addBlockHandlers = function (block) {\n\n if (!block) return;\n\n /**\n * Block keydowns\n */\n editor.listeners.add(block, 'keydown', editor.callback.blockKeydown, false);\n\n /**\n * Pasting content from another source\n * We have two type of sanitization\n * First - uses deep-first search algorithm to get sub nodes,\n * sanitizes whole Block_content and replaces cleared nodes\n * This method is deprecated\n * Method is used in editor.callback.blockPaste(event)\n *\n * Secont - uses Mutation observer.\n * Observer \"observe\" DOM changes and send changings to callback.\n * Callback gets changed node, not whole Block_content.\n * Inserted or changed node, which we've gotten have been cleared and replaced with diry node\n *\n * Method is used in editor.callback.blockPasteViaSanitize(event)\n *\n * @uses html-janitor\n * @example editor.callback.blockPasteViaSanitize(event), the second method.\n *\n */\n editor.listeners.add(block, 'paste', editor.paste.blockPasteCallback, false);\n\n /**\n * Show inline toolbar for selected text\n */\n editor.listeners.add(block, 'mouseup', editor.toolbar.inline.show, false);\n editor.listeners.add(block, 'keyup', editor.toolbar.inline.show, false);\n\n };\n\n /** getting all contenteditable elements */\n ui.saveInputs = function () {\n\n var redactor = editor.nodes.redactor;\n\n editor.state.inputs = [];\n\n /** Save all inputs in global variable state */\n var inputs = redactor.querySelectorAll('[contenteditable], input, textarea');\n\n Array.prototype.map.call(inputs, function (current) {\n\n if (!current.type || current.type == 'text' || current.type == 'textarea') {\n\n editor.state.inputs.push(current);\n\n }\n\n });\n\n };\n\n /**\n * Adds first initial block on empty redactor\n */\n ui.addInitialBlock = function () {\n\n var initialBlockType = editor.settings.initialBlockPlugin,\n initialBlock;\n\n if ( !editor.tools[initialBlockType] ) {\n\n editor.core.log('Plugin %o was not implemented and can\\'t be used as initial block', 'warn', initialBlockType);\n return;\n\n }\n\n initialBlock = editor.tools[initialBlockType].render();\n\n initialBlock.setAttribute('data-placeholder', editor.settings.placeholder);\n\n editor.content.insertBlock({\n type : initialBlockType,\n block : initialBlock\n });\n\n editor.content.workingNodeChanged(initialBlock);\n\n };\n\n ui.setInlineToolbarButtonBehaviour = function (button, type) {\n\n editor.listeners.add(button, 'mousedown', function (event) {\n\n editor.toolbar.inline.toolClicked(event, type);\n\n }, false);\n\n };\n\n return ui;\n\n})({});\n\n\n\n// WEBPACK FOOTER //\n// ./modules/ui.js","/**\n *\n * Codex.Editor Transport Module\n *\n * @copyright 2017 Codex-Team\n * @version 1.2.0\n */\n\nmodule.exports = (function (transport) {\n\n let editor = codex.editor;\n\n\n /**\n * @private {Object} current XmlHttpRequest instance\n */\n var currentRequest = null;\n\n\n /**\n * @type {null} | {DOMElement} input - keeps input element in memory\n */\n transport.input = null;\n\n /**\n * @property {Object} arguments - keep plugin settings and defined callbacks\n */\n transport.arguments = null;\n\n /**\n * Prepares input element where will be files\n */\n transport.prepare = function () {\n\n let input = editor.draw.node( 'INPUT', '', { type : 'file' } );\n\n editor.listeners.add(input, 'change', editor.transport.fileSelected);\n editor.transport.input = input;\n\n };\n\n /** Clear input when files is uploaded */\n transport.clearInput = function () {\n\n /** Remove old input */\n transport.input = null;\n\n /** Prepare new one */\n transport.prepare();\n\n };\n\n /**\n * Callback for file selection\n * @param {Event} event\n */\n transport.fileSelected = function () {\n\n var input = this,\n i,\n files = input.files,\n formData = new FormData();\n\n if (editor.transport.arguments.multiple === true) {\n\n for ( i = 0; i < files.length; i++) {\n\n formData.append('files[]', files[i], files[i].name);\n\n }\n\n } else {\n\n formData.append('files', files[0], files[0].name);\n\n }\n\n currentRequest = editor.core.ajax({\n type : 'POST',\n data : formData,\n url : editor.transport.arguments.url,\n beforeSend : editor.transport.arguments.beforeSend,\n success : editor.transport.arguments.success,\n error : editor.transport.arguments.error,\n progress : editor.transport.arguments.progress\n });\n\n /** Clear input */\n transport.clearInput();\n\n };\n\n /**\n * Use plugin callbacks\n * @protected\n *\n * @param {Object} args - can have :\n * @param {String} args.url - fetch URL\n * @param {Function} args.beforeSend - function calls before sending ajax\n * @param {Function} args.success - success callback\n * @param {Function} args.error - on error handler\n * @param {Function} args.progress - xhr onprogress handler\n * @param {Boolean} args.multiple - allow select several files\n * @param {String} args.accept - adds accept attribute\n */\n transport.selectAndUpload = function (args) {\n\n transport.arguments = args;\n\n if ( args.multiple === true) {\n\n transport.input.setAttribute('multiple', 'multiple');\n\n }\n\n if ( args.accept ) {\n\n transport.input.setAttribute('accept', args.accept);\n\n }\n\n transport.input.click();\n\n };\n\n transport.abort = function () {\n\n currentRequest.abort();\n\n currentRequest = null;\n\n };\n\n return transport;\n\n})({});\n\n\n// WEBPACK FOOTER //\n// ./modules/transport.js","/**\n * Codex Editor Renderer Module\n *\n * @author Codex Team\n * @version 1.0\n */\n\nmodule.exports = (function (renderer) {\n\n let editor = codex.editor;\n\n /**\n * Asyncronously parses input JSON to redactor blocks\n */\n renderer.makeBlocksFromData = function () {\n\n /**\n * If redactor is empty, add first paragraph to start writing\n */\n if (editor.core.isEmpty(editor.state.blocks) || !editor.state.blocks.items.length) {\n\n editor.ui.addInitialBlock();\n return;\n\n }\n\n Promise.resolve()\n\n /** First, get JSON from state */\n .then(function () {\n\n return editor.state.blocks;\n\n })\n\n /** Then, start to iterate they */\n .then(editor.renderer.appendBlocks)\n\n /** Write log if something goes wrong */\n .catch(function (error) {\n\n editor.core.log('Error while parsing JSON: %o', 'error', error);\n\n });\n\n };\n\n /**\n * Parses JSON to blocks\n * @param {object} data\n * @return Primise -> nodeList\n */\n renderer.appendBlocks = function (data) {\n\n var blocks = data.items;\n\n /**\n * Sequence of one-by-one blocks appending\n * Uses to save blocks order after async-handler\n */\n var nodeSequence = Promise.resolve();\n\n for (var index = 0; index < blocks.length ; index++ ) {\n\n /** Add node to sequence at specified index */\n editor.renderer.appendNodeAtIndex(nodeSequence, blocks, index);\n\n }\n\n };\n\n /**\n * Append node at specified index\n */\n renderer.appendNodeAtIndex = function (nodeSequence, blocks, index) {\n\n /** We need to append node to sequence */\n nodeSequence\n\n /** first, get node async-aware */\n .then(function () {\n\n return editor.renderer.getNodeAsync(blocks, index);\n\n })\n\n /**\n * second, compose editor-block from JSON object\n */\n .then(editor.renderer.createBlockFromData)\n\n /**\n * now insert block to redactor\n */\n .then(function (blockData) {\n\n /**\n * blockData has 'block', 'type' and 'stretched' information\n */\n editor.content.insertBlock(blockData);\n\n /** Pass created block to next step */\n return blockData.block;\n\n })\n\n /** Log if something wrong with node */\n .catch(function (error) {\n\n editor.core.log('Node skipped while parsing because %o', 'error', error);\n\n });\n\n };\n\n /**\n * Asynchronously returns block data from blocksList by index\n * @return Promise to node\n */\n renderer.getNodeAsync = function (blocksList, index) {\n\n return Promise.resolve().then(function () {\n\n return {\n tool : blocksList[index],\n position : index\n };\n\n });\n\n };\n\n /**\n * Creates editor block by JSON-data\n *\n * @uses render method of each plugin\n *\n * @param {Object} toolData.tool\n * { header : {\n * text: '',\n * type: 'H3', ...\n * }\n * }\n * @param {Number} toolData.position - index in input-blocks array\n * @return {Object} with type and Element\n */\n renderer.createBlockFromData = function ( toolData ) {\n\n /** New parser */\n var block,\n tool = toolData.tool,\n pluginName = tool.type;\n\n /** Get first key of object that stores plugin name */\n // for (var pluginName in blockData) break;\n\n /** Check for plugin existance */\n if (!editor.tools[pluginName]) {\n\n throw Error(`Plugin «${pluginName}» not found`);\n\n }\n\n /** Check for plugin having render method */\n if (typeof editor.tools[pluginName].render != 'function') {\n\n throw Error(`Plugin «${pluginName}» must have «render» method`);\n\n }\n\n if ( editor.tools[pluginName].available === false ) {\n\n block = editor.draw.unavailableBlock();\n\n block.innerHTML = editor.tools[pluginName].loadingMessage;\n\n /**\n * Saver will extract data from initial block data by position in array\n */\n block.dataset.inputPosition = toolData.position;\n\n } else {\n\n /** New Parser */\n block = editor.tools[pluginName].render(tool.data);\n\n }\n\n /** is first-level block stretched */\n var stretched = editor.tools[pluginName].isStretched || false;\n\n /** Retrun type and block */\n return {\n type : pluginName,\n block : block,\n stretched : stretched\n };\n\n };\n\n return renderer;\n\n})({});\n\n\n// WEBPACK FOOTER //\n// ./modules/renderer.js","/**\n * Codex Editor Saver\n *\n * @author Codex Team\n * @version 1.1.0\n */\n\nmodule.exports = (function (saver) {\n\n let editor = codex.editor;\n\n /**\n * @public\n * Save blocks\n */\n saver.save = function () {\n\n /** Save html content of redactor to memory */\n editor.state.html = editor.nodes.redactor.innerHTML;\n\n /** Clean jsonOutput state */\n editor.state.jsonOutput = [];\n\n return saveBlocks(editor.nodes.redactor.childNodes);\n\n };\n\n /**\n * @private\n * Save each block data\n *\n * @param blocks\n * @returns {Promise.}\n */\n let saveBlocks = function (blocks) {\n\n let data = [];\n\n for(let index = 0; index < blocks.length; index++) {\n\n data.push(getBlockData(blocks[index]));\n\n }\n\n return Promise.all(data)\n .then(makeOutput)\n .catch(editor.core.log);\n\n };\n\n /** Save and validate block data */\n let getBlockData = function (block) {\n\n return saveBlockData(block)\n .then(validateBlockData)\n .catch(editor.core.log);\n\n };\n\n /**\n * @private\n * Call block`s plugin save method and return saved data\n *\n * @param block\n * @returns {Object}\n */\n let saveBlockData = function (block) {\n\n let pluginName = block.dataset.tool;\n\n /** Check for plugin existence */\n if (!editor.tools[pluginName]) {\n\n editor.core.log(`Plugin «${pluginName}» not found`, 'error');\n return {data: null, pluginName: null};\n\n }\n\n /** Check for plugin having save method */\n if (typeof editor.tools[pluginName].save !== 'function') {\n\n editor.core.log(`Plugin «${pluginName}» must have save method`, 'error');\n return {data: null, pluginName: null};\n\n }\n\n /** Result saver */\n let blockContent = block.childNodes[0],\n pluginsContent = blockContent.childNodes[0],\n position = pluginsContent.dataset.inputPosition;\n\n /** If plugin wasn't available then return data from cache */\n if ( editor.tools[pluginName].available === false ) {\n\n return Promise.resolve({data: codex.editor.state.blocks.items[position].data, pluginName});\n\n }\n\n return Promise.resolve(pluginsContent)\n .then(editor.tools[pluginName].save)\n .then(data => Object({data, pluginName}));\n\n };\n\n /**\n * Call plugin`s validate method. Return false if validation failed\n *\n * @param data\n * @param pluginName\n * @returns {Object|Boolean}\n */\n let validateBlockData = function ({data, pluginName}) {\n\n if (!data || !pluginName) {\n\n return false;\n\n }\n\n if (editor.tools[pluginName].validate) {\n\n let result = editor.tools[pluginName].validate(data);\n\n /**\n * Do not allow invalid data\n */\n if (!result) {\n\n return false;\n\n }\n\n }\n\n return {data, pluginName};\n\n\n };\n\n /**\n * Compile article output\n *\n * @param savedData\n * @returns {{time: number, version, items: (*|Array)}}\n */\n let makeOutput = function (savedData) {\n\n savedData = savedData.filter(blockData => blockData);\n\n let items = savedData.map(blockData => Object({type: blockData.pluginName, data: blockData.data}));\n\n editor.state.jsonOutput = items;\n\n return {\n id: editor.state.blocks.id || null,\n time: +new Date(),\n version: editor.version,\n items\n };\n\n };\n\n return saver;\n\n})({});\n\n\n\n// WEBPACK FOOTER //\n// ./modules/saver.js","/**\n * Codex Editor Content Module\n * Works with DOM\n *\n * @module Codex Editor content module\n *\n * @author Codex Team\n * @version 1.3.13\n *\n * @description Module works with Elements that have been appended to the main DOM\n */\n\nmodule.exports = (function (content) {\n\n let editor = codex.editor;\n\n /**\n * Links to current active block\n * @type {null | Element}\n */\n content.currentNode = null;\n\n /**\n * clicked in redactor area\n * @type {null | Boolean}\n */\n content.editorAreaHightlighted = null;\n\n /**\n * @deprecated\n * Synchronizes redactor with original textarea\n */\n content.sync = function () {\n\n editor.core.log('syncing...');\n\n /**\n * Save redactor content to editor.state\n */\n editor.state.html = editor.nodes.redactor.innerHTML;\n\n };\n\n /**\n * Appends background to the block\n *\n * @description add CSS class to highlight visually first-level block area\n */\n content.markBlock = function () {\n\n editor.content.currentNode.classList.add(editor.ui.className.BLOCK_HIGHLIGHTED);\n\n };\n\n /**\n * Clear background\n *\n * @description clears styles that highlights block\n */\n content.clearMark = function () {\n\n if (editor.content.currentNode) {\n\n editor.content.currentNode.classList.remove(editor.ui.className.BLOCK_HIGHLIGHTED);\n\n }\n\n };\n\n /**\n * Finds first-level block\n *\n * @param {Element} node - selected or clicked in redactors area node\n * @protected\n *\n * @description looks for first-level block.\n * gets parent while node is not first-level\n */\n content.getFirstLevelBlock = function (node) {\n\n if (!editor.core.isDomNode(node)) {\n\n node = node.parentNode;\n\n }\n\n if (node === editor.nodes.redactor || node === document.body) {\n\n return null;\n\n } else {\n\n while(!node.classList.contains(editor.ui.className.BLOCK_CLASSNAME)) {\n\n node = node.parentNode;\n\n }\n\n return node;\n\n }\n\n };\n\n /**\n * Trigger this event when working node changed\n * @param {Element} targetNode - first-level of this node will be current\n * @protected\n *\n * @description If targetNode is first-level then we set it as current else we look for parents to find first-level\n */\n content.workingNodeChanged = function (targetNode) {\n\n /** Clear background from previous marked block before we change */\n editor.content.clearMark();\n\n if (!targetNode) {\n\n return;\n\n }\n\n content.currentNode = content.getFirstLevelBlock(targetNode);\n\n };\n\n /**\n * Replaces one redactor block with another\n * @protected\n * @param {Element} targetBlock - block to replace. Mostly currentNode.\n * @param {Element} newBlock\n * @param {string} newBlockType - type of new block; we need to store it to data-attribute\n *\n * [!] Function does not saves old block content.\n * You can get it manually and pass with newBlock.innerHTML\n */\n content.replaceBlock = function (targetBlock, newBlock) {\n\n if (!targetBlock || !newBlock) {\n\n editor.core.log('replaceBlock: missed params');\n return;\n\n }\n\n /** If target-block is not a frist-level block, then we iterate parents to find it */\n while(!targetBlock.classList.contains(editor.ui.className.BLOCK_CLASSNAME)) {\n\n targetBlock = targetBlock.parentNode;\n\n }\n\n /** Replacing */\n editor.nodes.redactor.replaceChild(newBlock, targetBlock);\n\n /**\n * Set new node as current\n */\n editor.content.workingNodeChanged(newBlock);\n\n /**\n * Add block handlers\n */\n editor.ui.addBlockHandlers(newBlock);\n\n /**\n * Save changes\n */\n editor.ui.saveInputs();\n\n };\n\n /**\n * @protected\n *\n * Inserts new block to redactor\n * Wrapps block into a DIV with BLOCK_CLASSNAME class\n *\n * @param blockData {object}\n * @param blockData.block {Element} element with block content\n * @param blockData.type {string} block plugin\n * @param needPlaceCaret {bool} pass true to set caret in new block\n *\n */\n content.insertBlock = function ( blockData, needPlaceCaret ) {\n\n var workingBlock = editor.content.currentNode,\n newBlockContent = blockData.block,\n blockType = blockData.type,\n isStretched = blockData.stretched;\n\n var newBlock = composeNewBlock_(newBlockContent, blockType, isStretched);\n\n if (workingBlock) {\n\n editor.core.insertAfter(workingBlock, newBlock);\n\n } else {\n\n /**\n * If redactor is empty, append as first child\n */\n editor.nodes.redactor.appendChild(newBlock);\n\n }\n\n /**\n * Block handler\n */\n editor.ui.addBlockHandlers(newBlock);\n\n /**\n * Set new node as current\n */\n editor.content.workingNodeChanged(newBlock);\n\n /**\n * Save changes\n */\n editor.ui.saveInputs();\n\n\n if ( needPlaceCaret ) {\n\n /**\n * If we don't know input index then we set default value -1\n */\n var currentInputIndex = editor.caret.getCurrentInputIndex() || -1;\n\n\n if (currentInputIndex == -1) {\n\n\n var editableElement = newBlock.querySelector('[contenteditable]'),\n emptyText = document.createTextNode('');\n\n editableElement.appendChild(emptyText);\n editor.caret.set(editableElement, 0, 0);\n\n editor.toolbar.move();\n editor.toolbar.showPlusButton();\n\n\n } else {\n\n if (currentInputIndex === editor.state.inputs.length - 1)\n return;\n\n /** Timeout for browsers execution */\n window.setTimeout(function () {\n\n /** Setting to the new input */\n editor.caret.setToNextBlock(currentInputIndex);\n editor.toolbar.move();\n editor.toolbar.open();\n\n }, 10);\n\n }\n\n }\n\n /**\n * Block is inserted, wait for new click that defined focusing on editors area\n * @type {boolean}\n */\n content.editorAreaHightlighted = false;\n\n };\n\n /**\n * Replaces blocks with saving content\n * @protected\n * @param {Element} noteToReplace\n * @param {Element} newNode\n * @param {Element} blockType\n */\n content.switchBlock = function (blockToReplace, newBlock, tool) {\n\n tool = tool || editor.content.currentNode.dataset.tool;\n var newBlockComposed = composeNewBlock_(newBlock, tool);\n\n /** Replacing */\n editor.content.replaceBlock(blockToReplace, newBlockComposed);\n\n /** Save new Inputs when block is changed */\n editor.ui.saveInputs();\n\n };\n\n /**\n * Iterates between child noted and looking for #text node on deepest level\n * @protected\n *\n * @param {Element} block - node where find\n * @param {int} postiton - starting postion\n * Example: childNodex.length to find from the end\n * or 0 to find from the start\n * @return {Text} block\n * @uses DFS\n */\n content.getDeepestTextNodeFromPosition = function (block, position) {\n\n /**\n * Clear Block from empty and useless spaces with trim.\n * Such nodes we should remove\n */\n var blockChilds = block.childNodes,\n index,\n node,\n text;\n\n for(index = 0; index < blockChilds.length; index++) {\n\n node = blockChilds[index];\n\n if (node.nodeType == editor.core.nodeTypes.TEXT) {\n\n text = node.textContent.trim();\n\n /** Text is empty. We should remove this child from node before we start DFS\n * decrease the quantity of childs.\n */\n if (text === '') {\n\n block.removeChild(node);\n position--;\n\n }\n\n }\n\n }\n\n if (block.childNodes.length === 0) {\n\n return document.createTextNode('');\n\n }\n\n /** Setting default position when we deleted all empty nodes */\n if ( position < 0 )\n position = 1;\n\n var lookingFromStart = false;\n\n /** For looking from START */\n if (position === 0) {\n\n lookingFromStart = true;\n position = 1;\n\n }\n\n while ( position ) {\n\n /** initial verticle of node. */\n if ( lookingFromStart ) {\n\n block = block.childNodes[0];\n\n } else {\n\n block = block.childNodes[position - 1];\n\n }\n\n if ( block.nodeType == editor.core.nodeTypes.TAG ) {\n\n position = block.childNodes.length;\n\n } else if (block.nodeType == editor.core.nodeTypes.TEXT ) {\n\n position = 0;\n\n }\n\n }\n\n return block;\n\n };\n\n /**\n * @private\n * @param {Element} block - current plugins render\n * @param {String} tool - plugins name\n * @param {Boolean} isStretched - make stretched block or not\n *\n * @description adds necessary information to wrap new created block by first-level holder\n */\n var composeNewBlock_ = function (block, tool, isStretched) {\n\n var newBlock = editor.draw.node('DIV', editor.ui.className.BLOCK_CLASSNAME, {}),\n blockContent = editor.draw.node('DIV', editor.ui.className.BLOCK_CONTENT, {});\n\n blockContent.appendChild(block);\n newBlock.appendChild(blockContent);\n\n if (isStretched) {\n\n blockContent.classList.add(editor.ui.className.BLOCK_STRETCHED);\n\n }\n\n newBlock.dataset.tool = tool;\n return newBlock;\n\n };\n\n /**\n * Returns Range object of current selection\n * @protected\n */\n content.getRange = function () {\n\n var selection = window.getSelection().getRangeAt(0);\n\n return selection;\n\n };\n\n /**\n * Divides block in two blocks (after and before caret)\n *\n * @protected\n * @param {int} inputIndex - target input index\n *\n * @description splits current input content to the separate blocks\n * When enter is pressed among the words, that text will be splited.\n */\n content.splitBlock = function (inputIndex) {\n\n var selection = window.getSelection(),\n anchorNode = selection.anchorNode,\n anchorNodeText = anchorNode.textContent,\n caretOffset = selection.anchorOffset,\n textBeforeCaret,\n textNodeBeforeCaret,\n textAfterCaret,\n textNodeAfterCaret;\n\n var currentBlock = editor.content.currentNode.querySelector('[contentEditable]');\n\n\n textBeforeCaret = anchorNodeText.substring(0, caretOffset);\n textAfterCaret = anchorNodeText.substring(caretOffset);\n\n textNodeBeforeCaret = document.createTextNode(textBeforeCaret);\n\n if (textAfterCaret) {\n\n textNodeAfterCaret = document.createTextNode(textAfterCaret);\n\n }\n\n var previousChilds = [],\n nextChilds = [],\n reachedCurrent = false;\n\n if (textNodeAfterCaret) {\n\n nextChilds.push(textNodeAfterCaret);\n\n }\n\n for ( var i = 0, child; !!(child = currentBlock.childNodes[i]); i++) {\n\n if ( child != anchorNode ) {\n\n if ( !reachedCurrent ) {\n\n previousChilds.push(child);\n\n } else {\n\n nextChilds.push(child);\n\n }\n\n } else {\n\n reachedCurrent = true;\n\n }\n\n }\n\n /** Clear current input */\n editor.state.inputs[inputIndex].innerHTML = '';\n\n /**\n * Append all childs founded before anchorNode\n */\n var previousChildsLength = previousChilds.length;\n\n for(i = 0; i < previousChildsLength; i++) {\n\n editor.state.inputs[inputIndex].appendChild(previousChilds[i]);\n\n }\n\n editor.state.inputs[inputIndex].appendChild(textNodeBeforeCaret);\n\n /**\n * Append text node which is after caret\n */\n var nextChildsLength = nextChilds.length,\n newNode = document.createElement('div');\n\n for(i = 0; i < nextChildsLength; i++) {\n\n newNode.appendChild(nextChilds[i]);\n\n }\n\n newNode = newNode.innerHTML;\n\n /** This type of block creates when enter is pressed */\n var NEW_BLOCK_TYPE = editor.settings.initialBlockPlugin;\n\n /**\n * Make new paragraph with text after caret\n */\n editor.content.insertBlock({\n type : NEW_BLOCK_TYPE,\n block : editor.tools[NEW_BLOCK_TYPE].render({\n text : newNode\n })\n }, true );\n\n };\n\n /**\n * Merges two blocks — current and target\n * If target index is not exist, then previous will be as target\n *\n * @protected\n * @param {int} currentInputIndex\n * @param {int} targetInputIndex\n *\n * @description gets two inputs indexes and merges into one\n */\n content.mergeBlocks = function (currentInputIndex, targetInputIndex) {\n\n /** If current input index is zero, then prevent method execution */\n if (currentInputIndex === 0) {\n\n return;\n\n }\n\n var targetInput,\n currentInputContent = editor.state.inputs[currentInputIndex].innerHTML;\n\n if (!targetInputIndex) {\n\n targetInput = editor.state.inputs[currentInputIndex - 1];\n\n } else {\n\n targetInput = editor.state.inputs[targetInputIndex];\n\n }\n\n targetInput.innerHTML += currentInputContent;\n\n };\n\n /**\n * Iterates all right siblings and parents, which has right siblings\n * while it does not reached the first-level block\n *\n * @param {Element} node\n * @return {boolean}\n */\n content.isLastNode = function (node) {\n\n // console.log('погнали перебор родителей');\n\n var allChecked = false;\n\n while ( !allChecked ) {\n\n // console.log('Смотрим на %o', node);\n // console.log('Проверим, пустые ли соседи справа');\n\n if ( !allSiblingsEmpty_(node) ) {\n\n // console.log('Есть непустые соседи. Узел не последний. Выходим.');\n return false;\n\n }\n\n node = node.parentNode;\n\n /**\n * Проверяем родителей до тех пор, пока не найдем блок первого уровня\n */\n if ( node.classList.contains(editor.ui.className.BLOCK_CONTENT) ) {\n\n allChecked = true;\n\n }\n\n }\n\n return true;\n\n };\n\n /**\n * Checks if all element right siblings is empty\n * @param node\n */\n var allSiblingsEmpty_ = function (node) {\n\n /**\n * Нужно убедиться, что после пустого соседа ничего нет\n */\n var sibling = node.nextSibling;\n\n while ( sibling ) {\n\n if (sibling.textContent.length) {\n\n return false;\n\n }\n\n sibling = sibling.nextSibling;\n\n }\n\n return true;\n\n };\n\n /**\n * @public\n *\n * @param {string} htmlData - html content as string\n * @param {string} plainData - plain text\n * @return {string} - html content as string\n */\n content.wrapTextWithParagraphs = function (htmlData, plainData) {\n\n if (!htmlData.trim()) {\n\n return wrapPlainTextWithParagraphs(plainData);\n\n }\n\n var wrapper = document.createElement('DIV'),\n newWrapper = document.createElement('DIV'),\n i,\n paragraph,\n firstLevelBlocks = ['DIV', 'P'],\n blockTyped,\n node;\n\n /**\n * Make HTML Element to Wrap Text\n * It allows us to work with input data as HTML content\n */\n wrapper.innerHTML = htmlData;\n paragraph = document.createElement('P');\n\n for (i = 0; i < wrapper.childNodes.length; i++) {\n\n node = wrapper.childNodes[i];\n\n blockTyped = firstLevelBlocks.indexOf(node.tagName) != -1;\n\n /**\n * If node is first-levet\n * we add this node to our new wrapper\n */\n if ( blockTyped ) {\n\n /**\n * If we had splitted inline nodes to paragraph before\n */\n if ( paragraph.childNodes.length ) {\n\n newWrapper.appendChild(paragraph.cloneNode(true));\n\n /** empty paragraph */\n paragraph = null;\n paragraph = document.createElement('P');\n\n }\n\n newWrapper.appendChild(node.cloneNode(true));\n\n } else {\n\n /** Collect all inline nodes to one as paragraph */\n paragraph.appendChild(node.cloneNode(true));\n\n /** if node is last we should append this node to paragraph and paragraph to new wrapper */\n if ( i == wrapper.childNodes.length - 1 ) {\n\n newWrapper.appendChild(paragraph.cloneNode(true));\n\n }\n\n }\n\n }\n\n return newWrapper.innerHTML;\n\n };\n\n /**\n * Splits strings on new line and wraps paragraphs with
tag\n * @param plainText\n * @returns {string}\n */\n var wrapPlainTextWithParagraphs = function (plainText) {\n\n if (!plainText) return '';\n\n return '
' + plainText.split('\\n\\n').join('
') + '
';\n\n };\n\n /**\n * Finds closest Contenteditable parent from Element\n * @param {Element} node element looking from\n * @return {Element} node contenteditable\n */\n content.getEditableParent = function (node) {\n\n while (node && node.contentEditable != 'true') {\n\n node = node.parentNode;\n\n }\n\n return node;\n\n };\n\n /**\n * Clear editors content\n *\n * @param {Boolean} all — if true, delete all article data (content, id, etc.)\n */\n content.clear = function (all) {\n\n editor.nodes.redactor.innerHTML = '';\n editor.content.sync();\n editor.ui.saveInputs();\n if (all) {\n\n editor.state.blocks = {};\n\n } else if (editor.state.blocks) {\n\n editor.state.blocks.items = [];\n\n }\n\n editor.content.currentNode = null;\n\n };\n\n /**\n *\n * Load new data to editor\n * If editor is not empty, just append articleData.items\n *\n * @param articleData.items\n */\n content.load = function (articleData) {\n\n var currentContent = Object.assign({}, editor.state.blocks);\n\n editor.content.clear();\n\n if (!Object.keys(currentContent).length) {\n\n editor.state.blocks = articleData;\n\n } else if (!currentContent.items) {\n\n currentContent.items = articleData.items;\n editor.state.blocks = currentContent;\n\n } else {\n\n currentContent.items = currentContent.items.concat(articleData.items);\n editor.state.blocks = currentContent;\n\n }\n\n editor.renderer.makeBlocksFromData();\n\n };\n\n return content;\n\n})({});\n\n\n// WEBPACK FOOTER //\n// ./modules/content.js","/**\n * Codex Editor toolbar module\n *\n * Contains:\n * - Inline toolbox\n * - Toolbox within plus button\n * - Settings section\n *\n * @author Codex Team\n * @version 1.0\n */\n\nmodule.exports = (function (toolbar) {\n\n let editor = codex.editor;\n\n toolbar.settings = require('./settings');\n toolbar.inline = require('./inline');\n toolbar.toolbox = require('./toolbox');\n\n /**\n * Margin between focused node and toolbar\n */\n toolbar.defaultToolbarHeight = 49;\n\n toolbar.defaultOffset = 34;\n\n toolbar.opened = false;\n\n toolbar.current = null;\n\n /**\n * @protected\n */\n toolbar.open = function () {\n\n if (editor.hideToolbar) {\n\n return;\n\n }\n\n let toolType = editor.content.currentNode.dataset.tool;\n\n if (!editor.tools[toolType] || !editor.tools[toolType].makeSettings ) {\n\n editor.nodes.showSettingsButton.classList.add('hide');\n\n } else {\n\n editor.nodes.showSettingsButton.classList.remove('hide');\n\n }\n\n editor.nodes.toolbar.classList.add('opened');\n this.opened = true;\n\n };\n\n /**\n * @protected\n */\n toolbar.close = function () {\n\n editor.nodes.toolbar.classList.remove('opened');\n\n toolbar.opened = false;\n toolbar.current = null;\n\n for (var button in editor.nodes.toolbarButtons) {\n\n editor.nodes.toolbarButtons[button].classList.remove('selected');\n\n }\n\n /** Close toolbox when toolbar is not displayed */\n editor.toolbar.toolbox.close();\n editor.toolbar.settings.close();\n\n };\n\n toolbar.toggle = function () {\n\n if ( !this.opened ) {\n\n this.open();\n\n } else {\n\n this.close();\n\n }\n\n };\n\n toolbar.hidePlusButton = function () {\n\n editor.nodes.plusButton.classList.add('hide');\n\n };\n\n toolbar.showPlusButton = function () {\n\n editor.nodes.plusButton.classList.remove('hide');\n\n };\n\n /**\n * Moving toolbar to the specified node\n */\n toolbar.move = function () {\n\n /** Close Toolbox when we move toolbar */\n editor.toolbar.toolbox.close();\n\n if (!editor.content.currentNode) {\n\n return;\n\n }\n\n var newYCoordinate = editor.content.currentNode.offsetTop - (editor.toolbar.defaultToolbarHeight / 2) + editor.toolbar.defaultOffset;\n\n editor.nodes.toolbar.style.transform = `translate3D(0, ${Math.floor(newYCoordinate)}px, 0)`;\n\n /** Close trash actions */\n editor.toolbar.settings.hideRemoveActions();\n\n };\n\n return toolbar;\n\n})({});\n\n\n\n// WEBPACK FOOTER //\n// ./modules/toolbar/toolbar.js","/**\n * Toolbar settings\n *\n * @version 1.0.5\n */\n\nmodule.exports = (function (settings) {\n\n let editor = codex.editor;\n\n settings.opened = false;\n\n settings.setting = null;\n settings.actions = null;\n\n /**\n * Append and open settings\n */\n settings.open = function (toolType) {\n\n /**\n * Append settings content\n * It's stored in tool.settings\n */\n if ( !editor.tools[toolType] || !editor.tools[toolType].makeSettings ) {\n\n return;\n\n }\n\n /**\n * Draw settings block\n */\n var settingsBlock = editor.tools[toolType].makeSettings();\n\n editor.nodes.pluginSettings.appendChild(settingsBlock);\n\n\n /** Open settings block */\n editor.nodes.blockSettings.classList.add('opened');\n this.opened = true;\n\n };\n\n /**\n * Close and clear settings\n */\n settings.close = function () {\n\n editor.nodes.blockSettings.classList.remove('opened');\n editor.nodes.pluginSettings.innerHTML = '';\n\n this.opened = false;\n\n };\n\n /**\n * @param {string} toolType - plugin type\n */\n settings.toggle = function ( toolType ) {\n\n if ( !this.opened ) {\n\n this.open(toolType);\n\n } else {\n\n this.close();\n\n }\n\n };\n\n /**\n * Here we will draw buttons and add listeners to components\n */\n settings.makeRemoveBlockButton = function () {\n\n var removeBlockWrapper = editor.draw.node('SPAN', 'ce-toolbar__remove-btn', {}),\n settingButton = editor.draw.node('SPAN', 'ce-toolbar__remove-setting', { innerHTML : '' }),\n actionWrapper = editor.draw.node('DIV', 'ce-toolbar__remove-confirmation', {}),\n confirmAction = editor.draw.node('DIV', 'ce-toolbar__remove-confirm', { textContent : 'Удалить блок' }),\n cancelAction = editor.draw.node('DIV', 'ce-toolbar__remove-cancel', { textContent : 'Отмена' });\n\n editor.listeners.add(settingButton, 'click', editor.toolbar.settings.removeButtonClicked, false);\n\n editor.listeners.add(confirmAction, 'click', editor.toolbar.settings.confirmRemovingRequest, false);\n\n editor.listeners.add(cancelAction, 'click', editor.toolbar.settings.cancelRemovingRequest, false);\n\n actionWrapper.appendChild(confirmAction);\n actionWrapper.appendChild(cancelAction);\n\n removeBlockWrapper.appendChild(settingButton);\n removeBlockWrapper.appendChild(actionWrapper);\n\n /** Save setting */\n editor.toolbar.settings.setting = settingButton;\n editor.toolbar.settings.actions = actionWrapper;\n\n return removeBlockWrapper;\n\n };\n\n settings.removeButtonClicked = function () {\n\n var action = editor.toolbar.settings.actions;\n\n if (action.classList.contains('opened')) {\n\n editor.toolbar.settings.hideRemoveActions();\n\n } else {\n\n editor.toolbar.settings.showRemoveActions();\n\n }\n\n editor.toolbar.toolbox.close();\n editor.toolbar.settings.close();\n\n };\n\n settings.cancelRemovingRequest = function () {\n\n editor.toolbar.settings.actions.classList.remove('opened');\n\n };\n\n settings.confirmRemovingRequest = function () {\n\n var currentBlock = editor.content.currentNode,\n firstLevelBlocksCount;\n\n currentBlock.remove();\n\n firstLevelBlocksCount = editor.nodes.redactor.childNodes.length;\n\n /**\n * If all blocks are removed\n */\n if (firstLevelBlocksCount === 0) {\n\n /** update currentNode variable */\n editor.content.currentNode = null;\n\n /** Inserting new empty initial block */\n editor.ui.addInitialBlock();\n\n }\n\n editor.ui.saveInputs();\n\n editor.toolbar.close();\n\n };\n\n settings.showRemoveActions = function () {\n\n editor.toolbar.settings.actions.classList.add('opened');\n\n };\n\n settings.hideRemoveActions = function () {\n\n editor.toolbar.settings.actions.classList.remove('opened');\n\n };\n\n return settings;\n\n})({});\n\n\n\n// WEBPACK FOOTER //\n// ./modules/toolbar/settings.js","/**\n * Inline toolbar\n *\n * Contains from tools:\n * Bold, Italic, Underline and Anchor\n *\n * @author Codex Team\n * @version 1.0\n */\n\nmodule.exports = (function (inline) {\n\n let editor = codex.editor;\n\n inline.buttonsOpened = null;\n inline.actionsOpened = null;\n inline.wrappersOffset = null;\n\n /**\n * saving selection that need for execCommand for styling\n *\n */\n inline.storedSelection = null;\n\n /**\n * @protected\n *\n * Open inline toobar\n */\n inline.show = function () {\n\n var currentNode = editor.content.currentNode,\n tool = currentNode.dataset.tool,\n plugin;\n\n /**\n * tool allowed to open inline toolbar\n */\n plugin = editor.tools[tool];\n\n if (!plugin.showInlineToolbar)\n return;\n\n var selectedText = inline.getSelectionText(),\n toolbar = editor.nodes.inlineToolbar.wrapper;\n\n if (selectedText.length > 0) {\n\n /** Move toolbar and open */\n editor.toolbar.inline.move();\n\n /** Open inline toolbar */\n toolbar.classList.add('opened');\n\n /** show buttons of inline toolbar */\n editor.toolbar.inline.showButtons();\n\n }\n\n };\n\n /**\n * @protected\n *\n * Closes inline toolbar\n */\n inline.close = function () {\n\n var toolbar = editor.nodes.inlineToolbar.wrapper;\n\n toolbar.classList.remove('opened');\n\n };\n\n /**\n * @private\n *\n * Moving toolbar\n */\n inline.move = function () {\n\n if (!this.wrappersOffset) {\n\n this.wrappersOffset = this.getWrappersOffset();\n\n }\n\n var coords = this.getSelectionCoords(),\n defaultOffset = 0,\n toolbar = editor.nodes.inlineToolbar.wrapper,\n newCoordinateX,\n newCoordinateY;\n\n if (toolbar.offsetHeight === 0) {\n\n defaultOffset = 40;\n\n }\n\n newCoordinateX = coords.x - this.wrappersOffset.left;\n newCoordinateY = coords.y + window.scrollY - this.wrappersOffset.top - defaultOffset - toolbar.offsetHeight;\n\n toolbar.style.transform = `translate3D(${Math.floor(newCoordinateX)}px, ${Math.floor(newCoordinateY)}px, 0)`;\n\n /** Close everything */\n editor.toolbar.inline.closeButtons();\n editor.toolbar.inline.closeAction();\n\n };\n\n /**\n * @private\n *\n * Tool Clicked\n */\n\n inline.toolClicked = function (event, type) {\n\n /**\n * For simple tools we use default browser function\n * For more complicated tools, we should write our own behavior\n */\n switch (type) {\n case 'createLink' : editor.toolbar.inline.createLinkAction(event, type); break;\n default : editor.toolbar.inline.defaultToolAction(type); break;\n }\n\n /**\n * highlight buttons\n * after making some action\n */\n editor.nodes.inlineToolbar.buttons.childNodes.forEach(editor.toolbar.inline.hightlight);\n\n };\n\n /**\n * @private\n *\n * Saving wrappers offset in DOM\n */\n inline.getWrappersOffset = function () {\n\n var wrapper = editor.nodes.wrapper,\n offset = this.getOffset(wrapper);\n\n this.wrappersOffset = offset;\n return offset;\n\n };\n\n /**\n * @private\n *\n * Calculates offset of DOM element\n *\n * @param el\n * @returns {{top: number, left: number}}\n */\n inline.getOffset = function ( el ) {\n\n var _x = 0;\n var _y = 0;\n\n while( el && !isNaN( el.offsetLeft ) && !isNaN( el.offsetTop ) ) {\n\n _x += (el.offsetLeft + el.clientLeft);\n _y += (el.offsetTop + el.clientTop);\n el = el.offsetParent;\n\n }\n return { top: _y, left: _x };\n\n };\n\n /**\n * @private\n *\n * Calculates position of selected text\n * @returns {{x: number, y: number}}\n */\n inline.getSelectionCoords = function () {\n\n var sel = document.selection, range;\n var x = 0, y = 0;\n\n if (sel) {\n\n if (sel.type != 'Control') {\n\n range = sel.createRange();\n range.collapse(true);\n x = range.boundingLeft;\n y = range.boundingTop;\n\n }\n\n } else if (window.getSelection) {\n\n sel = window.getSelection();\n\n if (sel.rangeCount) {\n\n range = sel.getRangeAt(0).cloneRange();\n if (range.getClientRects) {\n\n range.collapse(true);\n var rect = range.getClientRects()[0];\n\n if (!rect) {\n\n return;\n\n }\n\n x = rect.left;\n y = rect.top;\n\n }\n\n }\n\n }\n return { x: x, y: y };\n\n };\n\n /**\n * @private\n *\n * Returns selected text as String\n * @returns {string}\n */\n inline.getSelectionText = function () {\n\n var selectedText = '';\n\n // all modern browsers and IE9+\n if (window.getSelection) {\n\n selectedText = window.getSelection().toString();\n\n }\n\n return selectedText;\n\n };\n\n /** Opens buttons block */\n inline.showButtons = function () {\n\n var buttons = editor.nodes.inlineToolbar.buttons;\n\n buttons.classList.add('opened');\n\n editor.toolbar.inline.buttonsOpened = true;\n\n /** highlight buttons */\n editor.nodes.inlineToolbar.buttons.childNodes.forEach(editor.toolbar.inline.hightlight);\n\n };\n\n /** Makes buttons disappear */\n inline.closeButtons = function () {\n\n var buttons = editor.nodes.inlineToolbar.buttons;\n\n buttons.classList.remove('opened');\n\n editor.toolbar.inline.buttonsOpened = false;\n\n };\n\n /** Open buttons defined action if exist */\n inline.showActions = function () {\n\n var action = editor.nodes.inlineToolbar.actions;\n\n action.classList.add('opened');\n\n editor.toolbar.inline.actionsOpened = true;\n\n };\n\n /** Close actions block */\n inline.closeAction = function () {\n\n var action = editor.nodes.inlineToolbar.actions;\n\n action.innerHTML = '';\n action.classList.remove('opened');\n editor.toolbar.inline.actionsOpened = false;\n\n };\n\n\n /**\n * Callback for keydowns in inline toolbar \"Insert link...\" input\n */\n let inlineToolbarAnchorInputKeydown_ = function (event) {\n\n if (event.keyCode != editor.core.keys.ENTER) {\n\n return;\n\n }\n\n let editable = editor.content.currentNode,\n storedSelection = editor.toolbar.inline.storedSelection;\n\n editor.toolbar.inline.restoreSelection(editable, storedSelection);\n editor.toolbar.inline.setAnchor(this.value);\n\n /**\n * Preventing events that will be able to happen\n */\n event.preventDefault();\n event.stopImmediatePropagation();\n\n editor.toolbar.inline.clearRange();\n\n };\n\n /** Action for link creation or for setting anchor */\n inline.createLinkAction = function (event) {\n\n var isActive = this.isLinkActive();\n\n var editable = editor.content.currentNode,\n storedSelection = editor.toolbar.inline.saveSelection(editable);\n\n /** Save globally selection */\n editor.toolbar.inline.storedSelection = storedSelection;\n\n if (isActive) {\n\n\n /**\n * Changing stored selection. if we want to remove anchor from word\n * we should remove anchor from whole word, not only selected part.\n * The solution is than we get the length of current link\n * Change start position to - end of selection minus length of anchor\n */\n editor.toolbar.inline.restoreSelection(editable, storedSelection);\n\n editor.toolbar.inline.defaultToolAction('unlink');\n\n } else {\n\n /** Create input and close buttons */\n var action = editor.draw.inputForLink();\n\n editor.nodes.inlineToolbar.actions.appendChild(action);\n\n editor.toolbar.inline.closeButtons();\n editor.toolbar.inline.showActions();\n\n /**\n * focus to input\n * Solution: https://developer.mozilla.org/ru/docs/Web/API/HTMLElement/focus\n * Prevents event after showing input and when we need to focus an input which is in unexisted form\n */\n action.focus();\n event.preventDefault();\n\n /** Callback to link action */\n editor.listeners.add(action, 'keydown', inlineToolbarAnchorInputKeydown_, false);\n\n }\n\n };\n\n inline.isLinkActive = function () {\n\n var isActive = false;\n\n editor.nodes.inlineToolbar.buttons.childNodes.forEach(function (tool) {\n\n var dataType = tool.dataset.type;\n\n if (dataType == 'link' && tool.classList.contains('hightlighted')) {\n\n isActive = true;\n\n }\n\n });\n\n return isActive;\n\n };\n\n /** default action behavior of tool */\n inline.defaultToolAction = function (type) {\n\n document.execCommand(type, false, null);\n\n };\n\n /**\n * @private\n *\n * Sets URL\n *\n * @param {String} url - URL\n */\n inline.setAnchor = function (url) {\n\n document.execCommand('createLink', false, url);\n\n /** Close after URL inserting */\n editor.toolbar.inline.closeAction();\n\n };\n\n /**\n * @private\n *\n * Saves selection\n */\n inline.saveSelection = function (containerEl) {\n\n var range = window.getSelection().getRangeAt(0),\n preSelectionRange = range.cloneRange(),\n start;\n\n preSelectionRange.selectNodeContents(containerEl);\n preSelectionRange.setEnd(range.startContainer, range.startOffset);\n\n start = preSelectionRange.toString().length;\n\n return {\n start: start,\n end: start + range.toString().length\n };\n\n };\n\n /**\n * @private\n *\n * Sets to previous selection (Range)\n *\n * @param {Element} containerEl - editable element where we restore range\n * @param {Object} savedSel - range basic information to restore\n */\n inline.restoreSelection = function (containerEl, savedSel) {\n\n var range = document.createRange(),\n charIndex = 0;\n\n range.setStart(containerEl, 0);\n range.collapse(true);\n\n var nodeStack = [ containerEl ],\n node,\n foundStart = false,\n stop = false,\n nextCharIndex;\n\n while (!stop && (node = nodeStack.pop())) {\n\n if (node.nodeType == 3) {\n\n nextCharIndex = charIndex + node.length;\n\n if (!foundStart && savedSel.start >= charIndex && savedSel.start <= nextCharIndex) {\n\n range.setStart(node, savedSel.start - charIndex);\n foundStart = true;\n\n }\n if (foundStart && savedSel.end >= charIndex && savedSel.end <= nextCharIndex) {\n\n range.setEnd(node, savedSel.end - charIndex);\n stop = true;\n\n }\n charIndex = nextCharIndex;\n\n } else {\n\n var i = node.childNodes.length;\n\n while (i--) {\n\n nodeStack.push(node.childNodes[i]);\n\n }\n\n }\n\n }\n\n var sel = window.getSelection();\n\n sel.removeAllRanges();\n sel.addRange(range);\n\n };\n\n /**\n * @private\n *\n * Removes all ranges from window selection\n */\n inline.clearRange = function () {\n\n var selection = window.getSelection();\n\n selection.removeAllRanges();\n\n };\n\n /**\n * @private\n *\n * sets or removes hightlight\n */\n inline.hightlight = function (tool) {\n\n var dataType = tool.dataset.type;\n\n if (document.queryCommandState(dataType)) {\n\n editor.toolbar.inline.setButtonHighlighted(tool);\n\n } else {\n\n editor.toolbar.inline.removeButtonsHighLight(tool);\n\n }\n\n /**\n *\n * hightlight for anchors\n */\n var selection = window.getSelection(),\n tag = selection.anchorNode.parentNode;\n\n if (tag.tagName == 'A' && dataType == 'link') {\n\n editor.toolbar.inline.setButtonHighlighted(tool);\n\n }\n\n };\n\n /**\n * @private\n *\n * Mark button if text is already executed\n */\n inline.setButtonHighlighted = function (button) {\n\n button.classList.add('hightlighted');\n\n /** At link tool we also change icon */\n if (button.dataset.type == 'link') {\n\n var icon = button.childNodes[0];\n\n icon.classList.remove('ce-icon-link');\n icon.classList.add('ce-icon-unlink');\n\n }\n\n };\n\n /**\n * @private\n *\n * Removes hightlight\n */\n inline.removeButtonsHighLight = function (button) {\n\n button.classList.remove('hightlighted');\n\n /** At link tool we also change icon */\n if (button.dataset.type == 'link') {\n\n var icon = button.childNodes[0];\n\n icon.classList.remove('ce-icon-unlink');\n icon.classList.add('ce-icon-link');\n\n }\n\n };\n\n\n return inline;\n\n})({});\n\n\n// WEBPACK FOOTER //\n// ./modules/toolbar/inline.js","/**\n * Codex Editor toolbox\n *\n * All tools be able to appended here\n *\n * @author Codex Team\n * @version 1.0\n */\n\nmodule.exports = (function (toolbox) {\n\n let editor = codex.editor;\n\n toolbox.opened = false;\n toolbox.openedOnBlock = null;\n\n /** Shows toolbox */\n toolbox.open = function () {\n\n /** Close setting if toolbox is opened */\n if (editor.toolbar.settings.opened) {\n\n editor.toolbar.settings.close();\n\n }\n\n /** Add 'toolbar-opened' class for current block **/\n toolbox.openedOnBlock = editor.content.currentNode;\n toolbox.openedOnBlock.classList.add('toolbar-opened');\n\n /** display toolbox */\n editor.nodes.toolbox.classList.add('opened');\n\n /** Animate plus button */\n editor.nodes.plusButton.classList.add('clicked');\n\n /** toolbox state */\n editor.toolbar.toolbox.opened = true;\n\n };\n\n /** Closes toolbox */\n toolbox.close = function () {\n\n /** Remove 'toolbar-opened' class from current block **/\n if (toolbox.openedOnBlock) toolbox.openedOnBlock.classList.remove('toolbar-opened');\n toolbox.openedOnBlock = null;\n\n /** Makes toolbox disappear */\n editor.nodes.toolbox.classList.remove('opened');\n\n /** Rotate plus button */\n editor.nodes.plusButton.classList.remove('clicked');\n\n /** toolbox state */\n editor.toolbar.toolbox.opened = false;\n\n editor.toolbar.current = null;\n\n };\n\n toolbox.leaf = function () {\n\n let currentTool = editor.toolbar.current,\n tools = Object.keys(editor.tools),\n barButtons = editor.nodes.toolbarButtons,\n nextToolIndex = 0,\n toolToSelect,\n visibleTool,\n tool;\n\n if ( !currentTool ) {\n\n /** Get first tool from object*/\n for(tool in editor.tools) {\n\n if (editor.tools[tool].displayInToolbox) {\n\n break;\n\n }\n\n nextToolIndex ++;\n\n }\n\n } else {\n\n nextToolIndex = (tools.indexOf(currentTool) + 1) % tools.length;\n visibleTool = tools[nextToolIndex];\n\n while (!editor.tools[visibleTool].displayInToolbox) {\n\n nextToolIndex = (nextToolIndex + 1) % tools.length;\n visibleTool = tools[nextToolIndex];\n\n }\n\n }\n\n toolToSelect = tools[nextToolIndex];\n\n for ( var button in barButtons ) {\n\n barButtons[button].classList.remove('selected');\n\n }\n\n barButtons[toolToSelect].classList.add('selected');\n editor.toolbar.current = toolToSelect;\n\n };\n\n /**\n * Transforming selected node type into selected toolbar element type\n * @param {event} event\n */\n toolbox.toolClicked = function (event) {\n\n /**\n * UNREPLACEBLE_TOOLS this types of tools are forbidden to replace even they are empty\n */\n var UNREPLACEBLE_TOOLS = ['image', 'link', 'list', 'instagram', 'twitter', 'embed'],\n tool = editor.tools[editor.toolbar.current],\n workingNode = editor.content.currentNode,\n currentInputIndex = editor.caret.inputIndex,\n newBlockContent,\n appendCallback,\n blockData;\n\n /** Make block from plugin */\n newBlockContent = tool.render();\n\n /** information about block */\n blockData = {\n block : newBlockContent,\n type : tool.type,\n stretched : false\n };\n\n if (\n workingNode &&\n UNREPLACEBLE_TOOLS.indexOf(workingNode.dataset.tool) === -1 &&\n workingNode.textContent.trim() === ''\n ) {\n\n /** Replace current block */\n editor.content.switchBlock(workingNode, newBlockContent, tool.type);\n\n } else {\n\n /** Insert new Block from plugin */\n editor.content.insertBlock(blockData);\n\n /** increase input index */\n currentInputIndex++;\n\n }\n\n /** Fire tool append callback */\n appendCallback = tool.appendCallback;\n\n if (appendCallback && typeof appendCallback == 'function') {\n\n appendCallback.call(event);\n\n }\n\n window.setTimeout(function () {\n\n /** Set caret to current block */\n editor.caret.setToBlock(currentInputIndex);\n\n }, 10);\n\n\n /**\n * Changing current Node\n */\n editor.content.workingNodeChanged();\n\n /**\n * Move toolbar when node is changed\n */\n editor.toolbar.move();\n\n };\n\n return toolbox;\n\n})({});\n\n\n// WEBPACK FOOTER //\n// ./modules/toolbar/toolbox.js","/**\n * @module Codex Editor Callbacks module\n * @description Module works with editor added Elements\n *\n * @author Codex Team\n * @version 1.4.0\n */\n\nmodule.exports = (function (callbacks) {\n\n let editor = codex.editor;\n\n /**\n * used by UI module\n * @description Routes all keydowns on document\n * @param {Object} event\n */\n callbacks.globalKeydown = function (event) {\n\n switch (event.keyCode) {\n case editor.core.keys.ENTER : enterKeyPressed_(event); break;\n }\n\n };\n\n /**\n * used by UI module\n * @description Routes all keydowns on redactors area\n * @param {Object} event\n */\n callbacks.redactorKeyDown = function (event) {\n\n switch (event.keyCode) {\n case editor.core.keys.TAB : tabKeyPressedOnRedactorsZone_(event); break;\n case editor.core.keys.ENTER : enterKeyPressedOnRedactorsZone_(event); break;\n case editor.core.keys.ESC : escapeKeyPressedOnRedactorsZone_(event); break;\n default : defaultKeyPressedOnRedactorsZone_(event); break;\n }\n\n };\n\n /**\n * used by UI module\n * @description Routes all keyup events\n * @param {Object} event\n */\n callbacks.globalKeyup = function (event) {\n\n switch (event.keyCode) {\n case editor.core.keys.UP :\n case editor.core.keys.LEFT :\n case editor.core.keys.RIGHT :\n case editor.core.keys.DOWN : arrowKeyPressed_(event); break;\n }\n\n };\n\n /**\n * @param {Object} event\n * @private\n *\n * Handles behaviour when tab pressed\n * @description if Content is empty show toolbox (if it is closed) or leaf tools\n * uses Toolbars toolbox module to handle the situation\n */\n var tabKeyPressedOnRedactorsZone_ = function (event) {\n\n /**\n * Wait for solution. Would like to know the behaviour\n * @todo Add spaces\n */\n event.preventDefault();\n\n\n if (!editor.core.isBlockEmpty(editor.content.currentNode)) {\n\n return;\n\n }\n\n if ( !editor.toolbar.opened ) {\n\n editor.toolbar.open();\n\n }\n\n if (editor.toolbar.opened && !editor.toolbar.toolbox.opened) {\n\n editor.toolbar.toolbox.open();\n\n } else {\n\n editor.toolbar.toolbox.leaf();\n\n }\n\n };\n\n /**\n * Handles global EnterKey Press\n * @see enterPressedOnBlock_\n * @param {Object} event\n */\n var enterKeyPressed_ = function () {\n\n if (editor.content.editorAreaHightlighted) {\n\n /**\n * it means that we lose input index, saved index before is not correct\n * therefore we need to set caret when we insert new block\n */\n editor.caret.inputIndex = -1;\n\n enterPressedOnBlock_();\n\n }\n\n };\n\n /**\n * Callback for enter key pressing in first-level block area\n *\n * @param {Event} event\n * @private\n *\n * @description Inserts new block with initial type from settings\n */\n var enterPressedOnBlock_ = function () {\n\n var NEW_BLOCK_TYPE = editor.settings.initialBlockPlugin;\n\n editor.content.insertBlock({\n type : NEW_BLOCK_TYPE,\n block : editor.tools[NEW_BLOCK_TYPE].render()\n }, true );\n\n editor.toolbar.move();\n editor.toolbar.open();\n\n };\n\n\n /**\n * ENTER key handler\n *\n * @param {Object} event\n * @private\n *\n * @description Makes new block with initial type from settings\n */\n var enterKeyPressedOnRedactorsZone_ = function (event) {\n\n if (event.target.contentEditable == 'true') {\n\n /** Update input index */\n editor.caret.saveCurrentInputIndex();\n\n }\n\n var currentInputIndex = editor.caret.getCurrentInputIndex() || 0,\n workingNode = editor.content.currentNode,\n tool = workingNode.dataset.tool,\n isEnterPressedOnToolbar = editor.toolbar.opened &&\n editor.toolbar.current &&\n event.target == editor.state.inputs[currentInputIndex];\n\n /** The list of tools which needs the default browser behaviour */\n var enableLineBreaks = editor.tools[tool].enableLineBreaks;\n\n /** This type of block creates when enter is pressed */\n var NEW_BLOCK_TYPE = editor.settings.initialBlockPlugin;\n\n /**\n * When toolbar is opened, select tool instead of making new paragraph\n */\n if ( isEnterPressedOnToolbar ) {\n\n event.preventDefault();\n\n editor.toolbar.toolbox.toolClicked(event);\n\n editor.toolbar.close();\n\n /**\n * Stop other listeners callback executions\n */\n event.stopPropagation();\n event.stopImmediatePropagation();\n\n return;\n\n }\n\n /**\n * Allow paragraph lineBreaks with shift enter\n * Or if shiftkey pressed and enter and enabledLineBreaks, the let new block creation\n */\n if ( event.shiftKey || enableLineBreaks ) {\n\n event.stopPropagation();\n event.stopImmediatePropagation();\n return;\n\n }\n\n var currentSelection = window.getSelection(),\n currentSelectedNode = currentSelection.anchorNode,\n caretAtTheEndOfText = editor.caret.position.atTheEnd(),\n isTextNodeHasParentBetweenContenteditable = false;\n\n /**\n * Allow making new
in same block by SHIFT+ENTER and forbids to prevent default browser behaviour\n */\n if ( event.shiftKey && !enableLineBreaks ) {\n\n editor.callback.enterPressedOnBlock(editor.content.currentBlock, event);\n event.preventDefault();\n return;\n\n }\n\n /**\n * Workaround situation when caret at the Text node that has some wrapper Elements\n * Split block cant handle this.\n * We need to save default behavior\n */\n isTextNodeHasParentBetweenContenteditable = currentSelectedNode && currentSelectedNode.parentNode.contentEditable != 'true';\n\n /**\n * Split blocks when input has several nodes and caret placed in textNode\n */\n if (\n currentSelectedNode.nodeType == editor.core.nodeTypes.TEXT &&\n !isTextNodeHasParentBetweenContenteditable &&\n !caretAtTheEndOfText\n ) {\n\n event.preventDefault();\n\n editor.core.log('Splitting Text node...');\n\n editor.content.splitBlock(currentInputIndex);\n\n /** Show plus button when next input after split is empty*/\n if (!editor.state.inputs[currentInputIndex + 1].textContent.trim()) {\n\n editor.toolbar.showPlusButton();\n\n }\n\n } else {\n\n var islastNode = editor.content.isLastNode(currentSelectedNode);\n\n if ( islastNode && caretAtTheEndOfText ) {\n\n event.preventDefault();\n event.stopPropagation();\n event.stopImmediatePropagation();\n\n editor.core.log('ENTER clicked in last textNode. Create new BLOCK');\n\n editor.content.insertBlock({\n type: NEW_BLOCK_TYPE,\n block: editor.tools[NEW_BLOCK_TYPE].render()\n }, true);\n\n editor.toolbar.move();\n editor.toolbar.open();\n\n /** Show plus button with empty block */\n editor.toolbar.showPlusButton();\n\n }\n\n }\n\n /** get all inputs after new appending block */\n editor.ui.saveInputs();\n\n };\n\n /**\n * Escape behaviour\n * @param event\n * @private\n *\n * @description Closes toolbox and toolbar. Prevents default behaviour\n */\n var escapeKeyPressedOnRedactorsZone_ = function (event) {\n\n /** Close all toolbar */\n editor.toolbar.close();\n\n /** Close toolbox */\n editor.toolbar.toolbox.close();\n\n event.preventDefault();\n\n };\n\n /**\n * @param {Event} event\n * @private\n *\n * closes and moves toolbar\n */\n var arrowKeyPressed_ = function (event) {\n\n editor.content.workingNodeChanged();\n\n /* Closing toolbar */\n editor.toolbar.close();\n editor.toolbar.move();\n\n };\n\n /**\n * @private\n * @param {Event} event\n *\n * @description Closes all opened bars from toolbar.\n * If block is mark, clears highlightning\n */\n var defaultKeyPressedOnRedactorsZone_ = function () {\n\n editor.toolbar.close();\n\n if (!editor.toolbar.inline.actionsOpened) {\n\n editor.toolbar.inline.close();\n editor.content.clearMark();\n\n }\n\n };\n\n /**\n * Handler when clicked on redactors area\n *\n * @protected\n * @param event\n *\n * @description Detects clicked area. If it is first-level block area, marks as detected and\n * on next enter press will be inserted new block\n * Otherwise, save carets position (input index) and put caret to the editable zone.\n *\n * @see detectWhenClickedOnFirstLevelBlockArea_\n *\n */\n callbacks.redactorClicked = function (event) {\n\n detectWhenClickedOnFirstLevelBlockArea_();\n\n editor.content.workingNodeChanged(event.target);\n editor.ui.saveInputs();\n\n var selectedText = editor.toolbar.inline.getSelectionText(),\n firstLevelBlock;\n\n /** If selection range took off, then we hide inline toolbar */\n if (selectedText.length === 0) {\n\n editor.toolbar.inline.close();\n\n }\n\n /** Update current input index in memory when caret focused into existed input */\n if (event.target.contentEditable == 'true') {\n\n editor.caret.saveCurrentInputIndex();\n\n }\n\n if (editor.content.currentNode === null) {\n\n /**\n * If inputs in redactor does not exits, then we put input index 0 not -1\n */\n var indexOfLastInput = editor.state.inputs.length > 0 ? editor.state.inputs.length - 1 : 0;\n\n /** If we have any inputs */\n if (editor.state.inputs.length) {\n\n /** getting firstlevel parent of input */\n firstLevelBlock = editor.content.getFirstLevelBlock(editor.state.inputs[indexOfLastInput]);\n\n }\n\n /** If input is empty, then we set caret to the last input */\n if (editor.state.inputs.length && editor.state.inputs[indexOfLastInput].textContent === '' && firstLevelBlock.dataset.tool == editor.settings.initialBlockPlugin) {\n\n editor.caret.setToBlock(indexOfLastInput);\n\n } else {\n\n /** Create new input when caret clicked in redactors area */\n var NEW_BLOCK_TYPE = editor.settings.initialBlockPlugin;\n\n editor.content.insertBlock({\n type : NEW_BLOCK_TYPE,\n block : editor.tools[NEW_BLOCK_TYPE].render()\n });\n\n /** If there is no inputs except inserted */\n if (editor.state.inputs.length === 1) {\n\n editor.caret.setToBlock(indexOfLastInput);\n\n } else {\n\n /** Set caret to this appended input */\n editor.caret.setToNextBlock(indexOfLastInput);\n\n }\n\n }\n\n } else {\n\n /** Close all panels */\n editor.toolbar.settings.close();\n editor.toolbar.toolbox.close();\n\n }\n\n /**\n * Move toolbar and open\n */\n editor.toolbar.move();\n editor.toolbar.open();\n\n var inputIsEmpty = !editor.content.currentNode.textContent.trim(),\n currentNodeType = editor.content.currentNode.dataset.tool,\n isInitialType = currentNodeType == editor.settings.initialBlockPlugin;\n\n\n /** Hide plus buttons */\n editor.toolbar.hidePlusButton();\n\n if (!inputIsEmpty) {\n\n /** Mark current block */\n editor.content.markBlock();\n\n }\n\n if ( isInitialType && inputIsEmpty ) {\n\n /** Show plus button */\n editor.toolbar.showPlusButton();\n\n }\n\n\n };\n\n /**\n * This method allows to define, is caret in contenteditable element or not.\n *\n * @private\n *\n * @description Otherwise, if we get TEXT node from range container, that will means we have input index.\n * In this case we use default browsers behaviour (if plugin allows that) or overwritten action.\n * Therefore, to be sure that we've clicked first-level block area, we should have currentNode, which always\n * specifies to the first-level block. Other cases we just ignore.\n */\n var detectWhenClickedOnFirstLevelBlockArea_ = function () {\n\n var selection = window.getSelection(),\n anchorNode = selection.anchorNode,\n flag = false;\n\n if (selection.rangeCount === 0) {\n\n editor.content.editorAreaHightlighted = true;\n\n } else {\n\n if (!editor.core.isDomNode(anchorNode)) {\n\n anchorNode = anchorNode.parentNode;\n\n }\n\n /** Already founded, without loop */\n if (anchorNode.contentEditable == 'true') {\n\n flag = true;\n\n }\n\n while (anchorNode.contentEditable != 'true') {\n\n anchorNode = anchorNode.parentNode;\n\n if (anchorNode.contentEditable == 'true') {\n\n flag = true;\n\n }\n\n if (anchorNode == document.body) {\n\n break;\n\n }\n\n }\n\n /** If editable element founded, flag is \"TRUE\", Therefore we return \"FALSE\" */\n editor.content.editorAreaHightlighted = !flag;\n\n }\n\n };\n\n /**\n * Toolbar button click handler\n *\n * @param {Object} event - cursor to the button\n * @protected\n *\n * @description gets current tool and calls render method\n */\n callbacks.toolbarButtonClicked = function (event) {\n\n var button = this;\n\n editor.toolbar.current = button.dataset.type;\n\n editor.toolbar.toolbox.toolClicked(event);\n editor.toolbar.close();\n\n };\n\n /**\n * Show or Hide toolbox when plus button is clicked\n */\n callbacks.plusButtonClicked = function () {\n\n if (!editor.nodes.toolbox.classList.contains('opened')) {\n\n editor.toolbar.toolbox.open();\n\n } else {\n\n editor.toolbar.toolbox.close();\n\n }\n\n };\n\n /**\n * Block handlers for KeyDown events\n *\n * @protected\n * @param {Object} event\n *\n * Handles keydowns on block\n * @see blockRightOrDownArrowPressed_\n * @see backspacePressed_\n * @see blockLeftOrUpArrowPressed_\n */\n callbacks.blockKeydown = function (event) {\n\n let block = event.target; // event.target is input\n\n switch (event.keyCode) {\n\n case editor.core.keys.DOWN:\n case editor.core.keys.RIGHT:\n blockRightOrDownArrowPressed_(event);\n break;\n\n case editor.core.keys.BACKSPACE:\n backspacePressed_(block, event);\n break;\n\n case editor.core.keys.UP:\n case editor.core.keys.LEFT:\n blockLeftOrUpArrowPressed_(event);\n break;\n\n }\n\n };\n\n /**\n * RIGHT or DOWN keydowns on block\n *\n * @param {Object} event\n * @private\n *\n * @description watches the selection and gets closest editable element.\n * Uses method getDeepestTextNodeFromPosition to get the last node of next block\n * Sets caret if it is contenteditable\n */\n var blockRightOrDownArrowPressed_ = function (event) {\n\n var selection = window.getSelection(),\n inputs = editor.state.inputs,\n focusedNode = selection.anchorNode,\n focusedNodeHolder;\n\n /** Check for caret existance */\n if (!focusedNode) {\n\n return false;\n\n }\n\n /** Looking for closest (parent) contentEditable element of focused node */\n while (focusedNode.contentEditable != 'true') {\n\n focusedNodeHolder = focusedNode.parentNode;\n focusedNode = focusedNodeHolder;\n\n }\n\n /** Input index in DOM level */\n var editableElementIndex = 0;\n\n while (focusedNode != inputs[editableElementIndex]) {\n\n editableElementIndex ++;\n\n }\n\n /**\n * Founded contentEditable element doesn't have childs\n * Or maybe New created block\n */\n if (!focusedNode.textContent) {\n\n editor.caret.setToNextBlock(editableElementIndex);\n return;\n\n }\n\n /**\n * Do nothing when caret doesn not reaches the end of last child\n */\n var caretInLastChild = false,\n caretAtTheEndOfText = false;\n\n var lastChild,\n deepestTextnode;\n\n lastChild = focusedNode.childNodes[focusedNode.childNodes.length - 1 ];\n\n if (editor.core.isDomNode(lastChild)) {\n\n deepestTextnode = editor.content.getDeepestTextNodeFromPosition(lastChild, lastChild.childNodes.length);\n\n } else {\n\n deepestTextnode = lastChild;\n\n }\n\n caretInLastChild = selection.anchorNode == deepestTextnode;\n caretAtTheEndOfText = deepestTextnode.length == selection.anchorOffset;\n\n if ( !caretInLastChild || !caretAtTheEndOfText ) {\n\n editor.core.log('arrow [down|right] : caret does not reached the end');\n return false;\n\n }\n\n editor.caret.setToNextBlock(editableElementIndex);\n\n };\n\n /**\n * LEFT or UP keydowns on block\n *\n * @param {Object} event\n * @private\n *\n * watches the selection and gets closest editable element.\n * Uses method getDeepestTextNodeFromPosition to get the last node of previous block\n * Sets caret if it is contenteditable\n *\n */\n var blockLeftOrUpArrowPressed_ = function (event) {\n\n var selection = window.getSelection(),\n inputs = editor.state.inputs,\n focusedNode = selection.anchorNode,\n focusedNodeHolder;\n\n /** Check for caret existance */\n if (!focusedNode) {\n\n return false;\n\n }\n\n /**\n * LEFT or UP not at the beginning\n */\n if ( selection.anchorOffset !== 0) {\n\n return false;\n\n }\n\n /** Looking for parent contentEditable block */\n while (focusedNode.contentEditable != 'true') {\n\n focusedNodeHolder = focusedNode.parentNode;\n focusedNode = focusedNodeHolder;\n\n }\n\n /** Input index in DOM level */\n var editableElementIndex = 0;\n\n while (focusedNode != inputs[editableElementIndex]) {\n\n editableElementIndex ++;\n\n }\n\n /**\n * Do nothing if caret is not at the beginning of first child\n */\n var caretInFirstChild = false,\n caretAtTheBeginning = false;\n\n var firstChild,\n deepestTextnode;\n\n /**\n * Founded contentEditable element doesn't have childs\n * Or maybe New created block\n */\n if (!focusedNode.textContent) {\n\n editor.caret.setToPreviousBlock(editableElementIndex);\n return;\n\n }\n\n firstChild = focusedNode.childNodes[0];\n\n if (editor.core.isDomNode(firstChild)) {\n\n deepestTextnode = editor.content.getDeepestTextNodeFromPosition(firstChild, 0);\n\n } else {\n\n deepestTextnode = firstChild;\n\n }\n\n caretInFirstChild = selection.anchorNode == deepestTextnode;\n caretAtTheBeginning = selection.anchorOffset === 0;\n\n if ( caretInFirstChild && caretAtTheBeginning ) {\n\n editor.caret.setToPreviousBlock(editableElementIndex);\n\n }\n\n };\n\n /**\n * Handles backspace keydown\n *\n * @param {Element} block\n * @param {Object} event\n * @private\n *\n * @description if block is empty, delete the block and set caret to the previous block\n * If block is not empty, try to merge two blocks - current and previous\n * But it we try'n to remove first block, then we should set caret to the next block, not previous.\n * If we removed the last block, create new one\n */\n var backspacePressed_ = function (block, event) {\n\n var currentInputIndex = editor.caret.getCurrentInputIndex(),\n range,\n selectionLength,\n firstLevelBlocksCount;\n\n if (editor.core.isNativeInput(event.target)) {\n\n /** If input value is empty - remove block */\n if (event.target.value.trim() == '') {\n\n block.remove();\n\n } else {\n\n return;\n\n }\n\n }\n\n if (block.textContent.trim()) {\n\n range = editor.content.getRange();\n selectionLength = range.endOffset - range.startOffset;\n\n if (editor.caret.position.atStart() && !selectionLength && editor.state.inputs[currentInputIndex - 1]) {\n\n editor.content.mergeBlocks(currentInputIndex);\n\n } else {\n\n return;\n\n }\n\n }\n\n if (!selectionLength) {\n\n block.remove();\n\n }\n\n\n firstLevelBlocksCount = editor.nodes.redactor.childNodes.length;\n\n /**\n * If all blocks are removed\n */\n if (firstLevelBlocksCount === 0) {\n\n /** update currentNode variable */\n editor.content.currentNode = null;\n\n /** Inserting new empty initial block */\n editor.ui.addInitialBlock();\n\n /** Updating inputs state after deleting last block */\n editor.ui.saveInputs();\n\n /** Set to current appended block */\n window.setTimeout(function () {\n\n editor.caret.setToPreviousBlock(1);\n\n }, 10);\n\n } else {\n\n if (editor.caret.inputIndex !== 0) {\n\n /** Target block is not first */\n editor.caret.setToPreviousBlock(editor.caret.inputIndex);\n\n } else {\n\n /** If we try to delete first block */\n editor.caret.setToNextBlock(editor.caret.inputIndex);\n\n }\n\n }\n\n editor.toolbar.move();\n\n if (!editor.toolbar.opened) {\n\n editor.toolbar.open();\n\n }\n\n /** Updating inputs state */\n editor.ui.saveInputs();\n\n /** Prevent default browser behaviour */\n event.preventDefault();\n\n };\n\n /**\n * used by UI module\n * Clicks on block settings button\n *\n * @param {Object} event\n * @protected\n * @description Opens toolbar settings\n */\n callbacks.showSettingsButtonClicked = function (event) {\n\n /**\n * Get type of current block\n * It uses to append settings from tool.settings property.\n * ...\n * Type is stored in data-type attribute on block\n */\n var currentToolType = editor.content.currentNode.dataset.tool;\n\n editor.toolbar.settings.toggle(currentToolType);\n\n /** Close toolbox when settings button is active */\n editor.toolbar.toolbox.close();\n editor.toolbar.settings.hideRemoveActions();\n\n };\n\n return callbacks;\n\n})({});\n\n\n// WEBPACK FOOTER //\n// ./modules/callbacks.js","/**\n * Codex Editor Draw module\n *\n * @author Codex Team\n * @version 1.0.\n */\n\nmodule.exports = (function (draw) {\n\n /**\n * Base editor wrapper\n */\n draw.wrapper = function () {\n\n var wrapper = document.createElement('div');\n\n wrapper.className += 'codex-editor';\n\n return wrapper;\n\n };\n\n /**\n * Content-editable holder\n */\n draw.redactor = function () {\n\n var redactor = document.createElement('div');\n\n redactor.className += 'ce-redactor';\n\n return redactor;\n\n };\n\n draw.ceBlock = function () {\n\n var block = document.createElement('DIV');\n\n block.className += 'ce_block';\n\n return block;\n\n };\n\n /**\n * Empty toolbar with toggler\n */\n draw.toolbar = function () {\n\n var bar = document.createElement('div');\n\n bar.className += 'ce-toolbar';\n\n return bar;\n\n };\n\n draw.toolbarContent = function () {\n\n var wrapper = document.createElement('DIV');\n\n wrapper.classList.add('ce-toolbar__content');\n\n return wrapper;\n\n };\n\n /**\n * Inline toolbar\n */\n draw.inlineToolbar = function () {\n\n var bar = document.createElement('DIV');\n\n bar.className += 'ce-toolbar-inline';\n\n return bar;\n\n };\n\n /**\n * Wrapper for inline toobar buttons\n */\n draw.inlineToolbarButtons = function () {\n\n var wrapper = document.createElement('DIV');\n\n wrapper.className += 'ce-toolbar-inline__buttons';\n\n return wrapper;\n\n };\n\n /**\n * For some actions\n */\n draw.inlineToolbarActions = function () {\n\n var wrapper = document.createElement('DIV');\n\n wrapper.className += 'ce-toolbar-inline__actions';\n\n return wrapper;\n\n };\n\n draw.inputForLink = function () {\n\n var input = document.createElement('INPUT');\n\n input.type = 'input';\n input.className += 'inputForLink';\n input.placeholder = 'Вставьте ссылку ...';\n input.setAttribute('form', 'defaultForm');\n\n input.setAttribute('autofocus', 'autofocus');\n\n return input;\n\n };\n\n /**\n * @todo Desc\n */\n draw.blockButtons = function () {\n\n var block = document.createElement('div');\n\n block.className += 'ce-toolbar__actions';\n\n return block;\n\n };\n\n /**\n * Block settings panel\n */\n draw.blockSettings = function () {\n\n var settings = document.createElement('div');\n\n settings.className += 'ce-settings';\n\n return settings;\n\n };\n\n draw.defaultSettings = function () {\n\n var div = document.createElement('div');\n\n div.classList.add('ce-settings_default');\n\n return div;\n\n };\n\n draw.pluginsSettings = function () {\n\n var div = document.createElement('div');\n\n div.classList.add('ce-settings_plugin');\n\n return div;\n\n };\n\n draw.plusButton = function () {\n\n var button = document.createElement('span');\n\n button.className = 'ce-toolbar__plus';\n // button.innerHTML = '';\n\n return button;\n\n };\n\n /**\n * Settings button in toolbar\n */\n draw.settingsButton = function () {\n\n var toggler = document.createElement('span');\n\n toggler.className = 'ce-toolbar__settings-btn';\n\n /** Toggler button*/\n toggler.innerHTML = '';\n\n return toggler;\n\n };\n\n /**\n * Redactor tools wrapper\n */\n\n draw.toolbox = function () {\n\n var wrapper = document.createElement('div');\n\n wrapper.className = 'ce-toolbar__tools';\n\n return wrapper;\n\n };\n\n /**\n * @protected\n *\n * Draws tool buttons for toolbox\n *\n * @param {String} type\n * @param {String} classname\n * @returns {Element}\n */\n draw.toolbarButton = function (type, classname) {\n\n var button = document.createElement('li'),\n toolIcon = document.createElement('i'),\n toolTitle = document.createElement('span');\n\n button.dataset.type = type;\n button.setAttribute('title', type);\n\n toolIcon.classList.add(classname);\n toolTitle.classList.add('ce_toolbar_tools--title');\n\n\n button.appendChild(toolIcon);\n button.appendChild(toolTitle);\n\n return button;\n\n };\n\n /**\n * @protected\n *\n * Draws tools for inline toolbar\n *\n * @param {String} type\n * @param {String} classname\n */\n draw.toolbarButtonInline = function (type, classname) {\n\n var button = document.createElement('BUTTON'),\n toolIcon = document.createElement('I');\n\n button.type = 'button';\n button.dataset.type = type;\n toolIcon.classList.add(classname);\n\n button.appendChild(toolIcon);\n\n return button;\n\n };\n\n /**\n * Redactor block\n */\n draw.block = function (tagName, content) {\n\n var node = document.createElement(tagName);\n\n node.innerHTML = content || '';\n\n return node;\n\n };\n\n /**\n * Creates Node with passed tagName and className\n * @param {string} tagName\n * @param {string} className\n * @param {object} properties - allow to assign properties\n */\n draw.node = function ( tagName, className, properties ) {\n\n var el = document.createElement( tagName );\n\n if ( className ) el.className = className;\n\n if ( properties ) {\n\n for (var name in properties) {\n\n el[name] = properties[name];\n\n }\n\n }\n\n return el;\n\n };\n\n /**\n * Unavailable plugin block\n */\n draw.unavailableBlock = function () {\n\n var wrapper = document.createElement('DIV');\n\n wrapper.classList.add('cdx-unavailable-block');\n\n return wrapper;\n\n };\n\n return draw;\n\n})({});\n\n\n// WEBPACK FOOTER //\n// ./modules/draw.js","/**\n * Codex Editor Caret Module\n *\n * @author Codex Team\n * @version 1.0\n */\n\nmodule.exports = (function (caret) {\n\n let editor = codex.editor;\n\n /**\n * @var {int} InputIndex - editable element in DOM\n */\n caret.inputIndex = null;\n\n /**\n * @var {int} offset - caret position in a text node.\n */\n caret.offset = null;\n\n /**\n * @var {int} focusedNodeIndex - we get index of child node from first-level block\n */\n caret.focusedNodeIndex = null;\n\n /**\n * Creates Document Range and sets caret to the element.\n * @protected\n * @uses caret.save — if you need to save caret position\n * @param {Element} el - Changed Node.\n */\n caret.set = function ( el, index, offset) {\n\n offset = offset || caret.offset || 0;\n index = index || caret.focusedNodeIndex || 0;\n\n var childs = el.childNodes,\n nodeToSet;\n\n if ( childs.length === 0 ) {\n\n nodeToSet = el;\n\n } else {\n\n nodeToSet = childs[index];\n\n }\n\n /** If Element is INPUT */\n if (el.contentEditable != 'true') {\n\n el.focus();\n return;\n\n }\n\n if (editor.core.isDomNode(nodeToSet)) {\n\n nodeToSet = editor.content.getDeepestTextNodeFromPosition(nodeToSet, nodeToSet.childNodes.length);\n\n }\n\n var range = document.createRange(),\n selection = window.getSelection();\n\n window.setTimeout(function () {\n\n range.setStart(nodeToSet, offset);\n range.setEnd(nodeToSet, offset);\n\n selection.removeAllRanges();\n selection.addRange(range);\n\n editor.caret.saveCurrentInputIndex();\n\n }, 20);\n\n };\n\n /**\n * @protected\n * Updates index of input and saves it in caret object\n */\n caret.saveCurrentInputIndex = function () {\n\n /** Index of Input that we paste sanitized content */\n var selection = window.getSelection(),\n inputs = editor.state.inputs,\n focusedNode = selection.anchorNode,\n focusedNodeHolder;\n\n if (!focusedNode) {\n\n return;\n\n }\n\n /** Looking for parent contentEditable block */\n while (focusedNode.contentEditable != 'true') {\n\n focusedNodeHolder = focusedNode.parentNode;\n focusedNode = focusedNodeHolder;\n\n }\n\n /** Input index in DOM level */\n var editableElementIndex = 0;\n\n while (focusedNode != inputs[editableElementIndex]) {\n\n editableElementIndex ++;\n\n }\n\n caret.inputIndex = editableElementIndex;\n\n };\n\n /**\n * Returns current input index (caret object)\n */\n caret.getCurrentInputIndex = function () {\n\n return caret.inputIndex;\n\n };\n\n /**\n * @param {int} index - index of first-level block after that we set caret into next input\n */\n caret.setToNextBlock = function (index) {\n\n var inputs = editor.state.inputs,\n nextInput = inputs[index + 1];\n\n if (!nextInput) {\n\n editor.core.log('We are reached the end');\n return;\n\n }\n\n /**\n * When new Block created or deleted content of input\n * We should add some text node to set caret\n */\n if (!nextInput.childNodes.length) {\n\n var emptyTextElement = document.createTextNode('');\n\n nextInput.appendChild(emptyTextElement);\n\n }\n\n editor.caret.inputIndex = index + 1;\n editor.caret.set(nextInput, 0, 0);\n editor.content.workingNodeChanged(nextInput);\n\n };\n\n /**\n * @param {int} index - index of target input.\n * Sets caret to input with this index\n */\n caret.setToBlock = function (index) {\n\n var inputs = editor.state.inputs,\n targetInput = inputs[index];\n\n if ( !targetInput ) {\n\n return;\n\n }\n\n /**\n * When new Block created or deleted content of input\n * We should add some text node to set caret\n */\n if (!targetInput.childNodes.length) {\n\n var emptyTextElement = document.createTextNode('');\n\n targetInput.appendChild(emptyTextElement);\n\n }\n\n editor.caret.inputIndex = index;\n editor.caret.set(targetInput, 0, 0);\n editor.content.workingNodeChanged(targetInput);\n\n };\n\n /**\n * @param {int} index - index of input\n */\n caret.setToPreviousBlock = function (index) {\n\n index = index || 0;\n\n var inputs = editor.state.inputs,\n previousInput = inputs[index - 1],\n lastChildNode,\n lengthOfLastChildNode,\n emptyTextElement;\n\n\n if (!previousInput) {\n\n editor.core.log('We are reached first node');\n return;\n\n }\n\n lastChildNode = editor.content.getDeepestTextNodeFromPosition(previousInput, previousInput.childNodes.length);\n lengthOfLastChildNode = lastChildNode.length;\n\n /**\n * When new Block created or deleted content of input\n * We should add some text node to set caret\n */\n if (!previousInput.childNodes.length) {\n\n emptyTextElement = document.createTextNode('');\n previousInput.appendChild(emptyTextElement);\n\n }\n editor.caret.inputIndex = index - 1;\n editor.caret.set(previousInput, previousInput.childNodes.length - 1, lengthOfLastChildNode);\n editor.content.workingNodeChanged(inputs[index - 1]);\n\n };\n\n caret.position = {\n\n atStart : function () {\n\n var selection = window.getSelection(),\n anchorOffset = selection.anchorOffset,\n anchorNode = selection.anchorNode,\n firstLevelBlock = editor.content.getFirstLevelBlock(anchorNode),\n pluginsRender = firstLevelBlock.childNodes[0];\n\n if (!editor.core.isDomNode(anchorNode)) {\n\n anchorNode = anchorNode.parentNode;\n\n }\n\n var isFirstNode = anchorNode === pluginsRender.childNodes[0],\n isOffsetZero = anchorOffset === 0;\n\n return isFirstNode && isOffsetZero;\n\n },\n\n atTheEnd : function () {\n\n var selection = window.getSelection(),\n anchorOffset = selection.anchorOffset,\n anchorNode = selection.anchorNode;\n\n /** Caret is at the end of input */\n return !anchorNode || !anchorNode.length || anchorOffset === anchorNode.length;\n\n }\n };\n\n\n /**\n * Inserts node at the caret location\n * @param {HTMLElement|DocumentFragment} node\n */\n caret.insertNode = function (node) {\n\n var selection, range,\n lastNode = node;\n\n if (node.nodeType == editor.core.nodeTypes.DOCUMENT_FRAGMENT) {\n\n lastNode = node.lastChild;\n\n }\n\n selection = window.getSelection();\n\n range = selection.getRangeAt(0);\n range.deleteContents();\n\n range.insertNode(node);\n\n range.setStartAfter(lastNode);\n range.collapse(true);\n\n selection.removeAllRanges();\n selection.addRange(range);\n\n\n };\n\n return caret;\n\n})({});\n\n\n// WEBPACK FOOTER //\n// ./modules/caret.js","/**\n * Codex Editor Notification Module\n *\n * @author Codex Team\n * @version 1.0\n */\n\nmodule.exports = (function (notifications) {\n\n let editor = codex.editor;\n\n var queue = [];\n\n var addToQueue = function (settings) {\n\n queue.push(settings);\n\n var index = 0;\n\n while ( index < queue.length && queue.length > 5) {\n\n if (queue[index].type == 'confirm' || queue[index].type == 'prompt') {\n\n index++;\n continue;\n\n }\n\n queue[index].close();\n queue.splice(index, 1);\n\n }\n\n };\n\n notifications.createHolder = function () {\n\n var holder = editor.draw.node('DIV', 'cdx-notifications-block');\n\n editor.nodes.notifications = document.body.appendChild(holder);\n\n return holder;\n\n };\n\n\n /**\n * Error notificator. Shows block with message\n * @protected\n */\n notifications.errorThrown = function (errorMsg, event) {\n\n editor.notifications.notification({message: 'This action is not available currently', type: event.type});\n\n };\n\n /**\n *\n * Appends notification\n *\n * settings = {\n * type - notification type (reserved types: alert, confirm, prompt). Just add class 'cdx-notification-'+type\n * message - notification message\n * okMsg - confirm button text (default - 'Ok')\n * cancelBtn - cancel button text (default - 'Cancel'). Only for confirm and prompt types\n * confirm - function-handler for ok button click\n * cancel - function-handler for cancel button click. Only for confirm and prompt types\n * time - time (in seconds) after which notification will close (default - 10s)\n * }\n *\n * @param settings\n */\n notifications.notification = function (constructorSettings) {\n\n /** Private vars and methods */\n var notification = null,\n cancel = null,\n type = null,\n confirm = null,\n inputField = null;\n\n var confirmHandler = function () {\n\n close();\n\n if (typeof confirm !== 'function' ) {\n\n return;\n\n }\n\n if (type == 'prompt') {\n\n confirm(inputField.value);\n return;\n\n }\n\n confirm();\n\n };\n\n var cancelHandler = function () {\n\n close();\n\n if (typeof cancel !== 'function' ) {\n\n return;\n\n }\n\n cancel();\n\n };\n\n\n /** Public methods */\n function create(settings) {\n\n if (!(settings && settings.message)) {\n\n editor.core.log('Can\\'t create notification. Message is missed');\n return;\n\n }\n\n settings.type = settings.type || 'alert';\n settings.time = settings.time*1000 || 10000;\n\n var wrapper = editor.draw.node('DIV', 'cdx-notification'),\n message = editor.draw.node('DIV', 'cdx-notification__message'),\n input = editor.draw.node('INPUT', 'cdx-notification__input'),\n okBtn = editor.draw.node('SPAN', 'cdx-notification__ok-btn'),\n cancelBtn = editor.draw.node('SPAN', 'cdx-notification__cancel-btn');\n\n message.textContent = settings.message;\n okBtn.textContent = settings.okMsg || 'ОК';\n cancelBtn.textContent = settings.cancelMsg || 'Отмена';\n\n editor.listeners.add(okBtn, 'click', confirmHandler);\n editor.listeners.add(cancelBtn, 'click', cancelHandler);\n\n wrapper.appendChild(message);\n\n if (settings.type == 'prompt') {\n\n wrapper.appendChild(input);\n\n }\n\n wrapper.appendChild(okBtn);\n\n if (settings.type == 'prompt' || settings.type == 'confirm') {\n\n wrapper.appendChild(cancelBtn);\n\n }\n\n wrapper.classList.add('cdx-notification-' + settings.type);\n wrapper.dataset.type = settings.type;\n\n notification = wrapper;\n type = settings.type;\n confirm = settings.confirm;\n cancel = settings.cancel;\n inputField = input;\n\n if (settings.type != 'prompt' && settings.type != 'confirm') {\n\n window.setTimeout(close, settings.time);\n\n }\n\n };\n\n /**\n * Show notification block\n */\n function send() {\n\n editor.nodes.notifications.appendChild(notification);\n inputField.focus();\n\n editor.nodes.notifications.classList.add('cdx-notification__notification-appending');\n\n window.setTimeout(function () {\n\n editor.nodes.notifications.classList.remove('cdx-notification__notification-appending');\n\n }, 100);\n\n addToQueue({type: type, close: close});\n\n };\n\n /**\n * Remove notification block\n */\n function close() {\n\n notification.remove();\n\n };\n\n\n if (constructorSettings) {\n\n create(constructorSettings);\n send();\n\n }\n\n return {\n create: create,\n send: send,\n close: close\n };\n\n };\n\n notifications.clear = function () {\n\n editor.nodes.notifications.innerHTML = '';\n queue = [];\n\n };\n\n return notifications;\n\n})({});\n\n\n// WEBPACK FOOTER //\n// ./modules/notifications.js","/**\n * Codex Editor Parser Module\n *\n * @author Codex Team\n * @version 1.1\n */\n\nmodule.exports = (function (parser) {\n\n let editor = codex.editor;\n\n /** inserting text */\n parser.insertPastedContent = function (blockType, tag) {\n\n editor.content.insertBlock({\n type : blockType.type,\n block : blockType.render({\n text : tag.innerHTML\n })\n });\n\n };\n\n /**\n * Check DOM node for display style: separated block or child-view\n */\n parser.isFirstLevelBlock = function (node) {\n\n return node.nodeType == editor.core.nodeTypes.TAG &&\n node.classList.contains(editor.ui.className.BLOCK_CLASSNAME);\n\n };\n\n return parser;\n\n})({});\n\n\n\n// WEBPACK FOOTER //\n// ./modules/parser.js","/**\n * Codex Sanitizer\n */\n\nmodule.exports = (function (sanitizer) {\n\n /** HTML Janitor library */\n let janitor = require('html-janitor');\n\n /** Codex Editor */\n let editor = codex.editor;\n\n sanitizer.prepare = function () {\n\n if (editor.settings.sanitizer && !editor.core.isEmpty(editor.settings.sanitizer)) {\n\n Config.CUSTOM = editor.settings.sanitizer;\n\n }\n\n };\n\n /**\n * Basic config\n */\n var Config = {\n\n /** User configuration */\n CUSTOM : null,\n\n BASIC : {\n\n tags: {\n p: {},\n a: {\n href: true,\n target: '_blank',\n rel: 'nofollow'\n }\n }\n }\n };\n\n sanitizer.Config = Config;\n\n /**\n *\n * @param userCustomConfig\n * @returns {*}\n * @private\n *\n * @description If developer uses editor's API, then he can customize sane restrictions.\n * Or, sane config can be defined globally in editors initialization. That config will be used everywhere\n * At least, if there is no config overrides, that API uses BASIC Default configation\n */\n let init_ = function (userCustomConfig) {\n\n let configuration = userCustomConfig || Config.CUSTOM || Config.BASIC;\n\n return new janitor(configuration);\n\n };\n\n /**\n * Cleans string from unwanted tags\n * @protected\n * @param {String} dirtyString - taint string\n * @param {Object} customConfig - allowed tags\n */\n sanitizer.clean = function (dirtyString, customConfig) {\n\n let janitorInstance = init_(customConfig);\n\n return janitorInstance.clean(dirtyString);\n\n };\n\n return sanitizer;\n\n})({});\n\n\n// WEBPACK FOOTER //\n// ./modules/sanitizer.js","(function (root, factory) {\n if (typeof define === 'function' && define.amd) {\n define('html-janitor', factory);\n } else if (typeof exports === 'object') {\n module.exports = factory();\n } else {\n root.HTMLJanitor = factory();\n }\n}(this, function () {\n\n /**\n * @param {Object} config.tags Dictionary of allowed tags.\n * @param {boolean} config.keepNestedBlockElements Default false.\n */\n function HTMLJanitor(config) {\n\n var tagDefinitions = config['tags'];\n var tags = Object.keys(tagDefinitions);\n\n var validConfigValues = tags\n .map(function(k) { return typeof tagDefinitions[k]; })\n .every(function(type) { return type === 'object' || type === 'boolean' || type === 'function'; });\n\n if(!validConfigValues) {\n throw new Error(\"The configuration was invalid\");\n }\n\n this.config = config;\n }\n\n // TODO: not exhaustive?\n var blockElementNames = ['P', 'LI', 'TD', 'TH', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'PRE'];\n function isBlockElement(node) {\n return blockElementNames.indexOf(node.nodeName) !== -1;\n }\n\n var inlineElementNames = ['A', 'B', 'STRONG', 'I', 'EM', 'SUB', 'SUP', 'U', 'STRIKE'];\n function isInlineElement(node) {\n return inlineElementNames.indexOf(node.nodeName) !== -1;\n }\n\n HTMLJanitor.prototype.clean = function (html) {\n var sandbox = document.createElement('div');\n sandbox.innerHTML = html;\n\n this._sanitize(sandbox);\n\n return sandbox.innerHTML;\n };\n\n HTMLJanitor.prototype._sanitize = function (parentNode) {\n var treeWalker = createTreeWalker(parentNode);\n var node = treeWalker.firstChild();\n if (!node) { return; }\n\n do {\n // Ignore nodes that have already been sanitized\n if (node._sanitized) {\n continue;\n }\n\n if (node.nodeType === Node.TEXT_NODE) {\n // If this text node is just whitespace and the previous or next element\n // sibling is a block element, remove it\n // N.B.: This heuristic could change. Very specific to a bug with\n // `contenteditable` in Firefox: http://jsbin.com/EyuKase/1/edit?js,output\n // FIXME: make this an option?\n if (node.data.trim() === ''\n && ((node.previousElementSibling && isBlockElement(node.previousElementSibling))\n || (node.nextElementSibling && isBlockElement(node.nextElementSibling)))) {\n parentNode.removeChild(node);\n this._sanitize(parentNode);\n break;\n } else {\n continue;\n }\n }\n\n // Remove all comments\n if (node.nodeType === Node.COMMENT_NODE) {\n parentNode.removeChild(node);\n this._sanitize(parentNode);\n break;\n }\n\n var isInline = isInlineElement(node);\n var containsBlockElement;\n if (isInline) {\n containsBlockElement = Array.prototype.some.call(node.childNodes, isBlockElement);\n }\n\n // Block elements should not be nested (e.g.
...); if\n // they are, we want to unwrap the inner block element.\n var isNotTopContainer = !! parentNode.parentNode;\n var isNestedBlockElement =\n isBlockElement(parentNode) &&\n isBlockElement(node) &&\n isNotTopContainer;\n\n var nodeName = node.nodeName.toLowerCase();\n\n var allowedAttrs = getAllowedAttrs(this.config, nodeName, node);\n\n var isInvalid = isInline && containsBlockElement;\n\n // Drop tag entirely according to the whitelist *and* if the markup\n // is invalid.\n if (isInvalid || shouldRejectNode(node, allowedAttrs)\n || (!this.config.keepNestedBlockElements && isNestedBlockElement)) {\n // Do not keep the inner text of SCRIPT/STYLE elements.\n if (! (node.nodeName === 'SCRIPT' || node.nodeName === 'STYLE')) {\n while (node.childNodes.length > 0) {\n parentNode.insertBefore(node.childNodes[0], node);\n }\n }\n parentNode.removeChild(node);\n\n this._sanitize(parentNode);\n break;\n }\n\n // Sanitize attributes\n for (var a = 0; a < node.attributes.length; a += 1) {\n var attr = node.attributes[a];\n\n if (shouldRejectAttr(attr, allowedAttrs, node)) {\n node.removeAttribute(attr.name);\n // Shift the array to continue looping.\n a = a - 1;\n }\n }\n\n // Sanitize children\n this._sanitize(node);\n\n // Mark node as sanitized so it's ignored in future runs\n node._sanitized = true;\n } while ((node = treeWalker.nextSibling()));\n };\n\n function createTreeWalker(node) {\n return document.createTreeWalker(node,\n NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT,\n null, false);\n }\n\n function getAllowedAttrs(config, nodeName, node){\n if (typeof config.tags[nodeName] === 'function') {\n return config.tags[nodeName](node);\n } else {\n return config.tags[nodeName];\n }\n }\n\n function shouldRejectNode(node, allowedAttrs){\n if (typeof allowedAttrs === 'undefined') {\n return true;\n } else if (typeof allowedAttrs === 'boolean') {\n return !allowedAttrs;\n }\n\n return false;\n }\n\n function shouldRejectAttr(attr, allowedAttrs, node){\n var attrName = attr.name.toLowerCase();\n\n if (allowedAttrs === true){\n return false;\n } else if (typeof allowedAttrs[attrName] === 'function'){\n return !allowedAttrs[attrName](attr.value, node);\n } else if (typeof allowedAttrs[attrName] === 'undefined'){\n return true;\n } else if (allowedAttrs[attrName] === false) {\n return true;\n } else if (typeof allowedAttrs[attrName] === 'string') {\n return (allowedAttrs[attrName] !== attr.value);\n }\n\n return false;\n }\n\n return HTMLJanitor;\n\n}));\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./~/html-janitor/src/html-janitor.js\n// module id = 18\n// module chunks = 0","/**\n * Codex Editor Listeners module\n *\n * @author Codex Team\n * @version 1.0\n */\n\n/**\n * Module-decorator for event listeners assignment\n */\nmodule.exports = function (listeners) {\n\n var allListeners = [];\n\n /**\n * Search methods\n *\n * byElement, byType and byHandler returns array of suitable listeners\n * one and all takes element, eventType, and handler and returns first (all) suitable listener\n *\n */\n listeners.search = function () {\n\n var byElement = function (element, context) {\n\n var listenersOnElement = [];\n\n context = context || allListeners;\n\n for (var i = 0; i < context.length; i++) {\n\n var listener = context[i];\n\n if (listener.element === element) {\n\n listenersOnElement.push(listener);\n\n }\n\n }\n\n return listenersOnElement;\n\n };\n\n var byType = function (eventType, context) {\n\n var listenersWithType = [];\n\n context = context || allListeners;\n\n for (var i = 0; i < context.length; i++) {\n\n var listener = context[i];\n\n if (listener.type === eventType) {\n\n listenersWithType.push(listener);\n\n }\n\n }\n\n return listenersWithType;\n\n };\n\n var byHandler = function (handler, context) {\n\n var listenersWithHandler = [];\n\n context = context || allListeners;\n\n for (var i = 0; i < context.length; i++) {\n\n var listener = context[i];\n\n if (listener.handler === handler) {\n\n listenersWithHandler.push(listener);\n\n }\n\n }\n\n return listenersWithHandler;\n\n };\n\n var one = function (element, eventType, handler) {\n\n var result = allListeners;\n\n if (element)\n result = byElement(element, result);\n\n if (eventType)\n result = byType(eventType, result);\n\n if (handler)\n result = byHandler(handler, result);\n\n return result[0];\n\n };\n\n var all = function (element, eventType, handler) {\n\n var result = allListeners;\n\n if (element)\n result = byElement(element, result);\n\n if (eventType)\n result = byType(eventType, result);\n\n if (handler)\n result = byHandler(handler, result);\n\n return result;\n\n };\n\n return {\n byElement : byElement,\n byType : byType,\n byHandler : byHandler,\n one : one,\n all : all\n };\n\n }();\n\n listeners.add = function (element, eventType, handler, isCapture) {\n\n element.addEventListener(eventType, handler, isCapture);\n\n var data = {\n element: element,\n type: eventType,\n handler: handler\n };\n\n var alreadyAddedListener = listeners.search.one(element, eventType, handler);\n\n if (!alreadyAddedListener) {\n\n allListeners.push(data);\n\n }\n\n };\n\n listeners.remove = function (element, eventType, handler) {\n\n element.removeEventListener(eventType, handler);\n\n var existingListeners = listeners.search.all(element, eventType, handler);\n\n for (var i = 0; i < existingListeners.length; i++) {\n\n var index = allListeners.indexOf(existingListeners[i]);\n\n if (index > 0) {\n\n allListeners.splice(index, 1);\n\n }\n\n }\n\n };\n\n listeners.removeAll = function () {\n\n allListeners.map(function (current) {\n\n listeners.remove(current.element, current.type, current.handler);\n\n });\n\n };\n\n listeners.get = function (element, eventType, handler) {\n\n return listeners.search.all(element, eventType, handler);\n\n };\n\n return listeners;\n\n}({});\n\n\n// WEBPACK FOOTER //\n// ./modules/listeners.js","/**\n * Codex Editor Destroyer module\n *\n * @auhor Codex Team\n * @version 1.0\n */\n\nmodule.exports = function (destroyer) {\n\n let editor = codex.editor;\n\n destroyer.removeNodes = function () {\n\n editor.nodes.wrapper.remove();\n editor.nodes.notifications.remove();\n\n };\n\n destroyer.destroyPlugins = function () {\n\n for (var tool in editor.tools) {\n\n if (typeof editor.tools[tool].destroy === 'function') {\n\n editor.tools[tool].destroy();\n\n }\n\n }\n\n };\n\n destroyer.destroyScripts = function () {\n\n var scripts = document.getElementsByTagName('SCRIPT');\n\n for (var i = 0; i < scripts.length; i++) {\n\n if (scripts[i].id.indexOf(editor.scriptPrefix) + 1) {\n\n scripts[i].remove();\n i--;\n\n }\n\n }\n\n };\n\n\n /**\n * Delete editor data from webpage.\n * You should send settings argument with boolean flags:\n * @param settings.ui- remove redactor event listeners and DOM nodes\n * @param settings.scripts - remove redactor scripts from DOM\n * @param settings.plugins - remove plugin's objects\n * @param settings.core - remove editor core. You can remove core only if UI and scripts flags is true\n * }\n *\n */\n destroyer.destroy = function (settings) {\n\n if (!settings || typeof settings !== 'object') {\n\n return;\n\n }\n\n if (settings.ui) {\n\n destroyer.removeNodes();\n editor.listeners.removeAll();\n\n }\n\n if (settings.scripts) {\n\n destroyer.destroyScripts();\n\n }\n\n if (settings.plugins) {\n\n destroyer.destroyPlugins();\n\n }\n\n if (settings.ui && settings.scripts && settings.core) {\n\n delete codex.editor;\n\n }\n\n };\n\n return destroyer;\n\n}({});\n\n\n// WEBPACK FOOTER //\n// ./modules/destroyer.js","/**\n * Codex Editor Paste module\n *\n * @author Codex Team\n * @version 1.1.1\n */\n\nmodule.exports = function (paste) {\n\n let editor = codex.editor;\n\n var patterns = [];\n\n paste.prepare = function () {\n\n var tools = editor.tools;\n\n for (var tool in tools) {\n\n if (!tools[tool].renderOnPastePatterns || !Array.isArray(tools[tool].renderOnPastePatterns)) {\n\n continue;\n\n }\n\n tools[tool].renderOnPastePatterns.map(function (pattern) {\n\n\n patterns.push(pattern);\n\n });\n\n }\n\n return Promise.resolve();\n\n };\n\n /**\n * Saves data\n * @param event\n */\n paste.pasted = function (event) {\n\n var clipBoardData = event.clipboardData || window.clipboardData,\n content = clipBoardData.getData('Text');\n\n var result = analize(content);\n\n if (result) {\n\n event.preventDefault();\n event.stopImmediatePropagation();\n\n }\n\n return result;\n\n };\n\n /**\n * Analizes pated string and calls necessary method\n */\n\n var analize = function (string) {\n\n var result = false,\n content = editor.content.currentNode,\n plugin = content.dataset.tool;\n\n patterns.map( function (pattern) {\n\n var execArray = pattern.regex.exec(string),\n match = execArray && execArray[0];\n\n if ( match && match === string.trim()) {\n\n /** current block is not empty */\n if ( content.textContent.trim() && plugin == editor.settings.initialBlockPlugin ) {\n\n pasteToNewBlock_();\n\n }\n\n pattern.callback(string, pattern);\n result = true;\n\n }\n\n });\n\n return result;\n\n };\n\n var pasteToNewBlock_ = function () {\n\n /** Create new initial block */\n editor.content.insertBlock({\n\n type : editor.settings.initialBlockPlugin,\n block : editor.tools[editor.settings.initialBlockPlugin].render({\n text : ''\n })\n\n }, false);\n\n };\n\n /**\n * This method prevents default behaviour.\n *\n * @param {Object} event\n * @protected\n *\n * @description We get from clipboard pasted data, sanitize, make a fragment that contains of this sanitized nodes.\n * Firstly, we need to memorize the caret position. We can do that by getting the range of selection.\n * After all, we insert clear fragment into caret placed position. Then, we should move the caret to the last node\n */\n paste.blockPasteCallback = function (event) {\n\n\n if (!needsToHandlePasteEvent(event.target)) {\n\n return;\n\n }\n\n /** Prevent default behaviour */\n event.preventDefault();\n\n /** get html pasted data - dirty data */\n var htmlData = event.clipboardData.getData('text/html'),\n plainData = event.clipboardData.getData('text/plain');\n\n /** Temporary DIV that is used to work with text's paragraphs as DOM-elements*/\n var paragraphs = editor.draw.node('DIV', '', {}),\n cleanData,\n wrappedData;\n\n /** Create fragment, that we paste to range after proccesing */\n cleanData = editor.sanitizer.clean(htmlData);\n\n /**\n * We wrap pasted text with
tags to split it logically\n * @type {string}\n */\n wrappedData = editor.content.wrapTextWithParagraphs(cleanData, plainData);\n paragraphs.innerHTML = wrappedData;\n\n /**\n * If there only one paragraph, just insert in at the caret location\n */\n if (paragraphs.childNodes.length == 1) {\n\n emulateUserAgentBehaviour(paragraphs.firstChild);\n return;\n\n }\n\n insertPastedParagraphs(paragraphs.childNodes);\n\n };\n\n /**\n * Checks if we should handle paste event on block\n * @param block\n *\n * @return {boolean}\n */\n var needsToHandlePasteEvent = function (block) {\n\n /** If area is input or textarea then allow default behaviour */\n if ( editor.core.isNativeInput(block) ) {\n\n return false;\n\n }\n\n var editableParent = editor.content.getEditableParent(block);\n\n /** Allow paste when event target placed in Editable element */\n if (!editableParent) {\n\n return false;\n\n }\n\n return true;\n\n };\n\n /**\n * Inserts new initial plugin blocks with data in paragraphs\n *\n * @param {Array} paragraphs - array of paragraphs (
) whit content, that should be inserted\n */\n var insertPastedParagraphs = function (paragraphs) {\n\n var NEW_BLOCK_TYPE = editor.settings.initialBlockPlugin,\n currentNode = editor.content.currentNode;\n\n\n paragraphs.forEach(function (paragraph) {\n\n /** Don't allow empty paragraphs */\n if (editor.core.isBlockEmpty(paragraph)) {\n\n return;\n\n }\n\n editor.content.insertBlock({\n type : NEW_BLOCK_TYPE,\n block : editor.tools[NEW_BLOCK_TYPE].render({\n text : paragraph.innerHTML\n })\n });\n\n editor.caret.inputIndex++;\n\n });\n\n editor.caret.setToPreviousBlock(editor.caret.getCurrentInputIndex() + 1);\n\n\n /**\n * If there was no data in working node, remove it\n */\n if (editor.core.isBlockEmpty(currentNode)) {\n\n currentNode.remove();\n editor.ui.saveInputs();\n\n }\n\n\n };\n\n /**\n * Inserts node content at the caret position\n *\n * @param {Node} node - DOM node (could be DocumentFragment), that should be inserted at the caret location\n */\n var emulateUserAgentBehaviour = function (node) {\n\n var newNode;\n\n if (node.childElementCount) {\n\n newNode = document.createDocumentFragment();\n\n node.childNodes.forEach(function (current) {\n\n if (!editor.core.isDomNode(current) && current.data.trim() === '') {\n\n return;\n\n }\n\n newNode.appendChild(current.cloneNode(true));\n\n });\n\n } else {\n\n newNode = document.createTextNode(node.textContent);\n\n }\n\n editor.caret.insertNode(newNode);\n\n };\n\n\n return paste;\n\n}({});\n\n\n// WEBPACK FOOTER //\n// ./modules/paste.js"],"sourceRoot":""}
\ No newline at end of file
diff --git a/codex.js b/codex.js
deleted file mode 100644
index 874dc4a0..00000000
--- a/codex.js
+++ /dev/null
@@ -1,146 +0,0 @@
-/**
- *
- * Codex Editor
- *
- * @author Codex Team
- */
-
-module.exports = (function (editor) {
-
- 'use strict';
-
- editor.version = VERSION;
- editor.scriptPrefix = 'cdx-script-';
-
- var init = function () {
-
- editor.core = require('./modules/core');
- editor.tools = require('./modules/tools');
- editor.ui = require('./modules/ui');
- editor.transport = require('./modules/transport');
- editor.renderer = require('./modules/renderer');
- editor.saver = require('./modules/saver');
- editor.content = require('./modules/content');
- editor.toolbar = require('./modules/toolbar/toolbar');
- editor.callback = require('./modules/callbacks');
- editor.draw = require('./modules/draw');
- editor.caret = require('./modules/caret');
- editor.notifications = require('./modules/notifications');
- editor.parser = require('./modules/parser');
- editor.sanitizer = require('./modules/sanitizer');
- editor.listeners = require('./modules/listeners');
- editor.destroyer = require('./modules/destroyer');
- editor.paste = require('./modules/paste');
-
- };
-
- /**
- * @public
- * holds initial settings
- */
- editor.settings = {
- tools : ['paragraph', 'header', 'picture', 'list', 'quote', 'code', 'twitter', 'instagram', 'smile'],
- holderId : 'codex-editor',
-
- // Type of block showing on empty editor
- initialBlockPlugin: 'paragraph'
- };
-
- /**
- * public
- *
- * Static nodes
- */
- editor.nodes = {
- holder : null,
- wrapper : null,
- toolbar : null,
- inlineToolbar : {
- wrapper : null,
- buttons : null,
- actions : null
- },
- toolbox : null,
- notifications : null,
- plusButton : null,
- showSettingsButton: null,
- showTrashButton : null,
- blockSettings : null,
- pluginSettings : null,
- defaultSettings : null,
- toolbarButtons : {}, // { type : DomEl, ... }
- redactor : null
- };
-
- /**
- * @public
- *
- * Output state
- */
- editor.state = {
- jsonOutput : [],
- blocks : [],
- inputs : []
- };
-
- /**
- * @public
- * Editor plugins
- */
- editor.tools = {};
-
- /**
- * Initialization
- * @uses Promise cEditor.core.prepare
- * @param {Object} userSettings
- * @param {Array} userSettings.tools list of plugins
- * @param {String} userSettings.holderId Element's id to append editor
- *
- * Load user defined tools
- * Tools must contain this important objects :
- * @param {String} type - this is a type of plugin. It can be used as plugin name
- * @param {String} iconClassname - this a icon in toolbar
- * @param {Object} make - what should plugin do, when it is clicked
- * @param {Object} appendCallback - callback after clicking
- * @param {Element} settings - what settings does it have
- * @param {Object} render - plugin get JSON, and should return HTML
- * @param {Object} save - plugin gets HTML content, returns JSON
- * @param {Boolean} displayInToolbox - will be displayed in toolbox. Default value is TRUE
- * @param {Boolean} enableLineBreaks - inserts new block or break lines. Default value is FALSE
- *
- * @example
- * - type : 'header',
- * - iconClassname : 'ce-icon-header',
- * - make : headerTool.make,
- * - appendCallback : headerTool.appendCallback,
- * - settings : headerTool.makeSettings(),
- * - render : headerTool.render,
- * - save : headerTool.save,
- * - displayInToolbox : true,
- * - enableLineBreaks : false
- */
- editor.start = function (userSettings) {
-
- init();
-
- editor.core.prepare(userSettings)
-
- // If all ok, make UI, bind events and parse initial-content
- .then(editor.ui.prepare)
- .then(editor.tools.prepare)
- .then(editor.sanitizer.prepare)
- .then(editor.paste.prepare)
- .then(editor.transport.prepare)
- .then(editor.renderer.makeBlocksFromData)
- .then(editor.ui.saveInputs)
- .catch(function (error) {
-
- editor.core.log('Initialization failed with error: %o', 'warn', error);
-
- });
-
- };
-
- return editor;
-
-})({});
diff --git a/docs/api.md b/docs/api.md
new file mode 100644
index 00000000..7fc85086
--- /dev/null
+++ b/docs/api.md
@@ -0,0 +1,76 @@
+# CodeX Editor API
+
+Blocks have access to the public methods provided by CodeX Editor API Module. Plugin and Tune Developers
+can use Editor API as they want.
+
+## Api object description
+
+Common API interface.
+
+```js
+export interface IAPI {
+ blocks: IBlocksAPI;
+ caret: ICaretAPI;
+ sanitizer: ISanitizerAPI;
+ toolbar: IToolbarAPI;
+ }
+ ```
+
+#### IBlocksAPI
+
+Methods that working with Blocks
+
+```swap(fromIndex, toIndex)``` - swaps two Blocks by their positions
+
+```delete(blockIndex?: Number)``` - deletes Block with passed index
+
+```getCurrentBlockIndex()``` - current Block index
+
+```getBlockByIndex(index: Number)``` - returns Block with passed index
+
+```getBlocksCount()``` - returns Blocks count
+
+#### ISanitizerAPI
+
+```clean(taintString, config)``` - method uses HTMLJanitor to clean taint string.
+CodeX Editor provides basic config without attributes, but you can inherit by passing your own config.
+
+Usage:
+
+```js
+let taintString = '
BlockWithText
'
+let customConfig = {
+ b: true,
+ p: {
+ style: true,
+ },
+}
+this.api.sanitizer.clean(taintString, customConfig);
+```
+
+### IToolbarAPI
+
+Methods that working with Toolbar
+
+```open()``` - Opens toolbar
+
+```close()``` - Closes toolbar, toolbox and blockSettings if they are opened
+
+### IEventsAPI
+
+Methods that allows to subscribe on CodeX Editor events
+
+```on(eventName: string, callback: Function)``` - subscribe callback on event
+
+```off(eventName: string, callback: Function)``` - unsubscribe callback from event
+
+```emit(eventName: string, data: object)``` - fires all subscribed callbacks with passed data
+
+### IListenerAPI
+
+Methods that allows to work with DOM listener. Useful when you forgot to remove listener.
+Module collects all listeners and destroys automatically
+
+```on(element: HTMLElement, eventType: string, handler: Function, useCapture?: boolean)``` - add event listener to HTML element
+
+```off(element: HTMLElement, eventType: string, handler: Function)``` - remove event handler from HTML element
diff --git a/docs/caret.md b/docs/caret.md
new file mode 100644
index 00000000..de2ac2e0
--- /dev/null
+++ b/docs/caret.md
@@ -0,0 +1,37 @@
+# CodeX Editor Caret Module
+
+The `Caret` module contains methods working with caret. Uses [Range](https://developer.mozilla.org/en-US/docs/Web/API/Range) methods to navigate caret
+between blocks.
+
+Caret class implements basic Module class that holds User configuration
+and default CodeX Editor instances
+
+## Properties
+
+## Methods
+
+### setToBlock
+
+```javascript
+Caret.setToBlock(block, offset, atEnd)
+```
+
+> Method gets Block instance and puts caret to the text node with offset
+
+#### params
+
+| Param | Type | Description|
+| -------------|------ |:-------------:|
+| block | Object | Block instance that BlockManager created|
+| offset | Number | caret offset regarding to the text node (Default: 0)|
+| atEnd | Boolean | puts caret at the end of last text node|
+
+
+### setToTheLastBlock
+
+```javascript
+Caret.setToTheLastBlock()
+```
+
+> sets Caret at the end of last Block
+If last block is not empty, inserts another empty Block which is passed as initial
diff --git a/docs/events.md b/docs/events.md
new file mode 100644
index 00000000..ee7c0512
--- /dev/null
+++ b/docs/events.md
@@ -0,0 +1,50 @@
+# CodeX Editor Events Module
+
+Module allows Developers to subscribe on events or trigger own events
+
+## Methods
+
+### On
+
+```javascript
+Events.on(eventName, callback)
+```
+
+> Method subscribes callback on event. It will be called when CodeX Editor emits this event
+
+#### params
+
+| Param | Type | Description|
+| -------------|------ |:-------------:|
+| eventName | String | event name|
+| callback | Function | event callback|
+
+### Off
+
+```javascript
+Events.off(eventName, callback)
+```
+
+> Method unsubscribes callback on event
+
+#### params
+
+| Param | Type | Description|
+| -------------|------ |:-------------:|
+| eventName | String | event name|
+| callback | Function | event callback|
+
+### Emit
+
+```javascript
+Events.emit(eventName, data)
+```
+
+> Method emits data to all subscribed callbacks
+
+#### params
+
+| Param | Type | Description|
+| -------------|------ |:-------------:|
+| eventName | String | event name|
+| data | Object | any data|
diff --git a/docs/renderer.md b/docs/renderer.md
new file mode 100644
index 00000000..6f964020
--- /dev/null
+++ b/docs/renderer.md
@@ -0,0 +1,7 @@
+# CodeX Editor Renderer
+
+`Renderer` is a class that is responsible for rendering JSON data to HTML.
+
+ Inside it uses BlockManager to compose block's list bounded to the Tool instance.
+
+ ## Methods
diff --git a/docs/sanitizer.md b/docs/sanitizer.md
new file mode 100644
index 00000000..31081ac3
--- /dev/null
+++ b/docs/sanitizer.md
@@ -0,0 +1,45 @@
+# CodeX Editor Sanitizer Module
+
+The `Sanitizer` module represents a set of methods that clears taint strings.
+Uses lightweight npm package with simple API [html-janitor](https://www.npmjs.com/package/html-janitor)
+
+Sanitizer class implements basic Module class that holds User configuration
+and default CodeX Editor instances
+
+You can read more about Module class [here]()
+
+## Properties
+
+Default Editor Sanitizer configuration according to the html-janitor API
+```javascript
+defaultConfig
+```
+
+Custom User configuration which passed on Editor initialization. Data type must be according to the html-janitor API
+```javascript
+sanitizerConfig
+```
+
+
+Property that holds an instance used in Module
+```javascript
+sanitizerInstance
+```
+
+## Methods
+
+### clean
+
+```javascript
+clean(taintString, customConfig)
+```
+
+> Cleans up the passed taint string
+
+#### params
+
+| Param | Type | Description|
+| -------------|------ |:-------------:|
+| taintString | String | string that needs to be cleaned|
+| customConfig | Object | Can be passed new config per usage (Default: uses default configuration)|
+
diff --git a/docs/saver.md b/docs/saver.md
new file mode 100644
index 00000000..c54a6e08
--- /dev/null
+++ b/docs/saver.md
@@ -0,0 +1,2 @@
+# CodeX Editor Saver Module
+
diff --git a/docs/toolbar-settings.md b/docs/toolbar-settings.md
new file mode 100644
index 00000000..d3082b75
--- /dev/null
+++ b/docs/toolbar-settings.md
@@ -0,0 +1,91 @@
+# CodeX Editor Toolbar Block Settings Module
+
+Toolbar Module has space for Block settings. Settings divided into:
+ - space for plugin's settings, that is described by «Plugin»'s Developer
+ - space for default settings. This option is also can be implemented and expanded
+
+They difference between zones is that the first option is specified by plugin
+and each Block can have different options, when second option is for every Block
+regardless to the plugin's option.
+
+### Let's look the examples:
+
+«Plugin»'s Developers need to expand «renderSettings» method that returns HTML.
+Every user action will be handled by itself. So, you can easily write
+callbacks that switches your content or makes better. For more information
+read [Tools](tools.md).
+
+---
+
+«Tune»'s Developers need to implement core-provided interface to develop
+tunes that will be appeared in Toolbar default settings zone.
+
+Tunes must expand two important methods:
+ - `render()` - returns HTML and it is appended to the default settings zone
+ - `save()` - extracts important information to be saved
+
+No restrictions. Handle user action by yourself
+
+Create Class that implements block-tune.ts
+
+Your Tune's constructor gets argument as object and it includes:
+ - {Object} api - object contains public methods from modules. @see [API](api.md)
+ - {Object} settings - settings contains block default state.
+This object could have information about cover, anchor and so on.
+
+Example on TypeScript:
+
+```js
+
+import IBlockTune from './block-tune';
+
+export default class YourCustomTune implements IBlockTune {
+
+ public constructor({api, settings}) {
+ this.api = api;
+ this.settings = settings;
+ }
+
+ render() {
+ let someHTML = '...';
+ return someHTML;
+ }
+
+ save() {
+ // Return the important data that needs to be saved
+ return object
+ }
+
+ someMethod() {
+ // moves current block down
+ this.api.blocks.moveDown();
+ }
+}
+```
+
+Example on ES6
+
+```js
+export default class YourCustomTune {
+
+ constructor({api, settings}) {
+ this.api = api;
+ this.settings = settings;
+ }
+
+ render() {
+ let someHTML = '...';
+ return someHTML;
+ }
+
+ save() {
+ // Return the important data that needs to be saved
+ return object
+ }
+
+ someMethod() {
+ // moves current block down
+ this.api.blocks.moveDown();
+ }
+}
+```
diff --git a/docs/tools-inline.md b/docs/tools-inline.md
new file mode 100644
index 00000000..ff34d02e
--- /dev/null
+++ b/docs/tools-inline.md
@@ -0,0 +1,105 @@
+# Tools for the Inline Toolbar
+
+Similar with [Tools](tools.md) represented Blocks, you can create Tools for the Inline Toolbar. It will work with
+selected fragment of text. The simplest example is `bold` or `italic` Tools.
+
+## Base structure
+
+First of all, Tool's class should have a `isInline` property (static getter) set as `true`.
+
+After that Inline Tool should implement next methods.
+
+- `render()` — create a button
+- `surround()` — works with selected range
+- `checkState()` — get Tool's activated state by selected range
+
+Also, you can provide optional methods
+
+- `renderActions()` — create additional element below the buttons
+- `clear()` — clear Tool's stuff on opening/closing of Inline Toolbar
+- `shortcut()` — shortcut that handles Tool
+
+At the constructor of Tool's class exemplar you will accept an object with the [API](api.md) as a parameter.
+
+---
+
+### render()
+
+Method that returns button to append at the Inline Toolbar
+
+#### Parameters
+
+Method does not accept any parameters
+
+#### Return value
+
+type | description |
+-- | -- |
+`HTMLElement` | element that will be added to the Inline Toolbar |
+
+---
+
+### surround(range: Range)
+
+Method that accepts selected range and wrap it somehow
+
+#### Parameters
+
+name | type | description |
+-- |-- | -- |
+range | Range | first range of current Selection |
+
+#### Return value
+
+There is no return value
+
+---
+
+### checkState(selection: Selection)
+
+Get Selection and detect if Tool was applied. For example, after that Tool can highlight button or show some details.
+
+#### Parameters
+
+name | type | description |
+-- |-- | -- |
+selection | Selection | current Selection |
+
+#### Return value
+
+type | description |
+-- | -- |
+`Boolean` | `true` if Tool is active, otherwise `false` |
+
+---
+
+### renderActions()
+
+Optional method that returns additional Element with actions.
+For example, input for the 'link' tool or textarea for the 'comment' tool.
+It will be places below the buttons list at Inline Toolbar.
+
+#### Parameters
+
+Method does not accept any parameters
+
+#### Return value
+
+type | description |
+-- | -- |
+`HTMLElement` | element that will be added to the Inline Toolbar |
+
+---
+
+### clear()
+
+Optional method that will be called on opening/closing of Inline Toolbar.
+Can contain logic for clearing Tool's stuff, such as inputs, states and other.
+
+#### Parameters
+
+Method does not accept any parameters
+
+#### Return value
+
+Method should not return a value.
diff --git a/docs/tools.md b/docs/tools.md
new file mode 100644
index 00000000..423dbb18
--- /dev/null
+++ b/docs/tools.md
@@ -0,0 +1,144 @@
+# 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
+
+### Render
+
+### Save
+
+### Validate
+
+### Merge (optional)
+
+Method that specifies how to merge two `Blocks` of the same type, for example on `Backspace` keypress.
+Method does accept data object in same format as the `Render` and it should provide logic how to combine new
+data with the currently stored value.
+
+### Internal Tool Settings
+
+Options that Tool can specify. All settings should be passed as static properties of Tool's class.
+
+| 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` |
+| `toolboxIcon` | _String_ | — | Tool's SVG icon for Toolbox |
+| `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` |
+| `isInline` | _Boolean_ | `false` | Describes Tool as a [Tool for the Inline Toolbar](tools-inline.md) |
+
+### User configuration
+
+All Tools can be configured by users. For this reason, we provide `toolConfig` option at the Editor Initial Settings.
+Unlike Internal Tool Settings, this options can be specified outside the Tool class,
+so users can set up different configurations for the same Tool.
+
+```javascript
+var editor = new CodexEditor({
+ holderId : 'codex-editor',
+ initialBlock : 'text',
+ tools: {
+ text: Text // 'Text' Tool class for Blocks with type 'text'
+ },
+ toolsConfig: {
+ text: { // user configuration for Blocks with type 'text'
+ inlineToolbar : true,
+ }
+ }
+});
+```
+
+There are few options available by CodeX Editor.
+
+| Name | Type | Default Value | Description |
+| -- | -- | -- | -- |
+| `enableLineBreaks` | _Boolean_ | `false` | With this option, CodeX Editor won't handle Enter keydowns. Can be helpful for Tools like `` where line breaks should be handled by default behaviour. |
+| `inlineToolbar` | _Boolean/Array_ | `false` | Pass `true` to enable the Inline Toolbar with all Tools, or pass an array with specified Tools list |
+| `disallowPaste` | _Boolean_ | `false` | Pass `true` if you want to prevent any paste into your Tool
+
+### Paste handling
+
+CodeX Editor handles paste on Blocks and provides API for Tools to process the pasted data.
+
+When user pastes content into Editor, pasted content is splitted into blocks.
+
+1. If plain text has been pasted, it is split by new line characters
+2. If HTML string has been pasted, it is split by block tags
+
+Also Editor API allows you to define RegExp patterns to substitute them by your data.
+
+To provide paste handling for your Tool you need to define static getter `onPaste` in Tool class.
+`onPaste` getter should return object with fields described below.
+
+##### HTML tags handling
+
+To handle pasted HTML elements object returned from `onPaste` getter should contain following fields:
+
+| Name | Type | Description |
+| -- | -- | -- |
+| `handler(content: HTMLElement)` | `Function` | _Optional_. Pasted HTML elements handler. Gets one argument `content`. `content` is HTML element extracted from pasted data. Handler should return the same object as Tool's `save` method |
+| `tags` | `String[]` | _Optional_. Should contain all tag names you want to be extracted from pasted data and be passed to your `handler` method |
+
+
+For correct work you MUST provide `onPaste.handler` at least for `initialBlock` Tool.
+
+> Example
+
+Header tool can handle `H1`-`H6` tags using paste handling API
+
+```javascript
+static get onPaste() {
+ return {
+ tags: ['H1', 'H2', 'H3', 'H4', 'H5', 'H6'],
+ handler: (element) => ({
+ type: element.tagName,
+ text: element.innerHTML
+ })
+ }
+}
+```
+
+> One tag can be handled by one Tool only.
+
+##### Patterns handling
+
+Your Tool can analyze text by RegExp patterns to substitute pasted string with data you want. Object returned from `onPaste` getter should contain following fields to use patterns:
+
+| Name | Type | Description |
+| -- | -- | -- |
+| `patterns` | `Object` | _Optional_. `patterns` object contains RegExp patterns with their names as object's keys |
+| `patternHandler(text: string, key: string)` | `Function` | _Optional_. Gets pasted string and pattern name. Should return the same object as Tool `save` method |
+
+
+Pattern will be processed only if paste was on `initialBlock` Tool and pasted string length is less than 450 characters.
+
+> Example
+
+You can handle youtube links and insert embeded video instead:
+
+```javascript
+static get onPaste() {
+ return {
+ patterns: {
+ youtube: /http(?:s?):\/\/(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-\_]*)(&(amp;)?[\w\?=]*)?/
+ },
+ patternHandler: (text, key) => {
+ const urlData = Youtube.onPaste.patterns[key].exec(text);
+
+ return {
+ iframe: Youtube.makeEmbededFromURL(urlData)
+ };
+ }
+ }
+}
+```
+
+> Both `onPaste.handler` and `onPaste.patternHandler` can be `async` or return a `Promise`.
+
+### Sanitize
diff --git a/example.html b/example.html
deleted file mode 100644
index 5f4732b3..00000000
--- a/example.html
+++ /dev/null
@@ -1,244 +0,0 @@
-
-
-
-
- CodeX Editor example
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/example/example.html b/example/example.html
new file mode 100644
index 00000000..10a9c404
--- /dev/null
+++ b/example/example.html
@@ -0,0 +1,112 @@
+
+
+
+
+ CodeX Editor example
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/code/code.css b/example/plugins/code/code.css
similarity index 100%
rename from plugins/code/code.css
rename to example/plugins/code/code.css
diff --git a/example/plugins/code/code.js b/example/plugins/code/code.js
new file mode 100644
index 00000000..d3fe0112
--- /dev/null
+++ b/example/plugins/code/code.js
@@ -0,0 +1,71 @@
+/**
+ * Code Plugin\
+ * Creates code tag and adds content to this tag
+ */
+
+var code = (function (code_plugin) {
+ var baseClass = 'ce-code';
+
+ /**
+ * Make initial header block
+ * @param {object} JSON with block data
+ * @return {Element} element to append
+ */
+ var make_ = function (data) {
+ var tag = codex.editor.draw.node('TEXTAREA', [ baseClass ], {});
+
+ if (data && data.text) {
+ tag.value = data.text;
+ }
+
+ return tag;
+ };
+
+ /**
+ * Escapes HTML chars
+ *
+ * @param {string} input
+ * @return {string} — escaped string
+ */
+ var escapeHTML_ = function (input) {
+ var div = document.createElement('DIV'),
+ text = document.createTextNode(input);
+
+ div.appendChild(text);
+
+ return div.innerHTML;
+ };
+
+ /**
+ * Method to render HTML block from JSON
+ */
+ code_plugin.render = function (data) {
+ return make_(data);
+ };
+
+ /**
+ * Method to extract JSON data from HTML block
+ */
+ code_plugin.save = function (blockContent) {
+ var escaped = escapeHTML_(blockContent.value),
+ data = {
+ text : escaped
+ };
+
+
+ return data;
+ };
+
+ code_plugin.validate = function (data) {
+ if (data.text.trim() == '')
+ return;
+
+ return true;
+ };
+
+ code_plugin.destroy = function () {
+ code = null;
+ };
+
+ return code_plugin;
+})({});
diff --git a/plugins/embed/embed.css b/example/plugins/embed/embed.css
similarity index 100%
rename from plugins/embed/embed.css
rename to example/plugins/embed/embed.css
diff --git a/example/plugins/embed/embed.js b/example/plugins/embed/embed.js
new file mode 100644
index 00000000..d727a623
--- /dev/null
+++ b/example/plugins/embed/embed.js
@@ -0,0 +1,199 @@
+/**
+ * Embed plugin by gohabereg
+ * @version 1.0.0
+ */
+var embed = function (embed_plugin) {
+ var methods = {
+
+ addInternal: function (content) {
+ codex.editor.content.switchBlock(codex.editor.content.currentNode, content);
+
+ var blockContent = codex.editor.content.currentNode.childNodes[0];
+
+ blockContent.classList.add('embed__loader');
+
+ setTimeout(function () {
+ blockContent.classList.remove('embed__loader');
+ }, 1000);
+ },
+
+ getHtmlWithEmbedId: function (type, id) {
+ return services[type].html.replace(/<\%\= remote\_id \%\>/g, id);
+ },
+
+ makeElementFromHtml: function (html) {
+ var wrapper = document.createElement('DIV');
+
+ wrapper.innerHTML = html;
+
+ return wrapper;
+ },
+
+ getRemoteId: function (source, execArray) {
+ switch(source) {
+ case 'yandex-music-track':
+ id = execArray[2]+'/'+execArray[1];
+ break;
+ case 'yandex-music-playlist':
+ id = execArray[1]+'/'+execArray[2];
+ break;
+ default:
+ id = execArray[1];
+ }
+
+ return id;
+ }
+ };
+
+ var services = {
+ youtube: {
+ regex: /^.*(?:(?:youtu\.be\/)|(?:youtube\.com)\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*)(?:[\?\&]t\=(\d*)|)/,
+ html: '',
+ height: 320,
+ width: 580
+ }
+ };
+
+
+ embed_plugin.make = function (data, isInternal) {
+ if (!data.remote_id)
+ return;
+
+ var html = methods.getHtmlWithEmbedId(data.source, data.remote_id),
+ block = methods.makeElementFromHtml(html);
+
+ block.dataset.remoteId = data.remote_id;
+ block.dataset.source = data.source;
+ block.dataset.thumbnailUrl = data.thumbnailUrl;
+
+ block.classList.add('embed');
+
+ // var sidePadding = (600 - services[data.source].width) / 2 + 'px';
+
+ // block.style.padding = '30px ' + sidePadding;
+
+ if (isInternal) {
+ methods.addInternal(block);
+ }
+
+ return block;
+ };
+
+ /**
+ * Saving JSON output.
+ * Upload data via ajax
+ */
+ embed_plugin.save = function (blockContent) {
+ if (!blockContent)
+ return;
+
+ var data,
+ source = blockContent.dataset.source;
+
+ data = {
+ source: source,
+ remote_id: blockContent.dataset.remoteId,
+ thumbnailUrl: blockContent.dataset.thumbnailUrl,
+ height: services[source].height,
+ width: services[source].width
+ };
+
+ return data;
+ };
+
+ /**
+ * Render data
+ */
+ embed_plugin.render = function (data) {
+ return embed_plugin.make(data);
+ };
+
+ embed_plugin.urlPastedCallback = function (url, pattern) {
+ var execArray = pattern.regex.exec(url),
+ id = methods.getRemoteId(pattern.type, execArray);
+
+ var data = {
+ source: pattern.type,
+ remote_id: id,
+ thumbnailUrl: url
+ };
+
+ embed_plugin.make(data, true);
+ };
+
+ embed_plugin.validate = function (savedData) {
+ var source = savedData.source,
+ execArray = services[source].regex.exec(savedData.thumbnailUrl),
+ remoteId = methods.getRemoteId(source, execArray);
+
+ return remoteId == savedData.remote_id;
+ };
+
+ embed_plugin.pastePatterns = [
+ {
+ type: 'vk',
+ regex: /https?:\/\/vk\.com\/.*(?:video)([-0-9]+_[0-9]+)/, // /https?.+vk?.com\/feed\?w=wall\d+_\d+/,
+ callback: embed_plugin.urlPastedCallback
+ },
+ {
+ type: 'youtube',
+ regex: /(?:https?:\/{2})?(?:w{3}\.)?youtu(?:be)?\.(?:com|be)(?:\/watch\?v=|\/)([^\s&]+)/,
+ callback: embed_plugin.urlPastedCallback
+ },
+ {
+ type: 'vimeo',
+ regex: /(?:http[s]?:\/\/)?(?:www.)?vimeo\.co(?:.+\/([^\/]\d+)(?:#t=[\d]+)?s?$)/,
+ callback: embed_plugin.urlPastedCallback
+ },
+ {
+ type: 'coub',
+ regex: /https?:\/\/coub\.com\/view\/([^\/\?\&]+)/,
+ callback: embed_plugin.urlPastedCallback
+ },
+ {
+ type: 'vine',
+ regex: /https?:\/\/vine\.co\/v\/([^\/\?\&]+)/,
+ callback: embed_plugin.urlPastedCallback
+ },
+ {
+ type: 'imgur',
+ regex: /https?:\/\/(?:i\.)?imgur\.com.*\/([a-zA-Z0-9]+)(?:\.gifv)?/,
+ callback: embed_plugin.urlPastedCallback
+ },
+ {
+ type: 'gfycat',
+ regex: /https?:\/\/gfycat\.com(?:\/detail)?\/([a-zA-Z]+)/,
+ callback: embed_plugin.urlPastedCallback
+ },
+ {
+ type: 'twitch-channel',
+ regex: /https?:\/\/www.twitch.tv\/([^\/\?\&]*)/,
+ callback: embed_plugin.urlPastedCallback
+ },
+ {
+ type: 'twitch-video',
+ regex: /https?:\/\/www.twitch.tv\/(?:[^\/\?\&]*\/v|videos)\/([0-9]*)/,
+ callback: embed_plugin.urlPastedCallback
+ },
+ {
+ type: 'yandex-music-album',
+ regex: /https?:\/\/music.yandex.ru\/album\/([0-9]*)/,
+ callback: embed_plugin.urlPastedCallback
+ },
+ {
+ type: 'yandex-music-track',
+ regex: /https?:\/\/music.yandex.ru\/album\/([0-9]*)\/track\/([0-9]*)/,
+ callback: embed_plugin.urlPastedCallback
+ },
+ {
+ type: 'yandex-music-playlist',
+ regex: /https?:\/\/music.yandex.ru\/users\/([^\/\?\&]*)\/playlists\/([0-9]*)/,
+ callback: embed_plugin.urlPastedCallback
+ } ];
+
+ embed_plugin.destroy = function () {
+ embed = null;
+ };
+
+ return embed_plugin;
+}({});
\ No newline at end of file
diff --git a/plugins/embed/loading.gif b/example/plugins/embed/loading.gif
similarity index 100%
rename from plugins/embed/loading.gif
rename to example/plugins/embed/loading.gif
diff --git a/example/plugins/header/header.css b/example/plugins/header/header.css
new file mode 100644
index 00000000..55280500
--- /dev/null
+++ b/example/plugins/header/header.css
@@ -0,0 +1,20 @@
+/**
+ * Plugin styles
+ */
+.ce-header {
+ padding: .7em 0;
+ margin: 0;
+ line-height: 1.5em;
+ outline: none;
+}
+
+ .ce-header p,
+ .ce-header div{
+ padding: 0 !important;
+ margin: 0 !important;
+ }
+
+/**
+ * Styles for Plugin icon in Toolbar
+ */
+.cdx-header-icon {}
diff --git a/example/plugins/header/header.js b/example/plugins/header/header.js
new file mode 100644
index 00000000..0d1b9187
--- /dev/null
+++ b/example/plugins/header/header.js
@@ -0,0 +1,393 @@
+/**
+ * @typedef {Object} HeaderData
+ * @description Tool's input and output data format
+ * @property {String} text — Header's content
+ * @property {number} level - Header's level from 1 to 3
+ */
+
+/**
+ * @typedef {Object} HeaderConfig
+ * @description Tool's config from Editor
+ * @property {string} placeholder — Block's placeholder
+ */
+
+/**
+ * Header block for the CodeX Editor.
+ *
+ * @author CodeX Team (team@ifmo.su)
+ * @copyright CodeX Team 2018
+ * @license The MIT License (MIT)
+ * @version 2.0.0
+ */
+class Header {
+ /**
+ * Should this tools be displayed at the Editor's Toolbox
+ * @returns {boolean}
+ * @public
+ */
+ static get displayInToolbox() {
+ return true;
+ }
+
+ /**
+ * Class for the Toolbox icon
+ * @returns {string}
+ * @public
+ */
+ static get iconClassName() {
+ return 'cdx-header-icon';
+ }
+
+ /**
+ * Render plugin`s main Element and fill it with saved data
+ * @param {HeaderData} blockData - previously saved data
+ * @param {HeaderConfig} blockConfig - Tool's config from Editor
+ */
+ constructor(blockData = {}, blockConfig = {}) {
+ /**
+ * Styles
+ * @type {Object}
+ */
+ this._CSS = {
+ wrapper: 'ce-header',
+ settingsButton: 'ce-settings__button',
+ settingsSelected: 'ce-settings__button--selected',
+ };
+
+ /**
+ * Tool's settings passed from Editor
+ * @type {HeaderConfig}
+ * @private
+ */
+ this._settings = blockConfig;
+
+ /**
+ * Block's data
+ * @type {HeaderData}
+ * @private
+ */
+ this._data = blockData || {};
+
+ /**
+ * List of settings buttons
+ * @type {HTMLElement[]}
+ */
+ this.settingsButtons = [];
+
+ /**
+ * Main Block wrapper
+ * @type {HTMLElement}
+ * @private
+ */
+ this._element = this.getTag();
+ }
+
+ /**
+ * Return Tool's view
+ * @returns {HTMLHeadingElement}
+ * @public
+ */
+ render() {
+ return this._element;
+ }
+
+ /**
+ * Create Block's settings block
+ *
+ * @return {HTMLElement}
+ */
+ renderSettings() {
+ let holder = document.createElement('DIV');
+
+ /** Add type selectors */
+ this.levels.forEach( level => {
+ let selectTypeButton = document.createElement('SPAN');
+
+ selectTypeButton.classList.add(this._CSS.settingsButton);
+
+ /**
+ * Highlight current level button
+ */
+ if (this.currentLevel.number === level.number) {
+ selectTypeButton.classList.add(this._CSS.settingsSelected);
+ }
+
+ /**
+ * Add SVG icon
+ */
+ selectTypeButton.innerHTML = level.svg;
+
+ /**
+ * Save level to its button
+ */
+ selectTypeButton.dataset.level = level.number;
+
+ /**
+ * Set up click handler
+ */
+ selectTypeButton.addEventListener('click', () => {
+ this.setLevel(level.number);
+ });
+
+ /**
+ * Append settings button to holder
+ */
+ holder.appendChild(selectTypeButton);
+
+ /**
+ * Save settings buttons
+ */
+ this.settingsButtons.push(selectTypeButton);
+ });
+
+ return holder;
+ }
+
+ /**
+ * Callback for Block's settings buttons
+ * @param level
+ */
+ setLevel(level) {
+ this.data = {
+ level: level
+ };
+
+ /**
+ * Highlight button by selected level
+ */
+ this.settingsButtons.forEach(button => {
+ button.classList.toggle(this._CSS.settingsSelected, parseInt(button.dataset.level) === level);
+ });
+ }
+
+ /**
+ * Method that specified how to merge two Text blocks.
+ * Called by CodeX Editor by backspace at the beginning of the Block
+ * @param {HeaderData} data
+ * @public
+ */
+ merge(data) {
+ let newData = {
+ text: this.data.text + data.text,
+ level: this.data.level
+ };
+
+ this.data = newData;
+ }
+
+ /**
+ * Validate Text block data:
+ * - check for emptiness
+ *
+ * @param {HeaderData} blockData — data received after saving
+ * @returns {boolean} false if saved data is not correct, otherwise true
+ * @public
+ */
+ validate(blockData) {
+ return blockData.text.trim() !== '';
+ }
+
+ /**
+ * Extract Tool's data from the view
+ * @param {HTMLHeadingElement} toolsContent - Text tools rendered view
+ * @returns {HeaderData} - saved data
+ * @public
+ */
+ save(toolsContent) {
+ /**
+ * @todo sanitize data
+ */
+
+ return {
+ text: toolsContent.innerHTML,
+ level: this.currentLevel.number
+ };
+ }
+
+ /**
+ * Get current Tools`s data
+ * @returns {HeaderData} Current data
+ * @private
+ */
+ get data() {
+ this._data.text = this._element.innerHTML;
+ this._data.level = this.currentLevel.number;
+
+ return this._data;
+ }
+
+ /**
+ * Store data in plugin:
+ * - at the this._data property
+ * - at the HTML
+ *
+ * @param {HeaderData} data — data to set
+ * @private
+ */
+ set data(data) {
+ this._data = data || {};
+
+ /**
+ * If level is set and block in DOM
+ * then replace it to a new block
+ */
+ if (data.level !== undefined && this._element.parentNode) {
+ /**
+ * Create a new tag
+ * @type {HTMLHeadingElement}
+ */
+ let newHeader = this.getTag();
+
+ /**
+ * Save Block's content
+ */
+ newHeader.innerHTML = this._element.innerHTML;
+
+ /**
+ * Replace blocks
+ */
+ this._element.parentNode.replaceChild(newHeader, this._element);
+
+ /**
+ * Save new block to private variable
+ * @type {HTMLHeadingElement}
+ * @private
+ */
+ this._element = newHeader;
+ }
+
+ /**
+ * If data.text was passed then update block's content
+ */
+ if (data.text !== undefined) {
+ this._element.innerHTML = this._data.text || '';
+ }
+ }
+
+ /**
+ * Get tag for target level
+ * By default returns second-leveled header
+ * @return {HTMLElement}
+ */
+ getTag() {
+ /**
+ * Create element for current Block's level
+ */
+ let tag = document.createElement(this.currentLevel.tag);
+
+ /**
+ * Add text to block
+ */
+ tag.innerHTML = this._data.text || '';
+
+ /**
+ * Add styles class
+ */
+ tag.classList.add(this._CSS.wrapper);
+
+ /**
+ * Make tag editable
+ */
+ tag.contentEditable = 'true';
+
+ /**
+ * Add Placeholder
+ */
+ tag.dataset.placeholder = this._settings.placeholder || '';
+
+ return tag;
+ }
+
+ /**
+ * Get current level
+ * @return {level}
+ */
+ get currentLevel() {
+ let level = this.levels.find( level => level.number === this._data.level);
+
+ if (!level) {
+ level = this.levels[0];
+ }
+
+ return level;
+ }
+
+ /**
+ * @typedef {object} level
+ * @property {number} number - level number
+ * @property {string} tag - tag correspondes with level number
+ * @property {string} svg - icon
+ */
+
+ /**
+ * Available header levels
+ * @return {level[]}
+ */
+ get levels() {
+ return [
+ {
+ number: 2,
+ tag: 'H2',
+ svg: ''
+ },
+ {
+ number: 3,
+ tag: 'H3',
+ svg: ''
+ },
+ {
+ number: 4,
+ tag: 'H4',
+ svg: ''
+ }
+ ];
+ }
+
+ /**
+ * Handle H1-H6 tags on paste to substitute it with header Tool
+ *
+ * @private
+ * @param {HTMLElement} content - pasted element
+ * @returns {{level: number, text: *}}
+ */
+ static onPasteHandler(content) {
+ let level = 4;
+
+ switch (content.tagName) {
+ case 'H1':
+ case 'H2':
+ level = 2;
+ break;
+
+ case 'H3':
+ level = 3;
+ break;
+ }
+
+ return {
+ level,
+ text: content.innerHTML
+ };
+ }
+
+ /**
+ * Used by Codex Editor paste handling API.
+ * Provides configuration to handle H1-H6 tags.
+ *
+ * @returns {{handler: (function(HTMLElement): {text: string}), tags: string[]}}
+ */
+ static get onPaste() {
+ return {
+ handler: Header.onPasteHandler,
+ tags: ['H1', 'H2', 'H3', 'H4', 'H5', 'H6']
+ };
+ }
+
+ /**
+ * Get Tool icon's SVG
+ * @return {string}
+ */
+ static get toolboxIcon() {
+ return '';
+ }
+}
diff --git a/plugins/image/image.css b/example/plugins/image/image.css
similarity index 100%
rename from plugins/image/image.css
rename to example/plugins/image/image.css
diff --git a/example/plugins/image/image.js b/example/plugins/image/image.js
new file mode 100644
index 00000000..85889122
--- /dev/null
+++ b/example/plugins/image/image.js
@@ -0,0 +1,647 @@
+/**
+ * Image plugin for codex-editor
+ * @author CodeX Team
+ *
+ * @version 1.3.0
+ */
+var image = (function (image_plugin) {
+ /**
+ * @private
+ *
+ * CSS classNames
+ */
+ var elementClasses_ = {
+
+ ce_image : 'ce-image',
+ loading : 'ce-plugin-image__loader',
+ blockStretched: 'ce-block--stretched',
+ uploadedImage : {
+ centered : 'ce-plugin-image__uploaded--centered',
+ stretched : 'ce-plugin-image__uploaded--stretched'
+ },
+ imageCaption : 'ce-plugin-image__caption',
+ imageWrapper : 'ce-plugin-image__wrapper',
+ formHolder : 'ce-plugin-image__holder',
+ uploadButton : 'ce-plugin-image__button',
+ imagePreview : 'ce-image__preview',
+ selectorHolder: 'ce-settings-checkbox',
+ selectorButton: 'ce-settings-checkbox__toggler',
+ settingsItem: 'ce-image-settings__item',
+ imageWrapperBordered : 'ce-image__wrapper--bordered',
+ toggled : 'ce-image-settings__item--toggled'
+
+ };
+
+ /**
+ *
+ * @private
+ *
+ * UI methods
+ */
+ var ui_ = {
+
+ holder : function () {
+ var element = document.createElement('DIV');
+
+ element.classList.add(elementClasses_.formHolder);
+ element.classList.add(elementClasses_.ce_image);
+
+ return element;
+ },
+
+ uploadButton : function () {
+ var button = document.createElement('SPAN');
+
+ button.classList.add(elementClasses_.uploadButton);
+
+ button.innerHTML = '';
+ button.innerHTML += 'Загрузить фотографию';
+
+ return button;
+ },
+
+ /**
+ * @param {object} file - file path
+ * @param {string} style - css class
+ * @return {object} image - document IMG tag
+ */
+ image : function (file, styles) {
+ var image = document.createElement('IMG');
+
+ styles.map(function (item) {
+ image.classList.add(item);
+ });
+
+ image.src = file.url;
+ image.dataset.bigUrl = file.bigUrl;
+
+ return image;
+ },
+
+ wrapper : function () {
+ var div = document.createElement('div');
+
+ div.classList.add(elementClasses_.imageWrapper);
+
+ return div;
+ },
+
+ caption : function () {
+ var div = document.createElement('div');
+
+ div.classList.add(elementClasses_.imageCaption);
+ div.contentEditable = true;
+
+ return div;
+ },
+ /**
+ * Draws form for image upload
+ */
+ makeForm : function () {
+ var holder = ui_.holder(),
+ uploadButton = ui_.uploadButton();
+
+ holder.appendChild(uploadButton);
+
+ uploadButton.addEventListener('click', uploadButtonClicked_, false );
+
+ image.holder = holder;
+
+ return holder;
+ },
+
+
+ /**
+ * wraps image and caption
+ * @param {object} data - image information
+ * @param {string} imageTypeClass - plugin's style
+ * @param {boolean} stretched - stretched or not
+ * @return wrapped block with image and caption
+ */
+ makeImage : function (data, imageTypeClasses, stretched, bordered) {
+ var file = data,
+ text = data.caption,
+ type = data.type,
+ image = ui_.image(file, imageTypeClasses),
+ caption = ui_.caption(),
+ wrapper = ui_.wrapper();
+
+ caption.innerHTML = text || '';
+
+ wrapper.dataset.stretched = stretched;
+ wrapper.dataset.bordered = bordered;
+
+ /** Appeding to the wrapper */
+ wrapper.appendChild(image);
+ wrapper.appendChild(caption);
+
+ return wrapper;
+ },
+
+ /**
+ * @param {HTML} data - Rendered block with image
+ */
+ getImage : function (data) {
+ var image = data.querySelector('.' + elementClasses_.uploadedImage.centered) ||
+ data.querySelector('.' + elementClasses_.uploadedImage.stretched);
+
+ return image;
+ },
+
+ /**
+ * wraps image and caption
+ * @deprecated
+ * @param {object} data - image information
+ * @return wrapped block with image and caption
+ */
+ centeredImage : function (data) {
+ var file = data.file,
+ text = data.caption,
+ type = data.type,
+ image = ui_.image(file, elementClasses_.uploadedImage.centered),
+ caption = ui_.caption(),
+ wrapper = ui_.wrapper();
+
+ caption.textContent = text;
+
+ wrapper.dataset.stretched = 'false';
+
+ /** Appeding to the wrapper */
+ wrapper.appendChild(image);
+ wrapper.appendChild(caption);
+
+ return wrapper;
+ },
+
+ /**
+ * wraps image and caption
+ * @deprecated
+ * @param {object} data - image information
+ * @return stretched image
+ */
+ stretchedImage : function (data) {
+ var file = data.file,
+ text = data.caption,
+ type = data.type,
+ image = ui_.image(file, elementClasses_.uploadedImage.stretched),
+ caption = ui_.caption(),
+ wrapper = ui_.wrapper();
+
+ caption.textContent = text;
+
+ wrapper.dataset.stretched = 'true';
+
+ /** Appeding to the wrapper */
+ wrapper.appendChild(image);
+ wrapper.appendChild(caption);
+
+ return wrapper;
+ }
+
+ };
+
+ /**
+ * @private
+ *
+ * After render callback
+ */
+ var uploadButtonClicked_ = function (event) {
+ var url = image_plugin.config.uploadImage,
+ beforeSend = uploadingCallbacks_.ByClick.beforeSend,
+ success = uploadingCallbacks_.ByClick.success,
+ error = uploadingCallbacks_.ByClick.error;
+
+ /** Define callbacks */
+ codex.editor.transport.selectAndUpload({
+ url : url,
+ multiple : false,
+ accept : 'image/*',
+ beforeSend : beforeSend,
+ success : success,
+ error : error
+ });
+ };
+
+ var methods_ = {
+
+ addSelectTypeClickListener : function (el, type) {
+ el.addEventListener('click', function () {
+ // el - settings element
+
+ switch (type) {
+ case 'bordered':
+ methods_.toggleBordered(type, this); break;
+ case 'stretched':
+ methods_.toggleStretched(type, this); break;
+ }
+ }, false);
+ },
+
+ toggleBordered : function (type, clickedSettingsItem ) {
+ var current = codex.editor.content.currentNode,
+ blockContent = current.childNodes[0],
+ img = ui_.getImage(current),
+ wrapper = current.querySelector('.' + elementClasses_.imageWrapper);
+
+ if (!img) {
+ return;
+ }
+
+ /**
+ * Add classes to the IMG tag and to the Settings element
+ */
+ img.classList.toggle(elementClasses_.imageWrapperBordered);
+ clickedSettingsItem.classList.toggle(elementClasses_.toggled);
+
+ /**
+ * Save settings in dataset
+ */
+ wrapper.dataset.bordered = img.classList.contains(elementClasses_.imageWrapperBordered);
+
+ setTimeout(function () {
+ codex.editor.toolbar.settings.close();
+ }, 200);
+ },
+
+ toggleStretched : function ( type, clickedSettingsItem ) {
+ var current = codex.editor.content.currentNode,
+ blockContent = current.childNodes[0],
+ img = ui_.getImage(current),
+ wrapper = current.querySelector('.' + elementClasses_.imageWrapper);
+
+ if (!img) {
+ return;
+ }
+
+ /** Clear classList */
+ blockContent.classList.add(elementClasses_.blockStretched);
+ img.classList.toggle(elementClasses_.uploadedImage.stretched);
+ img.classList.toggle(elementClasses_.uploadedImage.centered);
+
+ clickedSettingsItem.classList.toggle(elementClasses_.toggled);
+
+ wrapper.dataset.stretched = img.classList.contains(elementClasses_.uploadedImage.stretched);
+
+ setTimeout(function () {
+ codex.editor.toolbar.settings.close();
+ }, 1000);
+ }
+ };
+
+ /**
+ * @private
+ * Callbacks
+ */
+ var uploadingCallbacks_ = {
+
+ ByClick : {
+
+ /**
+ * Before sending ajax request
+ */
+ beforeSend : function () {
+ var input = codex.editor.transport.input,
+ files = input.files;
+
+ var validFileExtensions = ['jpg', 'jpeg', 'bmp', 'gif', 'png'];
+
+ var type = files[0].type.split('/');
+
+ var result = validFileExtensions.some(function (ext) {
+ return ext == type[1];
+ });
+
+ if (!result) {
+ return;
+ }
+
+ var reader = new FileReader();
+
+ reader.readAsDataURL(files[0]);
+
+ reader.onload = function (e) {
+ var data = {
+ background : false,
+ border : false,
+ isstretch : false,
+ url : e.target.result,
+ bigUrl : null,
+ width : null,
+ height : null,
+ additionalData : null,
+ caption : '',
+ cover : null
+ };
+
+ var newImage = make_(data);
+
+ codex.editor.content.switchBlock(image.holder, newImage, 'image');
+ newImage.classList.add(elementClasses_.imagePreview);
+
+ /**
+ * Change holder to image
+ */
+ image.holder = newImage;
+ };
+ },
+
+ /** Photo was uploaded successfully */
+ success : function (result) {
+ var parsed = JSON.parse(result),
+ data,
+ currentBlock = codex.editor.content.currentNode;
+
+ /**
+ * Preparing {Object} data to draw an image
+ * @uses ceImage.make method
+ */
+ data = parsed.data;
+
+ image.holder.classList.remove(elementClasses_.imagePreview);
+
+ /**
+ * Change src of image
+ */
+ var newImage = image.holder.getElementsByTagName('IMG')[0];
+
+ newImage.src = parsed.data.url;
+ newImage.dataset.bigUrl = parsed.data.bigUrl;
+ newImage.dataset.width = parsed.data.width;
+ newImage.dataset.height = parsed.data.height;
+ newImage.dataset.additionalData = parsed.data.additionalData;
+ },
+
+ /** Error callback. Sends notification to user that something happend or plugin doesn't supports method */
+ error : function (result) {
+ var oldHolder = image.holder;
+ var form = ui_.makeForm();
+
+ codex.editor.content.switchBlock(oldHolder, form, 'image');
+ }
+ },
+
+ ByPaste : {
+
+ /**
+ * Direct upload
+ * Any URL that contains image extension
+ * @param url
+ */
+ uploadImageFromUrl : function (path) {
+ var image,
+ current = codex.editor.content.currentNode,
+ beforeSend,
+ success_callback;
+
+ /** When image is uploaded to redactors folder */
+ success_callback = function (data) {
+ var imageInfo = JSON.parse(data);
+
+ var newImage = image.getElementsByTagName('IMG')[0];
+
+ newImage.dataset.stretched = false;
+ newImage.dataset.src = imageInfo.url;
+ newImage.dataset.bigUrl = imageInfo.bigUrl;
+ newImage.dataset.width = imageInfo.width;
+ newImage.dataset.height = imageInfo.height;
+ newImage.dataset.additionalData = imageInfo.additionalData;
+
+ image.classList.remove(elementClasses_.imagePreview);
+ };
+
+ /** Before sending XMLHTTP request */
+ beforeSend = function () {
+ var content = current.querySelector('.ce-block__content');
+
+ var data = {
+ background: false,
+ border: false,
+ isStretch: false,
+ file: {
+ url: path,
+ bigUrl: null,
+ width: null,
+ height: null,
+ additionalData: null
+ },
+ caption: '',
+ cover: null
+ };
+
+ image = codex.editor.tools.image_extended.render(data);
+
+ image.classList.add(elementClasses_.imagePreview);
+
+ var img = image.querySelector('img');
+
+ codex.editor.content.switchBlock(codex.editor.content.currentNode, image, 'image');
+ };
+
+ /** Preparing data for XMLHTTP */
+ var data = {
+ url: image_plugin.config.uploadFromUrl,
+ type: 'POST',
+ data : {
+ url: path
+ },
+ beforeSend : beforeSend,
+ success : success_callback
+ };
+
+ codex.editor.core.ajax(data);
+ }
+
+ }
+ };
+
+ /**
+ * Image path
+ * @type {null}
+ */
+ image_plugin.path = null;
+
+ /**
+ * Plugin configuration
+ */
+ image_plugin.config = null;
+
+ /**
+ *
+ * @private
+ *
+ * @param data
+ * @return {*}
+ *
+ */
+ var make_ = function ( data ) {
+ var holder,
+ classes = [];
+
+ if (data) {
+ if (data.border) {
+ classes.push(elementClasses_.imageWrapperBordered);
+ }
+
+ if ( data.isstretch || data.isstretch === 'true') {
+ classes.push(elementClasses_.uploadedImage.stretched);
+ holder = ui_.makeImage(data, classes, 'true', data.border);
+ } else {
+ classes.push(elementClasses_.uploadedImage.centered);
+ holder = ui_.makeImage(data, classes, 'false', data.border);
+ }
+
+ return holder;
+ } else {
+ holder = ui_.makeForm();
+
+ return holder;
+ }
+ };
+
+ /**
+ * @private
+ *
+ * Prepare clear data before save
+ *
+ * @param data
+ */
+ var prepareDataForSave_ = function (data) {
+
+ };
+
+ /**
+ * @public
+ * @param config
+ */
+ image_plugin.prepare = function (config) {
+ image_plugin.config = config;
+
+ return Promise.resolve();
+ };
+
+ /**
+ * @public
+ *
+ * this tool works when tool is clicked in toolbox
+ */
+ image_plugin.appendCallback = function (event) {
+ /** Upload image and call success callback*/
+ uploadButtonClicked_(event);
+ };
+
+ /**
+ * @public
+ *
+ * @param data
+ * @return {*}
+ */
+ image_plugin.render = function ( data ) {
+ return make_(data);
+ };
+
+ /**
+ * @public
+ *
+ * @param block
+ * @return {{background: boolean, border: boolean, isstretch: boolean, file: {url: (*|string|Object), bigUrl: (null|*), width: *, height: *, additionalData: null}, caption: (string|*|string), cover: null}}
+ */
+ image_plugin.save = function ( block ) {
+ var content = block,
+ image = ui_.getImage(content),
+ caption = content.querySelector('.' + elementClasses_.imageCaption);
+
+ var data = {
+ background : false,
+ border : content.dataset.bordered === 'true' ? true : false,
+ isstretch : content.dataset.stretched === 'true' ? true : false,
+ // file : {
+ url : image.dataset.src || image.src,
+ bigUrl : image.dataset.bigUrl,
+ width : image.width,
+ height : image.height,
+ additionalData :null,
+ // },
+ caption : caption.innerHTML || '',
+ cover : null
+ };
+
+ return data;
+ };
+
+ /**
+ * @public
+ *
+ * Settings panel content
+ * @return {Element} element contains all settings
+ */
+ image_plugin.makeSettings = function () {
+ var currentNode = codex.editor.content.currentNode,
+ wrapper = currentNode.querySelector('.' + elementClasses_.imageWrapper),
+ holder = document.createElement('DIV'),
+ types = {
+ stretched : 'На всю ширину',
+ bordered : 'Добавить рамку'
+ },
+ currentImageWrapper = currentNode.querySelector('.' + elementClasses_.imageWrapper ),
+ currentImageSettings = currentImageWrapper.dataset;
+
+ /** Add holder classname */
+ holder.className = 'ce-image-settings';
+
+ /** Now add type selectors */
+ for (var type in types) {
+ /**
+ * Settings template
+ */
+ var settingsItem = document.createElement('DIV'),
+ selectorsHolder = document.createElement('SPAN'),
+ selectorsButton = document.createElement('SPAN');
+
+ settingsItem.classList.add(elementClasses_.settingsItem);
+ selectorsHolder.classList.add(elementClasses_.selectorHolder);
+ selectorsButton.classList.add(elementClasses_.selectorButton);
+
+ selectorsHolder.appendChild(selectorsButton);
+ settingsItem.appendChild(selectorsHolder);
+
+ selectTypeButton = document.createTextNode(types[type]);
+ settingsItem.appendChild(selectTypeButton);
+
+ /**
+ * Activate previously selected settings
+ */
+ if ( currentImageSettings[type] == 'true' ) {
+ settingsItem.classList.add(elementClasses_.toggled);
+ }
+
+ methods_.addSelectTypeClickListener(settingsItem, type);
+
+ holder.appendChild(settingsItem);
+ }
+
+ return holder;
+ };
+
+ /**
+ * Share as API
+ */
+ image_plugin.uploadImageFromUri = uploadingCallbacks_.ByPaste.uploadImageFromUrl;
+
+ image_plugin.pastePatterns = [
+ {
+ type: 'image',
+ regex: /(?:([^:\/?#]+):)?(?:\/\/([^\/?#]*))?([^?#]*\.(?:jpe?g|gif|png))(?:\?([^#]*))?(?:#(.*))?/i,
+ callback: image_plugin.uploadImageFromUri
+ },
+ {
+ type: 'uploadCare',
+ regex: /^https:\/\/(uploadcare\.cmtt\.ru|ucarecdn\.com|static[0-9]+\.siliconrus\.cmtt\.ru|static[0-9]+\.cmtt\.ru)/i,
+ callback: image_plugin.uploadImageFromUri
+ } ];
+
+ image_plugin.destroy = function () {
+ image = null;
+ };
+
+ return image_plugin;
+})({});
\ No newline at end of file
diff --git a/plugins/instagram/instagram.css b/example/plugins/instagram/instagram.css
similarity index 100%
rename from plugins/instagram/instagram.css
rename to example/plugins/instagram/instagram.css
diff --git a/example/plugins/instagram/instagram.js b/example/plugins/instagram/instagram.js
new file mode 100644
index 00000000..093f0220
--- /dev/null
+++ b/example/plugins/instagram/instagram.js
@@ -0,0 +1,143 @@
+/**
+ * Instagram plugin
+ * @version 1.0.0
+ */
+var instagram = (function (instagram_plugin) {
+ var methods = {
+
+ render : function (content) {
+ codex.editor.content.switchBlock(codex.editor.content.currentNode, content);
+
+ setTimeout(function () {
+ window.instgrm.Embeds.process();
+ }, 200);
+ },
+
+ /**
+ * Drawing html content.
+ *
+ * @param url
+ * @returns {Element} blockquote - HTML template for Instagram Embed JS
+ */
+ instagramBlock : function (url) {
+ var blockquote = codex.editor.draw.node('BLOCKQUOTE', 'instagram-media instagram', {}),
+ div = codex.editor.draw.node('DIV', '', {}),
+ paragraph = codex.editor.draw.node('P', 'ce-paste__instagram--p', {}),
+ anchor = codex.editor.draw.node('A', '', { href : url });
+
+ blockquote.dataset.instgrmVersion = 4;
+
+ paragraph.appendChild(anchor);
+ div.appendChild(paragraph);
+ blockquote.appendChild(div);
+
+ return blockquote;
+ }
+ };
+
+ /**
+ * Prepare before usage
+ * Load important scripts to render embed
+ */
+ instagram_plugin.prepare = function () {
+ return new Promise(function (resolve, reject) {
+ codex.editor.core.importScript('https://platform.instagram.com/en_US/embeds.js', 'instagram-api').then(function () {
+ resolve();
+ }).catch(function () {
+ reject(Error('Instagram API was not loaded'));
+ });
+ });
+ };
+
+ /**
+ * @private
+ *
+ * Make instagram embed via Widgets method
+ */
+ var make_ = function (data, isInternal) {
+ if (!data.instagram_url)
+ return;
+
+ var block = methods.instagramBlock(data.instagram_url);
+
+ if (isInternal) {
+ setTimeout(function () {
+ /** Render block */
+ methods.render(block);
+ }, 200);
+ }
+
+ if (!isInternal) {
+ methods.render(block);
+ }
+
+ return block;
+ };
+
+ instagram_plugin.validate = function (data) {
+ return true;
+ };
+
+ /**
+ * Saving JSON output.
+ * Upload data via ajax
+ */
+ instagram_plugin.save = function (blockContent) {
+ var data;
+
+ if (!blockContent)
+ return;
+
+ /** Example */
+ data = {
+ instagram_url: blockContent.src
+ };
+
+ return data;
+ };
+
+ instagram_plugin.validate = function (data) {
+ var checkUrl = new RegExp('http?.+instagram.com\/p?.');
+
+ if (!data.instagram_url || checkUrl.exec(data.instagram_url).length == 0)
+ return;
+
+ return true;
+ };
+
+ /**
+ * Render data
+ */
+ instagram_plugin.render = function (data) {
+ return make_(data);
+ };
+
+ /**
+ * callback for instagram url's coming from pasteTool
+ * Using instagram Embed Widgete to render
+ * @param url
+ */
+ instagram_plugin.urlPastedCallback = function (url) {
+ var data = {
+ instagram_url: url
+ };
+
+ make_(data, true);
+ };
+
+ instagram_plugin.pastePatterns = [
+ {
+ type: 'instagram',
+ regex: /http?.+instagram.com\/p\/([a-zA-Z0-9]*)\S*/,
+ callback: instagram_plugin.urlPastedCallback
+ }
+ ];
+
+ instagram_plugin.destroy = function () {
+ instagram = null;
+ delete window.instgrm;
+ };
+
+ return instagram_plugin;
+})({});
+
diff --git a/plugins/instagram/loading.gif b/example/plugins/instagram/loading.gif
similarity index 100%
rename from plugins/instagram/loading.gif
rename to example/plugins/instagram/loading.gif
diff --git a/plugins/link/link.css b/example/plugins/link/link.css
similarity index 100%
rename from plugins/link/link.css
rename to example/plugins/link/link.css
diff --git a/example/plugins/link/link.js b/example/plugins/link/link.js
new file mode 100644
index 00000000..9477d637
--- /dev/null
+++ b/example/plugins/link/link.js
@@ -0,0 +1,301 @@
+/**
+ * Created by nostr on 29.06.16.
+ */
+
+/**
+ * Link tool plugin
+ */
+
+var link = (function (link_plugin) {
+ var settings = {
+ defaultText : 'Вставьте ссылку ...',
+ ENTER_KEY : 13,
+ currentBlock : null,
+ currentInput : null,
+ elementClasses : {
+ link: 'tool-link-link',
+ image: 'tool-link-image',
+ title: 'tool-link-title',
+ description: 'tool-link-description',
+ loader: 'tool-link-loader',
+ error: 'tool-link-error'
+ }
+ };
+
+ var ui = {
+
+ make : function (json) {
+ var wrapper = ui.wrapper(),
+ siteImage = ui.image(json.image, settings.elementClasses.image),
+ siteTitle = ui.title(json.title),
+ siteDescription = ui.description(json.description),
+ siteLink = ui.link(json.url, json.url);
+
+ wrapper.appendChild(siteImage);
+ wrapper.appendChild(siteTitle);
+ wrapper.appendChild(siteLink);
+ wrapper.appendChild(siteDescription);
+
+ siteTitle.contentEditable = true;
+ siteDescription.contentEditable = true;
+
+ return wrapper;
+ },
+
+ mainBlock : function () {
+ var wrapper = document.createElement('div');
+
+ wrapper.classList.add('ceditor-tool-link');
+
+ return wrapper;
+ },
+
+ input : function () {
+ var inputTag = document.createElement('input');
+
+ inputTag.classList.add('ceditor-tool-link-input');
+
+ inputTag.placeholder = settings.defaultText;
+
+ inputTag.contentEditable = false;
+
+ return inputTag;
+ },
+
+ wrapper : function () {
+ var wrapper = document.createElement('div');
+
+ wrapper.classList.add('tool-link-panel', 'clearfix');
+
+ return wrapper;
+ },
+
+ image : function (imageSrc, imageClass) {
+ var imageTag = document.createElement('img');
+
+ imageTag.classList.add(imageClass);
+
+ imageTag.setAttribute('src', imageSrc);
+
+ return imageTag;
+ },
+
+ link : function (linkUrl, linkText) {
+ var linkTag = document.createElement('a');
+
+ linkTag.classList.add(settings.elementClasses.link);
+
+ linkTag.href = linkUrl;
+
+ linkTag.target = '_blank';
+
+ linkTag.innerText = linkText;
+
+ return linkTag;
+ },
+
+ title : function (titleText) {
+ var titleTag = document.createElement('div');
+
+ titleTag.classList.add('tool-link-content', settings.elementClasses.title);
+
+ titleTag.innerHTML = titleText;
+
+ return titleTag;
+ },
+
+ description : function (descriptionText) {
+ var descriptionTag = document.createElement('div');
+
+ descriptionTag.classList.add('tool-link-content', settings.elementClasses.description);
+
+ descriptionTag.innerHTML = descriptionText;
+
+ return descriptionTag;
+ }
+
+ };
+
+ var methods = {
+
+ blockPasteCallback : function (event) {
+ var clipboardData = event.clipboardData || window.clipboardData,
+ pastedData = clipboardData.getData('Text'),
+ block = event.target.parentNode;
+
+ methods.renderLink(pastedData, block);
+
+ event.stopPropagation();
+ },
+
+ blockKeyDownCallback : function (event) {
+ var inputTag = event.target,
+ block = inputTag.parentNode,
+ url;
+
+ if ( block.classList.contains(settings.elementClasses.error) ) {
+ block.classList.remove(settings.elementClasses.error);
+ }
+
+ if (event.keyCode == settings.ENTER_KEY) {
+ url = inputTag.value;
+
+ methods.renderLink(url, block);
+
+ event.preventDefault();
+ }
+ },
+
+ renderLink : function (url, block) {
+ Promise.resolve()
+
+ .then(function () {
+ return methods.urlify(url);
+ })
+
+ .then(function (url) {
+ /* Show loader gif **/
+ block.classList.add(settings.elementClasses.loader);
+
+ return fetch( link_plugin.config.fetchUrl + '?url=' + encodeURI(url) );
+ })
+
+ .then(function (response) {
+ if (response.status == '200') {
+ return response.json();
+ } else {
+ return Error('Invalid response status: %o', response);
+ }
+ })
+
+ .then(function (json) {
+ methods.composeLinkPreview(json, block);
+ })
+
+ .catch(function (error) {
+ /* Hide loader gif **/
+ block.classList.remove(settings.elementClasses.loader);
+
+ block.classList.add(settings.elementClasses.error);
+
+ codex.editor.core.log('Error while doing things with link paste: %o', 'error', error);
+ });
+ },
+
+ urlify : function (text) {
+ var urlRegex = /(https?:\/\/\S+)/g;
+
+ var links = text.match(urlRegex);
+
+ if (links) {
+ return links[0];
+ }
+
+ return Promise.reject(Error('Url is not matched'));
+ },
+
+ composeLinkPreview : function (json, currentBlock) {
+ if (json == {}) {
+ return;
+ }
+
+ var previewBlock = ui.make(json);
+
+ settings.currentInput.remove();
+
+ currentBlock.appendChild(previewBlock);
+
+ currentBlock.classList.remove(settings.elementClasses.loader);
+ }
+ };
+
+ link_plugin.prepare = function (config) {
+ link_plugin.config = config;
+
+ return Promise.resolve();
+ };
+
+ /**
+ * Make initial header block
+ * @param {object} JSON with block data
+ * @return {Element} element to append
+ */
+ link_plugin.makeNewBlock = function (data) {
+ var wrapper = ui.mainBlock(),
+ tag = ui.input();
+
+ settings.currentInput = tag;
+
+ wrapper.appendChild(tag);
+
+ wrapper.classList.add('ce-link');
+ /**
+ * Bind callbacks
+ **/
+ tag.addEventListener('paste', methods.blockPasteCallback, false);
+ tag.addEventListener('keydown', methods.blockKeyDownCallback, false);
+
+ return wrapper;
+ };
+
+ /**
+ * Method to render HTML block from JSON
+ */
+ link_plugin.render = function (json) {
+ if ( json ) {
+ var block = ui.mainBlock(),
+ tag = ui.make(json);
+
+ block.classList.add('ce-link');
+ block.appendChild(tag);
+
+ return block;
+ } else {
+ var wrapper = ui.mainBlock(),
+ tag = ui.input();
+
+ settings.currentInput = tag;
+
+ wrapper.appendChild(tag);
+
+ wrapper.classList.add('ce-link');
+ /**
+ * Bind callbacks
+ **/
+ tag.addEventListener('paste', methods.blockPasteCallback, false);
+ tag.addEventListener('keydown', methods.blockKeyDownCallback, false);
+
+ return wrapper;
+ }
+ };
+
+ link_plugin.validate = function (data) {
+ if (data.url.trim() == '' || data.title.trim() == '' || data.description.trim() == '')
+ return;
+
+ return true;
+ };
+
+ /**
+ * Method to extract JSON data from HTML block
+ */
+ link_plugin.save = function (blockContent) {
+ var linkElement = settings.elementClasses.link;
+
+ var data = {
+ url : blockContent.querySelector('.' + linkElement).href,
+ shortLink : blockContent.querySelector('.' + linkElement).textContent || '',
+ image : blockContent.querySelector('.' + settings.elementClasses.image).src || '',
+ title : blockContent.querySelector('.' + settings.elementClasses.title).textContent || '',
+ description : blockContent.querySelector('.' + settings.elementClasses.description).textContent || ''
+ };
+
+ return data;
+ };
+
+ link_plugin.destroy = function () {
+ link = null;
+ };
+
+ return link_plugin;
+})({});
diff --git a/plugins/list/list.css b/example/plugins/list/list.css
similarity index 100%
rename from plugins/list/list.css
rename to example/plugins/list/list.css
diff --git a/example/plugins/list/list.js b/example/plugins/list/list.js
new file mode 100644
index 00000000..df356968
--- /dev/null
+++ b/example/plugins/list/list.js
@@ -0,0 +1,209 @@
+/**
+ * Code Plugin\
+ * Creates code tag and adds content to this tag
+ */
+var list = (function (list_plugin) {
+ /**
+ * CSS class names
+ */
+ var elementClasses_ = {
+ pluginWrapper: 'cdx-plugin-list',
+ li: 'cdx-plugin-list__li',
+ settings: 'cdx-plugin-list__settings',
+ settingsItem: 'cdx-plugin-settings__item'
+ };
+
+ var LIST_ITEM_TAG = 'LI';
+
+ var ui = {
+
+ make: function (blockType) {
+ var wrapper = this.block(blockType || 'UL', elementClasses_.pluginWrapper);
+
+ wrapper.dataset.type = blockType;
+ wrapper.contentEditable = true;
+
+ wrapper.addEventListener('keydown', methods_.keyDown);
+
+ return wrapper;
+ },
+
+ block: function (blockType, blockClass) {
+ var block = document.createElement(blockType);
+
+ if (blockClass) block.classList.add(blockClass);
+
+ return block;
+ },
+
+ button: function (buttonType) {
+ var types = {
+ unordered: 'Обычный',
+ ordered: 'Нумерованный'
+ },
+ button = document.createElement('DIV');
+
+ button.innerHTML = types[buttonType];
+
+ button.classList.add(elementClasses_.settingsItem);
+
+ return button;
+ }
+ };
+
+ var methods_ = {
+
+ /**
+ * Changes block type => OL or UL
+ * @param event
+ * @param blockType
+ */
+ changeBlockStyle : function (event, blockType) {
+ var currentBlock = codex.editor.content.currentNode,
+ newEditable = ui.make(blockType),
+ oldEditable = currentBlock.querySelector('[contenteditable]');
+
+ newEditable.dataset.type = blockType;
+ newEditable.innerHTML = oldEditable.innerHTML;
+ newEditable.classList.add(elementClasses_.pluginWrapper);
+
+ codex.editor.content.switchBlock(currentBlock, newEditable, 'list');
+ },
+ keyDown: function (e) {
+ var controlKeyPressed = e.ctrlKey || e.metaKey,
+ keyCodeForA = 65;
+
+ /**
+ * If CTRL+A (CMD+A) was pressed, we should select only one list item,
+ * not all or
+ */
+ if (controlKeyPressed && e.keyCode == keyCodeForA) {
+ e.preventDefault();
+
+ /**
+ * Select
content
+ */
+ methods_.selectListItem();
+ }
+ },
+
+ /**
+ * Select all content of
with caret
+ */
+ selectListItem : function () {
+ var selection = window.getSelection(),
+ currentSelectedNode = selection.anchorNode.parentNode,
+ range = new Range();
+
+ /**
+ * Search for
+ *
+ * @return {Element[]}
+ */
+ getHigherLevelSiblings(from, direction ) {
+ let current = from,
+ siblings = [];
+
+ /**
+ * Find passed node's firs-level parent (in example - blockquote)
+ */
+ while (current.parentNode && current.parentNode.contentEditable !== 'true') {
+ current = current.parentNode;
+ }
+
+ let sibling = direction === 'left' ? 'previousSibling' : 'nextSibling';
+
+ /**
+ * Find all left/right siblings
+ */
+ while (current[sibling]) {
+ current = current[sibling];
+ siblings.push(current);
+ }
+
+ return siblings;
+ }
+
+ /**
+ * Set's caret to the next Block
+ * Before moving caret, we should check if caret position is at the end of Plugins node
+ * Using {@link Dom#getDeepestNode} to get a last node and match with current selection
+ *
+ * @param {Boolean} force - force navigation even if caret is not at the end
+ *
+ * @return {Boolean}
+ */
+ navigateNext(force = false) {
+ let nextBlock = this.Editor.BlockManager.nextBlock;
+
+ if (!nextBlock) {
+ return false;
+ }
+
+ if (force || this.isAtEnd) {
+ this.setToBlock(nextBlock);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Set's caret to the previous Block
+ * Before moving caret, we should check if caret position is start of the Plugins node
+ * Using {@link Dom#getDeepestNode} to get a last node and match with current selection
+ *
+ * @param {Boolean} force - force navigation even if caret is not at the start
+ *
+ * @return {Boolean}
+ */
+ navigatePrevious(force = false) {
+ let previousBlock = this.Editor.BlockManager.previousBlock;
+
+ if (!previousBlock) {
+ return false;
+ }
+
+ if (force || this.isAtStart) {
+ this.setToBlock( previousBlock, 0, true );
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Get's deepest first node and checks if offset is zero
+ * @return {boolean}
+ */
+ get isAtStart() {
+ /**
+ * Don't handle ranges
+ */
+ if (!Selection.isCollapsed) {
+ return false;
+ }
+
+ let selection = Selection.get(),
+ anchorNode = selection.anchorNode,
+ firstNode = $.getDeepestNode(this.Editor.BlockManager.currentBlock.pluginsContent);
+
+ /**
+ * Workaround case when caret in the text like " |Hello!"
+ * selection.anchorOffset is 1, but real caret visible position is 0
+ * @type {number}
+ */
+ let firstLetterPosition = anchorNode.textContent.search(/\S/);
+
+ if (firstLetterPosition === -1) { // empty text
+ firstLetterPosition = 0;
+ }
+
+ /**
+ * In case of
+ *
+ *
<-- first (and deepest) node is
+ * |adaddad <-- anchor node
+ *
+ */
+ if ($.isEmpty(firstNode)) {
+ let leftSiblings = this.getHigherLevelSiblings(anchorNode, 'left'),
+ nothingAtLeft = leftSiblings.every( node => $.isEmpty(node) );
+
+
+
+ if (nothingAtLeft && selection.anchorOffset === firstLetterPosition) {
+ return true;
+ }
+ }
+
+ /**
+ * We use <= comparison for case:
+ * "| Hello" <--- selection.anchorOffset is 0, but firstLetterPosition is 1
+ */
+ return firstNode === null || anchorNode === firstNode && selection.anchorOffset <= firstLetterPosition;
+ }
+
+ /**
+ * Get's deepest last node and checks if offset is last node text length
+ * @return {boolean}
+ */
+ get isAtEnd() {
+ /**
+ * Don't handle ranges
+ */
+ if (!Selection.isCollapsed) {
+ return false;
+ }
+
+ let selection = Selection.get(),
+ anchorNode = selection.anchorNode,
+ lastNode = $.getDeepestNode(this.Editor.BlockManager.currentBlock.pluginsContent, true);
+
+ /**
+ * In case of
+ *
+ * adaddad| <-- anchor node
+ *
<-- first (and deepest) node is
+ *
+ */
+ if ($.isEmpty(lastNode)) {
+ let leftSiblings = this.getHigherLevelSiblings(anchorNode, 'right'),
+ nothingAtRight = leftSiblings.every( node => $.isEmpty(node) );
+
+ if (nothingAtRight && selection.anchorOffset === anchorNode.textContent.length) {
+ return true;
+ }
+ }
+
+ /**
+ * Workaround case:
+ * hello | <--- anchorOffset will be 5, but textContent.length will be 6.
+ * Why not regular .trim():
+ * in case of ' hello |' trim() will also remove space at the beginning, so length will be lower than anchorOffset
+ */
+ let rightTrimmedText = lastNode.textContent.replace(/\s+$/, '');
+
+ /**
+ * We use >= comparison for case:
+ * "Hello |" <--- selection.anchorOffset is 7, but rightTrimmedText is 6
+ */
+ return anchorNode === lastNode && selection.anchorOffset >= rightTrimmedText.length;
+ }
+
+ /**
+ * Inserts shadow element after passed element where caret can be placed
+ * @param {Node} element
+ */
+ createShadow(element) {
+ let shadowCaret = document.createElement('span');
+
+ shadowCaret.classList.add(Caret.CSS.shadowCaret);
+ element.insertAdjacentElement('beforeEnd', shadowCaret);
+ }
+
+ /**
+ * Restores caret position
+ * @param {Node} element
+ */
+ restoreCaret(element) {
+ let shadowCaret = element.querySelector(`.${Caret.CSS.shadowCaret}`);
+
+ if (!shadowCaret) {
+ return;
+ }
+
+ /**
+ * After we set the caret to the required place
+ * we need to clear shadow caret
+ *
+ * - make new range
+ * - select shadowed span
+ * - use extractContent to remove it from DOM
+ */
+ let sel = new Selection();
+
+ sel.expandToTag(shadowCaret);
+
+ setTimeout(() => {
+ let newRange = document.createRange();
+
+ newRange.selectNode(shadowCaret);
+ newRange.extractContents();
+ }, 50);
+ }
+}
diff --git a/src/components/modules/events.js b/src/components/modules/events.js
new file mode 100644
index 00000000..5cb42283
--- /dev/null
+++ b/src/components/modules/events.js
@@ -0,0 +1,78 @@
+/**
+ * @module eventDispatcher
+ *
+ * Has two important methods:
+ * - {Function} on - appends subscriber to the event. If event doesn't exist - creates new one
+ * - {Function} emit - fires all subscribers with data
+ * - {Function off - unsubsribes callback
+ *
+ * @version 1.0.0
+ *
+ * @typedef {Events} Events
+ * @property {Object} subscribers - all subscribers grouped by event name
+ */
+export default class Events extends Module {
+ /**
+ * @constructor
+ */
+ constructor({config}) {
+ super({config});
+ this.subscribers = {};
+ }
+
+ /**
+ * Subscribe any event on callback
+ *
+ * @param {String} eventName - event name
+ * @param {Function} callback - subscriber
+ */
+ on(eventName, callback) {
+ if (!(eventName in this.subscribers)) {
+ this.subscribers[eventName] = [];
+ }
+
+ // group by events
+ this.subscribers[eventName].push(callback);
+ }
+
+ /**
+ * Emit callbacks with passed data
+ *
+ * @param {String} eventName - event name
+ * @param {Object} data - subscribers get this data when they were fired
+ */
+ emit(eventName, data) {
+ if (!this.subscribers[eventName]) {
+ return;
+ }
+
+ this.subscribers[eventName].reduce(function (previousData, currentHandler) {
+ let newData = currentHandler(previousData);
+
+ return newData ? newData : previousData;
+ }, data);
+ }
+
+ /**
+ * Unsubsribe callback from event
+ *
+ * @param eventName
+ * @param callback
+ */
+ off(eventName, callback) {
+ for(let i = 0; i < this.subscribers[eventName].length; i++) {
+ if (this.subscribers[eventName][i] === callback) {
+ delete this.subscribers[eventName][i];
+ break;
+ }
+ }
+ }
+
+ /**
+ * Destroyer
+ * clears subsribers list
+ */
+ destroy() {
+ this.subscribers = null;
+ }
+}
diff --git a/src/components/modules/listeners.js b/src/components/modules/listeners.js
new file mode 100644
index 00000000..435d75e5
--- /dev/null
+++ b/src/components/modules/listeners.js
@@ -0,0 +1,174 @@
+/**
+ * Codex Editor Listeners module
+ *
+ * @module Listeners
+ *
+ * Module-decorator for event listeners assignment
+ *
+ * @author Codex Team
+ * @version 2.0.0
+ */
+
+/**
+ * @typedef {Listeners} Listeners
+ * @property {Array} allListeners
+ */
+export default class Listeners extends Module {
+ /**
+ * @constructor
+ * @param {EditorConfig} config
+ */
+ constructor({config}) {
+ super({config});
+ this.allListeners = [];
+ }
+
+ /**
+ * Assigns event listener on element
+ *
+ * @param {Element} element - DOM element that needs to be listened
+ * @param {String} eventType - event type
+ * @param {Function} handler - method that will be fired on event
+ * @param {Boolean} useCapture - use event bubbling
+ */
+ on(element, eventType, handler, useCapture = false) {
+ let assignedEventData = {
+ element,
+ eventType,
+ handler,
+ useCapture
+ };
+
+ let alreadyExist = this.findOne(element, eventType, handler);
+
+ if (alreadyExist) return;
+
+ this.allListeners.push(assignedEventData);
+ element.addEventListener(eventType, handler, useCapture);
+ }
+
+ /**
+ * Removes event listener from element
+ *
+ * @param {Element} element - DOM element that we removing listener
+ * @param {String} eventType - event type
+ * @param {Function} handler - remove handler, if element listens several handlers on the same event type
+ * @param {Boolean} useCapture - use event bubbling
+ */
+ off(element, eventType, handler, useCapture = false) {
+ let existingListeners = this.findAll(element, eventType, handler);
+
+ for (let i = 0; i < existingListeners.length; i++) {
+ let index = this.allListeners.indexOf(existingListeners[i]);
+
+ if (index > 0) {
+ this.allListeners.splice(index, 1);
+ }
+ }
+
+ element.removeEventListener(eventType, handler, useCapture);
+ }
+
+ /**
+ * Search method: looks for listener by passed element
+ * @param {Element} element - searching element
+ * @returns {Array} listeners that found on element
+ */
+ findByElement(element) {
+ let listenersOnElement = [];
+
+ for (let i = 0; i < this.allListeners.length; i++) {
+ let listener = this.allListeners[i];
+
+ if (listener.element === element) {
+ listenersOnElement.push(listener);
+ }
+ }
+
+ return listenersOnElement;
+ }
+
+ /**
+ * Search method: looks for listener by passed event type
+ * @param {String} eventType
+ * @return {Array} listeners that found on element
+ */
+ findByType(eventType) {
+ let listenersWithType = [];
+
+ for (let i = 0; i < this.allListeners.length; i++) {
+ let listener = this.allListeners[i];
+
+ if (listener.type === eventType) {
+ listenersWithType.push(listener);
+ }
+ }
+
+ return listenersWithType;
+ }
+
+ /**
+ * Search method: looks for listener by passed handler
+ * @param {Function} handler
+ * @return {Array} listeners that found on element
+ */
+ findByHandler(handler) {
+ let listenersWithHandler = [];
+
+ for (let i = 0; i < this.allListeners.length; i++) {
+ let listener = this.allListeners[i];
+
+ if (listener.handler === handler) {
+ listenersWithHandler.push(listener);
+ }
+ }
+
+ return listenersWithHandler;
+ }
+
+ /**
+ * @param {Element} element
+ * @param {String} eventType
+ * @param {Function} handler
+ * @return {Element|null}
+ */
+ findOne(element, eventType, handler) {
+ let foundListeners = this.findAll(element, eventType, handler);
+
+ return foundListeners.length > 0 ? foundListeners[0] : null;
+ }
+
+ /**
+ * @param {Element} element
+ * @param {String} eventType
+ * @param {Function} handler
+ * @return {Array}
+ */
+ findAll(element, eventType, handler) {
+ let found,
+ foundByElements = element ? this.findByElement(element) : [];
+ // foundByEventType = eventType ? this.findByType(eventType) : [],
+ // foundByHandler = handler ? this.findByHandler(handler) : [];
+
+ if (element && eventType && handler) {
+ found = foundByElements.filter( event => event.eventType === eventType && event.handler === handler );
+ } else if (element && eventType) {
+ found = foundByElements.filter( event => event.eventType === eventType);
+ } else {
+ found = foundByElements;
+ }
+
+ return found;
+ }
+
+ /**
+ * Removes all listeners
+ */
+ removeAll() {
+ this.allListeners.map( (current) => {
+ current.element.removeEventListener(current.eventType, current.handler);
+ });
+
+ this.allListeners = [];
+ }
+}
diff --git a/src/components/modules/paste.ts b/src/components/modules/paste.ts
new file mode 100644
index 00000000..3e78a088
--- /dev/null
+++ b/src/components/modules/paste.ts
@@ -0,0 +1,487 @@
+/**
+ * @class Paste
+ * @classdesc Contains methods to handle paste on editor
+ *
+ * @module Paste
+ *
+ * @version 2.0.0
+ */
+
+import {IBlockToolData} from '../interfaces/block-tool';
+
+declare const Module: any;
+declare const $: any;
+declare const _: any;
+
+/**
+ * Tag substitute object.
+ *
+ * @param {string} tool - name of related Tool
+ * @param {Function} handler - callback to handle pasted element
+ */
+interface ITagSubstitute {
+ tool: string;
+ handler: (element: HTMLElement) => IBlockToolData;
+}
+
+/**
+ * Pattern substitute object.
+ *
+ * @param {string} key - pattern`s key
+ * @param {RegExp} pattern - pasted pattern
+ * @param {Function} handler - callback to handle pasted pattern
+ * @param {string} tool - name of related Tool
+ */
+interface IPatternSubstitute {
+ key: string;
+ pattern: RegExp;
+ handler: (text: string, key: string) => IBlockToolData;
+ tool: string;
+}
+
+/**
+ * Processed paste data object.
+ *
+ * @param {string} tool - name of related Tool
+ * @param {HTMLElement} content - processed pasted content
+ * @param {boolean} isBlock - true if content should be inserted as new Block
+ * @param {Function} handler - callback that returns pasted data in IBlockToolData format
+ */
+interface IPasteData {
+ tool: string;
+ content: HTMLElement;
+ isBlock: boolean;
+ handler: (content: HTMLElement|string, patten?: RegExp) => IBlockToolData;
+}
+
+export default class Paste extends Module {
+
+ /** If string`s length is greater than this number we don't check paste patterns */
+ public static readonly PATTERN_PROCESSING_MAX_LENGTH = 450;
+
+ /**
+ * Tags` substitutions parameters
+ */
+ private toolsTags: {[tag: string]: ITagSubstitute} = {};
+
+ /** Patterns` substitutions parameters */
+ private toolsPatterns: IPatternSubstitute[] = [];
+
+ /**
+ * @constructor
+ */
+ constructor({config}) {
+ super({config});
+ }
+
+ public async prepare(): Promise {
+ this.setCallback();
+ this.processTools();
+ }
+
+ /**
+ * Set onPaste callback handler
+ */
+ private setCallback(): void {
+ const {Listeners, UI} = this.Editor;
+ Listeners.on(UI.nodes.redactor, 'paste', this.processPastedData);
+ }
+
+ /**
+ * Get and process tool`s paste configs
+ */
+ private processTools(): void {
+ const tools = this.Editor.Tools.blockTools;
+ Object.entries(tools).forEach(this.processTool);
+ }
+
+ /**
+ * Process paste config for each tools
+ *
+ * @param {string} tool
+ */
+ private processTool = ([name, tool]) => {
+
+ const toolPasteConfig = tool.onPaste || {};
+
+ if (!toolPasteConfig.handler) {
+ _.log(
+ `"${name}" Tool MUST provide paste handler.`,
+ 'warn',
+ );
+ }
+
+ if (typeof toolPasteConfig.handler !== 'function') {
+ _.log(
+ `Paste handler for "${name}" Tool should be a function.`,
+ 'warn',
+ );
+ } else {
+ const tags = toolPasteConfig.tags || [];
+
+ tags.forEach((tag) => {
+ if (this.toolsTags.hasOwnProperty(tag)) {
+ _.log(
+ `Paste handler for "${name}" Tool on "${tag}" tag is skipped ` +
+ `because it is already used by "${this.toolsTags[tag].tool}" Tool.`,
+ 'warn',
+ );
+ return;
+ }
+
+ this.toolsTags[tag] = {
+ handler: toolPasteConfig.handler,
+ tool: name,
+ };
+ });
+ }
+
+ if (!toolPasteConfig.patternHandler || _.isEmpty(toolPasteConfig.patterns)) {
+ return;
+ }
+
+ if (typeof toolPasteConfig.patternHandler !== 'function') {
+ _.log(
+ `Pattern parser for "${name}" Tool should be a function.`,
+ 'warn',
+ );
+ } else {
+ Object.entries(toolPasteConfig.patterns).forEach(([key, pattern]: [string, RegExp]) => {
+ /** Still need to validate pattern as it provided by user */
+ if (!(pattern instanceof RegExp)) {
+ _.log(
+ `Pattern ${pattern} for "${tool}" Tool is skipped because it should be a Regexp instance.`,
+ 'warn',
+ );
+ }
+
+ this.toolsPatterns.push({
+ key,
+ pattern,
+ handler: toolPasteConfig.patternHandler,
+ tool: name,
+ });
+ });
+ }
+ }
+
+ /**
+ * Check if browser behavior suits better
+ *
+ * @param {EventTarget} element - element where content has been pasted
+ * @returns {boolean}
+ */
+ private isNativeBehaviour(element: EventTarget): boolean {
+ const {Editor: {BlockManager}} = this;
+
+ if ( $.isNativeInput(element) ) {
+ return true;
+ }
+
+ const block = BlockManager.getBlock(element);
+
+ return !block;
+ }
+
+ /**
+ * Get pasted data, process it and insert into editor
+ *
+ * @param {ClipboardEvent} event
+ */
+ private processPastedData = async (event: ClipboardEvent): Promise => {
+ const {
+ Editor: {Tools, Sanitizer, BlockManager, Caret},
+ config: {toolsConfig},
+ } = this;
+
+ /** If target is native input or is not Block, use browser behaviour */
+ if (this.isNativeBehaviour(event.target)) {
+ return;
+ }
+
+ event.preventDefault();
+
+ const block = BlockManager.getBlock(event.target);
+ const toolConfig = toolsConfig[block.name];
+
+ /** If paste is dissalowed in block do nothing */
+ if (toolConfig && toolConfig[Tools.apiSettings.IS_PASTE_DISALLOWED]) {
+ return;
+ }
+
+ const htmlData = event.clipboardData.getData('text/html'),
+ plainData = event.clipboardData.getData('text/plain');
+
+ /** Add all block tags and tags can be substituted to sanitizer configuration */
+ const blockTags = $.blockElements.reduce((result, tag) => {
+ result[tag.toLowerCase()] = {};
+
+ return result;
+ }, {});
+ const toolsTags = Object.keys(this.toolsTags).reduce((result, tag) => {
+ result[tag.toLowerCase()] = {};
+
+ return result;
+ }, {});
+
+ const customConfig = {tags: Object.assign({}, blockTags, toolsTags, Sanitizer.defaultConfig.tags)};
+ const cleanData = Sanitizer.clean(htmlData, customConfig);
+
+ let dataToInsert = [];
+
+ /** If there is no HTML or HTML string is equal to plain one, process it as plain text */
+ if (!cleanData.trim() || cleanData.trim() === plainData || !$.isHTMLString(cleanData)) {
+ dataToInsert = this.processPlain(plainData);
+ } else {
+ dataToInsert = this.processHTML(htmlData);
+ }
+
+ if (dataToInsert.length === 1 && !dataToInsert[0].isBlock) {
+ this.processSingleBlock(dataToInsert.pop());
+ return;
+ }
+
+ this.splitBlock();
+
+ await Promise.all(dataToInsert.map(
+ async (data, i) => await this.insertBlock(data, i === 0),
+ ));
+
+ Caret.setToBlock(BlockManager.currentBlock, 0, true);
+ }
+
+ /**
+ * Process paste to single Block:
+ * 1. Find patterns` matches
+ * 2. Insert new block if it is not the same type as current one
+ * 3. Just insert text if there is no substitutions
+ *
+ * @param {IPasteData} dataToInsert
+ */
+ private async processSingleBlock(dataToInsert: IPasteData): Promise {
+ const initialTool = this.config.initialBlock;
+ const {BlockManager} = this.Editor;
+ const {content, tool} = dataToInsert;
+
+ if (tool === initialTool && content.textContent.length < Paste.PATTERN_PROCESSING_MAX_LENGTH) {
+ const blockData = await this.processPattern(content.textContent);
+
+ if (blockData) {
+ this.splitBlock();
+
+ if (BlockManager.currentBlock.isEmpty) {
+ BlockManager.replace(blockData.tool, blockData.data);
+ } else {
+ BlockManager.insert(blockData.tool, blockData.data);
+ }
+ return;
+ }
+ }
+
+ /** If there is no pattern substitute - insert string as it is */
+ document.execCommand('insertHTML', false, content.innerHTML);
+ }
+
+ /**
+ * Get patterns` matches
+ *
+ * @param {string} text
+ * @returns Promise<{data: IBlockToolData, tool: string}>
+ */
+ private async processPattern(text: string): Promise<{data: IBlockToolData, tool: string}> {
+ const pattern = this.toolsPatterns.find((substitute) => {
+ const execResult = substitute.pattern.exec(text);
+
+ if (!execResult) {
+ return false;
+ }
+
+ return text === execResult.shift();
+ });
+
+ const data = pattern && await pattern.handler(text, pattern.key);
+
+ return data && {
+ data,
+ tool: pattern.tool,
+ };
+ }
+
+ /**
+ *
+ * @param {IPasteData} data
+ * @param {Boolean} canReplaceCurrentBlock - if true and is current Block is empty, will replace current Block
+ * @returns {Promise}
+ */
+ private async insertBlock(data: IPasteData, canReplaceCurrentBlock: boolean = false): Promise {
+ const blockData = await data.handler(data.content);
+ const {BlockManager} = this.Editor;
+ const {currentBlock} = BlockManager;
+
+ if (canReplaceCurrentBlock && currentBlock.isEmpty) {
+ BlockManager.replace(data.tool, blockData);
+ return;
+ }
+
+ BlockManager.insert(data.tool, blockData);
+ }
+
+ /**
+ * Split current block if paste isn't in the end of the block
+ */
+ private splitBlock() {
+ const {BlockManager, Caret} = this.Editor;
+
+ /** If we paste into middle of the current block:
+ * 1. Split
+ * 2. Navigate to the first part
+ */
+ if (!BlockManager.currentBlock.isEmpty && !Caret.isAtEnd) {
+ BlockManager.split();
+ BlockManager.currentBlockIndex--;
+ }
+ }
+
+ /**
+ * Split HTML string to blocks and return it as array of Block data
+ *
+ * @param {string} innerHTML
+ * @returns {IPasteData[]}
+ */
+ private processHTML(innerHTML: string): IPasteData[] {
+ const {Tools, Sanitizer} = this.Editor;
+ const initialTool = this.config.initialBlock;
+ const wrapper = $.make('DIV');
+
+ wrapper.innerHTML = innerHTML;
+
+ const nodes = this.getNodes(wrapper);
+
+ return nodes
+ .map((node) => {
+ let content, tool = initialTool, isBlock = false;
+
+ switch (node.nodeType) {
+ /** If node is a document fragment, use temp wrapper to get innerHTML */
+ case Node.DOCUMENT_FRAGMENT_NODE:
+ content = $.make('div');
+ content.appendChild(node);
+ content.innerHTML = Sanitizer.clean(content.innerHTML);
+ break;
+
+ /** If node is an element, then there might be a substitution */
+ case Node.ELEMENT_NODE:
+ content = node as HTMLElement;
+ isBlock = true;
+ content.innerHTML = Sanitizer.clean(content.innerHTML);
+
+ if (this.toolsTags[content.tagName]) {
+ tool = this.toolsTags[content.tagName].tool;
+ }
+ break;
+ }
+
+ const handler = Tools.blockTools[tool].onPaste.handler;
+
+ return {content, isBlock, handler, tool};
+ })
+ .filter((data) => !$.isNodeEmpty(data.content));
+ }
+
+ /**
+ * Split plain text by new line symbols and return it as array of Block data
+ *
+ * @param {string} plain
+ * @returns {IPasteData[]}
+ */
+ private processPlain(plain: string): IPasteData[] {
+ const {initialBlock} = this.config as {initialBlock: string};
+ const {Tools} = this.Editor;
+
+ if (!plain) {
+ return [];
+ }
+ const tool = initialBlock;
+ const handler = Tools.blockTools[tool].onPaste.handler;
+
+ return plain.split('\n\n').map((text) => {
+ const content = $.make('div');
+
+ content.innerHTML = plain;
+ return {content, tool, isBlock: false, handler};
+ });
+ }
+
+ /**
+ * Recursively divide HTML string to two types of nodes:
+ * 1. Block element
+ * 2. Document Fragments contained text and markup tags like a, b, i etc.
+ *
+ * @param {Node} wrapper
+ * @returns {Node[]}
+ */
+ private getNodes(wrapper: Node): Node[] {
+ const children = Array.from(wrapper.childNodes);
+ const tags = Object.keys(this.toolsTags);
+
+ const reducer = (nodes: Node[], node: Node): Node[] => {
+ if ($.isEmpty(node)) {
+ return nodes;
+ }
+
+ const lastNode = nodes[nodes.length - 1];
+ let destNode: Node = new DocumentFragment();
+
+ if (lastNode && $.isFragment(lastNode)) {
+ destNode = nodes.pop();
+ }
+
+ switch (node.nodeType) {
+ /**
+ * If node is HTML element:
+ * 1. Check if it is inline element
+ * 2. Check if it contains another block or substitutable elements
+ */
+ case Node.ELEMENT_NODE:
+ const element = node as HTMLElement;
+ /** Append inline elements to previous fragment */
+ if (
+ !$.blockElements.includes(element.tagName.toLowerCase()) &&
+ !tags.includes(element.tagName.toLowerCase())
+ ) {
+ destNode.appendChild(element);
+ return [...nodes, destNode];
+ }
+
+ if (
+ (
+ tags.includes(element.tagName.toLowerCase()) &&
+ Array.from(element.children).every(({tagName}) => !tags.includes(tagName.toLowerCase()))
+ ) || (
+ $.blockElements.includes(element.tagName.toLowerCase()) &&
+ Array.from(element.children).every(
+ ({tagName}) => !$.blockElements.includes(tagName.toLowerCase()),
+ )
+ )
+ ) {
+ return [...nodes, element];
+ }
+ break;
+
+ /**
+ * If node is text node, wrap it with DocumentFragment
+ */
+ case Node.TEXT_NODE:
+ destNode.appendChild(node);
+ return [...nodes, destNode];
+
+ default:
+ return [...nodes, destNode];
+ }
+
+ return [...nodes, ...Array.from(node.childNodes).reduce(reducer, [])];
+ };
+
+ return children.reduce(reducer, []);
+ }
+}
diff --git a/src/components/modules/renderer.js b/src/components/modules/renderer.js
new file mode 100644
index 00000000..dd6ea93a
--- /dev/null
+++ b/src/components/modules/renderer.js
@@ -0,0 +1,88 @@
+/**
+ * Codex Editor Renderer Module
+ *
+ * @module Renderer
+ * @author CodeX Team
+ *
+ * @version 2.0.0
+ */
+export default class Renderer extends Module {
+ /**
+ * @constructor
+ * @param {EditorConfig} config
+ */
+ constructor({config}) {
+ super({config});
+ }
+
+ /**
+ * @typedef {Object} RendererItems
+ * @property {String} type - tool name
+ * @property {Object} data - tool data
+ */
+
+ /**
+ * @example
+ *
+ * items: [
+ * {
+ * type : 'paragraph',
+ * data : {
+ * text : 'Hello from Codex!'
+ * }
+ * },
+ * {
+ * type : 'paragraph',
+ * data : {
+ * text : 'Leave feedback if you like it!'
+ * }
+ * },
+ * ]
+ *
+ */
+
+ /**
+ * Make plugin blocks from array of plugin`s data
+ * @param {RendererItems[]} items
+ */
+ render(items) {
+ let chainData = [];
+
+ for (let i = 0; i < items.length; i++) {
+ chainData.push({
+ function: () => this.insertBlock(items[i])
+ });
+ }
+
+ return _.sequence(chainData);
+ }
+
+ /**
+ * Get plugin instance
+ * Add plugin instance to BlockManager
+ * Insert block to working zone
+ *
+ * @param {Object} item
+ * @returns {Promise.}
+ * @private
+ */
+ insertBlock(item) {
+ let tool = item.type,
+ data = item.data,
+ settings = item.settings;
+
+ if (tool in this.Editor.Tools.available) {
+ this.Editor.BlockManager.insert(tool, data, settings);
+ } else {
+ /**
+ * @todo show warning notification message
+ *
+ * `${tool} blocks was skipped.`
+ */
+
+ _.log(`Tool «${tool}» is not found. Check 'tools' property at your initial CodeX Editor config.`, 'warn');
+ }
+
+ return Promise.resolve();
+ }
+}
diff --git a/src/components/modules/sanitizer.js b/src/components/modules/sanitizer.js
new file mode 100644
index 00000000..b0cb51d6
--- /dev/null
+++ b/src/components/modules/sanitizer.js
@@ -0,0 +1,131 @@
+/**
+ * CodeX Sanitizer
+ *
+ * @module Sanitizer
+ * Clears HTML from taint tags
+ *
+ * @version 2.0.0
+ *
+ * @example
+ * Module can be used within two ways:
+ * 1) When you have an instance
+ * - this.Editor.Sanitizer.clean(yourTaintString);
+ * 2) As static method
+ * - CodexEditor.Sanitizer.clean(yourTaintString, yourCustomConfiguration);
+ *
+ * {@link SanitizerConfig}
+ */
+
+
+/**
+ * @typedef {Object} SanitizerConfig
+ * @property {Object} tags - define tags restrictions
+ *
+ * @example
+ *
+ * tags : {
+ * p: true,
+ * a: {
+ * href: true,
+ * rel: "nofollow",
+ * target: "_blank"
+ * }
+ * }
+ */
+export default class Sanitizer extends Module {
+ /**
+ * Initializes Sanitizer module
+ * Sets default configuration if custom not exists
+ *
+ * @property {SanitizerConfig} this.defaultConfig
+ * @property {HTMLJanitor} this._sanitizerInstance - Sanitizer library
+ *
+ * @param {SanitizerConfig} config
+ */
+ constructor({config}) {
+ super({config});
+
+ // default config
+ this.defaultConfig = null;
+ this._sanitizerInstance = null;
+
+ /** Custom configuration */
+ this.sanitizerConfig = config.settings ? config.settings.sanitizer : {};
+
+ /** HTML Janitor library */
+ this.sanitizerInstance = require('html-janitor');
+ }
+
+ /**
+ * If developer uses editor's API, then he can customize sanitize restrictions.
+ * Or, sanitizing config can be defined globally in editors initialization. That config will be used everywhere
+ * At least, if there is no config overrides, that API uses Default configuration
+ *
+ * @uses https://www.npmjs.com/package/html-janitor
+ *
+ * @param {HTMLJanitor} library - sanitizer extension
+ */
+ set sanitizerInstance(library) {
+ this._sanitizerInstance = new library(this.defaultConfig);
+ }
+
+ /**
+ * Sets sanitizer configuration. Uses default config if user didn't pass the restriction
+ * @param {SanitizerConfig} config
+ */
+ set sanitizerConfig(config) {
+ if (_.isEmpty(config)) {
+ this.defaultConfig = {
+ tags: {
+ p: {},
+ a: {
+ href: true,
+ target: '_blank',
+ rel: 'nofollow'
+ },
+ b: {},
+ i: {}
+ }
+ };
+ } else {
+ this.defaultConfig = config;
+ }
+ }
+
+ /**
+ * Cleans string from unwanted tags
+ * @param {String} taintString - HTML string
+ * @param {Object} customConfig - custom sanitizer configuration. Method uses default if param is empty
+ * @return {String} clean HTML
+ */
+ clean(taintString, customConfig = {}) {
+ if (_.isEmpty(customConfig)) {
+ return this._sanitizerInstance.clean(taintString);
+ } else {
+ return Sanitizer.clean(taintString, customConfig);
+ }
+ }
+
+ /**
+ * Cleans string from unwanted tags
+ * @static
+ *
+ * Method allows to use default config
+ *
+ * @param {String} taintString - taint string
+ * @param {SanitizerConfig} customConfig - allowed tags
+ *
+ * @return {String} clean HTML
+ */
+ static clean(taintString, customConfig) {
+ let newInstance = new Sanitizer({
+ config: {
+ settings: {
+ sanitizer: customConfig
+ }
+ }
+ });
+
+ return newInstance.clean(taintString);
+ }
+}
diff --git a/src/components/modules/saver.js b/src/components/modules/saver.js
new file mode 100644
index 00000000..e274cf19
--- /dev/null
+++ b/src/components/modules/saver.js
@@ -0,0 +1,243 @@
+/**
+ * Codex Editor Saver
+ *
+ * @module Saver
+ * @author Codex Team
+ * @version 2.0.0
+ */
+
+/**
+ * @typedef {Object} SavedData
+ * @property {Date} time - saving proccess time
+ * @property {Object} items - extracted data
+ * @property {String} version - CodexEditor version
+ */
+
+/**
+ * @classdesc This method reduces all Blocks asyncronically and calls Block's save method to extract data
+ *
+ * @typedef {Saver} Saver
+ * @property {Element} html - Editor HTML content
+ * @property {String} json - Editor JSON output
+ */
+export default class Saver extends Module {
+ /**
+ * @constructor
+ * @param config
+ */
+ constructor({config}) {
+ super({config});
+
+ this.output = null;
+ this.blocksData = [];
+ }
+
+ /**
+ * Composes new chain of Promises to fire them alternatelly
+ * @return {SavedData}
+ */
+ save() {
+ let blocks = this.Editor.BlockManager.blocks,
+ chainData = [];
+
+ blocks.forEach((block) => {
+ chainData.push(block.data);
+ });
+
+ return Promise.all(chainData)
+ .then((allExtractedData) => this.makeOutput(allExtractedData))
+ .then((outputData) => {
+ return outputData;
+ });
+ }
+
+ /**
+ * Creates output object with saved data, time and version of editor
+ * @param {Object} allExtractedData
+ * @return {SavedData}
+ */
+ makeOutput(allExtractedData) {
+ let items = [],
+ totalTime = 0;
+
+ console.groupCollapsed('[CodexEditor saving]:');
+
+ allExtractedData.forEach((extraction) => {
+ /** Group process info */
+ console.log(`«${extraction.tool}» saving info`, extraction);
+ totalTime += extraction.time;
+ items.push({
+ type: extraction.tool,
+ data: extraction.data
+ });
+ });
+
+ console.log('Total', totalTime);
+ console.groupEnd();
+
+ return {
+ time : +new Date(),
+ items : items,
+ version : VERSION,
+ };
+ }
+}
+
+// module.exports = (function (saver) {
+//
+// let editor = codex.editor;
+//
+// /**
+// * @public
+// * Save blocks
+// */
+// saver.save = function () {
+//
+// /** Save html content of redactor to memory */
+// editor.state.html = editor.nodes.redactor.innerHTML;
+//
+// /** Clean jsonOutput state */
+// editor.state.jsonOutput = [];
+//
+// return saveBlocks(editor.nodes.redactor.childNodes);
+//
+// };
+//
+// /**
+// * @private
+// * Save each block data
+// *
+// * @param blocks
+// * @returns {Promise.}
+// */
+// let saveBlocks = function (blocks) {
+//
+// let data = [];
+//
+// for(let index = 0; index < blocks.length; index++) {
+//
+// data.push(getBlockData(blocks[index]));
+//
+// }
+//
+// return Promise.all(data)
+// .then(makeOutput)
+// .catch(editor.core.log);
+//
+// };
+//
+// /** Save and validate block data */
+// let getBlockData = function (block) {
+//
+// return saveBlockData(block)
+// .then(validateBlockData)
+// .catch(editor.core.log);
+//
+// };
+//
+// /**
+// * @private
+// * Call block`s plugin save method and return saved data
+// *
+// * @param block
+// * @returns {Object}
+// */
+// let saveBlockData = function (block) {
+//
+// let pluginName = block.dataset.tool;
+//
+// /** Check for plugin existence */
+// if (!editor.tools[pluginName]) {
+//
+// editor.core.log(`Plugin «${pluginName}» not found`, 'error');
+// return {data: null, pluginName: null};
+//
+// }
+//
+// /** Check for plugin having save method */
+// if (typeof editor.tools[pluginName].save !== 'function') {
+//
+// editor.core.log(`Plugin «${pluginName}» must have save method`, 'error');
+// return {data: null, pluginName: null};
+//
+// }
+//
+// /** Result saver */
+// let blockContent = block.childNodes[0],
+// pluginsContent = blockContent.childNodes[0],
+// position = pluginsContent.dataset.inputPosition;
+//
+// /** If plugin wasn't available then return data from cache */
+// if ( editor.tools[pluginName].available === false ) {
+//
+// return Promise.resolve({data: codex.editor.state.blocks.items[position].data, pluginName});
+//
+// }
+//
+// return Promise.resolve(pluginsContent)
+// .then(editor.tools[pluginName].save)
+// .then(data => Object({data, pluginName}));
+//
+// };
+//
+// /**
+// * Call plugin`s validate method. Return false if validation failed
+// *
+// * @param data
+// * @param pluginName
+// * @returns {Object|Boolean}
+// */
+// let validateBlockData = function ({data, pluginName}) {
+//
+// if (!data || !pluginName) {
+//
+// return false;
+//
+// }
+//
+// if (editor.tools[pluginName].validate) {
+//
+// let result = editor.tools[pluginName].validate(data);
+//
+// /**
+// * Do not allow invalid data
+// */
+// if (!result) {
+//
+// return false;
+//
+// }
+//
+// }
+//
+// return {data, pluginName};
+//
+//
+// };
+//
+// /**
+// * Compile article output
+// *
+// * @param savedData
+// * @returns {{time: number, version, items: (*|Array)}}
+// */
+// let makeOutput = function (savedData) {
+//
+// savedData = savedData.filter(blockData => blockData);
+//
+// let items = savedData.map(blockData => Object({type: blockData.pluginName, data: blockData.data}));
+//
+// editor.state.jsonOutput = items;
+//
+// return {
+// id: editor.state.blocks.id || null,
+// time: +new Date(),
+// version: editor.version,
+// items
+// };
+//
+// };
+//
+// return saver;
+//
+// })({});
diff --git a/src/components/modules/toolbar-blockSettings.js b/src/components/modules/toolbar-blockSettings.js
new file mode 100644
index 00000000..e57163d8
--- /dev/null
+++ b/src/components/modules/toolbar-blockSettings.js
@@ -0,0 +1,126 @@
+/**
+ * Block Settings
+ *
+ * ____ Settings Panel ____
+ * | ...................... |
+ * | . Tool Settings . |
+ * | ...................... |
+ * | . Default Settings . |
+ * | ...................... |
+ * |________________________|
+ */
+export default class BlockSettings extends Module {
+ /**
+ * @constructor
+ */
+ constructor({config}) {
+ super({config});
+
+ this.nodes = {
+ wrapper: null,
+ toolSettings: null,
+ defaultSettings: null
+ };
+ }
+
+ /**
+ * Module Events
+ * @return {{opened: string, closed: string}}
+ */
+ get events() {
+ return {
+ opened: 'block-settings-opened',
+ closed: 'block-settings-closed',
+ };
+ }
+
+ /**
+ * Block Settings CSS
+ * @return {{wrapper, wrapperOpened, toolSettings, defaultSettings, button}}
+ */
+ static get CSS() {
+ return {
+ // Settings Panel
+ wrapper: 'ce-settings',
+ wrapperOpened: 'ce-settings--opened',
+ toolSettings: 'ce-settings__plugin-zone',
+ defaultSettings: 'ce-settings__default-zone',
+
+ button: 'ce-settings__button'
+ };
+ }
+
+ /**
+ * Panel with block settings with 2 sections:
+ * - Tool's Settings
+ * - Default Settings [Move, Remove, etc]
+ *
+ * @return {Element}
+ */
+ make() {
+ this.nodes.wrapper = $.make('div', BlockSettings.CSS.wrapper);
+
+ this.nodes.toolSettings = $.make('div', BlockSettings.CSS.toolSettings);
+ this.nodes.defaultSettings = $.make('div', BlockSettings.CSS.defaultSettings);
+
+ $.append(this.nodes.wrapper, [this.nodes.toolSettings, this.nodes.defaultSettings]);
+ }
+
+ /**
+ * Add Tool's settings
+ */
+ addToolSettings() {
+ if (typeof this.Editor.BlockManager.currentBlock.tool.renderSettings === 'function') {
+ $.append(this.nodes.toolSettings, this.Editor.BlockManager.currentBlock.tool.renderSettings());
+ }
+ }
+
+ /**
+ * Add default settings
+ */
+ addDefaultSettings() {
+ $.append(this.nodes.defaultSettings, this.Editor.BlockManager.currentBlock.renderTunes());
+ }
+
+ /**
+ * Is Block Settings opened or not
+ * @returns {boolean}
+ */
+ get opened() {
+ return this.nodes.wrapper.classList.contains(BlockSettings.CSS.wrapperOpened);
+ }
+
+ /**
+ * Open Block Settings pane
+ */
+ open() {
+ this.nodes.wrapper.classList.add(BlockSettings.CSS.wrapperOpened);
+
+ /**
+ * Fill Tool's settings
+ */
+ this.addToolSettings();
+
+ /**
+ * Add default settings that presents for all Blocks
+ */
+ this.addDefaultSettings();
+
+ /** Tell to subscribers that block settings is opened */
+ this.Editor.Events.emit(this.events.opened);
+ }
+
+ /**
+ * Close Block Settings pane
+ */
+ close() {
+ this.nodes.wrapper.classList.remove(BlockSettings.CSS.wrapperOpened);
+
+ /** Clear settings */
+ this.nodes.toolSettings.innerHTML = '';
+ this.nodes.defaultSettings.innerHTML = '';
+
+ /** Tell to subscribers that block settings is closed */
+ this.Editor.Events.emit(this.events.closed);
+ }
+}
diff --git a/src/components/modules/toolbar-inline.ts b/src/components/modules/toolbar-inline.ts
new file mode 100644
index 00000000..b5aaa930
--- /dev/null
+++ b/src/components/modules/toolbar-inline.ts
@@ -0,0 +1,276 @@
+/**
+ * Inline toolbar with actions that modifies selected text fragment
+ *
+ * |¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯|
+ * | B i [link] [mark] |
+ * | _______________________|
+ */
+declare var Module: any;
+declare var $: any;
+declare var _: any;
+import BoldInlineTool from '../inline-tools/inline-tool-bold';
+import ItalicInlineTool from '../inline-tools/inline-tool-italic';
+import LinkInlineTool from '../inline-tools/inline-tool-link';
+import InlineTool from '../interfaces/inline-tool';
+import Selection from '../selection';
+
+export default class InlineToolbar extends Module {
+
+ /**
+ * CSS styles
+ */
+ public CSS = {
+ inlineToolbar: 'ce-inline-toolbar',
+ inlineToolbarShowed: 'ce-inline-toolbar--showed',
+ buttonsWrapper: 'ce-inline-toolbar__buttons',
+ actionsWrapper: 'ce-inline-toolbar__actions',
+ };
+
+ /**
+ * Inline Toolbar elements
+ */
+ private nodes = {
+ wrapper: null,
+ buttons: null,
+ /**
+ * Zone below the buttons where Tools can create additional actions by 'renderActions()' method
+ * For example, input for the 'link' tool or textarea for the 'comment' tool
+ */
+ actions: null,
+ };
+
+ /**
+ * Margin above/below the Toolbar
+ */
+ private readonly toolbarVerticalMargin: number = 20;
+
+ /**
+ * Tools instances
+ */
+ private toolsInstances: InlineTool[];
+
+ /**
+ * @constructor
+ */
+ constructor({config}) {
+ super({config});
+ }
+
+ /**
+ * Inline Toolbar Tools
+ * @todo Merge internal tools with external
+ */
+ get tools(): InlineTool[] {
+ if (!this.toolsInstances) {
+ this.toolsInstances = [
+ new BoldInlineTool(this.Editor.API.methods),
+ new ItalicInlineTool(this.Editor.API.methods),
+ new LinkInlineTool(this.Editor.API.methods),
+ ...this.Editor.Tools.inline.map( (Tool) => new Tool(this.Editor.API.methods) ),
+ ];
+ }
+ return this.toolsInstances;
+ }
+
+ /**
+ * Making DOM
+ */
+ public make() {
+
+ this.nodes.wrapper = $.make('div', this.CSS.inlineToolbar);
+ this.nodes.buttons = $.make('div', this.CSS.buttonsWrapper);
+ this.nodes.actions = $.make('div', this.CSS.actionsWrapper);
+
+ /**
+ * Append Inline Toolbar to the Editor
+ */
+ $.append(this.nodes.wrapper, [this.nodes.buttons, this.nodes.actions]);
+ $.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper);
+
+ /**
+ * Append Inline Toolbar Tools
+ */
+ this.addTools();
+
+ }
+
+ /**
+ *
+ *
+ * Moving / appearance
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ */
+
+ /**
+ * Shows Inline Toolbar by keyup/mouseup
+ * @param {KeyboardEvent|MouseEvent} event
+ */
+ public handleShowingEvent(event): void {
+ if (!this.allowedToShow(event)) {
+ this.close();
+ return;
+ }
+
+ this.move();
+ this.open();
+
+ /** Check Tools state for selected fragment */
+ this.checkToolsState();
+ }
+
+ /**
+ * Move Toolbar to the selected text
+ */
+ public move(): void {
+
+ const selectionRect = Selection.rect;
+ const wrapperOffset = this.Editor.UI.nodes.wrapper.getBoundingClientRect();
+
+ const newCoords = {
+ x: selectionRect.x - wrapperOffset.left,
+ y: selectionRect.y
+ + selectionRect.height
+ // + window.scrollY
+ - wrapperOffset.top
+ + this.toolbarVerticalMargin,
+ };
+
+ /**
+ * If we know selections width, place InlineToolbar to center
+ */
+ if (selectionRect.width) {
+ newCoords.x += Math.floor(selectionRect.width / 2);
+ }
+
+ this.nodes.wrapper.style.left = Math.floor(newCoords.x) + 'px';
+ this.nodes.wrapper.style.top = Math.floor(newCoords.y) + 'px';
+ }
+
+ /**
+ * Shows Inline Toolbar
+ */
+ private open() {
+ this.nodes.wrapper.classList.add(this.CSS.inlineToolbarShowed);
+
+ this.tools.forEach( (tool) => {
+ if (typeof tool.clear === 'function') {
+ tool.clear();
+ }
+ });
+ }
+
+ /**
+ * Hides Inline Toolbar
+ */
+ private close() {
+ this.nodes.wrapper.classList.remove(this.CSS.inlineToolbarShowed);
+
+ this.tools.forEach( (tool) => {
+ if (typeof tool.clear === 'function') {
+ tool.clear();
+ }
+ });
+ }
+
+ /**
+ * Need to show Inline Toolbar or not
+ * @param {KeyboardEvent|MouseEvent} event
+ */
+ private allowedToShow(event): boolean {
+ /**
+ * Tags conflicts with window.selection function.
+ * Ex. IMG tag returns null (Firefox) or Redactors wrapper (Chrome)
+ */
+ const tagsConflictsWithSelection = ['IMG', 'INPUT'];
+ if (event && tagsConflictsWithSelection.includes(event.target.tagName)) {
+ return false;
+ }
+
+ const currentSelection = Selection.get(),
+ selectedText = Selection.text;
+
+ // old browsers
+ if (!currentSelection || !currentSelection.anchorNode) {
+ return false;
+ }
+
+ // empty selection
+ if (currentSelection.isCollapsed || selectedText.length < 1) {
+ return false;
+ }
+
+ // is enabled by current Block's Tool
+ const currentBlock = this.Editor.BlockManager.getBlock(currentSelection.anchorNode);
+
+ if (!currentBlock) {
+ return false;
+ }
+
+ const toolConfig = this.config.toolsConfig[currentBlock.name];
+
+ return toolConfig && toolConfig[this.Editor.Tools.apiSettings.IS_ENABLED_INLINE_TOOLBAR];
+ }
+
+ /**
+ *
+ *
+ * Working with Tools
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ */
+
+ /**
+ * Fill Inline Toolbar with Tools
+ */
+ private addTools(): void {
+ this.tools.forEach( (tool) => {
+ this.addTool(tool);
+ });
+ }
+
+ /**
+ * Add tool button and activate clicks
+ * @param {InlineTool} tool - Tool's instance
+ */
+ private addTool(tool: InlineTool): void {
+ const button = tool.render();
+
+ if (!button) {
+ _.log('Render method must return an instance of Node', 'warn', tool);
+ return;
+ }
+
+ this.nodes.buttons.appendChild(button);
+
+ if (typeof tool.renderActions === 'function') {
+ const actions = tool.renderActions();
+ this.nodes.actions.appendChild(actions);
+ }
+
+ this.Editor.Listeners.on(button, 'click', () => {
+ this.toolClicked(tool);
+ });
+ }
+
+ /**
+ * Inline Tool button clicks
+ * @param {InlineTool} tool - Tool's instance
+ */
+ private toolClicked(tool: InlineTool): void {
+ const range = Selection.range;
+
+ tool.surround(range);
+ this.checkToolsState();
+
+ }
+
+ /**
+ * Check Tools` state by selection
+ */
+ private checkToolsState(): void {
+ this.tools.forEach( (tool) => {
+ tool.checkState(Selection.get());
+ });
+ }
+}
diff --git a/src/components/modules/toolbar-toolbox.js b/src/components/modules/toolbar-toolbox.js
new file mode 100644
index 00000000..ae3be68a
--- /dev/null
+++ b/src/components/modules/toolbar-toolbox.js
@@ -0,0 +1,198 @@
+/**
+ * @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 {IBlockTool} tool - tool class
+ */
+ addTool(toolName, tool) {
+ const api = this.Editor.Tools.apiSettings;
+
+ if (tool[api.IS_DISPLAYED_IN_TOOLBOX] && !tool[api.TOOLBAR_ICON_CLASS]) {
+ _.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[api.IS_DISPLAYED_IN_TOOLBOX]) {
+ return;
+ }
+
+ let button = $.make('li', [Toolbox.CSS.toolboxButton, tool[api.TOOLBAR_ICON_CLASS]], {
+ title: toolName
+ });
+
+ button.innerHTML = tool.toolboxIcon;
+
+ /**
+ * 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;
+
+ toolButton = toolButton.closest(`.${Toolbox.CSS.toolboxButton}`);
+
+ if (!toolButton) {
+ return;
+ }
+
+ let 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[this.Editor.Tools.apiSettings.IS_IRREPLACEBLE_TOOL] && 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();
+ }
+ }
+}
diff --git a/src/components/modules/toolbar.js b/src/components/modules/toolbar.js
new file mode 100644
index 00000000..be82e6d1
--- /dev/null
+++ b/src/components/modules/toolbar.js
@@ -0,0 +1,246 @@
+/**
+ *
+ * «Toolbar» is the node that moves up/down over current block
+ *
+ * ______________________________________ Toolbar ____________________________________________
+ * | |
+ * | ..................... Content .................... ......... Block Actions .......... |
+ * | . . . . |
+ * | . . . [Open Settings] . |
+ * | . [Plus Button] [Toolbox: {Tool1}, {Tool2}] . . . |
+ * | . . . [Settings Panel] . |
+ * | .................................................. .................................. |
+ * | |
+ * |___________________________________________________________________________________________|
+ *
+ *
+ * Toolbox — its an Element contains tools buttons. Can be shown by Plus Button.
+ *
+ * _______________ Toolbox _______________
+ * | |
+ * | [Header] [Image] [List] [Quote] ... |
+ * |_______________________________________|
+ *
+ *
+ * Settings Panel — is an Element with block settings:
+ *
+ * ____ Settings Panel ____
+ * | ...................... |
+ * | . Tool Settings . |
+ * | ...................... |
+ * | . Default Settings . |
+ * | ...................... |
+ * |________________________|
+ *
+ *
+ * @class
+ * @classdesc Toolbar module
+ *
+ * @typedef {Toolbar} Toolbar
+ * @property {Object} nodes
+ * @property {Element} nodes.wrapper - Toolbar main element
+ * @property {Element} nodes.content - Zone with Plus button and toolbox.
+ * @property {Element} nodes.actions - Zone with Block Settings and Remove Button
+ * @property {Element} nodes.blockActionsButtons - Zone with Block Buttons: [Settings]
+ * @property {Element} nodes.plusButton - Button that opens or closes Toolbox
+ * @property {Element} nodes.toolbox - Container for tools
+ * @property {Element} nodes.settingsToggler - open/close Settings Panel button
+ * @property {Element} nodes.settings - Settings Panel
+ * @property {Element} nodes.pluginSettings - Plugin Settings section of Settings Panel
+ * @property {Element} nodes.defaultSettings - Default Settings section of Settings Panel
+ */
+export default class Toolbar extends Module {
+ /**
+ * @constructor
+ */
+ constructor({config}) {
+ super({config});
+
+ this.nodes = {
+ wrapper : null,
+ content : null,
+ actions : null,
+
+ // Content Zone
+ plusButton : null,
+
+ // Actions Zone
+ blockActionsButtons: null,
+ settingsToggler : null,
+ };
+ }
+
+ /**
+ * 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
+ plusButton: 'ce-toolbar__plus',
+ plusButtonHidden: 'ce-toolbar__plus--hidden',
+
+ // Actions Zone
+ blockActionsButtons: 'ce-toolbar__actions-buttons',
+ settingsToggler: 'ce-toolbar__settings-btn',
+ };
+ }
+
+ /**
+ * Makes toolbar
+ */
+ make() {
+ this.nodes.wrapper = $.make('div', Toolbar.CSS.toolbar);
+
+ /**
+ * Make Content Zone and Actions Zone
+ */
+ ['content', 'actions'].forEach( el => {
+ this.nodes[el] = $.make('div', Toolbar.CSS[el]);
+ $.append(this.nodes.wrapper, this.nodes[el]);
+ });
+
+
+ /**
+ * Fill Content Zone:
+ * - Plus Button
+ * - Toolbox
+ */
+ this.nodes.plusButton = $.make('div', Toolbar.CSS.plusButton);
+ $.append(this.nodes.plusButton, $.svg('plus', 14, 14));
+ $.append(this.nodes.content, this.nodes.plusButton);
+ this.nodes.plusButton.addEventListener('click', event => this.plusButtonClicked(event), false);
+
+
+ /**
+ * Make a Toolbox
+ */
+ this.Editor.Toolbox.make();
+
+ /**
+ * Fill Actions Zone:
+ * - Settings Toggler
+ * - Remove Block Button
+ * - Settings Panel
+ */
+ this.nodes.blockActionsButtons = $.make('div', Toolbar.CSS.blockActionsButtons);
+ this.nodes.settingsToggler = $.make('span', Toolbar.CSS.settingsToggler);
+ const settingsIcon = $.svg('dots', 18, 4);
+
+ $.append(this.nodes.settingsToggler, settingsIcon);
+ $.append(this.nodes.blockActionsButtons, this.nodes.settingsToggler);
+ $.append(this.nodes.actions, this.nodes.blockActionsButtons);
+
+ /**
+ * Make and append Settings Panel
+ */
+ this.Editor.BlockSettings.make();
+ $.append(this.nodes.actions, this.Editor.BlockSettings.nodes.wrapper);
+
+ /**
+ * Append toolbar to the Editor
+ */
+ $.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper);
+
+ /**
+ * Bind events on the Toolbar elements
+ */
+ this.bindEvents();
+ }
+
+ /**
+ * Move Toolbar to the Current Block
+ * @param {Boolean} forceClose - force close Toolbar Settings and Toolbar
+ */
+ move(forceClose = true) {
+ if (forceClose) {
+ /** Close Toolbox when we move toolbar */
+ this.Editor.Toolbox.close();
+ this.Editor.BlockSettings.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)`;
+ }
+
+ /**
+ * 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() {
+ this.Editor.Toolbox.toggle();
+ }
+
+ /**
+ * Bind events on the Toolbar Elements:
+ * - Block Settings
+ */
+ bindEvents() {
+ /**
+ * Settings toggler
+ */
+ this.Editor.Listeners.on(this.nodes.settingsToggler, 'click', (event) => {
+ this.settingsTogglerClicked(event);
+ });
+ }
+
+ /**
+ * Clicks on the Block Settings toggler
+ */
+ settingsTogglerClicked() {
+ if (this.Editor.BlockSettings.opened) {
+ this.Editor.BlockSettings.close();
+ } else {
+ this.Editor.BlockSettings.open();
+ }
+ }
+}
diff --git a/src/components/modules/toolbar/inline.js b/src/components/modules/toolbar/inline.js
new file mode 100644
index 00000000..fb50e8dd
--- /dev/null
+++ b/src/components/modules/toolbar/inline.js
@@ -0,0 +1,490 @@
+/**
+ * Inline toolbar
+ *
+ * Contains from tools:
+ * Bold, Italic, Underline and Anchor
+ *
+ * @author Codex Team
+ * @version 1.0
+ */
+
+module.exports = (function (inline) {
+ let editor = codex.editor;
+
+ inline.buttonsOpened = null;
+ inline.actionsOpened = null;
+ inline.wrappersOffset = null;
+
+ /**
+ * saving selection that need for execCommand for styling
+ *
+ */
+ inline.storedSelection = null;
+
+ /**
+ * @protected
+ *
+ * Open inline toobar
+ */
+ inline.show = function () {
+ var currentNode = editor.content.currentNode,
+ tool = currentNode.dataset.tool,
+ plugin;
+
+ /**
+ * tool allowed to open inline toolbar
+ */
+ plugin = editor.tools[tool];
+
+ if (!plugin.showInlineToolbar)
+ return;
+
+ var selectedText = inline.getSelectionText(),
+ toolbar = editor.nodes.inlineToolbar.wrapper;
+
+ if (selectedText.length > 0) {
+ /** Move toolbar and open */
+ editor.toolbar.inline.move();
+
+ /** Open inline toolbar */
+ toolbar.classList.add('opened');
+
+ /** show buttons of inline toolbar */
+ editor.toolbar.inline.showButtons();
+ }
+ };
+
+ /**
+ * @protected
+ *
+ * Closes inline toolbar
+ */
+ inline.close = function () {
+ var toolbar = editor.nodes.inlineToolbar.wrapper;
+
+ toolbar.classList.remove('opened');
+ };
+
+ /**
+ * @private
+ *
+ * Moving toolbar
+ */
+ inline.move = function () {
+ if (!this.wrappersOffset) {
+ this.wrappersOffset = this.getWrappersOffset();
+ }
+
+ var coords = this.getSelectionCoords(),
+ defaultOffset = 0,
+ toolbar = editor.nodes.inlineToolbar.wrapper,
+ newCoordinateX,
+ newCoordinateY;
+
+ if (toolbar.offsetHeight === 0) {
+ defaultOffset = 40;
+ }
+
+ newCoordinateX = coords.x - this.wrappersOffset.left;
+ newCoordinateY = coords.y + window.scrollY - this.wrappersOffset.top - defaultOffset - toolbar.offsetHeight;
+
+ toolbar.style.transform = `translate3D(${Math.floor(newCoordinateX)}px, ${Math.floor(newCoordinateY)}px, 0)`;
+
+ /** Close everything */
+ editor.toolbar.inline.closeButtons();
+ editor.toolbar.inline.closeAction();
+ };
+
+ /**
+ * @private
+ *
+ * Tool Clicked
+ */
+
+ inline.toolClicked = function (event, type) {
+ /**
+ * For simple tools we use default browser function
+ * For more complicated tools, we should write our own behavior
+ */
+ switch (type) {
+ case 'createLink' : editor.toolbar.inline.createLinkAction(event, type); break;
+ default : editor.toolbar.inline.defaultToolAction(type); break;
+ }
+
+ /**
+ * highlight buttons
+ * after making some action
+ */
+ editor.nodes.inlineToolbar.buttons.childNodes.forEach(editor.toolbar.inline.hightlight);
+ };
+
+ /**
+ * @private
+ *
+ * Saving wrappers offset in DOM
+ */
+ inline.getWrappersOffset = function () {
+ var wrapper = editor.nodes.wrapper,
+ offset = this.getOffset(wrapper);
+
+ this.wrappersOffset = offset;
+ return offset;
+ };
+
+ /**
+ * @private
+ *
+ * Calculates offset of DOM element
+ *
+ * @param el
+ * @returns {{top: number, left: number}}
+ */
+ inline.getOffset = function ( el ) {
+ var _x = 0;
+ var _y = 0;
+
+ while( el && !isNaN( el.offsetLeft ) && !isNaN( el.offsetTop ) ) {
+ _x += (el.offsetLeft + el.clientLeft);
+ _y += (el.offsetTop + el.clientTop);
+ el = el.offsetParent;
+ }
+ return { top: _y, left: _x };
+ };
+
+ /**
+ * @private
+ *
+ * Calculates position of selected text
+ * @returns {{x: number, y: number}}
+ */
+ inline.getSelectionCoords = function () {
+ var sel = document.selection, range;
+ var x = 0, y = 0;
+
+ if (sel) {
+ if (sel.type != 'Control') {
+ range = sel.createRange();
+ range.collapse(true);
+ x = range.boundingLeft;
+ y = range.boundingTop;
+ }
+ } else if (window.getSelection) {
+ sel = window.getSelection();
+
+ if (sel.rangeCount) {
+ range = sel.getRangeAt(0).cloneRange();
+ if (range.getClientRects) {
+ range.collapse(true);
+ var rect = range.getClientRects()[0];
+
+ if (!rect) {
+ return;
+ }
+
+ x = rect.left;
+ y = rect.top;
+ }
+ }
+ }
+ return { x: x, y: y };
+ };
+
+ /**
+ * @private
+ *
+ * Returns selected text as String
+ * @returns {string}
+ */
+ inline.getSelectionText = function () {
+ var selectedText = '';
+
+ // all modern browsers and IE9+
+ if (window.getSelection) {
+ selectedText = window.getSelection().toString();
+ }
+
+ return selectedText;
+ };
+
+ /** Opens buttons block */
+ inline.showButtons = function () {
+ var buttons = editor.nodes.inlineToolbar.buttons;
+
+ buttons.classList.add('opened');
+
+ editor.toolbar.inline.buttonsOpened = true;
+
+ /** highlight buttons */
+ editor.nodes.inlineToolbar.buttons.childNodes.forEach(editor.toolbar.inline.hightlight);
+ };
+
+ /** Makes buttons disappear */
+ inline.closeButtons = function () {
+ var buttons = editor.nodes.inlineToolbar.buttons;
+
+ buttons.classList.remove('opened');
+
+ editor.toolbar.inline.buttonsOpened = false;
+ };
+
+ /** Open buttons defined action if exist */
+ inline.showActions = function () {
+ var action = editor.nodes.inlineToolbar.actions;
+
+ action.classList.add('opened');
+
+ editor.toolbar.inline.actionsOpened = true;
+ };
+
+ /** Close actions block */
+ inline.closeAction = function () {
+ var action = editor.nodes.inlineToolbar.actions;
+
+ action.innerHTML = '';
+ action.classList.remove('opened');
+ editor.toolbar.inline.actionsOpened = false;
+ };
+
+
+ /**
+ * Callback for keydowns in inline toolbar "Insert link..." input
+ */
+ let inlineToolbarAnchorInputKeydown_ = function (event) {
+ if (event.keyCode != editor.core.keys.ENTER) {
+ return;
+ }
+
+ let editable = editor.content.currentNode,
+ storedSelection = editor.toolbar.inline.storedSelection;
+
+ editor.toolbar.inline.restoreSelection(editable, storedSelection);
+ editor.toolbar.inline.setAnchor(this.value);
+
+ /**
+ * Preventing events that will be able to happen
+ */
+ event.preventDefault();
+ event.stopImmediatePropagation();
+
+ editor.toolbar.inline.clearRange();
+ };
+
+ /** Action for link creation or for setting anchor */
+ inline.createLinkAction = function (event) {
+ var isActive = this.isLinkActive();
+
+ var editable = editor.content.currentNode,
+ storedSelection = editor.toolbar.inline.saveSelection(editable);
+
+ /** Save globally selection */
+ editor.toolbar.inline.storedSelection = storedSelection;
+
+ if (isActive) {
+ /**
+ * Changing stored selection. if we want to remove anchor from word
+ * we should remove anchor from whole word, not only selected part.
+ * The solution is than we get the length of current link
+ * Change start position to - end of selection minus length of anchor
+ */
+ editor.toolbar.inline.restoreSelection(editable, storedSelection);
+
+ editor.toolbar.inline.defaultToolAction('unlink');
+ } else {
+ /** Create input and close buttons */
+ var action = editor.draw.inputForLink();
+
+ editor.nodes.inlineToolbar.actions.appendChild(action);
+
+ editor.toolbar.inline.closeButtons();
+ editor.toolbar.inline.showActions();
+
+ /**
+ * focus to input
+ * Solution: https://developer.mozilla.org/ru/docs/Web/API/HTMLElement/focus
+ * Prevents event after showing input and when we need to focus an input which is in unexisted form
+ */
+ action.focus();
+ event.preventDefault();
+
+ /** Callback to link action */
+ editor.listeners.add(action, 'keydown', inlineToolbarAnchorInputKeydown_, false);
+ }
+ };
+
+ inline.isLinkActive = function () {
+ var isActive = false;
+
+ editor.nodes.inlineToolbar.buttons.childNodes.forEach(function (tool) {
+ var dataType = tool.dataset.type;
+
+ if (dataType == 'link' && tool.classList.contains('hightlighted')) {
+ isActive = true;
+ }
+ });
+
+ return isActive;
+ };
+
+ /** default action behavior of tool */
+ inline.defaultToolAction = function (type) {
+ document.execCommand(type, false, null);
+ };
+
+ /**
+ * @private
+ *
+ * Sets URL
+ *
+ * @param {String} url - URL
+ */
+ inline.setAnchor = function (url) {
+ document.execCommand('createLink', false, url);
+
+ /** Close after URL inserting */
+ editor.toolbar.inline.closeAction();
+ };
+
+ /**
+ * @private
+ *
+ * Saves selection
+ */
+ inline.saveSelection = function (containerEl) {
+ var range = window.getSelection().getRangeAt(0),
+ preSelectionRange = range.cloneRange(),
+ start;
+
+ preSelectionRange.selectNodeContents(containerEl);
+ preSelectionRange.setEnd(range.startContainer, range.startOffset);
+
+ start = preSelectionRange.toString().length;
+
+ return {
+ start: start,
+ end: start + range.toString().length
+ };
+ };
+
+ /**
+ * @private
+ *
+ * Sets to previous selection (Range)
+ *
+ * @param {Element} containerEl - editable element where we restore range
+ * @param {Object} savedSel - range basic information to restore
+ */
+ inline.restoreSelection = function (containerEl, savedSel) {
+ var range = document.createRange(),
+ charIndex = 0;
+
+ range.setStart(containerEl, 0);
+ range.collapse(true);
+
+ var nodeStack = [ containerEl ],
+ node,
+ foundStart = false,
+ stop = false,
+ nextCharIndex;
+
+ while (!stop && (node = nodeStack.pop())) {
+ if (node.nodeType == 3) {
+ nextCharIndex = charIndex + node.length;
+
+ if (!foundStart && savedSel.start >= charIndex && savedSel.start <= nextCharIndex) {
+ range.setStart(node, savedSel.start - charIndex);
+ foundStart = true;
+ }
+ if (foundStart && savedSel.end >= charIndex && savedSel.end <= nextCharIndex) {
+ range.setEnd(node, savedSel.end - charIndex);
+ stop = true;
+ }
+ charIndex = nextCharIndex;
+ } else {
+ var i = node.childNodes.length;
+
+ while (i--) {
+ nodeStack.push(node.childNodes[i]);
+ }
+ }
+ }
+
+ var sel = window.getSelection();
+
+ sel.removeAllRanges();
+ sel.addRange(range);
+ };
+
+ /**
+ * @private
+ *
+ * Removes all ranges from window selection
+ */
+ inline.clearRange = function () {
+ var selection = window.getSelection();
+
+ selection.removeAllRanges();
+ };
+
+ /**
+ * @private
+ *
+ * sets or removes hightlight
+ */
+ inline.hightlight = function (tool) {
+ var dataType = tool.dataset.type;
+
+ if (document.queryCommandState(dataType)) {
+ editor.toolbar.inline.setButtonHighlighted(tool);
+ } else {
+ editor.toolbar.inline.removeButtonsHighLight(tool);
+ }
+
+ /**
+ *
+ * hightlight for anchors
+ */
+ var selection = window.getSelection(),
+ tag = selection.anchorNode.parentNode;
+
+ if (tag.tagName == 'A' && dataType == 'link') {
+ editor.toolbar.inline.setButtonHighlighted(tool);
+ }
+ };
+
+ /**
+ * @private
+ *
+ * Mark button if text is already executed
+ */
+ inline.setButtonHighlighted = function (button) {
+ button.classList.add('hightlighted');
+
+ /** At link tool we also change icon */
+ if (button.dataset.type == 'link') {
+ var icon = button.childNodes[0];
+
+ icon.classList.remove('ce-icon-link');
+ icon.classList.add('ce-icon-unlink');
+ }
+ };
+
+ /**
+ * @private
+ *
+ * Removes hightlight
+ */
+ inline.removeButtonsHighLight = function (button) {
+ button.classList.remove('hightlighted');
+
+ /** At link tool we also change icon */
+ if (button.dataset.type == 'link') {
+ var icon = button.childNodes[0];
+
+ icon.classList.remove('ce-icon-unlink');
+ icon.classList.add('ce-icon-link');
+ }
+ };
+
+
+ return inline;
+})({});
\ No newline at end of file
diff --git a/src/components/modules/toolbar/settings.js b/src/components/modules/toolbar/settings.js
new file mode 100644
index 00000000..34cf423a
--- /dev/null
+++ b/src/components/modules/toolbar/settings.js
@@ -0,0 +1,140 @@
+/**
+ * Toolbar settings
+ *
+ * @version 1.0.5
+ */
+
+module.exports = (function (settings) {
+ let editor = codex.editor;
+
+ settings.opened = false;
+
+ settings.setting = null;
+ settings.actions = null;
+
+ /**
+ * Append and open settings
+ */
+ settings.open = function (toolType) {
+ /**
+ * Append settings content
+ * It's stored in tool.settings
+ */
+ if ( !editor.tools[toolType] || !editor.tools[toolType].renderSettings ) {
+ return;
+ }
+
+ /**
+ * Draw settings block
+ */
+ var settingsBlock = editor.tools[toolType].renderSettings();
+
+ editor.nodes.pluginSettings.appendChild(settingsBlock);
+
+
+ /** Open settings block */
+ editor.nodes.blockSettings.classList.add('opened');
+ this.opened = true;
+ };
+
+ /**
+ * Close and clear settings
+ */
+ settings.close = function () {
+ editor.nodes.blockSettings.classList.remove('opened');
+ editor.nodes.pluginSettings.innerHTML = '';
+
+ this.opened = false;
+ };
+
+ /**
+ * @param {string} toolType - plugin type
+ */
+ settings.toggle = function ( toolType ) {
+ if ( !this.opened ) {
+ this.open(toolType);
+ } else {
+ this.close();
+ }
+ };
+
+ /**
+ * Here we will draw buttons and add listeners to components
+ */
+ settings.makeRemoveBlockButton = function () {
+ var removeBlockWrapper = editor.draw.node('SPAN', 'ce-toolbar__remove-btn', {}),
+ settingButton = editor.draw.node('SPAN', 'ce-toolbar__remove-setting', { innerHTML : '' }),
+ actionWrapper = editor.draw.node('DIV', 'ce-toolbar__remove-confirmation', {}),
+ confirmAction = editor.draw.node('DIV', 'ce-toolbar__remove-confirm', { textContent : 'Удалить блок' }),
+ cancelAction = editor.draw.node('DIV', 'ce-toolbar__remove-cancel', { textContent : 'Отмена' });
+
+ editor.listeners.add(settingButton, 'click', editor.toolbar.settings.removeButtonClicked, false);
+
+ editor.listeners.add(confirmAction, 'click', editor.toolbar.settings.confirmRemovingRequest, false);
+
+ editor.listeners.add(cancelAction, 'click', editor.toolbar.settings.cancelRemovingRequest, false);
+
+ actionWrapper.appendChild(confirmAction);
+ actionWrapper.appendChild(cancelAction);
+
+ removeBlockWrapper.appendChild(settingButton);
+ removeBlockWrapper.appendChild(actionWrapper);
+
+ /** Save setting */
+ editor.toolbar.settings.setting = settingButton;
+ editor.toolbar.settings.actions = actionWrapper;
+
+ return removeBlockWrapper;
+ };
+
+ settings.removeButtonClicked = function () {
+ var action = editor.toolbar.settings.actions;
+
+ if (action.classList.contains('opened')) {
+ editor.toolbar.settings.hideRemoveActions();
+ } else {
+ editor.toolbar.settings.showRemoveActions();
+ }
+
+ editor.toolbar.toolbox.close();
+ editor.toolbar.settings.close();
+ };
+
+ settings.cancelRemovingRequest = function () {
+ editor.toolbar.settings.actions.classList.remove('opened');
+ };
+
+ settings.confirmRemovingRequest = function () {
+ var currentBlock = editor.content.currentNode,
+ firstLevelBlocksCount;
+
+ currentBlock.remove();
+
+ firstLevelBlocksCount = editor.nodes.redactor.childNodes.length;
+
+ /**
+ * If all blocks are removed
+ */
+ if (firstLevelBlocksCount === 0) {
+ /** update currentNode variable */
+ editor.content.currentNode = null;
+
+ /** Inserting new empty initial block */
+ editor.ui.addInitialBlock();
+ }
+
+ editor.ui.saveInputs();
+
+ editor.toolbar.close();
+ };
+
+ settings.showRemoveActions = function () {
+ editor.toolbar.settings.actions.classList.add('opened');
+ };
+
+ settings.hideRemoveActions = function () {
+ editor.toolbar.settings.actions.classList.remove('opened');
+ };
+
+ return settings;
+})({});
diff --git a/src/components/modules/toolbar/toolbar.js b/src/components/modules/toolbar/toolbar.js
new file mode 100644
index 00000000..1ce427f3
--- /dev/null
+++ b/src/components/modules/toolbar/toolbar.js
@@ -0,0 +1,105 @@
+/**
+ * Codex Editor toolbar module
+ *
+ * Contains:
+ * - Inline toolbox
+ * - Toolbox within plus button
+ * - Settings section
+ *
+ * @author Codex Team
+ * @version 1.0
+ */
+
+module.exports = (function (toolbar) {
+ let editor = codex.editor;
+
+ toolbar.settings = require('./settings');
+ toolbar.inline = require('./inline');
+ toolbar.toolbox = require('./toolbox');
+
+ /**
+ * Margin between focused node and toolbar
+ */
+ toolbar.defaultToolbarHeight = 49;
+
+ toolbar.defaultOffset = 34;
+
+ toolbar.opened = false;
+
+ toolbar.current = null;
+
+ /**
+ * @protected
+ */
+ toolbar.open = function () {
+ if (editor.hideToolbar) {
+ return;
+ }
+
+ let toolType = editor.content.currentNode.dataset.tool;
+
+ if (!editor.tools[toolType] || !editor.tools[toolType].renderSettings ) {
+ editor.nodes.showSettingsButton.classList.add('hide');
+ } else {
+ editor.nodes.showSettingsButton.classList.remove('hide');
+ }
+
+ editor.nodes.toolbar.classList.add('opened');
+ this.opened = true;
+ };
+
+ /**
+ * @protected
+ */
+ toolbar.close = function () {
+ editor.nodes.toolbar.classList.remove('opened');
+
+ toolbar.opened = false;
+ toolbar.current = null;
+
+ for (var button in editor.nodes.toolbarButtons) {
+ editor.nodes.toolbarButtons[button].classList.remove('selected');
+ }
+
+ /** Close toolbox when toolbar is not displayed */
+ editor.toolbar.toolbox.close();
+ editor.toolbar.settings.close();
+ };
+
+ toolbar.toggle = function () {
+ if ( !this.opened ) {
+ this.open();
+ } else {
+ this.close();
+ }
+ };
+
+ toolbar.hidePlusButton = function () {
+ editor.nodes.plusButton.classList.add('hide');
+ };
+
+ toolbar.showPlusButton = function () {
+ editor.nodes.plusButton.classList.remove('hide');
+ };
+
+ /**
+ * Moving toolbar to the specified node
+ */
+ toolbar.move = function () {
+ /** Close Toolbox when we move toolbar */
+ editor.toolbar.toolbox.close();
+
+ if (!editor.content.currentNode) {
+ return;
+ }
+
+ var newYCoordinate = editor.content.currentNode.offsetTop - (editor.toolbar.defaultToolbarHeight / 2) + editor.toolbar.defaultOffset;
+
+ editor.nodes.toolbar.style.transform = `translate3D(0, ${Math.floor(newYCoordinate)}px, 0)`;
+
+ /** Close trash actions */
+ editor.toolbar.settings.hideRemoveActions();
+ };
+
+ return toolbar;
+})({});
diff --git a/src/components/modules/toolbar/toolbox.js b/src/components/modules/toolbar/toolbox.js
new file mode 100644
index 00000000..16f4f6d3
--- /dev/null
+++ b/src/components/modules/toolbar/toolbox.js
@@ -0,0 +1,159 @@
+/**
+ * Codex Editor toolbox
+ *
+ * All tools be able to appended here
+ *
+ * @author Codex Team
+ * @version 1.0
+ */
+
+module.exports = (function (toolbox) {
+ let editor = codex.editor;
+
+ toolbox.opened = false;
+ toolbox.openedOnBlock = null;
+
+ /** Shows toolbox */
+ toolbox.open = function () {
+ /** Close setting if toolbox is opened */
+ if (editor.toolbar.settings.opened) {
+ editor.toolbar.settings.close();
+ }
+
+ /** Add 'toolbar-opened' class for current block **/
+ toolbox.openedOnBlock = editor.content.currentNode;
+ toolbox.openedOnBlock.classList.add('toolbar-opened');
+
+ /** display toolbox */
+ editor.nodes.toolbox.classList.add('opened');
+
+ /** Animate plus button */
+ editor.nodes.plusButton.classList.add('clicked');
+
+ /** toolbox state */
+ editor.toolbar.toolbox.opened = true;
+ };
+
+ /** Closes toolbox */
+ toolbox.close = function () {
+ /** Remove 'toolbar-opened' class from current block **/
+ if (toolbox.openedOnBlock) toolbox.openedOnBlock.classList.remove('toolbar-opened');
+ toolbox.openedOnBlock = null;
+
+ /** Makes toolbox disappear */
+ editor.nodes.toolbox.classList.remove('opened');
+
+ /** Rotate plus button */
+ editor.nodes.plusButton.classList.remove('clicked');
+
+ /** toolbox state */
+ editor.toolbar.toolbox.opened = false;
+
+ editor.toolbar.current = null;
+ };
+
+ toolbox.leaf = function () {
+ let currentTool = editor.toolbar.current,
+ tools = Object.keys(editor.tools),
+ barButtons = editor.nodes.toolbarButtons,
+ nextToolIndex = 0,
+ toolToSelect,
+ visibleTool,
+ tool;
+
+ if ( !currentTool ) {
+ /** Get first tool from object*/
+ for(tool in editor.tools) {
+ if (editor.tools[tool].displayInToolbox) {
+ break;
+ }
+
+ nextToolIndex ++;
+ }
+ } else {
+ nextToolIndex = (tools.indexOf(currentTool) + 1) % tools.length;
+ visibleTool = tools[nextToolIndex];
+
+ while (!editor.tools[visibleTool].displayInToolbox) {
+ nextToolIndex = (nextToolIndex + 1) % tools.length;
+ visibleTool = tools[nextToolIndex];
+ }
+ }
+
+ toolToSelect = tools[nextToolIndex];
+
+ for ( var button in barButtons ) {
+ barButtons[button].classList.remove('selected');
+ }
+
+ barButtons[toolToSelect].classList.add('selected');
+ editor.toolbar.current = toolToSelect;
+ };
+
+ /**
+ * Transforming selected node type into selected toolbar element type
+ * @param {event} event
+ */
+ toolbox.toolClicked = function (event) {
+ /**
+ * UNREPLACEBLE_TOOLS this types of tools are forbidden to replace even they are empty
+ */
+ var UNREPLACEBLE_TOOLS = ['image', 'link', 'list', 'instagram', 'twitter', 'embed'],
+ tool = editor.tools[editor.toolbar.current],
+ workingNode = editor.content.currentNode,
+ currentInputIndex = editor.caret.inputIndex,
+ newBlockContent,
+ appendCallback,
+ blockData;
+
+ /** Make block from plugin */
+ newBlockContent = tool.render();
+
+ /** information about block */
+ blockData = {
+ block : newBlockContent,
+ type : tool.type,
+ stretched : false
+ };
+
+ if (
+ workingNode &&
+ UNREPLACEBLE_TOOLS.indexOf(workingNode.dataset.tool) === -1 &&
+ workingNode.textContent.trim() === ''
+ ) {
+ /** Replace current block */
+ editor.content.switchBlock(workingNode, newBlockContent, tool.type);
+ } else {
+ /** Insert new Block from plugin */
+ editor.content.insertBlock(blockData);
+
+ /** increase input index */
+ currentInputIndex++;
+ }
+
+ /** Fire tool append callback */
+ appendCallback = tool.appendCallback;
+
+ if (appendCallback && typeof appendCallback == 'function') {
+ appendCallback.call(event);
+ }
+
+ window.setTimeout(function () {
+ /** Set caret to current block */
+ editor.caret.setToBlock(currentInputIndex);
+ }, 10);
+
+
+ /**
+ * Changing current Node
+ */
+ editor.content.workingNodeChanged();
+
+ /**
+ * Move toolbar when node is changed
+ */
+ editor.toolbar.move();
+ };
+
+ return toolbox;
+})({});
\ No newline at end of file
diff --git a/src/components/modules/tools.js b/src/components/modules/tools.js
new file mode 100644
index 00000000..6c9b3e26
--- /dev/null
+++ b/src/components/modules/tools.js
@@ -0,0 +1,269 @@
+/**
+ * @module Codex Editor Tools Submodule
+ *
+ * Creates Instances from Plugins and binds external config to the instances
+ */
+
+/**
+ * Each Tool must contain the following important objects:
+ *
+ * @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
+ * @property {Boolean|String[]} inlineToolbar - Pass `true` to enable the Inline Toolbar with all Tools, all pass an array with specified Tools list |
+ * @property render @todo add description
+ * @property save @todo add description
+ * @property settings @todo add description
+ * @property validate - method that validates output data before saving
+ */
+
+/**
+ * @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 {
+ /**
+ * Returns available Tools
+ * @return {Tool[]}
+ */
+ get available() {
+ return this.toolsAvailable;
+ }
+
+ /**
+ * Returns unavailable Tools
+ * @return {Tool[]}
+ */
+ get unavailable() {
+ return this.toolsUnavailable;
+ }
+
+ /**
+ * Return Tools for the Inline Toolbar
+ * @return {Array} - array of Inline Tool's classes
+ */
+ get inline() {
+ return Object.values(this.available).filter( tool => {
+ if (!tool[this.apiSettings.IS_INLINE]) {
+ return false;
+ }
+
+ /**
+ * Some Tools validation
+ */
+ const inlineToolRequiredMethods = ['render', 'surround', 'checkState'];
+ const notImplementedMethods = inlineToolRequiredMethods.filter( method => !new tool()[method] );
+
+ if (notImplementedMethods.length) {
+ _.log(`Incorrect Inline Tool: ${tool.name}. Some of required methods is not implemented %o`, 'warn', notImplementedMethods);
+ return false;
+ }
+
+ return true;
+ });
+ }
+
+ /**
+ * Return editor block tools
+ */
+ get blockTools() {
+ // eslint-disable-next-line no-unused-vars
+ const tools = Object.entries(this.available).filter( ([name, tool]) => {
+ if (tool[this.apiSettings.IS_INLINE]) {
+ return false;
+ }
+
+ return true;
+ });
+
+ const result = {};
+
+ tools.forEach(([name, tool]) => result[name] = tool);
+
+ return result;
+ }
+
+ /**
+ * Constant for available Tools Settings
+ * @return {object}
+ */
+ get apiSettings() {
+ return {
+ IS_INLINE: 'isInline',
+ TOOLBAR_ICON_CLASS: 'iconClassName',
+ IS_DISPLAYED_IN_TOOLBOX: 'displayInToolbox',
+ IS_ENABLED_LINE_BREAKS: 'enableLineBreaks',
+ IS_IRREPLACEBLE_TOOL: 'irreplaceable',
+ IS_ENABLED_INLINE_TOOLBAR: 'inlineToolbar',
+ IS_PASTE_DISALLOWED: 'disallowPaste'
+ };
+ }
+
+ /**
+ * Static getter for default Tool config fields
+ * @return {ToolConfig}
+ */
+ get defaultConfig() {
+ return {
+ [this.apiSettings.TOOLBAR_ICON_CLASS] : false,
+ [this.apiSettings.IS_DISPLAYED_IN_TOOLBOX] : false,
+ [this.apiSettings.IS_ENABLED_LINE_BREAKS] : false,
+ [this.apiSettings.IS_IRREPLACEBLE_TOOL] : false,
+ [this.apiSettings.IS_ENABLED_INLINE_TOOLBAR]: false,
+ };
+ }
+
+ /**
+ * @constructor
+ *
+ * @param {EditorConfig} config
+ */
+ constructor({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 {Promise}
+ */
+ prepare() {
+ if (!this.config.hasOwnProperty('tools')) {
+ return Promise.reject("Can't start without tools");
+ }
+
+ for(let toolName in this.config.tools) {
+ this.toolClasses[toolName] = this.config.tools[toolName];
+ }
+
+ /**
+ * getting classes that has prepare method
+ */
+ let sequenceData = this.getListOfPrepareFunctions();
+
+ /**
+ * if sequence data contains nothing then resolve current chain and run other module prepare
+ */
+ if (sequenceData.length === 0) {
+ return Promise.resolve();
+ }
+
+ /**
+ * to see how it works {@link Util#sequence}
+ */
+ return _.sequence(sequenceData, (data) => {
+ this.success(data);
+ }, (data) => {
+ this.fallback(data);
+ });
+ }
+
+ /**
+ * Binds prepare function of plugins with user or default config
+ * @return {Array} list of functions that needs to be fired sequentially
+ */
+ getListOfPrepareFunctions() {
+ let toolPreparationList = [];
+
+ for(let toolName in this.toolClasses) {
+ let toolClass = this.toolClasses[toolName];
+
+ if (typeof toolClass.prepare === 'function') {
+ toolPreparationList.push({
+ function : toolClass.prepare,
+ data : {
+ toolName
+ }
+ });
+ } else {
+ /**
+ * If Tool hasn't a prepare method, mark it as available
+ */
+ this.toolsAvailable[toolName] = toolClass;
+ }
+ }
+
+ return toolPreparationList;
+ }
+
+ /**
+ * @param {ChainData.data} data - append tool to available list
+ */
+ success(data) {
+ this.toolsAvailable[data.toolName] = this.toolClasses[data.toolName];
+ }
+
+ /**
+ * @param {ChainData.data} data - append tool to unavailable list
+ */
+ fallback(data) {
+ this.toolsUnavailable[data.toolName] = this.toolClasses[data.toolName];
+ }
+
+ /**
+ * Return tool`a instance
+ *
+ * @param {String} tool — tool name
+ * @param {Object} data — initial data
+ *
+ * @todo throw exceptions if tool doesnt exist
+ *
+ */
+ construct(tool, data) {
+ let plugin = this.toolClasses[tool],
+ config = this.config.toolsConfig[tool];
+
+ let instance = new plugin(data, config || {});
+
+ return instance;
+ }
+
+ /**
+ * 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];
+ }
+}
diff --git a/src/components/modules/ui.js b/src/components/modules/ui.js
new file mode 100644
index 00000000..bd12bfcf
--- /dev/null
+++ b/src/components/modules/ui.js
@@ -0,0 +1,537 @@
+/**
+ * Module UI
+ *
+ * @type {UI}
+ */
+
+/**
+ * Prebuilded sprite of SVG icons
+ */
+import sprite from '../../../build/sprite.svg';
+import Selection from '../selection';
+
+/**
+ * @class
+ *
+ * @classdesc Makes CodeX Editor UI:
+ *
+ *
+ *
+ *
+ *
+ *
+ * @typedef {UI} UI
+ * @property {EditorConfig} config - editor configuration {@link CodexEditor#configuration}
+ * @property {Object} Editor - available editor modules {@link CodexEditor#moduleInstances}
+ * @property {Object} nodes -
+ * @property {Element} nodes.holder - element where we need to append redactor
+ * @property {Element} nodes.wrapper -
+ * @property {Element} nodes.redactor -
+ */
+export default class UI extends Module {
+ /**
+ * @constructor
+ *
+ * @param {EditorConfig} config
+ */
+ constructor({config}) {
+ super({config});
+
+ this.nodes = {
+ holder: null,
+ wrapper: null,
+ redactor: null
+ };
+ }
+
+ /**
+ * Making main interface
+ */
+ async prepare() {
+ await this.make();
+
+ /**
+ * Append SVG sprite
+ */
+ await this.appendSVGSprite();
+
+ /**
+ * Make toolbar
+ */
+ await this.Editor.Toolbar.make();
+
+ /**
+ * Make the Inline toolbar
+ */
+ await this.Editor.InlineToolbar.make();
+
+ /**
+ * Load and append CSS
+ */
+ await this.loadStyles();
+
+ /**
+ * Bind events for the UI elements
+ */
+ await this.bindEvents();
+ }
+
+ /**
+ * 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}
+ */
+ async make() {
+ /**
+ * Element where we need to append CodeX Editor
+ * @type {Element}
+ */
+ this.nodes.holder = document.getElementById(this.config.holderId);
+
+ if (!this.nodes.holder) {
+ throw Error("Holder wasn't found by ID: #" + this.config.holderId);
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * Appends CSS
+ */
+ loadStyles() {
+ /**
+ * Load CSS
+ */
+ let styles = require('../../styles/main.css');
+
+ /**
+ * Make tag
+ */
+ let tag = $.make('style', null, {
+ textContent: styles.toString()
+ });
+
+ /**
+ * Append styles
+ */
+ $.append(document.head, tag);
+ }
+
+ /**
+ * Bind events on the CodeX Editor interface
+ */
+ bindEvents() {
+ this.Editor.Listeners.on(this.nodes.redactor, 'click', event => this.redactorClicked(event), false );
+ this.Editor.Listeners.on(document, 'keydown', event => this.documentKeydown(event), true );
+ this.Editor.Listeners.on(document, 'click', event => this.documentClicked(event), false );
+ }
+
+ /**
+ * All keydowns on document
+ * @param event
+ */
+ documentKeydown(event) {
+ switch (event.keyCode) {
+ case _.keyCodes.ENTER:
+ this.enterPressed(event);
+ break;
+
+ default:
+ this.defaultBehaviour(event);
+ break;
+ }
+ }
+
+ /**
+ * Ignore all other document's keydown events
+ * @param {KeyboardEvent} event
+ */
+ defaultBehaviour(event) {
+ const keyDownOnEditor = event.target.closest(`.${this.CSS.editorWrapper}`);
+
+ /**
+ * Ignore keydowns on document
+ * clear pointer and close toolbar
+ */
+ if (!keyDownOnEditor) {
+ /**
+ * Remove all highlights and remove caret
+ */
+ this.Editor.BlockManager.dropPointer();
+
+ /**
+ * Close Toolbar
+ */
+ this.Editor.Toolbar.close();
+ }
+ }
+
+ /**
+ * Enter pressed on document
+ * @param event
+ */
+ enterPressed(event) {
+ let hasPointerToBlock = this.Editor.BlockManager.currentBlockIndex >= 0;
+
+ /**
+ * If Selection is out of Editor and document has some selection
+ */
+ if (!Selection.isAtEditor && Selection.anchorNode) {
+ return;
+ }
+
+ /**
+ * If there is no selection (caret is not placed) and BlockManager points some to Block
+ */
+ if (hasPointerToBlock && !Selection.anchorNode) {
+ /**
+ * Insert initial typed Block
+ */
+ this.Editor.BlockManager.insert();
+ this.Editor.BlockManager.highlightCurrentNode();
+
+ /**
+ * Move toolbar and show plus button because new Block is empty
+ */
+ this.Editor.Toolbar.move();
+ this.Editor.Toolbar.plusButton.show();
+ }
+ }
+
+ /**
+ * All clicks on document
+ * @param {MouseEvent} event - Click
+ */
+ documentClicked(event) {
+ /**
+ * Close Inline Toolbar when nothing selected
+ * Do not fire check on clicks at the Inline Toolbar buttons
+ */
+ const clickedOnInlineToolbarButton = event.target.closest(`.${this.Editor.InlineToolbar.CSS.inlineToolbar}`);
+ const clickedInsideofEditor = event.target.closest(`.${this.CSS.editorWrapper}`);
+
+ /** Clear highlightings and pointer on BlockManager */
+ if (!clickedInsideofEditor) {
+ this.Editor.BlockManager.dropPointer();
+ this.Editor.Toolbar.close();
+ }
+
+ if (!clickedOnInlineToolbarButton) {
+ this.Editor.InlineToolbar.handleShowingEvent(event);
+ }
+ }
+
+ /**
+ * 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 {
+ /**
+ * Renew Current Block
+ */
+ this.Editor.BlockManager.setCurrentBlockByChildNode(clickedNode);
+
+ /**
+ * Highlight Current Node
+ */
+ this.Editor.BlockManager.highlightCurrentNode();
+ } catch (e) {
+ /**
+ * If clicked outside first-level Blocks, set Caret to the last empty Block
+ */
+ this.Editor.Caret.setToTheLastBlock();
+ }
+
+ /**
+ * Move toolbar and open
+ */
+ this.Editor.Toolbar.move();
+ this.Editor.Toolbar.open();
+
+ /**
+ * 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();
+ }
+ }
+
+ /**
+ * Append prebuilded sprite with SVG icons
+ */
+ appendSVGSprite() {
+ let spriteHolder = $.make('div');
+
+ spriteHolder.innerHTML = sprite;
+
+ $.append(this.nodes.wrapper, spriteHolder);
+ }
+}
+
+// /**
+// * Codex Editor UI module
+// *
+// * @author Codex Team
+// * @version 1.2.0
+// */
+//
+// module.exports = (function (ui) {
+//
+// let editor = codex.editor;
+//
+// /**
+// * Basic editor classnames
+// */
+// ui.prepare = function () {
+//
+
+//
+// };
+//
+// /** Draw notifications holder */
+// var makeNotificationHolder_ = function () {
+//
+// /** Append block with notifications to the document */
+// editor.nodes.notifications = editor.notifications.createHolder();
+//
+// };
+//
+//
+// var addInlineToolbarTools_ = function () {
+//
+// var tools = {
+//
+// bold: {
+// icon : 'ce-icon-bold',
+// command : 'bold'
+// },
+//
+// italic: {
+// icon : 'ce-icon-italic',
+// command : 'italic'
+// },
+//
+// link: {
+// icon : 'ce-icon-link',
+// command : 'createLink'
+// }
+// };
+//
+// var toolButton,
+// tool;
+//
+// for(var name in tools) {
+//
+// tool = tools[name];
+//
+// toolButton = editor.draw.toolbarButtonInline(name, tool.icon);
+//
+// editor.nodes.inlineToolbar.buttons.appendChild(toolButton);
+// /**
+// * Add callbacks to this buttons
+// */
+// editor.ui.setInlineToolbarButtonBehaviour(toolButton, tool.command);
+//
+// }
+//
+// };
+//
+// /**
+// * @private
+// * Bind editor UI events
+// */
+// var bindEvents_ = function () {
+//
+// editor.core.log('ui.bindEvents fired', 'info');
+//
+// // window.addEventListener('error', function (errorMsg, url, lineNumber) {
+// // editor.notifications.errorThrown(errorMsg, event);
+// // }, false );
+//
+// /** All keydowns on Document */
+// editor.listeners.add(document, 'keydown', editor.callback.globalKeydown, false);
+//
+// /** All keydowns on Redactor zone */
+// editor.listeners.add(editor.nodes.redactor, 'keydown', editor.callback.redactorKeyDown, false);
+//
+// /** All keydowns on Document */
+// editor.listeners.add(document, 'keyup', editor.callback.globalKeyup, false );
+//
+// /**
+// * Mouse click to radactor
+// */
+// editor.listeners.add(editor.nodes.redactor, 'click', editor.callback.redactorClicked, false );
+//
+// /**
+// * Clicks to the Plus button
+// */
+// editor.listeners.add(editor.nodes.plusButton, 'click', editor.callback.plusButtonClicked, false);
+//
+// /**
+// * Clicks to SETTINGS button in toolbar
+// */
+// editor.listeners.add(editor.nodes.showSettingsButton, 'click', editor.callback.showSettingsButtonClicked, false );
+//
+// /** Bind click listeners on toolbar buttons */
+// for (var button in editor.nodes.toolbarButtons) {
+//
+// editor.listeners.add(editor.nodes.toolbarButtons[button], 'click', editor.callback.toolbarButtonClicked, false);
+//
+// }
+//
+// };
+//
+// ui.addBlockHandlers = function (block) {
+//
+// if (!block) return;
+//
+// /**
+// * Block keydowns
+// */
+// editor.listeners.add(block, 'keydown', editor.callback.blockKeydown, false);
+//
+// /**
+// * Pasting content from another source
+// * We have two type of sanitization
+// * First - uses deep-first search algorithm to get sub nodes,
+// * sanitizes whole Block_content and replaces cleared nodes
+// * This method is deprecated
+// * Method is used in editor.callback.blockPaste(event)
+// *
+// * Secont - uses Mutation observer.
+// * Observer "observe" DOM changes and send changings to callback.
+// * Callback gets changed node, not whole Block_content.
+// * Inserted or changed node, which we've gotten have been cleared and replaced with diry node
+// *
+// * Method is used in editor.callback.blockPasteViaSanitize(event)
+// *
+// * @uses html-janitor
+// * @example editor.callback.blockPasteViaSanitize(event), the second method.
+// *
+// */
+// editor.listeners.add(block, 'paste', editor.paste.blockPasteCallback, false);
+//
+// /**
+// * Show inline toolbar for selected text
+// */
+// editor.listeners.add(block, 'mouseup', editor.toolbar.inline.show, false);
+// editor.listeners.add(block, 'keyup', editor.toolbar.inline.show, false);
+//
+// };
+//
+// /** getting all contenteditable elements */
+// ui.saveInputs = function () {
+//
+// var redactor = editor.nodes.redactor;
+//
+// editor.state.inputs = [];
+//
+// /** Save all inputs in global variable state */
+// var inputs = redactor.querySelectorAll('[contenteditable], input, textarea');
+//
+// Array.prototype.map.call(inputs, function (current) {
+//
+// if (!current.type || current.type == 'text' || current.type == 'textarea') {
+//
+// editor.state.inputs.push(current);
+//
+// }
+//
+// });
+//
+// };
+//
+// /**
+// * Adds first initial block on empty redactor
+// */
+// ui.addInitialBlock = function () {
+//
+// var initialBlockType = editor.settings.initialBlockPlugin,
+// initialBlock;
+//
+// if ( !editor.tools[initialBlockType] ) {
+//
+// editor.core.log('Plugin %o was not implemented and can\'t be used as initial block', 'warn', initialBlockType);
+// return;
+//
+// }
+//
+// initialBlock = editor.tools[initialBlockType].render();
+//
+// initialBlock.setAttribute('data-placeholder', editor.settings.placeholder);
+//
+// editor.content.insertBlock({
+// type : initialBlockType,
+// block : initialBlock
+// });
+//
+// editor.content.workingNodeChanged(initialBlock);
+//
+// };
+//
+// ui.setInlineToolbarButtonBehaviour = function (button, type) {
+//
+// editor.listeners.add(button, 'mousedown', function (event) {
+//
+// editor.toolbar.inline.toolClicked(event, type);
+//
+// }, false);
+//
+// };
+//
+// return ui;
+//
+// })({});
diff --git a/src/components/polyfills.js b/src/components/polyfills.js
new file mode 100644
index 00000000..2f317fe4
--- /dev/null
+++ b/src/components/polyfills.js
@@ -0,0 +1,20 @@
+/**
+ * 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;
+ };
diff --git a/src/components/selection.js b/src/components/selection.js
new file mode 100644
index 00000000..e992d422
--- /dev/null
+++ b/src/components/selection.js
@@ -0,0 +1,294 @@
+/**
+ * Working with selection
+ * @typedef {Selection} Selection
+ */
+export default class Selection {
+ /**
+ * @constructor
+ */
+ constructor() {
+ this.instance = null;
+ this.selection = null;
+
+ /**
+ * This property can store Selection's range for restoring later
+ * @type {Range|null}
+ */
+ this.savedSelectionRange = null;
+ }
+
+ /**
+ * Editor styles
+ * @return {{editorWrapper: string, editorZone: string}}
+ * @constructor
+ */
+ static get CSS() {
+ return {
+ editorWrapper : 'codex-editor',
+ editorZone : 'codex-editor__redactor',
+ };
+ }
+
+ /**
+ * Returns window Selection
+ * {@link https://developer.mozilla.org/ru/docs/Web/API/Window/getSelection}
+ * @return {Selection}
+ */
+ static get() {
+ return window.getSelection();
+ }
+
+ /**
+ * Returns selected anchor
+ * {@link https://developer.mozilla.org/ru/docs/Web/API/Selection/anchorNode}
+ * @return {Node|null}
+ */
+ static get anchorNode() {
+ const selection = window.getSelection();
+
+ return selection ? selection.anchorNode : null;
+ }
+
+ /**
+ * Returns selection offset according to the anchor node
+ * {@link https://developer.mozilla.org/ru/docs/Web/API/Selection/anchorOffset}
+ * @return {Number|null}
+ */
+ static get anchorOffset() {
+ const selection = window.getSelection();
+
+ return selection ? selection.anchorOffset : null;
+ }
+
+ /**
+ * Is current selection range collapsed
+ * @return {boolean|null}
+ */
+ static get isCollapsed() {
+ const selection = window.getSelection();
+
+ return selection ? selection.isCollapsed : null;
+ }
+
+ /**
+ * Check current selection if it is at Editor's zone
+ * @return {boolean}
+ */
+ static get isAtEditor() {
+ let selection = Selection.get(),
+ selectedNode,
+ editorZone = false;
+
+ /**
+ * Something selected on document
+ */
+ selectedNode = selection.anchorNode || selection.focusNode;
+
+ if (selectedNode && selectedNode.nodeType === Node.TEXT_NODE) {
+ selectedNode = selectedNode.parentNode;
+ }
+
+ if (selectedNode) {
+ editorZone = selectedNode.closest(`.${Selection.CSS.editorZone}`);
+ }
+
+ /**
+ * Selection is not out of Editor because Editor's wrapper was found
+ */
+ return editorZone && editorZone.nodeType === Node.ELEMENT_NODE;
+ }
+
+ /**
+ * Return first range
+ * @return {Range|null}
+ */
+ static get range() {
+ const selection = window.getSelection();
+
+ return selection && selection.rangeCount ? selection.getRangeAt(0) : null;
+ }
+
+ /**
+ * Calculates position and size of selected text
+ * @return {{x, y, width, height, top?, left?, bottom?, right?}}
+ */
+ static get rect() {
+ let sel = document.selection, range;
+ let rect = {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0
+ };
+
+ if (sel && sel.type !== 'Control') {
+ range = sel.createRange();
+ rect.x = range.boundingLeft;
+ rect.y = range.boundingTop;
+ rect.width = range.boundingWidth;
+ rect.height = range.boundingHeight;
+
+ return rect;
+ }
+
+ if (!window.getSelection) {
+ _.log('Method window.getSelection is not supported', 'warn');
+ return rect;
+ }
+
+ sel = window.getSelection();
+
+ if (!sel.rangeCount) {
+ _.log('Method Selection.rangeCount() is not supported', 'warn');
+ return rect;
+ }
+
+ range = sel.getRangeAt(0).cloneRange();
+
+ if (range.getBoundingClientRect) {
+ rect = range.getBoundingClientRect();
+ }
+ // Fall back to inserting a temporary element
+ if (rect.x === 0 && rect.y === 0) {
+ let span = document.createElement('span');
+
+ if (span.getBoundingClientRect) {
+ // Ensure span has dimensions and position by
+ // adding a zero-width space character
+ span.appendChild( document.createTextNode('\u200b') );
+ range.insertNode(span);
+ rect = span.getBoundingClientRect();
+
+ let spanParent = span.parentNode;
+
+ spanParent.removeChild(span);
+
+ // Glue any broken text nodes back together
+ spanParent.normalize();
+ }
+ }
+
+ return rect;
+ }
+
+ /**
+ * Returns selected text as String
+ * @returns {string}
+ */
+ static get text() {
+ return window.getSelection ? window.getSelection().toString() : '';
+ };
+
+ /**
+ * Save Selection's range
+ */
+ save() {
+ this.savedSelectionRange = Selection.range;
+ }
+
+ /**
+ * Restore saved Selection's range
+ */
+ restore() {
+ if (!this.savedSelectionRange) {
+ return;
+ }
+
+ const sel = window.getSelection();
+
+ sel.removeAllRanges();
+ sel.addRange(this.savedSelectionRange);
+ }
+
+ /**
+ * Clears saved selection
+ */
+ clearSaved() {
+ this.savedSelectionRange = null;
+ }
+
+ /**
+ * Looks ahead to find passed tag from current selection
+ *
+ * @param {String} tagName - tag to found
+ * @param {String} [className] - tag's class name
+ * @param {Number} [searchDepth] - count of tags that can be included. For better performance.
+ * @return {HTMLElement|null}
+ */
+ findParentTag(tagName, className, searchDepth = 10) {
+ let selection = window.getSelection(),
+ parentTag = null;
+
+ /**
+ * If selection is missing or no anchorNode or focusNode were found then return null
+ */
+ if (!selection || !selection.anchorNode || !selection.focusNode) {
+ return null;
+ }
+
+ /**
+ * Define Nodes for start and end of selection
+ */
+ let boundNodes = [
+ /** the Node in which the selection begins */
+ selection.anchorNode,
+ /** the Node in which the selection ends */
+ selection.focusNode
+ ];
+
+ /**
+ * For each selection parent Nodes we try to find target tag [with target class name]
+ * It would be saved in parentTag variable
+ */
+ boundNodes.forEach(parent => {
+ /** Reset tags limit */
+ let searchDepthIterable = searchDepth;
+
+ while (searchDepthIterable > 0 && parent.parentNode) {
+ /**
+ * Check tag's name
+ */
+ if (parent.tagName === tagName) {
+ /**
+ * Optional additional check for class-name matching
+ */
+ if (className && parent.classList && !parent.classList.contains(className)) {
+ continue;
+ }
+
+ /**
+ * If we have found required tag with class then save the result and go out from cycle
+ */
+ parentTag = parent;
+ break;
+ }
+
+ /**
+ * Target tag was not found. Go up to the parent and check it
+ */
+ parent = parent.parentNode;
+ searchDepthIterable--;
+ }
+ });
+
+ /**
+ * Return found tag or null
+ */
+ return parentTag;
+ }
+
+ /**
+ * Expands selection range to the passed parent node
+ *
+ * @param {HTMLElement} node
+ */
+ expandToTag(node) {
+ let selection = window.getSelection();
+
+ selection.removeAllRanges();
+ let range = document.createRange();
+
+ range.selectNodeContents(node);
+ selection.addRange(range);
+ }
+}
diff --git a/src/components/utils.js b/src/components/utils.js
new file mode 100644
index 00000000..ba700d91
--- /dev/null
+++ b/src/components/utils.js
@@ -0,0 +1,175 @@
+/**
+ * Codex Editor Util
+ */
+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) {
+ if (['time', 'timeEnd'].includes(type)) {
+ msg = `[codex-editor]: ${msg}`;
+ } else {
+ 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
+ }
+ }
+
+ /**
+ * Returns basic keycodes as constants
+ * @return {{}}
+ */
+ static get keyCodes() {
+ return {
+ 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
+ };
+ }
+
+ /**
+ * @typedef {Object} ChainData
+ * @property {Object} data - data that will be passed to the success or fallback
+ * @property {Function} function - function's that must be called asynchronically
+ */
+
+ /**
+ * Fires a promise sequence asyncronically
+ *
+ * @param {Object[]} chains - list or ChainData's
+ * @param {Function} success - success callback
+ * @param {Function} fallback - callback that fires in case of errors
+ *
+ * @return {Promise}
+ */
+ static sequence(chains, success = () => {}, fallback = () => {}) {
+ return new Promise(function (resolve) {
+ /**
+ * pluck each element from queue
+ * First, send resolved Promise as previous value
+ * Each plugins "prepare" method returns a Promise, that's why
+ * reduce current element will not be able to continue while can't get
+ * a resolved Promise
+ */
+ chains.reduce(function (previousValue, currentValue, iteration) {
+ return previousValue
+ .then(() => waitNextBlock(currentValue, success, fallback))
+ .then(() => {
+ // finished
+ if (iteration === chains.length - 1) {
+ resolve();
+ }
+ });
+ }, Promise.resolve());
+ });
+
+ /**
+ * Decorator
+ *
+ * @param {ChainData} chainData
+ *
+ * @param {Function} successCallback
+ * @param {Function} fallbackCallback
+ *
+ * @return {Promise}
+ */
+ function waitNextBlock(chainData, successCallback, fallbackCallback) {
+ return new Promise(function (resolve) {
+ chainData.function()
+ .then(() => {
+ successCallback(chainData.data || {});
+ })
+ .then(resolve)
+ .catch(function () {
+ fallbackCallback(chainData.data || {});
+
+ // anyway, go ahead even it falls
+ resolve();
+ });
+ });
+ }
+ }
+
+ /**
+ * Make array from array-like collection
+ *
+ * @param {*} collection
+ *
+ * @return {Array}
+ */
+ static array(collection) {
+ return Array.prototype.slice.call(collection);
+ }
+
+ /**
+ * Checks if object is empty
+ *
+ * @param {Object} object
+ * @return {boolean}
+ */
+ static isEmpty(object) {
+ return Object.keys(object).length === 0 && object.constructor === Object;
+ }
+
+ /**
+ * Check if passed object is a Promise
+ * @param {*} object - object to check
+ * @return {Boolean}
+ */
+ static isPromise(object) {
+ return Promise.resolve(object) === object;
+ }
+
+ /**
+ * Check if passed element is contenteditable
+ * @param element
+ * @return {boolean}
+ */
+ static isContentEditable(element) {
+ return element.contentEditable === 'true';
+ }
+
+ /**
+ * Delays method execution
+ *
+ * @param method
+ * @param timeout
+ */
+ static delay(method, timeout) {
+ return function () {
+ let context = this,
+ args = arguments;
+
+ window.setTimeout(() => method.apply(context, args), timeout);
+ };
+ }
+};
diff --git a/codex-editor.css b/src/styles/_legacy.css
similarity index 97%
rename from codex-editor.css
rename to src/styles/_legacy.css
index 1fb54f95..6b72d89e 100644
--- a/codex-editor.css
+++ b/src/styles/_legacy.css
@@ -1,24 +1,17 @@
-/**
-* CodeX Editor stylesheets
-* @author CodeX Team https://ifmo.su
-*
-* https://github.com/codex-team/codex.editor
-*/
-
-
-@import url('icons.css');
-
-
/**
* Editor wrapper
*/
.codex-editor{
position: relative;
-}
+ border: 1px solid #ccc;
+ padding: 10px;
- .codex-editor .hide {
+ .hide {
display: none;
}
+}
+
+
/**
* Working zone - redactor
@@ -27,6 +20,7 @@
position: relative;
padding-bottom: 120px;
min-height: 350px;
+ border: 1px dotted #ccc;
}
.ce-block__content a {
@@ -69,7 +63,6 @@
*/
.ce-toolbar__plus{
position: absolute;
- background-image: url('fonts/codex_editor/icon-plus.svg');
background-position: center center;
background-repeat: no-repeat;
text-align: center;
@@ -327,7 +320,6 @@
.ce-settings__anchor-hash {
display: inline-block;
- background: url('fonts/codex_editor/icon-hash.svg') no-repeat center center;
background-size: contain;
height: 11px;
width: 10px;
diff --git a/src/styles/animations.css b/src/styles/animations.css
new file mode 100644
index 00000000..935a9703
--- /dev/null
+++ b/src/styles/animations.css
@@ -0,0 +1,37 @@
+.wobble {
+ animation-name: wobble;
+ animation-duration: 400ms;
+}
+
+/**
+ * @author Nick Pettit - https://github.com/nickpettit/glide
+ */
+@keyframes wobble {
+ from {
+ transform: translate3d(0, 0, 0);
+ }
+
+ 15% {
+ transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -5deg);
+ }
+
+ 30% {
+ transform: translate3d(2%, 0, 0) rotate3d(0, 0, 1, 3deg);
+ }
+
+ 45% {
+ transform: translate3d(-3%, 0, 0) rotate3d(0, 0, 1, -3deg);
+ }
+
+ 60% {
+ transform: translate3d(2%, 0, 0) rotate3d(0, 0, 1, 2deg);
+ }
+
+ 75% {
+ transform: translate3d(-1%, 0, 0) rotate3d(0, 0, 1, -1deg);
+ }
+
+ to {
+ transform: translate3d(0, 0, 0);
+ }
+}
diff --git a/src/styles/block.css b/src/styles/block.css
new file mode 100644
index 00000000..c1628508
--- /dev/null
+++ b/src/styles/block.css
@@ -0,0 +1,15 @@
+.ce-block {
+ &:first-of-type {
+ margin-top: 0;
+ }
+
+ &--selected {
+ background-image: linear-gradient(17deg, rgba(243, 248, 255, 0.03) 63.45%, rgba(207, 214, 229, 0.27) 98%);
+ border-radius: 3px;
+ }
+
+ &__content {
+ max-width: var(--content-width);
+ margin: 0 auto;
+ }
+}
diff --git a/src/styles/inline-toolbar.css b/src/styles/inline-toolbar.css
new file mode 100644
index 00000000..3d9ff6af
--- /dev/null
+++ b/src/styles/inline-toolbar.css
@@ -0,0 +1,58 @@
+.ce-inline-toolbar {
+ @apply --overlay-pane;
+ padding: 6px;
+ transform: translateX(-50%);
+ display: none;
+ box-shadow: 0 6px 12px -6px rgba(131, 147, 173, 0.46),
+ 5px -12px 34px -13px rgba(97, 105, 134, 0.6),
+ 0 26px 52px 3px rgba(147, 165, 186, 0.24);
+
+ &--showed {
+ display: block;
+ }
+}
+
+.ce-inline-tool {
+ @apply --toolbar-button;
+ line-height: normal;
+
+ &--link {
+ .icon {
+ margin-top: -2px;
+ }
+
+ .icon--unlink {
+ display: none;
+ }
+ }
+
+ &--unlink {
+ .icon--link {
+ display: none;
+ }
+ .icon--unlink {
+ display: inline-block;
+ }
+ }
+
+ &-input {
+ background-color: var(--bg-light);
+ outline: none;
+ border: 0;
+ border-radius: 3px;
+ margin: 6px 0 0;
+ font-size: 13px;
+ padding: 8px;
+ width: 100%;
+ box-sizing: border-box;
+ display: none;
+
+ &::placeholder {
+ color: var(--grayText);
+ }
+
+ &--showed {
+ display: block;
+ }
+ }
+}
diff --git a/src/styles/main.css b/src/styles/main.css
new file mode 100644
index 00000000..1553265d
--- /dev/null
+++ b/src/styles/main.css
@@ -0,0 +1,8 @@
+@import url('variables.css');
+@import url('ui.css');
+@import url('toolbar.css');
+@import url('toolbox.css');
+@import url('inline-toolbar.css');
+@import url('settings.css');
+@import url('block.css');
+@import url('animations.css');
diff --git a/src/styles/settings.css b/src/styles/settings.css
new file mode 100644
index 00000000..0af84b0d
--- /dev/null
+++ b/src/styles/settings.css
@@ -0,0 +1,65 @@
+.ce-settings {
+ @apply --overlay-pane;
+ right: 5px;
+ top: 35px;
+ min-width: 124px;
+
+ &::before{
+ left: auto;
+ right: 12px;
+ }
+
+ display: none;
+
+ &--opened {
+ display: block;
+ }
+
+ &__plugin-zone {
+ &:not(:empty){
+ padding: 6px 6px 0;
+ }
+ }
+
+ &__default-zone {
+ &:not(:empty){
+ padding: 6px;
+ }
+ }
+
+ &__button {
+ @apply --toolbar-button;
+
+ &--disabled {
+ cursor: not-allowed !important;
+ opacity: .3;
+ }
+
+ &--selected {
+ color: var(--color-active-icon);
+ }
+
+ &--delete {
+ transition: background-color 300ms ease;
+ will-change: background-color;
+
+ .icon {
+ transition: transform 200ms ease-out;
+ will-change: transform;
+ }
+ }
+
+ &--confirm {
+ background-color: var(--color-confirm);
+ color: #fff;
+
+ &:hover {
+ background-color: color-mod(var(--color-confirm) blackness(+5%)) !important;
+ }
+
+ .icon {
+ transform: rotate(90deg);
+ }
+ }
+ }
+}
diff --git a/src/styles/toolbar.css b/src/styles/toolbar.css
new file mode 100644
index 00000000..953090ce
--- /dev/null
+++ b/src/styles/toolbar.css
@@ -0,0 +1,57 @@
+.ce-toolbar {
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 0;
+ /*opacity: 0;*/
+ /*visibility: hidden;*/
+ transition: opacity 100ms ease;
+ will-change: opacity, transform;
+ display: none;
+
+ &--opened {
+ display: block;
+ /*opacity: 1;*/
+ /*visibility: visible;*/
+ }
+
+ &__content {
+ max-width: var(--content-width);
+ margin: 0 auto;
+ position: relative;
+ }
+
+ &__plus {
+ @apply --toolbox-button;
+
+ position: absolute;
+ left: calc(calc(var(--toolbox-buttons-size) + 10px) * -1);
+
+ &--hidden {
+ display: none;
+ }
+ }
+
+ /**
+ * Block actions Zone
+ * -------------------------
+ */
+ &__actions {
+ position: absolute;
+ right: 0;
+ top: 0;
+ padding-right: 16px;
+
+ &-buttons {
+ text-align: right;
+ }
+ }
+
+ &__settings-btn {
+ display: inline-block;
+ width: 24px;
+ height: 24px;
+ color: var(--grayText);
+ cursor: pointer;
+ }
+}
diff --git a/src/styles/toolbox.css b/src/styles/toolbox.css
new file mode 100644
index 00000000..d9d06c8b
--- /dev/null
+++ b/src/styles/toolbox.css
@@ -0,0 +1,15 @@
+.ce-toolbox {
+ position: absolute;
+ visibility: hidden;
+ transition: opacity 100ms ease;
+ will-change: opacity;
+
+ &--opened {
+ opacity: 1;
+ visibility: visible;
+ }
+
+ &__button {
+ @apply --toolbox-button;
+ }
+}
diff --git a/src/styles/ui.css b/src/styles/ui.css
new file mode 100644
index 00000000..5c129de8
--- /dev/null
+++ b/src/styles/ui.css
@@ -0,0 +1,37 @@
+/**
+* Editor wrapper
+*/
+.codex-editor {
+ position: relative;
+ box-sizing: border-box;
+
+ .hide {
+ display: none;
+ }
+
+ &__redactor {
+ padding-bottom: 300px;
+ }
+
+ svg {
+ fill: currentColor;
+ vertical-align: middle;
+ max-height: 100%;
+ }
+}
+
+/**
+ * Set color for native selection
+ */
+::selection{
+ background-color: var(--selectionColor);
+}
+
+/**
+ * Add placeholder to content editable elements with data attribute
+ * data-placeholder="Hello world!"
+ */
+[contentEditable=true][data-placeholder]:empty:not(:focus):before{
+ content: attr(data-placeholder);
+ color: var(--grayText);
+}
diff --git a/src/styles/variables.css b/src/styles/variables.css
new file mode 100644
index 00000000..f5f21296
--- /dev/null
+++ b/src/styles/variables.css
@@ -0,0 +1,113 @@
+:root {
+ /**
+ * Selection color
+ */
+ --selectionColor: rgba(61,166,239,0.63);
+
+ /**
+ * Toolbar buttons
+ */
+ --bg-light: #eff2f5;
+
+ /**
+ * All gray texts: placeholders, settings
+ */
+ --grayText: #707684;
+
+ /** Blue icons */
+ --color-active-icon: #388AE5;
+
+ /**
+ * Block content width
+ */
+ --content-width: 650px;
+
+ /**
+ * Toolbar buttons height and width
+ */
+ --toolbar-buttons-size: 34px;
+
+ /**
+ * Toolbar Plus Button and Toolbox buttons height and width
+ */
+ --toolbox-buttons-size: 20px;
+
+ /**
+ * Confirm deletion bg
+ */
+ --color-confirm: #E24A4A;
+
+ --overlay-pane: {
+ position: absolute;
+ background-color: #FFFFFF;
+ box-shadow: 0 8px 23px -6px rgba(21,40,54,0.31), 22px -14px 34px -18px rgba(33,48,73,0.26);
+ border-radius: 4px;
+ z-index: 2;
+
+ &::before {
+ content: '';
+ width: 15px;
+ height: 15px;
+ position: absolute;
+ top: -7px;
+ left: 50%;
+ margin-left: -7px;
+ transform: rotate(-45deg);
+ background-color: #fff;
+ z-index: -1;
+ }
+ };
+
+ /**
+ * Styles for Toolbox Buttons and Plus Button
+ */
+ --toolbox-button: {
+ color: var(--grayText);
+ cursor: pointer;
+ display: inline-block;
+ width: var(--toolbox-buttons-size);
+ height: var(--toolbox-buttons-size);
+ /*line-height: var(--toolbox-buttons-size);*/
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ &:not(:last-of-type){
+ margin-right: 3px;
+ }
+
+ &:hover {
+ color: var(--color-active-icon);
+ }
+ };
+
+ /**
+ * Styles for Settings Button in Toolbar
+ */
+ --toolbar-button: {
+ display: inline-block;
+ width: 34px;
+ height: 34px;
+ line-height: 34px;
+ text-align: center;
+ border-radius: 3px;
+ cursor: pointer;
+ border: 0;
+ outline: none;
+ background-color: transparent;
+ vertical-align: bottom;
+ color: var(--grayText);
+
+ &:not(:last-of-type){
+ margin-right: 5px;
+ }
+
+ &:hover {
+ background-color: var(--bg-light);
+ }
+
+ &--active {
+ color: var(--color-active-icon);
+ }
+ };
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 00000000..436deb5c
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "compilerOptions" : {
+ "sourceMap": true,
+ "target": "es2017",
+ "declaration": false,
+ "lib": ["es2017", "dom"]
+ }
+}
diff --git a/tslint.json b/tslint.json
new file mode 100644
index 00000000..b24cbdfa
--- /dev/null
+++ b/tslint.json
@@ -0,0 +1,15 @@
+{
+ "extends": "tslint:recommended",
+ "rules": {
+ "indent": [true, "spaces", 2],
+ "quotemark": [true, "single"],
+ "no-console": false,
+ "one-variable-per-declaration": false,
+ "object-literal-sort-keys": false,
+ "ordered-imports": [true, {
+ "import-sources-order": "any",
+ "named-imports-order": "case-insensitive"
+ }],
+ "no-empty": false
+ }
+}
diff --git a/webpack.config.js b/webpack.config.js
index 31562936..d0919d70 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -20,79 +20,192 @@ const VERSION = process.env.VERSION || pkg.version;
* Plugins for bundle
* @type {webpack}
*/
-var webpack = require('webpack');
-var ExtractTextWebpackPlugin = require('extract-text-webpack-plugin');
+var webpack = require('webpack');
+
+/**
+ * File system
+ */
+var fs = require('fs');
+
+/**
+ * Available CodeX Editor modules placed in components/modules folder
+ * They will required automatically.
+ * Folders and files starting with '_' will be skipped
+ * @type {Array}
+ */
+var editorModules = fs.readdirSync('./src/components/modules').filter( name => /.(j|t)s$/.test(name) && name.substring(0,1) !== '_' );
+
+editorModules.forEach( name => {
+ console.log('Require modules/' + name);
+});
+
+/**
+ * Options for the Babel
+ */
+var babelLoader = {
+ loader: 'babel-loader',
+ options: {
+ cacheDirectory: true,
+ presets: [
+ "env"
+ ],
+ plugins: [
+ /**
+ * Dont need to use «.default» after «export default Class Ui {}»
+ * @see {@link https://github.com/59naga/babel-plugin-add-module-exports}
+ */
+ 'add-module-exports',
+ /**
+ * Babel transforms some awesome ES6 features to ES5 with extra code, such as Class, JSX.
+ * This plugin makes all generated extra codes to one module which significantly reduces the bundle code size.
+ *
+ * {@link https://github.com/brianZeng/babel-plugin-transform-helper}
+ * @since 11 dec 2017 - removed due to plugin does not supports class inheritance
+ */
+ // ['babel-plugin-transform-helper', {
+ // helperFilename:'build/__tmp_babel_helpers.js'
+ // }],
+ 'class-display-name',
+ ]
+ }
+};
+
+
module.exports = {
- entry: {
- "codex-editor": "./codex"
- },
- output: {
- filename: "[name].js",
- library: ["codex","editor"]
- },
+ entry: {
+ 'codex-editor': ['babel-polyfill', './src/codex']
+ },
+ output: {
+ path: path.resolve(__dirname, 'build'),
+ filename: '[name].js',
+ library: [ 'CodexEditor' ],
+ libraryTarget: 'umd'
+ },
- watch: true,
+ watch: true,
+ watchOptions: {
+ aggregateTimeout: 50
+ },
- watchOptions: {
- aggregateTimeOut: 50
- },
+ devtool: NODE_ENV == 'development' ? 'source-map' : null,
- devtool: NODE_ENV == 'development' ? "source-map" : null,
-
- resolve : {
- fallback: path.join(__dirname, "node_modules"),
- modulesDirectories : ['./node_modules', './modules'],
- extensions : ['', '.js', '.json']
- },
-
- resolveLoader : {
- modulesDirectories: ['./node_modules'],
- moduleTemplates: ["*-webpack-loader", "*-web-loader", "*-loader", "*"],
- extensions: ['', '.js']
- },
-
- plugins: [
-
- /** Pass variables into modules */
- new webpack.DefinePlugin({
- NODE_ENV: JSON.stringify(NODE_ENV),
- VERSION: JSON.stringify(VERSION)
- }),
-
- /** Минифицируем CSS и JS */
- new webpack.optimize.UglifyJsPlugin({
- /** Disable warning messages. Cant disable uglify for 3rd party libs such as html-janitor */
- compress: {
- warnings: false
- }
- }),
-
- /** Block biuld if errors found */
- new webpack.NoErrorsPlugin(),
-
- ],
-
- module : {
-
- loaders : [{
- test : /\.js$/,
- exclude: /(node_modules)/,
- loader : 'babel',
- query: {
- presets: [__dirname + '/node_modules/babel-preset-es2015']
- }
- },
- {
- test : /\.js$/,
- loader: 'eslint-loader?fix=true',
- exclude: /(node_modules)/
- },
- {
- test : /\.css$/,
- exclude: /(node_modules)/,
- loader: ExtractTextWebpackPlugin.extract('style-loader', 'css-loader')
- }]
+ /**
+ * Tell webpack what directories should be searched when resolving modules.
+ */
+ resolve : {
+ // fallback: path.join(__dirname, 'node_modules'),
+ modules : [ path.join(__dirname, "src"), "node_modules"],
+ extensions: ['.js', '.ts'],
+ alias: {
+ 'utils': path.resolve(__dirname + '/src/components/', './utils'),
+ 'dom': path.resolve(__dirname + '/src/components/', './dom'),
}
-};
\ No newline at end of file
+ },
+ //
+
+ // resolveLoader : {
+ // modules: [ path.resolve(__dirname, "src"), "node_modules" ],
+ // moduleTemplates: ['*-webpack-loader', '*-web-loader', '*-loader', '*'],
+ // extensions: ['.js']
+ // },
+
+ plugins: [
+
+ /** Pass variables into modules */
+ new webpack.DefinePlugin({
+ NODE_ENV: JSON.stringify(NODE_ENV),
+ VERSION: JSON.stringify(VERSION),
+ editorModules: JSON.stringify(editorModules)
+ }),
+
+ /**
+ * Setting up a dynamic requires that we use to autoload Editor Modules from 'components/modules' dir
+ * {@link https://webpack.js.org/plugins/context-replacement-plugin/}
+ */
+ new webpack.ContextReplacementPlugin(
+ /src\/components\/modules/,
+ false, // newContentRecursive=false because we dont need to include folders
+ new RegExp(
+ '[^_]' + // dont match names started with '_'
+ `(${editorModules.join('|')})` + // module names pattern: (events.js|ui.js|...)
+ '$' // at the end of path
+ )
+ ),
+
+ /**
+ * Automatically load global visible modules
+ * instead of having to import/require them everywhere.
+ */
+ new webpack.ProvidePlugin({
+ '_': 'utils',
+ '$': 'dom',
+ 'Module': './../__module.ts',
+ }),
+
+ /** Минифицируем CSS и JS */
+ // new webpack.optimize.UglifyJsPlugin({
+ /** Disable warning messages. Cant disable uglify for 3rd party libs such as html-janitor */
+ // compress: {
+ // warnings: false
+ // }
+ // }),
+
+ /** Block biuld if errors found */
+ // new webpack.NoErrorsPlugin(),
+
+ ],
+
+ module : {
+ rules : [
+ {
+ test: /\.ts$/,
+ use: [
+ babelLoader,
+ {
+ loader: 'ts-loader'
+ },
+ {
+ loader: 'tslint-loader',
+ }
+ ]
+ },
+ {
+ test : /\.js$/,
+ use: [
+ babelLoader,
+ {
+ loader: 'eslint-loader?fix=true&esModules=true',
+ }
+ ],
+ exclude: /(node_modules|build)/, // dont need to look in '/build' to prevent analyse __tmp_babel_helper.js
+ },
+ {
+ test: /\.css$/,
+ exclude: /node_modules/,
+ use: [
+ {
+ loader: 'css-loader',
+ options: {
+ // minimize: 1,
+ importLoaders: 1
+ }
+ },
+ 'postcss-loader'
+ ]
+ },
+ {
+ test: /\.(svg)$/,
+ use: [
+ {
+ loader: 'raw-loader',
+ }
+ ]
+ }
+ ]
+ },
+ optimization: {
+ minimize: false
+ },
+};