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