Version 3.18.1
Show:

File: editor/js/editor-selection.js

                /**
                 * Wraps some common Selection/Range functionality into a simple object
                 * @class EditorSelection
                 * @constructor
                 * @module editor
                 * @submodule selection
                 */
            
                //TODO This shouldn't be there, Y.Node doesn't normalize getting textnode content.
                var textContent = 'textContent',
                INNER_HTML = 'innerHTML',
                FONT_FAMILY = 'fontFamily';
            
                if (Y.UA.ie && Y.UA.ie < 11) {
                    textContent = 'nodeValue';
                }
            
                Y.EditorSelection = function(domEvent) {
                    var sel, par, ieNode, nodes, rng, i,
                        comp, moved = 0, n, id, root = Y.EditorSelection.ROOT;
            
            
                    if (Y.config.win.getSelection && (!Y.UA.ie || Y.UA.ie < 9 || Y.UA.ie > 10)) {
                        sel = Y.config.win.getSelection();
                    } else if (Y.config.doc.selection) {
                        sel = Y.config.doc.selection.createRange();
                    }
                    this._selection = sel;
            
                    if (!sel) {
                        return false;
                    }
            
                    if (sel.pasteHTML) {
                        this.isCollapsed = (sel.compareEndPoints('StartToEnd', sel)) ? false : true;
                        if (this.isCollapsed) {
                            this.anchorNode = this.focusNode = Y.one(sel.parentElement());
            
                            if (domEvent) {
                                ieNode = Y.config.doc.elementFromPoint(domEvent.clientX, domEvent.clientY);
                            }
                            rng = sel.duplicate();
                            if (!ieNode) {
                                par = sel.parentElement();
                                nodes = par.childNodes;
            
                                for (i = 0; i < nodes.length; i++) {
                                    //This causes IE to not allow a selection on a doubleclick
                                    //rng.select(nodes[i]);
                                    if (rng.inRange(sel)) {
                                        if (!ieNode) {
                                            ieNode = nodes[i];
                                        }
                                    }
                                }
                            }
            
                            this.ieNode = ieNode;
            
                            if (ieNode) {
                                if (ieNode.nodeType !== 3) {
                                    if (ieNode.firstChild) {
                                        ieNode = ieNode.firstChild;
                                    }
                                    if (root.compareTo(ieNode)) {
                                        if (ieNode.firstChild) {
                                            ieNode = ieNode.firstChild;
                                        }
                                    }
                                }
                                this.anchorNode = this.focusNode = Y.EditorSelection.resolve(ieNode);
            
                                rng.moveToElementText(sel.parentElement());
                                comp = sel.compareEndPoints('StartToStart', rng);
                                if (comp) {
                                    //We are not at the beginning of the selection.
                                    //Setting the move to something large, may need to increase it later
                                    moved = this.getEditorOffset(root);
                                    sel.move('character', -(moved));
                                }
            
                                this.anchorOffset = this.focusOffset = moved;
            
                                this.anchorTextNode = this.focusTextNode = Y.one(ieNode);
                            }
            
            
                        } else {
                            //This helps IE deal with a selection and nodeChange events
                            if (sel.htmlText && sel.htmlText !== '') {
                                n = Y.Node.create(sel.htmlText);
                                if (n && n.get('id')) {
                                    id = n.get('id');
                                    this.anchorNode = this.focusNode = Y.one('#' + id);
                                } else if (n) {
                                    n = n.get('childNodes');
                                    this.anchorNode = this.focusNode = n.item(0);
                                }
                            }
                        }
            
                        //var self = this;
                        //debugger;
                    } else {
                        this.isCollapsed = sel.isCollapsed;
                        this.anchorNode = Y.EditorSelection.resolve(sel.anchorNode);
                        this.focusNode = Y.EditorSelection.resolve(sel.focusNode);
                        this.anchorOffset = sel.anchorOffset;
                        this.focusOffset = sel.focusOffset;
            
                        this.anchorTextNode = Y.one(sel.anchorNode || this.anchorNode);
                        this.focusTextNode = Y.one(sel.focusNode || this.focusNode);
                    }
                    if (Y.Lang.isString(sel.text)) {
                        this.text = sel.text;
                    } else {
                        if (sel.toString) {
                            this.text = sel.toString();
                        } else {
                            this.text = '';
                        }
                    }
                };
            
                /**
                * Utility method to remove dead font-family styles from an element.
                * @static
                * @method removeFontFamily
                */
                Y.EditorSelection.removeFontFamily = function(n) {
                    n.removeAttribute('face');
                    var s = n.getAttribute('style').toLowerCase();
                    if (s === '' || (s === 'font-family: ')) {
                        n.removeAttribute('style');
                    }
                    if (s.match(Y.EditorSelection.REG_FONTFAMILY)) {
                        s = s.replace(Y.EditorSelection.REG_FONTFAMILY, '');
                        n.setAttribute('style', s);
                    }
                };
            
                /**
                * Performs a prefilter on all nodes in the editor. Looks for nodes with a style: fontFamily or font face
                * It then creates a dynamic class assigns it and removed the property. This is so that we don't lose
                * the fontFamily when selecting nodes.
                * @static
                * @method filter
                */
                Y.EditorSelection.filter = function(blocks) {
                    Y.log('Filtering nodes', 'info', 'editor-selection');
            
                    var startTime = (new Date()).getTime(),
                        editorSelection = Y.EditorSelection,
                        root = editorSelection.ROOT,
                        endTime,
                        nodes = root.all(editorSelection.ALL),
                        baseNodes = root.all('strong,em'),
                        doc = Y.config.doc, hrs,
                        classNames = {}, cssString = '',
                        ls, startTime1 = (new Date()).getTime(),
                        endTime1;
            
                    nodes.each(function(n) {
                        var raw = Y.Node.getDOMNode(n);
                        if (raw.style[FONT_FAMILY]) {
                            classNames['.' + n._yuid] = raw.style[FONT_FAMILY];
                            n.addClass(n._yuid);
            
                            editorSelection.removeFontFamily(raw);
                        }
                    });
                    endTime1 = (new Date()).getTime();
                    Y.log('Node Filter Timer: ' + (endTime1 - startTime1) + 'ms', 'info', 'editor-selection');
            
                    root.all('.hr').addClass('yui-skip').addClass('yui-non');
            
                    if (Y.UA.ie) {
                        hrs = Y.Node.getDOMNode(root).getElementsByTagName('hr');
                        Y.each(hrs, function(hr) {
                            var el = doc.createElement('div'),
                            s = el.style;
            
                            el.className = 'hr yui-non yui-skip';
            
                            el.setAttribute('readonly', true);
                            el.setAttribute('contenteditable', false); //Keep it from being Edited
                            if (hr.parentNode) {
                                hr.parentNode.replaceChild(el, hr);
                            }
                            //Had to move to inline style. writes for ie's < 8. They don't render el.setAttribute('style');
                            s.border = '1px solid #ccc';
                            s.lineHeight = '0';
                            s.height = '0';
                            s.fontSize = '0';
                            s.marginTop = '5px';
                            s.marginBottom = '5px';
                            s.marginLeft = '0px';
                            s.marginRight = '0px';
                            s.padding = '0';
                        });
                    }
            
            
                    Y.each(classNames, function(v, k) {
                        cssString += k + ' { font-family: ' + v.replace(/"/gi, '') + '; }';
                    });
                    Y.StyleSheet(cssString, 'editor');
            
            
                    //Not sure about this one?
                    baseNodes.each(function(n, k) {
                        var t = n.get('tagName').toLowerCase(),
                            newTag = 'i';
                        if (t === 'strong') {
                            newTag = 'b';
                        }
                        editorSelection.prototype._swap(baseNodes.item(k), newTag);
                    });
            
                    //Filter out all the empty UL/OL's
                    ls = root.all('ol,ul');
                    ls.each(function(v) {
                        var lis = v.all('li');
                        if (!lis.size()) {
                            v.remove();
                        }
                    });
            
                    if (blocks) {
                        editorSelection.filterBlocks();
                    }
                    endTime = (new Date()).getTime();
                    Y.log('Filter Timer: ' + (endTime - startTime) + 'ms', 'info', 'editor-selection');
                };
            
                /**
                * Method attempts to replace all "orphined" text nodes in the main body by wrapping them with a <p>. Called from filter.
                * @static
                * @method filterBlocks
                */
                Y.EditorSelection.filterBlocks = function() {
                    Y.log('RAW filter blocks', 'info', 'editor-selection');
                    var startTime = (new Date()).getTime(), endTime,
                        childs = Y.Node.getDOMNode(Y.EditorSelection.ROOT).childNodes, i, node, wrapped = false, doit = true,
                        sel, single, br, c, s, html;
            
                    if (childs) {
                        for (i = 0; i < childs.length; i++) {
                            node = Y.one(childs[i]);
                            if (!node.test(Y.EditorSelection.BLOCKS)) {
                                doit = true;
                                if (childs[i].nodeType === 3) {
                                    c = childs[i][textContent].match(Y.EditorSelection.REG_CHAR);
                                    s = childs[i][textContent].match(Y.EditorSelection.REG_NON);
                                    if (c === null && s) {
                                        doit = false;
            
                                    }
                                }
                                if (doit) {
                                    if (!wrapped) {
                                        wrapped = [];
                                    }
                                    wrapped.push(childs[i]);
                                }
                            } else {
                                wrapped = Y.EditorSelection._wrapBlock(wrapped);
                            }
                        }
                        wrapped = Y.EditorSelection._wrapBlock(wrapped);
                    }
            
                    single = Y.all(Y.EditorSelection.DEFAULT_BLOCK_TAG);
                    if (single.size() === 1) {
                        Y.log('Only One default block tag (' + Y.EditorSelection.DEFAULT_BLOCK_TAG + '), focus it..', 'info', 'editor-selection');
                        br = single.item(0).all('br');
                        if (br.size() === 1) {
                            if (!br.item(0).test('.yui-cursor')) {
                                br.item(0).remove();
                            }
                            html = single.item(0).get('innerHTML');
                            if (html === '' || html === ' ') {
                                Y.log('Paragraph empty, focusing cursor', 'info', 'editor-selection');
                                single.set('innerHTML', Y.EditorSelection.CURSOR);
                                sel = new Y.EditorSelection();
                                sel.focusCursor(true, true);
                            }
                            if (br.item(0).test('.yui-cursor') && Y.UA.ie) {
                                br.item(0).remove();
                            }
                        }
                    } else {
                        single.each(function(p) {
                            var html = p.get('innerHTML');
                            if (html === '') {
                                Y.log('Empty Paragraph Tag Found, Removing It', 'info', 'editor-selection');
                                p.remove();
                            }
                        });
                    }
            
                    endTime = (new Date()).getTime();
                    Y.log('FilterBlocks Timer: ' + (endTime - startTime) + 'ms', 'info', 'editor-selection');
                };
            
                /**
                * Regular Expression used to find dead font-family styles
                * @static
                * @property REG_FONTFAMILY
                */
                Y.EditorSelection.REG_FONTFAMILY = /font-family:\s*;/;
            
                /**
                * Regular Expression to determine if a string has a character in it
                * @static
                * @property REG_CHAR
                */
                Y.EditorSelection.REG_CHAR = /[a-zA-Z-0-9_!@#\$%\^&*\(\)-=_+\[\]\\{}|;':",.\/<>\?]/gi;
            
                /**
                * Regular Expression to determine if a string has a non-character in it
                * @static
                * @property REG_NON
                */
                Y.EditorSelection.REG_NON = /[\s|\n|\t]/gi;
            
                /**
                * Regular Expression to remove all HTML from a string
                * @static
                * @property REG_NOHTML
                */
                Y.EditorSelection.REG_NOHTML = /<\S[^><]*>/g;
            
            
                /**
                * Wraps an array of elements in a Block level tag
                * @static
                * @private
                * @method _wrapBlock
                */
                Y.EditorSelection._wrapBlock = function(wrapped) {
                    if (wrapped) {
                        var newChild = Y.Node.create('<' + Y.EditorSelection.DEFAULT_BLOCK_TAG + '></' + Y.EditorSelection.DEFAULT_BLOCK_TAG + '>'),
                            firstChild = Y.one(wrapped[0]), i;
            
                        for (i = 1; i < wrapped.length; i++) {
                            newChild.append(wrapped[i]);
                        }
                        firstChild.replace(newChild);
                        newChild.prepend(firstChild);
                    }
                    return false;
                };
            
                /**
                * Undoes what filter does enough to return the HTML from the Editor, then re-applies the filter.
                * @static
                * @method unfilter
                * @return {String} The filtered HTML
                */
                Y.EditorSelection.unfilter = function() {
                    var root = Y.EditorSelection.ROOT,
                        nodes = root.all('[class]'),
                        html = '', nons, ids,
                        body = root;
            
                    Y.log('UnFiltering nodes', 'info', 'editor-selection');
            
                    nodes.each(function(n) {
                        if (n.hasClass(n._yuid)) {
                            //One of ours
                            n.setStyle(FONT_FAMILY, n.getStyle(FONT_FAMILY));
                            n.removeClass(n._yuid);
                            if (n.getAttribute('class') === '') {
                                n.removeAttribute('class');
                            }
                        }
                    });
            
                    nons = root.all('.yui-non');
                    nons.each(function(n) {
                        if (!n.hasClass('yui-skip') && n.get('innerHTML') === '') {
                            n.remove();
                        } else {
                            n.removeClass('yui-non').removeClass('yui-skip');
                        }
                    });
            
                    ids = root.all('[id]');
                    ids.each(function(n) {
                        if (n.get('id').indexOf('yui_3_') === 0) {
                            n.removeAttribute('id');
                            n.removeAttribute('_yuid');
                        }
                    });
            
                    if (body) {
                        html = body.get('innerHTML');
                    }
            
                    root.all('.hr').addClass('yui-skip').addClass('yui-non');
            
                    /*
                    nodes.each(function(n) {
                        n.addClass(n._yuid);
                        n.setStyle(FONT_FAMILY, '');
                        if (n.getAttribute('style') === '') {
                            n.removeAttribute('style');
                        }
                    });
                    */
            
                    return html;
                };
                /**
                * Resolve a node from the selection object and return a Node instance
                * @static
                * @method resolve
                * @param {HTMLElement} n The HTMLElement to resolve. Might be a TextNode, gives parentNode.
                * @return {Node} The Resolved node
                */
                Y.EditorSelection.resolve = function(n) {
                    if (!n) {
                        return Y.EditorSelection.ROOT;
                    }
            
                    if (n && n.nodeType === 3) {
                        //Adding a try/catch here because in rare occasions IE will
                        //Throw a error accessing the parentNode of a stranded text node.
                        //In the case of Ctrl+Z (Undo)
                        try {
                            n = n.parentNode;
                        } catch (re) {
                            n = Y.EditorSelection.ROOT;
                        }
                    }
                    return Y.one(n);
                };
            
                /**
                * Returns the innerHTML of a node with all HTML tags removed.
                * @static
                * @method getText
                * @param {Node} node The Node instance to remove the HTML from
                * @return {String} The string of text
                */
                Y.EditorSelection.getText = function(node) {
                    var txt = node.get('innerHTML').replace(Y.EditorSelection.REG_NOHTML, '');
                    //Clean out the cursor subs to see if the Node is empty
                    txt = txt.replace('<span><br></span>', '').replace('<br>', '');
                    return txt;
                };
            
                //Y.EditorSelection.DEFAULT_BLOCK_TAG = 'div';
                Y.EditorSelection.DEFAULT_BLOCK_TAG = 'p';
            
                /**
                * The selector to use when looking for Nodes to cache the value of: [style],font[face]
                * @static
                * @property ALL
                */
                Y.EditorSelection.ALL = '[style],font[face]';
            
                /**
                * The selector to use when looking for block level items.
                * @static
                * @property BLOCKS
                */
                Y.EditorSelection.BLOCKS = 'p,div,ul,ol,table,style';
                /**
                * The temporary fontname applied to a selection to retrieve their values: yui-tmp
                * @static
                * @property TMP
                */
                Y.EditorSelection.TMP = 'yui-tmp';
                /**
                * The default tag to use when creating elements: span
                * @static
                * @property DEFAULT_TAG
                */
                Y.EditorSelection.DEFAULT_TAG = 'span';
            
                /**
                * The id of the outer cursor wrapper
                * @static
                * @property CURID
                */
                Y.EditorSelection.CURID = 'yui-cursor';
            
                /**
                * The id used to wrap the inner space of the cursor position
                * @static
                * @property CUR_WRAPID
                */
                Y.EditorSelection.CUR_WRAPID = 'yui-cursor-wrapper';
            
                /**
                * The default HTML used to focus the cursor..
                * @static
                * @property CURSOR
                */
                Y.EditorSelection.CURSOR = '<span><br class="yui-cursor"></span>';
            
                /**
                * The default HTML element from which data will be retrieved. Default: body
                * @static
                * @property ROOT
                */
                Y.EditorSelection.ROOT = Y.one('body');
            
                Y.EditorSelection.hasCursor = function() {
                    var cur = Y.all('#' + Y.EditorSelection.CUR_WRAPID);
                    Y.log('Has Cursor: ' + cur.size(), 'info', 'editor-selection');
                    return cur.size();
                };
            
                /**
                * Called from Editor keydown to remove the "extra" space before the cursor.
                * @static
                * @method cleanCursor
                */
                Y.EditorSelection.cleanCursor = function() {
                    //Y.log('Cleaning Cursor', 'info', 'Selection');
                    var cur, sel = 'br.yui-cursor';
                    cur = Y.all(sel);
                    if (cur.size()) {
                        cur.each(function(b) {
                            var c = b.get('parentNode.parentNode.childNodes'), html;
                            if (c.size()) {
                                b.remove();
                            } else {
                                html = Y.EditorSelection.getText(c.item(0));
                                if (html !== '') {
                                    b.remove();
                                }
                            }
                        });
                    }
                    /*
                    var cur = Y.all('#' + Y.EditorSelection.CUR_WRAPID);
                    if (cur.size()) {
                        cur.each(function(c) {
                            var html = c.get('innerHTML');
                            if (html == '&nbsp;' || html == '<br>') {
                                if (c.previous() || c.next()) {
                                    c.remove();
                                }
                            }
                        });
                    }
                    */
                };
            
                Y.EditorSelection.prototype = {
                    /**
                    * Range text value
                    * @property text
                    * @type String
                    */
                    text: null,
                    /**
                    * Flag to show if the range is collapsed or not
                    * @property isCollapsed
                    * @type Boolean
                    */
                    isCollapsed: null,
                    /**
                    * A Node instance of the parentNode of the anchorNode of the range
                    * @property anchorNode
                    * @type Node
                    */
                    anchorNode: null,
                    /**
                    * The offset from the range object
                    * @property anchorOffset
                    * @type Number
                    */
                    anchorOffset: null,
                    /**
                    * A Node instance of the actual textNode of the range.
                    * @property anchorTextNode
                    * @type Node
                    */
                    anchorTextNode: null,
                    /**
                    * A Node instance of the parentNode of the focusNode of the range
                    * @property focusNode
                    * @type Node
                    */
                    focusNode: null,
                    /**
                    * The offset from the range object
                    * @property focusOffset
                    * @type Number
                    */
                    focusOffset: null,
                    /**
                    * A Node instance of the actual textNode of the range.
                    * @property focusTextNode
                    * @type Node
                    */
                    focusTextNode: null,
                    /**
                    * The actual Selection/Range object
                    * @property _selection
                    * @private
                    */
                    _selection: null,
                    /**
                    * Wrap an element, with another element
                    * @private
                    * @method _wrap
                    * @param {HTMLElement} n The node to wrap
                    * @param {String} tag The tag to use when creating the new element.
                    * @return {HTMLElement} The wrapped node
                    */
                    _wrap: function(n, tag) {
                        var tmp = Y.Node.create('<' + tag + '></' + tag + '>');
                        tmp.set(INNER_HTML, n.get(INNER_HTML));
                        n.set(INNER_HTML, '');
                        n.append(tmp);
                        return Y.Node.getDOMNode(tmp);
                    },
                    /**
                    * Swap an element, with another element
                    * @private
                    * @method _swap
                    * @param {HTMLElement} n The node to swap
                    * @param {String} tag The tag to use when creating the new element.
                    * @return {HTMLElement} The new node
                    */
                    _swap: function(n, tag) {
                        var tmp = Y.Node.create('<' + tag + '></' + tag + '>');
                        tmp.set(INNER_HTML, n.get(INNER_HTML));
                        n.replace(tmp, n);
                        return Y.Node.getDOMNode(tmp);
                    },
                    /**
                    * Get all the nodes in the current selection. This method will actually perform a filter first.
                    * Then it calls doc.execCommand('fontname', null, 'yui-tmp') to touch all nodes in the selection.
                    * The it compiles a list of all nodes affected by the execCommand and builds a NodeList to return.
                    * @method getSelected
                    * @return {NodeList} A NodeList of all items in the selection.
                    */
                    getSelected: function() {
                        var editorSelection = Y.EditorSelection,
                            root = editorSelection.ROOT,
                            nodes,
                            items = [];
            
                        editorSelection.filter();
                        Y.config.doc.execCommand('fontname', null, editorSelection.TMP);
                        nodes = root.all(editorSelection.ALL);
            
                        nodes.each(function(n, k) {
                            if (n.getStyle(FONT_FAMILY) === editorSelection.TMP) {
                                n.setStyle(FONT_FAMILY, '');
                                editorSelection.removeFontFamily(n);
                                if (!n.compareTo(root)) {
                                    items.push(Y.Node.getDOMNode(nodes.item(k)));
                                }
                            }
                        });
                        return Y.all(items);
                    },
                    /**
                    * Insert HTML at the current cursor position and return a Node instance of the newly inserted element.
                    * @method insertContent
                    * @param {String} html The HTML to insert.
                    * @return {Node} The inserted Node.
                    */
                    insertContent: function(html) {
                        return this.insertAtCursor(html, this.anchorTextNode, this.anchorOffset, true);
                    },
                    /**
                    * Insert HTML at the current cursor position, this method gives you control over the text node to insert into and the offset where to put it.
                    * @method insertAtCursor
                    * @param {String} html The HTML to insert.
                    * @param {Node} node The text node to break when inserting.
                    * @param {Number} offset The left offset of the text node to break and insert the new content.
                    * @param {Boolean} collapse Should the range be collapsed after insertion. default: false
                    * @return {Node} The inserted Node.
                    */
                    insertAtCursor: function(html, node, offset, collapse) {
                        var cur = Y.Node.create('<' + Y.EditorSelection.DEFAULT_TAG + ' class="yui-non"></' + Y.EditorSelection.DEFAULT_TAG + '>'),
                            inHTML, txt, txt2, newNode, range = this.createRange(), b, root = Y.EditorSelection.ROOT;
            
                        if (root.compareTo(node)) {
                            b = Y.Node.create('<span></span>');
                            node.append(b);
                            node = b;
                        }
            
            
                        if (range.pasteHTML) {
                            if (offset === 0 && node && !node.previous() && node.get('nodeType') === 3) {
                                /*
                                * For some strange reason, range.pasteHTML fails if the node is a textNode and
                                * the offset is 0. (The cursor is at the beginning of the line)
                                * It will always insert the new content at position 1 instead of
                                * position 0. Here we test for that case and do it the hard way.
                                */
                                node.insert(html, 'before');
                                if (range.moveToElementText) {
                                    range.moveToElementText(Y.Node.getDOMNode(node.previous()));
                                }
                                //Move the cursor after the new node
                                range.collapse(false);
                                range.select();
                                return node.previous();
                            } else {
                                newNode = Y.Node.create(html);
                                try {
                                    range.pasteHTML('<span id="rte-insert"></span>');
                                } catch (e) {}
                                inHTML = root.one('#rte-insert');
                                if (inHTML) {
                                    inHTML.set('id', '');
                                    inHTML.replace(newNode);
                                    if (range.moveToElementText) {
                                        range.moveToElementText(Y.Node.getDOMNode(newNode));
                                    }
                                    range.collapse(false);
                                    range.select();
                                    return newNode;
                                } else {
                                    Y.on('available', function() {
                                        inHTML.set('id', '');
                                        inHTML.replace(newNode);
                                        if (range.moveToElementText) {
                                            range.moveToElementText(Y.Node.getDOMNode(newNode));
                                        }
                                        range.collapse(false);
                                        range.select();
                                    }, '#rte-insert');
                                }
                            }
                        } else {
                            //TODO using Y.Node.create here throws warnings & strips first white space character
                            //txt = Y.one(Y.Node.create(inHTML.substr(0, offset)));
                            //txt2 = Y.one(Y.Node.create(inHTML.substr(offset)));
                            if (offset > 0) {
                                inHTML = node.get(textContent);
            
                                txt = Y.one(Y.config.doc.createTextNode(inHTML.substr(0, offset)));
                                txt2 = Y.one(Y.config.doc.createTextNode(inHTML.substr(offset)));
            
                                node.replace(txt, node);
                                newNode = Y.Node.create(html);
                                if (newNode.get('nodeType') === 11) {
                                    b = Y.Node.create('<span></span>');
                                    b.append(newNode);
                                    newNode = b;
                                }
                                txt.insert(newNode, 'after');
                                //if (txt2 && txt2.get('length')) {
                                if (txt2) {
                                    newNode.insert(cur, 'after');
                                    cur.insert(txt2, 'after');
                                    this.selectNode(cur, collapse);
                                }
                            } else {
                                if (node.get('nodeType') === 3) {
                                    node = node.get('parentNode') || root;
                                }
                                newNode = Y.Node.create(html);
                                html = node.get('innerHTML').replace(/\n/gi, '');
                                if (html === '' || html === '<br>') {
                                    node.append(newNode);
                                } else {
                                    if (newNode.get('parentNode')) {
                                        node.insert(newNode, 'before');
                                    } else {
                                        root.prepend(newNode);
                                    }
                                }
                                if (node.get('firstChild').test('br')) {
                                    node.get('firstChild').remove();
                                }
                            }
                        }
                        return newNode;
                    },
                    /**
                    * Get all elements inside a selection and wrap them with a new element and return a NodeList of all elements touched.
                    * @method wrapContent
                    * @param {String} tag The tag to wrap all selected items with.
                    * @return {NodeList} A NodeList of all items in the selection.
                    */
                    wrapContent: function(tag) {
                        tag = (tag) ? tag : Y.EditorSelection.DEFAULT_TAG;
            
                        if (!this.isCollapsed) {
                            Y.log('Wrapping selection with: ' + tag, 'info', 'editor-selection');
                            var items = this.getSelected(),
                                changed = [], range, last, first, range2;
            
                            items.each(function(n, k) {
                                var t = n.get('tagName').toLowerCase();
                                if (t === 'font') {
                                    changed.push(this._swap(items.item(k), tag));
                                } else {
                                    changed.push(this._wrap(items.item(k), tag));
                                }
                            }, this);
            
                            range = this.createRange();
                            first = changed[0];
                            last = changed[changed.length - 1];
                            if (this._selection.removeAllRanges) {
                                range.setStart(changed[0], 0);
                                range.setEnd(last, last.childNodes.length);
                                this._selection.removeAllRanges();
                                this._selection.addRange(range);
                            } else {
                                if (range.moveToElementText) {
                                    range.moveToElementText(Y.Node.getDOMNode(first));
                                    range2 = this.createRange();
                                    range2.moveToElementText(Y.Node.getDOMNode(last));
                                    range.setEndPoint('EndToEnd', range2);
                                }
                                range.select();
                            }
            
                            changed = Y.all(changed);
                            Y.log('Returning NodeList with (' + changed.size() + ') item(s)' , 'info', 'editor-selection');
                            return changed;
            
            
                        } else {
                            Y.log('Can not wrap a collapsed selection, use insertContent', 'error', 'editor-selection');
                            return Y.all([]);
                        }
                    },
                    /**
                    * Find and replace a string inside a text node and replace it with HTML focusing the node after
                    * to allow you to continue to type.
                    * @method replace
                    * @param {String} se The string to search for.
                    * @param {String} re The string of HTML to replace it with.
                    * @return {Node} The node inserted.
                    */
                    replace: function(se,re) {
                        Y.log('replacing (' + se + ') with (' + re + ')');
                        var range = this.createRange(), node, txt, index, newNode;
            
                        if (range.getBookmark) {
                            index = range.getBookmark();
                            txt = this.anchorNode.get('innerHTML').replace(se, re);
                            this.anchorNode.set('innerHTML', txt);
                            range.moveToBookmark(index);
                            newNode = Y.one(range.parentElement());
                        } else {
                            node = this.anchorTextNode;
                            txt = node.get(textContent);
                            index = txt.indexOf(se);
            
                            txt = txt.replace(se, '');
                            node.set(textContent, txt);
                            newNode = this.insertAtCursor(re, node, index, true);
                        }
                        return newNode;
                    },
                    /**
                    * Destroy the range.
                    * @method remove
                    * @chainable
                    * @return {EditorSelection}
                    */
                    remove: function() {
                        if (this._selection && this._selection.removeAllRanges) {
                            this._selection.removeAllRanges();
                        }
                        return this;
                    },
                    /**
                    * Wrapper for the different range creation methods.
                    * @method createRange
                    * @return {Range}
                    */
                    createRange: function() {
                        if (Y.config.doc.selection) {
                            return Y.config.doc.selection.createRange();
                        } else {
                            return Y.config.doc.createRange();
                        }
                    },
                    /**
                    * Select a Node (hilighting it).
                    * @method selectNode
                    * @param {Node} node The node to select
                    * @param {Boolean} collapse Should the range be collapsed after insertion. default: false
                    * @chainable
                    * @return {EditorSelection}
                    */
                    selectNode: function(node, collapse, end) {
                        if (!node) {
                            Y.log('Node passed to selectNode is null', 'error', 'editor-selection');
                            return;
                        }
                        end = end || 0;
                        node = Y.Node.getDOMNode(node);
                        var range = this.createRange();
                        if (range.selectNode) {
                            try {
                                range.selectNode(node);
                            } catch (err) {
                                // Ignore selection errors like INVALID_NODE_TYPE_ERR
                            }
                            this._selection.removeAllRanges();
                            this._selection.addRange(range);
                            if (collapse) {
                                try {
                                    this._selection.collapse(node, end);
                                } catch (err) {
                                    this._selection.collapse(node, 0);
                                }
                            }
                        } else {
                            if (node.nodeType === 3) {
                                node = node.parentNode;
                            }
                            try {
                                range.moveToElementText(node);
                            } catch(e) {}
                            if (collapse) {
                                range.collapse(((end) ? false : true));
                            }
                            range.select();
                        }
                        return this;
                    },
                    /**
                    * Put a placeholder in the DOM at the current cursor position.
                    * @method setCursor
                    * @return {Node}
                    */
                    setCursor: function() {
                        this.removeCursor(false);
                        return this.insertContent(Y.EditorSelection.CURSOR);
                    },
                    /**
                    * Get the placeholder in the DOM at the current cursor position.
                    * @method getCursor
                    * @return {Node}
                    */
                    getCursor: function() {
                        return Y.EditorSelection.ROOT.all('.' + Y.EditorSelection.CURID).get('parentNode');
                    },
                    /**
                    * Remove the cursor placeholder from the DOM.
                    * @method removeCursor
                    * @param {Boolean} keep Setting this to true will keep the node, but remove the unique parts that make it the cursor.
                    * @return {Node}
                    */
                    removeCursor: function(keep) {
                        var cur = this.getCursor();
                        if (cur && cur.remove) {
                            if (keep) {
                                cur.set('innerHTML', '<br class="yui-cursor">');
                            } else {
                                cur.remove();
                            }
                        }
                        return cur;
                    },
                    /**
                    * Gets a stored cursor and focuses it for editing, must be called sometime after setCursor
                    * @method focusCursor
                    * @return {Node}
                    */
                    focusCursor: function(collapse, end) {
                        if (collapse !== false) {
                            collapse = true;
                        }
                        if (end !== false) {
                            end = true;
                        }
                        var cur = this.removeCursor(true);
                        if (cur) {
                            cur.each(function(c) {
                                this.selectNode(c, collapse, end);
                            }, this);
                        }
                    },
                    /**
                    * Generic toString for logging.
                    * @method toString
                    * @return {String}
                    */
                    toString: function() {
                        return 'EditorSelection Object';
                    },
            
                    /**
                     Gets the offset of the selection for the selection within the current
                     editor
                     @public
                     @method getEditorOffset
                     @param {Y.Node} [node] Element used to measure the offset to
                     @return Number Number of characters the selection is from the beginning
                     @since 3.13.0
                     */
                    getEditorOffset: function(node) {
                        var container = (node || Y.EditorSelection.ROOT).getDOMNode(),
                            caretOffset = 0,
                            doc = Y.config.doc,
                            win = Y.config.win,
                            sel,
                            range,
                            preCaretRange;
            
                        if (typeof win.getSelection !== "undefined") {
                            range = win.getSelection().getRangeAt(0);
                            preCaretRange = range.cloneRange();
                            preCaretRange.selectNodeContents(container);
                            preCaretRange.setEnd(range.endContainer, range.endOffset);
                            caretOffset = preCaretRange.toString().length;
                        } else {
                            sel = doc.selection;
            
                            if ( sel && sel.type !== "Control") {
                                range = sel.createRange();
                                preCaretRange = doc.body.createTextRange();
                                preCaretRange.moveToElementText(container);
                                preCaretRange.setEndPoint("EndToEnd", range);
                                caretOffset = preCaretRange.text.length;
                            }
                        }
            
                        return caretOffset;
                    }
                };
            
                //TODO Remove this alias in 3.6.0
                Y.Selection = Y.EditorSelection;