NOBUG: Fixed file access permissions
[moodle.git] / lib / yuilib / 3.13.0 / editor-selection / editor-selection-debug.js
blob53747f2ae3de369aef07b074b2b52b703b47c90b
1 /*
2 YUI 3.13.0 (build 508226d)
3 Copyright 2013 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
6 */
8 YUI.add('editor-selection', function (Y, NAME) {
10     /**
11      * Wraps some common Selection/Range functionality into a simple object
12      * @class EditorSelection
13      * @constructor
14      * @module editor
15      * @submodule selection
16      */
18     //TODO This shouldn't be there, Y.Node doesn't normalize getting textnode content.
19     var textContent = 'textContent',
20     INNER_HTML = 'innerHTML',
21     FONT_FAMILY = 'fontFamily';
23     if (Y.UA.ie) {
24         textContent = 'nodeValue';
25     }
27     Y.EditorSelection = function(domEvent) {
28         var sel, par, ieNode, nodes, rng, i,
29             comp, moved = 0, n, id, root = Y.EditorSelection.ROOT;
32         if (Y.config.win.getSelection && (!Y.UA.ie || Y.UA.ie < 9)) {
33             sel = Y.config.win.getSelection();
34         } else if (Y.config.doc.selection) {
35             sel = Y.config.doc.selection.createRange();
36         }
37         this._selection = sel;
39         if (!sel) {
40             return false;
41         }
43         if (sel.pasteHTML) {
44             this.isCollapsed = (sel.compareEndPoints('StartToEnd', sel)) ? false : true;
45             if (this.isCollapsed) {
46                 this.anchorNode = this.focusNode = Y.one(sel.parentElement());
48                 if (domEvent) {
49                     ieNode = Y.config.doc.elementFromPoint(domEvent.clientX, domEvent.clientY);
50                 }
51                 rng = sel.duplicate();
52                 if (!ieNode) {
53                     par = sel.parentElement();
54                     nodes = par.childNodes;
56                     for (i = 0; i < nodes.length; i++) {
57                         //This causes IE to not allow a selection on a doubleclick
58                         //rng.select(nodes[i]);
59                         if (rng.inRange(sel)) {
60                             if (!ieNode) {
61                                 ieNode = nodes[i];
62                             }
63                         }
64                     }
65                 }
67                 this.ieNode = ieNode;
69                 if (ieNode) {
70                     if (ieNode.nodeType !== 3) {
71                         if (ieNode.firstChild) {
72                             ieNode = ieNode.firstChild;
73                         }
74                         if (root.compareTo(ieNode)) {
75                             if (ieNode.firstChild) {
76                                 ieNode = ieNode.firstChild;
77                             }
78                         }
79                     }
80                     this.anchorNode = this.focusNode = Y.EditorSelection.resolve(ieNode);
82                     rng.moveToElementText(sel.parentElement());
83                     comp = sel.compareEndPoints('StartToStart', rng);
84                     if (comp) {
85                         //We are not at the beginning of the selection.
86                         //Setting the move to something large, may need to increase it later
87                         moved = this.getEditorOffset(root);
88                         sel.move('character', -(moved));
89                     }
91                     this.anchorOffset = this.focusOffset = moved;
93                     this.anchorTextNode = this.focusTextNode = Y.one(ieNode);
94                 }
97             } else {
98                 //This helps IE deal with a selection and nodeChange events
99                 if (sel.htmlText && sel.htmlText !== '') {
100                     n = Y.Node.create(sel.htmlText);
101                     if (n && n.get('id')) {
102                         id = n.get('id');
103                         this.anchorNode = this.focusNode = Y.one('#' + id);
104                     } else if (n) {
105                         n = n.get('childNodes');
106                         this.anchorNode = this.focusNode = n.item(0);
107                     }
108                 }
109             }
111             //var self = this;
112             //debugger;
113         } else {
114             this.isCollapsed = sel.isCollapsed;
115             this.anchorNode = Y.EditorSelection.resolve(sel.anchorNode);
116             this.focusNode = Y.EditorSelection.resolve(sel.focusNode);
117             this.anchorOffset = sel.anchorOffset;
118             this.focusOffset = sel.focusOffset;
120             this.anchorTextNode = Y.one(sel.anchorNode || this.anchorNode);
121             this.focusTextNode = Y.one(sel.focusNode || this.focusNode);
122         }
123         if (Y.Lang.isString(sel.text)) {
124             this.text = sel.text;
125         } else {
126             if (sel.toString) {
127                 this.text = sel.toString();
128             } else {
129                 this.text = '';
130             }
131         }
132     };
134     /**
135     * Utility method to remove dead font-family styles from an element.
136     * @static
137     * @method removeFontFamily
138     */
139     Y.EditorSelection.removeFontFamily = function(n) {
140         n.removeAttribute('face');
141         var s = n.getAttribute('style').toLowerCase();
142         if (s === '' || (s === 'font-family: ')) {
143             n.removeAttribute('style');
144         }
145         if (s.match(Y.EditorSelection.REG_FONTFAMILY)) {
146             s = s.replace(Y.EditorSelection.REG_FONTFAMILY, '');
147             n.setAttribute('style', s);
148         }
149     };
151     /**
152     * Performs a prefilter on all nodes in the editor. Looks for nodes with a style: fontFamily or font face
153     * It then creates a dynamic class assigns it and removed the property. This is so that we don't lose
154     * the fontFamily when selecting nodes.
155     * @static
156     * @method filter
157     */
158     Y.EditorSelection.filter = function(blocks) {
159         Y.log('Filtering nodes', 'info', 'editor-selection');
161         var startTime = (new Date()).getTime(),
162             editorSelection = Y.EditorSelection,
163             root = editorSelection.ROOT,
164             endTime,
165             nodes = root.all(editorSelection.ALL),
166             baseNodes = root.all('strong,em'),
167             doc = Y.config.doc, hrs,
168             classNames = {}, cssString = '',
169             ls, startTime1 = (new Date()).getTime(),
170             endTime1;
172         nodes.each(function(n) {
173             var raw = Y.Node.getDOMNode(n);
174             if (raw.style[FONT_FAMILY]) {
175                 classNames['.' + n._yuid] = raw.style[FONT_FAMILY];
176                 n.addClass(n._yuid);
178                 editorSelection.removeFontFamily(raw);
179             }
180         });
181         endTime1 = (new Date()).getTime();
182         Y.log('Node Filter Timer: ' + (endTime1 - startTime1) + 'ms', 'info', 'editor-selection');
184         root.all('.hr').addClass('yui-skip').addClass('yui-non');
186         if (Y.UA.ie) {
187             hrs = Y.Node.getDOMNode(root).getElementsByTagName('hr');
188             Y.each(hrs, function(hr) {
189                 var el = doc.createElement('div'),
190                 s = el.style;
192                 el.className = 'hr yui-non yui-skip';
194                 el.setAttribute('readonly', true);
195                 el.setAttribute('contenteditable', false); //Keep it from being Edited
196                 if (hr.parentNode) {
197                     hr.parentNode.replaceChild(el, hr);
198                 }
199                 //Had to move to inline style. writes for ie's < 8. They don't render el.setAttribute('style');
200                 s.border = '1px solid #ccc';
201                 s.lineHeight = '0';
202                 s.height = '0';
203                 s.fontSize = '0';
204                 s.marginTop = '5px';
205                 s.marginBottom = '5px';
206                 s.marginLeft = '0px';
207                 s.marginRight = '0px';
208                 s.padding = '0';
209             });
210         }
213         Y.each(classNames, function(v, k) {
214             cssString += k + ' { font-family: ' + v.replace(/"/gi, '') + '; }';
215         });
216         Y.StyleSheet(cssString, 'editor');
219         //Not sure about this one?
220         baseNodes.each(function(n, k) {
221             var t = n.get('tagName').toLowerCase(),
222                 newTag = 'i';
223             if (t === 'strong') {
224                 newTag = 'b';
225             }
226             editorSelection.prototype._swap(baseNodes.item(k), newTag);
227         });
229         //Filter out all the empty UL/OL's
230         ls = root.all('ol,ul');
231         ls.each(function(v) {
232             var lis = v.all('li');
233             if (!lis.size()) {
234                 v.remove();
235             }
236         });
238         if (blocks) {
239             editorSelection.filterBlocks();
240         }
241         endTime = (new Date()).getTime();
242         Y.log('Filter Timer: ' + (endTime - startTime) + 'ms', 'info', 'editor-selection');
243     };
245     /**
246     * Method attempts to replace all "orphined" text nodes in the main body by wrapping them with a <p>. Called from filter.
247     * @static
248     * @method filterBlocks
249     */
250     Y.EditorSelection.filterBlocks = function() {
251         Y.log('RAW filter blocks', 'info', 'editor-selection');
252         var startTime = (new Date()).getTime(), endTime,
253             childs = Y.Node.getDOMNode(Y.EditorSelection.ROOT).childNodes, i, node, wrapped = false, doit = true,
254             sel, single, br, c, s, html;
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;
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         }
282         single = Y.all(Y.EditorSelection.DEFAULT_BLOCK_TAG);
283         if (single.size() === 1) {
284             Y.log('Only One default block tag (' + Y.EditorSelection.DEFAULT_BLOCK_TAG + '), focus it..', 'info', 'editor-selection');
285             br = single.item(0).all('br');
286             if (br.size() === 1) {
287                 if (!br.item(0).test('.yui-cursor')) {
288                     br.item(0).remove();
289                 }
290                 html = single.item(0).get('innerHTML');
291                 if (html === '' || html === ' ') {
292                     Y.log('Paragraph empty, focusing cursor', 'info', 'editor-selection');
293                     single.set('innerHTML', Y.EditorSelection.CURSOR);
294                     sel = new Y.EditorSelection();
295                     sel.focusCursor(true, true);
296                 }
297                 if (br.item(0).test('.yui-cursor') && Y.UA.ie) {
298                     br.item(0).remove();
299                 }
300             }
301         } else {
302             single.each(function(p) {
303                 var html = p.get('innerHTML');
304                 if (html === '') {
305                     Y.log('Empty Paragraph Tag Found, Removing It', 'info', 'editor-selection');
306                     p.remove();
307                 }
308             });
309         }
311         endTime = (new Date()).getTime();
312         Y.log('FilterBlocks Timer: ' + (endTime - startTime) + 'ms', 'info', 'editor-selection');
313     };
315     /**
316     * Regular Expression used to find dead font-family styles
317     * @static
318     * @property REG_FONTFAMILY
319     */
320     Y.EditorSelection.REG_FONTFAMILY = /font-family:\s*;/;
322     /**
323     * Regular Expression to determine if a string has a character in it
324     * @static
325     * @property REG_CHAR
326     */
327     Y.EditorSelection.REG_CHAR = /[a-zA-Z-0-9_!@#\$%\^&*\(\)-=_+\[\]\\{}|;':",.\/<>\?]/gi;
329     /**
330     * Regular Expression to determine if a string has a non-character in it
331     * @static
332     * @property REG_NON
333     */
334     Y.EditorSelection.REG_NON = /[\s|\n|\t]/gi;
336     /**
337     * Regular Expression to remove all HTML from a string
338     * @static
339     * @property REG_NOHTML
340     */
341     Y.EditorSelection.REG_NOHTML = /<\S[^><]*>/g;
344     /**
345     * Wraps an array of elements in a Block level tag
346     * @static
347     * @private
348     * @method _wrapBlock
349     */
350     Y.EditorSelection._wrapBlock = function(wrapped) {
351         if (wrapped) {
352             var newChild = Y.Node.create('<' + Y.EditorSelection.DEFAULT_BLOCK_TAG + '></' + Y.EditorSelection.DEFAULT_BLOCK_TAG + '>'),
353                 firstChild = Y.one(wrapped[0]), i;
355             for (i = 1; i < wrapped.length; i++) {
356                 newChild.append(wrapped[i]);
357             }
358             firstChild.replace(newChild);
359             newChild.prepend(firstChild);
360         }
361         return false;
362     };
364     /**
365     * Undoes what filter does enough to return the HTML from the Editor, then re-applies the filter.
366     * @static
367     * @method unfilter
368     * @return {String} The filtered HTML
369     */
370     Y.EditorSelection.unfilter = function() {
371         var root = Y.EditorSelection.ROOT,
372             nodes = root.all('[class]'),
373             html = '', nons, ids,
374             body = root;
376         Y.log('UnFiltering nodes', 'info', 'editor-selection');
378         nodes.each(function(n) {
379             if (n.hasClass(n._yuid)) {
380                 //One of ours
381                 n.setStyle(FONT_FAMILY, n.getStyle(FONT_FAMILY));
382                 n.removeClass(n._yuid);
383                 if (n.getAttribute('class') === '') {
384                     n.removeAttribute('class');
385                 }
386             }
387         });
389         nons = root.all('.yui-non');
390         nons.each(function(n) {
391             if (!n.hasClass('yui-skip') && n.get('innerHTML') === '') {
392                 n.remove();
393             } else {
394                 n.removeClass('yui-non').removeClass('yui-skip');
395             }
396         });
398         ids = root.all('[id]');
399         ids.each(function(n) {
400             if (n.get('id').indexOf('yui_3_') === 0) {
401                 n.removeAttribute('id');
402                 n.removeAttribute('_yuid');
403             }
404         });
406         if (body) {
407             html = body.get('innerHTML');
408         }
410         root.all('.hr').addClass('yui-skip').addClass('yui-non');
412         /*
413         nodes.each(function(n) {
414             n.addClass(n._yuid);
415             n.setStyle(FONT_FAMILY, '');
416             if (n.getAttribute('style') === '') {
417                 n.removeAttribute('style');
418             }
419         });
420         */
422         return html;
423     };
424     /**
425     * Resolve a node from the selection object and return a Node instance
426     * @static
427     * @method resolve
428     * @param {HTMLElement} n The HTMLElement to resolve. Might be a TextNode, gives parentNode.
429     * @return {Node} The Resolved node
430     */
431     Y.EditorSelection.resolve = function(n) {
432         if (!n) {
433             return Y.EditorSelection.ROOT;
434         }
436         if (n && n.nodeType === 3) {
437             //Adding a try/catch here because in rare occasions IE will
438             //Throw a error accessing the parentNode of a stranded text node.
439             //In the case of Ctrl+Z (Undo)
440             try {
441                 n = n.parentNode;
442             } catch (re) {
443                 n = Y.EditorSelection.ROOT;
444             }
445         }
446         return Y.one(n);
447     };
449     /**
450     * Returns the innerHTML of a node with all HTML tags removed.
451     * @static
452     * @method getText
453     * @param {Node} node The Node instance to remove the HTML from
454     * @return {String} The string of text
455     */
456     Y.EditorSelection.getText = function(node) {
457         var txt = node.get('innerHTML').replace(Y.EditorSelection.REG_NOHTML, '');
458         //Clean out the cursor subs to see if the Node is empty
459         txt = txt.replace('<span><br></span>', '').replace('<br>', '');
460         return txt;
461     };
463     //Y.EditorSelection.DEFAULT_BLOCK_TAG = 'div';
464     Y.EditorSelection.DEFAULT_BLOCK_TAG = 'p';
466     /**
467     * The selector to use when looking for Nodes to cache the value of: [style],font[face]
468     * @static
469     * @property ALL
470     */
471     Y.EditorSelection.ALL = '[style],font[face]';
473     /**
474     * The selector to use when looking for block level items.
475     * @static
476     * @property BLOCKS
477     */
478     Y.EditorSelection.BLOCKS = 'p,div,ul,ol,table,style';
479     /**
480     * The temporary fontname applied to a selection to retrieve their values: yui-tmp
481     * @static
482     * @property TMP
483     */
484     Y.EditorSelection.TMP = 'yui-tmp';
485     /**
486     * The default tag to use when creating elements: span
487     * @static
488     * @property DEFAULT_TAG
489     */
490     Y.EditorSelection.DEFAULT_TAG = 'span';
492     /**
493     * The id of the outer cursor wrapper
494     * @static
495     * @property DEFAULT_TAG
496     */
497     Y.EditorSelection.CURID = 'yui-cursor';
499     /**
500     * The id used to wrap the inner space of the cursor position
501     * @static
502     * @property CUR_WRAPID
503     */
504     Y.EditorSelection.CUR_WRAPID = 'yui-cursor-wrapper';
506     /**
507     * The default HTML used to focus the cursor..
508     * @static
509     * @property CURSOR
510     */
511     Y.EditorSelection.CURSOR = '<span><br class="yui-cursor"></span>';
513     /**
514     * The default HTML element from which data will be retrieved. Default: body
515     * @static
516     * @property ROOT
517     */
518     Y.EditorSelection.ROOT = Y.one('body');
520     Y.EditorSelection.hasCursor = function() {
521         var cur = Y.all('#' + Y.EditorSelection.CUR_WRAPID);
522         Y.log('Has Cursor: ' + cur.size(), 'info', 'editor-selection');
523         return cur.size();
524     };
526     /**
527     * Called from Editor keydown to remove the "extra" space before the cursor.
528     * @static
529     * @method cleanCursor
530     */
531     Y.EditorSelection.cleanCursor = function() {
532         //Y.log('Cleaning Cursor', 'info', 'Selection');
533         var cur, sel = 'br.yui-cursor';
534         cur = Y.all(sel);
535         if (cur.size()) {
536             cur.each(function(b) {
537                 var c = b.get('parentNode.parentNode.childNodes'), html;
538                 if (c.size()) {
539                     b.remove();
540                 } else {
541                     html = Y.EditorSelection.getText(c.item(0));
542                     if (html !== '') {
543                         b.remove();
544                     }
545                 }
546             });
547         }
548         /*
549         var cur = Y.all('#' + Y.EditorSelection.CUR_WRAPID);
550         if (cur.size()) {
551             cur.each(function(c) {
552                 var html = c.get('innerHTML');
553                 if (html == '&nbsp;' || html == '<br>') {
554                     if (c.previous() || c.next()) {
555                         c.remove();
556                     }
557                 }
558             });
559         }
560         */
561     };
563     Y.EditorSelection.prototype = {
564         /**
565         * Range text value
566         * @property text
567         * @type String
568         */
569         text: null,
570         /**
571         * Flag to show if the range is collapsed or not
572         * @property isCollapsed
573         * @type Boolean
574         */
575         isCollapsed: null,
576         /**
577         * A Node instance of the parentNode of the anchorNode of the range
578         * @property anchorNode
579         * @type Node
580         */
581         anchorNode: null,
582         /**
583         * The offset from the range object
584         * @property anchorOffset
585         * @type Number
586         */
587         anchorOffset: null,
588         /**
589         * A Node instance of the actual textNode of the range.
590         * @property anchorTextNode
591         * @type Node
592         */
593         anchorTextNode: null,
594         /**
595         * A Node instance of the parentNode of the focusNode of the range
596         * @property focusNode
597         * @type Node
598         */
599         focusNode: null,
600         /**
601         * The offset from the range object
602         * @property focusOffset
603         * @type Number
604         */
605         focusOffset: null,
606         /**
607         * A Node instance of the actual textNode of the range.
608         * @property focusTextNode
609         * @type Node
610         */
611         focusTextNode: null,
612         /**
613         * The actual Selection/Range object
614         * @property _selection
615         * @private
616         */
617         _selection: null,
618         /**
619         * Wrap an element, with another element
620         * @private
621         * @method _wrap
622         * @param {HTMLElement} n The node to wrap
623         * @param {String} tag The tag to use when creating the new element.
624         * @return {HTMLElement} The wrapped node
625         */
626         _wrap: function(n, tag) {
627             var tmp = Y.Node.create('<' + tag + '></' + tag + '>');
628             tmp.set(INNER_HTML, n.get(INNER_HTML));
629             n.set(INNER_HTML, '');
630             n.append(tmp);
631             return Y.Node.getDOMNode(tmp);
632         },
633         /**
634         * Swap an element, with another element
635         * @private
636         * @method _swap
637         * @param {HTMLElement} n The node to swap
638         * @param {String} tag The tag to use when creating the new element.
639         * @return {HTMLElement} The new node
640         */
641         _swap: function(n, tag) {
642             var tmp = Y.Node.create('<' + tag + '></' + tag + '>');
643             tmp.set(INNER_HTML, n.get(INNER_HTML));
644             n.replace(tmp, n);
645             return Y.Node.getDOMNode(tmp);
646         },
647         /**
648         * Get all the nodes in the current selection. This method will actually perform a filter first.
649         * Then it calls doc.execCommand('fontname', null, 'yui-tmp') to touch all nodes in the selection.
650         * The it compiles a list of all nodes affected by the execCommand and builds a NodeList to return.
651         * @method getSelected
652         * @return {NodeList} A NodeList of all items in the selection.
653         */
654         getSelected: function() {
655             var editorSelection = Y.EditorSelection,
656                 root = editorSelection.ROOT,
657                 nodes,
658                 items = [];
660             editorSelection.filter();
661             Y.config.doc.execCommand('fontname', null, editorSelection.TMP);
662             nodes = root.all(editorSelection.ALL);
664             nodes.each(function(n, k) {
665                 if (n.getStyle(FONT_FAMILY) === editorSelection.TMP) {
666                     n.setStyle(FONT_FAMILY, '');
667                     editorSelection.removeFontFamily(n);
668                     if (!n.compareTo(root)) {
669                         items.push(Y.Node.getDOMNode(nodes.item(k)));
670                     }
671                 }
672             });
673             return Y.all(items);
674         },
675         /**
676         * Insert HTML at the current cursor position and return a Node instance of the newly inserted element.
677         * @method insertContent
678         * @param {String} html The HTML to insert.
679         * @return {Node} The inserted Node.
680         */
681         insertContent: function(html) {
682             return this.insertAtCursor(html, this.anchorTextNode, this.anchorOffset, true);
683         },
684         /**
685         * 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.
686         * @method insertAtCursor
687         * @param {String} html The HTML to insert.
688         * @param {Node} node The text node to break when inserting.
689         * @param {Number} offset The left offset of the text node to break and insert the new content.
690         * @param {Boolean} collapse Should the range be collapsed after insertion. default: false
691         * @return {Node} The inserted Node.
692         */
693         insertAtCursor: function(html, node, offset, collapse) {
694             var cur = Y.Node.create('<' + Y.EditorSelection.DEFAULT_TAG + ' class="yui-non"></' + Y.EditorSelection.DEFAULT_TAG + '>'),
695                 inHTML, txt, txt2, newNode, range = this.createRange(), b, root = Y.EditorSelection.ROOT;
697             if (root.compareTo(node)) {
698                 b = Y.Node.create('<span></span>');
699                 node.append(b);
700                 node = b;
701             }
704             if (range.pasteHTML) {
705                 if (offset === 0 && node && !node.previous() && node.get('nodeType') === 3) {
706                     /*
707                     * For some strange reason, range.pasteHTML fails if the node is a textNode and
708                     * the offset is 0. (The cursor is at the beginning of the line)
709                     * It will always insert the new content at position 1 instead of
710                     * position 0. Here we test for that case and do it the hard way.
711                     */
712                     node.insert(html, 'before');
713                     if (range.moveToElementText) {
714                         range.moveToElementText(Y.Node.getDOMNode(node.previous()));
715                     }
716                     //Move the cursor after the new node
717                     range.collapse(false);
718                     range.select();
719                     return node.previous();
720                 } else {
721                     newNode = Y.Node.create(html);
722                     try {
723                         range.pasteHTML('<span id="rte-insert"></span>');
724                     } catch (e) {}
725                     inHTML = root.one('#rte-insert');
726                     if (inHTML) {
727                         inHTML.set('id', '');
728                         inHTML.replace(newNode);
729                         if (range.moveToElementText) {
730                             range.moveToElementText(Y.Node.getDOMNode(newNode));
731                         }
732                         range.collapse(false);
733                         range.select();
734                         return newNode;
735                     } else {
736                         Y.on('available', function() {
737                             inHTML.set('id', '');
738                             inHTML.replace(newNode);
739                             if (range.moveToElementText) {
740                                 range.moveToElementText(Y.Node.getDOMNode(newNode));
741                             }
742                             range.collapse(false);
743                             range.select();
744                         }, '#rte-insert');
745                     }
746                 }
747             } else {
748                 //TODO using Y.Node.create here throws warnings & strips first white space character
749                 //txt = Y.one(Y.Node.create(inHTML.substr(0, offset)));
750                 //txt2 = Y.one(Y.Node.create(inHTML.substr(offset)));
751                 if (offset > 0) {
752                     inHTML = node.get(textContent);
754                     txt = Y.one(Y.config.doc.createTextNode(inHTML.substr(0, offset)));
755                     txt2 = Y.one(Y.config.doc.createTextNode(inHTML.substr(offset)));
757                     node.replace(txt, node);
758                     newNode = Y.Node.create(html);
759                     if (newNode.get('nodeType') === 11) {
760                         b = Y.Node.create('<span></span>');
761                         b.append(newNode);
762                         newNode = b;
763                     }
764                     txt.insert(newNode, 'after');
765                     //if (txt2 && txt2.get('length')) {
766                     if (txt2) {
767                         newNode.insert(cur, 'after');
768                         cur.insert(txt2, 'after');
769                         this.selectNode(cur, collapse);
770                     }
771                 } else {
772                     if (node.get('nodeType') === 3) {
773                         node = node.get('parentNode') || root;
774                     }
775                     newNode = Y.Node.create(html);
776                     html = node.get('innerHTML').replace(/\n/gi, '');
777                     if (html === '' || html === '<br>') {
778                         node.append(newNode);
779                     } else {
780                         if (newNode.get('parentNode')) {
781                             node.insert(newNode, 'before');
782                         } else {
783                             root.prepend(newNode);
784                         }
785                     }
786                     if (node.get('firstChild').test('br')) {
787                         node.get('firstChild').remove();
788                     }
789                 }
790             }
791             return newNode;
792         },
793         /**
794         * Get all elements inside a selection and wrap them with a new element and return a NodeList of all elements touched.
795         * @method wrapContent
796         * @param {String} tag The tag to wrap all selected items with.
797         * @return {NodeList} A NodeList of all items in the selection.
798         */
799         wrapContent: function(tag) {
800             tag = (tag) ? tag : Y.EditorSelection.DEFAULT_TAG;
802             if (!this.isCollapsed) {
803                 Y.log('Wrapping selection with: ' + tag, 'info', 'editor-selection');
804                 var items = this.getSelected(),
805                     changed = [], range, last, first, range2;
807                 items.each(function(n, k) {
808                     var t = n.get('tagName').toLowerCase();
809                     if (t === 'font') {
810                         changed.push(this._swap(items.item(k), tag));
811                     } else {
812                         changed.push(this._wrap(items.item(k), tag));
813                     }
814                 }, this);
816                 range = this.createRange();
817                 first = changed[0];
818                 last = changed[changed.length - 1];
819                 if (this._selection.removeAllRanges) {
820                     range.setStart(changed[0], 0);
821                     range.setEnd(last, last.childNodes.length);
822                     this._selection.removeAllRanges();
823                     this._selection.addRange(range);
824                 } else {
825                     if (range.moveToElementText) {
826                         range.moveToElementText(Y.Node.getDOMNode(first));
827                         range2 = this.createRange();
828                         range2.moveToElementText(Y.Node.getDOMNode(last));
829                         range.setEndPoint('EndToEnd', range2);
830                     }
831                     range.select();
832                 }
834                 changed = Y.all(changed);
835                 Y.log('Returning NodeList with (' + changed.size() + ') item(s)' , 'info', 'editor-selection');
836                 return changed;
839             } else {
840                 Y.log('Can not wrap a collapsed selection, use insertContent', 'error', 'editor-selection');
841                 return Y.all([]);
842             }
843         },
844         /**
845         * Find and replace a string inside a text node and replace it with HTML focusing the node after
846         * to allow you to continue to type.
847         * @method replace
848         * @param {String} se The string to search for.
849         * @param {String} re The string of HTML to replace it with.
850         * @return {Node} The node inserted.
851         */
852         replace: function(se,re) {
853             Y.log('replacing (' + se + ') with (' + re + ')');
854             var range = this.createRange(), node, txt, index, newNode;
856             if (range.getBookmark) {
857                 index = range.getBookmark();
858                 txt = this.anchorNode.get('innerHTML').replace(se, re);
859                 this.anchorNode.set('innerHTML', txt);
860                 range.moveToBookmark(index);
861                 newNode = Y.one(range.parentElement());
862             } else {
863                 node = this.anchorTextNode;
864                 txt = node.get(textContent);
865                 index = txt.indexOf(se);
867                 txt = txt.replace(se, '');
868                 node.set(textContent, txt);
869                 newNode = this.insertAtCursor(re, node, index, true);
870             }
871             return newNode;
872         },
873         /**
874         * Destroy the range.
875         * @method remove
876         * @chainable
877         * @return {EditorSelection}
878         */
879         remove: function() {
880             if (this._selection && this._selection.removeAllRanges) {
881                 this._selection.removeAllRanges();
882             }
883             return this;
884         },
885         /**
886         * Wrapper for the different range creation methods.
887         * @method createRange
888         * @return {RangeObject}
889         */
890         createRange: function() {
891             if (Y.config.doc.selection) {
892                 return Y.config.doc.selection.createRange();
893             } else {
894                 return Y.config.doc.createRange();
895             }
896         },
897         /**
898         * Select a Node (hilighting it).
899         * @method selectNode
900         * @param {Node} node The node to select
901         * @param {Boolean} collapse Should the range be collapsed after insertion. default: false
902         * @chainable
903         * @return {EditorSelection}
904         */
905         selectNode: function(node, collapse, end) {
906             if (!node) {
907                 Y.log('Node passed to selectNode is null', 'error', 'editor-selection');
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.EditorSelection.ROOT.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         },
1001         /**
1002          Gets the offset of the selection for the selection within the current
1003          editor
1004          @public
1005          @method getEditorOffset
1006          @param {Y.Node} [node] Element used to measure the offset to
1007          @return Number Number of characters the selection is from the beginning
1008          @since 3.13.0
1009          */
1010         getEditorOffset: function(node) {
1011             var container = (node || Y.EditorSelection.ROOT).getDOMNode(),
1012                 caretOffset = 0,
1013                 doc = Y.config.doc,
1014                 win = Y.config.win,
1015                 sel,
1016                 range,
1017                 preCaretRange;
1019             if (typeof win.getSelection !== "undefined") {
1020                 range = win.getSelection().getRangeAt(0);
1021                 preCaretRange = range.cloneRange();
1022                 preCaretRange.selectNodeContents(container);
1023                 preCaretRange.setEnd(range.endContainer, range.endOffset);
1024                 caretOffset = preCaretRange.toString().length;
1025             } else {
1026                 sel = doc.selection;
1028                 if ( sel && sel.type !== "Control") {
1029                     range = sel.createRange();
1030                     preCaretRange = doc.body.createTextRange();
1031                     preCaretRange.moveToElementText(container);
1032                     preCaretRange.setEndPoint("EndToEnd", range);
1033                     caretOffset = preCaretRange.text.length;
1034                 }
1035             }
1037             return caretOffset;
1038         }
1039     };
1041     //TODO Remove this alias in 3.6.0
1042     Y.Selection = Y.EditorSelection;
1046 }, '3.13.0', {"requires": ["node"]});