/* * Medium.js * * Copyright 2013, Jacob Kelley - http://jakiestfu.com/ * Released under the MIT Licence * http://opensource.org/licenses/MIT * * Github: http://github.com/jakiestfu/Medium.js/ * Version: 1.0 */ (function (w, d) { 'use strict'; var Medium = (function () { var trim = function (string) { return string.replace(/^\s+|\s+$/g, ''); }, arrayContains = function (array, variable) { var i = array.length; while (i--) { if (array[i] === variable) { return true; } } return false; }, //two modes, wild (native) or domesticated (rangy + undo.js) rangy = w['rangy'] || null, undo = w['Undo'] || null, wild = (!rangy || !undo), domesticated = (!wild), key = w.Key = { 'backspace': 8, 'tab': 9, 'enter': 13, 'shift': 16, 'ctrl': 17, 'alt': 18, 'pause': 19, 'capsLock': 20, 'escape': 27, 'pageUp': 33, 'pageDown': 34, 'end': 35, 'home': 36, 'leftArrow': 37, 'upArrow': 38, 'rightArrow': 39, 'downArrow': 40, 'insert': 45, 'delete': 46, '0': 48, '1': 49, '2': 50, '3': 51, '4': 52, '5': 53, '6': 54, '7': 55, '8': 56, '9': 57, 'a': 65, 'b': 66, 'c': 67, 'd': 68, 'e': 69, 'f': 70, 'g': 71, 'h': 72, 'i': 73, 'j': 74, 'k': 75, 'l': 76, 'm': 77, 'n': 78, 'o': 79, 'p': 80, 'q': 81, 'r': 82, 's': 83, 't': 84, 'u': 85, 'v': 86, 'w': 87, 'x': 88, 'y': 89, 'z': 90, 'leftWindow': 91, 'rightWindowKey': 92, 'select': 93, 'numpad0': 96, 'numpad1': 97, 'numpad2': 98, 'numpad3': 99, 'numpad4': 100, 'numpad5': 101, 'numpad6': 102, 'numpad7': 103, 'numpad8': 104, 'numpad9': 105, 'multiply': 106, 'add': 107, 'subtract': 109, 'decimalPoint': 110, 'divide': 111, 'f1': 112, 'f2': 113, 'f3': 114, 'f4': 115, 'f5': 116, 'f6': 117, 'f7': 118, 'f8': 119, 'f9': 120, 'f10': 121, 'f11': 122, 'f12': 123, 'numLock': 144, 'scrollLock': 145, 'semiColon': 186, 'equalSign': 187, 'comma': 188, 'dash': 189, 'period': 190, 'forwardSlash': 191, 'graveAccent': 192, 'openBracket': 219, 'backSlash': 220, 'closeBraket': 221, 'singleQuote': 222 }, /** * Medium.js - Taking control of content editable * @constructor * @param {Object} [userSettings] user options */ Medium = function (userSettings) { var medium = this, action = new Medium.Action(), cache = new Medium.Cache(), cursor = new Medium.Cursor(), html = new Medium.HtmlAssistant(), utils = new Medium.Utilities(), selection = new Medium.Selection(), intercept = { focus: function (e) { e = e || w.event; Medium.activeElement = el; }, blur: function (e) { e = e || w.event; if (Medium.activeElement === el) { Medium.activeElement = null; } html.placeholders(); }, down: function (e) { e = e || w.event; var keepEvent = true; //in Chrome it sends out this event before every regular event, not sure why if (e.keyCode === 229) return; utils.isCommand(e, function () { cache.cmd = true; }, function () { cache.cmd = false; }); utils.isShift(e, function () { cache.shift = true; }, function () { cache.shift = false; }); utils.isModifier(e, function (cmd) { if (cache.cmd) { if (( (settings.mode === Medium.inlineMode) || (settings.mode === Medium.partialMode) ) && cmd !== "paste") { utils.preventDefaultEvent(e); return; } var cmdType = typeof cmd, fn = null; if (cmdType === "function") { fn = cmd; } else { fn = intercept.command[cmd]; } keepEvent = fn.call(medium, e); if (keepEvent === false) { utils.preventDefaultEvent(e); utils.stopPropagation(e); } } }); if (settings.maxLength !== -1) { var len = html.text().length, hasSelection = false, selection = w.getSelection(); if (selection) { hasSelection = !selection.isCollapsed; } if (len >= settings.maxLength && !utils.isSpecial(e) && !utils.isNavigational(e) && !hasSelection) { return utils.preventDefaultEvent(e); } } switch (e.keyCode) { case key['enter']: intercept.enterKey(e); break; case key['backspace']: case key['delete']: intercept.backspaceOrDeleteKey(e); break; } return keepEvent; }, up: function (e) { e = e || w.event; utils.isCommand(e, function () { cache.cmd = false; }, function () { cache.cmd = true; }); html.clean(); html.placeholders(); //here we have a key context, so if you need to create your own object within a specific context it is doable var keyContext; if ( settings.keyContext !== null && ( keyContext = settings.keyContext[e.keyCode] ) ) { var el = cursor.parent(); if (el) { keyContext.call(medium, e, el); } } action.preserveElementFocus(); }, command: { bold: function (e) { utils.preventDefaultEvent(e); // IE uses strong instead of b (new Medium.Element(medium, 'bold')) .setClean(false) .invoke(settings.beforeInvokeElement); }, underline: function (e) { utils.preventDefaultEvent(e); (new Medium.Element(medium, 'underline')) .setClean(false) .invoke(settings.beforeInvokeElement); }, italicize: function (e) { utils.preventDefaultEvent(e); (new Medium.Element(medium, 'italic')) .setClean(false) .invoke(settings.beforeInvokeElement); }, quote: function (e) { }, paste: function (e) { medium.makeUndoable(); if (settings.pasteAsText) { var sel = utils.selection.saveSelection(); utils.pasteHook(function (text) { utils.selection.restoreSelection(sel); text = text.replace(/\n/g, '
'); (new Medium.Html(medium, text)) .setClean(false) .insert(settings.beforeInsertHtml, true); html.clean(); html.placeholders(); }); } else { html.clean(); html.placeholders(); } } }, enterKey: function (e) { if (settings.mode === Medium.inlineMode) { return utils.preventDefaultEvent(e); } if (!cache.shift) { var focusedElement = html.atCaret() || {}, children = el.children, lastChild = focusedElement === el.lastChild ? el.lastChild : null, makeHR, secondToLast, paragraph; if ( lastChild && lastChild !== el.firstChild && settings.autoHR && settings.mode !== 'partial' && settings.tags.horizontalRule ) { utils.preventDefaultEvent(e); makeHR = html.text(lastChild) === "" && lastChild.nodeName.toLowerCase() === settings.tags.paragraph; if (makeHR && children.length >= 2) { secondToLast = children[children.length - 2]; if (secondToLast.nodeName.toLowerCase() === settings.tags.horizontalRule) { makeHR = false; } } if (makeHR) { html.addTag(settings.tags.horizontalRule, false, true, focusedElement); focusedElement = focusedElement.nextSibling; } if ((paragraph = html.addTag(settings.tags.paragraph, true, null, focusedElement)) !== null) { paragraph.innerHTML = ''; cursor.set(0, paragraph); } } } return true; }, backspaceOrDeleteKey: function (e) { if (el.lastChild === null) return; var lastChild = el.lastChild, beforeLastChild = lastChild.previousSibling; if ( lastChild && settings.tags.horizontalRule && lastChild.nodeName.toLocaleLowerCase() === settings.tags.horizontalRule ) { el.removeChild(lastChild); } else if ( lastChild && beforeLastChild && utils.html.text(lastChild).length < 1 && beforeLastChild.nodeName.toLowerCase() === settings.tags.horizontalRule && lastChild.nodeName.toLowerCase() === settings.tags.paragraph ) { el.removeChild(lastChild); el.removeChild(beforeLastChild); } } }, defaultSettings = { element: null, modifier: 'auto', placeholder: "", autofocus: false, autoHR: true, mode: Medium.richMode, maxLength: -1, modifiers: { 'b': 'bold', 'i': 'italicize', 'u': 'underline', 'v': 'paste' }, tags: { 'break': 'br', 'horizontalRule': 'hr', 'paragraph': 'p', 'outerLevel': ['pre', 'blockquote', 'figure'], 'innerLevel': ['a', 'b', 'u', 'i', 'img', 'strong'] }, cssClasses: { editor: 'Medium', pasteHook: 'Medium-paste-hook', placeholder: 'Medium-placeholder', clear: 'Medium-clear' }, attributes: { remove: ['style', 'class'] }, pasteAsText: true, beforeInvokeElement: function () { //this = Medium.Element }, beforeInsertHtml: function () { //this = Medium.Html }, beforeAddTag: function (tag, shouldFocus, isEditable, afterElement) { }, keyContext: null, pasteEventHandler: function (e) { e = e || w.event; medium.makeUndoable(); var length = medium.value().length, totalLength; if (settings.pasteAsText) { utils.preventDefaultEvent(e); var sel = utils.selection.saveSelection(), text = prompt(Medium.Messages.pastHere) || ''; if (text.length > 0) { el.focus(); Medium.activeElement = el; utils.selection.restoreSelection(sel); //encode the text first text = html.encodeHtml(text); //cut down it's length totalLength = text.length + length; if (settings.maxLength > 0 && totalLength > settings.maxLength) { text = text.substring(0, settings.maxLength - length); } if (settings.mode !== Medium.inlineMode) { text = text.replace(/\n/g, '
'); } (new Medium.Html(medium, text)) .setClean(false) .insert(settings.beforeInsertHtml, true); html.clean(); html.placeholders(); return false; } } else { setTimeout(function () { html.clean(); html.placeholders(); }, 20); } } }, settings = utils.deepExtend(defaultSettings, userSettings), el, newVal, i, bridge = {}; for (i in defaultSettings) { // Override defaults with data-attributes if ( typeof defaultSettings[i] !== 'object' && defaultSettings.hasOwnProperty(i) && settings.element.getAttribute('data-medium-' + key) ) { newVal = settings.element.getAttribute('data-medium-' + key); if (newVal.toLowerCase() === "false" || newVal.toLowerCase() === "true") { newVal = newVal.toLowerCase() === "true"; } settings[i] = newVal; } } if (settings.modifiers) { for (i in settings.modifiers) { if (typeof(key[i]) !== 'undefined') { settings.modifiers[key[i]] = settings.modifiers[i]; } } } if (settings.keyContext) { for (i in settings.keyContext) { if (typeof(key[i]) !== 'undefined') { settings.keyContext[key[i]] = settings.keyContext[i]; } } } // Extend Settings el = settings.element; // Editable el.contentEditable = true; el.className += (' ' + settings.cssClasses.editor) + (' ' + settings.cssClasses.editor + '-' + settings.mode); settings.tags = (settings.tags || {}); if (settings.tags.outerLevel) { settings.tags.outerLevel = settings.tags.outerLevel.concat([settings.tags.paragraph, settings.tags.horizontalRule]); } this.settings = settings; this.element = el; this.intercept = intercept; this.action = action; this.cache = cache; this.cursor = cursor; this.html = html; this.utils = utils; this.selection = selection; bridge.element = el; bridge.medium = this; bridge.settings = settings; bridge.action = action; bridge.cache = cache; bridge.cursor = cursor; bridge.html = html; bridge.intercept = intercept; bridge.utils = utils; bridge.selection = selection; action.setBridge(bridge); cache.setBridge(bridge); cursor.setBridge(bridge); html.setBridge(bridge); utils.setBridge(bridge); selection.setBridge(bridge); // Initialize editor html.clean(); html.placeholders(); action.preserveElementFocus(); // Capture Events action.listen(); if (wild) { this.makeUndoable = function () { }; } else { this.dirty = false; this.undoable = new Medium.Undoable(this); this.undo = this.undoable.undo; this.redo = this.undoable.redo; this.makeUndoable = this.undoable.makeUndoable; } el.medium = this; // Set as initialized cache.initialized = true; }; Medium.prototype = { /** * * @param {String|Object} html * @param {Function} [callback] * @returns {Medium} */ insertHtml: function (html, callback) { var result = (new Medium.Html(this, html)) .insert(this.settings.beforeInsertHtml); this.utils.triggerEvent(this.element, "change"); if (callback) { callback.apply(result); } return this; }, /** * * @param {String} tagName * @param {Object} [attributes] * @returns {Medium} */ invokeElement: function (tagName, attributes) { var settings = this.settings, attributes = attributes || {}, remove = attributes.remove || []; switch (settings.mode) { case Medium.inlineMode: case Medium.partialMode: return this; default: } //invoke works off class, so if it isn't there, we just add it if (remove.length > 0) { if (!arrayContains(settings, 'class')) { remove.push('class'); } } (new Medium.Element(this, tagName, attributes)) .invoke(this.settings.beforeInvokeElement); this.utils.triggerEvent(this.element, "change"); return this; }, /** * @returns {string} */ behavior: function () { return (wild ? 'wild' : 'domesticated'); }, /** * * @param value * @returns {Medium} */ value: function (value) { if (typeof value !== 'undefined') { this.element.innerHTML = value; this.html.clean(); this.html.placeholders(); } else { return this.element.innerHTML; } return this; }, /** * Focus on element * @returns {Medium} */ focus: function () { var el = this.element; el.focus(); return this; }, /** * Select all text * @returns {Medium} */ select: function () { var el = this.element, range, selection; el.focus(); if (d.body.createTextRange) { range = d.body.createTextRange(); range.moveToElementText(el); range.select(); } else if (w.getSelection) { selection = w.getSelection(); range = d.createRange(); range.selectNodeContents(el); selection.removeAllRanges(); selection.addRange(range); } return this; }, isActive: function () { return (Medium.activeElement === this.element); }, destroy: function () { var el = this.element, intercept = this.intercept, settings = this.settings, placeholder = this.placeholder || null; if (placeholder !== null && placeholder.setup) { //remove placeholder placeholder.parentNode.removeChild(placeholder); delete el.placeHolderActive; } //remove contenteditable el.removeAttribute('contenteditable'); //remove classes el.className = trim(el.className .replace(settings.cssClasses.editor, '') .replace(settings.cssClasses.clear, '') .replace(settings.cssClasses.editor + '-' + settings.mode, '')); //remove events this.utils .removeEvent(el, 'keyup', intercept.up) .removeEvent(el, 'keydown', intercept.down) .removeEvent(el, 'focus', intercept.focus) .removeEvent(el, 'blur', intercept.focus) .removeEvent(el, 'paste', settings.pasteEventHandler); }, // Clears the element and restores the placeholder clear: function () { this.element.innerHTML = ''; this.html.placeholders(); } }; /** * @param {Medium} medium * @param {String} tagName * @param {Object} [attributes] * @constructor */ Medium.Element = function (medium, tagName, attributes) { this.medium = medium; this.element = medium.settings.element; if (wild) { this.tagName = tagName; } else { switch (tagName.toLowerCase()) { case 'bold': this.tagName = 'b'; break; case 'italic': this.tagName = 'i'; break; case 'underline': this.tagName = 'u'; break; default: this.tagName = tagName; } } this.attributes = attributes || {}; this.clean = true; }; /** * @constructor * @param {Medium} medium * @param {String|HtmlElement} html */ Medium.Html = function (medium, html) { this.medium = medium; this.element = medium.settings.element; this.html = html; this.clean = true; }; /** * * @constructor */ Medium.Injector = function () { }; if (wild) { Medium.Element.prototype = { /** * @methodOf Medium.Element * @param {Function} [fn] */ invoke: function (fn) { if (Medium.activeElement === this.element) { if (fn) { fn.apply(this); } d.execCommand(this.tagName, false); } }, setClean: function () { return this; } }; Medium.Injector.prototype = { /** * @methodOf Medium.Injector * @param {String|HtmlElement} htmlRaw * @param {Boolean} [selectInserted] * @returns {null} */ inject: function (htmlRaw, selectInserted) { this.insertHTML(htmlRaw, selectInserted); return null; } }; /** * * @constructor */ Medium.Undoable = function () { }; } //if medium is domesticated (ie, not wild) else { rangy.rangePrototype.insertNodeAtEnd = function (node) { var range = this.cloneRange(); range.collapse(false); range.insertNode(node); range.detach(); this.setEndAfter(node); }; Medium.Element.prototype = { /** * @methodOf Medium.Element * @param {Function} [fn] */ invoke: function (fn) { if (Medium.activeElement === this.element) { if (fn) { fn.apply(this); } var attr = this.attributes, tagName = this.tagName.toLowerCase(), applier, cl; if (attr.className !== undefined) { cl = (attr.className.split[' '] || [attr.className]).shift(); delete attr.className; } else { cl = 'medium-' + tagName; } applier = rangy.createClassApplier(cl, { elementTagName: tagName, elementAttributes: this.attributes }); this.medium.makeUndoable(); applier.toggleSelection(w); if (this.clean) { //cleanup this.medium.html.clean(); this.medium.html.placeholders(); } } }, /** * * @param {Boolean} clean * @returns {Medium.Element} */ setClean: function (clean) { this.clean = clean; return this; } }; Medium.Injector.prototype = { /** * @methodOf Medium.Injector * @param {String|HtmlElement} htmlRaw * @returns {HtmlElement} */ inject: function (htmlRaw) { var html, isConverted = false; if (typeof htmlRaw === 'string') { var htmlConverter = d.createElement('div'); htmlConverter.innerHTML = htmlRaw; html = htmlConverter.childNodes; isConverted = true; } else { html = htmlRaw; } this.insertHTML(''); var wedge = d.getElementById('wedge'), parent = wedge.parentNode, i = 0; wedge.removeAttribute('id'); if (isConverted) { while (i < html.length) { parent.insertBefore(html[i], wedge); } } else { parent.insertBefore(html, wedge); } parent.removeChild(wedge); wedge = null; return html; } }; /** * @param {Medium} medium * @constructor */ Medium.Undoable = function (medium) { var me = this, element = medium.settings.element, utils = medium.utils, addEvent = utils.addEvent, startValue = element.innerHTML, timer, stack = new Undo.Stack(), EditCommand = Undo.Command.extend({ constructor: function (oldValue, newValue) { this.oldValue = oldValue; this.newValue = newValue; }, execute: function () { }, undo: function () { element.innerHTML = this.oldValue; medium.canUndo = stack.canUndo(); medium.canRedo = stack.canRedo(); medium.dirty = stack.dirty(); }, redo: function () { element.innerHTML = this.newValue; medium.canUndo = stack.canUndo(); medium.canRedo = stack.canRedo(); medium.dirty = stack.dirty(); } }), makeUndoable = function () { var newValue = element.innerHTML; // ignore meta key presses if (newValue != startValue) { if (!me.movingThroughStack) { // this could try and make a diff instead of storing snapshots stack.execute(new EditCommand(startValue, newValue)); startValue = newValue; medium.dirty = stack.dirty(); } utils.triggerEvent(medium.settings.element, "change"); } }; this.medium = medium; this.timer = timer; this.stack = stack; this.makeUndoable = makeUndoable; this.EditCommand = EditCommand; this.movingThroughStack = false; addEvent(element, 'keyup', function (e) { if (e.ctrlKey || e.keyCode === key.z) { utils.preventDefaultEvent(e); return; } // a way too simple algorithm in place of single-character undo clearTimeout(timer); timer = setTimeout(function () { makeUndoable(); }, 250); }); addEvent(element, 'keydown', function (e) { if (!e.ctrlKey || e.keyCode !== key.z) { me.movingThroughStack = false; return true; } utils.preventDefaultEvent(e); me.movingThroughStack = true; if (e.shiftKey) { stack.canRedo() && stack.redo() } else { stack.canUndo() && stack.undo(); } }); }; } //Thank you Tim Down (super uber genius): http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div/6691294#6691294 Medium.Injector.prototype.insertHTML = function (html, selectPastedContent) { var sel, range; if (w.getSelection) { // IE9 and non-IE sel = w.getSelection(); if (sel.getRangeAt && sel.rangeCount) { range = sel.getRangeAt(0); range.deleteContents(); // Range.createContextualFragment() would be useful here but is // only relatively recently standardized and is not supported in // some browsers (IE9, for one) var el = d.createElement("div"); el.innerHTML = html; var frag = d.createDocumentFragment(), node, lastNode; while ((node = el.firstChild)) { lastNode = frag.appendChild(node); } var firstNode = frag.firstChild; range.insertNode(frag); // Preserve the selection if (lastNode) { range = range.cloneRange(); range.setStartAfter(lastNode); if (selectPastedContent) { range.setStartBefore(firstNode); } else { range.collapse(true); } sel.removeAllRanges(); sel.addRange(range); } } } else if ((sel = d.selection) && sel.type != "Control") { // IE < 9 var originalRange = sel.createRange(); originalRange.collapse(true); sel.createRange().pasteHTML(html); if (selectPastedContent) { range = sel.createRange(); range.setEndPoint("StartToStart", originalRange); range.select(); } } }; Medium.Html.prototype = { setBridge: function (bridge) { for (var i in bridge) { this[i] = bridge[i]; } }, /** * @methodOf Medium.Html * @param {Function} [fn] * @param {Boolean} [selectInserted] * @returns {HtmlElement} */ insert: function (fn, selectInserted) { if (Medium.activeElement === this.element) { if (fn) { fn.apply(this); } var inserted = this.injector.inject(this.html, selectInserted); if (this.clean) { //cleanup this.medium.html.clean(); this.medium.html.placeholders(); } this.medium.makeUndoable(); return inserted; } else { return null; } }, /** * @attributeOf {Medium.Injector} Medium.Html */ injector: new Medium.Injector(), /** * @methodOf Medium.Html * @param clean * @returns {Medium.Html} */ setClean: function (clean) { this.clean = clean; return this; } }; Medium.Utilities = function () { }; Medium.Utilities.prototype = { setBridge: function (bridge) { for (var i in bridge) { this[i] = bridge[i]; } }, /* * Keyboard Interface events */ isCommand: function (e, fnTrue, fnFalse) { var s = this.settings; if ((s.modifier === 'ctrl' && e.ctrlKey ) || (s.modifier === 'cmd' && e.metaKey ) || (s.modifier === 'auto' && (e.ctrlKey || e.metaKey) ) ) { return fnTrue.call(); } else { return fnFalse.call(); } }, isShift: function (e, fnTrue, fnFalse) { if (e.shiftKey) { return fnTrue.call(); } else { return fnFalse.call(); } }, isModifier: function (e, fn) { var cmd = this.settings.modifiers[e.keyCode]; if (cmd) { return fn.call(null, cmd); } return false; }, special: (function () { var special = {}; special[key['backspace']] = true; special[key['shift']] = true; special[key['ctrl']] = true; special[key['alt']] = true; special[key['delete']] = true; special[key['cmd']] = true; return special; })(), isSpecial: function (e) { if (this.cache.cmd) { return true; } return typeof this.special[e.keyCode] !== 'undefined'; }, navigational: (function () { var navigational = {}; navigational[key['upArrow']] = true; navigational[key['downArrow']] = true; navigational[key['leftArrow']] = true; navigational[key['rightArrow']] = true; return navigational; })(), isNavigational: function (e) { return typeof this.navigational[e.keyCode] !== 'undefined'; }, /* * Handle Events */ addEvent: function addEvent(element, eventName, func) { if (element.addEventListener) { element.addEventListener(eventName, func, false); } else if (element.attachEvent) { element.attachEvent("on" + eventName, func); } else { element['on' + eventName] = func; } return this; }, removeEvent: function removeEvent(element, eventName, func) { if (element.removeEventListener) { element.removeEventListener(eventName, func, false); } else if (element.detachEvent) { element.detachEvent("on" + eventName, func); } else { element['on' + eventName] = null; } return this; }, preventDefaultEvent: function (e) { if (e.preventDefault) { e.preventDefault(); } else { e.returnValue = false; } return this; }, stopPropagation: function (e) { e = e || window.event; e.cancelBubble = true; if (e.stopPropagation !== undefined) { e.stopPropagation(); } }, triggerEvent: function (element, eventName) { var e; if (d.createEvent) { e = d.createEvent("HTMLEvents"); e.initEvent(eventName, true, true); e.eventName = eventName; element.dispatchEvent(e); } else { e = d.createEventObject(); element.fireEvent("on" + eventName, e); } return this; }, deepExtend: function (destination, source) { for (var property in source) { if ( source[property] && source[property].constructor && source[property].constructor === Object ) { destination[property] = destination[property] || {}; this.deepExtend(destination[property], source[property]); } else { destination[property] = source[property]; } } return destination; }, /* * This is a Paste Hook. When the user pastes * content, this ultimately converts it into * plain text before inserting the data. */ pasteHook: function (fn) { var textarea = d.createElement('textarea'), el = this.element, existingValue, existingLength, overallLength, s = this.settings, medium = this.medium, html = this.html; textarea.className = s.cssClasses.pasteHook; el.parentNode.appendChild(textarea); textarea.focus(); if (!wild) { medium.makeUndoable(); } setTimeout(function () { el.focus(); if (s.maxLength > 0) { existingValue = html.text(el); existingLength = existingValue.length; overallLength = existingLength + textarea.value.length; if (overallLength > existingLength) { textarea.value = textarea.value.substring(0, s.maxLength - existingLength); } } fn(textarea.value); html.deleteNode(textarea); }, 2); }, setupContents: function () { var el = this.element, children = el.children, childNodes = el.childNodes, initialParagraph; if ( !this.settings.tags.paragraph || children.length > 0 || this.settings.mode === Medium.inlineMode ) { return; } //has content, but no children if (childNodes.length > 0) { initialParagraph = d.createElement(this.settings.tags.paragraph); if (el.innerHTML.match('^[&]nbsp[;]')) { el.innerHTML = el.innerHTML.substring(6, el.innerHTML.length - 1); } initialParagraph.innerHTML = el.innerHTML; el.innerHTML = ''; el.appendChild(initialParagraph); this.cursor.set(initialParagraph.innerHTML.length, initialParagraph); } else { initialParagraph = d.createElement(this.settings.tags.paragraph); initialParagraph.innerHTML = ' '; el.appendChild(initialParagraph); } }, traverseAll: function (element, options, depth) { var children = element.childNodes, length = children.length, i = 0, node, depth = depth || 1; options = options || {}; if (length > 0) { for (; i < length; i++) { node = children[i]; switch (node.nodeType) { case 1: this.traverseAll(node, options, depth + 1); if (options.element !== undefined) options.element(node, i, depth, element); break; case 3: if (options.fragment !== undefined) options.fragment(node, i, depth, element); } //length may change length = children.length; //if length did change, and we are at the last item, this causes infinite recursion, so if we are at the last item, then stop to prevent this if (node === element.lastChild) { i = length; } } } } }; /* * Handle Selection Logic */ Medium.Selection = function () { }; Medium.Selection.prototype = { setBridge: function (bridge) { for (var i in bridge) { this[i] = bridge[i]; } }, saveSelection: function () { if (w.getSelection) { var sel = w.getSelection(); if (sel.rangeCount > 0) { return sel.getRangeAt(0); } } else if (d.selection && d.selection.createRange) { // IE return d.selection.createRange(); } return null; }, restoreSelection: function (range) { if (range) { if (w.getSelection) { var sel = w.getSelection(); sel.removeAllRanges(); sel.addRange(range); } else if (d.selection && range.select) { // IE range.select(); } } } }; /* * Handle Cursor Logic */ Medium.Cursor = function () { }; Medium.Cursor.prototype = { setBridge: function (bridge) { for (var i in bridge) { this[i] = bridge[i]; } }, set: function (pos, el) { var range, html = this.html; if (d.createRange) { var selection = w.getSelection(), lastChild = html.lastChild(), length = html.text(lastChild).length - 1, toModify = el ? el : lastChild, theLength = ((typeof pos !== 'undefined') && (pos !== null) ? pos : length); range = d.createRange(); try{ range.setStart(toModify, theLength); } catch(e){}; range.collapse(true); selection.removeAllRanges(); selection.addRange(range); } else { range = d.body.createTextRange(); range.moveToElementText(el); range.collapse(false); range.select(); } }, parent: function () { var target = null, range; if (w.getSelection) { range = w.getSelection().getRangeAt(0); target = range.commonAncestorContainer; target = (target.nodeType === 1 ? target : target.parentNode ); } else if (d.selection) { target = d.selection.createRange().parentElement(); } if (target.tagName == 'SPAN') { target = target.parentNode; } return target; }, caretToBeginning: function (el) { this.set(0, el); }, caretToEnd: function (el) { this.set(this.html.text(el).length, el); } }; /* * HTML Abstractions */ Medium.HtmlAssistant = function () { }; Medium.HtmlAssistant.prototype = { setBridge: function (bridge) { for (var i in bridge) { this[i] = bridge[i]; } }, encodeHtml: function (html) { return d.createElement('a').appendChild( d.createTextNode(html)).parentNode.innerHTML; }, text: function (node, val) { node = node || this.settings.element; if (val) { if ((node.textContent) && (typeof (node.textContent) != "undefined")) { node.textContent = val; } else { node.innerText = val; } } else if (node.innerText) { return trim(node.innerText); } else if (node.textContent) { return trim(node.textContent); } //document fragment else if (node.data) { return trim(node.data); } //for good measure return ''; }, changeTag: function (oldNode, newTag) { var newNode = d.createElement(newTag), node, nextNode; node = oldNode.firstChild; while (node) { nextNode = node.nextSibling; newNode.appendChild(node); node = nextNode; } oldNode.parentNode.insertBefore(newNode, oldNode); oldNode.parentNode.removeChild(oldNode); return newNode; }, deleteNode: function (el) { el.parentNode.removeChild(el); }, placeholders: function () { //in IE8, just gracefully degrade to no placeholders if (!w.getComputedStyle) return; var that = this, s = this.settings, placeholder = this.medium.placeholder || (this.medium.placeholder = d.createElement('div')), el = s.element, style = placeholder.style, elStyle = w.getComputedStyle(el, null), qStyle = function (prop) { return elStyle.getPropertyValue(prop) }, utils = this.utils, text = utils.html.text(el), cursor = this.cursor, childCount = el.children.length; el.placeholder = placeholder; // Empty Editor if (text.length < 1 && childCount < 2) { if (el.placeHolderActive) return; if (!el.innerHTML.match('<' + s.tags.paragraph)) { el.innerHTML = ''; } // We need to add placeholders if (s.placeholder.length > 0) { if (!placeholder.setup) { placeholder.setup = true; //background & background color style.background = qStyle('background'); style.backgroundColor = qStyle('background-color'); //text size & text color style.fontSize = qStyle('font-size'); style.color = elStyle.color; //begin box-model //margin style.marginTop = qStyle('margin-top'); style.marginBottom = qStyle('margin-bottom'); style.marginLeft = qStyle('margin-left'); style.marginRight = qStyle('margin-right'); //padding style.paddingTop = qStyle('padding-top'); style.paddingBottom = qStyle('padding-bottom'); style.paddingLeft = qStyle('padding-left'); style.paddingRight = qStyle('padding-right'); //border style.borderTopWidth = qStyle('border-top-width'); style.borderTopColor = qStyle('border-top-color'); style.borderTopStyle = qStyle('border-top-style'); style.borderBottomWidth = qStyle('border-bottom-width'); style.borderBottomColor = qStyle('border-bottom-color'); style.borderBottomStyle = qStyle('border-bottom-style'); style.borderLeftWidth = qStyle('border-left-width'); style.borderLeftColor = qStyle('border-left-color'); style.borderLeftStyle = qStyle('border-left-style'); style.borderRightWidth = qStyle('border-right-width'); style.borderRightColor = qStyle('border-right-color'); style.borderRightStyle = qStyle('border-right-style'); //end box model //element setup placeholder.className = s.cssClasses.placeholder + ' ' + s.cssClasses.placeholder + '-' + s.mode; placeholder.innerHTML = '
' + s.placeholder + '
'; el.parentNode.insertBefore(placeholder, el); } el.className += ' ' + s.cssClasses.clear; style.display = ''; // Add base P tag and do auto focus, give it a min height if el has one style.minHeight = el.clientHeight + 'px'; style.minWidth = el.clientWidth + 'px'; if (s.mode !== Medium.inlineMode) { utils.setupContents(); if (childCount === 0 && el.firstChild) { cursor.set(0, el.firstChild); } } } el.placeHolderActive = true; } else if (el.placeHolderActive) { el.placeHolderActive = false; style.display = 'none'; el.className = trim(el.className.replace(s.cssClasses.clear, '')); utils.setupContents(); } }, /** * Cleans element * @param {HtmlElement} [el] default is settings.element */ clean: function (el) { /* * Deletes invalid nodes * Removes Attributes */ var s = this.settings, placeholderClass = s.cssClasses.placeholder, attributesToRemove = (s.attributes || {}).remove || [], tags = s.tags || {}, onlyOuter = tags.outerLevel || null, onlyInner = tags.innerLevel || null, outerSwitch = {}, innerSwitch = {}, paragraphTag = (tags.paragraph || '').toUpperCase(), html = this.html, attr, text, j; el = el || s.element; if (onlyOuter !== null) { for (j = 0; j < onlyOuter.length; j++) { outerSwitch[onlyOuter[j].toUpperCase()] = true; } } if (onlyInner !== null) { for (j = 0; j < onlyInner.length; j++) { innerSwitch[onlyInner[j].toUpperCase()] = true; } } this.utils.traverseAll(el, { element: function (child, i, depth, parent) { var nodeName = child.nodeName, shouldDelete = true; // Remove attributes for (j = 0; j < attributesToRemove.length; j++) { attr = attributesToRemove[j]; if (child.hasAttribute(attr)) { if (child.getAttribute(attr) !== placeholderClass) { child.removeAttribute(attr); } } } if (onlyOuter === null && onlyInner === null) { return; } if (depth === 1 && outerSwitch[nodeName] !== undefined) { shouldDelete = false; } else if (depth > 1 && innerSwitch[nodeName] !== undefined) { shouldDelete = false; } // Convert tags or delete if (shouldDelete) { if (w.getComputedStyle(child, null).getPropertyValue('display') === 'block') { if (paragraphTag.length > 0 && paragraphTag !== nodeName) { html.changeTag(child, paragraphTag); } if (depth > 1) { while (parent.childNodes.length > i) { parent.parentNode.insertBefore(parent.lastChild, parent.nextSibling); } } } else { switch (nodeName) { case 'BR': if (child === child.parentNode.lastChild) { if (child === child.parentNode.firstChild) { break; } text = document.createTextNode(""); text.innerHTML = ' '; parent.insertBefore(text, child); break; } default: while (child.firstChild !== null) { parent.insertBefore(child.firstChild, child); } html.deleteNode(child); break; } } } } }); }, lastChild: function () { return this.element.lastChild; }, addTag: function (tag, shouldFocus, isEditable, afterElement) { if (!this.settings.beforeAddTag(tag, shouldFocus, isEditable, afterElement)) { var newEl = d.createElement(tag), toFocus; if (typeof isEditable !== "undefined" && isEditable === false) { newEl.contentEditable = false; } if (newEl.innerHTML.length == 0) { newEl.innerHTML = ' '; } if (afterElement && afterElement.nextSibling) { afterElement.parentNode.insertBefore(newEl, afterElement.nextSibling); toFocus = afterElement.nextSibling; } else { this.settings.element.appendChild(newEl); toFocus = this.html.lastChild(); } if (shouldFocus) { this.cache.focusedElement = toFocus; this.cursor.set(0, toFocus); } return newEl; } return null; }, baseAtCaret: function () { if (!this.medium.isActive()) return null; var sel = w.getSelection ? w.getSelection() : document.selection; if (sel.rangeCount) { var selRange = sel.getRangeAt(0), container = selRange.endContainer; switch (container.nodeType) { case 3: if (container.data && container.data.length != selRange.endOffset) return false; break; } return container; } return null; }, atCaret: function () { var container = this.baseAtCaret() || {}, el = this.element; if (container === false) return null; while (container && container.parentNode !== el) { container = container.parentNode; } if (container && container.nodeType == 1) { return container; } return null; } }; Medium.Action = function () { }; Medium.Action.prototype = { setBridge: function (bridge) { for (var i in bridge) { this[i] = bridge[i]; } }, listen: function () { var el = this.element, intercept = this.intercept; this.utils .addEvent(el, 'keyup', intercept.up) .addEvent(el, 'keydown', intercept.down) .addEvent(el, 'focus', intercept.focus) .addEvent(el, 'blur', intercept.blur) .addEvent(el, 'paste', this.settings.pasteEventHandler); }, preserveElementFocus: function () { // Fetch node that has focus var anchorNode = w.getSelection ? w.getSelection().anchorNode : d.activeElement; if (anchorNode) { var cache = this.medium.cache, s = this.settings, cur = anchorNode.parentNode, children = s.element.children, diff = cur !== cache.focusedElement, elementIndex = 0, i; // anchorNode is our target if element is empty if (cur === s.element) { cur = anchorNode; } // Find our child index for (i = 0; i < children.length; i++) { if (cur === children[i]) { elementIndex = i; break; } } // Focused element is different if (diff) { cache.focusedElement = cur; cache.focusedElementIndex = elementIndex; } } } }; Medium.Cache = function () { this.initialized = false; this.cmd = false; this.focusedElement = null }; Medium.Cache.prototype = { setBridge: function (bridge) { for (var i in bridge) { this[i] = bridge[i]; } } }; //Modes; Medium.inlineMode = 'inline'; Medium.partialMode = 'partial'; Medium.richMode = 'rich'; Medium.Messages = { pastHere: 'Paste Here' }; return Medium; }()); if (typeof define === 'function' && define['amd']) { define(function () { return Medium; }); } else if (typeof module !== 'undefined' && module.exports) { module.exports = Medium; } else if (typeof this !== 'undefined') { this.Medium = Medium; } }).call(this, window, document);