Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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