diff --git a/app/assets/javascripts/app/controllers/layout_ref.js.coffee b/app/assets/javascripts/app/controllers/layout_ref.js.coffee index 6d9801083..acbd2d8f2 100644 --- a/app/assets/javascripts/app/controllers/layout_ref.js.coffee +++ b/app/assets/javascripts/app/controllers/layout_ref.js.coffee @@ -24,8 +24,8 @@ class Content extends App.ControllerContent for avatar in @$('.user.avatar') avatar = $(avatar) - size = switch - when avatar.hasClass('size-80') then 80 + size = switch + when avatar.hasClass('size-80') then 80 when avatar.hasClass('size-50') then 50 else 40 @createUniqueAvatar avatar, size, avatar.data('firstname'), avatar.data('lastname'), avatar.data('userid') @@ -641,8 +641,76 @@ class ReferenceSetupWizard extends App.ControllerWizard @agentEmail.add(@agentFirstName).add(@agentLastName).val('') @agentFirstName.focus() +App.Config.set( 'layout_ref/richtext', ReferenceSetupWizard, 'Routes' ) +class RichText extends App.ControllerContent + constructor: -> + super + @render() -App.Config.set( 'layout_ref/setup', ReferenceSetupWizard, 'Routes' ) + @$('.js-text-oneline').ce({ + mode: 'textonly' + multiline: false + maxlength: 250 + }) -App.Config.set( 'LayoutRef', { prio: 1700, parent: '#current_user', name: 'Layout Reference', target: '#layout_ref', role: [ 'Admin' ] }, 'NavBarRight' ) + @$('.js-text-multiline').ce({ + mode: 'textonly' + multiline: true + maxlength: 250 + }) + + @$('.js-text-richtext').ce({ + mode: 'richtext' + multiline: true + maxlength: 250 + }) + return + + @$('.js-textarea').on('keyup', (e) => + console.log('KU') + textarea = @$('.js-textarea') + App.Utils.htmlClanup(textarea) + ) + + @$('.js-textarea').on('paste', (e) => + console.log('paste') + #console.log('PPP', e, e.originalEvent.clipboardData) + + execute = => + + # add marker for cursor + getFirstRange = -> + sel = rangy.getSelection(); + if sel.rangeCount + sel.getRangeAt(0) + else + null + range = getFirstRange() + if range + el = document.createElement('span') + $(el).attr('data-cursor', 1) + range.insertNode(el) + rangy.getSelection().setSingleRange(range) + + # cleanup + textarea = @$('.js-textarea') + App.Utils.htmlClanup(textarea) + + # remove marker for cursor + textarea.find('[data-cursor=1]').focus() + textarea.find('[data-cursor=1]').remove() + @delay( execute, 1) + + return + ) + #editable.style.borderColor = '#54c8eb'; + #aloha(editable); + return + + render: -> + @html App.view('layout_ref/richtext')() + +App.Config.set( 'layout_ref/richtext', RichText, 'Routes' ) + +App.Config.set( 'LayoutRef', { prio: 1700, parent: '#current_user', name: 'Layout Reference', target: '#layout_ref', role: [ 'Admin' ] }, 'NavBarRight' ) \ No newline at end of file diff --git a/app/assets/javascripts/app/controllers/ticket_zoom.js.coffee b/app/assets/javascripts/app/controllers/ticket_zoom.js.coffee index dbecd4bd6..a477b936b 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom.js.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom.js.coffee @@ -1488,16 +1488,17 @@ class ArticleView extends App.Controller # add quoted text if needed selectedText = App.ClipBoard.getSelected() - if selectedText body = @ui.el.find('[data-name="body"]').html() || '' # quote text selectedText = App.Utils.textCleanup( selectedText ) - selectedText = App.Utils.quote( selectedText ) + #selectedText = App.Utils.quote( selectedText ) # convert to html selectedText = App.Utils.text2html( selectedText ) + if selectedText + selectedText = "


#{selectedText}

" articleNew.body = selectedText + body diff --git a/app/assets/javascripts/app/lib/app_post/utils.js.coffee b/app/assets/javascripts/app/lib/app_post/utils.js.coffee index 3cfc8318d..be002fe44 100644 --- a/app/assets/javascripts/app/lib/app_post/utils.js.coffee +++ b/app/assets/javascripts/app/lib/app_post/utils.js.coffee @@ -77,3 +77,52 @@ class App.Utils '> ' + match else '>' + + @htmlRemoveTags: (textarea) -> + # remove tags, keep content + textarea.find('a, div, span, li, ul, ol, a, hr, blockquote, br').replaceWith( -> + $(@).contents() + ) + + @htmlRemoveRichtext: (textarea) -> + + # remove style and class + textarea.find('div, span, li, ul, ol, a').removeAttr( 'style' ).removeAttr( 'class' ).removeAttr( 'title' ) + + # remove tags, keep content + textarea.find('a, li, ul, ol, a, hr').replaceWith( -> + $(@).contents() + ) + + @htmlClanup: (textarea) -> + + # remove style and class + textarea.find('div, span, li, ul, ol, a').removeAttr( 'style' ).removeAttr( 'class' ).removeAttr( 'title' ) + + # remove tags & content + textarea.find('hr').remove() + + # remove tags, keep content + textarea.find('a').replaceWith( -> + $(@).contents() + ) + + # replace tags with generic div + # New type of the tag + replacementTag = 'div'; + + # Replace all a tags with the type of replacementTag + textarea.find('h1, h2, h3, h4, h5, h6').each( -> + outer = this.outerHTML; + + # Replace opening tag + regex = new RegExp('<' + this.tagName, 'i'); + newTag = outer.replace(regex, '<' + replacementTag); + + # Replace closing tag + regex = new RegExp(' 0 ) { + text = text.substr( 0, pasteLength - overSize ) + overlimit = true + } + } + + // insert new text + if (document.selection) { // IE + var range = document.selection.createRange() + range.pasteHTML(text) + } + else { + document.execCommand('inserttext', false, text) + } + _this.maxLengthOk( overlimit ) + } + } + }) + + // disable rich text b/u/i + if ( this.options.mode === 'textonly' ) { + this.$element.on('keydown', function (e) { + if ( _this.richTextKey(e) ) { + e.preventDefault() + } + }) } - // link textarea - else if ( this.options.multiline && this.options.mode != 'richtext' ) { - editorMode = Medium.partialMode - } - // rich text - else { - editorMode = Medium.richMode - } + } - // max length validation - var validation = function(element) { + // check if rich text key is pressed + Plugin.prototype.richTextKey = function(e) { + // e.altKey + // e.ctrlKey + // e.metaKey + // on mac block e.metaKey + i/b/u + if ( !e.altKey && e.metaKey && this.options.richTextFormatKey[ e.keyCode ] ) { + return true + } + // on win block e.ctrlKey + i/b/u + if ( !e.altKey && e.ctrlKey && this.options.richTextFormatKey[ e.keyCode ] ) { + return true + } + return false + } + + // max length check + Plugin.prototype.maxLengthOk = function(typeAhead) { + var length = this.$element.text().length + if (typeAhead) { + length = length + 1 + } + if ( length > this.options.maxlength ) { // try to set error on framework form - var parent = $(element).parent().parent() + var parent = this.$element.parent().parent() if ( parent.hasClass('controls') ) { parent.addClass('has-error') setTimeout($.proxy(function(){ @@ -59,31 +216,15 @@ // set validation on element else { - $(element).addClass('invalid') + this.$element.addClass('invalid') setTimeout($.proxy(function(){ - $(element).removeClass('invalid') + this.$element.removeClass('invalid') }, this), 1000) return false } } - new Medium({ - element: element, - modifier: 'auto', - placeholder: this.options.placeholder || '', - autofocus: false, - autoHR: false, - mode: editorMode, - maxLength: this.options.maxlength || -1, - maxLengthReached: validation, - tags: { - 'break': 'br', - 'horizontalRule': 'hr', - 'paragraph': 'div', - 'outerLevel': ['pre', 'blockquote', 'figure'], - 'innerLevel': ['a', 'b', 'u', 'i', 'img', 'strong'] - }, - }); + return true } // get value diff --git a/app/assets/javascripts/app/lib/base/medium.js b/app/assets/javascripts/app/lib/base/medium.js deleted file mode 100644 index d94670eeb..000000000 --- a/app/assets/javascripts/app/lib/base/medium.js +++ /dev/null @@ -1,1952 +0,0 @@ -/* - * Medium.js - * - * Copyright 2013-2014, Jacob Kelley - http://jakiestfu.com/ - * Released under the MIT Licence - * http://opensource.org/licenses/MIT - * - * Github: http://github.com/jakiestfu/Medium.js/ - * Version: master - */ - -(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; - html.placeholders(); - }, - 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; - var 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) { - settings.maxLengthReached(settings.element) - 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); - (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 || settings.mode === Medium.inlineRichMode ){ - return utils.preventDefaultEvent(e); - } - - if (cache.shift) { - if (settings.tags['break']) { - utils.preventDefaultEvent(e); - html.addTag(settings.tags['break'], true); - return false; - } - - } else { - - 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 (settings.onBackspaceOrDelete !== undefined) { - var result = settings.onBackspaceOrDelete.call(medium, e, el); - - if (result) { - return; - } - } - - 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 - }, - maxLengthReached: function (element) { - //element - }, - 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] - * @param {Boolean} [skipChangeEvent] - * @returns {Medium} - */ - insertHtml: function (html, callback, skipChangeEvent) { - var result = (new Medium.Html(this, html)) - .insert(this.settings.beforeInsertHtml); - - if (skipChangeEvent === true) { - this.utils.triggerEvent(this.element, "change"); - } - - if (callback) { - callback.apply(result); - } - - return this; - }, - - /** - * - * @param {String} tagName - * @param {Object} [attributes] - * @param {Boolean} [skipChangeEvent] - * @returns {Medium} - */ - invokeElement: function (tagName, attributes, skipChangeEvent) { - 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); - - if (skipChangeEvent === true) { - this.utils.triggerEvent(this.element, "change"); - } - - return this; - }, - - /** - * @returns {string} - */ - behavior: function () { - return (wild ? Medium.wildBehavior : Medium.domesticatedBehavior); - }, - - /** - * - * @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(); - }, - - /** - * Splits content in medium element at cursor - * @returns {DocumentFragment|null} - */ - splitAtCaret: function() { - if (!this.isActive()) return null; - - var selector = (w.getSelection || d.selection), - sel = selector(), - offset = sel.focusOffset, - node = sel.focusNode, - el = this.element, - range = d.createRange(), - endRange = d.createRange(), - contents; - - range.setStart(node, offset); - endRange.selectNodeContents(el); - range.setEnd(endRange.endContainer, endRange.endOffset); - - contents = range.extractContents(); - - return contents; - }, - - /** - * Deletes selection - */ - deleteSelection: function() { - if (!this.isActive()) return; - - var sel = rangy.getSelection(), - range; - - if (sel.rangeCount > 0) { - range = sel.getRangeAt(0); - range.deleteContents(); - } - } - }; - - /** - * @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(); - } - }, - isEventSupported: function (eventName) { - eventName = 'on' + eventName; - var el = d.createElement(this.element.tagName), - isSupported = (eventName in el); - - if (!isSupported) { - el.setAttribute(eventName, 'return;'); - isSupported = typeof el[eventName] == 'function'; - } - el = null; - return isSupported; - }, - 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 - || this.settings.mode === Medium.inlineRichMode - ) { - 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(); - range.setStart(toModify, theLength); - 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, - hasFocus = Medium.activeElement === el; - - el.placeholder = placeholder; - - // Empty Editor - if ( - !hasFocus - && 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 && s.mode !== Medium.inlineRichMode ) { - utils.setupContents(); - - // set auto focus - if (this.settings.autofocus && 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 (s.mode === Medium.inlineRichMode) { - onlyOuter = s.tags.innerLevel; - } - - 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 = ' '; - child.parentNode.insertBefore(text, child); - break; - } - default: - while (child.firstChild !== null) { - child.parentNode.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.inlineRichMode = 'inlineRich'; - Medium.Messages = { - pastHere: 'Paste Here' - }; - - //Behaviours - Medium.domesticatedBehavior = 'domesticated'; - Medium.wildBehavior = 'wild'; - - 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); diff --git a/app/assets/javascripts/app/lib/base/rangy-core.js b/app/assets/javascripts/app/lib/base/rangy-core.js new file mode 100755 index 000000000..4bd65a460 --- /dev/null +++ b/app/assets/javascripts/app/lib/base/rangy-core.js @@ -0,0 +1,3755 @@ +/** + * Rangy, a cross-browser JavaScript range and selection library + * https://github.com/timdown/rangy + * + * Copyright 2014, Tim Down + * Licensed under the MIT license. + * Version: 1.3.0-alpha.20140921 + * Build date: 21 September 2014 + */ + +(function(factory, root) { + if (typeof define == "function" && define.amd) { + // AMD. Register as an anonymous module. + define(factory); + } else if (typeof module != "undefined" && typeof exports == "object") { + // Node/CommonJS style + module.exports = factory(); + } else { + // No AMD or CommonJS support so we place Rangy in (probably) the global variable + root.rangy = factory(); + } +})(function() { + + var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined"; + + // Minimal set of properties required for DOM Level 2 Range compliance. Comparison constants such as START_TO_START + // are omitted because ranges in KHTML do not have them but otherwise work perfectly well. See issue 113. + var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed", + "commonAncestorContainer"]; + + // Minimal set of methods required for DOM Level 2 Range compliance + var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore", + "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents", + "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"]; + + var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"]; + + // Subset of TextRange's full set of methods that we're interested in + var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "moveToElementText", "parentElement", "select", + "setEndPoint", "getBoundingClientRect"]; + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Trio of functions taken from Peter Michaux's article: + // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting + function isHostMethod(o, p) { + var t = typeof o[p]; + return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown"; + } + + function isHostObject(o, p) { + return !!(typeof o[p] == OBJECT && o[p]); + } + + function isHostProperty(o, p) { + return typeof o[p] != UNDEFINED; + } + + // Creates a convenience function to save verbose repeated calls to tests functions + function createMultiplePropertyTest(testFunc) { + return function(o, props) { + var i = props.length; + while (i--) { + if (!testFunc(o, props[i])) { + return false; + } + } + return true; + }; + } + + // Next trio of functions are a convenience to save verbose repeated calls to previous two functions + var areHostMethods = createMultiplePropertyTest(isHostMethod); + var areHostObjects = createMultiplePropertyTest(isHostObject); + var areHostProperties = createMultiplePropertyTest(isHostProperty); + + function isTextRange(range) { + return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties); + } + + function getBody(doc) { + return isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0]; + } + + var modules = {}; + + var isBrowser = (typeof window != UNDEFINED && typeof document != UNDEFINED); + + var util = { + isHostMethod: isHostMethod, + isHostObject: isHostObject, + isHostProperty: isHostProperty, + areHostMethods: areHostMethods, + areHostObjects: areHostObjects, + areHostProperties: areHostProperties, + isTextRange: isTextRange, + getBody: getBody + }; + + var api = { + version: "1.3.0-alpha.20140921", + initialized: false, + isBrowser: isBrowser, + supported: true, + util: util, + features: {}, + modules: modules, + config: { + alertOnFail: true, + alertOnWarn: false, + preferTextRange: false, + autoInitialize: (typeof rangyAutoInitialize == UNDEFINED) ? true : rangyAutoInitialize + } + }; + + function consoleLog(msg) { + if (typeof console != UNDEFINED && isHostMethod(console, "log")) { + console.log(msg); + } + } + + function alertOrLog(msg, shouldAlert) { + if (isBrowser && shouldAlert) { + alert(msg); + } else { + consoleLog(msg); + } + } + + function fail(reason) { + api.initialized = true; + api.supported = false; + alertOrLog("Rangy is not supported in this environment. Reason: " + reason, api.config.alertOnFail); + } + + api.fail = fail; + + function warn(msg) { + alertOrLog("Rangy warning: " + msg, api.config.alertOnWarn); + } + + api.warn = warn; + + // Add utility extend() method + var extend; + if ({}.hasOwnProperty) { + util.extend = extend = function(obj, props, deep) { + var o, p; + for (var i in props) { + if (props.hasOwnProperty(i)) { + o = obj[i]; + p = props[i]; + if (deep && o !== null && typeof o == "object" && p !== null && typeof p == "object") { + extend(o, p, true); + } + obj[i] = p; + } + } + // Special case for toString, which does not show up in for...in loops in IE <= 8 + if (props.hasOwnProperty("toString")) { + obj.toString = props.toString; + } + return obj; + }; + + util.createOptions = function(optionsParam, defaults) { + var options = {}; + extend(options, defaults); + if (optionsParam) { + extend(options, optionsParam); + } + return options; + }; + } else { + fail("hasOwnProperty not supported"); + } + + // Test whether we're in a browser and bail out if not + if (!isBrowser) { + fail("Rangy can only run in a browser"); + } + + // Test whether Array.prototype.slice can be relied on for NodeLists and use an alternative toArray() if not + (function() { + var toArray; + + if (isBrowser) { + var el = document.createElement("div"); + el.appendChild(document.createElement("span")); + var slice = [].slice; + try { + if (slice.call(el.childNodes, 0)[0].nodeType == 1) { + toArray = function(arrayLike) { + return slice.call(arrayLike, 0); + }; + } + } catch (e) {} + } + + if (!toArray) { + toArray = function(arrayLike) { + var arr = []; + for (var i = 0, len = arrayLike.length; i < len; ++i) { + arr[i] = arrayLike[i]; + } + return arr; + }; + } + + util.toArray = toArray; + })(); + + // Very simple event handler wrapper function that doesn't attempt to solve issues such as "this" handling or + // normalization of event properties + var addListener; + if (isBrowser) { + if (isHostMethod(document, "addEventListener")) { + addListener = function(obj, eventType, listener) { + obj.addEventListener(eventType, listener, false); + }; + } else if (isHostMethod(document, "attachEvent")) { + addListener = function(obj, eventType, listener) { + obj.attachEvent("on" + eventType, listener); + }; + } else { + fail("Document does not have required addEventListener or attachEvent method"); + } + + util.addListener = addListener; + } + + var initListeners = []; + + function getErrorDesc(ex) { + return ex.message || ex.description || String(ex); + } + + // Initialization + function init() { + if (!isBrowser || api.initialized) { + return; + } + var testRange; + var implementsDomRange = false, implementsTextRange = false; + + // First, perform basic feature tests + + if (isHostMethod(document, "createRange")) { + testRange = document.createRange(); + if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) { + implementsDomRange = true; + } + } + + var body = getBody(document); + if (!body || body.nodeName.toLowerCase() != "body") { + fail("No body element found"); + return; + } + + if (body && isHostMethod(body, "createTextRange")) { + testRange = body.createTextRange(); + if (isTextRange(testRange)) { + implementsTextRange = true; + } + } + + if (!implementsDomRange && !implementsTextRange) { + fail("Neither Range nor TextRange are available"); + return; + } + + api.initialized = true; + api.features = { + implementsDomRange: implementsDomRange, + implementsTextRange: implementsTextRange + }; + + // Initialize modules + var module, errorMessage; + for (var moduleName in modules) { + if ( (module = modules[moduleName]) instanceof Module ) { + module.init(module, api); + } + } + + // Call init listeners + for (var i = 0, len = initListeners.length; i < len; ++i) { + try { + initListeners[i](api); + } catch (ex) { + errorMessage = "Rangy init listener threw an exception. Continuing. Detail: " + getErrorDesc(ex); + consoleLog(errorMessage); + } + } + } + + // Allow external scripts to initialize this library in case it's loaded after the document has loaded + api.init = init; + + // Execute listener immediately if already initialized + api.addInitListener = function(listener) { + if (api.initialized) { + listener(api); + } else { + initListeners.push(listener); + } + }; + + var shimListeners = []; + + api.addShimListener = function(listener) { + shimListeners.push(listener); + }; + + function shim(win) { + win = win || window; + init(); + + // Notify listeners + for (var i = 0, len = shimListeners.length; i < len; ++i) { + shimListeners[i](win); + } + } + + if (isBrowser) { + api.shim = api.createMissingNativeApi = shim; + } + + function Module(name, dependencies, initializer) { + this.name = name; + this.dependencies = dependencies; + this.initialized = false; + this.supported = false; + this.initializer = initializer; + } + + Module.prototype = { + init: function() { + var requiredModuleNames = this.dependencies || []; + for (var i = 0, len = requiredModuleNames.length, requiredModule, moduleName; i < len; ++i) { + moduleName = requiredModuleNames[i]; + + requiredModule = modules[moduleName]; + if (!requiredModule || !(requiredModule instanceof Module)) { + throw new Error("required module '" + moduleName + "' not found"); + } + + requiredModule.init(); + + if (!requiredModule.supported) { + throw new Error("required module '" + moduleName + "' not supported"); + } + } + + // Now run initializer + this.initializer(this); + }, + + fail: function(reason) { + this.initialized = true; + this.supported = false; + throw new Error("Module '" + this.name + "' failed to load: " + reason); + }, + + warn: function(msg) { + api.warn("Module " + this.name + ": " + msg); + }, + + deprecationNotice: function(deprecated, replacement) { + api.warn("DEPRECATED: " + deprecated + " in module " + this.name + "is deprecated. Please use " + + replacement + " instead"); + }, + + createError: function(msg) { + return new Error("Error in Rangy " + this.name + " module: " + msg); + } + }; + + function createModule(name, dependencies, initFunc) { + var newModule = new Module(name, dependencies, function(module) { + if (!module.initialized) { + module.initialized = true; + try { + initFunc(api, module); + module.supported = true; + } catch (ex) { + var errorMessage = "Module '" + name + "' failed to load: " + getErrorDesc(ex); + consoleLog(errorMessage); + if (ex.stack) { + consoleLog(ex.stack); + } + } + } + }); + modules[name] = newModule; + return newModule; + } + + api.createModule = function(name) { + // Allow 2 or 3 arguments (second argument is an optional array of dependencies) + var initFunc, dependencies; + if (arguments.length == 2) { + initFunc = arguments[1]; + dependencies = []; + } else { + initFunc = arguments[2]; + dependencies = arguments[1]; + } + + var module = createModule(name, dependencies, initFunc); + + // Initialize the module immediately if the core is already initialized + if (api.initialized && api.supported) { + module.init(); + } + }; + + api.createCoreModule = function(name, dependencies, initFunc) { + createModule(name, dependencies, initFunc); + }; + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Ensure rangy.rangePrototype and rangy.selectionPrototype are available immediately + + function RangePrototype() {} + api.RangePrototype = RangePrototype; + api.rangePrototype = new RangePrototype(); + + function SelectionPrototype() {} + api.selectionPrototype = new SelectionPrototype(); + + /*----------------------------------------------------------------------------------------------------------------*/ + + // DOM utility methods used by Rangy + api.createCoreModule("DomUtil", [], function(api, module) { + var UNDEF = "undefined"; + var util = api.util; + + // Perform feature tests + if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) { + module.fail("document missing a Node creation method"); + } + + if (!util.isHostMethod(document, "getElementsByTagName")) { + module.fail("document missing getElementsByTagName method"); + } + + var el = document.createElement("div"); + if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] || + !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) { + module.fail("Incomplete Element implementation"); + } + + // innerHTML is required for Range's createContextualFragment method + if (!util.isHostProperty(el, "innerHTML")) { + module.fail("Element is missing innerHTML property"); + } + + var textNode = document.createTextNode("test"); + if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] || + !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) || + !util.areHostProperties(textNode, ["data"]))) { + module.fail("Incomplete Text Node implementation"); + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been + // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that + // contains just the document as a single element and the value searched for is the document. + var arrayContains = /*Array.prototype.indexOf ? + function(arr, val) { + return arr.indexOf(val) > -1; + }:*/ + + function(arr, val) { + var i = arr.length; + while (i--) { + if (arr[i] === val) { + return true; + } + } + return false; + }; + + // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI + function isHtmlNamespace(node) { + var ns; + return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml"); + } + + function parentElement(node) { + var parent = node.parentNode; + return (parent.nodeType == 1) ? parent : null; + } + + function getNodeIndex(node) { + var i = 0; + while( (node = node.previousSibling) ) { + ++i; + } + return i; + } + + function getNodeLength(node) { + switch (node.nodeType) { + case 7: + case 10: + return 0; + case 3: + case 8: + return node.length; + default: + return node.childNodes.length; + } + } + + function getCommonAncestor(node1, node2) { + var ancestors = [], n; + for (n = node1; n; n = n.parentNode) { + ancestors.push(n); + } + + for (n = node2; n; n = n.parentNode) { + if (arrayContains(ancestors, n)) { + return n; + } + } + + return null; + } + + function isAncestorOf(ancestor, descendant, selfIsAncestor) { + var n = selfIsAncestor ? descendant : descendant.parentNode; + while (n) { + if (n === ancestor) { + return true; + } else { + n = n.parentNode; + } + } + return false; + } + + function isOrIsAncestorOf(ancestor, descendant) { + return isAncestorOf(ancestor, descendant, true); + } + + function getClosestAncestorIn(node, ancestor, selfIsAncestor) { + var p, n = selfIsAncestor ? node : node.parentNode; + while (n) { + p = n.parentNode; + if (p === ancestor) { + return n; + } + n = p; + } + return null; + } + + function isCharacterDataNode(node) { + var t = node.nodeType; + return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment + } + + function isTextOrCommentNode(node) { + if (!node) { + return false; + } + var t = node.nodeType; + return t == 3 || t == 8 ; // Text or Comment + } + + function insertAfter(node, precedingNode) { + var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode; + if (nextNode) { + parent.insertBefore(node, nextNode); + } else { + parent.appendChild(node); + } + return node; + } + + // Note that we cannot use splitText() because it is bugridden in IE 9. + function splitDataNode(node, index, positionsToPreserve) { + var newNode = node.cloneNode(false); + newNode.deleteData(0, index); + node.deleteData(index, node.length - index); + insertAfter(newNode, node); + + // Preserve positions + if (positionsToPreserve) { + for (var i = 0, position; position = positionsToPreserve[i++]; ) { + // Handle case where position was inside the portion of node after the split point + if (position.node == node && position.offset > index) { + position.node = newNode; + position.offset -= index; + } + // Handle the case where the position is a node offset within node's parent + else if (position.node == node.parentNode && position.offset > getNodeIndex(node)) { + ++position.offset; + } + } + } + return newNode; + } + + function getDocument(node) { + if (node.nodeType == 9) { + return node; + } else if (typeof node.ownerDocument != UNDEF) { + return node.ownerDocument; + } else if (typeof node.document != UNDEF) { + return node.document; + } else if (node.parentNode) { + return getDocument(node.parentNode); + } else { + throw module.createError("getDocument: no document found for node"); + } + } + + function getWindow(node) { + var doc = getDocument(node); + if (typeof doc.defaultView != UNDEF) { + return doc.defaultView; + } else if (typeof doc.parentWindow != UNDEF) { + return doc.parentWindow; + } else { + throw module.createError("Cannot get a window object for node"); + } + } + + function getIframeDocument(iframeEl) { + if (typeof iframeEl.contentDocument != UNDEF) { + return iframeEl.contentDocument; + } else if (typeof iframeEl.contentWindow != UNDEF) { + return iframeEl.contentWindow.document; + } else { + throw module.createError("getIframeDocument: No Document object found for iframe element"); + } + } + + function getIframeWindow(iframeEl) { + if (typeof iframeEl.contentWindow != UNDEF) { + return iframeEl.contentWindow; + } else if (typeof iframeEl.contentDocument != UNDEF) { + return iframeEl.contentDocument.defaultView; + } else { + throw module.createError("getIframeWindow: No Window object found for iframe element"); + } + } + + // This looks bad. Is it worth it? + function isWindow(obj) { + return obj && util.isHostMethod(obj, "setTimeout") && util.isHostObject(obj, "document"); + } + + function getContentDocument(obj, module, methodName) { + var doc; + + if (!obj) { + doc = document; + } + + // Test if a DOM node has been passed and obtain a document object for it if so + else if (util.isHostProperty(obj, "nodeType")) { + doc = (obj.nodeType == 1 && obj.tagName.toLowerCase() == "iframe") ? + getIframeDocument(obj) : getDocument(obj); + } + + // Test if the doc parameter appears to be a Window object + else if (isWindow(obj)) { + doc = obj.document; + } + + if (!doc) { + throw module.createError(methodName + "(): Parameter must be a Window object or DOM node"); + } + + return doc; + } + + function getRootContainer(node) { + var parent; + while ( (parent = node.parentNode) ) { + node = parent; + } + return node; + } + + function comparePoints(nodeA, offsetA, nodeB, offsetB) { + // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing + var nodeC, root, childA, childB, n; + if (nodeA == nodeB) { + // Case 1: nodes are the same + return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1; + } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) { + // Case 2: node C (container B or an ancestor) is a child node of A + return offsetA <= getNodeIndex(nodeC) ? -1 : 1; + } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) { + // Case 3: node C (container A or an ancestor) is a child node of B + return getNodeIndex(nodeC) < offsetB ? -1 : 1; + } else { + root = getCommonAncestor(nodeA, nodeB); + if (!root) { + throw new Error("comparePoints error: nodes have no common ancestor"); + } + + // Case 4: containers are siblings or descendants of siblings + childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true); + childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true); + + if (childA === childB) { + // This shouldn't be possible + throw module.createError("comparePoints got to case 4 and childA and childB are the same!"); + } else { + n = root.firstChild; + while (n) { + if (n === childA) { + return -1; + } else if (n === childB) { + return 1; + } + n = n.nextSibling; + } + } + } + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Test for IE's crash (IE 6/7) or exception (IE >= 8) when a reference to garbage-collected text node is queried + var crashyTextNodes = false; + + function isBrokenNode(node) { + var n; + try { + n = node.parentNode; + return false; + } catch (e) { + return true; + } + } + + (function() { + var el = document.createElement("b"); + el.innerHTML = "1"; + var textNode = el.firstChild; + el.innerHTML = "
"; + crashyTextNodes = isBrokenNode(textNode); + + api.features.crashyTextNodes = crashyTextNodes; + })(); + + /*----------------------------------------------------------------------------------------------------------------*/ + + function inspectNode(node) { + if (!node) { + return "[No node]"; + } + if (crashyTextNodes && isBrokenNode(node)) { + return "[Broken node]"; + } + if (isCharacterDataNode(node)) { + return '"' + node.data + '"'; + } + if (node.nodeType == 1) { + var idAttr = node.id ? ' id="' + node.id + '"' : ""; + return "<" + node.nodeName + idAttr + ">[index:" + getNodeIndex(node) + ",length:" + node.childNodes.length + "][" + (node.innerHTML || "[innerHTML not supported]").slice(0, 25) + "]"; + } + return node.nodeName; + } + + function fragmentFromNodeChildren(node) { + var fragment = getDocument(node).createDocumentFragment(), child; + while ( (child = node.firstChild) ) { + fragment.appendChild(child); + } + return fragment; + } + + var getComputedStyleProperty; + if (typeof window.getComputedStyle != UNDEF) { + getComputedStyleProperty = function(el, propName) { + return getWindow(el).getComputedStyle(el, null)[propName]; + }; + } else if (typeof document.documentElement.currentStyle != UNDEF) { + getComputedStyleProperty = function(el, propName) { + return el.currentStyle[propName]; + }; + } else { + module.fail("No means of obtaining computed style properties found"); + } + + function NodeIterator(root) { + this.root = root; + this._next = root; + } + + NodeIterator.prototype = { + _current: null, + + hasNext: function() { + return !!this._next; + }, + + next: function() { + var n = this._current = this._next; + var child, next; + if (this._current) { + child = n.firstChild; + if (child) { + this._next = child; + } else { + next = null; + while ((n !== this.root) && !(next = n.nextSibling)) { + n = n.parentNode; + } + this._next = next; + } + } + return this._current; + }, + + detach: function() { + this._current = this._next = this.root = null; + } + }; + + function createIterator(root) { + return new NodeIterator(root); + } + + function DomPosition(node, offset) { + this.node = node; + this.offset = offset; + } + + DomPosition.prototype = { + equals: function(pos) { + return !!pos && this.node === pos.node && this.offset == pos.offset; + }, + + inspect: function() { + return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]"; + }, + + toString: function() { + return this.inspect(); + } + }; + + function DOMException(codeName) { + this.code = this[codeName]; + this.codeName = codeName; + this.message = "DOMException: " + this.codeName; + } + + DOMException.prototype = { + INDEX_SIZE_ERR: 1, + HIERARCHY_REQUEST_ERR: 3, + WRONG_DOCUMENT_ERR: 4, + NO_MODIFICATION_ALLOWED_ERR: 7, + NOT_FOUND_ERR: 8, + NOT_SUPPORTED_ERR: 9, + INVALID_STATE_ERR: 11, + INVALID_NODE_TYPE_ERR: 24 + }; + + DOMException.prototype.toString = function() { + return this.message; + }; + + api.dom = { + arrayContains: arrayContains, + isHtmlNamespace: isHtmlNamespace, + parentElement: parentElement, + getNodeIndex: getNodeIndex, + getNodeLength: getNodeLength, + getCommonAncestor: getCommonAncestor, + isAncestorOf: isAncestorOf, + isOrIsAncestorOf: isOrIsAncestorOf, + getClosestAncestorIn: getClosestAncestorIn, + isCharacterDataNode: isCharacterDataNode, + isTextOrCommentNode: isTextOrCommentNode, + insertAfter: insertAfter, + splitDataNode: splitDataNode, + getDocument: getDocument, + getWindow: getWindow, + getIframeWindow: getIframeWindow, + getIframeDocument: getIframeDocument, + getBody: util.getBody, + isWindow: isWindow, + getContentDocument: getContentDocument, + getRootContainer: getRootContainer, + comparePoints: comparePoints, + isBrokenNode: isBrokenNode, + inspectNode: inspectNode, + getComputedStyleProperty: getComputedStyleProperty, + fragmentFromNodeChildren: fragmentFromNodeChildren, + createIterator: createIterator, + DomPosition: DomPosition + }; + + api.DOMException = DOMException; + }); + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Pure JavaScript implementation of DOM Range + api.createCoreModule("DomRange", ["DomUtil"], function(api, module) { + var dom = api.dom; + var util = api.util; + var DomPosition = dom.DomPosition; + var DOMException = api.DOMException; + + var isCharacterDataNode = dom.isCharacterDataNode; + var getNodeIndex = dom.getNodeIndex; + var isOrIsAncestorOf = dom.isOrIsAncestorOf; + var getDocument = dom.getDocument; + var comparePoints = dom.comparePoints; + var splitDataNode = dom.splitDataNode; + var getClosestAncestorIn = dom.getClosestAncestorIn; + var getNodeLength = dom.getNodeLength; + var arrayContains = dom.arrayContains; + var getRootContainer = dom.getRootContainer; + var crashyTextNodes = api.features.crashyTextNodes; + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Utility functions + + function isNonTextPartiallySelected(node, range) { + return (node.nodeType != 3) && + (isOrIsAncestorOf(node, range.startContainer) || isOrIsAncestorOf(node, range.endContainer)); + } + + function getRangeDocument(range) { + return range.document || getDocument(range.startContainer); + } + + function getBoundaryBeforeNode(node) { + return new DomPosition(node.parentNode, getNodeIndex(node)); + } + + function getBoundaryAfterNode(node) { + return new DomPosition(node.parentNode, getNodeIndex(node) + 1); + } + + function insertNodeAtPosition(node, n, o) { + var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node; + if (isCharacterDataNode(n)) { + if (o == n.length) { + dom.insertAfter(node, n); + } else { + n.parentNode.insertBefore(node, o == 0 ? n : splitDataNode(n, o)); + } + } else if (o >= n.childNodes.length) { + n.appendChild(node); + } else { + n.insertBefore(node, n.childNodes[o]); + } + return firstNodeInserted; + } + + function rangesIntersect(rangeA, rangeB, touchingIsIntersecting) { + assertRangeValid(rangeA); + assertRangeValid(rangeB); + + if (getRangeDocument(rangeB) != getRangeDocument(rangeA)) { + throw new DOMException("WRONG_DOCUMENT_ERR"); + } + + var startComparison = comparePoints(rangeA.startContainer, rangeA.startOffset, rangeB.endContainer, rangeB.endOffset), + endComparison = comparePoints(rangeA.endContainer, rangeA.endOffset, rangeB.startContainer, rangeB.startOffset); + + return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; + } + + function cloneSubtree(iterator) { + var partiallySelected; + for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) { + partiallySelected = iterator.isPartiallySelectedSubtree(); + node = node.cloneNode(!partiallySelected); + if (partiallySelected) { + subIterator = iterator.getSubtreeIterator(); + node.appendChild(cloneSubtree(subIterator)); + subIterator.detach(); + } + + if (node.nodeType == 10) { // DocumentType + throw new DOMException("HIERARCHY_REQUEST_ERR"); + } + frag.appendChild(node); + } + return frag; + } + + function iterateSubtree(rangeIterator, func, iteratorState) { + var it, n; + iteratorState = iteratorState || { stop: false }; + for (var node, subRangeIterator; node = rangeIterator.next(); ) { + if (rangeIterator.isPartiallySelectedSubtree()) { + if (func(node) === false) { + iteratorState.stop = true; + return; + } else { + // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of + // the node selected by the Range. + subRangeIterator = rangeIterator.getSubtreeIterator(); + iterateSubtree(subRangeIterator, func, iteratorState); + subRangeIterator.detach(); + if (iteratorState.stop) { + return; + } + } + } else { + // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its + // descendants + it = dom.createIterator(node); + while ( (n = it.next()) ) { + if (func(n) === false) { + iteratorState.stop = true; + return; + } + } + } + } + } + + function deleteSubtree(iterator) { + var subIterator; + while (iterator.next()) { + if (iterator.isPartiallySelectedSubtree()) { + subIterator = iterator.getSubtreeIterator(); + deleteSubtree(subIterator); + subIterator.detach(); + } else { + iterator.remove(); + } + } + } + + function extractSubtree(iterator) { + for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) { + + if (iterator.isPartiallySelectedSubtree()) { + node = node.cloneNode(false); + subIterator = iterator.getSubtreeIterator(); + node.appendChild(extractSubtree(subIterator)); + subIterator.detach(); + } else { + iterator.remove(); + } + if (node.nodeType == 10) { // DocumentType + throw new DOMException("HIERARCHY_REQUEST_ERR"); + } + frag.appendChild(node); + } + return frag; + } + + function getNodesInRange(range, nodeTypes, filter) { + var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex; + var filterExists = !!filter; + if (filterNodeTypes) { + regex = new RegExp("^(" + nodeTypes.join("|") + ")$"); + } + + var nodes = []; + iterateSubtree(new RangeIterator(range, false), function(node) { + if (filterNodeTypes && !regex.test(node.nodeType)) { + return; + } + if (filterExists && !filter(node)) { + return; + } + // Don't include a boundary container if it is a character data node and the range does not contain any + // of its character data. See issue 190. + var sc = range.startContainer; + if (node == sc && isCharacterDataNode(sc) && range.startOffset == sc.length) { + return; + } + + var ec = range.endContainer; + if (node == ec && isCharacterDataNode(ec) && range.endOffset == 0) { + return; + } + + nodes.push(node); + }); + return nodes; + } + + function inspect(range) { + var name = (typeof range.getName == "undefined") ? "Range" : range.getName(); + return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " + + dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]"; + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange) + + function RangeIterator(range, clonePartiallySelectedTextNodes) { + this.range = range; + this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes; + + + if (!range.collapsed) { + this.sc = range.startContainer; + this.so = range.startOffset; + this.ec = range.endContainer; + this.eo = range.endOffset; + var root = range.commonAncestorContainer; + + if (this.sc === this.ec && isCharacterDataNode(this.sc)) { + this.isSingleCharacterDataNode = true; + this._first = this._last = this._next = this.sc; + } else { + this._first = this._next = (this.sc === root && !isCharacterDataNode(this.sc)) ? + this.sc.childNodes[this.so] : getClosestAncestorIn(this.sc, root, true); + this._last = (this.ec === root && !isCharacterDataNode(this.ec)) ? + this.ec.childNodes[this.eo - 1] : getClosestAncestorIn(this.ec, root, true); + } + } + } + + RangeIterator.prototype = { + _current: null, + _next: null, + _first: null, + _last: null, + isSingleCharacterDataNode: false, + + reset: function() { + this._current = null; + this._next = this._first; + }, + + hasNext: function() { + return !!this._next; + }, + + next: function() { + // Move to next node + var current = this._current = this._next; + if (current) { + this._next = (current !== this._last) ? current.nextSibling : null; + + // Check for partially selected text nodes + if (isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) { + if (current === this.ec) { + (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo); + } + if (this._current === this.sc) { + (current = current.cloneNode(true)).deleteData(0, this.so); + } + } + } + + return current; + }, + + remove: function() { + var current = this._current, start, end; + + if (isCharacterDataNode(current) && (current === this.sc || current === this.ec)) { + start = (current === this.sc) ? this.so : 0; + end = (current === this.ec) ? this.eo : current.length; + if (start != end) { + current.deleteData(start, end - start); + } + } else { + if (current.parentNode) { + current.parentNode.removeChild(current); + } else { + } + } + }, + + // Checks if the current node is partially selected + isPartiallySelectedSubtree: function() { + var current = this._current; + return isNonTextPartiallySelected(current, this.range); + }, + + getSubtreeIterator: function() { + var subRange; + if (this.isSingleCharacterDataNode) { + subRange = this.range.cloneRange(); + subRange.collapse(false); + } else { + subRange = new Range(getRangeDocument(this.range)); + var current = this._current; + var startContainer = current, startOffset = 0, endContainer = current, endOffset = getNodeLength(current); + + if (isOrIsAncestorOf(current, this.sc)) { + startContainer = this.sc; + startOffset = this.so; + } + if (isOrIsAncestorOf(current, this.ec)) { + endContainer = this.ec; + endOffset = this.eo; + } + + updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset); + } + return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes); + }, + + detach: function() { + this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null; + } + }; + + /*----------------------------------------------------------------------------------------------------------------*/ + + var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10]; + var rootContainerNodeTypes = [2, 9, 11]; + var readonlyNodeTypes = [5, 6, 10, 12]; + var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11]; + var surroundNodeTypes = [1, 3, 4, 5, 7, 8]; + + function createAncestorFinder(nodeTypes) { + return function(node, selfIsAncestor) { + var t, n = selfIsAncestor ? node : node.parentNode; + while (n) { + t = n.nodeType; + if (arrayContains(nodeTypes, t)) { + return n; + } + n = n.parentNode; + } + return null; + }; + } + + var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] ); + var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes); + var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] ); + + function assertNoDocTypeNotationEntityAncestor(node, allowSelf) { + if (getDocTypeNotationEntityAncestor(node, allowSelf)) { + throw new DOMException("INVALID_NODE_TYPE_ERR"); + } + } + + function assertValidNodeType(node, invalidTypes) { + if (!arrayContains(invalidTypes, node.nodeType)) { + throw new DOMException("INVALID_NODE_TYPE_ERR"); + } + } + + function assertValidOffset(node, offset) { + if (offset < 0 || offset > (isCharacterDataNode(node) ? node.length : node.childNodes.length)) { + throw new DOMException("INDEX_SIZE_ERR"); + } + } + + function assertSameDocumentOrFragment(node1, node2) { + if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) { + throw new DOMException("WRONG_DOCUMENT_ERR"); + } + } + + function assertNodeNotReadOnly(node) { + if (getReadonlyAncestor(node, true)) { + throw new DOMException("NO_MODIFICATION_ALLOWED_ERR"); + } + } + + function assertNode(node, codeName) { + if (!node) { + throw new DOMException(codeName); + } + } + + function isOrphan(node) { + return (crashyTextNodes && dom.isBrokenNode(node)) || + !arrayContains(rootContainerNodeTypes, node.nodeType) && !getDocumentOrFragmentContainer(node, true); + } + + function isValidOffset(node, offset) { + return offset <= (isCharacterDataNode(node) ? node.length : node.childNodes.length); + } + + function isRangeValid(range) { + return (!!range.startContainer && !!range.endContainer && + !isOrphan(range.startContainer) && + !isOrphan(range.endContainer) && + isValidOffset(range.startContainer, range.startOffset) && + isValidOffset(range.endContainer, range.endOffset)); + } + + function assertRangeValid(range) { + if (!isRangeValid(range)) { + throw new Error("Range error: Range is no longer valid after DOM mutation (" + range.inspect() + ")"); + } + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Test the browser's innerHTML support to decide how to implement createContextualFragment + var styleEl = document.createElement("style"); + var htmlParsingConforms = false; + try { + styleEl.innerHTML = "x"; + htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node + } catch (e) { + // IE 6 and 7 throw + } + + api.features.htmlParsingConforms = htmlParsingConforms; + + var createContextualFragment = htmlParsingConforms ? + + // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See + // discussion and base code for this implementation at issue 67. + // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface + // Thanks to Aleks Williams. + function(fragmentStr) { + // "Let node the context object's start's node." + var node = this.startContainer; + var doc = getDocument(node); + + // "If the context object's start's node is null, raise an INVALID_STATE_ERR + // exception and abort these steps." + if (!node) { + throw new DOMException("INVALID_STATE_ERR"); + } + + // "Let element be as follows, depending on node's interface:" + // Document, Document Fragment: null + var el = null; + + // "Element: node" + if (node.nodeType == 1) { + el = node; + + // "Text, Comment: node's parentElement" + } else if (isCharacterDataNode(node)) { + el = dom.parentElement(node); + } + + // "If either element is null or element's ownerDocument is an HTML document + // and element's local name is "html" and element's namespace is the HTML + // namespace" + if (el === null || ( + el.nodeName == "HTML" && + dom.isHtmlNamespace(getDocument(el).documentElement) && + dom.isHtmlNamespace(el) + )) { + + // "let element be a new Element with "body" as its local name and the HTML + // namespace as its namespace."" + el = doc.createElement("body"); + } else { + el = el.cloneNode(false); + } + + // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm." + // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm." + // "In either case, the algorithm must be invoked with fragment as the input + // and element as the context element." + el.innerHTML = fragmentStr; + + // "If this raises an exception, then abort these steps. Otherwise, let new + // children be the nodes returned." + + // "Let fragment be a new DocumentFragment." + // "Append all new children to fragment." + // "Return fragment." + return dom.fragmentFromNodeChildren(el); + } : + + // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that + // previous versions of Rangy used (with the exception of using a body element rather than a div) + function(fragmentStr) { + var doc = getRangeDocument(this); + var el = doc.createElement("body"); + el.innerHTML = fragmentStr; + + return dom.fragmentFromNodeChildren(el); + }; + + function splitRangeBoundaries(range, positionsToPreserve) { + assertRangeValid(range); + + var sc = range.startContainer, so = range.startOffset, ec = range.endContainer, eo = range.endOffset; + var startEndSame = (sc === ec); + + if (isCharacterDataNode(ec) && eo > 0 && eo < ec.length) { + splitDataNode(ec, eo, positionsToPreserve); + } + + if (isCharacterDataNode(sc) && so > 0 && so < sc.length) { + sc = splitDataNode(sc, so, positionsToPreserve); + if (startEndSame) { + eo -= so; + ec = sc; + } else if (ec == sc.parentNode && eo >= getNodeIndex(sc)) { + eo++; + } + so = 0; + } + range.setStartAndEnd(sc, so, ec, eo); + } + + function rangeToHtml(range) { + assertRangeValid(range); + var container = range.commonAncestorContainer.parentNode.cloneNode(false); + container.appendChild( range.cloneContents() ); + return container.innerHTML; + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed", + "commonAncestorContainer"]; + + var s2s = 0, s2e = 1, e2e = 2, e2s = 3; + var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3; + + util.extend(api.rangePrototype, { + compareBoundaryPoints: function(how, range) { + assertRangeValid(this); + assertSameDocumentOrFragment(this.startContainer, range.startContainer); + + var nodeA, offsetA, nodeB, offsetB; + var prefixA = (how == e2s || how == s2s) ? "start" : "end"; + var prefixB = (how == s2e || how == s2s) ? "start" : "end"; + nodeA = this[prefixA + "Container"]; + offsetA = this[prefixA + "Offset"]; + nodeB = range[prefixB + "Container"]; + offsetB = range[prefixB + "Offset"]; + return comparePoints(nodeA, offsetA, nodeB, offsetB); + }, + + insertNode: function(node) { + assertRangeValid(this); + assertValidNodeType(node, insertableNodeTypes); + assertNodeNotReadOnly(this.startContainer); + + if (isOrIsAncestorOf(node, this.startContainer)) { + throw new DOMException("HIERARCHY_REQUEST_ERR"); + } + + // No check for whether the container of the start of the Range is of a type that does not allow + // children of the type of node: the browser's DOM implementation should do this for us when we attempt + // to add the node + + var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset); + this.setStartBefore(firstNodeInserted); + }, + + cloneContents: function() { + assertRangeValid(this); + + var clone, frag; + if (this.collapsed) { + return getRangeDocument(this).createDocumentFragment(); + } else { + if (this.startContainer === this.endContainer && isCharacterDataNode(this.startContainer)) { + clone = this.startContainer.cloneNode(true); + clone.data = clone.data.slice(this.startOffset, this.endOffset); + frag = getRangeDocument(this).createDocumentFragment(); + frag.appendChild(clone); + return frag; + } else { + var iterator = new RangeIterator(this, true); + clone = cloneSubtree(iterator); + iterator.detach(); + } + return clone; + } + }, + + canSurroundContents: function() { + assertRangeValid(this); + assertNodeNotReadOnly(this.startContainer); + assertNodeNotReadOnly(this.endContainer); + + // Check if the contents can be surrounded. Specifically, this means whether the range partially selects + // no non-text nodes. + var iterator = new RangeIterator(this, true); + var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) || + (iterator._last && isNonTextPartiallySelected(iterator._last, this))); + iterator.detach(); + return !boundariesInvalid; + }, + + surroundContents: function(node) { + assertValidNodeType(node, surroundNodeTypes); + + if (!this.canSurroundContents()) { + throw new DOMException("INVALID_STATE_ERR"); + } + + // Extract the contents + var content = this.extractContents(); + + // Clear the children of the node + if (node.hasChildNodes()) { + while (node.lastChild) { + node.removeChild(node.lastChild); + } + } + + // Insert the new node and add the extracted contents + insertNodeAtPosition(node, this.startContainer, this.startOffset); + node.appendChild(content); + + this.selectNode(node); + }, + + cloneRange: function() { + assertRangeValid(this); + var range = new Range(getRangeDocument(this)); + var i = rangeProperties.length, prop; + while (i--) { + prop = rangeProperties[i]; + range[prop] = this[prop]; + } + return range; + }, + + toString: function() { + assertRangeValid(this); + var sc = this.startContainer; + if (sc === this.endContainer && isCharacterDataNode(sc)) { + return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : ""; + } else { + var textParts = [], iterator = new RangeIterator(this, true); + iterateSubtree(iterator, function(node) { + // Accept only text or CDATA nodes, not comments + if (node.nodeType == 3 || node.nodeType == 4) { + textParts.push(node.data); + } + }); + iterator.detach(); + return textParts.join(""); + } + }, + + // The methods below are all non-standard. The following batch were introduced by Mozilla but have since + // been removed from Mozilla. + + compareNode: function(node) { + assertRangeValid(this); + + var parent = node.parentNode; + var nodeIndex = getNodeIndex(node); + + if (!parent) { + throw new DOMException("NOT_FOUND_ERR"); + } + + var startComparison = this.comparePoint(parent, nodeIndex), + endComparison = this.comparePoint(parent, nodeIndex + 1); + + if (startComparison < 0) { // Node starts before + return (endComparison > 0) ? n_b_a : n_b; + } else { + return (endComparison > 0) ? n_a : n_i; + } + }, + + comparePoint: function(node, offset) { + assertRangeValid(this); + assertNode(node, "HIERARCHY_REQUEST_ERR"); + assertSameDocumentOrFragment(node, this.startContainer); + + if (comparePoints(node, offset, this.startContainer, this.startOffset) < 0) { + return -1; + } else if (comparePoints(node, offset, this.endContainer, this.endOffset) > 0) { + return 1; + } + return 0; + }, + + createContextualFragment: createContextualFragment, + + toHtml: function() { + return rangeToHtml(this); + }, + + // touchingIsIntersecting determines whether this method considers a node that borders a range intersects + // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default) + intersectsNode: function(node, touchingIsIntersecting) { + assertRangeValid(this); + assertNode(node, "NOT_FOUND_ERR"); + if (getDocument(node) !== getRangeDocument(this)) { + return false; + } + + var parent = node.parentNode, offset = getNodeIndex(node); + assertNode(parent, "NOT_FOUND_ERR"); + + var startComparison = comparePoints(parent, offset, this.endContainer, this.endOffset), + endComparison = comparePoints(parent, offset + 1, this.startContainer, this.startOffset); + + return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; + }, + + isPointInRange: function(node, offset) { + assertRangeValid(this); + assertNode(node, "HIERARCHY_REQUEST_ERR"); + assertSameDocumentOrFragment(node, this.startContainer); + + return (comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) && + (comparePoints(node, offset, this.endContainer, this.endOffset) <= 0); + }, + + // The methods below are non-standard and invented by me. + + // Sharing a boundary start-to-end or end-to-start does not count as intersection. + intersectsRange: function(range) { + return rangesIntersect(this, range, false); + }, + + // Sharing a boundary start-to-end or end-to-start does count as intersection. + intersectsOrTouchesRange: function(range) { + return rangesIntersect(this, range, true); + }, + + intersection: function(range) { + if (this.intersectsRange(range)) { + var startComparison = comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset), + endComparison = comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset); + + var intersectionRange = this.cloneRange(); + if (startComparison == -1) { + intersectionRange.setStart(range.startContainer, range.startOffset); + } + if (endComparison == 1) { + intersectionRange.setEnd(range.endContainer, range.endOffset); + } + return intersectionRange; + } + return null; + }, + + union: function(range) { + if (this.intersectsOrTouchesRange(range)) { + var unionRange = this.cloneRange(); + if (comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) { + unionRange.setStart(range.startContainer, range.startOffset); + } + if (comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) { + unionRange.setEnd(range.endContainer, range.endOffset); + } + return unionRange; + } else { + throw new DOMException("Ranges do not intersect"); + } + }, + + containsNode: function(node, allowPartial) { + if (allowPartial) { + return this.intersectsNode(node, false); + } else { + return this.compareNode(node) == n_i; + } + }, + + containsNodeContents: function(node) { + return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getNodeLength(node)) <= 0; + }, + + containsRange: function(range) { + var intersection = this.intersection(range); + return intersection !== null && range.equals(intersection); + }, + + containsNodeText: function(node) { + var nodeRange = this.cloneRange(); + nodeRange.selectNode(node); + var textNodes = nodeRange.getNodes([3]); + if (textNodes.length > 0) { + nodeRange.setStart(textNodes[0], 0); + var lastTextNode = textNodes.pop(); + nodeRange.setEnd(lastTextNode, lastTextNode.length); + return this.containsRange(nodeRange); + } else { + return this.containsNodeContents(node); + } + }, + + getNodes: function(nodeTypes, filter) { + assertRangeValid(this); + return getNodesInRange(this, nodeTypes, filter); + }, + + getDocument: function() { + return getRangeDocument(this); + }, + + collapseBefore: function(node) { + this.setEndBefore(node); + this.collapse(false); + }, + + collapseAfter: function(node) { + this.setStartAfter(node); + this.collapse(true); + }, + + getBookmark: function(containerNode) { + var doc = getRangeDocument(this); + var preSelectionRange = api.createRange(doc); + containerNode = containerNode || dom.getBody(doc); + preSelectionRange.selectNodeContents(containerNode); + var range = this.intersection(preSelectionRange); + var start = 0, end = 0; + if (range) { + preSelectionRange.setEnd(range.startContainer, range.startOffset); + start = preSelectionRange.toString().length; + end = start + range.toString().length; + } + + return { + start: start, + end: end, + containerNode: containerNode + }; + }, + + moveToBookmark: function(bookmark) { + var containerNode = bookmark.containerNode; + var charIndex = 0; + this.setStart(containerNode, 0); + this.collapse(true); + var nodeStack = [containerNode], node, foundStart = false, stop = false; + var nextCharIndex, i, childNodes; + + while (!stop && (node = nodeStack.pop())) { + if (node.nodeType == 3) { + nextCharIndex = charIndex + node.length; + if (!foundStart && bookmark.start >= charIndex && bookmark.start <= nextCharIndex) { + this.setStart(node, bookmark.start - charIndex); + foundStart = true; + } + if (foundStart && bookmark.end >= charIndex && bookmark.end <= nextCharIndex) { + this.setEnd(node, bookmark.end - charIndex); + stop = true; + } + charIndex = nextCharIndex; + } else { + childNodes = node.childNodes; + i = childNodes.length; + while (i--) { + nodeStack.push(childNodes[i]); + } + } + } + }, + + getName: function() { + return "DomRange"; + }, + + equals: function(range) { + return Range.rangesEqual(this, range); + }, + + isValid: function() { + return isRangeValid(this); + }, + + inspect: function() { + return inspect(this); + }, + + detach: function() { + // In DOM4, detach() is now a no-op. + } + }); + + function copyComparisonConstantsToObject(obj) { + obj.START_TO_START = s2s; + obj.START_TO_END = s2e; + obj.END_TO_END = e2e; + obj.END_TO_START = e2s; + + obj.NODE_BEFORE = n_b; + obj.NODE_AFTER = n_a; + obj.NODE_BEFORE_AND_AFTER = n_b_a; + obj.NODE_INSIDE = n_i; + } + + function copyComparisonConstants(constructor) { + copyComparisonConstantsToObject(constructor); + copyComparisonConstantsToObject(constructor.prototype); + } + + function createRangeContentRemover(remover, boundaryUpdater) { + return function() { + assertRangeValid(this); + + var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer; + + var iterator = new RangeIterator(this, true); + + // Work out where to position the range after content removal + var node, boundary; + if (sc !== root) { + node = getClosestAncestorIn(sc, root, true); + boundary = getBoundaryAfterNode(node); + sc = boundary.node; + so = boundary.offset; + } + + // Check none of the range is read-only + iterateSubtree(iterator, assertNodeNotReadOnly); + + iterator.reset(); + + // Remove the content + var returnValue = remover(iterator); + iterator.detach(); + + // Move to the new position + boundaryUpdater(this, sc, so, sc, so); + + return returnValue; + }; + } + + function createPrototypeRange(constructor, boundaryUpdater) { + function createBeforeAfterNodeSetter(isBefore, isStart) { + return function(node) { + assertValidNodeType(node, beforeAfterNodeTypes); + assertValidNodeType(getRootContainer(node), rootContainerNodeTypes); + + var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node); + (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset); + }; + } + + function setRangeStart(range, node, offset) { + var ec = range.endContainer, eo = range.endOffset; + if (node !== range.startContainer || offset !== range.startOffset) { + // Check the root containers of the range and the new boundary, and also check whether the new boundary + // is after the current end. In either case, collapse the range to the new position + if (getRootContainer(node) != getRootContainer(ec) || comparePoints(node, offset, ec, eo) == 1) { + ec = node; + eo = offset; + } + boundaryUpdater(range, node, offset, ec, eo); + } + } + + function setRangeEnd(range, node, offset) { + var sc = range.startContainer, so = range.startOffset; + if (node !== range.endContainer || offset !== range.endOffset) { + // Check the root containers of the range and the new boundary, and also check whether the new boundary + // is after the current end. In either case, collapse the range to the new position + if (getRootContainer(node) != getRootContainer(sc) || comparePoints(node, offset, sc, so) == -1) { + sc = node; + so = offset; + } + boundaryUpdater(range, sc, so, node, offset); + } + } + + // Set up inheritance + var F = function() {}; + F.prototype = api.rangePrototype; + constructor.prototype = new F(); + + util.extend(constructor.prototype, { + setStart: function(node, offset) { + assertNoDocTypeNotationEntityAncestor(node, true); + assertValidOffset(node, offset); + + setRangeStart(this, node, offset); + }, + + setEnd: function(node, offset) { + assertNoDocTypeNotationEntityAncestor(node, true); + assertValidOffset(node, offset); + + setRangeEnd(this, node, offset); + }, + + /** + * Convenience method to set a range's start and end boundaries. Overloaded as follows: + * - Two parameters (node, offset) creates a collapsed range at that position + * - Three parameters (node, startOffset, endOffset) creates a range contained with node starting at + * startOffset and ending at endOffset + * - Four parameters (startNode, startOffset, endNode, endOffset) creates a range starting at startOffset in + * startNode and ending at endOffset in endNode + */ + setStartAndEnd: function() { + var args = arguments; + var sc = args[0], so = args[1], ec = sc, eo = so; + + switch (args.length) { + case 3: + eo = args[2]; + break; + case 4: + ec = args[2]; + eo = args[3]; + break; + } + + boundaryUpdater(this, sc, so, ec, eo); + }, + + setBoundary: function(node, offset, isStart) { + this["set" + (isStart ? "Start" : "End")](node, offset); + }, + + setStartBefore: createBeforeAfterNodeSetter(true, true), + setStartAfter: createBeforeAfterNodeSetter(false, true), + setEndBefore: createBeforeAfterNodeSetter(true, false), + setEndAfter: createBeforeAfterNodeSetter(false, false), + + collapse: function(isStart) { + assertRangeValid(this); + if (isStart) { + boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset); + } else { + boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset); + } + }, + + selectNodeContents: function(node) { + assertNoDocTypeNotationEntityAncestor(node, true); + + boundaryUpdater(this, node, 0, node, getNodeLength(node)); + }, + + selectNode: function(node) { + assertNoDocTypeNotationEntityAncestor(node, false); + assertValidNodeType(node, beforeAfterNodeTypes); + + var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node); + boundaryUpdater(this, start.node, start.offset, end.node, end.offset); + }, + + extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater), + + deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater), + + canSurroundContents: function() { + assertRangeValid(this); + assertNodeNotReadOnly(this.startContainer); + assertNodeNotReadOnly(this.endContainer); + + // Check if the contents can be surrounded. Specifically, this means whether the range partially selects + // no non-text nodes. + var iterator = new RangeIterator(this, true); + var boundariesInvalid = (iterator._first && isNonTextPartiallySelected(iterator._first, this) || + (iterator._last && isNonTextPartiallySelected(iterator._last, this))); + iterator.detach(); + return !boundariesInvalid; + }, + + splitBoundaries: function() { + splitRangeBoundaries(this); + }, + + splitBoundariesPreservingPositions: function(positionsToPreserve) { + splitRangeBoundaries(this, positionsToPreserve); + }, + + normalizeBoundaries: function() { + assertRangeValid(this); + + var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset; + + var mergeForward = function(node) { + var sibling = node.nextSibling; + if (sibling && sibling.nodeType == node.nodeType) { + ec = node; + eo = node.length; + node.appendData(sibling.data); + sibling.parentNode.removeChild(sibling); + } + }; + + var mergeBackward = function(node) { + var sibling = node.previousSibling; + if (sibling && sibling.nodeType == node.nodeType) { + sc = node; + var nodeLength = node.length; + so = sibling.length; + node.insertData(0, sibling.data); + sibling.parentNode.removeChild(sibling); + if (sc == ec) { + eo += so; + ec = sc; + } else if (ec == node.parentNode) { + var nodeIndex = getNodeIndex(node); + if (eo == nodeIndex) { + ec = node; + eo = nodeLength; + } else if (eo > nodeIndex) { + eo--; + } + } + } + }; + + var normalizeStart = true; + + if (isCharacterDataNode(ec)) { + if (ec.length == eo) { + mergeForward(ec); + } + } else { + if (eo > 0) { + var endNode = ec.childNodes[eo - 1]; + if (endNode && isCharacterDataNode(endNode)) { + mergeForward(endNode); + } + } + normalizeStart = !this.collapsed; + } + + if (normalizeStart) { + if (isCharacterDataNode(sc)) { + if (so == 0) { + mergeBackward(sc); + } + } else { + if (so < sc.childNodes.length) { + var startNode = sc.childNodes[so]; + if (startNode && isCharacterDataNode(startNode)) { + mergeBackward(startNode); + } + } + } + } else { + sc = ec; + so = eo; + } + + boundaryUpdater(this, sc, so, ec, eo); + }, + + collapseToPoint: function(node, offset) { + assertNoDocTypeNotationEntityAncestor(node, true); + assertValidOffset(node, offset); + this.setStartAndEnd(node, offset); + } + }); + + copyComparisonConstants(constructor); + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Updates commonAncestorContainer and collapsed after boundary change + function updateCollapsedAndCommonAncestor(range) { + range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset); + range.commonAncestorContainer = range.collapsed ? + range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer); + } + + function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) { + range.startContainer = startContainer; + range.startOffset = startOffset; + range.endContainer = endContainer; + range.endOffset = endOffset; + range.document = dom.getDocument(startContainer); + + updateCollapsedAndCommonAncestor(range); + } + + function Range(doc) { + this.startContainer = doc; + this.startOffset = 0; + this.endContainer = doc; + this.endOffset = 0; + this.document = doc; + updateCollapsedAndCommonAncestor(this); + } + + createPrototypeRange(Range, updateBoundaries); + + util.extend(Range, { + rangeProperties: rangeProperties, + RangeIterator: RangeIterator, + copyComparisonConstants: copyComparisonConstants, + createPrototypeRange: createPrototypeRange, + inspect: inspect, + toHtml: rangeToHtml, + getRangeDocument: getRangeDocument, + rangesEqual: function(r1, r2) { + return r1.startContainer === r2.startContainer && + r1.startOffset === r2.startOffset && + r1.endContainer === r2.endContainer && + r1.endOffset === r2.endOffset; + } + }); + + api.DomRange = Range; + }); + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Wrappers for the browser's native DOM Range and/or TextRange implementation + api.createCoreModule("WrappedRange", ["DomRange"], function(api, module) { + var WrappedRange, WrappedTextRange; + var dom = api.dom; + var util = api.util; + var DomPosition = dom.DomPosition; + var DomRange = api.DomRange; + var getBody = dom.getBody; + var getContentDocument = dom.getContentDocument; + var isCharacterDataNode = dom.isCharacterDataNode; + + + /*----------------------------------------------------------------------------------------------------------------*/ + + if (api.features.implementsDomRange) { + // This is a wrapper around the browser's native DOM Range. It has two aims: + // - Provide workarounds for specific browser bugs + // - provide convenient extensions, which are inherited from Rangy's DomRange + + (function() { + var rangeProto; + var rangeProperties = DomRange.rangeProperties; + + function updateRangeProperties(range) { + var i = rangeProperties.length, prop; + while (i--) { + prop = rangeProperties[i]; + range[prop] = range.nativeRange[prop]; + } + // Fix for broken collapsed property in IE 9. + range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset); + } + + function updateNativeRange(range, startContainer, startOffset, endContainer, endOffset) { + var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset); + var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset); + var nativeRangeDifferent = !range.equals(range.nativeRange); + + // Always set both boundaries for the benefit of IE9 (see issue 35) + if (startMoved || endMoved || nativeRangeDifferent) { + range.setEnd(endContainer, endOffset); + range.setStart(startContainer, startOffset); + } + } + + var createBeforeAfterNodeSetter; + + WrappedRange = function(range) { + if (!range) { + throw module.createError("WrappedRange: Range must be specified"); + } + this.nativeRange = range; + updateRangeProperties(this); + }; + + DomRange.createPrototypeRange(WrappedRange, updateNativeRange); + + rangeProto = WrappedRange.prototype; + + rangeProto.selectNode = function(node) { + this.nativeRange.selectNode(node); + updateRangeProperties(this); + }; + + rangeProto.cloneContents = function() { + return this.nativeRange.cloneContents(); + }; + + // Due to a long-standing Firefox bug that I have not been able to find a reliable way to detect, + // insertNode() is never delegated to the native range. + + rangeProto.surroundContents = function(node) { + this.nativeRange.surroundContents(node); + updateRangeProperties(this); + }; + + rangeProto.collapse = function(isStart) { + this.nativeRange.collapse(isStart); + updateRangeProperties(this); + }; + + rangeProto.cloneRange = function() { + return new WrappedRange(this.nativeRange.cloneRange()); + }; + + rangeProto.refresh = function() { + updateRangeProperties(this); + }; + + rangeProto.toString = function() { + return this.nativeRange.toString(); + }; + + // Create test range and node for feature detection + + var testTextNode = document.createTextNode("test"); + getBody(document).appendChild(testTextNode); + var range = document.createRange(); + + /*--------------------------------------------------------------------------------------------------------*/ + + // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and + // correct for it + + range.setStart(testTextNode, 0); + range.setEnd(testTextNode, 0); + + try { + range.setStart(testTextNode, 1); + + rangeProto.setStart = function(node, offset) { + this.nativeRange.setStart(node, offset); + updateRangeProperties(this); + }; + + rangeProto.setEnd = function(node, offset) { + this.nativeRange.setEnd(node, offset); + updateRangeProperties(this); + }; + + createBeforeAfterNodeSetter = function(name) { + return function(node) { + this.nativeRange[name](node); + updateRangeProperties(this); + }; + }; + + } catch(ex) { + + rangeProto.setStart = function(node, offset) { + try { + this.nativeRange.setStart(node, offset); + } catch (ex) { + this.nativeRange.setEnd(node, offset); + this.nativeRange.setStart(node, offset); + } + updateRangeProperties(this); + }; + + rangeProto.setEnd = function(node, offset) { + try { + this.nativeRange.setEnd(node, offset); + } catch (ex) { + this.nativeRange.setStart(node, offset); + this.nativeRange.setEnd(node, offset); + } + updateRangeProperties(this); + }; + + createBeforeAfterNodeSetter = function(name, oppositeName) { + return function(node) { + try { + this.nativeRange[name](node); + } catch (ex) { + this.nativeRange[oppositeName](node); + this.nativeRange[name](node); + } + updateRangeProperties(this); + }; + }; + } + + rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore"); + rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter"); + rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore"); + rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter"); + + /*--------------------------------------------------------------------------------------------------------*/ + + // Always use DOM4-compliant selectNodeContents implementation: it's simpler and less code than testing + // whether the native implementation can be trusted + rangeProto.selectNodeContents = function(node) { + this.setStartAndEnd(node, 0, dom.getNodeLength(node)); + }; + + /*--------------------------------------------------------------------------------------------------------*/ + + // Test for and correct WebKit bug that has the behaviour of compareBoundaryPoints round the wrong way for + // constants START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738 + + range.selectNodeContents(testTextNode); + range.setEnd(testTextNode, 3); + + var range2 = document.createRange(); + range2.selectNodeContents(testTextNode); + range2.setEnd(testTextNode, 4); + range2.setStart(testTextNode, 2); + + if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 && + range.compareBoundaryPoints(range.END_TO_START, range2) == 1) { + // This is the wrong way round, so correct for it + + rangeProto.compareBoundaryPoints = function(type, range) { + range = range.nativeRange || range; + if (type == range.START_TO_END) { + type = range.END_TO_START; + } else if (type == range.END_TO_START) { + type = range.START_TO_END; + } + return this.nativeRange.compareBoundaryPoints(type, range); + }; + } else { + rangeProto.compareBoundaryPoints = function(type, range) { + return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range); + }; + } + + /*--------------------------------------------------------------------------------------------------------*/ + + // Test for IE deleteContents() and extractContents() bug and correct it. See issue 107. + + var el = document.createElement("div"); + el.innerHTML = "123"; + var textNode = el.firstChild; + var body = getBody(document); + body.appendChild(el); + + range.setStart(textNode, 1); + range.setEnd(textNode, 2); + range.deleteContents(); + + if (textNode.data == "13") { + // Behaviour is correct per DOM4 Range so wrap the browser's implementation of deleteContents() and + // extractContents() + rangeProto.deleteContents = function() { + this.nativeRange.deleteContents(); + updateRangeProperties(this); + }; + + rangeProto.extractContents = function() { + var frag = this.nativeRange.extractContents(); + updateRangeProperties(this); + return frag; + }; + } else { + } + + body.removeChild(el); + body = null; + + /*--------------------------------------------------------------------------------------------------------*/ + + // Test for existence of createContextualFragment and delegate to it if it exists + if (util.isHostMethod(range, "createContextualFragment")) { + rangeProto.createContextualFragment = function(fragmentStr) { + return this.nativeRange.createContextualFragment(fragmentStr); + }; + } + + /*--------------------------------------------------------------------------------------------------------*/ + + // Clean up + getBody(document).removeChild(testTextNode); + + rangeProto.getName = function() { + return "WrappedRange"; + }; + + api.WrappedRange = WrappedRange; + + api.createNativeRange = function(doc) { + doc = getContentDocument(doc, module, "createNativeRange"); + return doc.createRange(); + }; + })(); + } + + if (api.features.implementsTextRange) { + /* + This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement() + method. For example, in the following (where pipes denote the selection boundaries): + + + + var range = document.selection.createRange(); + alert(range.parentElement().id); // Should alert "ul" but alerts "b" + + This method returns the common ancestor node of the following: + - the parentElement() of the textRange + - the parentElement() of the textRange after calling collapse(true) + - the parentElement() of the textRange after calling collapse(false) + */ + var getTextRangeContainerElement = function(textRange) { + var parentEl = textRange.parentElement(); + var range = textRange.duplicate(); + range.collapse(true); + var startEl = range.parentElement(); + range = textRange.duplicate(); + range.collapse(false); + var endEl = range.parentElement(); + var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl); + + return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer); + }; + + var textRangeIsCollapsed = function(textRange) { + return textRange.compareEndPoints("StartToEnd", textRange) == 0; + }; + + // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started + // out as an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/) + // but has grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange + // bugs, handling for inputs and images, plus optimizations. + var getTextRangeBoundaryPosition = function(textRange, wholeRangeContainerElement, isStart, isCollapsed, startInfo) { + var workingRange = textRange.duplicate(); + workingRange.collapse(isStart); + var containerElement = workingRange.parentElement(); + + // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so + // check for that + if (!dom.isOrIsAncestorOf(wholeRangeContainerElement, containerElement)) { + containerElement = wholeRangeContainerElement; + } + + + // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and + // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx + if (!containerElement.canHaveHTML) { + var pos = new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement)); + return { + boundaryPosition: pos, + nodeInfo: { + nodeIndex: pos.offset, + containerElement: pos.node + } + }; + } + + var workingNode = dom.getDocument(containerElement).createElement("span"); + + // Workaround for HTML5 Shiv's insane violation of document.createElement(). See Rangy issue 104 and HTML5 + // Shiv issue 64: https://github.com/aFarkas/html5shiv/issues/64 + if (workingNode.parentNode) { + workingNode.parentNode.removeChild(workingNode); + } + + var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd"; + var previousNode, nextNode, boundaryPosition, boundaryNode; + var start = (startInfo && startInfo.containerElement == containerElement) ? startInfo.nodeIndex : 0; + var childNodeCount = containerElement.childNodes.length; + var end = childNodeCount; + + // Check end first. Code within the loop assumes that the endth child node of the container is definitely + // after the range boundary. + var nodeIndex = end; + + while (true) { + if (nodeIndex == childNodeCount) { + containerElement.appendChild(workingNode); + } else { + containerElement.insertBefore(workingNode, containerElement.childNodes[nodeIndex]); + } + workingRange.moveToElementText(workingNode); + comparison = workingRange.compareEndPoints(workingComparisonType, textRange); + if (comparison == 0 || start == end) { + break; + } else if (comparison == -1) { + if (end == start + 1) { + // We know the endth child node is after the range boundary, so we must be done. + break; + } else { + start = nodeIndex; + } + } else { + end = (end == start + 1) ? start : nodeIndex; + } + nodeIndex = Math.floor((start + end) / 2); + containerElement.removeChild(workingNode); + } + + + // We've now reached or gone past the boundary of the text range we're interested in + // so have identified the node we want + boundaryNode = workingNode.nextSibling; + + if (comparison == -1 && boundaryNode && isCharacterDataNode(boundaryNode)) { + // This is a character data node (text, comment, cdata). The working range is collapsed at the start of + // the node containing the text range's boundary, so we move the end of the working range to the + // boundary point and measure the length of its text to get the boundary's offset within the node. + workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange); + + var offset; + + if (/[\r\n]/.test(boundaryNode.data)) { + /* + For the particular case of a boundary within a text node containing rendered line breaks (within a +
 element, for example), we need a slightly complicated approach to get the boundary's offset in
+                        IE. The facts:
+                        
+                        - Each line break is represented as \r in the text node's data/nodeValue properties
+                        - Each line break is represented as \r\n in the TextRange's 'text' property
+                        - The 'text' property of the TextRange does not contain trailing line breaks
+                        
+                        To get round the problem presented by the final fact above, we can use the fact that TextRange's
+                        moveStart() and moveEnd() methods return the actual number of characters moved, which is not
+                        necessarily the same as the number of characters it was instructed to move. The simplest approach is
+                        to use this to store the characters moved when moving both the start and end of the range to the
+                        start of the document body and subtracting the start offset from the end offset (the
+                        "move-negative-gazillion" method). However, this is extremely slow when the document is large and
+                        the range is near the end of it. Clearly doing the mirror image (i.e. moving the range boundaries to
+                        the end of the document) has the same problem.
+                        
+                        Another approach that works is to use moveStart() to move the start boundary of the range up to the
+                        end boundary one character at a time and incrementing a counter with the value returned by the
+                        moveStart() call. However, the check for whether the start boundary has reached the end boundary is
+                        expensive, so this method is slow (although unlike "move-negative-gazillion" is largely unaffected
+                        by the location of the range within the document).
+                        
+                        The approach used below is a hybrid of the two methods above. It uses the fact that a string
+                        containing the TextRange's 'text' property with each \r\n converted to a single \r character cannot
+                        be longer than the text of the TextRange, so the start of the range is moved that length initially
+                        and then a character at a time to make up for any trailing line breaks not contained in the 'text'
+                        property. This has good performance in most situations compared to the previous two methods.
+                        */
+                        var tempRange = workingRange.duplicate();
+                        var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
+
+                        offset = tempRange.moveStart("character", rangeLength);
+                        while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
+                            offset++;
+                            tempRange.moveStart("character", 1);
+                        }
+                    } else {
+                        offset = workingRange.text.length;
+                    }
+                    boundaryPosition = new DomPosition(boundaryNode, offset);
+                } else {
+
+                    // If the boundary immediately follows a character data node and this is the end boundary, we should favour
+                    // a position within that, and likewise for a start boundary preceding a character data node
+                    previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
+                    nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
+                    if (nextNode && isCharacterDataNode(nextNode)) {
+                        boundaryPosition = new DomPosition(nextNode, 0);
+                    } else if (previousNode && isCharacterDataNode(previousNode)) {
+                        boundaryPosition = new DomPosition(previousNode, previousNode.data.length);
+                    } else {
+                        boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
+                    }
+                }
+
+                // Clean up
+                workingNode.parentNode.removeChild(workingNode);
+
+                return {
+                    boundaryPosition: boundaryPosition,
+                    nodeInfo: {
+                        nodeIndex: nodeIndex,
+                        containerElement: containerElement
+                    }
+                };
+            };
+
+            // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that
+            // node. This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
+            // (http://code.google.com/p/ierange/)
+            var createBoundaryTextRange = function(boundaryPosition, isStart) {
+                var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
+                var doc = dom.getDocument(boundaryPosition.node);
+                var workingNode, childNodes, workingRange = getBody(doc).createTextRange();
+                var nodeIsDataNode = isCharacterDataNode(boundaryPosition.node);
+
+                if (nodeIsDataNode) {
+                    boundaryNode = boundaryPosition.node;
+                    boundaryParent = boundaryNode.parentNode;
+                } else {
+                    childNodes = boundaryPosition.node.childNodes;
+                    boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
+                    boundaryParent = boundaryPosition.node;
+                }
+
+                // Position the range immediately before the node containing the boundary
+                workingNode = doc.createElement("span");
+
+                // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within
+                // the element rather than immediately before or after it
+                workingNode.innerHTML = "&#feff;";
+
+                // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
+                // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
+                if (boundaryNode) {
+                    boundaryParent.insertBefore(workingNode, boundaryNode);
+                } else {
+                    boundaryParent.appendChild(workingNode);
+                }
+
+                workingRange.moveToElementText(workingNode);
+                workingRange.collapse(!isStart);
+
+                // Clean up
+                boundaryParent.removeChild(workingNode);
+
+                // Move the working range to the text offset, if required
+                if (nodeIsDataNode) {
+                    workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
+                }
+
+                return workingRange;
+            };
+
+            /*------------------------------------------------------------------------------------------------------------*/
+
+            // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
+            // prototype
+
+            WrappedTextRange = function(textRange) {
+                this.textRange = textRange;
+                this.refresh();
+            };
+
+            WrappedTextRange.prototype = new DomRange(document);
+
+            WrappedTextRange.prototype.refresh = function() {
+                var start, end, startBoundary;
+
+                // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
+                var rangeContainerElement = getTextRangeContainerElement(this.textRange);
+
+                if (textRangeIsCollapsed(this.textRange)) {
+                    end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true,
+                        true).boundaryPosition;
+                } else {
+                    startBoundary = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
+                    start = startBoundary.boundaryPosition;
+
+                    // An optimization used here is that if the start and end boundaries have the same parent element, the
+                    // search scope for the end boundary can be limited to exclude the portion of the element that precedes
+                    // the start boundary
+                    end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false,
+                        startBoundary.nodeInfo).boundaryPosition;
+                }
+
+                this.setStart(start.node, start.offset);
+                this.setEnd(end.node, end.offset);
+            };
+
+            WrappedTextRange.prototype.getName = function() {
+                return "WrappedTextRange";
+            };
+
+            DomRange.copyComparisonConstants(WrappedTextRange);
+
+            var rangeToTextRange = function(range) {
+                if (range.collapsed) {
+                    return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
+                } else {
+                    var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
+                    var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);
+                    var textRange = getBody( DomRange.getRangeDocument(range) ).createTextRange();
+                    textRange.setEndPoint("StartToStart", startRange);
+                    textRange.setEndPoint("EndToEnd", endRange);
+                    return textRange;
+                }
+            };
+
+            WrappedTextRange.rangeToTextRange = rangeToTextRange;
+
+            WrappedTextRange.prototype.toTextRange = function() {
+                return rangeToTextRange(this);
+            };
+
+            api.WrappedTextRange = WrappedTextRange;
+
+            // IE 9 and above have both implementations and Rangy makes both available. The next few lines sets which
+            // implementation to use by default.
+            if (!api.features.implementsDomRange || api.config.preferTextRange) {
+                // Add WrappedTextRange as the Range property of the global object to allow expression like Range.END_TO_END to work
+                var globalObj = (function(f) { return f("return this;")(); })(Function);
+                if (typeof globalObj.Range == "undefined") {
+                    globalObj.Range = WrappedTextRange;
+                }
+
+                api.createNativeRange = function(doc) {
+                    doc = getContentDocument(doc, module, "createNativeRange");
+                    return getBody(doc).createTextRange();
+                };
+
+                api.WrappedRange = WrappedTextRange;
+            }
+        }
+
+        api.createRange = function(doc) {
+            doc = getContentDocument(doc, module, "createRange");
+            return new api.WrappedRange(api.createNativeRange(doc));
+        };
+
+        api.createRangyRange = function(doc) {
+            doc = getContentDocument(doc, module, "createRangyRange");
+            return new DomRange(doc);
+        };
+
+        api.createIframeRange = function(iframeEl) {
+            module.deprecationNotice("createIframeRange()", "createRange(iframeEl)");
+            return api.createRange(iframeEl);
+        };
+
+        api.createIframeRangyRange = function(iframeEl) {
+            module.deprecationNotice("createIframeRangyRange()", "createRangyRange(iframeEl)");
+            return api.createRangyRange(iframeEl);
+        };
+
+        api.addShimListener(function(win) {
+            var doc = win.document;
+            if (typeof doc.createRange == "undefined") {
+                doc.createRange = function() {
+                    return api.createRange(doc);
+                };
+            }
+            doc = win = null;
+        });
+    });
+
+    /*----------------------------------------------------------------------------------------------------------------*/
+
+    // This module creates a selection object wrapper that conforms as closely as possible to the Selection specification
+    // in the HTML Editing spec (http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#selections)
+    api.createCoreModule("WrappedSelection", ["DomRange", "WrappedRange"], function(api, module) {
+        api.config.checkSelectionRanges = true;
+
+        var BOOLEAN = "boolean";
+        var NUMBER = "number";
+        var dom = api.dom;
+        var util = api.util;
+        var isHostMethod = util.isHostMethod;
+        var DomRange = api.DomRange;
+        var WrappedRange = api.WrappedRange;
+        var DOMException = api.DOMException;
+        var DomPosition = dom.DomPosition;
+        var getNativeSelection;
+        var selectionIsCollapsed;
+        var features = api.features;
+        var CONTROL = "Control";
+        var getDocument = dom.getDocument;
+        var getBody = dom.getBody;
+        var rangesEqual = DomRange.rangesEqual;
+
+
+        // Utility function to support direction parameters in the API that may be a string ("backward" or "forward") or a
+        // Boolean (true for backwards).
+        function isDirectionBackward(dir) {
+            return (typeof dir == "string") ? /^backward(s)?$/i.test(dir) : !!dir;
+        }
+
+        function getWindow(win, methodName) {
+            if (!win) {
+                return window;
+            } else if (dom.isWindow(win)) {
+                return win;
+            } else if (win instanceof WrappedSelection) {
+                return win.win;
+            } else {
+                var doc = dom.getContentDocument(win, module, methodName);
+                return dom.getWindow(doc);
+            }
+        }
+
+        function getWinSelection(winParam) {
+            return getWindow(winParam, "getWinSelection").getSelection();
+        }
+
+        function getDocSelection(winParam) {
+            return getWindow(winParam, "getDocSelection").document.selection;
+        }
+        
+        function winSelectionIsBackward(sel) {
+            var backward = false;
+            if (sel.anchorNode) {
+                backward = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
+            }
+            return backward;
+        }
+
+        // Test for the Range/TextRange and Selection features required
+        // Test for ability to retrieve selection
+        var implementsWinGetSelection = isHostMethod(window, "getSelection"),
+            implementsDocSelection = util.isHostObject(document, "selection");
+
+        features.implementsWinGetSelection = implementsWinGetSelection;
+        features.implementsDocSelection = implementsDocSelection;
+
+        var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
+
+        if (useDocumentSelection) {
+            getNativeSelection = getDocSelection;
+            api.isSelectionValid = function(winParam) {
+                var doc = getWindow(winParam, "isSelectionValid").document, nativeSel = doc.selection;
+
+                // Check whether the selection TextRange is actually contained within the correct document
+                return (nativeSel.type != "None" || getDocument(nativeSel.createRange().parentElement()) == doc);
+            };
+        } else if (implementsWinGetSelection) {
+            getNativeSelection = getWinSelection;
+            api.isSelectionValid = function() {
+                return true;
+            };
+        } else {
+            module.fail("Neither document.selection or window.getSelection() detected.");
+        }
+
+        api.getNativeSelection = getNativeSelection;
+
+        var testSelection = getNativeSelection();
+        var testRange = api.createNativeRange(document);
+        var body = getBody(document);
+
+        // Obtaining a range from a selection
+        var selectionHasAnchorAndFocus = util.areHostProperties(testSelection,
+            ["anchorNode", "focusNode", "anchorOffset", "focusOffset"]);
+
+        features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
+
+        // Test for existence of native selection extend() method
+        var selectionHasExtend = isHostMethod(testSelection, "extend");
+        features.selectionHasExtend = selectionHasExtend;
+        
+        // Test if rangeCount exists
+        var selectionHasRangeCount = (typeof testSelection.rangeCount == NUMBER);
+        features.selectionHasRangeCount = selectionHasRangeCount;
+
+        var selectionSupportsMultipleRanges = false;
+        var collapsedNonEditableSelectionsSupported = true;
+
+        var addRangeBackwardToNative = selectionHasExtend ?
+            function(nativeSelection, range) {
+                var doc = DomRange.getRangeDocument(range);
+                var endRange = api.createRange(doc);
+                endRange.collapseToPoint(range.endContainer, range.endOffset);
+                nativeSelection.addRange(getNativeRange(endRange));
+                nativeSelection.extend(range.startContainer, range.startOffset);
+            } : null;
+
+        if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
+                typeof testSelection.rangeCount == NUMBER && features.implementsDomRange) {
+
+            (function() {
+                // Previously an iframe was used but this caused problems in some circumstances in IE, so tests are
+                // performed on the current document's selection. See issue 109.
+
+                // Note also that if a selection previously existed, it is wiped by these tests. This should usually be fine
+                // because initialization usually happens when the document loads, but could be a problem for a script that
+                // loads and initializes Rangy later. If anyone complains, code could be added to save and restore the
+                // selection.
+                var sel = window.getSelection();
+                if (sel) {
+                    // Store the current selection
+                    var originalSelectionRangeCount = sel.rangeCount;
+                    var selectionHasMultipleRanges = (originalSelectionRangeCount > 1);
+                    var originalSelectionRanges = [];
+                    var originalSelectionBackward = winSelectionIsBackward(sel); 
+                    for (var i = 0; i < originalSelectionRangeCount; ++i) {
+                        originalSelectionRanges[i] = sel.getRangeAt(i);
+                    }
+                    
+                    // Create some test elements
+                    var body = getBody(document);
+                    var testEl = body.appendChild( document.createElement("div") );
+                    testEl.contentEditable = "false";
+                    var textNode = testEl.appendChild( document.createTextNode("\u00a0\u00a0\u00a0") );
+
+                    // Test whether the native selection will allow a collapsed selection within a non-editable element
+                    var r1 = document.createRange();
+
+                    r1.setStart(textNode, 1);
+                    r1.collapse(true);
+                    sel.addRange(r1);
+                    collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
+                    sel.removeAllRanges();
+
+                    // Test whether the native selection is capable of supporting multiple ranges.
+                    if (!selectionHasMultipleRanges) {
+                        // Doing the original feature test here in Chrome 36 (and presumably later versions) prints a
+                        // console error of "Discontiguous selection is not supported." that cannot be suppressed. There's
+                        // nothing we can do about this while retaining the feature test so we have to resort to a browser
+                        // sniff. I'm not happy about it. See
+                        // https://code.google.com/p/chromium/issues/detail?id=399791
+                        var chromeMatch = window.navigator.appVersion.match(/Chrome\/(.*?) /);
+                        if (chromeMatch && parseInt(chromeMatch[1]) >= 36) {
+                            selectionSupportsMultipleRanges = false;
+                        } else {
+                            var r2 = r1.cloneRange();
+                            r1.setStart(textNode, 0);
+                            r2.setEnd(textNode, 3);
+                            r2.setStart(textNode, 2);
+                            sel.addRange(r1);
+                            sel.addRange(r2);
+                            selectionSupportsMultipleRanges = (sel.rangeCount == 2);
+                        }
+                    }
+
+                    // Clean up
+                    body.removeChild(testEl);
+                    sel.removeAllRanges();
+
+                    for (i = 0; i < originalSelectionRangeCount; ++i) {
+                        if (i == 0 && originalSelectionBackward) {
+                            if (addRangeBackwardToNative) {
+                                addRangeBackwardToNative(sel, originalSelectionRanges[i]);
+                            } else {
+                                api.warn("Rangy initialization: original selection was backwards but selection has been restored forwards because the browser does not support Selection.extend");
+                                sel.addRange(originalSelectionRanges[i]);
+                            }
+                        } else {
+                            sel.addRange(originalSelectionRanges[i]);
+                        }
+                    }
+                }
+            })();
+        }
+
+        features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
+        features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
+
+        // ControlRanges
+        var implementsControlRange = false, testControlRange;
+
+        if (body && isHostMethod(body, "createControlRange")) {
+            testControlRange = body.createControlRange();
+            if (util.areHostProperties(testControlRange, ["item", "add"])) {
+                implementsControlRange = true;
+            }
+        }
+        features.implementsControlRange = implementsControlRange;
+
+        // Selection collapsedness
+        if (selectionHasAnchorAndFocus) {
+            selectionIsCollapsed = function(sel) {
+                return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
+            };
+        } else {
+            selectionIsCollapsed = function(sel) {
+                return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
+            };
+        }
+
+        function updateAnchorAndFocusFromRange(sel, range, backward) {
+            var anchorPrefix = backward ? "end" : "start", focusPrefix = backward ? "start" : "end";
+            sel.anchorNode = range[anchorPrefix + "Container"];
+            sel.anchorOffset = range[anchorPrefix + "Offset"];
+            sel.focusNode = range[focusPrefix + "Container"];
+            sel.focusOffset = range[focusPrefix + "Offset"];
+        }
+
+        function updateAnchorAndFocusFromNativeSelection(sel) {
+            var nativeSel = sel.nativeSelection;
+            sel.anchorNode = nativeSel.anchorNode;
+            sel.anchorOffset = nativeSel.anchorOffset;
+            sel.focusNode = nativeSel.focusNode;
+            sel.focusOffset = nativeSel.focusOffset;
+        }
+
+        function updateEmptySelection(sel) {
+            sel.anchorNode = sel.focusNode = null;
+            sel.anchorOffset = sel.focusOffset = 0;
+            sel.rangeCount = 0;
+            sel.isCollapsed = true;
+            sel._ranges.length = 0;
+        }
+
+        function getNativeRange(range) {
+            var nativeRange;
+            if (range instanceof DomRange) {
+                nativeRange = api.createNativeRange(range.getDocument());
+                nativeRange.setEnd(range.endContainer, range.endOffset);
+                nativeRange.setStart(range.startContainer, range.startOffset);
+            } else if (range instanceof WrappedRange) {
+                nativeRange = range.nativeRange;
+            } else if (features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {
+                nativeRange = range;
+            }
+            return nativeRange;
+        }
+
+        function rangeContainsSingleElement(rangeNodes) {
+            if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
+                return false;
+            }
+            for (var i = 1, len = rangeNodes.length; i < len; ++i) {
+                if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        function getSingleElementFromRange(range) {
+            var nodes = range.getNodes();
+            if (!rangeContainsSingleElement(nodes)) {
+                throw module.createError("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
+            }
+            return nodes[0];
+        }
+
+        // Simple, quick test which only needs to distinguish between a TextRange and a ControlRange
+        function isTextRange(range) {
+            return !!range && typeof range.text != "undefined";
+        }
+
+        function updateFromTextRange(sel, range) {
+            // Create a Range from the selected TextRange
+            var wrappedRange = new WrappedRange(range);
+            sel._ranges = [wrappedRange];
+
+            updateAnchorAndFocusFromRange(sel, wrappedRange, false);
+            sel.rangeCount = 1;
+            sel.isCollapsed = wrappedRange.collapsed;
+        }
+
+        function updateControlSelection(sel) {
+            // Update the wrapped selection based on what's now in the native selection
+            sel._ranges.length = 0;
+            if (sel.docSelection.type == "None") {
+                updateEmptySelection(sel);
+            } else {
+                var controlRange = sel.docSelection.createRange();
+                if (isTextRange(controlRange)) {
+                    // This case (where the selection type is "Control" and calling createRange() on the selection returns
+                    // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected
+                    // ControlRange have been removed from the ControlRange and removed from the document.
+                    updateFromTextRange(sel, controlRange);
+                } else {
+                    sel.rangeCount = controlRange.length;
+                    var range, doc = getDocument(controlRange.item(0));
+                    for (var i = 0; i < sel.rangeCount; ++i) {
+                        range = api.createRange(doc);
+                        range.selectNode(controlRange.item(i));
+                        sel._ranges.push(range);
+                    }
+                    sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
+                    updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
+                }
+            }
+        }
+
+        function addRangeToControlSelection(sel, range) {
+            var controlRange = sel.docSelection.createRange();
+            var rangeElement = getSingleElementFromRange(range);
+
+            // Create a new ControlRange containing all the elements in the selected ControlRange plus the element
+            // contained by the supplied range
+            var doc = getDocument(controlRange.item(0));
+            var newControlRange = getBody(doc).createControlRange();
+            for (var i = 0, len = controlRange.length; i < len; ++i) {
+                newControlRange.add(controlRange.item(i));
+            }
+            try {
+                newControlRange.add(rangeElement);
+            } catch (ex) {
+                throw module.createError("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
+            }
+            newControlRange.select();
+
+            // Update the wrapped selection based on what's now in the native selection
+            updateControlSelection(sel);
+        }
+
+        var getSelectionRangeAt;
+
+        if (isHostMethod(testSelection, "getRangeAt")) {
+            // try/catch is present because getRangeAt() must have thrown an error in some browser and some situation.
+            // Unfortunately, I didn't write a comment about the specifics and am now scared to take it out. Let that be a
+            // lesson to us all, especially me.
+            getSelectionRangeAt = function(sel, index) {
+                try {
+                    return sel.getRangeAt(index);
+                } catch (ex) {
+                    return null;
+                }
+            };
+        } else if (selectionHasAnchorAndFocus) {
+            getSelectionRangeAt = function(sel) {
+                var doc = getDocument(sel.anchorNode);
+                var range = api.createRange(doc);
+                range.setStartAndEnd(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset);
+
+                // Handle the case when the selection was selected backwards (from the end to the start in the
+                // document)
+                if (range.collapsed !== this.isCollapsed) {
+                    range.setStartAndEnd(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset);
+                }
+
+                return range;
+            };
+        }
+
+        function WrappedSelection(selection, docSelection, win) {
+            this.nativeSelection = selection;
+            this.docSelection = docSelection;
+            this._ranges = [];
+            this.win = win;
+            this.refresh();
+        }
+
+        WrappedSelection.prototype = api.selectionPrototype;
+
+        function deleteProperties(sel) {
+            sel.win = sel.anchorNode = sel.focusNode = sel._ranges = null;
+            sel.rangeCount = sel.anchorOffset = sel.focusOffset = 0;
+            sel.detached = true;
+        }
+
+        var cachedRangySelections = [];
+
+        function actOnCachedSelection(win, action) {
+            var i = cachedRangySelections.length, cached, sel;
+            while (i--) {
+                cached = cachedRangySelections[i];
+                sel = cached.selection;
+                if (action == "deleteAll") {
+                    deleteProperties(sel);
+                } else if (cached.win == win) {
+                    if (action == "delete") {
+                        cachedRangySelections.splice(i, 1);
+                        return true;
+                    } else {
+                        return sel;
+                    }
+                }
+            }
+            if (action == "deleteAll") {
+                cachedRangySelections.length = 0;
+            }
+            return null;
+        }
+
+        var getSelection = function(win) {
+            // Check if the parameter is a Rangy Selection object
+            if (win && win instanceof WrappedSelection) {
+                win.refresh();
+                return win;
+            }
+
+            win = getWindow(win, "getNativeSelection");
+
+            var sel = actOnCachedSelection(win);
+            var nativeSel = getNativeSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
+            if (sel) {
+                sel.nativeSelection = nativeSel;
+                sel.docSelection = docSel;
+                sel.refresh();
+            } else {
+                sel = new WrappedSelection(nativeSel, docSel, win);
+                cachedRangySelections.push( { win: win, selection: sel } );
+            }
+            return sel;
+        };
+
+        api.getSelection = getSelection;
+
+        api.getIframeSelection = function(iframeEl) {
+            module.deprecationNotice("getIframeSelection()", "getSelection(iframeEl)");
+            return api.getSelection(dom.getIframeWindow(iframeEl));
+        };
+
+        var selProto = WrappedSelection.prototype;
+
+        function createControlSelection(sel, ranges) {
+            // Ensure that the selection becomes of type "Control"
+            var doc = getDocument(ranges[0].startContainer);
+            var controlRange = getBody(doc).createControlRange();
+            for (var i = 0, el, len = ranges.length; i < len; ++i) {
+                el = getSingleElementFromRange(ranges[i]);
+                try {
+                    controlRange.add(el);
+                } catch (ex) {
+                    throw module.createError("setRanges(): Element within one of the specified Ranges could not be added to control selection (does it have layout?)");
+                }
+            }
+            controlRange.select();
+
+            // Update the wrapped selection based on what's now in the native selection
+            updateControlSelection(sel);
+        }
+
+        // Selecting a range
+        if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
+            selProto.removeAllRanges = function() {
+                this.nativeSelection.removeAllRanges();
+                updateEmptySelection(this);
+            };
+
+            var addRangeBackward = function(sel, range) {
+                addRangeBackwardToNative(sel.nativeSelection, range);
+                sel.refresh();
+            };
+
+            if (selectionHasRangeCount) {
+                selProto.addRange = function(range, direction) {
+                    if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
+                        addRangeToControlSelection(this, range);
+                    } else {
+                        if (isDirectionBackward(direction) && selectionHasExtend) {
+                            addRangeBackward(this, range);
+                        } else {
+                            var previousRangeCount;
+                            if (selectionSupportsMultipleRanges) {
+                                previousRangeCount = this.rangeCount;
+                            } else {
+                                this.removeAllRanges();
+                                previousRangeCount = 0;
+                            }
+                            // Clone the native range so that changing the selected range does not affect the selection.
+                            // This is contrary to the spec but is the only way to achieve consistency between browsers. See
+                            // issue 80.
+                            var clonedNativeRange = getNativeRange(range).cloneRange();
+                            try {
+                                this.nativeSelection.addRange(clonedNativeRange);
+                            } catch (ex) {
+                            }
+
+                            // Check whether adding the range was successful
+                            this.rangeCount = this.nativeSelection.rangeCount;
+
+                            if (this.rangeCount == previousRangeCount + 1) {
+                                // The range was added successfully
+
+                                // Check whether the range that we added to the selection is reflected in the last range extracted from
+                                // the selection
+                                if (api.config.checkSelectionRanges) {
+                                    var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);
+                                    if (nativeRange && !rangesEqual(nativeRange, range)) {
+                                        // Happens in WebKit with, for example, a selection placed at the start of a text node
+                                        range = new WrappedRange(nativeRange);
+                                    }
+                                }
+                                this._ranges[this.rangeCount - 1] = range;
+                                updateAnchorAndFocusFromRange(this, range, selectionIsBackward(this.nativeSelection));
+                                this.isCollapsed = selectionIsCollapsed(this);
+                            } else {
+                                // The range was not added successfully. The simplest thing is to refresh
+                                this.refresh();
+                            }
+                        }
+                    }
+                };
+            } else {
+                selProto.addRange = function(range, direction) {
+                    if (isDirectionBackward(direction) && selectionHasExtend) {
+                        addRangeBackward(this, range);
+                    } else {
+                        this.nativeSelection.addRange(getNativeRange(range));
+                        this.refresh();
+                    }
+                };
+            }
+
+            selProto.setRanges = function(ranges) {
+                if (implementsControlRange && implementsDocSelection && ranges.length > 1) {
+                    createControlSelection(this, ranges);
+                } else {
+                    this.removeAllRanges();
+                    for (var i = 0, len = ranges.length; i < len; ++i) {
+                        this.addRange(ranges[i]);
+                    }
+                }
+            };
+        } else if (isHostMethod(testSelection, "empty") && isHostMethod(testRange, "select") &&
+                   implementsControlRange && useDocumentSelection) {
+
+            selProto.removeAllRanges = function() {
+                // Added try/catch as fix for issue #21
+                try {
+                    this.docSelection.empty();
+
+                    // Check for empty() not working (issue #24)
+                    if (this.docSelection.type != "None") {
+                        // Work around failure to empty a control selection by instead selecting a TextRange and then
+                        // calling empty()
+                        var doc;
+                        if (this.anchorNode) {
+                            doc = getDocument(this.anchorNode);
+                        } else if (this.docSelection.type == CONTROL) {
+                            var controlRange = this.docSelection.createRange();
+                            if (controlRange.length) {
+                                doc = getDocument( controlRange.item(0) );
+                            }
+                        }
+                        if (doc) {
+                            var textRange = getBody(doc).createTextRange();
+                            textRange.select();
+                            this.docSelection.empty();
+                        }
+                    }
+                } catch(ex) {}
+                updateEmptySelection(this);
+            };
+
+            selProto.addRange = function(range) {
+                if (this.docSelection.type == CONTROL) {
+                    addRangeToControlSelection(this, range);
+                } else {
+                    api.WrappedTextRange.rangeToTextRange(range).select();
+                    this._ranges[0] = range;
+                    this.rangeCount = 1;
+                    this.isCollapsed = this._ranges[0].collapsed;
+                    updateAnchorAndFocusFromRange(this, range, false);
+                }
+            };
+
+            selProto.setRanges = function(ranges) {
+                this.removeAllRanges();
+                var rangeCount = ranges.length;
+                if (rangeCount > 1) {
+                    createControlSelection(this, ranges);
+                } else if (rangeCount) {
+                    this.addRange(ranges[0]);
+                }
+            };
+        } else {
+            module.fail("No means of selecting a Range or TextRange was found");
+            return false;
+        }
+
+        selProto.getRangeAt = function(index) {
+            if (index < 0 || index >= this.rangeCount) {
+                throw new DOMException("INDEX_SIZE_ERR");
+            } else {
+                // Clone the range to preserve selection-range independence. See issue 80.
+                return this._ranges[index].cloneRange();
+            }
+        };
+
+        var refreshSelection;
+
+        if (useDocumentSelection) {
+            refreshSelection = function(sel) {
+                var range;
+                if (api.isSelectionValid(sel.win)) {
+                    range = sel.docSelection.createRange();
+                } else {
+                    range = getBody(sel.win.document).createTextRange();
+                    range.collapse(true);
+                }
+
+                if (sel.docSelection.type == CONTROL) {
+                    updateControlSelection(sel);
+                } else if (isTextRange(range)) {
+                    updateFromTextRange(sel, range);
+                } else {
+                    updateEmptySelection(sel);
+                }
+            };
+        } else if (isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == NUMBER) {
+            refreshSelection = function(sel) {
+                if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {
+                    updateControlSelection(sel);
+                } else {
+                    sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;
+                    if (sel.rangeCount) {
+                        for (var i = 0, len = sel.rangeCount; i < len; ++i) {
+                            sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));
+                        }
+                        updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackward(sel.nativeSelection));
+                        sel.isCollapsed = selectionIsCollapsed(sel);
+                    } else {
+                        updateEmptySelection(sel);
+                    }
+                }
+            };
+        } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && features.implementsDomRange) {
+            refreshSelection = function(sel) {
+                var range, nativeSel = sel.nativeSelection;
+                if (nativeSel.anchorNode) {
+                    range = getSelectionRangeAt(nativeSel, 0);
+                    sel._ranges = [range];
+                    sel.rangeCount = 1;
+                    updateAnchorAndFocusFromNativeSelection(sel);
+                    sel.isCollapsed = selectionIsCollapsed(sel);
+                } else {
+                    updateEmptySelection(sel);
+                }
+            };
+        } else {
+            module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
+            return false;
+        }
+
+        selProto.refresh = function(checkForChanges) {
+            var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
+            var oldAnchorNode = this.anchorNode, oldAnchorOffset = this.anchorOffset;
+
+            refreshSelection(this);
+            if (checkForChanges) {
+                // Check the range count first
+                var i = oldRanges.length;
+                if (i != this._ranges.length) {
+                    return true;
+                }
+
+                // Now check the direction. Checking the anchor position is the same is enough since we're checking all the
+                // ranges after this
+                if (this.anchorNode != oldAnchorNode || this.anchorOffset != oldAnchorOffset) {
+                    return true;
+                }
+
+                // Finally, compare each range in turn
+                while (i--) {
+                    if (!rangesEqual(oldRanges[i], this._ranges[i])) {
+                        return true;
+                    }
+                }
+                return false;
+            }
+        };
+
+        // Removal of a single range
+        var removeRangeManually = function(sel, range) {
+            var ranges = sel.getAllRanges();
+            sel.removeAllRanges();
+            for (var i = 0, len = ranges.length; i < len; ++i) {
+                if (!rangesEqual(range, ranges[i])) {
+                    sel.addRange(ranges[i]);
+                }
+            }
+            if (!sel.rangeCount) {
+                updateEmptySelection(sel);
+            }
+        };
+
+        if (implementsControlRange && implementsDocSelection) {
+            selProto.removeRange = function(range) {
+                if (this.docSelection.type == CONTROL) {
+                    var controlRange = this.docSelection.createRange();
+                    var rangeElement = getSingleElementFromRange(range);
+
+                    // Create a new ControlRange containing all the elements in the selected ControlRange minus the
+                    // element contained by the supplied range
+                    var doc = getDocument(controlRange.item(0));
+                    var newControlRange = getBody(doc).createControlRange();
+                    var el, removed = false;
+                    for (var i = 0, len = controlRange.length; i < len; ++i) {
+                        el = controlRange.item(i);
+                        if (el !== rangeElement || removed) {
+                            newControlRange.add(controlRange.item(i));
+                        } else {
+                            removed = true;
+                        }
+                    }
+                    newControlRange.select();
+
+                    // Update the wrapped selection based on what's now in the native selection
+                    updateControlSelection(this);
+                } else {
+                    removeRangeManually(this, range);
+                }
+            };
+        } else {
+            selProto.removeRange = function(range) {
+                removeRangeManually(this, range);
+            };
+        }
+
+        // Detecting if a selection is backward
+        var selectionIsBackward;
+        if (!useDocumentSelection && selectionHasAnchorAndFocus && features.implementsDomRange) {
+            selectionIsBackward = winSelectionIsBackward;
+
+            selProto.isBackward = function() {
+                return selectionIsBackward(this);
+            };
+        } else {
+            selectionIsBackward = selProto.isBackward = function() {
+                return false;
+            };
+        }
+
+        // Create an alias for backwards compatibility. From 1.3, everything is "backward" rather than "backwards"
+        selProto.isBackwards = selProto.isBackward;
+
+        // Selection stringifier
+        // This is conformant to the old HTML5 selections draft spec but differs from WebKit and Mozilla's implementation.
+        // The current spec does not yet define this method.
+        selProto.toString = function() {
+            var rangeTexts = [];
+            for (var i = 0, len = this.rangeCount; i < len; ++i) {
+                rangeTexts[i] = "" + this._ranges[i];
+            }
+            return rangeTexts.join("");
+        };
+
+        function assertNodeInSameDocument(sel, node) {
+            if (sel.win.document != getDocument(node)) {
+                throw new DOMException("WRONG_DOCUMENT_ERR");
+            }
+        }
+
+        // No current browser conforms fully to the spec for this method, so Rangy's own method is always used
+        selProto.collapse = function(node, offset) {
+            assertNodeInSameDocument(this, node);
+            var range = api.createRange(node);
+            range.collapseToPoint(node, offset);
+            this.setSingleRange(range);
+            this.isCollapsed = true;
+        };
+
+        selProto.collapseToStart = function() {
+            if (this.rangeCount) {
+                var range = this._ranges[0];
+                this.collapse(range.startContainer, range.startOffset);
+            } else {
+                throw new DOMException("INVALID_STATE_ERR");
+            }
+        };
+
+        selProto.collapseToEnd = function() {
+            if (this.rangeCount) {
+                var range = this._ranges[this.rangeCount - 1];
+                this.collapse(range.endContainer, range.endOffset);
+            } else {
+                throw new DOMException("INVALID_STATE_ERR");
+            }
+        };
+
+        // The spec is very specific on how selectAllChildren should be implemented so the native implementation is
+        // never used by Rangy.
+        selProto.selectAllChildren = function(node) {
+            assertNodeInSameDocument(this, node);
+            var range = api.createRange(node);
+            range.selectNodeContents(node);
+            this.setSingleRange(range);
+        };
+
+        selProto.deleteFromDocument = function() {
+            // Sepcial behaviour required for IE's control selections
+            if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
+                var controlRange = this.docSelection.createRange();
+                var element;
+                while (controlRange.length) {
+                    element = controlRange.item(0);
+                    controlRange.remove(element);
+                    element.parentNode.removeChild(element);
+                }
+                this.refresh();
+            } else if (this.rangeCount) {
+                var ranges = this.getAllRanges();
+                if (ranges.length) {
+                    this.removeAllRanges();
+                    for (var i = 0, len = ranges.length; i < len; ++i) {
+                        ranges[i].deleteContents();
+                    }
+                    // The spec says nothing about what the selection should contain after calling deleteContents on each
+                    // range. Firefox moves the selection to where the final selected range was, so we emulate that
+                    this.addRange(ranges[len - 1]);
+                }
+            }
+        };
+
+        // The following are non-standard extensions
+        selProto.eachRange = function(func, returnValue) {
+            for (var i = 0, len = this._ranges.length; i < len; ++i) {
+                if ( func( this.getRangeAt(i) ) ) {
+                    return returnValue;
+                }
+            }
+        };
+
+        selProto.getAllRanges = function() {
+            var ranges = [];
+            this.eachRange(function(range) {
+                ranges.push(range);
+            });
+            return ranges;
+        };
+
+        selProto.setSingleRange = function(range, direction) {
+            this.removeAllRanges();
+            this.addRange(range, direction);
+        };
+
+        selProto.callMethodOnEachRange = function(methodName, params) {
+            var results = [];
+            this.eachRange( function(range) {
+                results.push( range[methodName].apply(range, params) );
+            } );
+            return results;
+        };
+        
+        function createStartOrEndSetter(isStart) {
+            return function(node, offset) {
+                var range;
+                if (this.rangeCount) {
+                    range = this.getRangeAt(0);
+                    range["set" + (isStart ? "Start" : "End")](node, offset);
+                } else {
+                    range = api.createRange(this.win.document);
+                    range.setStartAndEnd(node, offset);
+                }
+                this.setSingleRange(range, this.isBackward());
+            };
+        }
+
+        selProto.setStart = createStartOrEndSetter(true);
+        selProto.setEnd = createStartOrEndSetter(false);
+        
+        // Add select() method to Range prototype. Any existing selection will be removed.
+        api.rangePrototype.select = function(direction) {
+            getSelection( this.getDocument() ).setSingleRange(this, direction);
+        };
+
+        selProto.changeEachRange = function(func) {
+            var ranges = [];
+            var backward = this.isBackward();
+
+            this.eachRange(function(range) {
+                func(range);
+                ranges.push(range);
+            });
+
+            this.removeAllRanges();
+            if (backward && ranges.length == 1) {
+                this.addRange(ranges[0], "backward");
+            } else {
+                this.setRanges(ranges);
+            }
+        };
+
+        selProto.containsNode = function(node, allowPartial) {
+            return this.eachRange( function(range) {
+                return range.containsNode(node, allowPartial);
+            }, true ) || false;
+        };
+
+        selProto.getBookmark = function(containerNode) {
+            return {
+                backward: this.isBackward(),
+                rangeBookmarks: this.callMethodOnEachRange("getBookmark", [containerNode])
+            };
+        };
+
+        selProto.moveToBookmark = function(bookmark) {
+            var selRanges = [];
+            for (var i = 0, rangeBookmark, range; rangeBookmark = bookmark.rangeBookmarks[i++]; ) {
+                range = api.createRange(this.win);
+                range.moveToBookmark(rangeBookmark);
+                selRanges.push(range);
+            }
+            if (bookmark.backward) {
+                this.setSingleRange(selRanges[0], "backward");
+            } else {
+                this.setRanges(selRanges);
+            }
+        };
+
+        selProto.toHtml = function() {
+            var rangeHtmls = [];
+            this.eachRange(function(range) {
+                rangeHtmls.push( DomRange.toHtml(range) );
+            });
+            return rangeHtmls.join("");
+        };
+
+        if (features.implementsTextRange) {
+            selProto.getNativeTextRange = function() {
+                var sel, textRange;
+                if ( (sel = this.docSelection) ) {
+                    var range = sel.createRange();
+                    if (isTextRange(range)) {
+                        return range;
+                    } else {
+                        throw module.createError("getNativeTextRange: selection is a control selection"); 
+                    }
+                } else if (this.rangeCount > 0) {
+                    return api.WrappedTextRange.rangeToTextRange( this.getRangeAt(0) );
+                } else {
+                    throw module.createError("getNativeTextRange: selection contains no range");
+                }
+            };
+        }
+
+        function inspect(sel) {
+            var rangeInspects = [];
+            var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);
+            var focus = new DomPosition(sel.focusNode, sel.focusOffset);
+            var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";
+
+            if (typeof sel.rangeCount != "undefined") {
+                for (var i = 0, len = sel.rangeCount; i < len; ++i) {
+                    rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
+                }
+            }
+            return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
+                    ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
+        }
+
+        selProto.getName = function() {
+            return "WrappedSelection";
+        };
+
+        selProto.inspect = function() {
+            return inspect(this);
+        };
+
+        selProto.detach = function() {
+            actOnCachedSelection(this.win, "delete");
+            deleteProperties(this);
+        };
+
+        WrappedSelection.detachAll = function() {
+            actOnCachedSelection(null, "deleteAll");
+        };
+
+        WrappedSelection.inspect = inspect;
+        WrappedSelection.isDirectionBackward = isDirectionBackward;
+
+        api.Selection = WrappedSelection;
+
+        api.selectionPrototype = selProto;
+
+        api.addShimListener(function(win) {
+            if (typeof win.getSelection == "undefined") {
+                win.getSelection = function() {
+                    return getSelection(win);
+                };
+            }
+            win = null;
+        });
+    });
+    
+
+    /*----------------------------------------------------------------------------------------------------------------*/
+
+    // Wait for document to load before initializing
+    var docReady = false;
+
+    var loadHandler = function(e) {
+        if (!docReady) {
+            docReady = true;
+            if (!api.initialized && api.config.autoInitialize) {
+                init();
+            }
+        }
+    };
+
+    if (isBrowser) {
+        // Test whether the document has already been loaded and initialize immediately if so
+        if (document.readyState == "complete") {
+            loadHandler();
+        } else {
+            if (isHostMethod(document, "addEventListener")) {
+                document.addEventListener("DOMContentLoaded", loadHandler, false);
+            }
+
+            // Add a fallback in case the DOMContentLoaded event isn't supported
+            addListener(window, "load", loadHandler);
+        }
+    }
+
+    return api;
+}, this);
\ No newline at end of file
diff --git a/app/assets/javascripts/app/views/layout_ref/index.jst.eco b/app/assets/javascripts/app/views/layout_ref/index.jst.eco
index 7f04795df..ae9c4d412 100644
--- a/app/assets/javascripts/app/views/layout_ref/index.jst.eco
+++ b/app/assets/javascripts/app/views/layout_ref/index.jst.eco
@@ -18,6 +18,7 @@
     
  • User Profile
  • Organization Profile
  • Setup Wizard
  • +
  • Richtext
  • \ No newline at end of file diff --git a/app/assets/javascripts/app/views/layout_ref/richtext.jst.eco b/app/assets/javascripts/app/views/layout_ref/richtext.jst.eco new file mode 100644 index 000000000..b0230664d --- /dev/null +++ b/app/assets/javascripts/app/views/layout_ref/richtext.jst.eco @@ -0,0 +1,26 @@ +
    + +

    Richtext

    + +

    Singleline / Textonly

    + +
    +
    +
    +
    + +

    Multiline / Textonly

    + +
    +
    +
    +
    + +

    Multiline / Richtext

    + +
    +
    +
    +
    + +
    \ No newline at end of file diff --git a/app/assets/stylesheets/zammad.css.scss b/app/assets/stylesheets/zammad.css.scss index 65948a1ea..b5a3e0b8a 100644 --- a/app/assets/stylesheets/zammad.css.scss +++ b/app/assets/stylesheets/zammad.css.scss @@ -36,6 +36,9 @@ small { font-size: 12px; } +blockquote { + font-size: inherit; +} .u-unclickable { pointer-events: none;