From 2a3b093453d978a57b8bacfe456efd8185a5ac0d Mon Sep 17 00:00:00 2001 From: gdub22 Date: Fri, 25 Jul 2014 18:39:14 -0400 Subject: [PATCH] View class abstraction and code cleanup --- dist/content-kit-editor.css | 4 +- dist/content-kit-editor.js | 608 ++++++++++++++++---------------- gulpfile.js | 1 + src/css/embeds.less | 2 +- src/css/toolbar.less | 8 +- src/css/variables.less | 3 + src/js/commands.js | 32 +- src/js/editor.js | 35 +- src/js/embed-intent.js | 104 +++--- src/js/prompt.js | 57 ++- src/js/toolbar.js | 182 +++++----- src/js/tooltip.js | 92 ++--- src/js/utils/element-utils.js | 11 + src/js/utils/object-utils.js | 2 +- src/js/utils/selection-utils.js | 51 ++- src/js/view.js | 35 ++ 16 files changed, 619 insertions(+), 608 deletions(-) create mode 100644 src/js/view.js diff --git a/dist/content-kit-editor.css b/dist/content-kit-editor.css index 8b8f0cd0f..8685040de 100755 --- a/dist/content-kit-editor.css +++ b/dist/content-kit-editor.css @@ -69,13 +69,13 @@ margin: 0 0 0 0.5em; } .ck-toolbar.right:after { - left: -1em; + left: -0.95em; top: 50%; bottom: auto; margin: -0.5em 0 0 0; border-top: 0.5em solid transparent; border-bottom: 0.5em solid transparent; - border-right: 0.5em solid #2b2b2b; + border-right: 0.5em solid #393939; } .ck-toolbar, .ck-toolbar-prompt { diff --git a/dist/content-kit-editor.js b/dist/content-kit-editor.js index b638b1968..81a80890c 100755 --- a/dist/content-kit-editor.js +++ b/dist/content-kit-editor.js @@ -3,7 +3,7 @@ * @version 0.1.0 * @author Garth Poitras (http://garthpoitras.com/) * @license MIT - * Last modified: Jul 22, 2014 + * Last modified: Jul 25, 2014 */ (function(exports, document) { @@ -55,7 +55,7 @@ var Tags = { var RootTags = [ Tags.PARAGRAPH, Tags.HEADING, Tags.SUBHEADING, Tags.QUOTE, Tags.LIST, Tags.ORDERED_LIST ]; -function extend(object, updates) { +function merge(object, updates) { updates = updates || {}; for(var o in updates) { if (updates.hasOwnProperty(o)) { @@ -108,6 +108,17 @@ function getEventTargetMatchingTag(tag, target, container) { } } +function nodeIsDescendantOfElement(node, element) { + var parentNode = node.parentNode; + while(parentNode) { + if (parentNode === element) { + return true; + } + parentNode = parentNode.parentNode; + } + return false; +} + function getElementRelativeOffset(element) { var offset = { left: 0, top: -window.pageYOffset }; var offsetParent = element.offsetParent; @@ -176,58 +187,51 @@ function getDirectionOfSelection(selection) { return SelectionDirection.SAME_NODE; } -function getCurrentSelectionNode(selection) { +function getSelectionElement(selection) { selection = selection || window.getSelection(); var node = getDirectionOfSelection(selection) === SelectionDirection.LEFT_TO_RIGHT ? selection.anchorNode : selection.focusNode; return node && (node.nodeType === 3 ? node.parentNode : node); } -function getCurrentSelectionRootNode() { - var node = getCurrentSelectionNode(); - var tag = node && node.tagName; +function getSelectionBlockElement() { + var element = getSelectionElement(); + var tag = element && element.tagName; while (tag && RootTags.indexOf(tag) === -1) { - if (node.contentEditable === 'true') { break; } // Stop traversing up dom when hitting an editor element - node = node.parentNode; - tag = node.tagName; + if (element.contentEditable === 'true') { break; } // Stop traversing up dom when hitting an editor element + element = element.parentNode; + tag = element.tagName; } - return node; + return element; } -function getCurrentSelectionTag() { - var node = getCurrentSelectionNode(); - return node ? node.tagName : null; +function getSelectionTagName() { + var element = getSelectionElement(); + return element ? element.tagName : null; } -function getCurrentSelectionRootTag() { - var node = getCurrentSelectionRootNode(); - return node ? node.tagName : null; +function getSelectionBlockTagName() { + var element = getSelectionBlockElement(); + return element ? element.tagName : null; } function tagsInSelection(selection) { - var node = getCurrentSelectionNode(selection); + var element = getSelectionElement(selection); var tags = []; if (!selection.isCollapsed) { - while(node) { - if (node.contentEditable === 'true') { break; } // Stop traversing up dom when hitting an editor element - if (node.tagName) { - tags.push(node.tagName); + while(element) { + if (element.contentEditable === 'true') { break; } // Stop traversing up dom when hitting an editor element + if (element.tagName) { + tags.push(element.tagName); } - node = node.parentNode; + element = element.parentNode; } } return tags; } function selectionIsInElement(selection, element) { - var node = selection.anchorNode, - parentNode = node && node.parentNode; - while(parentNode) { - if (parentNode === element) { - return true; - } - parentNode = parentNode.parentNode; - } - return false; + var node = selection.anchorNode; + return node && nodeIsDescendantOfElement(node, element); } function moveCursorToBeginningOfSelection(selection) { @@ -254,56 +258,87 @@ function selectNode(node) { selection.addRange(range); } +function View(options) { + this.tagName = options.tagName || 'div'; + this.classNames = options.classNames || []; + this.element = document.createElement(this.tagName); + this.element.className = this.classNames.join(' '); + this.container = options.container || document.body; + this.isShowing = false; +} + +View.prototype = { + show: function() { + var view = this; + if(!view.isShowing) { + view.container.appendChild(view.element); + view.isShowing = true; + return true; + } + }, + hide: function() { + var view = this; + if(view.isShowing) { + view.container.removeChild(view.element); + view.isShowing = false; + return true; + } + }, + addClass: function(className) { + this.classNames.push(className); + this.element.className = this.classNames.join(' '); + }, + removeClass: function(className) { + this.classNames.splice(this.classNames.indexOf(className), 1); + this.element.className = this.classNames.join(' '); + } +}; + var Prompt = (function() { var container = document.body; var hiliter = createDiv('ck-editor-hilite'); function Prompt(options) { - if (options) { - var prompt = this; - var element = document.createElement('input'); - prompt.command = options.command; - prompt.element = element; - element.type = 'text'; - element.placeholder = options.placeholder || ''; - element.addEventListener('mouseup', function(e) { e.stopPropagation(); }); // prevents closing prompt when clicking input - element.addEventListener('keyup', function(e) { - var entry = this.value; - if(entry && !e.shiftKey && e.which === Keycodes.ENTER) { - restoreRange(prompt.range); - prompt.command.exec(entry); - if (prompt.onComplete) { prompt.onComplete(); } - } - }); + var prompt = this; + options.tagName = 'input'; + View.call(prompt, options); + + prompt.command = options.command; + prompt.element.placeholder = options.placeholder || ''; + prompt.element.addEventListener('mouseup', function(e) { e.stopPropagation(); }); // prevents closing prompt when clicking input + prompt.element.addEventListener('keyup', function(e) { + var entry = this.value; + if(entry && !e.shiftKey && e.which === Keycodes.ENTER) { + restoreRange(prompt.range); + prompt.command.exec(entry); + if (prompt.onComplete) { prompt.onComplete(); } + } + }); - window.addEventListener('resize', function() { - var activeHilite = hiliter.parentNode; - var range = prompt.range; - if(activeHilite && range) { - positionHiliteRange(range); - } - }); - } + window.addEventListener('resize', function() { + var activeHilite = hiliter.parentNode; + var range = prompt.range; + if(activeHilite && range) { + positionHiliteRange(range); + } + }); } + inherits(Prompt, View); Prompt.prototype = { - display: function(callback) { + show: function(callback) { var prompt = this; var element = prompt.element; + element.value = null; prompt.range = window.getSelection().getRangeAt(0); // save the selection range container.appendChild(hiliter); positionHiliteRange(prompt.range); - prompt.clear(); setTimeout(function(){ element.focus(); }); // defer focus (disrupts mouseup events) if (callback) { prompt.onComplete = callback; } }, - dismiss: function() { - this.clear(); + hide: function() { container.removeChild(hiliter); - }, - clear: function() { - this.element.value = null; } }; @@ -365,7 +400,7 @@ function BoldCommand() { inherits(BoldCommand, TextFormatCommand); BoldCommand.prototype.exec = function() { // Don't allow executing bold command on heading tags - if (!Regex.HEADING_TAG.test(getCurrentSelectionRootTag())) { + if (!Regex.HEADING_TAG.test(getSelectionBlockTagName())) { BoldCommand._super.prototype.exec.call(this); } }; @@ -394,7 +429,7 @@ function LinkCommand() { } inherits(LinkCommand, TextFormatCommand); LinkCommand.prototype.exec = function(url) { - if(this.tag === getCurrentSelectionTag()) { + if(this.tag === getSelectionTagName()) { this.unexec(); } else { if (!Regex.HTTP_PROTOCOL.test(url)) { @@ -413,16 +448,16 @@ FormatBlockCommand.prototype.exec = function() { var tag = this.tag; // Brackets neccessary for certain browsers var value = '<' + tag + '>'; - var rootNode = getCurrentSelectionRootNode(); + var blockElement = getSelectionBlockElement(); // Allow block commands to be toggled back to a paragraph - if(tag === rootNode.tagName) { + if(tag === blockElement.tagName) { value = Tags.PARAGRAPH; } else { // Flattens the selection before applying the block format. // Otherwise, undesirable nested blocks can occur. - var flatNode = document.createTextNode(rootNode.textContent); - rootNode.parentNode.insertBefore(flatNode, rootNode); - rootNode.parentNode.removeChild(rootNode); + var flatNode = document.createTextNode(blockElement.textContent); + blockElement.parentNode.insertBefore(flatNode, blockElement); + blockElement.parentNode.removeChild(blockElement); selectNode(flatNode); } @@ -464,13 +499,13 @@ ListCommand.prototype.exec = function() { ListCommand._super.prototype.exec.call(this); // After creation, lists need to be unwrapped from the default formatter P tag - var listNode = getCurrentSelectionRootNode(); - var wrapperNode = listNode.parentNode; - if (wrapperNode.firstChild === listNode) { + var listElement = getSelectionBlockElement(); + var wrapperNode = listElement.parentNode; + if (wrapperNode.firstChild === listElement) { var editorNode = wrapperNode.parentNode; - editorNode.insertBefore(listNode, wrapperNode); + editorNode.insertBefore(listElement, wrapperNode); editorNode.removeChild(wrapperNode); - selectNode(listNode); + selectNode(listElement); } }; @@ -538,14 +573,14 @@ ImageEmbedCommand.prototype = { var reader = new FileReader(); reader.onload = function(event) { var base64File = event.target.result; - var selectionRoot = getCurrentSelectionRootNode(); + var blockElement = getSelectionBlockElement(); var image = document.createElement('img'); image.src = base64File; // image needs to be placed outside of the current empty paragraph - var editorNode = selectionRoot.parentNode; - editorNode.insertBefore(image, selectionRoot); - editorNode.removeChild(selectionRoot); + var editorNode = blockElement.parentNode; + editorNode.insertBefore(image, blockElement); + editorNode.removeChild(blockElement); }; reader.readAsDataURL(file); target.value = null; // reset @@ -603,7 +638,7 @@ ContentKit.Editor = (function() { } if (elements) { - options = extend(defaults, options); + options = merge(defaults, options); elementsLen = elements.length; for (i = 0; i < elementsLen; i++) { editors.push(new Editor(elements[i], options)); @@ -621,7 +656,7 @@ ContentKit.Editor = (function() { */ function Editor(element, options) { var editor = this; - extend(editor, options); + merge(editor, options); if (element) { var className = element.className; @@ -642,16 +677,14 @@ ContentKit.Editor = (function() { element.setAttribute('contentEditable', true); editor.element = element; - bindTextSelectionEvents(editor); bindTypingEvents(editor); bindPasteEvents(editor); editor.parser = options.parser || new ContentKit.HTMLParser(); + var textFormatToolbar = new TextFormatToolbar({ rootElement: element, commands: editor.textFormatCommands }); var linkTooltips = new Tooltip({ rootElement: element, showForTag: Tags.LINK }); - editor.textFormatToolbar = new Toolbar({ commands: editor.textFormatCommands }); - if(editor.embedCommands) { // NOTE: must come after bindTypingEvents so those keyup handlers are executed first. // TODO: manage event listener order @@ -669,25 +702,13 @@ ContentKit.Editor = (function() { return this.parser.parse(this.element.innerHTML); }; - function bindTextSelectionEvents(editor) { - // Mouse text selection - document.addEventListener('mouseup', function(e) { - setTimeout(function(){ handleTextSelection(e, editor); }); - }); - - // Keyboard text selection - editor.element.addEventListener('keyup', function(e) { - handleTextSelection(e, editor); - }); - } - function bindTypingEvents(editor) { var editorEl = editor.element; // Breaks out of blockquotes when pressing enter. editorEl.addEventListener('keyup', function(e) { if(!e.shiftKey && e.which === Keycodes.ENTER) { - if(Tags.QUOTE === getCurrentSelectionRootTag()) { + if(Tags.QUOTE === getSelectionBlockTagName()) { document.execCommand('formatBlock', false, editor.defaultFormatter); e.stopPropagation(); } @@ -699,7 +720,7 @@ ContentKit.Editor = (function() { var selectedText = window.getSelection().anchorNode.textContent, selection, selectionNode, command, replaceRegex; - if (Tags.LIST_ITEM !== getCurrentSelectionTag()) { + if (Tags.LIST_ITEM !== getSelectionTagName()) { if (Regex.UL_START.test(selectedText)) { command = new UnorderedListCommand(); replaceRegex = Regex.UL_START; @@ -722,21 +743,12 @@ ContentKit.Editor = (function() { // Assure there is always a supported root tag, and not empty text nodes or divs. // Usually only happens when selecting all and deleting content. editorEl.addEventListener('keyup', function() { - if (this.innerHTML.length && RootTags.indexOf(getCurrentSelectionRootTag()) === -1) { + if (this.innerHTML.length && RootTags.indexOf(getSelectionBlockTagName()) === -1) { document.execCommand('formatBlock', false, editor.defaultFormatter); } }); } - function handleTextSelection(e, editor) { - var selection = window.getSelection(); - if (selection.isCollapsed || selection.toString().trim() === '' || !selectionIsInElement(selection, editor.element)) { - editor.textFormatToolbar.hide(); - } else { - editor.textFormatToolbar.updateForSelection(selection); - } - } - function bindPasteEvents(editor) { editor.element.addEventListener('paste', function(e) { var data = e.clipboardData, plainText; @@ -774,28 +786,26 @@ ContentKit.Editor = (function() { var Toolbar = (function() { - var container = document.body; - function Toolbar(options) { var toolbar = this; - var commands = options && options.commands; + var commands = options.commands; var commandCount = commands && commands.length; - var element = createDiv('ck-toolbar'); var i, button; - toolbar.element = element; toolbar.direction = options.direction || ToolbarDirection.TOP; + options.classNames = ['ck-toolbar']; if (toolbar.direction === ToolbarDirection.RIGHT) { - element.className += ' right'; + options.classNames.push('right'); } - toolbar.isShowing = false; + + View.call(toolbar, options); + toolbar.activePrompt = null; toolbar.buttons = []; - bindEvents(toolbar); toolbar.promptContainerElement = createDiv('ck-toolbar-prompt'); toolbar.buttonContainerElement = createDiv('ck-toolbar-buttons'); - element.appendChild(toolbar.promptContainerElement); - element.appendChild(toolbar.buttonContainerElement); + toolbar.element.appendChild(toolbar.promptContainerElement); + toolbar.element.appendChild(toolbar.buttonContainerElement); for(i = 0; i < commandCount; i++) { button = new ToolbarButton({ command: commands[i], toolbar: toolbar }); @@ -803,90 +813,67 @@ var Toolbar = (function() { toolbar.buttonContainerElement.appendChild(button.element); } } - - Toolbar.prototype = { - show: function() { - var toolbar = this; - if(!toolbar.isShowing) { - container.appendChild(toolbar.element); - toolbar.isShowing = true; - } - }, - hide: function() { - var toolbar = this; - var element = toolbar.element; - var style = element.style; - if(toolbar.isShowing) { - container.removeChild(element); - style.left = ''; - style.top = ''; - toolbar.dismissPrompt(); - toolbar.isShowing = false; - } - }, - displayPrompt: function(prompt) { - var toolbar = this; - swapElements(toolbar.promptContainerElement, toolbar.buttonContainerElement); - toolbar.promptContainerElement.appendChild(prompt.element); - prompt.display(function() { - toolbar.dismissPrompt(); - toolbar.updateForSelection(window.getSelection()); - }); - toolbar.activePrompt = prompt; - }, - dismissPrompt: function() { - var toolbar = this; - var activePrompt = toolbar.activePrompt; - if (activePrompt) { - activePrompt.dismiss(); - swapElements(toolbar.buttonContainerElement, toolbar.promptContainerElement); - toolbar.activePrompt = null; - } - }, - updateForSelection: function(selection) { - var toolbar = this; - if (selection.isCollapsed) { - toolbar.hide(); - } else { - toolbar.show(); - toolbar.positionToContent(selection.getRangeAt(0)); - updateButtonsForSelection(toolbar.buttons, selection); - } - }, - positionToContent: function(content) { - var directions = ToolbarDirection; - var positioningMethod; - switch(this.direction) { - case directions.RIGHT: - positioningMethod = positionElementToRightOf; - break; - default: - positioningMethod = positionElementCenteredAbove; - } - positioningMethod(this.element, content); + inherits(Toolbar, View); + + Toolbar.prototype.hide = function() { + if (Toolbar._super.prototype.hide.call(this)) { + var style = this.element.style; + style.left = ''; + style.top = ''; + this.dismissPrompt(); } }; - function bindEvents(toolbar) { - document.addEventListener('keyup', function(e) { - if (e.keyCode === Keycodes.ESC) { - toolbar.hide(); - } + Toolbar.prototype.displayPrompt = function(prompt) { + var toolbar = this; + swapElements(toolbar.promptContainerElement, toolbar.buttonContainerElement); + toolbar.promptContainerElement.appendChild(prompt.element); + prompt.show(function() { + toolbar.dismissPrompt(); + toolbar.updateForSelection(window.getSelection()); }); + toolbar.activePrompt = prompt; + }; - window.addEventListener('resize', function() { - var activePrompt = toolbar.activePrompt; - if(toolbar.isShowing) { - toolbar.positionToContent(activePrompt ? activePrompt.range : window.getSelection().getRangeAt(0)); - } - }); - } + Toolbar.prototype.dismissPrompt = function(prompt) { + var toolbar = this; + var activePrompt = toolbar.activePrompt; + if (activePrompt) { + activePrompt.hide(); + swapElements(toolbar.buttonContainerElement, toolbar.promptContainerElement); + toolbar.activePrompt = null; + } + }; + + Toolbar.prototype.updateForSelection = function(selection) { + var toolbar = this; + if (selection.isCollapsed) { + toolbar.hide(); + } else { + toolbar.show(); + toolbar.positionToContent(selection.getRangeAt(0)); + updateButtonsForSelection(toolbar.buttons, selection); + } + }; + + Toolbar.prototype.positionToContent = function(content) { + var directions = ToolbarDirection; + var positioningMethod; + switch(this.direction) { + case directions.RIGHT: + positioningMethod = positionElementToRightOf; + break; + default: + positioningMethod = positionElementCenteredAbove; + } + positioningMethod(this.element, content); + }; function updateButtonsForSelection(buttons, selection) { var selectedTags = tagsInSelection(selection), len = buttons.length, i, button; - + for (i = 0; i < len; i++) { button = buttons[i]; if (selectedTags.indexOf(button.command.tag) > -1) { @@ -900,6 +887,47 @@ var Toolbar = (function() { return Toolbar; }()); + +var TextFormatToolbar = (function() { + + function TextFormatToolbar(options) { + var toolbar = this; + Toolbar.call(this, options); + toolbar.rootElement = options.rootElement; + toolbar.rootElement.addEventListener('keyup', function() { toolbar.handleTextSelection(); }); + + document.addEventListener('keyup', function(e) { + if (e.keyCode === Keycodes.ESC) { + toolbar.hide(); + } + }); + + document.addEventListener('mouseup', function() { + setTimeout(function() { toolbar.handleTextSelection(); }); + }); + + window.addEventListener('resize', function() { + if(toolbar.isShowing) { + var activePromptRange = toolbar.activePrompt && toolbar.activePrompt.range; + toolbar.positionToContent(activePromptRange ? activePromptRange : window.getSelection().getRangeAt(0)); + } + }); + } + inherits(TextFormatToolbar, Toolbar); + + TextFormatToolbar.prototype.handleTextSelection = function() { + var toolbar = this; + var selection = window.getSelection(); + if (selection.isCollapsed || selection.toString().trim() === '' || !selectionIsInElement(selection, toolbar.rootElement)) { + toolbar.hide(); + } else { + toolbar.updateForSelection(selection); + } + }; + + return TextFormatToolbar; +}()); + var ToolbarButton = (function() { var buttonClassName = 'ck-toolbar-btn'; @@ -951,78 +979,57 @@ var ToolbarButton = (function() { return ToolbarButton; }()); -var Tooltip = (function() { - - var container = document.body; - var className = 'ck-tooltip'; - var delay = 200; - - function Tooltip(options) { - var tooltip = this; - var rootElement = options.rootElement; - var timeout; - - tooltip.element = createDiv(className); - tooltip.isShowing = false; - - rootElement.addEventListener('mouseover', function(e) { - var target = getEventTargetMatchingTag(options.showForTag, e.target, rootElement); - if (target) { - timeout = setTimeout(function() { - tooltip.showLink(target.href, target); - }, delay); - } - }); - - rootElement.addEventListener('mouseout', function(e) { - clearTimeout(timeout); - var toElement = e.toElement || e.relatedTarget; - if (toElement && toElement.className !== className) { - tooltip.hide(); - } - }); - } - - Tooltip.prototype = { - showMessage: function(message, element) { - var tooltip = this; - var tooltipElement = tooltip.element; - - tooltipElement.innerHTML = message; - if (!tooltip.isShowing) { - container.appendChild(tooltipElement); - tooltip.isShowing = true; - } - positionElementCenteredBelow(tooltipElement, element); - }, - showLink: function(link, element) { - var message = '' + link + ''; - this.showMessage(message, element); - }, - hide: function() { - var tooltip = this; - if (tooltip.isShowing) { - container.removeChild(tooltip.element); - tooltip.isShowing = false; - } +function Tooltip(options) { + var tooltip = this; + var rootElement = options.rootElement; + var delay = options.delay || 200; + var timeout; + options.classNames = ['ck-tooltip']; + View.call(tooltip, options); + + rootElement.addEventListener('mouseover', function(e) { + var target = getEventTargetMatchingTag(options.showForTag, e.target, rootElement); + if (target) { + timeout = setTimeout(function() { + tooltip.showLink(target.href, target); + }, delay); } - }; + }); + + rootElement.addEventListener('mouseout', function(e) { + clearTimeout(timeout); + var toElement = e.toElement || e.relatedTarget; + if (toElement && toElement.className !== tooltip.element.className) { + tooltip.hide(); + } + }); +} +inherits(Tooltip, View); - return Tooltip; -}()); +Tooltip.prototype.showMessage = function(message, element) { + var tooltip = this; + var tooltipElement = tooltip.element; + tooltipElement.innerHTML = message; + tooltip.show(); + positionElementCenteredBelow(tooltipElement, element); +}; -var EmbedIntent = (function() { +Tooltip.prototype.showLink = function(link, element) { + var message = '' + link + ''; + this.showMessage(message, element); +}; - var container = document.body; - var className = 'ck-embed-intent-btn'; +var EmbedIntent = (function() { function EmbedIntent(options) { var embedIntent = this; - var element = document.createElement('button'); var rootElement = options.rootElement; - element.className = className; - element.title = 'Insert image or embed...'; - element.addEventListener('mouseup', function(e) { + options.tagName = 'button'; + options.classNames = ['ck-embed-intent-btn']; + View.call(embedIntent, options); + + embedIntent.element.title = 'Insert image or embed...'; + embedIntent.element.addEventListener('mouseup', function(e) { if (embedIntent.isActive) { embedIntent.deactivate(); } else { @@ -1030,28 +1037,28 @@ var EmbedIntent = (function() { } e.stopPropagation(); }); - embedIntent.element = element; embedIntent.toolbar = new Toolbar({ commands: options.commands, direction: ToolbarDirection.RIGHT }); - embedIntent.isShowing = false; embedIntent.isActive = false; - function embedIntentHandler(e) { - var currentNode = getCurrentSelectionRootNode(); - if (!currentNode) { - embedIntent.hide(); - return; - } - var currentNodeHTML = currentNode.innerHTML; - if (currentNodeHTML === '' || currentNodeHTML === '
') { - embedIntent.showAt(currentNode); + function embedIntentHandler() { + var blockElement = getSelectionBlockElement(); + var blockElementContent = blockElement && blockElement.innerHTML; + if (blockElementContent === '' || blockElementContent === '
') { + embedIntent.showAt(blockElement); } else { embedIntent.hide(); } - e.stopPropagation(); } rootElement.addEventListener('keyup', embedIntentHandler); - document.addEventListener('mouseup', function(e) { setTimeout(function() { embedIntentHandler(e); }); }); + + document.addEventListener('mouseup', function(e) { + setTimeout(function() { + if (!nodeIsDescendantOfElement(e.target, embedIntent.toolbar.element)) { + embedIntentHandler(); + } + }); + }); document.addEventListener('keyup', function(e) { if (e.keyCode === Keycodes.ESC) { @@ -1062,44 +1069,41 @@ var EmbedIntent = (function() { window.addEventListener('resize', function() { if(embedIntent.isShowing) { positionElementToLeftOf(embedIntent.element, embedIntent.atNode); + if (embedIntent.toolbar.isShowing) { + embedIntent.toolbar.positionToContent(embedIntent.element); + } } }); } + inherits(EmbedIntent, View); - EmbedIntent.prototype = { - show: function() { - if (!this.isShowing) { - container.appendChild(this.element); - this.isShowing = true; - } - }, - showAt: function(node) { - this.hide(); - this.show(); - this.atNode = node; - positionElementToLeftOf(this.element, node); - }, - hide: function() { - if (this.isShowing) { - container.removeChild(this.element); - this.deactivate(); - this.isShowing = false; - } - }, - activate: function() { - if (!this.isActive) { - this.element.className = className + ' activated'; - this.toolbar.show(); - this.toolbar.positionToContent(this.element); - this.isActive = true; - } - }, - deactivate: function() { - if (this.isActive) { - this.element.className = className; - this.toolbar.hide(); - this.isActive = false; - } + EmbedIntent.prototype.hide = function() { + if (EmbedIntent._super.prototype.hide.call(this)) { + this.deactivate(); + } + }; + + EmbedIntent.prototype.showAt = function(node) { + this.show(); + this.deactivate(); + this.atNode = node; + positionElementToLeftOf(this.element, node); + }; + + EmbedIntent.prototype.activate = function() { + if (!this.isActive) { + this.addClass('activated'); + this.toolbar.show(); + this.toolbar.positionToContent(this.element); + this.isActive = true; + } + }; + + EmbedIntent.prototype.deactivate = function() { + if (this.isActive) { + this.removeClass('activated'); + this.toolbar.hide(); + this.isActive = false; } }; diff --git a/gulpfile.js b/gulpfile.js index 6d2d1b75e..9f7fb2109 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -20,6 +20,7 @@ var jsSrc = [ './src/js/utils/object-utils.js', './src/js/utils/element-utils.js', './src/js/utils/selection-utils.js', + './src/js/view.js', './src/js/prompt.js', './src/js/commands.js', './src/js/editor.js', diff --git a/src/css/embeds.less b/src/css/embeds.less index 1e8acea2c..2cfc1d35a 100644 --- a/src/css/embeds.less +++ b/src/css/embeds.less @@ -19,7 +19,7 @@ font-size: 2em; line-height: 0.7em; cursor: pointer; - transition: color 0.15s, border-color 0.15s, transform 0.35s; + transition: color @colorChangeSpeed, border-color @colorChangeSpeed, transform 0.35s; -webkit-animation: pop 0.5s linear; animation: pop 0.5s linear; } diff --git a/src/css/toolbar.less b/src/css/toolbar.less index 0fdf43681..37943a067 100644 --- a/src/css/toolbar.less +++ b/src/css/toolbar.less @@ -14,7 +14,7 @@ background: linear-gradient(to bottom, rgba(74,74,74,0.97) 0%,rgba(43,43,43,1) 100%); box-shadow: 0 1px 3px -1px rgba(0,0,0,0.8), inset 0 2px 0 rgba(255,255,255,0.12), inset 1px 1px 0 #282828, inset -1px -1px 0 #282828; border-radius: 5px; - transition: left 0.1s, top 0.1s; + transition: left @elementMoveSpeed, top @elementMoveSpeed; margin-bottom: 0.5em; /* space for arrow */ } @@ -35,13 +35,13 @@ margin: 0 0 0 0.5em; } .ck-toolbar.right:after { - left: -1em; + left: -0.95em; top: 50%; bottom: auto; margin: -0.5em 0 0 0; border-top: 0.5em solid transparent; border-bottom: 0.5em solid transparent; - border-right: 0.5em solid rgba(43,43,43,1); + border-right: 0.5em solid #393939; } .ck-toolbar, @@ -68,7 +68,7 @@ height: 44px; line-height: 44px; cursor: pointer; - transition: background-color 0.15s; + transition: background-color @colorChangeSpeed; text-shadow: 0 1px rgba(0,0,0,0.7); -moz-user-select: none; -webkit-user-select: none; diff --git a/src/css/variables.less b/src/css/variables.less index 2e9789665..f0f3580d2 100644 --- a/src/css/variables.less +++ b/src/css/variables.less @@ -2,3 +2,6 @@ @themeColor : rgb(76, 217, 100); @themeColorText : darken(@themeColor, 10%); + +@elementMoveSpeed : 0.1s; +@colorChangeSpeed : 0.15s; diff --git a/src/js/commands.js b/src/js/commands.js index 03594acdd..cda9f50a2 100644 --- a/src/js/commands.js +++ b/src/js/commands.js @@ -45,7 +45,7 @@ function BoldCommand() { inherits(BoldCommand, TextFormatCommand); BoldCommand.prototype.exec = function() { // Don't allow executing bold command on heading tags - if (!Regex.HEADING_TAG.test(getCurrentSelectionRootTag())) { + if (!Regex.HEADING_TAG.test(getSelectionBlockTagName())) { BoldCommand._super.prototype.exec.call(this); } }; @@ -74,7 +74,7 @@ function LinkCommand() { } inherits(LinkCommand, TextFormatCommand); LinkCommand.prototype.exec = function(url) { - if(this.tag === getCurrentSelectionTag()) { + if(this.tag === getSelectionTagName()) { this.unexec(); } else { if (!Regex.HTTP_PROTOCOL.test(url)) { @@ -93,16 +93,16 @@ FormatBlockCommand.prototype.exec = function() { var tag = this.tag; // Brackets neccessary for certain browsers var value = '<' + tag + '>'; - var rootNode = getCurrentSelectionRootNode(); + var blockElement = getSelectionBlockElement(); // Allow block commands to be toggled back to a paragraph - if(tag === rootNode.tagName) { + if(tag === blockElement.tagName) { value = Tags.PARAGRAPH; } else { // Flattens the selection before applying the block format. // Otherwise, undesirable nested blocks can occur. - var flatNode = document.createTextNode(rootNode.textContent); - rootNode.parentNode.insertBefore(flatNode, rootNode); - rootNode.parentNode.removeChild(rootNode); + var flatNode = document.createTextNode(blockElement.textContent); + blockElement.parentNode.insertBefore(flatNode, blockElement); + blockElement.parentNode.removeChild(blockElement); selectNode(flatNode); } @@ -144,13 +144,13 @@ ListCommand.prototype.exec = function() { ListCommand._super.prototype.exec.call(this); // After creation, lists need to be unwrapped from the default formatter P tag - var listNode = getCurrentSelectionRootNode(); - var wrapperNode = listNode.parentNode; - if (wrapperNode.firstChild === listNode) { + var listElement = getSelectionBlockElement(); + var wrapperNode = listElement.parentNode; + if (wrapperNode.firstChild === listElement) { var editorNode = wrapperNode.parentNode; - editorNode.insertBefore(listNode, wrapperNode); + editorNode.insertBefore(listElement, wrapperNode); editorNode.removeChild(wrapperNode); - selectNode(listNode); + selectNode(listElement); } }; @@ -218,14 +218,14 @@ ImageEmbedCommand.prototype = { var reader = new FileReader(); reader.onload = function(event) { var base64File = event.target.result; - var selectionRoot = getCurrentSelectionRootNode(); + var blockElement = getSelectionBlockElement(); var image = document.createElement('img'); image.src = base64File; // image needs to be placed outside of the current empty paragraph - var editorNode = selectionRoot.parentNode; - editorNode.insertBefore(image, selectionRoot); - editorNode.removeChild(selectionRoot); + var editorNode = blockElement.parentNode; + editorNode.insertBefore(image, blockElement); + editorNode.removeChild(blockElement); }; reader.readAsDataURL(file); target.value = null; // reset diff --git a/src/js/editor.js b/src/js/editor.js index 2f0d3c8a9..a3bcd1e8d 100644 --- a/src/js/editor.js +++ b/src/js/editor.js @@ -30,7 +30,7 @@ ContentKit.Editor = (function() { } if (elements) { - options = extend(defaults, options); + options = merge(defaults, options); elementsLen = elements.length; for (i = 0; i < elementsLen; i++) { editors.push(new Editor(elements[i], options)); @@ -48,7 +48,7 @@ ContentKit.Editor = (function() { */ function Editor(element, options) { var editor = this; - extend(editor, options); + merge(editor, options); if (element) { var className = element.className; @@ -69,16 +69,14 @@ ContentKit.Editor = (function() { element.setAttribute('contentEditable', true); editor.element = element; - bindTextSelectionEvents(editor); bindTypingEvents(editor); bindPasteEvents(editor); editor.parser = options.parser || new ContentKit.HTMLParser(); + var textFormatToolbar = new TextFormatToolbar({ rootElement: element, commands: editor.textFormatCommands }); var linkTooltips = new Tooltip({ rootElement: element, showForTag: Tags.LINK }); - editor.textFormatToolbar = new Toolbar({ commands: editor.textFormatCommands }); - if(editor.embedCommands) { // NOTE: must come after bindTypingEvents so those keyup handlers are executed first. // TODO: manage event listener order @@ -96,25 +94,13 @@ ContentKit.Editor = (function() { return this.parser.parse(this.element.innerHTML); }; - function bindTextSelectionEvents(editor) { - // Mouse text selection - document.addEventListener('mouseup', function(e) { - setTimeout(function(){ handleTextSelection(e, editor); }); - }); - - // Keyboard text selection - editor.element.addEventListener('keyup', function(e) { - handleTextSelection(e, editor); - }); - } - function bindTypingEvents(editor) { var editorEl = editor.element; // Breaks out of blockquotes when pressing enter. editorEl.addEventListener('keyup', function(e) { if(!e.shiftKey && e.which === Keycodes.ENTER) { - if(Tags.QUOTE === getCurrentSelectionRootTag()) { + if(Tags.QUOTE === getSelectionBlockTagName()) { document.execCommand('formatBlock', false, editor.defaultFormatter); e.stopPropagation(); } @@ -126,7 +112,7 @@ ContentKit.Editor = (function() { var selectedText = window.getSelection().anchorNode.textContent, selection, selectionNode, command, replaceRegex; - if (Tags.LIST_ITEM !== getCurrentSelectionTag()) { + if (Tags.LIST_ITEM !== getSelectionTagName()) { if (Regex.UL_START.test(selectedText)) { command = new UnorderedListCommand(); replaceRegex = Regex.UL_START; @@ -149,21 +135,12 @@ ContentKit.Editor = (function() { // Assure there is always a supported root tag, and not empty text nodes or divs. // Usually only happens when selecting all and deleting content. editorEl.addEventListener('keyup', function() { - if (this.innerHTML.length && RootTags.indexOf(getCurrentSelectionRootTag()) === -1) { + if (this.innerHTML.length && RootTags.indexOf(getSelectionBlockTagName()) === -1) { document.execCommand('formatBlock', false, editor.defaultFormatter); } }); } - function handleTextSelection(e, editor) { - var selection = window.getSelection(); - if (selection.isCollapsed || selection.toString().trim() === '' || !selectionIsInElement(selection, editor.element)) { - editor.textFormatToolbar.hide(); - } else { - editor.textFormatToolbar.updateForSelection(selection); - } - } - function bindPasteEvents(editor) { editor.element.addEventListener('paste', function(e) { var data = e.clipboardData, plainText; diff --git a/src/js/embed-intent.js b/src/js/embed-intent.js index f222b50c2..c2eef1a2b 100644 --- a/src/js/embed-intent.js +++ b/src/js/embed-intent.js @@ -1,15 +1,14 @@ var EmbedIntent = (function() { - var container = document.body; - var className = 'ck-embed-intent-btn'; - function EmbedIntent(options) { var embedIntent = this; - var element = document.createElement('button'); var rootElement = options.rootElement; - element.className = className; - element.title = 'Insert image or embed...'; - element.addEventListener('mouseup', function(e) { + options.tagName = 'button'; + options.classNames = ['ck-embed-intent-btn']; + View.call(embedIntent, options); + + embedIntent.element.title = 'Insert image or embed...'; + embedIntent.element.addEventListener('mouseup', function(e) { if (embedIntent.isActive) { embedIntent.deactivate(); } else { @@ -17,28 +16,28 @@ var EmbedIntent = (function() { } e.stopPropagation(); }); - embedIntent.element = element; embedIntent.toolbar = new Toolbar({ commands: options.commands, direction: ToolbarDirection.RIGHT }); - embedIntent.isShowing = false; embedIntent.isActive = false; - function embedIntentHandler(e) { - var currentNode = getCurrentSelectionRootNode(); - if (!currentNode) { - embedIntent.hide(); - return; - } - var currentNodeHTML = currentNode.innerHTML; - if (currentNodeHTML === '' || currentNodeHTML === '
') { - embedIntent.showAt(currentNode); + function embedIntentHandler() { + var blockElement = getSelectionBlockElement(); + var blockElementContent = blockElement && blockElement.innerHTML; + if (blockElementContent === '' || blockElementContent === '
') { + embedIntent.showAt(blockElement); } else { embedIntent.hide(); } - e.stopPropagation(); } rootElement.addEventListener('keyup', embedIntentHandler); - document.addEventListener('mouseup', function(e) { setTimeout(function() { embedIntentHandler(e); }); }); + + document.addEventListener('mouseup', function(e) { + setTimeout(function() { + if (!nodeIsDescendantOfElement(e.target, embedIntent.toolbar.element)) { + embedIntentHandler(); + } + }); + }); document.addEventListener('keyup', function(e) { if (e.keyCode === Keycodes.ESC) { @@ -49,44 +48,41 @@ var EmbedIntent = (function() { window.addEventListener('resize', function() { if(embedIntent.isShowing) { positionElementToLeftOf(embedIntent.element, embedIntent.atNode); + if (embedIntent.toolbar.isShowing) { + embedIntent.toolbar.positionToContent(embedIntent.element); + } } }); } + inherits(EmbedIntent, View); - EmbedIntent.prototype = { - show: function() { - if (!this.isShowing) { - container.appendChild(this.element); - this.isShowing = true; - } - }, - showAt: function(node) { - this.hide(); - this.show(); - this.atNode = node; - positionElementToLeftOf(this.element, node); - }, - hide: function() { - if (this.isShowing) { - container.removeChild(this.element); - this.deactivate(); - this.isShowing = false; - } - }, - activate: function() { - if (!this.isActive) { - this.element.className = className + ' activated'; - this.toolbar.show(); - this.toolbar.positionToContent(this.element); - this.isActive = true; - } - }, - deactivate: function() { - if (this.isActive) { - this.element.className = className; - this.toolbar.hide(); - this.isActive = false; - } + EmbedIntent.prototype.hide = function() { + if (EmbedIntent._super.prototype.hide.call(this)) { + this.deactivate(); + } + }; + + EmbedIntent.prototype.showAt = function(node) { + this.show(); + this.deactivate(); + this.atNode = node; + positionElementToLeftOf(this.element, node); + }; + + EmbedIntent.prototype.activate = function() { + if (!this.isActive) { + this.addClass('activated'); + this.toolbar.show(); + this.toolbar.positionToContent(this.element); + this.isActive = true; + } + }; + + EmbedIntent.prototype.deactivate = function() { + if (this.isActive) { + this.removeClass('activated'); + this.toolbar.hide(); + this.isActive = false; } }; diff --git a/src/js/prompt.js b/src/js/prompt.js index 5d5ecc7f1..487a55bdc 100644 --- a/src/js/prompt.js +++ b/src/js/prompt.js @@ -4,50 +4,45 @@ var Prompt = (function() { var hiliter = createDiv('ck-editor-hilite'); function Prompt(options) { - if (options) { - var prompt = this; - var element = document.createElement('input'); - prompt.command = options.command; - prompt.element = element; - element.type = 'text'; - element.placeholder = options.placeholder || ''; - element.addEventListener('mouseup', function(e) { e.stopPropagation(); }); // prevents closing prompt when clicking input - element.addEventListener('keyup', function(e) { - var entry = this.value; - if(entry && !e.shiftKey && e.which === Keycodes.ENTER) { - restoreRange(prompt.range); - prompt.command.exec(entry); - if (prompt.onComplete) { prompt.onComplete(); } - } - }); + var prompt = this; + options.tagName = 'input'; + View.call(prompt, options); - window.addEventListener('resize', function() { - var activeHilite = hiliter.parentNode; - var range = prompt.range; - if(activeHilite && range) { - positionHiliteRange(range); - } - }); - } + prompt.command = options.command; + prompt.element.placeholder = options.placeholder || ''; + prompt.element.addEventListener('mouseup', function(e) { e.stopPropagation(); }); // prevents closing prompt when clicking input + prompt.element.addEventListener('keyup', function(e) { + var entry = this.value; + if(entry && !e.shiftKey && e.which === Keycodes.ENTER) { + restoreRange(prompt.range); + prompt.command.exec(entry); + if (prompt.onComplete) { prompt.onComplete(); } + } + }); + + window.addEventListener('resize', function() { + var activeHilite = hiliter.parentNode; + var range = prompt.range; + if(activeHilite && range) { + positionHiliteRange(range); + } + }); } + inherits(Prompt, View); Prompt.prototype = { - display: function(callback) { + show: function(callback) { var prompt = this; var element = prompt.element; + element.value = null; prompt.range = window.getSelection().getRangeAt(0); // save the selection range container.appendChild(hiliter); positionHiliteRange(prompt.range); - prompt.clear(); setTimeout(function(){ element.focus(); }); // defer focus (disrupts mouseup events) if (callback) { prompt.onComplete = callback; } }, - dismiss: function() { - this.clear(); + hide: function() { container.removeChild(hiliter); - }, - clear: function() { - this.element.value = null; } }; diff --git a/src/js/toolbar.js b/src/js/toolbar.js index 3a0e174fe..e0b5bfb39 100644 --- a/src/js/toolbar.js +++ b/src/js/toolbar.js @@ -1,27 +1,25 @@ var Toolbar = (function() { - var container = document.body; - function Toolbar(options) { var toolbar = this; - var commands = options && options.commands; + var commands = options.commands; var commandCount = commands && commands.length; - var element = createDiv('ck-toolbar'); var i, button; - toolbar.element = element; toolbar.direction = options.direction || ToolbarDirection.TOP; + options.classNames = ['ck-toolbar']; if (toolbar.direction === ToolbarDirection.RIGHT) { - element.className += ' right'; + options.classNames.push('right'); } - toolbar.isShowing = false; + + View.call(toolbar, options); + toolbar.activePrompt = null; toolbar.buttons = []; - bindEvents(toolbar); toolbar.promptContainerElement = createDiv('ck-toolbar-prompt'); toolbar.buttonContainerElement = createDiv('ck-toolbar-buttons'); - element.appendChild(toolbar.promptContainerElement); - element.appendChild(toolbar.buttonContainerElement); + toolbar.element.appendChild(toolbar.promptContainerElement); + toolbar.element.appendChild(toolbar.buttonContainerElement); for(i = 0; i < commandCount; i++) { button = new ToolbarButton({ command: commands[i], toolbar: toolbar }); @@ -29,90 +27,67 @@ var Toolbar = (function() { toolbar.buttonContainerElement.appendChild(button.element); } } + inherits(Toolbar, View); - Toolbar.prototype = { - show: function() { - var toolbar = this; - if(!toolbar.isShowing) { - container.appendChild(toolbar.element); - toolbar.isShowing = true; - } - }, - hide: function() { - var toolbar = this; - var element = toolbar.element; - var style = element.style; - if(toolbar.isShowing) { - container.removeChild(element); - style.left = ''; - style.top = ''; - toolbar.dismissPrompt(); - toolbar.isShowing = false; - } - }, - displayPrompt: function(prompt) { - var toolbar = this; - swapElements(toolbar.promptContainerElement, toolbar.buttonContainerElement); - toolbar.promptContainerElement.appendChild(prompt.element); - prompt.display(function() { - toolbar.dismissPrompt(); - toolbar.updateForSelection(window.getSelection()); - }); - toolbar.activePrompt = prompt; - }, - dismissPrompt: function() { - var toolbar = this; - var activePrompt = toolbar.activePrompt; - if (activePrompt) { - activePrompt.dismiss(); - swapElements(toolbar.buttonContainerElement, toolbar.promptContainerElement); - toolbar.activePrompt = null; - } - }, - updateForSelection: function(selection) { - var toolbar = this; - if (selection.isCollapsed) { - toolbar.hide(); - } else { - toolbar.show(); - toolbar.positionToContent(selection.getRangeAt(0)); - updateButtonsForSelection(toolbar.buttons, selection); - } - }, - positionToContent: function(content) { - var directions = ToolbarDirection; - var positioningMethod; - switch(this.direction) { - case directions.RIGHT: - positioningMethod = positionElementToRightOf; - break; - default: - positioningMethod = positionElementCenteredAbove; - } - positioningMethod(this.element, content); + Toolbar.prototype.hide = function() { + if (Toolbar._super.prototype.hide.call(this)) { + var style = this.element.style; + style.left = ''; + style.top = ''; + this.dismissPrompt(); } }; - function bindEvents(toolbar) { - document.addEventListener('keyup', function(e) { - if (e.keyCode === Keycodes.ESC) { - toolbar.hide(); - } + Toolbar.prototype.displayPrompt = function(prompt) { + var toolbar = this; + swapElements(toolbar.promptContainerElement, toolbar.buttonContainerElement); + toolbar.promptContainerElement.appendChild(prompt.element); + prompt.show(function() { + toolbar.dismissPrompt(); + toolbar.updateForSelection(window.getSelection()); }); + toolbar.activePrompt = prompt; + }; - window.addEventListener('resize', function() { - var activePrompt = toolbar.activePrompt; - if(toolbar.isShowing) { - toolbar.positionToContent(activePrompt ? activePrompt.range : window.getSelection().getRangeAt(0)); - } - }); - } + Toolbar.prototype.dismissPrompt = function(prompt) { + var toolbar = this; + var activePrompt = toolbar.activePrompt; + if (activePrompt) { + activePrompt.hide(); + swapElements(toolbar.buttonContainerElement, toolbar.promptContainerElement); + toolbar.activePrompt = null; + } + }; + + Toolbar.prototype.updateForSelection = function(selection) { + var toolbar = this; + if (selection.isCollapsed) { + toolbar.hide(); + } else { + toolbar.show(); + toolbar.positionToContent(selection.getRangeAt(0)); + updateButtonsForSelection(toolbar.buttons, selection); + } + }; + + Toolbar.prototype.positionToContent = function(content) { + var directions = ToolbarDirection; + var positioningMethod; + switch(this.direction) { + case directions.RIGHT: + positioningMethod = positionElementToRightOf; + break; + default: + positioningMethod = positionElementCenteredAbove; + } + positioningMethod(this.element, content); + }; function updateButtonsForSelection(buttons, selection) { var selectedTags = tagsInSelection(selection), len = buttons.length, i, button; - + for (i = 0; i < len; i++) { button = buttons[i]; if (selectedTags.indexOf(button.command.tag) > -1) { @@ -125,3 +100,44 @@ var Toolbar = (function() { return Toolbar; }()); + + +var TextFormatToolbar = (function() { + + function TextFormatToolbar(options) { + var toolbar = this; + Toolbar.call(this, options); + toolbar.rootElement = options.rootElement; + toolbar.rootElement.addEventListener('keyup', function() { toolbar.handleTextSelection(); }); + + document.addEventListener('keyup', function(e) { + if (e.keyCode === Keycodes.ESC) { + toolbar.hide(); + } + }); + + document.addEventListener('mouseup', function() { + setTimeout(function() { toolbar.handleTextSelection(); }); + }); + + window.addEventListener('resize', function() { + if(toolbar.isShowing) { + var activePromptRange = toolbar.activePrompt && toolbar.activePrompt.range; + toolbar.positionToContent(activePromptRange ? activePromptRange : window.getSelection().getRangeAt(0)); + } + }); + } + inherits(TextFormatToolbar, Toolbar); + + TextFormatToolbar.prototype.handleTextSelection = function() { + var toolbar = this; + var selection = window.getSelection(); + if (selection.isCollapsed || selection.toString().trim() === '' || !selectionIsInElement(selection, toolbar.rootElement)) { + toolbar.hide(); + } else { + toolbar.updateForSelection(selection); + } + }; + + return TextFormatToolbar; +}()); diff --git a/src/js/tooltip.js b/src/js/tooltip.js index 18de8be1c..313841b70 100644 --- a/src/js/tooltip.js +++ b/src/js/tooltip.js @@ -1,59 +1,39 @@ -var Tooltip = (function() { +function Tooltip(options) { + var tooltip = this; + var rootElement = options.rootElement; + var delay = options.delay || 200; + var timeout; + options.classNames = ['ck-tooltip']; + View.call(tooltip, options); - var container = document.body; - var className = 'ck-tooltip'; - var delay = 200; - - function Tooltip(options) { - var tooltip = this; - var rootElement = options.rootElement; - var timeout; - - tooltip.element = createDiv(className); - tooltip.isShowing = false; - - rootElement.addEventListener('mouseover', function(e) { - var target = getEventTargetMatchingTag(options.showForTag, e.target, rootElement); - if (target) { - timeout = setTimeout(function() { - tooltip.showLink(target.href, target); - }, delay); - } - }); - - rootElement.addEventListener('mouseout', function(e) { - clearTimeout(timeout); - var toElement = e.toElement || e.relatedTarget; - if (toElement && toElement.className !== className) { - tooltip.hide(); - } - }); - } - - Tooltip.prototype = { - showMessage: function(message, element) { - var tooltip = this; - var tooltipElement = tooltip.element; - - tooltipElement.innerHTML = message; - if (!tooltip.isShowing) { - container.appendChild(tooltipElement); - tooltip.isShowing = true; - } - positionElementCenteredBelow(tooltipElement, element); - }, - showLink: function(link, element) { - var message = '' + link + ''; - this.showMessage(message, element); - }, - hide: function() { - var tooltip = this; - if (tooltip.isShowing) { - container.removeChild(tooltip.element); - tooltip.isShowing = false; - } + rootElement.addEventListener('mouseover', function(e) { + var target = getEventTargetMatchingTag(options.showForTag, e.target, rootElement); + if (target) { + timeout = setTimeout(function() { + tooltip.showLink(target.href, target); + }, delay); + } + }); + + rootElement.addEventListener('mouseout', function(e) { + clearTimeout(timeout); + var toElement = e.toElement || e.relatedTarget; + if (toElement && toElement.className !== tooltip.element.className) { + tooltip.hide(); } - }; + }); +} +inherits(Tooltip, View); + +Tooltip.prototype.showMessage = function(message, element) { + var tooltip = this; + var tooltipElement = tooltip.element; + tooltipElement.innerHTML = message; + tooltip.show(); + positionElementCenteredBelow(tooltipElement, element); +}; - return Tooltip; -}()); +Tooltip.prototype.showLink = function(link, element) { + var message = '' + link + ''; + this.showMessage(message, element); +}; diff --git a/src/js/utils/element-utils.js b/src/js/utils/element-utils.js index 7f2047ebe..0aa3a4279 100644 --- a/src/js/utils/element-utils.js +++ b/src/js/utils/element-utils.js @@ -29,6 +29,17 @@ function getEventTargetMatchingTag(tag, target, container) { } } +function nodeIsDescendantOfElement(node, element) { + var parentNode = node.parentNode; + while(parentNode) { + if (parentNode === element) { + return true; + } + parentNode = parentNode.parentNode; + } + return false; +} + function getElementRelativeOffset(element) { var offset = { left: 0, top: -window.pageYOffset }; var offsetParent = element.offsetParent; diff --git a/src/js/utils/object-utils.js b/src/js/utils/object-utils.js index 3c0bd7cb9..604a8b0fe 100644 --- a/src/js/utils/object-utils.js +++ b/src/js/utils/object-utils.js @@ -1,4 +1,4 @@ -function extend(object, updates) { +function merge(object, updates) { updates = updates || {}; for(var o in updates) { if (updates.hasOwnProperty(o)) { diff --git a/src/js/utils/selection-utils.js b/src/js/utils/selection-utils.js index 61c8315a0..504de0bb3 100644 --- a/src/js/utils/selection-utils.js +++ b/src/js/utils/selection-utils.js @@ -9,58 +9,51 @@ function getDirectionOfSelection(selection) { return SelectionDirection.SAME_NODE; } -function getCurrentSelectionNode(selection) { +function getSelectionElement(selection) { selection = selection || window.getSelection(); var node = getDirectionOfSelection(selection) === SelectionDirection.LEFT_TO_RIGHT ? selection.anchorNode : selection.focusNode; return node && (node.nodeType === 3 ? node.parentNode : node); } -function getCurrentSelectionRootNode() { - var node = getCurrentSelectionNode(); - var tag = node && node.tagName; +function getSelectionBlockElement() { + var element = getSelectionElement(); + var tag = element && element.tagName; while (tag && RootTags.indexOf(tag) === -1) { - if (node.contentEditable === 'true') { break; } // Stop traversing up dom when hitting an editor element - node = node.parentNode; - tag = node.tagName; + if (element.contentEditable === 'true') { break; } // Stop traversing up dom when hitting an editor element + element = element.parentNode; + tag = element.tagName; } - return node; + return element; } -function getCurrentSelectionTag() { - var node = getCurrentSelectionNode(); - return node ? node.tagName : null; +function getSelectionTagName() { + var element = getSelectionElement(); + return element ? element.tagName : null; } -function getCurrentSelectionRootTag() { - var node = getCurrentSelectionRootNode(); - return node ? node.tagName : null; +function getSelectionBlockTagName() { + var element = getSelectionBlockElement(); + return element ? element.tagName : null; } function tagsInSelection(selection) { - var node = getCurrentSelectionNode(selection); + var element = getSelectionElement(selection); var tags = []; if (!selection.isCollapsed) { - while(node) { - if (node.contentEditable === 'true') { break; } // Stop traversing up dom when hitting an editor element - if (node.tagName) { - tags.push(node.tagName); + while(element) { + if (element.contentEditable === 'true') { break; } // Stop traversing up dom when hitting an editor element + if (element.tagName) { + tags.push(element.tagName); } - node = node.parentNode; + element = element.parentNode; } } return tags; } function selectionIsInElement(selection, element) { - var node = selection.anchorNode, - parentNode = node && node.parentNode; - while(parentNode) { - if (parentNode === element) { - return true; - } - parentNode = parentNode.parentNode; - } - return false; + var node = selection.anchorNode; + return node && nodeIsDescendantOfElement(node, element); } function moveCursorToBeginningOfSelection(selection) { diff --git a/src/js/view.js b/src/js/view.js new file mode 100644 index 000000000..94b38c959 --- /dev/null +++ b/src/js/view.js @@ -0,0 +1,35 @@ +function View(options) { + this.tagName = options.tagName || 'div'; + this.classNames = options.classNames || []; + this.element = document.createElement(this.tagName); + this.element.className = this.classNames.join(' '); + this.container = options.container || document.body; + this.isShowing = false; +} + +View.prototype = { + show: function() { + var view = this; + if(!view.isShowing) { + view.container.appendChild(view.element); + view.isShowing = true; + return true; + } + }, + hide: function() { + var view = this; + if(view.isShowing) { + view.container.removeChild(view.element); + view.isShowing = false; + return true; + } + }, + addClass: function(className) { + this.classNames.push(className); + this.element.className = this.classNames.join(' '); + }, + removeClass: function(className) { + this.classNames.splice(this.classNames.indexOf(className), 1); + this.element.className = this.classNames.join(' '); + } +};