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 = "
"
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('' + this.tagName, 'i');
+ newTag = newTag.replace(regex, '' + replacementTag);
+
+ $(@).replaceWith(newTag);
+ )
+
diff --git a/app/assets/javascripts/app/lib/base/jquery.contenteditable.js b/app/assets/javascripts/app/lib/base/jquery.contenteditable.js
index 7723f0d2a..8c9d89dc3 100644
--- a/app/assets/javascripts/app/lib/base/jquery.contenteditable.js
+++ b/app/assets/javascripts/app/lib/base/jquery.contenteditable.js
@@ -12,6 +12,32 @@
defaults = {
mode: 'richtext',
multiline: true,
+ allowKey: {
+ 8: true, // backspace
+ 9: true, // tab
+ 16: true, // shift
+ 17: true, // ctrl
+ 18: true, // alt
+ 20: true, // cabslock
+ 37: true, // up
+ 38: true, // right
+ 39: true, // down
+ 40: true, // left
+ 91: true, // cmd left
+ 92: true, // cmd right
+ },
+ extraAllowKey: {
+ 65: true, // a + ctrl - select all
+ 67: true, // c + ctrl - copy
+ 86: true, // v + ctrl - paste
+ 88: true, // x + ctrl - cut
+ 90: true, // z + ctrl - undo
+ },
+ richTextFormatKey: {
+ 66: true, // b
+ 73: true, // i
+ 85: true, // u
+ },
};
function Plugin( element, options ) {
@@ -28,26 +54,157 @@
this.options.placeholder = this.$element.data('placeholder')
}
- // link input
- if ( !this.options.multiline ) {
- editorMode = Medium.inlineMode
+ this.preventInput = false
+
+ this.init();
+ // bind
+
+ // bind paste
+ }
+
+
+
+ Plugin.prototype.init = function () {
+ var _this = this
+
+ // handle enter
+ this.$element.on('keydown', function (e) {
+ console.log('keydown', e.keyCode)
+ if ( _this.preventInput ) {
+ console.log('preventInput', _this.preventInput)
+ return
+ }
+
+ // strap the return key being pressed
+ if (e.keyCode === 13) {
+
+ // disbale multi line
+ if ( !_this.options.multiline ) {
+ e.preventDefault()
+ return
+ }
+ // limit check
+ if ( !_this.maxLengthOk( true ) ) {
+ e.preventDefault()
+ return
+ }
+ }
+ })
+
+ this.$element.on('keyup', function (e) {
+ console.log('KU')
+ if ( _this.options.mode === 'textonly' ) {
+ console.log('REMOVE TAGS')
+ if ( !_this.options.multiline ) {
+ App.Utils.htmlRemoveTags(_this.$element)
+ }
+ else {
+ App.Utils.htmlRemoveRichtext(_this.$element)
+ }
+ }
+ else {
+ App.Utils.htmlClanup(_this.$element)
+ }
+ })
+
+
+ // just paste text
+ this.$element.on('paste', function (e) {
+ console.log('paste')
+ if ( _this.options.mode === 'textonly' ) {
+ console.log('REMOVE TAGS')
+ if ( !_this.options.multiline ) {
+ App.Utils.htmlRemoveTags(_this.$element)
+ }
+ else {
+ App.Utils.htmlRemoveRichtext(_this.$element)
+ }
+ }
+ else {
+ App.Utils.htmlClanup(_this.$element)
+ }
+ return true
+ if ( this.options.mode === 'textonly' ) {
+ e.preventDefault()
+ var text
+ if (window.clipboardData) { // IE
+ text = window.clipboardData.getData('Text')
+ }
+ else {
+ text = (e.originalEvent || e).clipboardData.getData('text/plain')
+ }
+ var overlimit = false
+ if (text) {
+
+ // replace new lines
+ if ( !_this.options.multiline ) {
+ text = text.replace(/\n/g, '')
+ text = text.replace(/\r/g, '')
+ text = text.replace(/\t/g, '')
+ }
+
+ // limit length, limit paste string
+ if ( _this.options.maxlength ) {
+ var pasteLength = text.length
+ var currentLength = _this.$element.text().length
+ var overSize = ( currentLength + pasteLength ) - _this.options.maxlength
+ if ( overSize > 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;