3 Copyright 2012 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
7 YUI.add('editor-selection', function (Y, NAME) {
10 * Wraps some common Selection/Range functionality into a simple object
11 * @class EditorSelection
14 * @submodule selection
17 //TODO This shouldn't be there, Y.Node doesn't normalize getting textnode content.
18 var textContent = 'textContent',
19 INNER_HTML = 'innerHTML',
20 FONT_FAMILY = 'fontFamily';
23 textContent = 'nodeValue';
26 Y.EditorSelection = function(domEvent) {
27 var sel, par, ieNode, nodes, rng, i;
29 if (Y.config.win.getSelection && (!Y.UA.ie || Y.UA.ie < 9)) {
30 sel = Y.config.win.getSelection();
31 } else if (Y.config.doc.selection) {
32 sel = Y.config.doc.selection.createRange();
34 this._selection = sel;
41 this.isCollapsed = (sel.compareEndPoints('StartToEnd', sel)) ? false : true;
42 if (this.isCollapsed) {
43 this.anchorNode = this.focusNode = Y.one(sel.parentElement());
46 ieNode = Y.config.doc.elementFromPoint(domEvent.clientX, domEvent.clientY);
48 rng = sel.duplicate();
50 par = sel.parentElement();
51 nodes = par.childNodes;
53 for (i = 0; i < nodes.length; i++) {
54 //This causes IE to not allow a selection on a doubleclick
55 //rng.select(nodes[i]);
56 if (rng.inRange(sel)) {
67 if (ieNode.nodeType !== 3) {
68 if (ieNode.firstChild) {
69 ieNode = ieNode.firstChild;
71 if (ieNode && ieNode.tagName && ieNode.tagName.toLowerCase() === 'body') {
72 if (ieNode.firstChild) {
73 ieNode = ieNode.firstChild;
77 this.anchorNode = this.focusNode = Y.EditorSelection.resolve(ieNode);
79 rng.moveToElementText(sel.parentElement());
80 var comp = sel.compareEndPoints('StartToStart', rng),
83 //We are not at the beginning of the selection.
84 //Setting the move to something large, may need to increase it later
85 moved = Math.abs(sel.move('character', -9999));
88 this.anchorOffset = this.focusOffset = moved;
90 this.anchorTextNode = this.focusTextNode = Y.one(ieNode);
95 //This helps IE deal with a selection and nodeChange events
96 if (sel.htmlText && sel.htmlText !== '') {
97 var n = Y.Node.create(sel.htmlText);
98 if (n && n.get('id')) {
100 this.anchorNode = this.focusNode = Y.one('#' + id);
102 n = n.get('childNodes');
103 this.anchorNode = this.focusNode = n.item(0);
111 this.isCollapsed = sel.isCollapsed;
112 this.anchorNode = Y.EditorSelection.resolve(sel.anchorNode);
113 this.focusNode = Y.EditorSelection.resolve(sel.focusNode);
114 this.anchorOffset = sel.anchorOffset;
115 this.focusOffset = sel.focusOffset;
117 this.anchorTextNode = Y.one(sel.anchorNode);
118 this.focusTextNode = Y.one(sel.focusNode);
120 if (Y.Lang.isString(sel.text)) {
121 this.text = sel.text;
124 this.text = sel.toString();
132 * Utility method to remove dead font-family styles from an element.
134 * @method removeFontFamily
136 Y.EditorSelection.removeFontFamily = function(n) {
137 n.removeAttribute('face');
138 var s = n.getAttribute('style').toLowerCase();
139 if (s === '' || (s == 'font-family: ')) {
140 n.removeAttribute('style');
142 if (s.match(Y.EditorSelection.REG_FONTFAMILY)) {
143 s = s.replace(Y.EditorSelection.REG_FONTFAMILY, '');
144 n.setAttribute('style', s);
149 * Performs a prefilter on all nodes in the editor. Looks for nodes with a style: fontFamily or font face
150 * It then creates a dynamic class assigns it and removed the property. This is so that we don't lose
151 * the fontFamily when selecting nodes.
155 Y.EditorSelection.filter = function(blocks) {
156 var startTime = (new Date()).getTime();
157 Y.log('Filtering nodes', 'info', 'editor-selection');
159 var nodes = Y.all(Y.EditorSelection.ALL),
160 baseNodes = Y.all('strong,em'),
161 doc = Y.config.doc, hrs,
162 classNames = {}, cssString = '',
165 var startTime1 = (new Date()).getTime();
166 nodes.each(function(n) {
167 var raw = Y.Node.getDOMNode(n);
168 if (raw.style[FONT_FAMILY]) {
169 classNames['.' + n._yuid] = raw.style[FONT_FAMILY];
172 Y.EditorSelection.removeFontFamily(raw);
175 if (n.getStyle(FONT_FAMILY)) {
176 classNames['.' + n._yuid] = n.getStyle(FONT_FAMILY);
178 n.removeAttribute('face');
179 n.setStyle(FONT_FAMILY, '');
180 if (n.getAttribute('style') === '') {
181 n.removeAttribute('style');
184 if (n.getAttribute('style').toLowerCase() === 'font-family: ') {
185 n.removeAttribute('style');
190 var endTime1 = (new Date()).getTime();
191 Y.log('Node Filter Timer: ' + (endTime1 - startTime1) + 'ms', 'info', 'editor-selection');
193 Y.all('.hr').addClass('yui-skip').addClass('yui-non');
196 hrs = doc.getElementsByTagName('hr');
197 Y.each(hrs, function(hr) {
198 var el = doc.createElement('div');
199 el.className = 'hr yui-non yui-skip';
201 el.setAttribute('readonly', true);
202 el.setAttribute('contenteditable', false); //Keep it from being Edited
204 hr.parentNode.replaceChild(el, hr);
206 //Had to move to inline style. writes for ie's < 8. They don't render el.setAttribute('style');
208 s.border = '1px solid #ccc';
213 s.marginBottom = '5px';
214 s.marginLeft = '0px';
215 s.marginRight = '0px';
221 Y.each(classNames, function(v, k) {
222 cssString += k + ' { font-family: ' + v.replace(/"/gi, '') + '; }';
224 Y.StyleSheet(cssString, 'editor');
227 //Not sure about this one?
228 baseNodes.each(function(n, k) {
229 var t = n.get('tagName').toLowerCase(),
231 if (t === 'strong') {
234 Y.EditorSelection.prototype._swap(baseNodes.item(k), newTag);
237 //Filter out all the empty UL/OL's
239 ls.each(function(v, k) {
240 var lis = v.all('li');
247 Y.EditorSelection.filterBlocks();
249 var endTime = (new Date()).getTime();
250 Y.log('Filter Timer: ' + (endTime - startTime) + 'ms', 'info', 'editor-selection');
254 * Method attempts to replace all "orphined" text nodes in the main body by wrapping them with a <p>. Called from filter.
256 * @method filterBlocks
258 Y.EditorSelection.filterBlocks = function() {
259 var startTime = (new Date()).getTime();
260 Y.log('RAW filter blocks', 'info', 'editor-selection');
261 var childs = Y.config.doc.body.childNodes, i, node, wrapped = false, doit = true,
262 sel, single, br, divs, spans, c, s;
265 for (i = 0; i < childs.length; i++) {
266 node = Y.one(childs[i]);
267 if (!node.test(Y.EditorSelection.BLOCKS)) {
269 if (childs[i].nodeType == 3) {
270 c = childs[i][textContent].match(Y.EditorSelection.REG_CHAR);
271 s = childs[i][textContent].match(Y.EditorSelection.REG_NON);
272 if (c === null && s) {
281 wrapped.push(childs[i]);
284 wrapped = Y.EditorSelection._wrapBlock(wrapped);
287 wrapped = Y.EditorSelection._wrapBlock(wrapped);
290 single = Y.all(Y.EditorSelection.DEFAULT_BLOCK_TAG);
291 if (single.size() === 1) {
292 Y.log('Only One default block tag (' + Y.EditorSelection.DEFAULT_BLOCK_TAG + '), focus it..', 'info', 'editor-selection');
293 br = single.item(0).all('br');
294 if (br.size() === 1) {
295 if (!br.item(0).test('.yui-cursor')) {
298 var html = single.item(0).get('innerHTML');
299 if (html === '' || html === ' ') {
300 Y.log('Paragraph empty, focusing cursor', 'info', 'editor-selection');
301 single.set('innerHTML', Y.EditorSelection.CURSOR);
302 sel = new Y.EditorSelection();
303 sel.focusCursor(true, true);
305 if (br.item(0).test('.yui-cursor') && Y.UA.ie) {
310 single.each(function(p) {
311 var html = p.get('innerHTML');
313 Y.log('Empty Paragraph Tag Found, Removing It', 'info', 'editor-selection');
321 divs = Y.all('div, p');
322 divs.each(function(d) {
323 if (d.hasClass('yui-non')) {
326 var html = d.get('innerHTML');
328 Y.log('Empty DIV/P Tag Found, Removing It', 'info', 'editor-selection');
331 Y.log('DIVS/PS Count: ' + d.get('childNodes').size(), 'info', 'editor-selection');
332 if (d.get('childNodes').size() == 1) {
333 Y.log('This Div/P only has one Child Node', 'info', 'editor-selection');
334 if (d.ancestor('p')) {
335 Y.log('This Div/P is a child of a paragraph, remove it..', 'info', 'editor-selection');
336 d.replace(d.get('firstChild'));
342 /* Removed this, as it was causing Pasting to be funky in Safari
343 spans = Y.all('.Apple-style-span, .apple-style-span');
344 Y.log('Apple Spans found: ' + spans.size(), 'info', 'editor-selection');
345 spans.each(function(s) {
346 s.setAttribute('style', '');
352 var endTime = (new Date()).getTime();
353 Y.log('FilterBlocks Timer: ' + (endTime - startTime) + 'ms', 'info', 'editor-selection');
357 * Regular Expression used to find dead font-family styles
359 * @property REG_FONTFAMILY
361 Y.EditorSelection.REG_FONTFAMILY = /font-family: ;/;
364 * Regular Expression to determine if a string has a character in it
368 Y.EditorSelection.REG_CHAR = /[a-zA-Z-0-9_!@#\$%\^&*\(\)-=_+\[\]\\{}|;':",.\/<>\?]/gi;
371 * Regular Expression to determine if a string has a non-character in it
375 Y.EditorSelection.REG_NON = /[\s|\n|\t]/gi;
378 * Regular Expression to remove all HTML from a string
380 * @property REG_NOHTML
382 Y.EditorSelection.REG_NOHTML = /<\S[^><]*>/g;
386 * Wraps an array of elements in a Block level tag
391 Y.EditorSelection._wrapBlock = function(wrapped) {
393 var newChild = Y.Node.create('<' + Y.EditorSelection.DEFAULT_BLOCK_TAG + '></' + Y.EditorSelection.DEFAULT_BLOCK_TAG + '>'),
394 firstChild = Y.one(wrapped[0]), i;
396 for (i = 1; i < wrapped.length; i++) {
397 newChild.append(wrapped[i]);
399 firstChild.replace(newChild);
400 newChild.prepend(firstChild);
406 * Undoes what filter does enough to return the HTML from the Editor, then re-applies the filter.
409 * @return {String} The filtered HTML
411 Y.EditorSelection.unfilter = function() {
412 var nodes = Y.all('body [class]'),
413 html = '', nons, ids,
414 body = Y.one('body');
416 Y.log('UnFiltering nodes', 'info', 'editor-selection');
418 nodes.each(function(n) {
419 if (n.hasClass(n._yuid)) {
421 n.setStyle(FONT_FAMILY, n.getStyle(FONT_FAMILY));
422 n.removeClass(n._yuid);
423 if (n.getAttribute('class') === '') {
424 n.removeAttribute('class');
429 nons = Y.all('.yui-non');
430 nons.each(function(n) {
431 if (!n.hasClass('yui-skip') && n.get('innerHTML') === '') {
434 n.removeClass('yui-non').removeClass('yui-skip');
438 ids = Y.all('body [id]');
439 ids.each(function(n) {
440 if (n.get('id').indexOf('yui_3_') === 0) {
441 n.removeAttribute('id');
442 n.removeAttribute('_yuid');
447 html = body.get('innerHTML');
450 Y.all('.hr').addClass('yui-skip').addClass('yui-non');
453 nodes.each(function(n) {
455 n.setStyle(FONT_FAMILY, '');
456 if (n.getAttribute('style') === '') {
457 n.removeAttribute('style');
465 * Resolve a node from the selection object and return a Node instance
468 * @param {HTMLElement} n The HTMLElement to resolve. Might be a TextNode, gives parentNode.
469 * @return {Node} The Resolved node
471 Y.EditorSelection.resolve = function(n) {
472 if (n && n.nodeType === 3) {
473 //Adding a try/catch here because in rare occasions IE will
474 //Throw a error accessing the parentNode of a stranded text node.
475 //In the case of Ctrl+Z (Undo)
486 * Returns the innerHTML of a node with all HTML tags removed.
489 * @param {Node} node The Node instance to remove the HTML from
490 * @return {String} The string of text
492 Y.EditorSelection.getText = function(node) {
493 var txt = node.get('innerHTML').replace(Y.EditorSelection.REG_NOHTML, '');
494 //Clean out the cursor subs to see if the Node is empty
495 txt = txt.replace('<span><br></span>', '').replace('<br>', '');
499 //Y.EditorSelection.DEFAULT_BLOCK_TAG = 'div';
500 Y.EditorSelection.DEFAULT_BLOCK_TAG = 'p';
503 * The selector to use when looking for Nodes to cache the value of: [style],font[face]
507 Y.EditorSelection.ALL = '[style],font[face]';
510 * The selector to use when looking for block level items.
514 Y.EditorSelection.BLOCKS = 'p,div,ul,ol,table,style';
516 * The temporary fontname applied to a selection to retrieve their values: yui-tmp
520 Y.EditorSelection.TMP = 'yui-tmp';
522 * The default tag to use when creating elements: span
524 * @property DEFAULT_TAG
526 Y.EditorSelection.DEFAULT_TAG = 'span';
529 * The id of the outer cursor wrapper
531 * @property DEFAULT_TAG
533 Y.EditorSelection.CURID = 'yui-cursor';
536 * The id used to wrap the inner space of the cursor position
538 * @property CUR_WRAPID
540 Y.EditorSelection.CUR_WRAPID = 'yui-cursor-wrapper';
543 * The default HTML used to focus the cursor..
547 Y.EditorSelection.CURSOR = '<span><br class="yui-cursor"></span>';
549 Y.EditorSelection.hasCursor = function() {
550 var cur = Y.all('#' + Y.EditorSelection.CUR_WRAPID);
551 Y.log('Has Cursor: ' + cur.size(), 'info', 'editor-selection');
556 * Called from Editor keydown to remove the "extra" space before the cursor.
558 * @method cleanCursor
560 Y.EditorSelection.cleanCursor = function() {
561 //Y.log('Cleaning Cursor', 'info', 'Selection');
562 var cur, sel = 'br.yui-cursor';
565 cur.each(function(b) {
566 var c = b.get('parentNode.parentNode.childNodes'), html;
570 html = Y.EditorSelection.getText(c.item(0));
578 var cur = Y.all('#' + Y.EditorSelection.CUR_WRAPID);
580 cur.each(function(c) {
581 var html = c.get('innerHTML');
582 if (html == ' ' || html == '<br>') {
583 if (c.previous() || c.next()) {
592 Y.EditorSelection.prototype = {
600 * Flag to show if the range is collapsed or not
601 * @property isCollapsed
606 * A Node instance of the parentNode of the anchorNode of the range
607 * @property anchorNode
612 * The offset from the range object
613 * @property anchorOffset
618 * A Node instance of the actual textNode of the range.
619 * @property anchorTextNode
622 anchorTextNode: null,
624 * A Node instance of the parentNode of the focusNode of the range
625 * @property focusNode
630 * The offset from the range object
631 * @property focusOffset
636 * A Node instance of the actual textNode of the range.
637 * @property focusTextNode
642 * The actual Selection/Range object
643 * @property _selection
648 * Wrap an element, with another element
651 * @param {HTMLElement} n The node to wrap
652 * @param {String} tag The tag to use when creating the new element.
653 * @return {HTMLElement} The wrapped node
655 _wrap: function(n, tag) {
656 var tmp = Y.Node.create('<' + tag + '></' + tag + '>');
657 tmp.set(INNER_HTML, n.get(INNER_HTML));
658 n.set(INNER_HTML, '');
660 return Y.Node.getDOMNode(tmp);
663 * Swap an element, with another element
666 * @param {HTMLElement} n The node to swap
667 * @param {String} tag The tag to use when creating the new element.
668 * @return {HTMLElement} The new node
670 _swap: function(n, tag) {
671 var tmp = Y.Node.create('<' + tag + '></' + tag + '>');
672 tmp.set(INNER_HTML, n.get(INNER_HTML));
674 return Y.Node.getDOMNode(tmp);
677 * Get all the nodes in the current selection. This method will actually perform a filter first.
678 * Then it calls doc.execCommand('fontname', null, 'yui-tmp') to touch all nodes in the selection.
679 * The it compiles a list of all nodes affected by the execCommand and builds a NodeList to return.
680 * @method getSelected
681 * @return {NodeList} A NodeList of all items in the selection.
683 getSelected: function() {
684 Y.EditorSelection.filter();
685 Y.config.doc.execCommand('fontname', null, Y.EditorSelection.TMP);
686 var nodes = Y.all(Y.EditorSelection.ALL),
689 nodes.each(function(n, k) {
690 if (n.getStyle(FONT_FAMILY) == Y.EditorSelection.TMP) {
691 n.setStyle(FONT_FAMILY, '');
692 Y.EditorSelection.removeFontFamily(n);
693 if (!n.test('body')) {
694 items.push(Y.Node.getDOMNode(nodes.item(k)));
701 * Insert HTML at the current cursor position and return a Node instance of the newly inserted element.
702 * @method insertContent
703 * @param {String} html The HTML to insert.
704 * @return {Node} The inserted Node.
706 insertContent: function(html) {
707 return this.insertAtCursor(html, this.anchorTextNode, this.anchorOffset, true);
710 * 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.
711 * @method insertAtCursor
712 * @param {String} html The HTML to insert.
713 * @param {Node} node The text node to break when inserting.
714 * @param {Number} offset The left offset of the text node to break and insert the new content.
715 * @param {Boolean} collapse Should the range be collapsed after insertion. default: false
716 * @return {Node} The inserted Node.
718 insertAtCursor: function(html, node, offset, collapse) {
719 var cur = Y.Node.create('<' + Y.EditorSelection.DEFAULT_TAG + ' class="yui-non"></' + Y.EditorSelection.DEFAULT_TAG + '>'),
720 inHTML, txt, txt2, newNode, range = this.createRange(), b;
722 if (node && node.test('body')) {
723 b = Y.Node.create('<span></span>');
729 if (range.pasteHTML) {
730 if (offset === 0 && node && !node.previous() && node.get('nodeType') === 3) {
732 * For some strange reason, range.pasteHTML fails if the node is a textNode and
733 * the offset is 0. (The cursor is at the beginning of the line)
734 * It will always insert the new content at position 1 instead of
735 * position 0. Here we test for that case and do it the hard way.
737 node.insert(html, 'before');
738 if (range.moveToElementText) {
739 range.moveToElementText(Y.Node.getDOMNode(node.previous()));
741 //Move the cursor after the new node
742 range.collapse(false);
744 return node.previous();
746 newNode = Y.Node.create(html);
748 range.pasteHTML('<span id="rte-insert"></span>');
750 inHTML = Y.one('#rte-insert');
752 inHTML.set('id', '');
753 inHTML.replace(newNode);
754 if (range.moveToElementText) {
755 range.moveToElementText(Y.Node.getDOMNode(newNode));
757 range.collapse(false);
761 Y.on('available', function() {
762 inHTML.set('id', '');
763 inHTML.replace(newNode);
764 if (range.moveToElementText) {
765 range.moveToElementText(Y.Node.getDOMNode(newNode));
767 range.collapse(false);
773 //TODO using Y.Node.create here throws warnings & strips first white space character
774 //txt = Y.one(Y.Node.create(inHTML.substr(0, offset)));
775 //txt2 = Y.one(Y.Node.create(inHTML.substr(offset)));
777 inHTML = node.get(textContent);
779 txt = Y.one(Y.config.doc.createTextNode(inHTML.substr(0, offset)));
780 txt2 = Y.one(Y.config.doc.createTextNode(inHTML.substr(offset)));
782 node.replace(txt, node);
783 newNode = Y.Node.create(html);
784 if (newNode.get('nodeType') === 11) {
785 b = Y.Node.create('<span></span>');
789 txt.insert(newNode, 'after');
790 //if (txt2 && txt2.get('length')) {
792 newNode.insert(cur, 'after');
793 cur.insert(txt2, 'after');
794 this.selectNode(cur, collapse);
797 if (node.get('nodeType') === 3) {
798 node = node.get('parentNode');
800 newNode = Y.Node.create(html);
801 html = node.get('innerHTML').replace(/\n/gi, '');
802 if (html === '' || html === '<br>') {
803 node.append(newNode);
805 if (newNode.get('parentNode')) {
806 node.insert(newNode, 'before');
808 Y.one('body').prepend(newNode);
811 if (node.get('firstChild').test('br')) {
812 node.get('firstChild').remove();
819 * Get all elements inside a selection and wrap them with a new element and return a NodeList of all elements touched.
820 * @method wrapContent
821 * @param {String} tag The tag to wrap all selected items with.
822 * @return {NodeList} A NodeList of all items in the selection.
824 wrapContent: function(tag) {
825 tag = (tag) ? tag : Y.EditorSelection.DEFAULT_TAG;
827 if (!this.isCollapsed) {
828 Y.log('Wrapping selection with: ' + tag, 'info', 'editor-selection');
829 var items = this.getSelected(),
830 changed = [], range, last, first, range2;
832 items.each(function(n, k) {
833 var t = n.get('tagName').toLowerCase();
835 changed.push(this._swap(items.item(k), tag));
837 changed.push(this._wrap(items.item(k), tag));
841 range = this.createRange();
843 last = changed[changed.length - 1];
844 if (this._selection.removeAllRanges) {
845 range.setStart(changed[0], 0);
846 range.setEnd(last, last.childNodes.length);
847 this._selection.removeAllRanges();
848 this._selection.addRange(range);
850 if (range.moveToElementText) {
851 range.moveToElementText(Y.Node.getDOMNode(first));
852 range2 = this.createRange();
853 range2.moveToElementText(Y.Node.getDOMNode(last));
854 range.setEndPoint('EndToEnd', range2);
859 changed = Y.all(changed);
860 Y.log('Returning NodeList with (' + changed.size() + ') item(s)' , 'info', 'editor-selection');
865 Y.log('Can not wrap a collapsed selection, use insertContent', 'error', 'editor-selection');
870 * Find and replace a string inside a text node and replace it with HTML focusing the node after
871 * to allow you to continue to type.
873 * @param {String} se The string to search for.
874 * @param {String} re The string of HTML to replace it with.
875 * @return {Node} The node inserted.
877 replace: function(se,re) {
878 Y.log('replacing (' + se + ') with (' + re + ')');
879 var range = this.createRange(), node, txt, index, newNode;
881 if (range.getBookmark) {
882 index = range.getBookmark();
883 txt = this.anchorNode.get('innerHTML').replace(se, re);
884 this.anchorNode.set('innerHTML', txt);
885 range.moveToBookmark(index);
886 newNode = Y.one(range.parentElement());
888 node = this.anchorTextNode;
889 txt = node.get(textContent);
890 index = txt.indexOf(se);
892 txt = txt.replace(se, '');
893 node.set(textContent, txt);
894 newNode = this.insertAtCursor(re, node, index, true);
902 * @return {EditorSelection}
905 if (this._selection && this._selection.removeAllRanges) {
906 this._selection.removeAllRanges();
911 * Wrapper for the different range creation methods.
912 * @method createRange
913 * @return {RangeObject}
915 createRange: function() {
916 if (Y.config.doc.selection) {
917 return Y.config.doc.selection.createRange();
919 return Y.config.doc.createRange();
923 * Select a Node (hilighting it).
925 * @param {Node} node The node to select
926 * @param {Boolean} collapse Should the range be collapsed after insertion. default: false
928 * @return {EditorSelection}
930 selectNode: function(node, collapse, end) {
932 Y.log('Node passed to selectNode is null', 'error', 'editor-selection');
936 node = Y.Node.getDOMNode(node);
937 var range = this.createRange();
938 if (range.selectNode) {
939 range.selectNode(node);
940 this._selection.removeAllRanges();
941 this._selection.addRange(range);
944 this._selection.collapse(node, end);
946 this._selection.collapse(node, 0);
950 if (node.nodeType === 3) {
951 node = node.parentNode;
954 range.moveToElementText(node);
957 range.collapse(((end) ? false : true));
964 * Put a placeholder in the DOM at the current cursor position.
968 setCursor: function() {
969 this.removeCursor(false);
970 return this.insertContent(Y.EditorSelection.CURSOR);
973 * Get the placeholder in the DOM at the current cursor position.
977 getCursor: function() {
978 return Y.all('#' + Y.EditorSelection.CURID);
981 * Remove the cursor placeholder from the DOM.
982 * @method removeCursor
983 * @param {Boolean} keep Setting this to true will keep the node, but remove the unique parts that make it the cursor.
986 removeCursor: function(keep) {
987 var cur = this.getCursor();
990 cur.removeAttribute('id');
991 cur.set('innerHTML', '<br class="yui-cursor">');
999 * Gets a stored cursor and focuses it for editing, must be called sometime after setCursor
1000 * @method focusCursor
1003 focusCursor: function(collapse, end) {
1004 if (collapse !== false) {
1007 if (end !== false) {
1010 var cur = this.removeCursor(true);
1012 cur.each(function(c) {
1013 this.selectNode(c, collapse, end);
1018 * Generic toString for logging.
1022 toString: function() {
1023 return 'EditorSelection Object';
1027 //TODO Remove this alias in 3.6.0
1028 Y.Selection = Y.EditorSelection;
1032 }, '3.7.1', {"requires": ["node"]});