weekly release 2.4dev
[moodle.git] / lib / yuilib / 3.7.1 / build / editor-selection / editor-selection-debug.js
blobde60c485b928e958bae2241458e3e54e802c6c11
1 /*
2 YUI 3.7.1 (build 5627)
3 Copyright 2012 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
6 */
7 YUI.add('editor-selection', function (Y, NAME) {
9     /**
10      * Wraps some common Selection/Range functionality into a simple object
11      * @class EditorSelection
12      * @constructor
13      * @module editor
14      * @submodule selection
15      */
16     
17     //TODO This shouldn't be there, Y.Node doesn't normalize getting textnode content.
18     var textContent = 'textContent',
19     INNER_HTML = 'innerHTML',
20     FONT_FAMILY = 'fontFamily';
22     if (Y.UA.ie) {
23         textContent = 'nodeValue';
24     }
26     Y.EditorSelection = function(domEvent) {
27         var sel, par, ieNode, nodes, rng, i;
29         if (Y.config.win.getSelection && (!Y.UA.ie || Y.UA.ie < 9)) {
30                 sel = Y.config.win.getSelection();
31         } else if (Y.config.doc.selection) {
32             sel = Y.config.doc.selection.createRange();
33         }
34         this._selection = sel;
36         if (!sel) {
37             return false;
38         }
40         if (sel.pasteHTML) {
41             this.isCollapsed = (sel.compareEndPoints('StartToEnd', sel)) ? false : true;
42             if (this.isCollapsed) {
43                 this.anchorNode = this.focusNode = Y.one(sel.parentElement());
45                 if (domEvent) {
46                     ieNode = Y.config.doc.elementFromPoint(domEvent.clientX, domEvent.clientY);
47                 }
48                 rng = sel.duplicate();
49                 if (!ieNode) {
50                     par = sel.parentElement();
51                     nodes = par.childNodes;
53                     for (i = 0; i < nodes.length; i++) {
54                         //This causes IE to not allow a selection on a doubleclick
55                         //rng.select(nodes[i]);
56                         if (rng.inRange(sel)) {
57                             if (!ieNode) {
58                                 ieNode = nodes[i];
59                             }
60                         }
61                     }
62                 }
64                 this.ieNode = ieNode;
65                 
66                 if (ieNode) {
67                     if (ieNode.nodeType !== 3) {
68                         if (ieNode.firstChild) {
69                             ieNode = ieNode.firstChild;
70                         }
71                         if (ieNode && ieNode.tagName && ieNode.tagName.toLowerCase() === 'body') {
72                             if (ieNode.firstChild) {
73                                 ieNode = ieNode.firstChild;
74                             }
75                         }
76                     }
77                     this.anchorNode = this.focusNode = Y.EditorSelection.resolve(ieNode);
78                     
79                     rng.moveToElementText(sel.parentElement());
80                     var comp = sel.compareEndPoints('StartToStart', rng),
81                     moved = 0;
82                     if (comp) {
83                         //We are not at the beginning of the selection.
84                         //Setting the move to something large, may need to increase it later
85                         moved = Math.abs(sel.move('character', -9999));
86                     }
87                     
88                     this.anchorOffset = this.focusOffset = moved;
89                     
90                     this.anchorTextNode = this.focusTextNode = Y.one(ieNode);
91                 }
92                 
93                 
94             } else {
95                 //This helps IE deal with a selection and nodeChange events
96                 if (sel.htmlText && sel.htmlText !== '') {
97                     var n = Y.Node.create(sel.htmlText);
98                     if (n && n.get('id')) {
99                         var id = n.get('id');
100                         this.anchorNode = this.focusNode = Y.one('#' + id);
101                     } else if (n) {
102                         n = n.get('childNodes');
103                         this.anchorNode = this.focusNode = n.item(0);
104                     }
105                 }
106             }
108             //var self = this;
109             //debugger;
110         } else {
111             this.isCollapsed = sel.isCollapsed;
112             this.anchorNode = Y.EditorSelection.resolve(sel.anchorNode);
113             this.focusNode = Y.EditorSelection.resolve(sel.focusNode);
114             this.anchorOffset = sel.anchorOffset;
115             this.focusOffset = sel.focusOffset;
116             
117             this.anchorTextNode = Y.one(sel.anchorNode);
118             this.focusTextNode = Y.one(sel.focusNode);
119         }
120         if (Y.Lang.isString(sel.text)) {
121             this.text = sel.text;
122         } else {
123             if (sel.toString) {
124                 this.text = sel.toString();
125             } else {
126                 this.text = '';
127             }
128         }
129     };
130     
131     /**
132     * Utility method to remove dead font-family styles from an element.
133     * @static
134     * @method removeFontFamily
135     */
136     Y.EditorSelection.removeFontFamily = function(n) {
137         n.removeAttribute('face');
138         var s = n.getAttribute('style').toLowerCase();
139         if (s === '' || (s == 'font-family: ')) {
140             n.removeAttribute('style');
141         }
142         if (s.match(Y.EditorSelection.REG_FONTFAMILY)) {
143             s = s.replace(Y.EditorSelection.REG_FONTFAMILY, '');
144             n.setAttribute('style', s);
145         }
146     };
148     /**
149     * Performs a prefilter on all nodes in the editor. Looks for nodes with a style: fontFamily or font face
150     * It then creates a dynamic class assigns it and removed the property. This is so that we don't lose
151     * the fontFamily when selecting nodes.
152     * @static
153     * @method filter
154     */
155     Y.EditorSelection.filter = function(blocks) {
156         var startTime = (new Date()).getTime();
157         Y.log('Filtering nodes', 'info', 'editor-selection');
159         var nodes = Y.all(Y.EditorSelection.ALL),
160             baseNodes = Y.all('strong,em'),
161             doc = Y.config.doc, hrs,
162             classNames = {}, cssString = '',
163             ls;
165         var startTime1 = (new Date()).getTime();
166         nodes.each(function(n) {
167             var raw = Y.Node.getDOMNode(n);
168             if (raw.style[FONT_FAMILY]) {
169                 classNames['.' + n._yuid] = raw.style[FONT_FAMILY];
170                 n.addClass(n._yuid);
172                 Y.EditorSelection.removeFontFamily(raw);
173             }
174             /*
175             if (n.getStyle(FONT_FAMILY)) {
176                 classNames['.' + n._yuid] = n.getStyle(FONT_FAMILY);
177                 n.addClass(n._yuid);
178                 n.removeAttribute('face');
179                 n.setStyle(FONT_FAMILY, '');
180                 if (n.getAttribute('style') === '') {
181                     n.removeAttribute('style');
182                 }
183                 //This is for IE
184                 if (n.getAttribute('style').toLowerCase() === 'font-family: ') {
185                     n.removeAttribute('style');
186                 }
187             }
188             */
189         });
190         var endTime1 = (new Date()).getTime();
191         Y.log('Node Filter Timer: ' + (endTime1 - startTime1) + 'ms', 'info', 'editor-selection');
193         Y.all('.hr').addClass('yui-skip').addClass('yui-non');
194         
195         if (Y.UA.ie) {
196             hrs = doc.getElementsByTagName('hr');
197             Y.each(hrs, function(hr) {
198                 var el = doc.createElement('div');
199                     el.className = 'hr yui-non yui-skip';
200                     
201                     el.setAttribute('readonly', true);
202                     el.setAttribute('contenteditable', false); //Keep it from being Edited
203                     if (hr.parentNode) {
204                         hr.parentNode.replaceChild(el, hr);
205                     }
206                     //Had to move to inline style. writes for ie's < 8. They don't render el.setAttribute('style');
207                     var s = el.style;
208                     s.border = '1px solid #ccc';
209                     s.lineHeight = '0';
210                     s.height = '0';
211                     s.fontSize = '0';
212                     s.marginTop = '5px';
213                     s.marginBottom = '5px';
214                     s.marginLeft = '0px';
215                     s.marginRight = '0px';
216                     s.padding = '0';
217             });
218         }
219         
221         Y.each(classNames, function(v, k) {
222             cssString += k + ' { font-family: ' + v.replace(/"/gi, '') + '; }';
223         });
224         Y.StyleSheet(cssString, 'editor');
226         
227         //Not sure about this one?
228         baseNodes.each(function(n, k) {
229             var t = n.get('tagName').toLowerCase(),
230                 newTag = 'i';
231             if (t === 'strong') {
232                 newTag = 'b';
233             }
234             Y.EditorSelection.prototype._swap(baseNodes.item(k), newTag);
235         });
237         //Filter out all the empty UL/OL's
238         ls = Y.all('ol,ul');
239         ls.each(function(v, k) {
240             var lis = v.all('li');
241             if (!lis.size()) {
242                 v.remove();
243             }
244         });
245         
246         if (blocks) {
247             Y.EditorSelection.filterBlocks();
248         }
249         var endTime = (new Date()).getTime();
250         Y.log('Filter Timer: ' + (endTime - startTime) + 'ms', 'info', 'editor-selection');
251     };
253     /**
254     * Method attempts to replace all "orphined" text nodes in the main body by wrapping them with a <p>. Called from filter.
255     * @static
256     * @method filterBlocks
257     */
258     Y.EditorSelection.filterBlocks = function() {
259         var startTime = (new Date()).getTime();
260         Y.log('RAW filter blocks', 'info', 'editor-selection');
261         var childs = Y.config.doc.body.childNodes, i, node, wrapped = false, doit = true,
262             sel, single, br, divs, spans, c, s;
264         if (childs) {
265             for (i = 0; i < childs.length; i++) {
266                 node = Y.one(childs[i]);
267                 if (!node.test(Y.EditorSelection.BLOCKS)) {
268                     doit = true;
269                     if (childs[i].nodeType == 3) {
270                         c = childs[i][textContent].match(Y.EditorSelection.REG_CHAR);
271                         s = childs[i][textContent].match(Y.EditorSelection.REG_NON);
272                         if (c === null && s) {
273                             doit = false;
274                             
275                         }
276                     }
277                     if (doit) {
278                         if (!wrapped) {
279                             wrapped = [];
280                         }
281                         wrapped.push(childs[i]);
282                     }
283                 } else {
284                     wrapped = Y.EditorSelection._wrapBlock(wrapped);
285                 }
286             }
287             wrapped = Y.EditorSelection._wrapBlock(wrapped);
288         }
290         single = Y.all(Y.EditorSelection.DEFAULT_BLOCK_TAG);
291         if (single.size() === 1) {
292             Y.log('Only One default block tag (' + Y.EditorSelection.DEFAULT_BLOCK_TAG + '), focus it..', 'info', 'editor-selection');
293             br = single.item(0).all('br');
294             if (br.size() === 1) {
295                 if (!br.item(0).test('.yui-cursor')) {
296                     br.item(0).remove();
297                 }
298                 var html = single.item(0).get('innerHTML');
299                 if (html === '' || html === ' ') {
300                     Y.log('Paragraph empty, focusing cursor', 'info', 'editor-selection');
301                     single.set('innerHTML', Y.EditorSelection.CURSOR);
302                     sel = new Y.EditorSelection();
303                     sel.focusCursor(true, true);
304                 }
305                 if (br.item(0).test('.yui-cursor') && Y.UA.ie) {
306                     br.item(0).remove();
307                 }
308             }
309         } else {
310             single.each(function(p) {
311                 var html = p.get('innerHTML');
312                 if (html === '') {
313                     Y.log('Empty Paragraph Tag Found, Removing It', 'info', 'editor-selection');
314                     p.remove();
315                 }
316             });
317         }
318         
319         if (!Y.UA.ie) {
320             /*
321             divs = Y.all('div, p');
322             divs.each(function(d) {
323                 if (d.hasClass('yui-non')) {
324                     return;
325                 }
326                 var html = d.get('innerHTML');
327                 if (html === '') {
328                     Y.log('Empty DIV/P Tag Found, Removing It', 'info', 'editor-selection');
329                     d.remove();
330                 } else {
331                     Y.log('DIVS/PS Count: ' + d.get('childNodes').size(), 'info', 'editor-selection');
332                     if (d.get('childNodes').size() == 1) {
333                         Y.log('This Div/P only has one Child Node', 'info', 'editor-selection');
334                         if (d.ancestor('p')) {
335                             Y.log('This Div/P is a child of a paragraph, remove it..', 'info', 'editor-selection');
336                             d.replace(d.get('firstChild'));
337                         }
338                     }
339                 }
340             });*/
342             /* Removed this, as it was causing Pasting to be funky in Safari
343             spans = Y.all('.Apple-style-span, .apple-style-span');
344             Y.log('Apple Spans found: ' + spans.size(), 'info', 'editor-selection');
345             spans.each(function(s) {
346                 s.setAttribute('style', '');
347             });
348             */
349         }
352         var endTime = (new Date()).getTime();
353         Y.log('FilterBlocks Timer: ' + (endTime - startTime) + 'ms', 'info', 'editor-selection');
354     };
356     /**
357     * Regular Expression used to find dead font-family styles
358     * @static
359     * @property REG_FONTFAMILY
360     */   
361     Y.EditorSelection.REG_FONTFAMILY = /font-family: ;/;
363     /**
364     * Regular Expression to determine if a string has a character in it
365     * @static
366     * @property REG_CHAR
367     */   
368     Y.EditorSelection.REG_CHAR = /[a-zA-Z-0-9_!@#\$%\^&*\(\)-=_+\[\]\\{}|;':",.\/<>\?]/gi;
370     /**
371     * Regular Expression to determine if a string has a non-character in it
372     * @static
373     * @property REG_NON
374     */
375     Y.EditorSelection.REG_NON = /[\s|\n|\t]/gi;
377     /**
378     * Regular Expression to remove all HTML from a string
379     * @static
380     * @property REG_NOHTML
381     */
382     Y.EditorSelection.REG_NOHTML = /<\S[^><]*>/g;
385     /**
386     * Wraps an array of elements in a Block level tag
387     * @static
388     * @private
389     * @method _wrapBlock
390     */
391     Y.EditorSelection._wrapBlock = function(wrapped) {
392         if (wrapped) {
393             var newChild = Y.Node.create('<' + Y.EditorSelection.DEFAULT_BLOCK_TAG + '></' + Y.EditorSelection.DEFAULT_BLOCK_TAG + '>'),
394                 firstChild = Y.one(wrapped[0]), i;
396             for (i = 1; i < wrapped.length; i++) {
397                 newChild.append(wrapped[i]);
398             }
399             firstChild.replace(newChild);
400             newChild.prepend(firstChild);
401         }
402         return false;
403     };
405     /**
406     * Undoes what filter does enough to return the HTML from the Editor, then re-applies the filter.
407     * @static
408     * @method unfilter
409     * @return {String} The filtered HTML
410     */
411     Y.EditorSelection.unfilter = function() {
412         var nodes = Y.all('body [class]'),
413             html = '', nons, ids,
414             body = Y.one('body');
415         
416         Y.log('UnFiltering nodes', 'info', 'editor-selection');
417         
418         nodes.each(function(n) {
419             if (n.hasClass(n._yuid)) {
420                 //One of ours
421                 n.setStyle(FONT_FAMILY, n.getStyle(FONT_FAMILY));
422                 n.removeClass(n._yuid);
423                 if (n.getAttribute('class') === '') {
424                     n.removeAttribute('class');
425                 }
426             }
427         });
429         nons = Y.all('.yui-non');
430         nons.each(function(n) {
431             if (!n.hasClass('yui-skip') && n.get('innerHTML') === '') {
432                 n.remove();
433             } else {
434                 n.removeClass('yui-non').removeClass('yui-skip');
435             }
436         });
438         ids = Y.all('body [id]');
439         ids.each(function(n) {
440             if (n.get('id').indexOf('yui_3_') === 0) {
441                 n.removeAttribute('id');
442                 n.removeAttribute('_yuid');
443             }
444         });
445         
446         if (body) {
447             html = body.get('innerHTML');
448         }
449         
450         Y.all('.hr').addClass('yui-skip').addClass('yui-non');
451         
452         /*
453         nodes.each(function(n) {
454             n.addClass(n._yuid);
455             n.setStyle(FONT_FAMILY, '');
456             if (n.getAttribute('style') === '') {
457                 n.removeAttribute('style');
458             }
459         });
460         */
461         
462         return html;
463     };
464     /**
465     * Resolve a node from the selection object and return a Node instance
466     * @static
467     * @method resolve
468     * @param {HTMLElement} n The HTMLElement to resolve. Might be a TextNode, gives parentNode.
469     * @return {Node} The Resolved node
470     */
471     Y.EditorSelection.resolve = function(n) {
472         if (n && n.nodeType === 3) {
473             //Adding a try/catch here because in rare occasions IE will
474             //Throw a error accessing the parentNode of a stranded text node.
475             //In the case of Ctrl+Z (Undo)
476             try {
477                 n = n.parentNode;
478             } catch (re) {
479                 n = 'body';
480             }
481         }
482         return Y.one(n);
483     };
485     /**
486     * Returns the innerHTML of a node with all HTML tags removed.
487     * @static
488     * @method getText
489     * @param {Node} node The Node instance to remove the HTML from
490     * @return {String} The string of text
491     */
492     Y.EditorSelection.getText = function(node) {
493         var txt = node.get('innerHTML').replace(Y.EditorSelection.REG_NOHTML, '');
494         //Clean out the cursor subs to see if the Node is empty
495         txt = txt.replace('<span><br></span>', '').replace('<br>', '');
496         return txt;
497     };
499     //Y.EditorSelection.DEFAULT_BLOCK_TAG = 'div';
500     Y.EditorSelection.DEFAULT_BLOCK_TAG = 'p';
502     /**
503     * The selector to use when looking for Nodes to cache the value of: [style],font[face]
504     * @static
505     * @property ALL
506     */
507     Y.EditorSelection.ALL = '[style],font[face]';
509     /**
510     * The selector to use when looking for block level items.
511     * @static
512     * @property BLOCKS
513     */
514     Y.EditorSelection.BLOCKS = 'p,div,ul,ol,table,style';
515     /**
516     * The temporary fontname applied to a selection to retrieve their values: yui-tmp
517     * @static
518     * @property TMP
519     */
520     Y.EditorSelection.TMP = 'yui-tmp';
521     /**
522     * The default tag to use when creating elements: span
523     * @static
524     * @property DEFAULT_TAG
525     */
526     Y.EditorSelection.DEFAULT_TAG = 'span';
528     /**
529     * The id of the outer cursor wrapper
530     * @static
531     * @property DEFAULT_TAG
532     */
533     Y.EditorSelection.CURID = 'yui-cursor';
535     /**
536     * The id used to wrap the inner space of the cursor position
537     * @static
538     * @property CUR_WRAPID
539     */
540     Y.EditorSelection.CUR_WRAPID = 'yui-cursor-wrapper';
542     /**
543     * The default HTML used to focus the cursor..
544     * @static
545     * @property CURSOR
546     */
547     Y.EditorSelection.CURSOR = '<span><br class="yui-cursor"></span>';
549     Y.EditorSelection.hasCursor = function() {
550         var cur = Y.all('#' + Y.EditorSelection.CUR_WRAPID);
551         Y.log('Has Cursor: ' + cur.size(), 'info', 'editor-selection');
552         return cur.size();
553     };
555     /**
556     * Called from Editor keydown to remove the "extra" space before the cursor.
557     * @static
558     * @method cleanCursor
559     */
560     Y.EditorSelection.cleanCursor = function() {
561         //Y.log('Cleaning Cursor', 'info', 'Selection');
562         var cur, sel = 'br.yui-cursor';
563         cur = Y.all(sel);
564         if (cur.size()) {
565             cur.each(function(b) {
566                 var c = b.get('parentNode.parentNode.childNodes'), html;
567                 if (c.size()) {
568                     b.remove();
569                 } else {
570                     html = Y.EditorSelection.getText(c.item(0));
571                     if (html !== '') {
572                         b.remove();
573                     }
574                 }
575             });
576         }
577         /*
578         var cur = Y.all('#' + Y.EditorSelection.CUR_WRAPID);
579         if (cur.size()) {
580             cur.each(function(c) {
581                 var html = c.get('innerHTML');
582                 if (html == '&nbsp;' || html == '<br>') {
583                     if (c.previous() || c.next()) {
584                         c.remove();
585                     }
586                 }
587             });
588         }
589         */
590     };
592     Y.EditorSelection.prototype = {
593         /**
594         * Range text value
595         * @property text
596         * @type String
597         */
598         text: null,
599         /**
600         * Flag to show if the range is collapsed or not
601         * @property isCollapsed
602         * @type Boolean
603         */
604         isCollapsed: null,
605         /**
606         * A Node instance of the parentNode of the anchorNode of the range
607         * @property anchorNode
608         * @type Node
609         */
610         anchorNode: null,
611         /**
612         * The offset from the range object
613         * @property anchorOffset
614         * @type Number
615         */
616         anchorOffset: null,
617         /**
618         * A Node instance of the actual textNode of the range.
619         * @property anchorTextNode
620         * @type Node
621         */
622         anchorTextNode: null,
623         /**
624         * A Node instance of the parentNode of the focusNode of the range
625         * @property focusNode
626         * @type Node
627         */
628         focusNode: null,
629         /**
630         * The offset from the range object
631         * @property focusOffset
632         * @type Number
633         */
634         focusOffset: null,
635         /**
636         * A Node instance of the actual textNode of the range.
637         * @property focusTextNode
638         * @type Node
639         */
640         focusTextNode: null,
641         /**
642         * The actual Selection/Range object
643         * @property _selection
644         * @private
645         */
646         _selection: null,
647         /**
648         * Wrap an element, with another element 
649         * @private
650         * @method _wrap
651         * @param {HTMLElement} n The node to wrap 
652         * @param {String} tag The tag to use when creating the new element.
653         * @return {HTMLElement} The wrapped node
654         */
655         _wrap: function(n, tag) {
656             var tmp = Y.Node.create('<' + tag + '></' + tag + '>');
657             tmp.set(INNER_HTML, n.get(INNER_HTML));
658             n.set(INNER_HTML, '');
659             n.append(tmp);
660             return Y.Node.getDOMNode(tmp);
661         },
662         /**
663         * Swap an element, with another element 
664         * @private
665         * @method _swap
666         * @param {HTMLElement} n The node to swap 
667         * @param {String} tag The tag to use when creating the new element.
668         * @return {HTMLElement} The new node
669         */
670         _swap: function(n, tag) {
671             var tmp = Y.Node.create('<' + tag + '></' + tag + '>');
672             tmp.set(INNER_HTML, n.get(INNER_HTML));
673             n.replace(tmp, n);
674             return Y.Node.getDOMNode(tmp);
675         },
676         /**
677         * Get all the nodes in the current selection. This method will actually perform a filter first.
678         * Then it calls doc.execCommand('fontname', null, 'yui-tmp') to touch all nodes in the selection.
679         * The it compiles a list of all nodes affected by the execCommand and builds a NodeList to return.
680         * @method getSelected
681         * @return {NodeList} A NodeList of all items in the selection.
682         */
683         getSelected: function() {
684             Y.EditorSelection.filter();
685             Y.config.doc.execCommand('fontname', null, Y.EditorSelection.TMP);
686             var nodes = Y.all(Y.EditorSelection.ALL),
687                 items = [];
688             
689             nodes.each(function(n, k) {
690                 if (n.getStyle(FONT_FAMILY) ==  Y.EditorSelection.TMP) {
691                     n.setStyle(FONT_FAMILY, '');
692                     Y.EditorSelection.removeFontFamily(n);
693                     if (!n.test('body')) {
694                         items.push(Y.Node.getDOMNode(nodes.item(k)));
695                     }
696                 }
697             });
698             return Y.all(items);
699         },
700         /**
701         * Insert HTML at the current cursor position and return a Node instance of the newly inserted element.
702         * @method insertContent
703         * @param {String} html The HTML to insert.
704         * @return {Node} The inserted Node.
705         */
706         insertContent: function(html) {
707             return this.insertAtCursor(html, this.anchorTextNode, this.anchorOffset, true);
708         },
709         /**
710         * 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.
711         * @method insertAtCursor
712         * @param {String} html The HTML to insert.
713         * @param {Node} node The text node to break when inserting.
714         * @param {Number} offset The left offset of the text node to break and insert the new content.
715         * @param {Boolean} collapse Should the range be collapsed after insertion. default: false
716         * @return {Node} The inserted Node.
717         */
718         insertAtCursor: function(html, node, offset, collapse) {
719             var cur = Y.Node.create('<' + Y.EditorSelection.DEFAULT_TAG + ' class="yui-non"></' + Y.EditorSelection.DEFAULT_TAG + '>'),
720                 inHTML, txt, txt2, newNode, range = this.createRange(), b;
722             if (node && node.test('body')) {
723                 b = Y.Node.create('<span></span>');
724                 node.append(b);
725                 node = b;
726             }
728             
729             if (range.pasteHTML) {
730                 if (offset === 0 && node && !node.previous() && node.get('nodeType') === 3) {
731                     /*
732                     * For some strange reason, range.pasteHTML fails if the node is a textNode and
733                     * the offset is 0. (The cursor is at the beginning of the line)
734                     * It will always insert the new content at position 1 instead of 
735                     * position 0. Here we test for that case and do it the hard way.
736                     */
737                     node.insert(html, 'before');
738                     if (range.moveToElementText) {
739                         range.moveToElementText(Y.Node.getDOMNode(node.previous()));
740                     }
741                     //Move the cursor after the new node
742                     range.collapse(false);
743                     range.select();
744                     return node.previous();
745                 } else {
746                     newNode = Y.Node.create(html);
747                     try {
748                         range.pasteHTML('<span id="rte-insert"></span>');
749                     } catch (e) {}
750                     inHTML = Y.one('#rte-insert');
751                     if (inHTML) {
752                         inHTML.set('id', '');
753                         inHTML.replace(newNode);
754                         if (range.moveToElementText) {
755                             range.moveToElementText(Y.Node.getDOMNode(newNode));
756                         }
757                         range.collapse(false);
758                         range.select();
759                         return newNode;
760                     } else {
761                         Y.on('available', function() {
762                             inHTML.set('id', '');
763                             inHTML.replace(newNode);
764                             if (range.moveToElementText) {
765                                 range.moveToElementText(Y.Node.getDOMNode(newNode));
766                             }
767                             range.collapse(false);
768                             range.select();
769                         }, '#rte-insert');
770                     }
771                 }
772             } else {
773                 //TODO using Y.Node.create here throws warnings & strips first white space character
774                 //txt = Y.one(Y.Node.create(inHTML.substr(0, offset)));
775                 //txt2 = Y.one(Y.Node.create(inHTML.substr(offset)));
776                 if (offset > 0) {
777                     inHTML = node.get(textContent);
779                     txt = Y.one(Y.config.doc.createTextNode(inHTML.substr(0, offset)));
780                     txt2 = Y.one(Y.config.doc.createTextNode(inHTML.substr(offset)));
782                     node.replace(txt, node);
783                     newNode = Y.Node.create(html);
784                     if (newNode.get('nodeType') === 11) {
785                         b = Y.Node.create('<span></span>');
786                         b.append(newNode);
787                         newNode = b;
788                     }
789                     txt.insert(newNode, 'after');
790                     //if (txt2 && txt2.get('length')) {
791                     if (txt2) {
792                         newNode.insert(cur, 'after');
793                         cur.insert(txt2, 'after');
794                         this.selectNode(cur, collapse);
795                     }
796                 } else {
797                     if (node.get('nodeType') === 3) {
798                         node = node.get('parentNode');
799                     }
800                     newNode = Y.Node.create(html);
801                     html = node.get('innerHTML').replace(/\n/gi, '');
802                     if (html === '' || html === '<br>') {
803                         node.append(newNode);
804                     } else {
805                         if (newNode.get('parentNode')) {
806                             node.insert(newNode, 'before');
807                         } else {
808                             Y.one('body').prepend(newNode);
809                         }
810                     }
811                     if (node.get('firstChild').test('br')) {
812                         node.get('firstChild').remove();
813                     }
814                 }
815             }
816             return newNode;
817         },
818         /**
819         * Get all elements inside a selection and wrap them with a new element and return a NodeList of all elements touched.
820         * @method wrapContent
821         * @param {String} tag The tag to wrap all selected items with.
822         * @return {NodeList} A NodeList of all items in the selection.
823         */
824         wrapContent: function(tag) {
825             tag = (tag) ? tag : Y.EditorSelection.DEFAULT_TAG;
827             if (!this.isCollapsed) {
828                 Y.log('Wrapping selection with: ' + tag, 'info', 'editor-selection');
829                 var items = this.getSelected(),
830                     changed = [], range, last, first, range2;
832                 items.each(function(n, k) {
833                     var t = n.get('tagName').toLowerCase();
834                     if (t === 'font') {
835                         changed.push(this._swap(items.item(k), tag));
836                     } else {
837                         changed.push(this._wrap(items.item(k), tag));
838                     }
839                 }, this);
840                 
841                         range = this.createRange();
842                 first = changed[0];
843                 last = changed[changed.length - 1];
844                 if (this._selection.removeAllRanges) {
845                     range.setStart(changed[0], 0);
846                     range.setEnd(last, last.childNodes.length);
847                     this._selection.removeAllRanges();
848                     this._selection.addRange(range);
849                 } else {
850                     if (range.moveToElementText) {
851                         range.moveToElementText(Y.Node.getDOMNode(first));
852                         range2 = this.createRange();
853                         range2.moveToElementText(Y.Node.getDOMNode(last));
854                         range.setEndPoint('EndToEnd', range2);
855                     }
856                     range.select();
857                 }
859                 changed = Y.all(changed);
860                 Y.log('Returning NodeList with (' + changed.size() + ') item(s)' , 'info', 'editor-selection');
861                 return changed;
864             } else {
865                 Y.log('Can not wrap a collapsed selection, use insertContent', 'error', 'editor-selection');
866                 return Y.all([]);
867             }
868         },
869         /**
870         * Find and replace a string inside a text node and replace it with HTML focusing the node after 
871         * to allow you to continue to type.
872         * @method replace
873         * @param {String} se The string to search for.
874         * @param {String} re The string of HTML to replace it with.
875         * @return {Node} The node inserted.
876         */
877         replace: function(se,re) {
878             Y.log('replacing (' + se + ') with (' + re + ')');
879             var range = this.createRange(), node, txt, index, newNode;
881             if (range.getBookmark) {
882                 index = range.getBookmark();
883                 txt = this.anchorNode.get('innerHTML').replace(se, re);
884                 this.anchorNode.set('innerHTML', txt);
885                 range.moveToBookmark(index);
886                 newNode = Y.one(range.parentElement());
887             } else {
888                 node = this.anchorTextNode;
889                 txt = node.get(textContent);
890                 index = txt.indexOf(se);
892                 txt = txt.replace(se, '');
893                 node.set(textContent, txt);
894                 newNode = this.insertAtCursor(re, node, index, true);
895             }
896             return newNode;
897         },
898         /**
899         * Destroy the range.
900         * @method remove
901         * @chainable
902         * @return {EditorSelection}
903         */
904         remove: function() {
905             if (this._selection && this._selection.removeAllRanges) {
906                 this._selection.removeAllRanges();
907             }
908             return this;
909         },
910         /**
911         * Wrapper for the different range creation methods.
912         * @method createRange
913         * @return {RangeObject}
914         */
915         createRange: function() {
916             if (Y.config.doc.selection) {
917                 return Y.config.doc.selection.createRange();
918             } else {
919                         return Y.config.doc.createRange();
920             }
921         },
922         /**
923         * Select a Node (hilighting it).
924         * @method selectNode
925         * @param {Node} node The node to select
926         * @param {Boolean} collapse Should the range be collapsed after insertion. default: false
927         * @chainable
928         * @return {EditorSelection}
929         */
930         selectNode: function(node, collapse, end) {
931             if (!node) {
932                 Y.log('Node passed to selectNode is null', 'error', 'editor-selection');
933                 return;
934             }
935             end = end || 0;
936             node = Y.Node.getDOMNode(node);
937                     var range = this.createRange();
938             if (range.selectNode) {
939                 range.selectNode(node);
940                 this._selection.removeAllRanges();
941                 this._selection.addRange(range);
942                 if (collapse) {
943                     try {
944                         this._selection.collapse(node, end);
945                     } catch (err) {
946                         this._selection.collapse(node, 0);
947                     }
948                 }
949             } else {
950                 if (node.nodeType === 3) {
951                     node = node.parentNode;
952                 }
953                 try {
954                     range.moveToElementText(node);
955                 } catch(e) {}
956                 if (collapse) {
957                     range.collapse(((end) ? false : true));
958                 }
959                 range.select();
960             }
961             return this;
962         },
963         /**
964         * Put a placeholder in the DOM at the current cursor position.
965         * @method setCursor
966         * @return {Node}
967         */
968         setCursor: function() {
969             this.removeCursor(false);
970             return this.insertContent(Y.EditorSelection.CURSOR);
971         },
972         /**
973         * Get the placeholder in the DOM at the current cursor position.
974         * @method getCursor
975         * @return {Node}
976         */
977         getCursor: function() {
978             return Y.all('#' + Y.EditorSelection.CURID);
979         },
980         /**
981         * Remove the cursor placeholder from the DOM.
982         * @method removeCursor
983         * @param {Boolean} keep Setting this to true will keep the node, but remove the unique parts that make it the cursor.
984         * @return {Node}
985         */
986         removeCursor: function(keep) {
987             var cur = this.getCursor();
988             if (cur) {
989                 if (keep) {
990                     cur.removeAttribute('id');
991                     cur.set('innerHTML', '<br class="yui-cursor">');
992                 } else {
993                     cur.remove();
994                 }
995             }
996             return cur;
997         },
998         /**
999         * Gets a stored cursor and focuses it for editing, must be called sometime after setCursor
1000         * @method focusCursor
1001         * @return {Node}
1002         */
1003         focusCursor: function(collapse, end) {
1004             if (collapse !== false) {
1005                 collapse = true;
1006             }
1007             if (end !== false) {
1008                 end = true;
1009             }
1010             var cur = this.removeCursor(true);
1011             if (cur) {
1012                 cur.each(function(c) {
1013                     this.selectNode(c, collapse, end);
1014                 }, this);
1015             }
1016         },
1017         /**
1018         * Generic toString for logging.
1019         * @method toString
1020         * @return {String}
1021         */
1022         toString: function() {
1023             return 'EditorSelection Object';
1024         }
1025     };
1027     //TODO Remove this alias in 3.6.0
1028     Y.Selection = Y.EditorSelection;
1032 }, '3.7.1', {"requires": ["node"]});