~smagoun/whoopsie/whoopsie-lp1017637

« back to all changes in this revision

Viewing changes to backend/stats/static/js/yui/build/editor-selection/editor-selection.js

  • Committer: Evan Dandrea
  • Date: 2012-05-09 05:53:45 UTC
  • Revision ID: evan.dandrea@canonical.com-20120509055345-z2j41tmcbf4as5uf
The backend now lives in lp:daisy and the website (errors.ubuntu.com) now lives in lp:errors.

Show diffs side-by-side

added added

removed removed

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