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