/** * Wraps some common Selection/Range functionality into a simple object * @class EditorSelection * @constructor * @module editor * @submodule selection */ //TODO This shouldn't be there, Y.Node doesn't normalize getting textnode content. var textContent = 'textContent', INNER_HTML = 'innerHTML', FONT_FAMILY = 'fontFamily'; if (Y.UA.ie && Y.UA.ie < 11) { textContent = 'nodeValue'; } Y.EditorSelection = function(domEvent) { var sel, par, ieNode, nodes, rng, i, comp, moved = 0, n, id, root = Y.EditorSelection.ROOT; if (Y.config.win.getSelection && (!Y.UA.ie || Y.UA.ie < 9 || Y.UA.ie > 10)) { sel = Y.config.win.getSelection(); } else if (Y.config.doc.selection) { sel = Y.config.doc.selection.createRange(); } this._selection = sel; if (!sel) { return false; } if (sel.pasteHTML) { this.isCollapsed = (sel.compareEndPoints('StartToEnd', sel)) ? false : true; if (this.isCollapsed) { this.anchorNode = this.focusNode = Y.one(sel.parentElement()); if (domEvent) { ieNode = Y.config.doc.elementFromPoint(domEvent.clientX, domEvent.clientY); } rng = sel.duplicate(); if (!ieNode) { par = sel.parentElement(); nodes = par.childNodes; for (i = 0; i < nodes.length; i++) { //This causes IE to not allow a selection on a doubleclick //rng.select(nodes[i]); if (rng.inRange(sel)) { if (!ieNode) { ieNode = nodes[i]; } } } } this.ieNode = ieNode; if (ieNode) { if (ieNode.nodeType !== 3) { if (ieNode.firstChild) { ieNode = ieNode.firstChild; } if (root.compareTo(ieNode)) { if (ieNode.firstChild) { ieNode = ieNode.firstChild; } } } this.anchorNode = this.focusNode = Y.EditorSelection.resolve(ieNode); rng.moveToElementText(sel.parentElement()); comp = sel.compareEndPoints('StartToStart', rng); if (comp) { //We are not at the beginning of the selection. //Setting the move to something large, may need to increase it later moved = this.getEditorOffset(root); sel.move('character', -(moved)); } this.anchorOffset = this.focusOffset = moved; this.anchorTextNode = this.focusTextNode = Y.one(ieNode); } } else { //This helps IE deal with a selection and nodeChange events if (sel.htmlText && sel.htmlText !== '') { n = Y.Node.create(sel.htmlText); if (n && n.get('id')) { id = n.get('id'); this.anchorNode = this.focusNode = Y.one('#' + id); } else if (n) { n = n.get('childNodes'); this.anchorNode = this.focusNode = n.item(0); } } } //var self = this; //debugger; } else { this.isCollapsed = sel.isCollapsed; this.anchorNode = Y.EditorSelection.resolve(sel.anchorNode); this.focusNode = Y.EditorSelection.resolve(sel.focusNode); this.anchorOffset = sel.anchorOffset; this.focusOffset = sel.focusOffset; this.anchorTextNode = Y.one(sel.anchorNode || this.anchorNode); this.focusTextNode = Y.one(sel.focusNode || this.focusNode); } if (Y.Lang.isString(sel.text)) { this.text = sel.text; } else { if (sel.toString) { this.text = sel.toString(); } else { this.text = ''; } } }; /** * Utility method to remove dead font-family styles from an element. * @static * @method removeFontFamily */ Y.EditorSelection.removeFontFamily = function(n) { n.removeAttribute('face'); var s = n.getAttribute('style').toLowerCase(); if (s === '' || (s === 'font-family: ')) { n.removeAttribute('style'); } if (s.match(Y.EditorSelection.REG_FONTFAMILY)) { s = s.replace(Y.EditorSelection.REG_FONTFAMILY, ''); n.setAttribute('style', s); } }; /** * Performs a prefilter on all nodes in the editor. Looks for nodes with a style: fontFamily or font face * It then creates a dynamic class assigns it and removed the property. This is so that we don't lose * the fontFamily when selecting nodes. * @static * @method filter */ Y.EditorSelection.filter = function(blocks) { Y.log('Filtering nodes', 'info', 'editor-selection'); var startTime = (new Date()).getTime(), editorSelection = Y.EditorSelection, root = editorSelection.ROOT, endTime, nodes = root.all(editorSelection.ALL), baseNodes = root.all('strong,em'), doc = Y.config.doc, hrs, classNames = {}, cssString = '', ls, startTime1 = (new Date()).getTime(), endTime1; nodes.each(function(n) { var raw = Y.Node.getDOMNode(n); if (raw.style[FONT_FAMILY]) { classNames['.' + n._yuid] = raw.style[FONT_FAMILY]; n.addClass(n._yuid); editorSelection.removeFontFamily(raw); } }); endTime1 = (new Date()).getTime(); Y.log('Node Filter Timer: ' + (endTime1 - startTime1) + 'ms', 'info', 'editor-selection'); root.all('.hr').addClass('yui-skip').addClass('yui-non'); if (Y.UA.ie) { hrs = Y.Node.getDOMNode(root).getElementsByTagName('hr'); Y.each(hrs, function(hr) { var el = doc.createElement('div'), s = el.style; el.className = 'hr yui-non yui-skip'; el.setAttribute('readonly', true); el.setAttribute('contenteditable', false); //Keep it from being Edited if (hr.parentNode) { hr.parentNode.replaceChild(el, hr); } //Had to move to inline style. writes for ie's < 8. They don't render el.setAttribute('style'); s.border = '1px solid #ccc'; s.lineHeight = '0'; s.height = '0'; s.fontSize = '0'; s.marginTop = '5px'; s.marginBottom = '5px'; s.marginLeft = '0px'; s.marginRight = '0px'; s.padding = '0'; }); } Y.each(classNames, function(v, k) { cssString += k + ' { font-family: ' + v.replace(/"/gi, '') + '; }'; }); Y.StyleSheet(cssString, 'editor'); //Not sure about this one? baseNodes.each(function(n, k) { var t = n.get('tagName').toLowerCase(), newTag = 'i'; if (t === 'strong') { newTag = 'b'; } editorSelection.prototype._swap(baseNodes.item(k), newTag); }); //Filter out all the empty UL/OL's ls = root.all('ol,ul'); ls.each(function(v) { var lis = v.all('li'); if (!lis.size()) { v.remove(); } }); if (blocks) { editorSelection.filterBlocks(); } endTime = (new Date()).getTime(); Y.log('Filter Timer: ' + (endTime - startTime) + 'ms', 'info', 'editor-selection'); }; /** * Method attempts to replace all "orphined" text nodes in the main body by wrapping them with a <p>. Called from filter. * @static * @method filterBlocks */ Y.EditorSelection.filterBlocks = function() { Y.log('RAW filter blocks', 'info', 'editor-selection'); var startTime = (new Date()).getTime(), endTime, childs = Y.Node.getDOMNode(Y.EditorSelection.ROOT).childNodes, i, node, wrapped = false, doit = true, sel, single, br, c, s, html; if (childs) { for (i = 0; i < childs.length; i++) { node = Y.one(childs[i]); if (!node.test(Y.EditorSelection.BLOCKS)) { doit = true; if (childs[i].nodeType === 3) { c = childs[i][textContent].match(Y.EditorSelection.REG_CHAR); s = childs[i][textContent].match(Y.EditorSelection.REG_NON); if (c === null && s) { doit = false; } } if (doit) { if (!wrapped) { wrapped = []; } wrapped.push(childs[i]); } } else { wrapped = Y.EditorSelection._wrapBlock(wrapped); } } wrapped = Y.EditorSelection._wrapBlock(wrapped); } single = Y.all(Y.EditorSelection.DEFAULT_BLOCK_TAG); if (single.size() === 1) { Y.log('Only One default block tag (' + Y.EditorSelection.DEFAULT_BLOCK_TAG + '), focus it..', 'info', 'editor-selection'); br = single.item(0).all('br'); if (br.size() === 1) { if (!br.item(0).test('.yui-cursor')) { br.item(0).remove(); } html = single.item(0).get('innerHTML'); if (html === '' || html === ' ') { Y.log('Paragraph empty, focusing cursor', 'info', 'editor-selection'); single.set('innerHTML', Y.EditorSelection.CURSOR); sel = new Y.EditorSelection(); sel.focusCursor(true, true); } if (br.item(0).test('.yui-cursor') && Y.UA.ie) { br.item(0).remove(); } } } else { single.each(function(p) { var html = p.get('innerHTML'); if (html === '') { Y.log('Empty Paragraph Tag Found, Removing It', 'info', 'editor-selection'); p.remove(); } }); } endTime = (new Date()).getTime(); Y.log('FilterBlocks Timer: ' + (endTime - startTime) + 'ms', 'info', 'editor-selection'); }; /** * Regular Expression used to find dead font-family styles * @static * @property REG_FONTFAMILY */ Y.EditorSelection.REG_FONTFAMILY = /font-family:\s*;/; /** * Regular Expression to determine if a string has a character in it * @static * @property REG_CHAR */ Y.EditorSelection.REG_CHAR = /[a-zA-Z-0-9_!@#\$%\^&*\(\)-=_+\[\]\\{}|;':",.\/<>\?]/gi; /** * Regular Expression to determine if a string has a non-character in it * @static * @property REG_NON */ Y.EditorSelection.REG_NON = /[\s|\n|\t]/gi; /** * Regular Expression to remove all HTML from a string * @static * @property REG_NOHTML */ Y.EditorSelection.REG_NOHTML = /<\S[^><]*>/g; /** * Wraps an array of elements in a Block level tag * @static * @private * @method _wrapBlock */ Y.EditorSelection._wrapBlock = function(wrapped) { if (wrapped) { var newChild = Y.Node.create('<' + Y.EditorSelection.DEFAULT_BLOCK_TAG + '></' + Y.EditorSelection.DEFAULT_BLOCK_TAG + '>'), firstChild = Y.one(wrapped[0]), i; for (i = 1; i < wrapped.length; i++) { newChild.append(wrapped[i]); } firstChild.replace(newChild); newChild.prepend(firstChild); } return false; }; /** * Undoes what filter does enough to return the HTML from the Editor, then re-applies the filter. * @static * @method unfilter * @return {String} The filtered HTML */ Y.EditorSelection.unfilter = function() { var root = Y.EditorSelection.ROOT, nodes = root.all('[class]'), html = '', nons, ids, body = root; Y.log('UnFiltering nodes', 'info', 'editor-selection'); nodes.each(function(n) { if (n.hasClass(n._yuid)) { //One of ours n.setStyle(FONT_FAMILY, n.getStyle(FONT_FAMILY)); n.removeClass(n._yuid); if (n.getAttribute('class') === '') { n.removeAttribute('class'); } } }); nons = root.all('.yui-non'); nons.each(function(n) { if (!n.hasClass('yui-skip') && n.get('innerHTML') === '') { n.remove(); } else { n.removeClass('yui-non').removeClass('yui-skip'); } }); ids = root.all('[id]'); ids.each(function(n) { if (n.get('id').indexOf('yui_3_') === 0) { n.removeAttribute('id'); n.removeAttribute('_yuid'); } }); if (body) { html = body.get('innerHTML'); } root.all('.hr').addClass('yui-skip').addClass('yui-non'); /* nodes.each(function(n) { n.addClass(n._yuid); n.setStyle(FONT_FAMILY, ''); if (n.getAttribute('style') === '') { n.removeAttribute('style'); } }); */ return html; }; /** * Resolve a node from the selection object and return a Node instance * @static * @method resolve * @param {HTMLElement} n The HTMLElement to resolve. Might be a TextNode, gives parentNode. * @return {Node} The Resolved node */ Y.EditorSelection.resolve = function(n) { if (!n) { return Y.EditorSelection.ROOT; } if (n && n.nodeType === 3) { //Adding a try/catch here because in rare occasions IE will //Throw a error accessing the parentNode of a stranded text node. //In the case of Ctrl+Z (Undo) try { n = n.parentNode; } catch (re) { n = Y.EditorSelection.ROOT; } } return Y.one(n); }; /** * Returns the innerHTML of a node with all HTML tags removed. * @static * @method getText * @param {Node} node The Node instance to remove the HTML from * @return {String} The string of text */ Y.EditorSelection.getText = function(node) { var txt = node.get('innerHTML').replace(Y.EditorSelection.REG_NOHTML, ''); //Clean out the cursor subs to see if the Node is empty txt = txt.replace('<span><br></span>', '').replace('<br>', ''); return txt; }; //Y.EditorSelection.DEFAULT_BLOCK_TAG = 'div'; Y.EditorSelection.DEFAULT_BLOCK_TAG = 'p'; /** * The selector to use when looking for Nodes to cache the value of: [style],font[face] * @static * @property ALL */ Y.EditorSelection.ALL = '[style],font[face]'; /** * The selector to use when looking for block level items. * @static * @property BLOCKS */ Y.EditorSelection.BLOCKS = 'p,div,ul,ol,table,style'; /** * The temporary fontname applied to a selection to retrieve their values: yui-tmp * @static * @property TMP */ Y.EditorSelection.TMP = 'yui-tmp'; /** * The default tag to use when creating elements: span * @static * @property DEFAULT_TAG */ Y.EditorSelection.DEFAULT_TAG = 'span'; /** * The id of the outer cursor wrapper * @static * @property CURID */ Y.EditorSelection.CURID = 'yui-cursor'; /** * The id used to wrap the inner space of the cursor position * @static * @property CUR_WRAPID */ Y.EditorSelection.CUR_WRAPID = 'yui-cursor-wrapper'; /** * The default HTML used to focus the cursor.. * @static * @property CURSOR */ Y.EditorSelection.CURSOR = '<span><br class="yui-cursor"></span>'; /** * The default HTML element from which data will be retrieved. Default: body * @static * @property ROOT */ Y.EditorSelection.ROOT = Y.one('body'); Y.EditorSelection.hasCursor = function() { var cur = Y.all('#' + Y.EditorSelection.CUR_WRAPID); Y.log('Has Cursor: ' + cur.size(), 'info', 'editor-selection'); return cur.size(); }; /** * Called from Editor keydown to remove the "extra" space before the cursor. * @static * @method cleanCursor */ Y.EditorSelection.cleanCursor = function() { //Y.log('Cleaning Cursor', 'info', 'Selection'); var cur, sel = 'br.yui-cursor'; cur = Y.all(sel); if (cur.size()) { cur.each(function(b) { var c = b.get('parentNode.parentNode.childNodes'), html; if (c.size()) { b.remove(); } else { html = Y.EditorSelection.getText(c.item(0)); if (html !== '') { b.remove(); } } }); } /* var cur = Y.all('#' + Y.EditorSelection.CUR_WRAPID); if (cur.size()) { cur.each(function(c) { var html = c.get('innerHTML'); if (html == ' ' || html == '<br>') { if (c.previous() || c.next()) { c.remove(); } } }); } */ }; Y.EditorSelection.prototype = { /** * Range text value * @property text * @type String */ text: null, /** * Flag to show if the range is collapsed or not * @property isCollapsed * @type Boolean */ isCollapsed: null, /** * A Node instance of the parentNode of the anchorNode of the range * @property anchorNode * @type Node */ anchorNode: null, /** * The offset from the range object * @property anchorOffset * @type Number */ anchorOffset: null, /** * A Node instance of the actual textNode of the range. * @property anchorTextNode * @type Node */ anchorTextNode: null, /** * A Node instance of the parentNode of the focusNode of the range * @property focusNode * @type Node */ focusNode: null, /** * The offset from the range object * @property focusOffset * @type Number */ focusOffset: null, /** * A Node instance of the actual textNode of the range. * @property focusTextNode * @type Node */ focusTextNode: null, /** * The actual Selection/Range object * @property _selection * @private */ _selection: null, /** * Wrap an element, with another element * @private * @method _wrap * @param {HTMLElement} n The node to wrap * @param {String} tag The tag to use when creating the new element. * @return {HTMLElement} The wrapped node */ _wrap: function(n, tag) { var tmp = Y.Node.create('<' + tag + '></' + tag + '>'); tmp.set(INNER_HTML, n.get(INNER_HTML)); n.set(INNER_HTML, ''); n.append(tmp); return Y.Node.getDOMNode(tmp); }, /** * Swap an element, with another element * @private * @method _swap * @param {HTMLElement} n The node to swap * @param {String} tag The tag to use when creating the new element. * @return {HTMLElement} The new node */ _swap: function(n, tag) { var tmp = Y.Node.create('<' + tag + '></' + tag + '>'); tmp.set(INNER_HTML, n.get(INNER_HTML)); n.replace(tmp, n); return Y.Node.getDOMNode(tmp); }, /** * Get all the nodes in the current selection. This method will actually perform a filter first. * Then it calls doc.execCommand('fontname', null, 'yui-tmp') to touch all nodes in the selection. * The it compiles a list of all nodes affected by the execCommand and builds a NodeList to return. * @method getSelected * @return {NodeList} A NodeList of all items in the selection. */ getSelected: function() { var editorSelection = Y.EditorSelection, root = editorSelection.ROOT, nodes, items = []; editorSelection.filter(); Y.config.doc.execCommand('fontname', null, editorSelection.TMP); nodes = root.all(editorSelection.ALL); nodes.each(function(n, k) { if (n.getStyle(FONT_FAMILY) === editorSelection.TMP) { n.setStyle(FONT_FAMILY, ''); editorSelection.removeFontFamily(n); if (!n.compareTo(root)) { items.push(Y.Node.getDOMNode(nodes.item(k))); } } }); return Y.all(items); }, /** * Insert HTML at the current cursor position and return a Node instance of the newly inserted element. * @method insertContent * @param {String} html The HTML to insert. * @return {Node} The inserted Node. */ insertContent: function(html) { return this.insertAtCursor(html, this.anchorTextNode, this.anchorOffset, true); }, /** * 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. * @method insertAtCursor * @param {String} html The HTML to insert. * @param {Node} node The text node to break when inserting. * @param {Number} offset The left offset of the text node to break and insert the new content. * @param {Boolean} collapse Should the range be collapsed after insertion. default: false * @return {Node} The inserted Node. */ insertAtCursor: function(html, node, offset, collapse) { var cur = Y.Node.create('<' + Y.EditorSelection.DEFAULT_TAG + ' class="yui-non"></' + Y.EditorSelection.DEFAULT_TAG + '>'), inHTML, txt, txt2, newNode, range = this.createRange(), b, root = Y.EditorSelection.ROOT; if (root.compareTo(node)) { b = Y.Node.create('<span></span>'); node.append(b); node = b; } if (range.pasteHTML) { if (offset === 0 && node && !node.previous() && node.get('nodeType') === 3) { /* * For some strange reason, range.pasteHTML fails if the node is a textNode and * the offset is 0. (The cursor is at the beginning of the line) * It will always insert the new content at position 1 instead of * position 0. Here we test for that case and do it the hard way. */ node.insert(html, 'before'); if (range.moveToElementText) { range.moveToElementText(Y.Node.getDOMNode(node.previous())); } //Move the cursor after the new node range.collapse(false); range.select(); return node.previous(); } else { newNode = Y.Node.create(html); try { range.pasteHTML('<span id="rte-insert"></span>'); } catch (e) {} inHTML = root.one('#rte-insert'); if (inHTML) { inHTML.set('id', ''); inHTML.replace(newNode); if (range.moveToElementText) { range.moveToElementText(Y.Node.getDOMNode(newNode)); } range.collapse(false); range.select(); return newNode; } else { Y.on('available', function() { inHTML.set('id', ''); inHTML.replace(newNode); if (range.moveToElementText) { range.moveToElementText(Y.Node.getDOMNode(newNode)); } range.collapse(false); range.select(); }, '#rte-insert'); } } } else { //TODO using Y.Node.create here throws warnings & strips first white space character //txt = Y.one(Y.Node.create(inHTML.substr(0, offset))); //txt2 = Y.one(Y.Node.create(inHTML.substr(offset))); if (offset > 0) { inHTML = node.get(textContent); txt = Y.one(Y.config.doc.createTextNode(inHTML.substr(0, offset))); txt2 = Y.one(Y.config.doc.createTextNode(inHTML.substr(offset))); node.replace(txt, node); newNode = Y.Node.create(html); if (newNode.get('nodeType') === 11) { b = Y.Node.create('<span></span>'); b.append(newNode); newNode = b; } txt.insert(newNode, 'after'); //if (txt2 && txt2.get('length')) { if (txt2) { newNode.insert(cur, 'after'); cur.insert(txt2, 'after'); this.selectNode(cur, collapse); } } else { if (node.get('nodeType') === 3) { node = node.get('parentNode') || root; } newNode = Y.Node.create(html); html = node.get('innerHTML').replace(/\n/gi, ''); if (html === '' || html === '<br>') { node.append(newNode); } else { if (newNode.get('parentNode')) { node.insert(newNode, 'before'); } else { root.prepend(newNode); } } if (node.get('firstChild').test('br')) { node.get('firstChild').remove(); } } } return newNode; }, /** * Get all elements inside a selection and wrap them with a new element and return a NodeList of all elements touched. * @method wrapContent * @param {String} tag The tag to wrap all selected items with. * @return {NodeList} A NodeList of all items in the selection. */ wrapContent: function(tag) { tag = (tag) ? tag : Y.EditorSelection.DEFAULT_TAG; if (!this.isCollapsed) { Y.log('Wrapping selection with: ' + tag, 'info', 'editor-selection'); var items = this.getSelected(), changed = [], range, last, first, range2; items.each(function(n, k) { var t = n.get('tagName').toLowerCase(); if (t === 'font') { changed.push(this._swap(items.item(k), tag)); } else { changed.push(this._wrap(items.item(k), tag)); } }, this); range = this.createRange(); first = changed[0]; last = changed[changed.length - 1]; if (this._selection.removeAllRanges) { range.setStart(changed[0], 0); range.setEnd(last, last.childNodes.length); this._selection.removeAllRanges(); this._selection.addRange(range); } else { if (range.moveToElementText) { range.moveToElementText(Y.Node.getDOMNode(first)); range2 = this.createRange(); range2.moveToElementText(Y.Node.getDOMNode(last)); range.setEndPoint('EndToEnd', range2); } range.select(); } changed = Y.all(changed); Y.log('Returning NodeList with (' + changed.size() + ') item(s)' , 'info', 'editor-selection'); return changed; } else { Y.log('Can not wrap a collapsed selection, use insertContent', 'error', 'editor-selection'); return Y.all([]); } }, /** * Find and replace a string inside a text node and replace it with HTML focusing the node after * to allow you to continue to type. * @method replace * @param {String} se The string to search for. * @param {String} re The string of HTML to replace it with. * @return {Node} The node inserted. */ replace: function(se,re) { Y.log('replacing (' + se + ') with (' + re + ')'); var range = this.createRange(), node, txt, index, newNode; if (range.getBookmark) { index = range.getBookmark(); txt = this.anchorNode.get('innerHTML').replace(se, re); this.anchorNode.set('innerHTML', txt); range.moveToBookmark(index); newNode = Y.one(range.parentElement()); } else { node = this.anchorTextNode; txt = node.get(textContent); index = txt.indexOf(se); txt = txt.replace(se, ''); node.set(textContent, txt); newNode = this.insertAtCursor(re, node, index, true); } return newNode; }, /** * Destroy the range. * @method remove * @chainable * @return {EditorSelection} */ remove: function() { if (this._selection && this._selection.removeAllRanges) { this._selection.removeAllRanges(); } return this; }, /** * Wrapper for the different range creation methods. * @method createRange * @return {Range} */ createRange: function() { if (Y.config.doc.selection) { return Y.config.doc.selection.createRange(); } else { return Y.config.doc.createRange(); } }, /** * Select a Node (hilighting it). * @method selectNode * @param {Node} node The node to select * @param {Boolean} collapse Should the range be collapsed after insertion. default: false * @chainable * @return {EditorSelection} */ selectNode: function(node, collapse, end) { if (!node) { Y.log('Node passed to selectNode is null', 'error', 'editor-selection'); return; } end = end || 0; node = Y.Node.getDOMNode(node); var range = this.createRange(); if (range.selectNode) { try { range.selectNode(node); } catch (err) { // Ignore selection errors like INVALID_NODE_TYPE_ERR } this._selection.removeAllRanges(); this._selection.addRange(range); if (collapse) { try { this._selection.collapse(node, end); } catch (err) { this._selection.collapse(node, 0); } } } else { if (node.nodeType === 3) { node = node.parentNode; } try { range.moveToElementText(node); } catch(e) {} if (collapse) { range.collapse(((end) ? false : true)); } range.select(); } return this; }, /** * Put a placeholder in the DOM at the current cursor position. * @method setCursor * @return {Node} */ setCursor: function() { this.removeCursor(false); return this.insertContent(Y.EditorSelection.CURSOR); }, /** * Get the placeholder in the DOM at the current cursor position. * @method getCursor * @return {Node} */ getCursor: function() { return Y.EditorSelection.ROOT.all('.' + Y.EditorSelection.CURID).get('parentNode'); }, /** * Remove the cursor placeholder from the DOM. * @method removeCursor * @param {Boolean} keep Setting this to true will keep the node, but remove the unique parts that make it the cursor. * @return {Node} */ removeCursor: function(keep) { var cur = this.getCursor(); if (cur && cur.remove) { if (keep) { cur.set('innerHTML', '<br class="yui-cursor">'); } else { cur.remove(); } } return cur; }, /** * Gets a stored cursor and focuses it for editing, must be called sometime after setCursor * @method focusCursor * @return {Node} */ focusCursor: function(collapse, end) { if (collapse !== false) { collapse = true; } if (end !== false) { end = true; } var cur = this.removeCursor(true); if (cur) { cur.each(function(c) { this.selectNode(c, collapse, end); }, this); } }, /** * Generic toString for logging. * @method toString * @return {String} */ toString: function() { return 'EditorSelection Object'; }, /** Gets the offset of the selection for the selection within the current editor @public @method getEditorOffset @param {Y.Node} [node] Element used to measure the offset to @return Number Number of characters the selection is from the beginning @since 3.13.0 */ getEditorOffset: function(node) { var container = (node || Y.EditorSelection.ROOT).getDOMNode(), caretOffset = 0, doc = Y.config.doc, win = Y.config.win, sel, range, preCaretRange; if (typeof win.getSelection !== "undefined") { range = win.getSelection().getRangeAt(0); preCaretRange = range.cloneRange(); preCaretRange.selectNodeContents(container); preCaretRange.setEnd(range.endContainer, range.endOffset); caretOffset = preCaretRange.toString().length; } else { sel = doc.selection; if ( sel && sel.type !== "Control") { range = sel.createRange(); preCaretRange = doc.body.createTextRange(); preCaretRange.moveToElementText(container); preCaretRange.setEndPoint("EndToEnd", range); caretOffset = preCaretRange.text.length; } } return caretOffset; } }; //TODO Remove this alias in 3.6.0 Y.Selection = Y.EditorSelection;