From ac7b1f0098465c66489a0d7d714d21a5dc299586 Mon Sep 17 00:00:00 2001 From: gdub22 Date: Mon, 11 Aug 2014 12:09:34 -0400 Subject: [PATCH] es6 modules --- .jshintrc | 4 +- dist/content-kit-editor.js | 4493 +++++++++-------- gulpfile.js | 29 +- package.json | 4 +- src/js/commands.js | 294 -- src/js/content-kit-compiler/compiler.js | 68 + src/js/content-kit-compiler/models/block.js | 31 + src/js/content-kit-compiler/models/embed.js | 45 + src/js/content-kit-compiler/models/image.js | 22 + src/js/content-kit-compiler/models/markup.js | 17 + src/js/content-kit-compiler/models/model.js | 20 + src/js/content-kit-compiler/models/text.js | 19 + .../parsers/html-parser.js | 172 + .../renderers/embeds/instagram.js | 7 + .../renderers/embeds/twitter.js | 7 + .../renderers/embeds/youtube.js | 19 + .../renderers/html-element-renderer.js | 98 + .../renderers/html-embed-renderer.js | 55 + .../renderers/html-renderer.js | 67 + .../types/default-types.js | 32 + src/js/content-kit-compiler/types/type-set.js | 55 + src/js/content-kit-compiler/types/type.js | 24 + src/js/content-kit-editor/commands/base.js | 13 + src/js/content-kit-editor/commands/bold.js | 22 + .../content-kit-editor/commands/commands.js | 40 + src/js/content-kit-editor/commands/embed.js | 53 + .../commands/format-block.js | 32 + src/js/content-kit-editor/commands/heading.js | 14 + src/js/content-kit-editor/commands/image.js | 58 + src/js/content-kit-editor/commands/italic.js | 14 + src/js/content-kit-editor/commands/link.js | 33 + src/js/content-kit-editor/commands/list.js | 24 + .../commands/ordered-list.js | 14 + src/js/content-kit-editor/commands/quote.js | 14 + .../content-kit-editor/commands/subheading.js | 14 + .../commands/text-format.js | 21 + .../commands/unordered-list.js | 14 + src/js/{ => content-kit-editor}/constants.js | 4 +- src/js/content-kit-editor/editor-factory.js | 42 + src/js/content-kit-editor/editor.js | 224 + .../utils/element-utils.js | 5 + src/js/content-kit-editor/utils/http-utils.js | 13 + .../utils/selection-utils.js | 7 + .../content-kit-editor/views/embed-intent.js | 113 + .../{ => content-kit-editor}/views/message.js | 7 +- src/js/content-kit-editor/views/prompt.js | 67 + .../views/text-format-toolbar.js | 41 + .../views/toolbar-button.js | 49 + src/js/content-kit-editor/views/toolbar.js | 119 + .../{ => content-kit-editor}/views/tooltip.js | 8 +- src/js/{ => content-kit-editor}/views/view.js | 2 + src/js/content-kit-utils/array-utils.js | 27 + src/js/content-kit-utils/node-utils.js | 70 + src/js/content-kit-utils/object-utils.js | 49 + src/js/content-kit-utils/string-utils.js | 43 + src/js/content-kit.js | 27 + src/js/editor.js | 250 - src/js/ext/content-kit-compiler.js | 1089 ---- src/js/index.js | 5 - src/js/utils/array-utils.js | 11 - src/js/utils/http-utils.js | 33 - src/js/utils/object-utils.js | 21 - src/js/views/embed-intent.js | 106 - src/js/views/prompt.js | 63 - src/js/views/toolbar-button.js | 50 - src/js/views/toolbar.js | 156 - 66 files changed, 4447 insertions(+), 4216 deletions(-) delete mode 100644 src/js/commands.js create mode 100644 src/js/content-kit-compiler/compiler.js create mode 100644 src/js/content-kit-compiler/models/block.js create mode 100644 src/js/content-kit-compiler/models/embed.js create mode 100644 src/js/content-kit-compiler/models/image.js create mode 100644 src/js/content-kit-compiler/models/markup.js create mode 100644 src/js/content-kit-compiler/models/model.js create mode 100644 src/js/content-kit-compiler/models/text.js create mode 100644 src/js/content-kit-compiler/parsers/html-parser.js create mode 100644 src/js/content-kit-compiler/renderers/embeds/instagram.js create mode 100644 src/js/content-kit-compiler/renderers/embeds/twitter.js create mode 100644 src/js/content-kit-compiler/renderers/embeds/youtube.js create mode 100644 src/js/content-kit-compiler/renderers/html-element-renderer.js create mode 100644 src/js/content-kit-compiler/renderers/html-embed-renderer.js create mode 100644 src/js/content-kit-compiler/renderers/html-renderer.js create mode 100644 src/js/content-kit-compiler/types/default-types.js create mode 100644 src/js/content-kit-compiler/types/type-set.js create mode 100644 src/js/content-kit-compiler/types/type.js create mode 100644 src/js/content-kit-editor/commands/base.js create mode 100644 src/js/content-kit-editor/commands/bold.js create mode 100644 src/js/content-kit-editor/commands/commands.js create mode 100644 src/js/content-kit-editor/commands/embed.js create mode 100644 src/js/content-kit-editor/commands/format-block.js create mode 100644 src/js/content-kit-editor/commands/heading.js create mode 100644 src/js/content-kit-editor/commands/image.js create mode 100644 src/js/content-kit-editor/commands/italic.js create mode 100644 src/js/content-kit-editor/commands/link.js create mode 100644 src/js/content-kit-editor/commands/list.js create mode 100644 src/js/content-kit-editor/commands/ordered-list.js create mode 100644 src/js/content-kit-editor/commands/quote.js create mode 100644 src/js/content-kit-editor/commands/subheading.js create mode 100644 src/js/content-kit-editor/commands/text-format.js create mode 100644 src/js/content-kit-editor/commands/unordered-list.js rename src/js/{ => content-kit-editor}/constants.js (89%) create mode 100644 src/js/content-kit-editor/editor-factory.js create mode 100644 src/js/content-kit-editor/editor.js rename src/js/{ => content-kit-editor}/utils/element-utils.js (90%) create mode 100644 src/js/content-kit-editor/utils/http-utils.js rename src/js/{ => content-kit-editor}/utils/selection-utils.js (88%) create mode 100644 src/js/content-kit-editor/views/embed-intent.js rename src/js/{ => content-kit-editor}/views/message.js (71%) create mode 100644 src/js/content-kit-editor/views/prompt.js create mode 100644 src/js/content-kit-editor/views/text-format-toolbar.js create mode 100644 src/js/content-kit-editor/views/toolbar-button.js create mode 100644 src/js/content-kit-editor/views/toolbar.js rename src/js/{ => content-kit-editor}/views/tooltip.js (82%) rename src/js/{ => content-kit-editor}/views/view.js (97%) create mode 100644 src/js/content-kit-utils/array-utils.js create mode 100644 src/js/content-kit-utils/node-utils.js create mode 100644 src/js/content-kit-utils/object-utils.js create mode 100644 src/js/content-kit-utils/string-utils.js create mode 100644 src/js/content-kit.js delete mode 100644 src/js/editor.js delete mode 100755 src/js/ext/content-kit-compiler.js delete mode 100644 src/js/index.js delete mode 100644 src/js/utils/array-utils.js delete mode 100644 src/js/utils/http-utils.js delete mode 100644 src/js/utils/object-utils.js delete mode 100644 src/js/views/embed-intent.js delete mode 100644 src/js/views/prompt.js delete mode 100644 src/js/views/toolbar-button.js delete mode 100644 src/js/views/toolbar.js diff --git a/.jshintrc b/.jshintrc index 97daaa015..63890519f 100644 --- a/.jshintrc +++ b/.jshintrc @@ -28,10 +28,11 @@ "noempty" : true, // Prohibit use of empty blocks. "nonew" : true, // Prohibit use of constructors for side-effects. "regexp" : true, // Prohibit `.` and `[^...]` in regular expressions. - //"undef" : true, // Require all non-global variables be declared before they are used. + "undef" : true, // Require all non-global variables be declared before they are used. //"unused" : true, // Warn when variables are created but not used. "trailing" : true, // Prohibit trailing whitespaces. "es3" : true, // Prohibit trailing commas for old IE + "esnext" : true, // Allow ES.next specific features such as `const` and `let`. // == Relaxing Options ================================================ // @@ -47,7 +48,6 @@ "debug" : false, // Allow debugger statements e.g. browser breakpoints. "eqnull" : false, // Tolerate use of `== null`. "es5" : false, // Allow EcmaScript 5 syntax. - "esnext" : false, // Allow ES.next specific features such as `const` and `let`. "evil" : false, // Tolerate use of `eval`. "expr" : false, // Tolerate `ExpressionStatement` as Programs. "funcscope" : false, // Tolerate declarations of variables inside of control structures while accessing them later from the outside. diff --git a/dist/content-kit-editor.js b/dist/content-kit-editor.js index fe05cc666..bc31dbdbd 100755 --- a/dist/content-kit-editor.js +++ b/dist/content-kit-editor.js @@ -3,1379 +3,16 @@ * @version 0.1.0 * @author Garth Poitras (http://garthpoitras.com/) * @license MIT - * Last modified: Aug 10, 2014 + * Last modified: Aug 11, 2014 */ (function(exports, document) { 'use strict'; -/** - * @namespace ContentKit - */ -var ContentKit = exports.ContentKit || {}; -exports.ContentKit = ContentKit; - -var Keycodes = { - BKSP : 8, - ENTER : 13, - ESC : 27, - DEL : 46 -}; - -var Regex = { - NEWLINE : /[\r\n]/g, - HTTP_PROTOCOL : /^https?:\/\//i, - HEADING_TAG : /^(H1|H2|H3|H4|H5|H6)$/i, - UL_START : /^[-*]\s/, - OL_START : /^1\.\s/ -}; - -var SelectionDirection = { - LEFT_TO_RIGHT : 1, - RIGHT_TO_LEFT : 2, - SAME_NODE : 3 -}; - -var ToolbarDirection = { - TOP : 1, - RIGHT : 2 -}; - -var Tags = { - PARAGRAPH : 'P', - HEADING : 'H2', - SUBHEADING : 'H3', - QUOTE : 'BLOCKQUOTE', - FIGURE : 'FIGURE', - LIST : 'UL', - ORDERED_LIST : 'OL', - LIST_ITEM : 'LI', - LINK : 'A', - BOLD : 'B', - ITALIC : 'I' -}; - -var RootTags = [ Tags.PARAGRAPH, Tags.HEADING, Tags.SUBHEADING, Tags.QUOTE, Tags.FIGURE, Tags.LIST, Tags.ORDERED_LIST ]; - -/** - * Converts an array-like object (i.e. NodeList) to Array - */ -function toArray(obj) { - var array = [], - i = obj.length >>> 0; // cast to Uint32 - while (i--) { - array[i] = obj[i]; - } - return array; -} - -function createDiv(className) { - var div = document.createElement('div'); - if (className) { - div.className = className; - } - return div; -} - -function hideElement(element) { - element.style.display = 'none'; -} - -function showElement(element) { - element.style.display = 'block'; -} - -function swapElements(elementToShow, elementToHide) { - hideElement(elementToHide); - showElement(elementToShow); -} - -function getEventTargetMatchingTag(tag, target, container) { - // Traverses up DOM from an event target to find the node matching specifed tag - while (target && target !== container) { - if (target.tagName === tag) { - return target; - } - target = target.parentNode; - } -} - -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; - var offsetParentPosition = window.getComputedStyle(offsetParent).position; - var offsetParentRect; - - if (offsetParentPosition === 'relative') { - offsetParentRect = offsetParent.getBoundingClientRect(); - offset.left = offsetParentRect.left; - offset.top = offsetParentRect.top; - } - return offset; -} - -function getElementComputedStyleNumericProp(element, prop) { - return parseFloat(window.getComputedStyle(element)[prop]); -} - -function positionElementToRect(element, rect, topOffset, leftOffset) { - var relativeOffset = getElementRelativeOffset(element); - var style = element.style; - var round = Math.round; - - topOffset = topOffset || 0; - leftOffset = leftOffset || 0; - style.left = round(rect.left - relativeOffset.left - leftOffset) + 'px'; - style.top = round(rect.top - relativeOffset.top - topOffset) + 'px'; -} - -function positionElementHorizontallyCenteredToRect(element, rect, topOffset) { - var horizontalCenter = (element.offsetWidth / 2) - (rect.width / 2); - positionElementToRect(element, rect, topOffset, horizontalCenter); -} - -function positionElementCenteredAbove(element, aboveElement) { - var elementMargin = getElementComputedStyleNumericProp(element, 'marginBottom'); - positionElementHorizontallyCenteredToRect(element, aboveElement.getBoundingClientRect(), element.offsetHeight + elementMargin); -} - -function positionElementCenteredBelow(element, belowElement) { - var elementMargin = getElementComputedStyleNumericProp(element, 'marginTop'); - positionElementHorizontallyCenteredToRect(element, belowElement.getBoundingClientRect(), -element.offsetHeight - elementMargin); -} - -function positionElementCenteredIn(element, inElement) { - var verticalCenter = (inElement.offsetHeight / 2) - (element.offsetHeight / 2); - positionElementHorizontallyCenteredToRect(element, inElement.getBoundingClientRect(), -verticalCenter); -} - -function positionElementToLeftOf(element, leftOfElement) { - var verticalCenter = (leftOfElement.offsetHeight / 2) - (element.offsetHeight / 2); - var elementMargin = getElementComputedStyleNumericProp(element, 'marginRight'); - positionElementToRect(element, leftOfElement.getBoundingClientRect(), -verticalCenter, element.offsetWidth + elementMargin); -} - -function positionElementToRightOf(element, rightOfElement) { - var verticalCenter = (rightOfElement.offsetHeight / 2) - (element.offsetHeight / 2); - var elementMargin = getElementComputedStyleNumericProp(element, 'marginLeft'); - var rightOfElementRect = rightOfElement.getBoundingClientRect(); - positionElementToRect(element, rightOfElementRect, -verticalCenter, -rightOfElement.offsetWidth - elementMargin); -} - -var HTTP = (function() { - - var head = document.head; - var uuid = 0; - - return { - get: function(url, callback) { - var request = new XMLHttpRequest(); - request.onload = function() { - callback(this.responseText); - }; - request.onerror = function(error) { - callback(null, error); - }; - request.open('GET', url); - request.send(); - }, - - jsonp: function(url, callback) { - var script = document.createElement('script'); - var name = '_jsonp_' + uuid++; - url += ( url.match(/\?/) ? '&' : '?' ) + 'callback=' + name; - script.src = url; - exports[name] = function(response) { - callback(JSON.parse(response)); - head.removeChild(script); - delete exports[name]; - }; - head.appendChild(script); - } - }; - -}()); - -function merge(object, updates) { - updates = updates || {}; - for(var o in updates) { - if (updates.hasOwnProperty(o)) { - object[o] = updates[o]; - } - } - return object; -} - -function inherits(Subclass, Superclass) { - Subclass._super = Superclass; - Subclass.prototype = Object.create(Superclass.prototype, { - constructor: { - value: Subclass, - enumerable: false, - writable: true, - configurable: true - } - }); -} - -function getDirectionOfSelection(selection) { - var node = selection.anchorNode; - var position = node && node.compareDocumentPosition(selection.focusNode); - if (position & Node.DOCUMENT_POSITION_FOLLOWING) { - return SelectionDirection.LEFT_TO_RIGHT; - } else if (position & Node.DOCUMENT_POSITION_PRECEDING) { - return SelectionDirection.RIGHT_TO_LEFT; - } - return SelectionDirection.SAME_NODE; -} - -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 getSelectionBlockElement(selection) { - selection = selection || window.getSelection(); - var element = getSelectionElement(); - var tag = element && element.tagName; - while (tag && RootTags.indexOf(tag) === -1) { - if (element.contentEditable === 'true') { break; } // Stop traversing up dom when hitting an editor element - element = element.parentNode; - tag = element.tagName; - } - return element; -} - -function getSelectionTagName() { - var element = getSelectionElement(); - return element ? element.tagName : null; -} - -function getSelectionBlockTagName() { - var element = getSelectionBlockElement(); - return element ? element.tagName : null; -} - -function tagsInSelection(selection) { - var element = getSelectionElement(selection); - var tags = []; - if (!selection.isCollapsed) { - while(element) { - if (element.contentEditable === 'true') { break; } // Stop traversing up dom when hitting an editor element - if (element.tagName) { - tags.push(element.tagName); - } - element = element.parentNode; - } - } - return tags; -} - -function selectionIsInElement(selection, element) { - var node = selection.anchorNode; - return node && nodeIsDescendantOfElement(node, element); -} - -function selectionIsEditable(selection) { - var el = getSelectionBlockElement(selection); - return el.contentEditable !== 'false'; -} - -/* -function saveSelection() { - var sel = window.getSelection(); - var ranges = [], i; - if (sel.rangeCount) { - var rangeCount = sel.rangeCount; - for (i = 0; i < rangeCount; i++) { - ranges.push(sel.getRangeAt(i)); - } - } - return ranges; -} - -function restoreSelection(savedSelection) { - var sel = window.getSelection(); - var len = savedSelection.length, i; - sel.removeAllRanges(); - for (i = 0; i < len; i++) { - sel.addRange(savedSelection[i]); - } -} -*/ - -function moveCursorToBeginningOfSelection(selection) { - var range = document.createRange(); - var node = selection.anchorNode; - range.setStart(node, 0); - range.setEnd(node, 0); - selection.removeAllRanges(); - selection.addRange(range); -} - -function restoreRange(range) { - var selection = window.getSelection(); - selection.removeAllRanges(); - selection.addRange(range); -} - -function selectNode(node) { - var range = document.createRange(); - var selection = window.getSelection(); - range.setStart(node, 0); - range.setEnd(node, node.length); - selection.removeAllRanges(); - 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; - } - }, - focus: function() { - this.element.focus(); - }, - 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 EmbedIntent = (function() { - - function EmbedIntent(options) { - var embedIntent = this; - var rootElement = options.rootElement; - options.tagName = 'button'; - options.classNames = ['ck-embed-intent-btn']; - View.call(embedIntent, options); - - embedIntent.editorContext = options.editorContext; - embedIntent.element.title = 'Insert image or embed...'; - embedIntent.element.addEventListener('mouseup', function(e) { - if (embedIntent.isActive) { - embedIntent.deactivate(); - } else { - embedIntent.activate(); - } - e.stopPropagation(); - }); - - embedIntent.toolbar = new Toolbar({ embedIntent: embedIntent, editor: embedIntent.editorContext, commands: options.commands, direction: ToolbarDirection.RIGHT }); - embedIntent.isActive = false; - - function embedIntentHandler() { - var blockElement = getSelectionBlockElement(); - var blockElementContent = blockElement && blockElement.innerHTML; - if (blockElementContent === '' || blockElementContent === '
') { - embedIntent.showAt(blockElement); - } else { - embedIntent.hide(); - } - } - - rootElement.addEventListener('keyup', embedIntentHandler); - - 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) { - embedIntent.hide(); - } - }); - - 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.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; - } - }; - - var loading = createDiv('div'); - loading.className = 'ck-embed-loading'; - loading.innerHTML = 'LOADING'; - EmbedIntent.prototype.showLoading = function() { - this.hide(); - document.body.appendChild(loading); - positionElementCenteredIn(loading, this.atNode); - }; - - EmbedIntent.prototype.hideLoading = function() { - document.body.removeChild(loading); - }; - - - return EmbedIntent; -}()); - -function Message(options) { - options = options || {}; - options.classNames = ['ck-message']; - View.call(this, options); -} -inherits(Message, View); - -Message.prototype.show = function(message) { - var messageView = this; - messageView.element.innerHTML = message; - Message._super.prototype.show.call(messageView); - setTimeout(function() { - messageView.hide(); - }, 3000); -}; - -var Prompt = (function() { - - var container = document.body; - var hiliter = createDiv('ck-editor-hilite'); - - function Prompt(options) { - 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 && prompt.range && !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.show = function(callback) { - var prompt = this; - var element = prompt.element; - var selection = window.getSelection(); - var range = selection && selection.rangeCount && selection.getRangeAt(0); - element.value = null; - prompt.range = range || null; - if (range) { - container.appendChild(hiliter); - positionHiliteRange(prompt.range); - setTimeout(function(){ element.focus(); }); // defer focus (disrupts mouseup events) - if (callback) { prompt.onComplete = callback; } - } - }; - - Prompt.prototype.hide = function() { - if (hiliter.parentNode) { - container.removeChild(hiliter); - } - }; - - function positionHiliteRange(range) { - var rect = range.getBoundingClientRect(); - var style = hiliter.style; - style.width = rect.width + 'px'; - style.height = rect.height + 'px'; - positionElementToRect(hiliter, rect); - } - - return Prompt; -}()); - -var ToolbarButton = (function() { - - var buttonClassName = 'ck-toolbar-btn'; - - function ToolbarButton(options) { - var button = this; - var toolbar = options.toolbar; - var command = options.command; - var prompt = command.prompt; - var element = document.createElement('button'); - - if(typeof command === 'string') { - command = Command.index[command]; - } - - button.element = element; - button.command = command; - button.isActive = false; - - element.title = command.name; - element.className = buttonClassName; - element.innerHTML = command.button; - element.addEventListener('click', function(e) { - if (!button.isActive && prompt) { - toolbar.displayPrompt(prompt); - } else { - command.exec(); - } - }); - } - - ToolbarButton.prototype = { - setActive: function() { - var button = this; - if (!button.isActive) { - button.element.className = buttonClassName + ' active'; - button.isActive = true; - } - }, - setInactive: function() { - var button = this; - if (button.isActive) { - button.element.className = buttonClassName; - button.isActive = false; - } - } - }; - - return ToolbarButton; -}()); - -var Toolbar = (function() { - - function Toolbar(options) { - var toolbar = this; - var commands = options.commands; - var commandCount = commands && commands.length; - var i, button, command; - toolbar.editor = options.editor || null; - toolbar.embedIntent = options.embedIntent || null; - toolbar.direction = options.direction || ToolbarDirection.TOP; - options.classNames = ['ck-toolbar']; - if (toolbar.direction === ToolbarDirection.RIGHT) { - options.classNames.push('right'); - } - - View.call(toolbar, options); - - toolbar.activePrompt = null; - toolbar.buttons = []; - - toolbar.promptContainerElement = createDiv('ck-toolbar-prompt'); - toolbar.buttonContainerElement = createDiv('ck-toolbar-buttons'); - toolbar.element.appendChild(toolbar.promptContainerElement); - toolbar.element.appendChild(toolbar.buttonContainerElement); - - for(i = 0; i < commandCount; i++) { - this.addCommand(commands[i]); - } - - // Closes prompt if displayed when changing selection - document.addEventListener('mouseup', function() { - toolbar.dismissPrompt(); - }); - } - 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(); - } - }; - - Toolbar.prototype.addCommand = function(command) { - command.editorContext = this.editor; - command.embedIntent = this.embedIntent; - var button = new ToolbarButton({ command: command, toolbar: this }); - this.buttons.push(button); - this.buttonContainerElement.appendChild(button.element); - }; - - 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; - }; - - Toolbar.prototype.dismissPrompt = function() { - 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) { - button.setActive(); - } else { - button.setInactive(); - } - } - } - - 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 || !selectionIsEditable(selection) || selection.toString().trim() === '' || !selectionIsInElement(selection, toolbar.rootElement)) { - toolbar.hide(); - } else { - toolbar.updateForSelection(selection); - } - }; - - return TextFormatToolbar; -}()); - -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); - -Tooltip.prototype.showMessage = function(message, element) { - var tooltip = this; - var tooltipElement = tooltip.element; - tooltipElement.innerHTML = message; - tooltip.show(); - positionElementCenteredBelow(tooltipElement, element); -}; - -Tooltip.prototype.showLink = function(link, element) { - var message = '' + link + ''; - this.showMessage(message, element); -}; - -function createCommandIndex(commands) { - var index = {}; - var len = commands.length, i, command; - for(i = 0; i < len; i++) { - command = commands[i]; - index[command.name] = command; - } - return index; -} - -function Command(options) { - var command = this; - var name = options.name; - var prompt = options.prompt; - command.name = name; - command.button = options.button || name; - command.editorContext = null; - if (prompt) { command.prompt = prompt; } -} -Command.prototype.exec = function(){}; - -function TextFormatCommand(options) { - Command.call(this, options); - this.tag = options.tag.toUpperCase(); - this.action = options.action || this.name; - this.removeAction = options.removeAction || this.action; -} -inherits(TextFormatCommand, Command); - -TextFormatCommand.prototype = { - exec: function(value) { - document.execCommand(this.action, false, value || null); - }, - unexec: function(value) { - document.execCommand(this.removeAction, false, value || null); - } -}; - -function BoldCommand() { - TextFormatCommand.call(this, { - name: 'bold', - tag: Tags.BOLD, - button: '' - }); -} -inherits(BoldCommand, TextFormatCommand); -BoldCommand.prototype.exec = function() { - // Don't allow executing bold command on heading tags - if (!Regex.HEADING_TAG.test(getSelectionBlockTagName())) { - BoldCommand._super.prototype.exec.call(this); - } -}; - -function ItalicCommand() { - TextFormatCommand.call(this, { - name: 'italic', - tag: Tags.ITALIC, - button: '' - }); -} -inherits(ItalicCommand, TextFormatCommand); - -function LinkCommand() { - TextFormatCommand.call(this, { - name: 'link', - tag: Tags.LINK, - action: 'createLink', - removeAction: 'unlink', - button: '', - prompt: new Prompt({ - command: this, - placeholder: 'Enter a url, press return...' - }) - }); -} -inherits(LinkCommand, TextFormatCommand); -LinkCommand.prototype.exec = function(url) { - if(this.tag === getSelectionTagName()) { - this.unexec(); - } else { - if (!Regex.HTTP_PROTOCOL.test(url)) { - url = 'http://' + url; - } - LinkCommand._super.prototype.exec.call(this, url); - } -}; - -function FormatBlockCommand(options) { - options.action = 'formatBlock'; - TextFormatCommand.call(this, options); -} -inherits(FormatBlockCommand, TextFormatCommand); -FormatBlockCommand.prototype.exec = function() { - var tag = this.tag; - // Brackets neccessary for certain browsers - var value = '<' + tag + '>'; - var blockElement = getSelectionBlockElement(); - // Allow block commands to be toggled back to a paragraph - 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(blockElement.textContent); - blockElement.parentNode.insertBefore(flatNode, blockElement); - blockElement.parentNode.removeChild(blockElement); - selectNode(flatNode); - } - - FormatBlockCommand._super.prototype.exec.call(this, value); -}; - -function QuoteCommand() { - FormatBlockCommand.call(this, { - name: 'quote', - tag: Tags.QUOTE, - button: '' - }); -} -inherits(QuoteCommand, FormatBlockCommand); - -function HeadingCommand() { - FormatBlockCommand.call(this, { - name: 'heading', - tag: Tags.HEADING, - button: '1' - }); -} -inherits(HeadingCommand, FormatBlockCommand); - -function SubheadingCommand() { - FormatBlockCommand.call(this, { - name: 'subheading', - tag: Tags.SUBHEADING, - button: '2' - }); -} -inherits(SubheadingCommand, FormatBlockCommand); - -function ListCommand(options) { - TextFormatCommand.call(this, options); -} -inherits(ListCommand, TextFormatCommand); -ListCommand.prototype.exec = function() { - ListCommand._super.prototype.exec.call(this); - - // After creation, lists need to be unwrapped from the default formatter P tag - var listElement = getSelectionBlockElement(); - var wrapperNode = listElement.parentNode; - if (wrapperNode.firstChild === listElement) { - var editorNode = wrapperNode.parentNode; - editorNode.insertBefore(listElement, wrapperNode); - editorNode.removeChild(wrapperNode); - selectNode(listElement); - } -}; - -function UnorderedListCommand() { - ListCommand.call(this, { - name: 'list', - tag: Tags.LIST, - action: 'insertUnorderedList' - }); -} -inherits(UnorderedListCommand, ListCommand); - -function OrderedListCommand() { - ListCommand.call(this, { - name: 'ordered list', - tag: Tags.ORDERED_LIST, - action: 'insertOrderedList' - }); -} -inherits(OrderedListCommand, ListCommand); - -TextFormatCommand.all = [ - new BoldCommand(), - new ItalicCommand(), - new LinkCommand(), - new QuoteCommand(), - new HeadingCommand(), - new SubheadingCommand() -]; - -TextFormatCommand.index = createCommandIndex(TextFormatCommand.all); - - -function EmbedCommand(options) { - Command.call(this, options); -} -inherits(EmbedCommand, Command); - -function ImageEmbedCommand(options) { - EmbedCommand.call(this, { - name: 'image', - button: '' - }); - if (window.XHRFileUploader) { - this.uploader = new XHRFileUploader({ url: '/upload', maxFileSize: 5000000 }); - } -} -inherits(ImageEmbedCommand, EmbedCommand); - -ImageEmbedCommand.prototype = { - exec: function() { - ImageEmbedCommand._super.prototype.exec.call(this); - var clickEvent = new MouseEvent('click', { bubbles: false }); - if (!this.fileInput) { - var command = this; - var fileInput = this.fileInput = document.createElement('input'); - fileInput.type = 'file'; - fileInput.accept = 'image/*'; - fileInput.className = 'ck-file-input'; - fileInput.addEventListener('change', function(e) { - command.handleFile(e); - }); - document.body.appendChild(fileInput); - } - this.fileInput.dispatchEvent(clickEvent); - }, - handleFile: function(e) { - var fileInput = e.target; - var editor = this.editorContext; - var embedIntent = this.embedIntent; - - embedIntent.showLoading(); - this.uploader.upload({ - fileInput: fileInput, - complete: function(response, error) { - embedIntent.hideLoading(); - if (error || !response || !response.url) { - return new Message().show(error.message || 'Error uploading image'); - } - var imageModel = new ContentKit.ImageModel({ src: response.url }); - var index = editor.getCurrentBlockIndex(); - editor.insertBlockAt(imageModel, index); - editor.syncVisualAt(index); - } - }); - fileInput.value = null; // reset file input - // TODO: client-side render while uploading - } -}; - -function OEmbedCommand(options) { - EmbedCommand.call(this, { - name: 'oEmbed', - button: '', - prompt: new Prompt({ - command: this, - placeholder: 'Paste a YouTube or Twitter url...' - }) - }); -} -inherits(OEmbedCommand, EmbedCommand); - -OEmbedCommand.prototype.exec = function(url) { - var command = this; - var editorContext = command.editorContext; - var index = editorContext.getCurrentBlockIndex(); - var oEmbedEndpoint = 'http://noembed.com/embed?url='; - - command.embedIntent.showLoading(); - if (!Regex.HTTP_PROTOCOL.test(url)) { - url = 'http://' + url; - } - - HTTP.get(oEmbedEndpoint + url, function(responseText, error) { - command.embedIntent.hideLoading(); - if (error) { - new Message().show('Embed error: status code ' + error.currentTarget.status); - } else { - var json = JSON.parse(responseText); - if (json.error) { - new Message().show('Embed error: ' + json.error); - } else { - var embedModel = new ContentKit.EmbedModel(json); - //if (!embedModel.attributes.provider_id) { - // new Message().show('Embed error: "' + embedModel.attributes.provider_name + '" embeds are not supported at this time'); - //} else { - editorContext.insertBlockAt(embedModel, index); - editorContext.syncVisualAt(index); - //} - } - } - }); -}; - -EmbedCommand.all = [ - new ImageEmbedCommand(), - new OEmbedCommand() -]; - -EmbedCommand.index = createCommandIndex(EmbedCommand.all); - -ContentKit.Editor = (function() { - - // Default `Editor` options - var defaults = { - defaultFormatter: Tags.PARAGRAPH, - placeholder: 'Write here...', - spellcheck: true, - autofocus: true, - textFormatCommands: TextFormatCommand.all, - embedCommands: EmbedCommand.all - }; - - var editorClassName = 'ck-editor'; - var editorClassNameRegExp = new RegExp(editorClassName); - - /** - * Publically expose this class which sets up indiviual `Editor` classes - * depending if user passes string selector, Node, or NodeList - */ - function EditorFactory(element, options) { - var editors = []; - var elements, elementsLen, i; - - if (typeof element === 'string') { - elements = document.querySelectorAll(element); - } else if (element && element.length) { - elements = element; - } else if (element) { - elements = [element]; - } - - if (elements) { - options = merge(defaults, options); - elementsLen = elements.length; - for (i = 0; i < elementsLen; i++) { - editors.push(new Editor(elements[i], options)); - } - } - - return editors.length > 1 ? editors : editors[0]; - } - - /** - * @class Editor - * An individual Editor - * @param element `Element` node - * @param options hash of options - */ - function Editor(element, options) { - var editor = this; - merge(editor, options); - - if (element) { - var className = element.className; - var dataset = element.dataset; - - if (!editorClassNameRegExp.test(className)) { - className += (className ? ' ' : '') + editorClassName; - } - element.className = className; - - if (!dataset.placeholder) { - dataset.placeholder = editor.placeholder; - } - if(!editor.spellcheck) { - element.spellcheck = false; - } - - element.setAttribute('contentEditable', true); - editor.element = element; - - var compiler = editor.compiler = options.compiler || new ContentKit.Compiler(); - editor.syncModel(); - - bindTypingEvents(editor); - bindPasteEvents(editor); - - editor.textFormatToolbar = new TextFormatToolbar({ rootElement: element, commands: editor.textFormatCommands }); - var linkTooltips = new Tooltip({ rootElement: element, showForTag: Tags.LINK }); - - if(editor.embedCommands) { - // NOTE: must come after bindTypingEvents so those keyup handlers are executed first. - // TODO: manage event listener order - var embedIntent = new EmbedIntent({ - editorContext: editor, - commands: editor.embedCommands, - rootElement: element - }); - - if (editor.imageServiceUrl) { - // TODO: lookup by name - editor.embedCommands[0].uploader.url = editor.imageServiceUrl; - } - } - - if(editor.autofocus) { element.focus(); } - } - } - - Editor.prototype.syncModel = function() { - this.model = this.compiler.parse(this.element.innerHTML); - }; - - Editor.prototype.syncModelAt = function(index) { - var blockElements = toArray(this.element.children); - var parsedBlockModel = this.compiler.parser.parseBlock(blockElements[index]); - this.model[index] = parsedBlockModel; - }; - - Editor.prototype.syncVisualAt = function(index) { - var blockModel = this.model[index]; - var html = this.compiler.render([blockModel]); - var blockElements = toArray(this.element.children); - var element = blockElements[index]; - element.innerHTML = html; - runAfterRenderHooks(element, blockModel); - }; - - Editor.prototype.getCurrentBlockIndex = function() { - var selectionEl = getSelectionBlockElement(); - var blockElements = toArray(this.element.children); - return blockElements.indexOf(selectionEl); - }; - - Editor.prototype.insertBlock = function(model) { - this.insertBlockAt(model, this.getCurrentBlockIndex()); - }; - - Editor.prototype.insertBlockAt = function(model, index) { - model = model || new ContentKit.TextModel(); - this.model.splice(index, 0, model); - }; - - Editor.prototype.addTextFormat = function(opts) { - var command = new TextFormatCommand(opts); - this.compiler.registerMarkupType(new ContentKit.Type({ - name : opts.name, - tag : opts.tag || opts.name - })); - this.textFormatCommands.push(command); - this.textFormatToolbar.addCommand(command); - }; - - Editor.prototype.willRenderType = function(type, renderer) { - this.compiler.renderer.willRenderType(type, renderer); - }; - - 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 === getSelectionBlockTagName()) { - document.execCommand('formatBlock', false, editor.defaultFormatter); - e.stopPropagation(); - } - } - }); - - // Creates unordered list when block starts with '- ', or ordered if starts with '1. ' - editorEl.addEventListener('keyup', function(e) { - var selectedText = window.getSelection().anchorNode.textContent, - selection, selectionNode, command, replaceRegex; - - if (Tags.LIST_ITEM !== getSelectionTagName()) { - if (Regex.UL_START.test(selectedText)) { - command = new UnorderedListCommand(); - replaceRegex = Regex.UL_START; - } else if (Regex.OL_START.test(selectedText)) { - command = new OrderedListCommand(); - replaceRegex = Regex.OL_START; - } - - if (command) { - command.exec(); - selection = window.getSelection(); - selectionNode = selection.anchorNode; - selectionNode.textContent = selectedText.replace(replaceRegex, ''); - moveCursorToBeginningOfSelection(selection); - e.stopPropagation(); - } - } - }); - - // Assure there is always a supported root tag, and not empty text nodes or divs. - editorEl.addEventListener('keyup', function() { - if (this.innerHTML.length && RootTags.indexOf(getSelectionBlockTagName()) === -1) { - document.execCommand('formatBlock', false, editor.defaultFormatter); - } - }); - - // Experimental: Live update - sync model with textual content as you type - editorEl.addEventListener('keyup', function(e) { - if (editor.model && editor.model.length) { - var index = editor.getCurrentBlockIndex(); - if (editor.model[index].type === 1) { - editor.syncModelAt(index); - } - } - }); - } - - var afterRenderHooks = []; - Editor.prototype.afterRender = function(callback) { - if ('function' === typeof callback) { - afterRenderHooks.push(callback); - } - }; - - function runAfterRenderHooks(element, blockModel) { - for (var i = 0, len = afterRenderHooks.length; i < len; i++) { - afterRenderHooks[i].call(null, element, blockModel); - } - } - - function bindPasteEvents(editor) { - editor.element.addEventListener('paste', function(e) { - var data = e.clipboardData, plainText; - e.preventDefault(); - if(data && data.getData) { - plainText = data.getData('text/plain'); - var formattedContent = plainTextToBlocks(plainText, editor.defaultFormatter); - document.execCommand('insertHTML', false, formattedContent); - } - }); - } - - function plainTextToBlocks(plainText, blockTag) { - var blocks = plainText.split(Regex.NEWLINE), - len = blocks.length, - block, openTag, closeTag, content, i; - if(len < 2) { - return plainText; - } else { - content = ''; - openTag = '<' + blockTag + '>'; - closeTag = ''; - for(i=0; i (http://garthpoitras.com/) - * @license MIT - * Last modified: Aug 9, 2014 - */ - -(function(window, document, define, undefined) { - define("content-kit", - ["./types/type","./models/block","./models/text","./models/image","./models/embed","./compiler","./parsers/html-parser","./renderers/html-renderer","exports"], - function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __exports__) { + ["./content-kit-compiler/types/type","./content-kit-compiler/models/block","./content-kit-compiler/models/text","./content-kit-compiler/models/image","./content-kit-compiler/models/embed","./content-kit-compiler/compiler","./content-kit-compiler/parsers/html-parser","./content-kit-compiler/renderers/html-renderer","./content-kit-editor/editor-factory","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __exports__) { "use strict"; var Type = __dependency1__["default"]; var BlockModel = __dependency2__["default"]; @@ -1385,6 +22,7 @@ define("content-kit", var Compiler = __dependency6__["default"]; var HTMLParser = __dependency7__["default"]; var HTMLRenderer = __dependency8__["default"]; + var EditorFactory = __dependency9__["default"]; /** * @namespace ContentKit @@ -1400,11 +38,12 @@ define("content-kit", ContentKit.Compiler = Compiler; ContentKit.HTMLParser = HTMLParser; ContentKit.HTMLRenderer = HTMLRenderer; + ContentKit.Editor = EditorFactory; __exports__["default"] = ContentKit; }); -define("compiler", - ["./parsers/html-parser","./renderers/html-renderer","./types/type","./types/default-types","../utils/object-utils","exports"], +define("content-kit-compiler/compiler", + ["./parsers/html-parser","./renderers/html-renderer","./types/type","./types/default-types","../../content-kit-utils/object-utils","exports"], function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __exports__) { "use strict"; var HTMLParser = __dependency1__["default"]; @@ -1412,7 +51,7 @@ define("compiler", var Type = __dependency3__["default"]; var DefaultBlockTypeSet = __dependency4__.DefaultBlockTypeSet; var DefaultMarkupTypeSet = __dependency4__.DefaultMarkupTypeSet; - var merge = __dependency5__.merge; + var mergeWithOptions = __dependency5__.mergeWithOptions; /** * @class Compiler @@ -1429,7 +68,7 @@ define("compiler", markupTypes : DefaultMarkupTypeSet, includeTypeNames : false // true will output type_name: 'TEXT' etc. when parsing for easier debugging }; - merge(this, defaults, options); + mergeWithOptions(this, defaults, options); // Reference the compiler settings parser.blockTypes = renderer.blockTypes = this.blockTypes; @@ -1477,293 +116,849 @@ define("compiler", __exports__["default"] = Compiler; }); -define("models/block", - ["./model","../utils/object-utils","exports"], - function(__dependency1__, __dependency2__, __exports__) { +define("content-kit-editor/constants", + ["exports"], + function(__exports__) { "use strict"; - var Model = __dependency1__["default"]; - var inherit = __dependency2__.inherit; + var Keycodes = { + BKSP : 8, + ENTER : 13, + ESC : 27, + DEL : 46 + }; + + var RegEx = { + NEWLINE : /[\r\n]/g, + HTTP_PROTOCOL : /^https?:\/\//i, + HEADING_TAG : /^(H1|H2|H3|H4|H5|H6)$/i, + UL_START : /^[-*]\s/, + OL_START : /^1\.\s/ + }; + + var SelectionDirection = { + LEFT_TO_RIGHT : 1, + RIGHT_TO_LEFT : 2, + SAME_NODE : 3 + }; + + var ToolbarDirection = { + TOP : 1, + RIGHT : 2 + }; + + var Tags = { + PARAGRAPH : 'P', + HEADING : 'H2', + SUBHEADING : 'H3', + QUOTE : 'BLOCKQUOTE', + FIGURE : 'FIGURE', + LIST : 'UL', + ORDERED_LIST : 'OL', + LIST_ITEM : 'LI', + LINK : 'A', + BOLD : 'B', + ITALIC : 'I' + }; + + var RootTags = [ Tags.PARAGRAPH, Tags.HEADING, Tags.SUBHEADING, Tags.QUOTE, Tags.FIGURE, Tags.LIST, Tags.ORDERED_LIST ]; + + __exports__.Keycodes = Keycodes; + __exports__.RegEx = RegEx; + __exports__.SelectionDirection = SelectionDirection; + __exports__.ToolbarDirection = ToolbarDirection; + __exports__.Tags = Tags; + __exports__.RootTags = RootTags; + }); +define("content-kit-editor/editor-factory", + ["./editor","./commands/commands","./constants","../content-kit-utils/object-utils","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __exports__) { + "use strict"; + var Editor = __dependency1__["default"]; + var TextFormatCommands = __dependency2__.TextFormatCommands; + var EmbedCommands = __dependency2__.EmbedCommands; + var Tags = __dependency3__.Tags; + var merge = __dependency4__.merge; + + var defaults = { + defaultFormatter: Tags.PARAGRAPH, + placeholder: 'Write here...', + spellcheck: true, + autofocus: true, + textFormatCommands: TextFormatCommands.all, + embedCommands: EmbedCommands.all + }; /** - * Ensures block markups at the same index are always in a specific order. - * For example, so all bold links are consistently marked up - * as text instead of text + * Publically expose this class which sets up indiviual `Editor` classes + * depending if user passes string selector, Node, or NodeList */ - function sortBlockMarkups(markups) { - return markups.sort(function(a, b) { - if (a.start === b.start && a.end === b.end) { - return b.type - a.type; + function EditorFactory(element, options) { + var editors = []; + var elements, elementsLen, i; + + if (typeof element === 'string') { + elements = document.querySelectorAll(element); + } else if (element && element.length) { + elements = element; + } else if (element) { + elements = [element]; + } + + if (elements) { + options = merge(defaults, options); + elementsLen = elements.length; + for (i = 0; i < elementsLen; i++) { + editors.push(new Editor(elements[i], options)); } - return 0; - }); - } + } - /** - * @class BlockModel - * @constructor - * @extends Model - */ - function BlockModel(options) { - options = options || {}; - Model.call(this, options); - this.value = options.value || ''; - this.markup = sortBlockMarkups(options.markup || []); + return editors.length > 1 ? editors : editors[0]; } - inherit(BlockModel, Model); - __exports__["default"] = BlockModel; + __exports__["default"] = EditorFactory; }); -define("models/embed", - ["../utils/object-utils","../models/model","../types/type","exports"], - function(__dependency1__, __dependency2__, __dependency3__, __exports__) { +define("content-kit-editor/editor", + ["./views/text-format-toolbar","./views/tooltip","./views/embed-intent","./commands/unordered-list","./commands/ordered-list","./commands/text-format","./constants","./utils/selection-utils","../content-kit-compiler/compiler","../content-kit-compiler/models/text","../content-kit-compiler/types/type","../content-kit-utils/array-utils","../content-kit-utils/object-utils","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __dependency11__, __dependency12__, __dependency13__, __exports__) { "use strict"; - var inherit = __dependency1__.inherit; - var Model = __dependency2__["default"]; - var Type = __dependency3__["default"]; + var TextFormatToolbar = __dependency1__["default"]; + var Tooltip = __dependency2__["default"]; + var EmbedIntent = __dependency3__["default"]; + var UnorderedListCommand = __dependency4__["default"]; + var OrderedListCommand = __dependency5__["default"]; + var TextFormatCommand = __dependency6__["default"]; + var Tags = __dependency7__.Tags; + var RootTags = __dependency7__.RootTags; + var Keycodes = __dependency7__.Keycodes; + var RegEx = __dependency7__.RegEx; + var moveCursorToBeginningOfSelection = __dependency8__.moveCursorToBeginningOfSelection; + var getSelectionTagName = __dependency8__.getSelectionTagName; + var getSelectionBlockElement = __dependency8__.getSelectionBlockElement; + var getSelectionBlockTagName = __dependency8__.getSelectionBlockTagName; + var Compiler = __dependency9__["default"]; + var TextModel = __dependency10__["default"]; + var Type = __dependency11__["default"]; + var toArray = __dependency12__.toArray; + var merge = __dependency13__.merge; + + var editorClassName = 'ck-editor'; + var editorClassNameRegExp = new RegExp(editorClassName); + var afterRenderHooks = []; + + function plainTextToBlocks(plainText, blockTag) { + var blocks = plainText.split(RegEx.NEWLINE), + len = blocks.length, + block, openTag, closeTag, content, i; + if(len < 2) { + return plainText; + } else { + content = ''; + openTag = '<' + blockTag + '>'; + closeTag = ''; + for(i=0; i>> 0; // cast to Uint32 + while (i--) { + array[i] = obj[i]; } - if (attributes) { - this.attributes = attributes; + return array; + } + + /** + * Computes the sum of values in a (sparse) array + */ + function sumSparseArray(array) { + var sum = 0, i; + for (i in array) { // 'for in' is better for sparse arrays + if (array.hasOwnProperty(i)) { + sum += array[i]; + } } + return sum; } - __exports__["default"] = Model; + __exports__.toArray = toArray; + __exports__.sumSparseArray = sumSparseArray; }); -define("models/text", - ["./block","../types/type","../utils/object-utils","exports"], - function(__dependency1__, __dependency2__, __dependency3__, __exports__) { +define("content-kit-utils/node-utils", + ["./string-utils","./array-utils","exports"], + function(__dependency1__, __dependency2__, __exports__) { "use strict"; - var BlockModel = __dependency1__["default"]; - var Type = __dependency2__["default"]; - var inherit = __dependency3__.inherit; + var sanitizeWhitespace = __dependency1__.sanitizeWhitespace; + var toArray = __dependency2__.toArray; /** - * @class TextModel - * @constructor - * @extends BlockModel - * A simple BlockModel subclass representing a paragraph of text + * A document instance separate from the page's document. (if browser supports it) + * Prevents images, scripts, and styles from executing while parsing nodes. */ - function TextModel(options) { - options = options || {}; - options.type = Type.TEXT.id; - options.type_name = Type.TEXT.name; - BlockModel.call(this, options); + var standaloneDocument = (function() { + var implementation = document.implementation; + var createHTMLDocument = implementation.createHTMLDocument; + + if (createHTMLDocument) { + return createHTMLDocument.call(implementation, ''); + } + return document; + })(); + + /** + * document.createElement with our lean, standalone document + */ + function createElement(type) { + return standaloneDocument.createElement(type); } - inherit(TextModel, BlockModel); - __exports__["default"] = TextModel; - }); -define("parsers/html-parser", - ["../models/block","../models/markup","../types/default-types","../utils/object-utils","../utils/array-utils","../utils/string-utils","../utils/node-utils","exports"], - function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __exports__) { - "use strict"; - var BlockModel = __dependency1__["default"]; - var MarkupModel = __dependency2__["default"]; - var DefaultBlockTypeSet = __dependency3__.DefaultBlockTypeSet; - var DefaultMarkupTypeSet = __dependency3__.DefaultMarkupTypeSet; - var merge = __dependency4__.merge; - var toArray = __dependency5__.toArray; - var trim = __dependency6__.trim; - var trimLeft = __dependency6__.trimLeft; - var sanitizeWhitespace = __dependency6__.sanitizeWhitespace; - var createElement = __dependency7__.createElement; - var DOMParsingNode = __dependency7__.DOMParsingNode; - var textOfNode = __dependency7__.textOfNode; - var unwrapNode = __dependency7__.unwrapNode; - var attributesForNode = __dependency7__.attributesForNode; + /** + * A reusable DOM Node for parsing html content. + */ + var DOMParsingNode = createElement('div'); /** - * Gets the last block in the set or creates and return a default block if none exist yet. + * Returns plain-text of a `Node` */ - function getLastBlockOrCreate(parser, blocks) { - var block; - if (blocks.length) { - block = blocks[blocks.length - 1]; - } else { - block = parser.parseBlock(createElement(DefaultBlockTypeSet.TEXT.tag)); - blocks.push(block); - } - return block; + function textOfNode(node) { + var text = node.textContent || node.innerText; + return text ? sanitizeWhitespace(text) : ''; } /** - * Helper to retain stray elements at the root of the html that aren't blocks + * Replaces a `Node` with its children */ - function handleNonBlockElementAtRoot(parser, elementNode, blocks) { - var block = getLastBlockOrCreate(parser, blocks), - markup = parser.parseElementMarkup(elementNode, block.value.length); - if (markup) { - block.markup.push(markup); + function unwrapNode(node) { + var children = toArray(node.childNodes); + var len = children.length; + var parent = node.parentNode, i; + for (i = 0; i < len; i++) { + parent.insertBefore(children[i], node); } - block.value += textOfNode(elementNode); } /** - * @class HTMLParser - * @constructor + * Extracts attributes of a `Node` to a hash of key/value pairs */ - function HTMLParser(options) { - var defaults = { - blockTypes : DefaultBlockTypeSet, - markupTypes : DefaultMarkupTypeSet, - includeTypeNames : false - }; - merge(this, defaults, options); + function attributesForNode(node /*,blacklist*/) { + var attrs = node.attributes; + var len = attrs && attrs.length; + var i, attr, name, hash; + + for (i = 0; i < len; i++) { + attr = attrs[i]; + name = attr.name; + if (attr.specified) { + //if (blacklist && name in blacklist)) { continue; } + hash = hash || {}; + hash[name] = attr.value; + } + } + return hash; } + __exports__.createElement = createElement; + __exports__.DOMParsingNode = DOMParsingNode; + __exports__.textOfNode = textOfNode; + __exports__.unwrapNode = unwrapNode; + __exports__.attributesForNode = attributesForNode; + }); +define("content-kit-utils/object-utils", + ["exports"], + function(__exports__) { + "use strict"; /** - * @method parse - * @param html String of HTML content - * @return Array Parsed JSON content array + * Merges defaults/options into an Object + * Useful for constructors */ - HTMLParser.prototype.parse = function(html) { - DOMParsingNode.innerHTML = sanitizeWhitespace(html); + function mergeWithOptions(original, updates, options) { + options = options || {}; + for(var prop in updates) { + if (options.hasOwnProperty(prop)) { + original[prop] = options[prop]; + } else if (updates.hasOwnProperty(prop)) { + original[prop] = updates[prop]; + } + } + return original; + } - var children = toArray(DOMParsingNode.childNodes), - len = children.length, - blocks = [], - i, currentNode, block, text; + /** + * Merges properties of one object into another + */ + function merge(original, updates) { + return mergeWithOptions(original, updates); + } - for (i = 0; i < len; i++) { - currentNode = children[i]; - // All top level nodes *should be* `Element` nodes and supported block types. - // We'll handle some cases if it isn't so we don't lose any content when parsing. - // Parser assumes sane input (such as from the ContentKit Editor) and is not intended to be a full html sanitizer. - if (currentNode.nodeType === 1) { - block = this.parseBlock(currentNode); - if (block) { - blocks.push(block); - } else { - handleNonBlockElementAtRoot(this, currentNode, blocks); + /** + * Prototype inheritance helper + */ + function inherit(Subclass, Superclass) { + if (typeof Object.create === 'function') { + Subclass._super = Superclass; + Subclass.prototype = Object.create(Superclass.prototype, { + constructor: { + value: Subclass, + enumerable: false, + writable: true, + configurable: true } - } else if (currentNode.nodeType === 3) { - text = currentNode.nodeValue; - if (trim(text)) { - block = getLastBlockOrCreate(this, blocks); - block.value += text; + }); + } else { + for (var key in Superclass) { + if (Superclass.hasOwnProperty(key)) { + Subclass[key] = Superclass[key]; } } + Subclass.prototype = new Superclass(); + Subclass.constructor = Subclass; } + } - return blocks; - }; + __exports__.mergeWithOptions = mergeWithOptions; + __exports__.merge = merge; + __exports__.inherit = inherit; + }); +define("content-kit-utils/string-utils", + ["exports"], + function(__exports__) { + "use strict"; + var RegExpTrim = /^\s+|\s+$/g; + var RegExpTrimLeft = /^\s+/; + var RegExpWSChars = /(\r\n|\n|\r|\t|\u00A0)/gm; + var RegExpMultiWS = /\s+/g; /** - * @method parseBlock - * @param node DOM node to parse - * @return {BlockModel} parsed block model - * Parses a single block type node into a model + * String.prototype.trim polyfill + * Removes whitespace at beginning and end of string */ - HTMLParser.prototype.parseBlock = function(node) { - var type = this.blockTypes.findByNode(node); + function trim(string) { + return string ? (string + '').replace(RegExpTrim, '') : ''; + } + + /** + * String.prototype.trimLeft polyfill + * Removes whitespace at beginning of string + */ + function trimLeft(string) { + return string ? (string + '').replace(RegExpTrimLeft, '') : ''; + } + + /** + * Replaces non-alphanumeric chars with underscores + */ + function underscore(string) { + return string ? (string + '').replace(/ /g, '_') : ''; + } + + /** + * Cleans line breaks, tabs, non-breaking spaces, then multiple occuring whitespaces. + */ + function sanitizeWhitespace(string) { + return string ? (string + '').replace(RegExpWSChars, '').replace(RegExpMultiWS, ' ') : ''; + } + + /** + * Injects a string into another string at the index specified + */ + function injectIntoString(string, injection, index) { + return string.substr(0, index) + injection + string.substr(index); + } + + __exports__.trim = trim; + __exports__.trimLeft = trimLeft; + __exports__.underscore = underscore; + __exports__.sanitizeWhitespace = sanitizeWhitespace; + __exports__.injectIntoString = injectIntoString; + }); +define("content-kit-compiler/models/block", + ["./model","../../content-kit-utils/object-utils","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + var Model = __dependency1__["default"]; + var inherit = __dependency2__.inherit; + + /** + * Ensures block markups at the same index are always in a specific order. + * For example, so all bold links are consistently marked up + * as text instead of text + */ + function sortBlockMarkups(markups) { + return markups.sort(function(a, b) { + if (a.start === b.start && a.end === b.end) { + return b.type - a.type; + } + return 0; + }); + } + + /** + * @class BlockModel + * @constructor + * @extends Model + */ + function BlockModel(options) { + options = options || {}; + Model.call(this, options); + this.value = options.value || ''; + this.markup = sortBlockMarkups(options.markup || []); + } + inherit(BlockModel, Model); + + __exports__["default"] = BlockModel; + }); +define("content-kit-compiler/models/embed", + ["../../content-kit-utils/object-utils","../models/model","../types/type","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { + "use strict"; + var inherit = __dependency1__.inherit; + var Model = __dependency2__["default"]; + var Type = __dependency3__["default"]; + + /** + * @class EmbedModel + * @constructor + * @extends Model + * Massages data from an oEmbed response into an EmbedModel + */ + function EmbedModel(options) { + if (!options) { return null; } + + Model.call(this, { + type: Type.EMBED.id, + type_name: Type.EMBED.name, + attributes: {} + }); + + var attributes = this.attributes; + var embedType = options.type; + var providerName = options.provider_name; + var embedUrl = options.url; + var embedTitle = options.title; + var embedThumbnail = options.thumbnail_url; + var embedHtml = options.html; + + if (embedType) { attributes.embed_type = embedType; } + if (providerName) { attributes.provider_name = providerName; } + if (embedUrl) { attributes.url = embedUrl; } + if (embedTitle) { attributes.title = embedTitle; } + + if (embedType === 'photo') { + attributes.thumbnail = options.media_url || embedUrl; + } else if (embedThumbnail) { + attributes.thumbnail = embedThumbnail; + } + + if (embedHtml && embedType === 'rich') { + attributes.html = embedHtml; + } + } + inherit(Model, EmbedModel); + + __exports__["default"] = EmbedModel; + }); +define("content-kit-compiler/models/image", + ["./block","../types/type","../../content-kit-utils/object-utils","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { + "use strict"; + var BlockModel = __dependency1__["default"]; + var Type = __dependency2__["default"]; + var inherit = __dependency3__.inherit; + + /** + * @class ImageModel + * @constructor + * @extends BlockModel + * A simple BlockModel subclass representing an image + */ + function ImageModel(options) { + options = options || {}; + options.type = Type.IMAGE.id; + options.type_name = Type.IMAGE.name; + if (options.src) { + options.attributes = { src: options.src }; + } + BlockModel.call(this, options); + } + inherit(ImageModel, BlockModel); + + __exports__["default"] = ImageModel; + }); +define("content-kit-compiler/models/markup", + ["./model","../../content-kit-utils/object-utils","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + var Model = __dependency1__["default"]; + var inherit = __dependency2__.inherit; + + /** + * @class MarkupModel + * @constructor + * @extends Model + */ + function MarkupModel(options) { + options = options || {}; + Model.call(this, options); + this.start = options.start || 0; + this.end = options.end || 0; + } + inherit(MarkupModel, Model); + + __exports__["default"] = MarkupModel; + }); +define("content-kit-compiler/models/model", + ["exports"], + function(__exports__) { + "use strict"; + /** + * @class Model + * @constructor + * @private + */ + function Model(options) { + options = options || {}; + var type_name = options.type_name; + var attributes = options.attributes; + + this.type = options.type || null; + if (type_name) { + this.type_name = type_name; + } + if (attributes) { + this.attributes = attributes; + } + } + + __exports__["default"] = Model; + }); +define("content-kit-compiler/models/text", + ["./block","../types/type","../../content-kit-utils/object-utils","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { + "use strict"; + var BlockModel = __dependency1__["default"]; + var Type = __dependency2__["default"]; + var inherit = __dependency3__.inherit; + + /** + * @class TextModel + * @constructor + * @extends BlockModel + * A simple BlockModel subclass representing a paragraph of text + */ + function TextModel(options) { + options = options || {}; + options.type = Type.TEXT.id; + options.type_name = Type.TEXT.name; + BlockModel.call(this, options); + } + inherit(TextModel, BlockModel); + + __exports__["default"] = TextModel; + }); +define("content-kit-compiler/parsers/html-parser", + ["../models/block","../models/markup","../types/default-types","../../content-kit-utils/object-utils","../../content-kit-utils/array-utils","../../content-kit-utils/string-utils","../../content-kit-utils/node-utils","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __exports__) { + "use strict"; + var BlockModel = __dependency1__["default"]; + var MarkupModel = __dependency2__["default"]; + var DefaultBlockTypeSet = __dependency3__.DefaultBlockTypeSet; + var DefaultMarkupTypeSet = __dependency3__.DefaultMarkupTypeSet; + var mergeWithOptions = __dependency4__.mergeWithOptions; + var toArray = __dependency5__.toArray; + var trim = __dependency6__.trim; + var trimLeft = __dependency6__.trimLeft; + var sanitizeWhitespace = __dependency6__.sanitizeWhitespace; + var createElement = __dependency7__.createElement; + var DOMParsingNode = __dependency7__.DOMParsingNode; + var textOfNode = __dependency7__.textOfNode; + var unwrapNode = __dependency7__.unwrapNode; + var attributesForNode = __dependency7__.attributesForNode; + + /** + * Gets the last block in the set or creates and return a default block if none exist yet. + */ + function getLastBlockOrCreate(parser, blocks) { + var block; + if (blocks.length) { + block = blocks[blocks.length - 1]; + } else { + block = parser.parseBlock(createElement(DefaultBlockTypeSet.TEXT.tag)); + blocks.push(block); + } + return block; + } + + /** + * Helper to retain stray elements at the root of the html that aren't blocks + */ + function handleNonBlockElementAtRoot(parser, elementNode, blocks) { + var block = getLastBlockOrCreate(parser, blocks), + markup = parser.parseElementMarkup(elementNode, block.value.length); + if (markup) { + block.markup.push(markup); + } + block.value += textOfNode(elementNode); + } + + /** + * @class HTMLParser + * @constructor + */ + function HTMLParser(options) { + var defaults = { + blockTypes : DefaultBlockTypeSet, + markupTypes : DefaultMarkupTypeSet, + includeTypeNames : false + }; + mergeWithOptions(this, defaults, options); + } + + /** + * @method parse + * @param html String of HTML content + * @return Array Parsed JSON content array + */ + HTMLParser.prototype.parse = function(html) { + DOMParsingNode.innerHTML = sanitizeWhitespace(html); + + var children = toArray(DOMParsingNode.childNodes), + len = children.length, + blocks = [], + i, currentNode, block, text; + + for (i = 0; i < len; i++) { + currentNode = children[i]; + // All top level nodes *should be* `Element` nodes and supported block types. + // We'll handle some cases if it isn't so we don't lose any content when parsing. + // Parser assumes sane input (such as from the ContentKit Editor) and is not intended to be a full html sanitizer. + if (currentNode.nodeType === 1) { + block = this.parseBlock(currentNode); + if (block) { + blocks.push(block); + } else { + handleNonBlockElementAtRoot(this, currentNode, blocks); + } + } else if (currentNode.nodeType === 3) { + text = currentNode.nodeValue; + if (trim(text)) { + block = getLastBlockOrCreate(this, blocks); + block.value += text; + } + } + } + + return blocks; + }; + + /** + * @method parseBlock + * @param node DOM node to parse + * @return {BlockModel} parsed block model + * Parses a single block type node into a model + */ + HTMLParser.prototype.parseBlock = function(node) { + var type = this.blockTypes.findByNode(node); if (type) { return new BlockModel({ type : type.id, @@ -1775,632 +970,1716 @@ define("parsers/html-parser", } }; - /** - * @method parseBlockMarkup - * @param node DOM node to parse - * @return {Array} parsed markups - * Parses a single block type node's markup - */ - HTMLParser.prototype.parseBlockMarkup = function(node) { - var processedText = '', - markups = [], - index = 0, - currentNode, markup; + /** + * @method parseBlockMarkup + * @param node DOM node to parse + * @return {Array} parsed markups + * Parses a single block type node's markup + */ + HTMLParser.prototype.parseBlockMarkup = function(node) { + var processedText = '', + markups = [], + index = 0, + currentNode, markup; + + // Clone the node since it will be recursively torn down + node = node.cloneNode(true); + + while (node.hasChildNodes()) { + currentNode = node.firstChild; + if (currentNode.nodeType === 1) { + markup = this.parseElementMarkup(currentNode, processedText.length); + if (markup) { + markups.push(markup); + } + // unwrap the element so we can process any children + if (currentNode.hasChildNodes()) { + unwrapNode(currentNode); + } + } else if (currentNode.nodeType === 3) { + var text = sanitizeWhitespace(currentNode.nodeValue); + if (index === 0) { text = trimLeft(text); } + if (text) { processedText += text; } + } + + // node has been processed, remove it + currentNode.parentNode.removeChild(currentNode); + index++; + } + + return markups; + }; + + /** + * @method parseElementMarkup + * @param node DOM node to parse + * @param startIndex DOM node to parse + * @return {MarkupModel} parsed markup model + * Parses markup of a single html element node + */ + HTMLParser.prototype.parseElementMarkup = function(node, startIndex) { + var type = this.markupTypes.findByNode(node), + selfClosing, endIndex; + + if (type) { + selfClosing = type.selfClosing; + if (!selfClosing && !node.hasChildNodes()) { return; } // check for empty nodes + + endIndex = startIndex + (selfClosing ? 0 : textOfNode(node).length); + if (endIndex > startIndex || (selfClosing && endIndex === startIndex)) { // check for empty nodes + return new MarkupModel({ + type : type.id, + type_name : this.includeTypeNames && type.name, + start : startIndex, + end : endIndex, + attributes : attributesForNode(node) + }); + } + } + }; + + __exports__["default"] = HTMLParser; + }); +define("content-kit-compiler/renderers/html-element-renderer", + ["../../content-kit-utils/string-utils","../../content-kit-utils/array-utils","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + var injectIntoString = __dependency1__.injectIntoString; + var sumSparseArray = __dependency2__.sumSparseArray; + + /** + * Builds an opening html tag. i.e. '' + link + ''; + this.showMessage(message, element); + }; + + __exports__["default"] = Tooltip; }); -define("utils/string-utils", +define("content-kit-editor/views/view", ["exports"], function(__exports__) { "use strict"; - var RegExpTrim = /^\s+|\s+$/g; - var RegExpTrimLeft = /^\s+/; - var RegExpWSChars = /(\r\n|\n|\r|\t|\u00A0)/gm; - var RegExpMultiWS = /\s+/g; - - /** - * String.prototype.trim polyfill - * Removes whitespace at beginning and end of string - */ - function trim(string) { - return string ? (string + '').replace(RegExpTrim, '') : ''; - } - - /** - * String.prototype.trimLeft polyfill - * Removes whitespace at beginning of string - */ - function trimLeft(string) { - return string ? (string + '').replace(RegExpTrimLeft, '') : ''; - } - - /** - * Replaces non-alphanumeric chars with underscores - */ - function underscore(string) { - return string ? (string + '').replace(/ /g, '_') : ''; - } - - /** - * Cleans line breaks, tabs, non-breaking spaces, then multiple occuring whitespaces. - */ - function sanitizeWhitespace(string) { - return string ? (string + '').replace(RegExpWSChars, '').replace(RegExpMultiWS, ' ') : ''; - } - - /** - * Injects a string into another string at the index specified - */ - function injectIntoString(string, injection, index) { - return string.substr(0, index) + injection + string.substr(index); - } + 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; + } + }, + focus: function() { + this.element.focus(); + }, + 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(' '); + } + }; - __exports__.trim = trim; - __exports__.trimLeft = trimLeft; - __exports__.underscore = underscore; - __exports__.sanitizeWhitespace = sanitizeWhitespace; - __exports__.injectIntoString = injectIntoString; + __exports__["default"] = View; }); -define("renderers/embeds/instagram", +define("content-kit-compiler/renderers/embeds/instagram", ["exports"], function(__exports__) { "use strict"; @@ -2412,7 +2691,7 @@ define("renderers/embeds/instagram", __exports__["default"] = InstagramRenderer; }); -define("renderers/embeds/twitter", +define("content-kit-compiler/renderers/embeds/twitter", ["exports"], function(__exports__) { "use strict"; @@ -2424,7 +2703,7 @@ define("renderers/embeds/twitter", __exports__["default"] = TwitterRenderer; }); -define("renderers/embeds/youtube", +define("content-kit-compiler/renderers/embeds/youtube", ["exports"], function(__exports__) { "use strict"; @@ -2448,4 +2727,4 @@ define("renderers/embeds/youtube", __exports__["default"] = YouTubeRenderer; }); -}(this, document, define)); +}(this, document)); diff --git a/gulpfile.js b/gulpfile.js index 8e84dd238..5b153b56a 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -8,24 +8,17 @@ var util = require('gulp-util'); var open = require('gulp-open'); var rimraf = require('gulp-rimraf'); var insert = require('gulp-insert'); -var runSequence = require('run-sequence'); +var es6ModuleTranspiler = require('gulp-es6-module-transpiler'); // ------------------------------------------- var pkg = require('./package.json'); var jsSrc = [ - './src/js/index.js', - './src/js/constants.js', - './src/js/utils/*.js', - './src/js/views/view.js', - './src/js/views/*.js', - './src/js/commands.js', - './src/js/editor.js' + './src/js/content-kit.js', + './src/js/**/*.js' ]; -var jsExtSrc = './src/js/ext/*'; - var cssSrc = [ './src/css/variables.less', './src/css/editor.less', @@ -72,27 +65,15 @@ gulp.task('lint', function() { .pipe(jshint.reporter('default')); }); -// Concatenates and builds javascript source code -gulp.task('build-js-src', function() { +gulp.task('build-js', ['lint'], function() { return gulp.src(jsSrc) + .pipe(es6ModuleTranspiler({ type: 'amd' })) .pipe(concat(jsDistName)) .pipe(insert.wrap(iifeHeader, iifeFooter)) .pipe(header(banner, { pkg : pkg } )) - . pipe(gulp.dest(distDest)); -}); - -// Concatenates external js dependencies with built js source -gulp.task('build-js-ext', ['build-js-src'], function() { - return gulp.src([jsDistPath, jsExtSrc]) - .pipe(concat(jsDistName)) .pipe(gulp.dest(distDest)); }); -// Builds final js output in sequence (tasks are order dependent) -gulp.task('build-js', ['lint', 'build-js-src', 'build-js-ext'], function() { - return runSequence('build-js-src', 'build-js-ext'); -}); - // Compiles LESS and concatenates css gulp.task('build-css', function() { return gulp.src(cssSrc) diff --git a/package.json b/package.json index 0fc089bad..06889439c 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "devDependencies": { "gulp": "^3.8.1", "gulp-concat": "~2.1.7", + "gulp-es6-module-transpiler": "^0.1.3", "gulp-header": "~1.0.2", "gulp-insert": "^0.4.0", "gulp-jshint": "~1.3.4", @@ -26,7 +27,6 @@ "gulp-open": "^0.2.8", "gulp-qunit": "^0.3.3", "gulp-rimraf": "^0.1.0", - "gulp-util": "~2.2.12", - "run-sequence": "^0.3.6" + "gulp-util": "~2.2.12" } } diff --git a/src/js/commands.js b/src/js/commands.js deleted file mode 100644 index 62e679c95..000000000 --- a/src/js/commands.js +++ /dev/null @@ -1,294 +0,0 @@ -function createCommandIndex(commands) { - var index = {}; - var len = commands.length, i, command; - for(i = 0; i < len; i++) { - command = commands[i]; - index[command.name] = command; - } - return index; -} - -function Command(options) { - var command = this; - var name = options.name; - var prompt = options.prompt; - command.name = name; - command.button = options.button || name; - command.editorContext = null; - if (prompt) { command.prompt = prompt; } -} -Command.prototype.exec = function(){}; - -function TextFormatCommand(options) { - Command.call(this, options); - this.tag = options.tag.toUpperCase(); - this.action = options.action || this.name; - this.removeAction = options.removeAction || this.action; -} -inherits(TextFormatCommand, Command); - -TextFormatCommand.prototype = { - exec: function(value) { - document.execCommand(this.action, false, value || null); - }, - unexec: function(value) { - document.execCommand(this.removeAction, false, value || null); - } -}; - -function BoldCommand() { - TextFormatCommand.call(this, { - name: 'bold', - tag: Tags.BOLD, - button: '' - }); -} -inherits(BoldCommand, TextFormatCommand); -BoldCommand.prototype.exec = function() { - // Don't allow executing bold command on heading tags - if (!Regex.HEADING_TAG.test(getSelectionBlockTagName())) { - BoldCommand._super.prototype.exec.call(this); - } -}; - -function ItalicCommand() { - TextFormatCommand.call(this, { - name: 'italic', - tag: Tags.ITALIC, - button: '' - }); -} -inherits(ItalicCommand, TextFormatCommand); - -function LinkCommand() { - TextFormatCommand.call(this, { - name: 'link', - tag: Tags.LINK, - action: 'createLink', - removeAction: 'unlink', - button: '', - prompt: new Prompt({ - command: this, - placeholder: 'Enter a url, press return...' - }) - }); -} -inherits(LinkCommand, TextFormatCommand); -LinkCommand.prototype.exec = function(url) { - if(this.tag === getSelectionTagName()) { - this.unexec(); - } else { - if (!Regex.HTTP_PROTOCOL.test(url)) { - url = 'http://' + url; - } - LinkCommand._super.prototype.exec.call(this, url); - } -}; - -function FormatBlockCommand(options) { - options.action = 'formatBlock'; - TextFormatCommand.call(this, options); -} -inherits(FormatBlockCommand, TextFormatCommand); -FormatBlockCommand.prototype.exec = function() { - var tag = this.tag; - // Brackets neccessary for certain browsers - var value = '<' + tag + '>'; - var blockElement = getSelectionBlockElement(); - // Allow block commands to be toggled back to a paragraph - 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(blockElement.textContent); - blockElement.parentNode.insertBefore(flatNode, blockElement); - blockElement.parentNode.removeChild(blockElement); - selectNode(flatNode); - } - - FormatBlockCommand._super.prototype.exec.call(this, value); -}; - -function QuoteCommand() { - FormatBlockCommand.call(this, { - name: 'quote', - tag: Tags.QUOTE, - button: '' - }); -} -inherits(QuoteCommand, FormatBlockCommand); - -function HeadingCommand() { - FormatBlockCommand.call(this, { - name: 'heading', - tag: Tags.HEADING, - button: '1' - }); -} -inherits(HeadingCommand, FormatBlockCommand); - -function SubheadingCommand() { - FormatBlockCommand.call(this, { - name: 'subheading', - tag: Tags.SUBHEADING, - button: '2' - }); -} -inherits(SubheadingCommand, FormatBlockCommand); - -function ListCommand(options) { - TextFormatCommand.call(this, options); -} -inherits(ListCommand, TextFormatCommand); -ListCommand.prototype.exec = function() { - ListCommand._super.prototype.exec.call(this); - - // After creation, lists need to be unwrapped from the default formatter P tag - var listElement = getSelectionBlockElement(); - var wrapperNode = listElement.parentNode; - if (wrapperNode.firstChild === listElement) { - var editorNode = wrapperNode.parentNode; - editorNode.insertBefore(listElement, wrapperNode); - editorNode.removeChild(wrapperNode); - selectNode(listElement); - } -}; - -function UnorderedListCommand() { - ListCommand.call(this, { - name: 'list', - tag: Tags.LIST, - action: 'insertUnorderedList' - }); -} -inherits(UnorderedListCommand, ListCommand); - -function OrderedListCommand() { - ListCommand.call(this, { - name: 'ordered list', - tag: Tags.ORDERED_LIST, - action: 'insertOrderedList' - }); -} -inherits(OrderedListCommand, ListCommand); - -TextFormatCommand.all = [ - new BoldCommand(), - new ItalicCommand(), - new LinkCommand(), - new QuoteCommand(), - new HeadingCommand(), - new SubheadingCommand() -]; - -TextFormatCommand.index = createCommandIndex(TextFormatCommand.all); - - -function EmbedCommand(options) { - Command.call(this, options); -} -inherits(EmbedCommand, Command); - -function ImageEmbedCommand(options) { - EmbedCommand.call(this, { - name: 'image', - button: '' - }); - if (window.XHRFileUploader) { - this.uploader = new XHRFileUploader({ url: '/upload', maxFileSize: 5000000 }); - } -} -inherits(ImageEmbedCommand, EmbedCommand); - -ImageEmbedCommand.prototype = { - exec: function() { - ImageEmbedCommand._super.prototype.exec.call(this); - var clickEvent = new MouseEvent('click', { bubbles: false }); - if (!this.fileInput) { - var command = this; - var fileInput = this.fileInput = document.createElement('input'); - fileInput.type = 'file'; - fileInput.accept = 'image/*'; - fileInput.className = 'ck-file-input'; - fileInput.addEventListener('change', function(e) { - command.handleFile(e); - }); - document.body.appendChild(fileInput); - } - this.fileInput.dispatchEvent(clickEvent); - }, - handleFile: function(e) { - var fileInput = e.target; - var editor = this.editorContext; - var embedIntent = this.embedIntent; - - embedIntent.showLoading(); - this.uploader.upload({ - fileInput: fileInput, - complete: function(response, error) { - embedIntent.hideLoading(); - if (error || !response || !response.url) { - return new Message().show(error.message || 'Error uploading image'); - } - var imageModel = new ContentKit.ImageModel({ src: response.url }); - var index = editor.getCurrentBlockIndex(); - editor.insertBlockAt(imageModel, index); - editor.syncVisualAt(index); - } - }); - fileInput.value = null; // reset file input - // TODO: client-side render while uploading - } -}; - -function OEmbedCommand(options) { - EmbedCommand.call(this, { - name: 'oEmbed', - button: '', - prompt: new Prompt({ - command: this, - placeholder: 'Paste a YouTube or Twitter url...' - }) - }); -} -inherits(OEmbedCommand, EmbedCommand); - -OEmbedCommand.prototype.exec = function(url) { - var command = this; - var editorContext = command.editorContext; - var index = editorContext.getCurrentBlockIndex(); - var oEmbedEndpoint = 'http://noembed.com/embed?url='; - - command.embedIntent.showLoading(); - if (!Regex.HTTP_PROTOCOL.test(url)) { - url = 'http://' + url; - } - - HTTP.get(oEmbedEndpoint + url, function(responseText, error) { - command.embedIntent.hideLoading(); - if (error) { - new Message().show('Embed error: status code ' + error.currentTarget.status); - } else { - var json = JSON.parse(responseText); - if (json.error) { - new Message().show('Embed error: ' + json.error); - } else { - var embedModel = new ContentKit.EmbedModel(json); - //if (!embedModel.attributes.provider_id) { - // new Message().show('Embed error: "' + embedModel.attributes.provider_name + '" embeds are not supported at this time'); - //} else { - editorContext.insertBlockAt(embedModel, index); - editorContext.syncVisualAt(index); - //} - } - } - }); -}; - -EmbedCommand.all = [ - new ImageEmbedCommand(), - new OEmbedCommand() -]; - -EmbedCommand.index = createCommandIndex(EmbedCommand.all); diff --git a/src/js/content-kit-compiler/compiler.js b/src/js/content-kit-compiler/compiler.js new file mode 100644 index 000000000..348898681 --- /dev/null +++ b/src/js/content-kit-compiler/compiler.js @@ -0,0 +1,68 @@ +import HTMLParser from './parsers/html-parser'; +import HTMLRenderer from './renderers/html-renderer'; +import Type from './types/type'; +import { DefaultBlockTypeSet, DefaultMarkupTypeSet } from './types/default-types'; +import { mergeWithOptions } from '../../content-kit-utils/object-utils'; + +/** + * @class Compiler + * @constructor + * @param options + */ +function Compiler(options) { + var parser = new HTMLParser(); + var renderer = new HTMLRenderer(); + var defaults = { + parser : parser, + renderer : renderer, + blockTypes : DefaultBlockTypeSet, + markupTypes : DefaultMarkupTypeSet, + includeTypeNames : false // true will output type_name: 'TEXT' etc. when parsing for easier debugging + }; + mergeWithOptions(this, defaults, options); + + // Reference the compiler settings + parser.blockTypes = renderer.blockTypes = this.blockTypes; + parser.markupTypes = renderer.markupTypes = this.markupTypes; + parser.includeTypeNames = this.includeTypeNames; +} + +/** + * @method parse + * @param input + * @return Object + */ +Compiler.prototype.parse = function(input) { + return this.parser.parse(input); +}; + +/** + * @method render + * @param data + * @return Object + */ +Compiler.prototype.render = function(data) { + return this.renderer.render(data); +}; + +/** + * @method registerBlockType + * @param {Type} type + */ +Compiler.prototype.registerBlockType = function(type) { + if (type instanceof Type) { + return this.blockTypes.addType(type); + } +}; + +/** + * @method registerMarkupType + * @param {Type} type + */ +Compiler.prototype.registerMarkupType = function(type) { + if (type instanceof Type) { + return this.markupTypes.addType(type); + } +}; + +export default Compiler; diff --git a/src/js/content-kit-compiler/models/block.js b/src/js/content-kit-compiler/models/block.js new file mode 100644 index 000000000..07c4e223f --- /dev/null +++ b/src/js/content-kit-compiler/models/block.js @@ -0,0 +1,31 @@ +import Model from './model'; +import { inherit } from '../../content-kit-utils/object-utils'; + +/** + * Ensures block markups at the same index are always in a specific order. + * For example, so all bold links are consistently marked up + * as text instead of text + */ +function sortBlockMarkups(markups) { + return markups.sort(function(a, b) { + if (a.start === b.start && a.end === b.end) { + return b.type - a.type; + } + return 0; + }); +} + +/** + * @class BlockModel + * @constructor + * @extends Model + */ +function BlockModel(options) { + options = options || {}; + Model.call(this, options); + this.value = options.value || ''; + this.markup = sortBlockMarkups(options.markup || []); +} +inherit(BlockModel, Model); + +export default BlockModel; diff --git a/src/js/content-kit-compiler/models/embed.js b/src/js/content-kit-compiler/models/embed.js new file mode 100644 index 000000000..da4a5927c --- /dev/null +++ b/src/js/content-kit-compiler/models/embed.js @@ -0,0 +1,45 @@ +import { inherit } from '../../content-kit-utils/object-utils'; +import Model from '../models/model'; +import Type from '../types/type'; + +/** + * @class EmbedModel + * @constructor + * @extends Model + * Massages data from an oEmbed response into an EmbedModel + */ +function EmbedModel(options) { + if (!options) { return null; } + + Model.call(this, { + type: Type.EMBED.id, + type_name: Type.EMBED.name, + attributes: {} + }); + + var attributes = this.attributes; + var embedType = options.type; + var providerName = options.provider_name; + var embedUrl = options.url; + var embedTitle = options.title; + var embedThumbnail = options.thumbnail_url; + var embedHtml = options.html; + + if (embedType) { attributes.embed_type = embedType; } + if (providerName) { attributes.provider_name = providerName; } + if (embedUrl) { attributes.url = embedUrl; } + if (embedTitle) { attributes.title = embedTitle; } + + if (embedType === 'photo') { + attributes.thumbnail = options.media_url || embedUrl; + } else if (embedThumbnail) { + attributes.thumbnail = embedThumbnail; + } + + if (embedHtml && embedType === 'rich') { + attributes.html = embedHtml; + } +} +inherit(Model, EmbedModel); + +export default EmbedModel; diff --git a/src/js/content-kit-compiler/models/image.js b/src/js/content-kit-compiler/models/image.js new file mode 100644 index 000000000..3e1c7ff34 --- /dev/null +++ b/src/js/content-kit-compiler/models/image.js @@ -0,0 +1,22 @@ +import BlockModel from './block'; +import Type from '../types/type'; +import { inherit } from '../../content-kit-utils/object-utils'; + +/** + * @class ImageModel + * @constructor + * @extends BlockModel + * A simple BlockModel subclass representing an image + */ +function ImageModel(options) { + options = options || {}; + options.type = Type.IMAGE.id; + options.type_name = Type.IMAGE.name; + if (options.src) { + options.attributes = { src: options.src }; + } + BlockModel.call(this, options); +} +inherit(ImageModel, BlockModel); + +export default ImageModel; diff --git a/src/js/content-kit-compiler/models/markup.js b/src/js/content-kit-compiler/models/markup.js new file mode 100644 index 000000000..da3aa4876 --- /dev/null +++ b/src/js/content-kit-compiler/models/markup.js @@ -0,0 +1,17 @@ +import Model from './model'; +import { inherit } from '../../content-kit-utils/object-utils'; + +/** + * @class MarkupModel + * @constructor + * @extends Model + */ +function MarkupModel(options) { + options = options || {}; + Model.call(this, options); + this.start = options.start || 0; + this.end = options.end || 0; +} +inherit(MarkupModel, Model); + +export default MarkupModel; diff --git a/src/js/content-kit-compiler/models/model.js b/src/js/content-kit-compiler/models/model.js new file mode 100644 index 000000000..094a8e7f0 --- /dev/null +++ b/src/js/content-kit-compiler/models/model.js @@ -0,0 +1,20 @@ +/** + * @class Model + * @constructor + * @private + */ +function Model(options) { + options = options || {}; + var type_name = options.type_name; + var attributes = options.attributes; + + this.type = options.type || null; + if (type_name) { + this.type_name = type_name; + } + if (attributes) { + this.attributes = attributes; + } +} + +export default Model; \ No newline at end of file diff --git a/src/js/content-kit-compiler/models/text.js b/src/js/content-kit-compiler/models/text.js new file mode 100644 index 000000000..602c8c650 --- /dev/null +++ b/src/js/content-kit-compiler/models/text.js @@ -0,0 +1,19 @@ +import BlockModel from './block'; +import Type from '../types/type'; +import { inherit } from '../../content-kit-utils/object-utils'; + +/** + * @class TextModel + * @constructor + * @extends BlockModel + * A simple BlockModel subclass representing a paragraph of text + */ +function TextModel(options) { + options = options || {}; + options.type = Type.TEXT.id; + options.type_name = Type.TEXT.name; + BlockModel.call(this, options); +} +inherit(TextModel, BlockModel); + +export default TextModel; diff --git a/src/js/content-kit-compiler/parsers/html-parser.js b/src/js/content-kit-compiler/parsers/html-parser.js new file mode 100644 index 000000000..2d158dcd5 --- /dev/null +++ b/src/js/content-kit-compiler/parsers/html-parser.js @@ -0,0 +1,172 @@ +import BlockModel from '../models/block'; +import MarkupModel from '../models/markup'; +import { DefaultBlockTypeSet, DefaultMarkupTypeSet } from '../types/default-types'; +import { mergeWithOptions } from '../../content-kit-utils/object-utils'; +import { toArray } from '../../content-kit-utils/array-utils'; +import { trim, trimLeft, sanitizeWhitespace } from '../../content-kit-utils/string-utils'; +import { createElement, DOMParsingNode, textOfNode, unwrapNode, attributesForNode } from '../../content-kit-utils/node-utils'; + +/** + * Gets the last block in the set or creates and return a default block if none exist yet. + */ +function getLastBlockOrCreate(parser, blocks) { + var block; + if (blocks.length) { + block = blocks[blocks.length - 1]; + } else { + block = parser.parseBlock(createElement(DefaultBlockTypeSet.TEXT.tag)); + blocks.push(block); + } + return block; +} + +/** + * Helper to retain stray elements at the root of the html that aren't blocks + */ +function handleNonBlockElementAtRoot(parser, elementNode, blocks) { + var block = getLastBlockOrCreate(parser, blocks), + markup = parser.parseElementMarkup(elementNode, block.value.length); + if (markup) { + block.markup.push(markup); + } + block.value += textOfNode(elementNode); +} + +/** + * @class HTMLParser + * @constructor + */ +function HTMLParser(options) { + var defaults = { + blockTypes : DefaultBlockTypeSet, + markupTypes : DefaultMarkupTypeSet, + includeTypeNames : false + }; + mergeWithOptions(this, defaults, options); +} + +/** + * @method parse + * @param html String of HTML content + * @return Array Parsed JSON content array + */ +HTMLParser.prototype.parse = function(html) { + DOMParsingNode.innerHTML = sanitizeWhitespace(html); + + var children = toArray(DOMParsingNode.childNodes), + len = children.length, + blocks = [], + i, currentNode, block, text; + + for (i = 0; i < len; i++) { + currentNode = children[i]; + // All top level nodes *should be* `Element` nodes and supported block types. + // We'll handle some cases if it isn't so we don't lose any content when parsing. + // Parser assumes sane input (such as from the ContentKit Editor) and is not intended to be a full html sanitizer. + if (currentNode.nodeType === 1) { + block = this.parseBlock(currentNode); + if (block) { + blocks.push(block); + } else { + handleNonBlockElementAtRoot(this, currentNode, blocks); + } + } else if (currentNode.nodeType === 3) { + text = currentNode.nodeValue; + if (trim(text)) { + block = getLastBlockOrCreate(this, blocks); + block.value += text; + } + } + } + + return blocks; +}; + +/** + * @method parseBlock + * @param node DOM node to parse + * @return {BlockModel} parsed block model + * Parses a single block type node into a model + */ +HTMLParser.prototype.parseBlock = function(node) { + var type = this.blockTypes.findByNode(node); + if (type) { + return new BlockModel({ + type : type.id, + type_name : this.includeTypeNames && type.name, + value : trim(textOfNode(node)), + attributes : attributesForNode(node), + markup : this.parseBlockMarkup(node) + }); + } +}; + +/** + * @method parseBlockMarkup + * @param node DOM node to parse + * @return {Array} parsed markups + * Parses a single block type node's markup + */ +HTMLParser.prototype.parseBlockMarkup = function(node) { + var processedText = '', + markups = [], + index = 0, + currentNode, markup; + + // Clone the node since it will be recursively torn down + node = node.cloneNode(true); + + while (node.hasChildNodes()) { + currentNode = node.firstChild; + if (currentNode.nodeType === 1) { + markup = this.parseElementMarkup(currentNode, processedText.length); + if (markup) { + markups.push(markup); + } + // unwrap the element so we can process any children + if (currentNode.hasChildNodes()) { + unwrapNode(currentNode); + } + } else if (currentNode.nodeType === 3) { + var text = sanitizeWhitespace(currentNode.nodeValue); + if (index === 0) { text = trimLeft(text); } + if (text) { processedText += text; } + } + + // node has been processed, remove it + currentNode.parentNode.removeChild(currentNode); + index++; + } + + return markups; +}; + +/** + * @method parseElementMarkup + * @param node DOM node to parse + * @param startIndex DOM node to parse + * @return {MarkupModel} parsed markup model + * Parses markup of a single html element node + */ +HTMLParser.prototype.parseElementMarkup = function(node, startIndex) { + var type = this.markupTypes.findByNode(node), + selfClosing, endIndex; + + if (type) { + selfClosing = type.selfClosing; + if (!selfClosing && !node.hasChildNodes()) { return; } // check for empty nodes + + endIndex = startIndex + (selfClosing ? 0 : textOfNode(node).length); + if (endIndex > startIndex || (selfClosing && endIndex === startIndex)) { // check for empty nodes + return new MarkupModel({ + type : type.id, + type_name : this.includeTypeNames && type.name, + start : startIndex, + end : endIndex, + attributes : attributesForNode(node) + }); + } + } +}; + +export default HTMLParser; diff --git a/src/js/content-kit-compiler/renderers/embeds/instagram.js b/src/js/content-kit-compiler/renderers/embeds/instagram.js new file mode 100644 index 000000000..b92005bb3 --- /dev/null +++ b/src/js/content-kit-compiler/renderers/embeds/instagram.js @@ -0,0 +1,7 @@ + +function InstagramRenderer() {} +InstagramRenderer.prototype.render = function(model) { + return ''; +}; + +export default InstagramRenderer; diff --git a/src/js/content-kit-compiler/renderers/embeds/twitter.js b/src/js/content-kit-compiler/renderers/embeds/twitter.js new file mode 100644 index 000000000..167e09a63 --- /dev/null +++ b/src/js/content-kit-compiler/renderers/embeds/twitter.js @@ -0,0 +1,7 @@ + +function TwitterRenderer() {} +TwitterRenderer.prototype.render = function(model) { + return ''; +}; + +export default TwitterRenderer; diff --git a/src/js/content-kit-compiler/renderers/embeds/youtube.js b/src/js/content-kit-compiler/renderers/embeds/youtube.js new file mode 100644 index 000000000..e88de5a32 --- /dev/null +++ b/src/js/content-kit-compiler/renderers/embeds/youtube.js @@ -0,0 +1,19 @@ + +var RegExVideoId = /.*(?:youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=)([^#\&\?]*).*/; + +function getVideoIdFromUrl(url) { + var match = url.match(RegExVideoId); + if (match && match[1].length === 11){ + return match[1]; + } + return null; +} + +function YouTubeRenderer() {} +YouTubeRenderer.prototype.render = function(model) { + var videoId = getVideoIdFromUrl(model.attributes.url); + var embedUrl = 'http://www.youtube.com/embed/' + videoId + '?controls=2&showinfo=0&color=white&theme=light'; + return ''; +}; + +export default YouTubeRenderer; \ No newline at end of file diff --git a/src/js/content-kit-compiler/renderers/html-element-renderer.js b/src/js/content-kit-compiler/renderers/html-element-renderer.js new file mode 100644 index 000000000..370083ac3 --- /dev/null +++ b/src/js/content-kit-compiler/renderers/html-element-renderer.js @@ -0,0 +1,98 @@ +import { injectIntoString } from '../../content-kit-utils/string-utils'; +import { sumSparseArray } from '../../content-kit-utils/array-utils'; + +/** + * Builds an opening html tag. i.e. '' + link + ''; this.showMessage(message, element); }; + +export default Tooltip; diff --git a/src/js/views/view.js b/src/js/content-kit-editor/views/view.js similarity index 97% rename from src/js/views/view.js rename to src/js/content-kit-editor/views/view.js index a788e5427..78db7d4e3 100644 --- a/src/js/views/view.js +++ b/src/js/content-kit-editor/views/view.js @@ -36,3 +36,5 @@ View.prototype = { this.element.className = this.classNames.join(' '); } }; + +export default View; diff --git a/src/js/content-kit-utils/array-utils.js b/src/js/content-kit-utils/array-utils.js new file mode 100644 index 000000000..08efc9a9a --- /dev/null +++ b/src/js/content-kit-utils/array-utils.js @@ -0,0 +1,27 @@ +/** + * Converts an array-like object (i.e. NodeList) to Array + * Note: could just use Array.prototype.slice but does not work in IE <= 8 + */ +function toArray(obj) { + var array = []; + var i = obj && obj.length >>> 0; // cast to Uint32 + while (i--) { + array[i] = obj[i]; + } + return array; +} + +/** + * Computes the sum of values in a (sparse) array + */ +function sumSparseArray(array) { + var sum = 0, i; + for (i in array) { // 'for in' is better for sparse arrays + if (array.hasOwnProperty(i)) { + sum += array[i]; + } + } + return sum; +} + +export { toArray, sumSparseArray }; diff --git a/src/js/content-kit-utils/node-utils.js b/src/js/content-kit-utils/node-utils.js new file mode 100644 index 000000000..5f9f1ac05 --- /dev/null +++ b/src/js/content-kit-utils/node-utils.js @@ -0,0 +1,70 @@ +import { sanitizeWhitespace } from './string-utils'; +import { toArray } from './array-utils'; + +/** + * A document instance separate from the page's document. (if browser supports it) + * Prevents images, scripts, and styles from executing while parsing nodes. + */ +var standaloneDocument = (function() { + var implementation = document.implementation; + var createHTMLDocument = implementation.createHTMLDocument; + + if (createHTMLDocument) { + return createHTMLDocument.call(implementation, ''); + } + return document; +})(); + +/** + * document.createElement with our lean, standalone document + */ +function createElement(type) { + return standaloneDocument.createElement(type); +} + +/** + * A reusable DOM Node for parsing html content. + */ +var DOMParsingNode = createElement('div'); + +/** + * Returns plain-text of a `Node` + */ +function textOfNode(node) { + var text = node.textContent || node.innerText; + return text ? sanitizeWhitespace(text) : ''; +} + +/** + * Replaces a `Node` with its children + */ +function unwrapNode(node) { + var children = toArray(node.childNodes); + var len = children.length; + var parent = node.parentNode, i; + for (i = 0; i < len; i++) { + parent.insertBefore(children[i], node); + } +} + +/** + * Extracts attributes of a `Node` to a hash of key/value pairs + */ +function attributesForNode(node /*,blacklist*/) { + var attrs = node.attributes; + var len = attrs && attrs.length; + var i, attr, name, hash; + + for (i = 0; i < len; i++) { + attr = attrs[i]; + name = attr.name; + if (attr.specified) { + //if (blacklist && name in blacklist)) { continue; } + hash = hash || {}; + hash[name] = attr.value; + } + } + return hash; +} + +export { createElement, DOMParsingNode, textOfNode, unwrapNode, attributesForNode }; diff --git a/src/js/content-kit-utils/object-utils.js b/src/js/content-kit-utils/object-utils.js new file mode 100644 index 000000000..0a04873e9 --- /dev/null +++ b/src/js/content-kit-utils/object-utils.js @@ -0,0 +1,49 @@ +/** + * Merges defaults/options into an Object + * Useful for constructors + */ +function mergeWithOptions(original, updates, options) { + options = options || {}; + for(var prop in updates) { + if (options.hasOwnProperty(prop)) { + original[prop] = options[prop]; + } else if (updates.hasOwnProperty(prop)) { + original[prop] = updates[prop]; + } + } + return original; +} + +/** + * Merges properties of one object into another + */ +function merge(original, updates) { + return mergeWithOptions(original, updates); +} + +/** + * Prototype inheritance helper + */ +function inherit(Subclass, Superclass) { + if (typeof Object.create === 'function') { + Subclass._super = Superclass; + Subclass.prototype = Object.create(Superclass.prototype, { + constructor: { + value: Subclass, + enumerable: false, + writable: true, + configurable: true + } + }); + } else { + for (var key in Superclass) { + if (Superclass.hasOwnProperty(key)) { + Subclass[key] = Superclass[key]; + } + } + Subclass.prototype = new Superclass(); + Subclass.constructor = Subclass; + } +} + +export { mergeWithOptions, merge, inherit }; diff --git a/src/js/content-kit-utils/string-utils.js b/src/js/content-kit-utils/string-utils.js new file mode 100644 index 000000000..ae170affe --- /dev/null +++ b/src/js/content-kit-utils/string-utils.js @@ -0,0 +1,43 @@ +var RegExpTrim = /^\s+|\s+$/g; +var RegExpTrimLeft = /^\s+/; +var RegExpWSChars = /(\r\n|\n|\r|\t|\u00A0)/gm; +var RegExpMultiWS = /\s+/g; + +/** + * String.prototype.trim polyfill + * Removes whitespace at beginning and end of string + */ +function trim(string) { + return string ? (string + '').replace(RegExpTrim, '') : ''; +} + +/** + * String.prototype.trimLeft polyfill + * Removes whitespace at beginning of string + */ +function trimLeft(string) { + return string ? (string + '').replace(RegExpTrimLeft, '') : ''; +} + +/** + * Replaces non-alphanumeric chars with underscores + */ +function underscore(string) { + return string ? (string + '').replace(/ /g, '_') : ''; +} + +/** + * Cleans line breaks, tabs, non-breaking spaces, then multiple occuring whitespaces. + */ +function sanitizeWhitespace(string) { + return string ? (string + '').replace(RegExpWSChars, '').replace(RegExpMultiWS, ' ') : ''; +} + +/** + * Injects a string into another string at the index specified + */ +function injectIntoString(string, injection, index) { + return string.substr(0, index) + injection + string.substr(index); +} + +export { trim, trimLeft, underscore, sanitizeWhitespace, injectIntoString}; diff --git a/src/js/content-kit.js b/src/js/content-kit.js new file mode 100644 index 000000000..887c0fdba --- /dev/null +++ b/src/js/content-kit.js @@ -0,0 +1,27 @@ +import Type from './content-kit-compiler/types/type'; +import BlockModel from './content-kit-compiler/models/block'; +import TextModel from './content-kit-compiler/models/text'; +import ImageModel from './content-kit-compiler/models/image'; +import EmbedModel from './content-kit-compiler/models/embed'; +import Compiler from './content-kit-compiler/compiler'; +import HTMLParser from './content-kit-compiler/parsers/html-parser'; +import HTMLRenderer from './content-kit-compiler/renderers/html-renderer'; +import EditorFactory from './content-kit-editor/editor-factory'; + +/** + * @namespace ContentKit + * Merge public modules into the common ContentKit namespace. + * Handy for working in the browser with globals. + */ +var ContentKit = window.ContentKit || {}; +ContentKit.Type = Type; +ContentKit.BlockModel = BlockModel; +ContentKit.TextModel = TextModel; +ContentKit.ImageModel = ImageModel; +ContentKit.EmbedModel = EmbedModel; +ContentKit.Compiler = Compiler; +ContentKit.HTMLParser = HTMLParser; +ContentKit.HTMLRenderer = HTMLRenderer; +ContentKit.Editor = EditorFactory; + +export default ContentKit; diff --git a/src/js/editor.js b/src/js/editor.js deleted file mode 100644 index 633616d8d..000000000 --- a/src/js/editor.js +++ /dev/null @@ -1,250 +0,0 @@ -ContentKit.Editor = (function() { - - // Default `Editor` options - var defaults = { - defaultFormatter: Tags.PARAGRAPH, - placeholder: 'Write here...', - spellcheck: true, - autofocus: true, - textFormatCommands: TextFormatCommand.all, - embedCommands: EmbedCommand.all - }; - - var editorClassName = 'ck-editor'; - var editorClassNameRegExp = new RegExp(editorClassName); - - /** - * Publically expose this class which sets up indiviual `Editor` classes - * depending if user passes string selector, Node, or NodeList - */ - function EditorFactory(element, options) { - var editors = []; - var elements, elementsLen, i; - - if (typeof element === 'string') { - elements = document.querySelectorAll(element); - } else if (element && element.length) { - elements = element; - } else if (element) { - elements = [element]; - } - - if (elements) { - options = merge(defaults, options); - elementsLen = elements.length; - for (i = 0; i < elementsLen; i++) { - editors.push(new Editor(elements[i], options)); - } - } - - return editors.length > 1 ? editors : editors[0]; - } - - /** - * @class Editor - * An individual Editor - * @param element `Element` node - * @param options hash of options - */ - function Editor(element, options) { - var editor = this; - merge(editor, options); - - if (element) { - var className = element.className; - var dataset = element.dataset; - - if (!editorClassNameRegExp.test(className)) { - className += (className ? ' ' : '') + editorClassName; - } - element.className = className; - - if (!dataset.placeholder) { - dataset.placeholder = editor.placeholder; - } - if(!editor.spellcheck) { - element.spellcheck = false; - } - - element.setAttribute('contentEditable', true); - editor.element = element; - - var compiler = editor.compiler = options.compiler || new ContentKit.Compiler(); - editor.syncModel(); - - bindTypingEvents(editor); - bindPasteEvents(editor); - - editor.textFormatToolbar = new TextFormatToolbar({ rootElement: element, commands: editor.textFormatCommands }); - var linkTooltips = new Tooltip({ rootElement: element, showForTag: Tags.LINK }); - - if(editor.embedCommands) { - // NOTE: must come after bindTypingEvents so those keyup handlers are executed first. - // TODO: manage event listener order - var embedIntent = new EmbedIntent({ - editorContext: editor, - commands: editor.embedCommands, - rootElement: element - }); - - if (editor.imageServiceUrl) { - // TODO: lookup by name - editor.embedCommands[0].uploader.url = editor.imageServiceUrl; - } - } - - if(editor.autofocus) { element.focus(); } - } - } - - Editor.prototype.syncModel = function() { - this.model = this.compiler.parse(this.element.innerHTML); - }; - - Editor.prototype.syncModelAt = function(index) { - var blockElements = toArray(this.element.children); - var parsedBlockModel = this.compiler.parser.parseBlock(blockElements[index]); - this.model[index] = parsedBlockModel; - }; - - Editor.prototype.syncVisualAt = function(index) { - var blockModel = this.model[index]; - var html = this.compiler.render([blockModel]); - var blockElements = toArray(this.element.children); - var element = blockElements[index]; - element.innerHTML = html; - runAfterRenderHooks(element, blockModel); - }; - - Editor.prototype.getCurrentBlockIndex = function() { - var selectionEl = getSelectionBlockElement(); - var blockElements = toArray(this.element.children); - return blockElements.indexOf(selectionEl); - }; - - Editor.prototype.insertBlock = function(model) { - this.insertBlockAt(model, this.getCurrentBlockIndex()); - }; - - Editor.prototype.insertBlockAt = function(model, index) { - model = model || new ContentKit.TextModel(); - this.model.splice(index, 0, model); - }; - - Editor.prototype.addTextFormat = function(opts) { - var command = new TextFormatCommand(opts); - this.compiler.registerMarkupType(new ContentKit.Type({ - name : opts.name, - tag : opts.tag || opts.name - })); - this.textFormatCommands.push(command); - this.textFormatToolbar.addCommand(command); - }; - - Editor.prototype.willRenderType = function(type, renderer) { - this.compiler.renderer.willRenderType(type, renderer); - }; - - 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 === getSelectionBlockTagName()) { - document.execCommand('formatBlock', false, editor.defaultFormatter); - e.stopPropagation(); - } - } - }); - - // Creates unordered list when block starts with '- ', or ordered if starts with '1. ' - editorEl.addEventListener('keyup', function(e) { - var selectedText = window.getSelection().anchorNode.textContent, - selection, selectionNode, command, replaceRegex; - - if (Tags.LIST_ITEM !== getSelectionTagName()) { - if (Regex.UL_START.test(selectedText)) { - command = new UnorderedListCommand(); - replaceRegex = Regex.UL_START; - } else if (Regex.OL_START.test(selectedText)) { - command = new OrderedListCommand(); - replaceRegex = Regex.OL_START; - } - - if (command) { - command.exec(); - selection = window.getSelection(); - selectionNode = selection.anchorNode; - selectionNode.textContent = selectedText.replace(replaceRegex, ''); - moveCursorToBeginningOfSelection(selection); - e.stopPropagation(); - } - } - }); - - // Assure there is always a supported root tag, and not empty text nodes or divs. - editorEl.addEventListener('keyup', function() { - if (this.innerHTML.length && RootTags.indexOf(getSelectionBlockTagName()) === -1) { - document.execCommand('formatBlock', false, editor.defaultFormatter); - } - }); - - // Experimental: Live update - sync model with textual content as you type - editorEl.addEventListener('keyup', function(e) { - if (editor.model && editor.model.length) { - var index = editor.getCurrentBlockIndex(); - if (editor.model[index].type === 1) { - editor.syncModelAt(index); - } - } - }); - } - - var afterRenderHooks = []; - Editor.prototype.afterRender = function(callback) { - if ('function' === typeof callback) { - afterRenderHooks.push(callback); - } - }; - - function runAfterRenderHooks(element, blockModel) { - for (var i = 0, len = afterRenderHooks.length; i < len; i++) { - afterRenderHooks[i].call(null, element, blockModel); - } - } - - function bindPasteEvents(editor) { - editor.element.addEventListener('paste', function(e) { - var data = e.clipboardData, plainText; - e.preventDefault(); - if(data && data.getData) { - plainText = data.getData('text/plain'); - var formattedContent = plainTextToBlocks(plainText, editor.defaultFormatter); - document.execCommand('insertHTML', false, formattedContent); - } - }); - } - - function plainTextToBlocks(plainText, blockTag) { - var blocks = plainText.split(Regex.NEWLINE), - len = blocks.length, - block, openTag, closeTag, content, i; - if(len < 2) { - return plainText; - } else { - content = ''; - openTag = '<' + blockTag + '>'; - closeTag = ''; - for(i=0; i (http://garthpoitras.com/) - * @license MIT - * Last modified: Aug 10, 2014 - */ - -(function(window, document, define, undefined) { - -define("content-kit", - ["./types/type","./models/block","./models/text","./models/image","./models/embed","./compiler","./parsers/html-parser","./renderers/html-renderer","exports"], - function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __exports__) { - "use strict"; - var Type = __dependency1__["default"]; - var BlockModel = __dependency2__["default"]; - var TextModel = __dependency3__["default"]; - var ImageModel = __dependency4__["default"]; - var EmbedModel = __dependency5__["default"]; - var Compiler = __dependency6__["default"]; - var HTMLParser = __dependency7__["default"]; - var HTMLRenderer = __dependency8__["default"]; - - /** - * @namespace ContentKit - * Merge public modules into the common ContentKit namespace. - * Handy for working in the browser with globals. - */ - var ContentKit = window.ContentKit || {}; - ContentKit.Type = Type; - ContentKit.BlockModel = BlockModel; - ContentKit.TextModel = TextModel; - ContentKit.ImageModel = ImageModel; - ContentKit.EmbedModel = EmbedModel; - ContentKit.Compiler = Compiler; - ContentKit.HTMLParser = HTMLParser; - ContentKit.HTMLRenderer = HTMLRenderer; - - __exports__["default"] = ContentKit; - }); -define("compiler", - ["./parsers/html-parser","./renderers/html-renderer","./types/type","./types/default-types","../utils/object-utils","exports"], - function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __exports__) { - "use strict"; - var HTMLParser = __dependency1__["default"]; - var HTMLRenderer = __dependency2__["default"]; - var Type = __dependency3__["default"]; - var DefaultBlockTypeSet = __dependency4__.DefaultBlockTypeSet; - var DefaultMarkupTypeSet = __dependency4__.DefaultMarkupTypeSet; - var merge = __dependency5__.merge; - - /** - * @class Compiler - * @constructor - * @param options - */ - function Compiler(options) { - var parser = new HTMLParser(); - var renderer = new HTMLRenderer(); - var defaults = { - parser : parser, - renderer : renderer, - blockTypes : DefaultBlockTypeSet, - markupTypes : DefaultMarkupTypeSet, - includeTypeNames : false // true will output type_name: 'TEXT' etc. when parsing for easier debugging - }; - merge(this, defaults, options); - - // Reference the compiler settings - parser.blockTypes = renderer.blockTypes = this.blockTypes; - parser.markupTypes = renderer.markupTypes = this.markupTypes; - parser.includeTypeNames = this.includeTypeNames; - } - - /** - * @method parse - * @param input - * @return Object - */ - Compiler.prototype.parse = function(input) { - return this.parser.parse(input); - }; - - /** - * @method render - * @param data - * @return Object - */ - Compiler.prototype.render = function(data) { - return this.renderer.render(data); - }; - - /** - * @method registerBlockType - * @param {Type} type - */ - Compiler.prototype.registerBlockType = function(type) { - if (type instanceof Type) { - return this.blockTypes.addType(type); - } - }; - - /** - * @method registerMarkupType - * @param {Type} type - */ - Compiler.prototype.registerMarkupType = function(type) { - if (type instanceof Type) { - return this.markupTypes.addType(type); - } - }; - - __exports__["default"] = Compiler; - }); -define("models/block", - ["./model","../utils/object-utils","exports"], - function(__dependency1__, __dependency2__, __exports__) { - "use strict"; - var Model = __dependency1__["default"]; - var inherit = __dependency2__.inherit; - - /** - * Ensures block markups at the same index are always in a specific order. - * For example, so all bold links are consistently marked up - * as text instead of text - */ - function sortBlockMarkups(markups) { - return markups.sort(function(a, b) { - if (a.start === b.start && a.end === b.end) { - return b.type - a.type; - } - return 0; - }); - } - - /** - * @class BlockModel - * @constructor - * @extends Model - */ - function BlockModel(options) { - options = options || {}; - Model.call(this, options); - this.value = options.value || ''; - this.markup = sortBlockMarkups(options.markup || []); - } - inherit(BlockModel, Model); - - __exports__["default"] = BlockModel; - }); -define("models/embed", - ["../utils/object-utils","../models/model","../types/type","exports"], - function(__dependency1__, __dependency2__, __dependency3__, __exports__) { - "use strict"; - var inherit = __dependency1__.inherit; - var Model = __dependency2__["default"]; - var Type = __dependency3__["default"]; - - /** - * @class EmbedModel - * @constructor - * @extends Model - * Massages data from an oEmbed response into an EmbedModel - */ - function EmbedModel(options) { - if (!options) { return null; } - - Model.call(this, { - type: Type.EMBED.id, - type_name: Type.EMBED.name, - attributes: {} - }); - - var attributes = this.attributes; - var embedType = options.type; - var providerName = options.provider_name; - var embedUrl = options.url; - var embedTitle = options.title; - var embedThumbnail = options.thumbnail_url; - var embedHtml = options.html; - - if (embedType) { attributes.embed_type = embedType; } - if (providerName) { attributes.provider_name = providerName; } - if (embedUrl) { attributes.url = embedUrl; } - if (embedTitle) { attributes.title = embedTitle; } - - if (embedType === 'photo') { - attributes.thumbnail = options.media_url || embedUrl; - } else if (embedThumbnail) { - attributes.thumbnail = embedThumbnail; - } - - if (embedHtml && embedType === 'rich') { - attributes.html = embedHtml; - } - } - inherit(Model, EmbedModel); - - __exports__["default"] = EmbedModel; - }); -define("models/image", - ["./block","../types/type","../utils/object-utils","exports"], - function(__dependency1__, __dependency2__, __dependency3__, __exports__) { - "use strict"; - var BlockModel = __dependency1__["default"]; - var Type = __dependency2__["default"]; - var inherit = __dependency3__.inherit; - - /** - * @class ImageModel - * @constructor - * @extends BlockModel - * A simple BlockModel subclass representing an image - */ - function ImageModel(options) { - options = options || {}; - options.type = Type.IMAGE.id; - options.type_name = Type.IMAGE.name; - if (options.src) { - options.attributes = { src: options.src }; - } - BlockModel.call(this, options); - } - inherit(ImageModel, BlockModel); - - __exports__["default"] = ImageModel; - }); -define("models/markup", - ["./model","../utils/object-utils","exports"], - function(__dependency1__, __dependency2__, __exports__) { - "use strict"; - var Model = __dependency1__["default"]; - var inherit = __dependency2__.inherit; - - /** - * @class MarkupModel - * @constructor - * @extends Model - */ - function MarkupModel(options) { - options = options || {}; - Model.call(this, options); - this.start = options.start || 0; - this.end = options.end || 0; - } - inherit(MarkupModel, Model); - - __exports__["default"] = MarkupModel; - }); -define("models/model", - ["exports"], - function(__exports__) { - "use strict"; - /** - * @class Model - * @constructor - * @private - */ - function Model(options) { - options = options || {}; - var type_name = options.type_name; - var attributes = options.attributes; - - this.type = options.type || null; - if (type_name) { - this.type_name = type_name; - } - if (attributes) { - this.attributes = attributes; - } - } - - __exports__["default"] = Model; - }); -define("models/text", - ["./block","../types/type","../utils/object-utils","exports"], - function(__dependency1__, __dependency2__, __dependency3__, __exports__) { - "use strict"; - var BlockModel = __dependency1__["default"]; - var Type = __dependency2__["default"]; - var inherit = __dependency3__.inherit; - - /** - * @class TextModel - * @constructor - * @extends BlockModel - * A simple BlockModel subclass representing a paragraph of text - */ - function TextModel(options) { - options = options || {}; - options.type = Type.TEXT.id; - options.type_name = Type.TEXT.name; - BlockModel.call(this, options); - } - inherit(TextModel, BlockModel); - - __exports__["default"] = TextModel; - }); -define("parsers/html-parser", - ["../models/block","../models/markup","../types/default-types","../utils/object-utils","../utils/array-utils","../utils/string-utils","../utils/node-utils","exports"], - function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __exports__) { - "use strict"; - var BlockModel = __dependency1__["default"]; - var MarkupModel = __dependency2__["default"]; - var DefaultBlockTypeSet = __dependency3__.DefaultBlockTypeSet; - var DefaultMarkupTypeSet = __dependency3__.DefaultMarkupTypeSet; - var merge = __dependency4__.merge; - var toArray = __dependency5__.toArray; - var trim = __dependency6__.trim; - var trimLeft = __dependency6__.trimLeft; - var sanitizeWhitespace = __dependency6__.sanitizeWhitespace; - var createElement = __dependency7__.createElement; - var DOMParsingNode = __dependency7__.DOMParsingNode; - var textOfNode = __dependency7__.textOfNode; - var unwrapNode = __dependency7__.unwrapNode; - var attributesForNode = __dependency7__.attributesForNode; - - /** - * Gets the last block in the set or creates and return a default block if none exist yet. - */ - function getLastBlockOrCreate(parser, blocks) { - var block; - if (blocks.length) { - block = blocks[blocks.length - 1]; - } else { - block = parser.parseBlock(createElement(DefaultBlockTypeSet.TEXT.tag)); - blocks.push(block); - } - return block; - } - - /** - * Helper to retain stray elements at the root of the html that aren't blocks - */ - function handleNonBlockElementAtRoot(parser, elementNode, blocks) { - var block = getLastBlockOrCreate(parser, blocks), - markup = parser.parseElementMarkup(elementNode, block.value.length); - if (markup) { - block.markup.push(markup); - } - block.value += textOfNode(elementNode); - } - - /** - * @class HTMLParser - * @constructor - */ - function HTMLParser(options) { - var defaults = { - blockTypes : DefaultBlockTypeSet, - markupTypes : DefaultMarkupTypeSet, - includeTypeNames : false - }; - merge(this, defaults, options); - } - - /** - * @method parse - * @param html String of HTML content - * @return Array Parsed JSON content array - */ - HTMLParser.prototype.parse = function(html) { - DOMParsingNode.innerHTML = sanitizeWhitespace(html); - - var children = toArray(DOMParsingNode.childNodes), - len = children.length, - blocks = [], - i, currentNode, block, text; - - for (i = 0; i < len; i++) { - currentNode = children[i]; - // All top level nodes *should be* `Element` nodes and supported block types. - // We'll handle some cases if it isn't so we don't lose any content when parsing. - // Parser assumes sane input (such as from the ContentKit Editor) and is not intended to be a full html sanitizer. - if (currentNode.nodeType === 1) { - block = this.parseBlock(currentNode); - if (block) { - blocks.push(block); - } else { - handleNonBlockElementAtRoot(this, currentNode, blocks); - } - } else if (currentNode.nodeType === 3) { - text = currentNode.nodeValue; - if (trim(text)) { - block = getLastBlockOrCreate(this, blocks); - block.value += text; - } - } - } - - return blocks; - }; - - /** - * @method parseBlock - * @param node DOM node to parse - * @return {BlockModel} parsed block model - * Parses a single block type node into a model - */ - HTMLParser.prototype.parseBlock = function(node) { - var type = this.blockTypes.findByNode(node); - if (type) { - return new BlockModel({ - type : type.id, - type_name : this.includeTypeNames && type.name, - value : trim(textOfNode(node)), - attributes : attributesForNode(node), - markup : this.parseBlockMarkup(node) - }); - } - }; - - /** - * @method parseBlockMarkup - * @param node DOM node to parse - * @return {Array} parsed markups - * Parses a single block type node's markup - */ - HTMLParser.prototype.parseBlockMarkup = function(node) { - var processedText = '', - markups = [], - index = 0, - currentNode, markup; - - // Clone the node since it will be recursively torn down - node = node.cloneNode(true); - - while (node.hasChildNodes()) { - currentNode = node.firstChild; - if (currentNode.nodeType === 1) { - markup = this.parseElementMarkup(currentNode, processedText.length); - if (markup) { - markups.push(markup); - } - // unwrap the element so we can process any children - if (currentNode.hasChildNodes()) { - unwrapNode(currentNode); - } - } else if (currentNode.nodeType === 3) { - var text = sanitizeWhitespace(currentNode.nodeValue); - if (index === 0) { text = trimLeft(text); } - if (text) { processedText += text; } - } - - // node has been processed, remove it - currentNode.parentNode.removeChild(currentNode); - index++; - } - - return markups; - }; - - /** - * @method parseElementMarkup - * @param node DOM node to parse - * @param startIndex DOM node to parse - * @return {MarkupModel} parsed markup model - * Parses markup of a single html element node - */ - HTMLParser.prototype.parseElementMarkup = function(node, startIndex) { - var type = this.markupTypes.findByNode(node), - selfClosing, endIndex; - - if (type) { - selfClosing = type.selfClosing; - if (!selfClosing && !node.hasChildNodes()) { return; } // check for empty nodes - - endIndex = startIndex + (selfClosing ? 0 : textOfNode(node).length); - if (endIndex > startIndex || (selfClosing && endIndex === startIndex)) { // check for empty nodes - return new MarkupModel({ - type : type.id, - type_name : this.includeTypeNames && type.name, - start : startIndex, - end : endIndex, - attributes : attributesForNode(node) - }); - } - } - }; - - __exports__["default"] = HTMLParser; - }); -define("renderers/html-element-renderer", - ["../utils/string-utils","../utils/array-utils","exports"], - function(__dependency1__, __dependency2__, __exports__) { - "use strict"; - var injectIntoString = __dependency1__.injectIntoString; - var sumSparseArray = __dependency2__.sumSparseArray; - - /** - * Builds an opening html tag. i.e. ''; - }; - - __exports__["default"] = TwitterRenderer; - }); -define("renderers/embeds/youtube", - ["exports"], - function(__exports__) { - "use strict"; - - var RegExVideoId = /.*(?:youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=)([^#\&\?]*).*/; - - function getVideoIdFromUrl(url) { - var match = url.match(RegExVideoId); - if (match && match[1].length === 11){ - return match[1]; - } - return null; - } - - function YouTubeRenderer() {} - YouTubeRenderer.prototype.render = function(model) { - var videoId = getVideoIdFromUrl(model.attributes.url); - var embedUrl = 'http://www.youtube.com/embed/' + videoId + '?controls=2&showinfo=0&color=white&theme=light'; - return ''; - }; - - __exports__["default"] = YouTubeRenderer; - }); -}(this, document, define)); diff --git a/src/js/index.js b/src/js/index.js deleted file mode 100644 index b88346861..000000000 --- a/src/js/index.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * @namespace ContentKit - */ -var ContentKit = exports.ContentKit || {}; -exports.ContentKit = ContentKit; diff --git a/src/js/utils/array-utils.js b/src/js/utils/array-utils.js deleted file mode 100644 index 1a6efc5bb..000000000 --- a/src/js/utils/array-utils.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Converts an array-like object (i.e. NodeList) to Array - */ -function toArray(obj) { - var array = [], - i = obj.length >>> 0; // cast to Uint32 - while (i--) { - array[i] = obj[i]; - } - return array; -} diff --git a/src/js/utils/http-utils.js b/src/js/utils/http-utils.js deleted file mode 100644 index caadef1ac..000000000 --- a/src/js/utils/http-utils.js +++ /dev/null @@ -1,33 +0,0 @@ -var HTTP = (function() { - - var head = document.head; - var uuid = 0; - - return { - get: function(url, callback) { - var request = new XMLHttpRequest(); - request.onload = function() { - callback(this.responseText); - }; - request.onerror = function(error) { - callback(null, error); - }; - request.open('GET', url); - request.send(); - }, - - jsonp: function(url, callback) { - var script = document.createElement('script'); - var name = '_jsonp_' + uuid++; - url += ( url.match(/\?/) ? '&' : '?' ) + 'callback=' + name; - script.src = url; - exports[name] = function(response) { - callback(JSON.parse(response)); - head.removeChild(script); - delete exports[name]; - }; - head.appendChild(script); - } - }; - -}()); diff --git a/src/js/utils/object-utils.js b/src/js/utils/object-utils.js deleted file mode 100644 index 604a8b0fe..000000000 --- a/src/js/utils/object-utils.js +++ /dev/null @@ -1,21 +0,0 @@ -function merge(object, updates) { - updates = updates || {}; - for(var o in updates) { - if (updates.hasOwnProperty(o)) { - object[o] = updates[o]; - } - } - return object; -} - -function inherits(Subclass, Superclass) { - Subclass._super = Superclass; - Subclass.prototype = Object.create(Superclass.prototype, { - constructor: { - value: Subclass, - enumerable: false, - writable: true, - configurable: true - } - }); -} diff --git a/src/js/views/embed-intent.js b/src/js/views/embed-intent.js deleted file mode 100644 index 46e761ac8..000000000 --- a/src/js/views/embed-intent.js +++ /dev/null @@ -1,106 +0,0 @@ -var EmbedIntent = (function() { - - function EmbedIntent(options) { - var embedIntent = this; - var rootElement = options.rootElement; - options.tagName = 'button'; - options.classNames = ['ck-embed-intent-btn']; - View.call(embedIntent, options); - - embedIntent.editorContext = options.editorContext; - embedIntent.element.title = 'Insert image or embed...'; - embedIntent.element.addEventListener('mouseup', function(e) { - if (embedIntent.isActive) { - embedIntent.deactivate(); - } else { - embedIntent.activate(); - } - e.stopPropagation(); - }); - - embedIntent.toolbar = new Toolbar({ embedIntent: embedIntent, editor: embedIntent.editorContext, commands: options.commands, direction: ToolbarDirection.RIGHT }); - embedIntent.isActive = false; - - function embedIntentHandler() { - var blockElement = getSelectionBlockElement(); - var blockElementContent = blockElement && blockElement.innerHTML; - if (blockElementContent === '' || blockElementContent === '
') { - embedIntent.showAt(blockElement); - } else { - embedIntent.hide(); - } - } - - rootElement.addEventListener('keyup', embedIntentHandler); - - 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) { - embedIntent.hide(); - } - }); - - 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.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; - } - }; - - var loading = createDiv('div'); - loading.className = 'ck-embed-loading'; - loading.innerHTML = 'LOADING'; - EmbedIntent.prototype.showLoading = function() { - this.hide(); - document.body.appendChild(loading); - positionElementCenteredIn(loading, this.atNode); - }; - - EmbedIntent.prototype.hideLoading = function() { - document.body.removeChild(loading); - }; - - - return EmbedIntent; -}()); diff --git a/src/js/views/prompt.js b/src/js/views/prompt.js deleted file mode 100644 index 7207505b1..000000000 --- a/src/js/views/prompt.js +++ /dev/null @@ -1,63 +0,0 @@ -var Prompt = (function() { - - var container = document.body; - var hiliter = createDiv('ck-editor-hilite'); - - function Prompt(options) { - 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 && prompt.range && !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.show = function(callback) { - var prompt = this; - var element = prompt.element; - var selection = window.getSelection(); - var range = selection && selection.rangeCount && selection.getRangeAt(0); - element.value = null; - prompt.range = range || null; - if (range) { - container.appendChild(hiliter); - positionHiliteRange(prompt.range); - setTimeout(function(){ element.focus(); }); // defer focus (disrupts mouseup events) - if (callback) { prompt.onComplete = callback; } - } - }; - - Prompt.prototype.hide = function() { - if (hiliter.parentNode) { - container.removeChild(hiliter); - } - }; - - function positionHiliteRange(range) { - var rect = range.getBoundingClientRect(); - var style = hiliter.style; - style.width = rect.width + 'px'; - style.height = rect.height + 'px'; - positionElementToRect(hiliter, rect); - } - - return Prompt; -}()); diff --git a/src/js/views/toolbar-button.js b/src/js/views/toolbar-button.js deleted file mode 100644 index 791a8a343..000000000 --- a/src/js/views/toolbar-button.js +++ /dev/null @@ -1,50 +0,0 @@ -var ToolbarButton = (function() { - - var buttonClassName = 'ck-toolbar-btn'; - - function ToolbarButton(options) { - var button = this; - var toolbar = options.toolbar; - var command = options.command; - var prompt = command.prompt; - var element = document.createElement('button'); - - if(typeof command === 'string') { - command = Command.index[command]; - } - - button.element = element; - button.command = command; - button.isActive = false; - - element.title = command.name; - element.className = buttonClassName; - element.innerHTML = command.button; - element.addEventListener('click', function(e) { - if (!button.isActive && prompt) { - toolbar.displayPrompt(prompt); - } else { - command.exec(); - } - }); - } - - ToolbarButton.prototype = { - setActive: function() { - var button = this; - if (!button.isActive) { - button.element.className = buttonClassName + ' active'; - button.isActive = true; - } - }, - setInactive: function() { - var button = this; - if (button.isActive) { - button.element.className = buttonClassName; - button.isActive = false; - } - } - }; - - return ToolbarButton; -}()); diff --git a/src/js/views/toolbar.js b/src/js/views/toolbar.js deleted file mode 100644 index 560385e06..000000000 --- a/src/js/views/toolbar.js +++ /dev/null @@ -1,156 +0,0 @@ -var Toolbar = (function() { - - function Toolbar(options) { - var toolbar = this; - var commands = options.commands; - var commandCount = commands && commands.length; - var i, button, command; - toolbar.editor = options.editor || null; - toolbar.embedIntent = options.embedIntent || null; - toolbar.direction = options.direction || ToolbarDirection.TOP; - options.classNames = ['ck-toolbar']; - if (toolbar.direction === ToolbarDirection.RIGHT) { - options.classNames.push('right'); - } - - View.call(toolbar, options); - - toolbar.activePrompt = null; - toolbar.buttons = []; - - toolbar.promptContainerElement = createDiv('ck-toolbar-prompt'); - toolbar.buttonContainerElement = createDiv('ck-toolbar-buttons'); - toolbar.element.appendChild(toolbar.promptContainerElement); - toolbar.element.appendChild(toolbar.buttonContainerElement); - - for(i = 0; i < commandCount; i++) { - this.addCommand(commands[i]); - } - - // Closes prompt if displayed when changing selection - document.addEventListener('mouseup', function() { - toolbar.dismissPrompt(); - }); - } - 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(); - } - }; - - Toolbar.prototype.addCommand = function(command) { - command.editorContext = this.editor; - command.embedIntent = this.embedIntent; - var button = new ToolbarButton({ command: command, toolbar: this }); - this.buttons.push(button); - this.buttonContainerElement.appendChild(button.element); - }; - - 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; - }; - - Toolbar.prototype.dismissPrompt = function() { - 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) { - button.setActive(); - } else { - button.setInactive(); - } - } - } - - 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 || !selectionIsEditable(selection) || selection.toString().trim() === '' || !selectionIsInElement(selection, toolbar.rootElement)) { - toolbar.hide(); - } else { - toolbar.updateForSelection(selection); - } - }; - - return TextFormatToolbar; -}());