selection.js revision a3d67a254e415c93408a9d4314e46e3cb7d87d99
3421N/A //TODO This shouldn't be there, Y.Node doesn't normalize getting textnode content. 3421N/A //This causes IE to not allow a selection on a doubleclick 3421N/A * Performs a prefilter on all nodes in the editor. Looks for nodes with a style: fontFamily or font face 3421N/A * It then creates a dynamic class assigns it and removed the property. This is so that we don't lose 3421N/A * the fontFamily when selecting nodes. 3421N/A Y.
log(
'Filtering nodes',
'info',
'selection');
3421N/A if (n.getStyle(FONT_FAMILY)) { 3421N/A classNames['.' + n._yuid] = n.getStyle(FONT_FAMILY); 3421N/A n.setStyle(FONT_FAMILY, ''); 3421N/A if (n.getAttribute('style') === '') { 3421N/A n.removeAttribute('style'); 3421N/A if (n.getAttribute('style').toLowerCase() === 'font-family: ') { 3421N/A n.removeAttribute('style'); 3421N/A el.
setAttribute(
'style',
'border: 1px solid #ccc; line-height: 0; font-size: 0;margin-top: 5px; margin-bottom: 5px;');
3421N/A Y.StyleSheet(cssString, 'editor'); 3421N/A baseNodes.each(function(n, k) { 3421N/A var t = n.get('tagName').toLowerCase(), 3421N/A Y.Selection.prototype._swap(baseNodes.item(k), newTag); Y.Selection.filterBlocks(); var endTime = (new Date()).getTime(); Y.log('Filter Timer: ' + (endTime - startTime) + 'ms', 'info', 'selection'); * Method attempts to replace all "orphined" text nodes in the main body by wrapping them with a <p>. Called from filter. Y.Selection.filterBlocks = function() { var startTime = (new Date()).getTime(); Y.log('RAW filter blocks', 'info', 'selection'); var childs = Y.config.doc.body.childNodes, i, node, wrapped = false, doit = true, sel, single, br, divs, spans, c, s; for (i = 0; i < childs.length; i++) { if (!node.test(Y.Selection.BLOCKS)) { if (childs[i].nodeType == 3) { c = childs[i][textContent].match(Y.Selection.REG_CHAR); s = childs[i][textContent].match(Y.Selection.REG_NON); wrapped = Y.Selection._wrapBlock(wrapped); wrapped = Y.Selection._wrapBlock(wrapped); if (single.size() === 1) { Y.log('Only One Paragragh, focus it..', 'info', 'selection'); br = single.item(0).all('br'); var html = single.item(0).get('innerHTML'); if (html == '' || html == ' ') { single.set('innerHTML', Y.Selection.CURSOR); sel.focusCursor(true, true); single.each(function(p) { var html = p.get('innerHTML'); Y.log('Empty Paragraph Tag Found, Removing It', 'info', 'selection'); if (d.hasClass('yui-non')) { var html = d.get('innerHTML'); //Y.log('Empty DIV/P Tag Found, Removing It', 'info', 'selection'); //Y.log('DIVS/PS Count: ' + d.get('childNodes').size(), 'info', 'selection'); if (d.get('childNodes').size() == 1) { //Y.log('This Div/P only has one Child Node', 'info', 'selection'); //Y.log('This Div/P is a child of a paragraph, remove it..', 'info', 'selection'); d.replace(d.get('firstChild')); spans = Y.all('.Apple-style-span, .apple-style-span'); Y.log('Apple Spans found: ' + spans.size(), 'info', 'selection'); s.setAttribute('style', ''); var endTime = (new Date()).getTime(); Y.log('FilterBlocks Timer: ' + (endTime - startTime) + 'ms', 'info', 'selection'); * Regular Expression to determine if a string has a character in it Y.Selection.REG_CHAR = /[a-zA-Z-0-9_]/gi; * Regular Expression to determine if a string has a non-character in it Y.Selection.REG_NON = /[\s\S|\n|\t]/gi; * Wraps an array of elements in a Block level tag Y.Selection._wrapBlock = function(wrapped) { var newChild = Y.Node.create('<p></p>'), firstChild = Y.one(wrapped[0]), i; for (i = 1; i < wrapped.length; i++) { newChild.append(wrapped[i]); firstChild.replace(newChild); newChild.prepend(firstChild); * Undoes what filter does enough to return the HTML from the Editor, then re-applies the filter. * @return {String} The filtered HTML Y.Selection.unfilter = function() { var nodes = Y.all('body [class]'), Y.log('UnFiltering nodes', 'info', 'selection'); if (n.hasClass(n._yuid)) { n.setStyle(FONT_FAMILY, n.getStyle(FONT_FAMILY)); if (n.getAttribute('class') === '') { n.removeAttribute('class'); nons = Y.all('.yui-non'); if (n.get('innerHTML') === '') { n.removeClass('yui-non'); ids = Y.all('body [id]'); if (n.get('id').indexOf('yui_3_') === 0) { n.removeAttribute('_yuid'); html = Y.one('body').get('innerHTML'); n.setStyle(FONT_FAMILY, ''); if (n.getAttribute('style') === '') { n.removeAttribute('style'); * Resolve a node from the selection object and return a Node instance * @param {HTMLElement} n The HTMLElement to resolve. Might be a TextNode, gives parentNode. * @return {Node} The Resolved node Y.Selection.resolve = function(n) { if (n && n.nodeType === 3) { * Returns the innerHTML of a node with all HTML tags removed. * @param {Node} node The Node instance to remove the HTML from * @return {String} The string of text Y.Selection.getText = function(node) { var t = node.get('innerHTML').replace(Y.Selection.STRIP_HTML, ''), c = t.match(Y.Selection.REG_CHAR), s = t.match(Y.Selection.REG_NON); * The selector to use when looking for Nodes to cache the value of: [style],font[face] Y.Selection.ALL = '[style],font[face]'; * RegExp used to strip HTML tags from a string Y.Selection.STRIP_HTML = /<\S[^><]*>/g; * The selector to use when looking for block level items. Y.Selection.BLOCKS = 'p,div,ul,ol,table,style'; * The temporary fontname applied to a selection to retrieve their values: yui-tmp Y.Selection.TMP = 'yui-tmp'; * The default tag to use when creating elements: span Y.Selection.DEFAULT_TAG = 'span'; * The id of the outer cursor wrapper Y.Selection.CURID = 'yui-cursor'; * The id used to wrap the inner space of the cursor position Y.Selection.CUR_WRAPID = 'yui-cursor-wrapper'; * The default HTML used to focus the cursor.. Y.Selection.CURSOR = '<span id="' + Y.Selection.CURID + '"><span id="' + Y.Selection.CUR_WRAPID + '"> </span></span>'; * Called from Editor keydown to remove the "extra" space before the cursor. Y.Selection.cleanCursor = function() { var cur = Y.config.doc.getElementById(Y.Selection.CUR_WRAPID); if (cur.innerHTML == ' ' || cur.innerHTML == '<br>') { cur.parentNode.removeChild(cur); var cur = Y.all('#' + Y.Selection.CUR_WRAPID); var html = c.get('innerHTML'); if (html == ' ' || html == '<br>') { Y.Selection.prototype = { * Flag to show if the range is collapsed or not * A Node instance of the parentNode of the anchorNode of the range * The offset from the range object * A Node instance of the actual textNode of the range. * @property anchorTextNode * A Node instance of the parentNode of the focusNode of the range * The offset from the range object * A Node instance of the actual textNode of the range. * @property focusTextNode * Wrap an element, with another element * @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)); return Y.Node.getDOMNode(tmp); * Swap an element, with another element * @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)); 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. * @return {NodeList} A NodeList of all items in the selection. getSelected: function() { Y.config.doc.execCommand('fontname', null, Y.Selection.TMP); var nodes = Y.all(Y.Selection.ALL), nodes.each(function(n, k) { if (n.getStyle(FONT_FAMILY, Y.Selection.TMP)) { n.setStyle(FONT_FAMILY, ''); n.removeAttribute('face'); if (n.getAttribute('style') === '') { n.removeAttribute('style'); items.push(Y.Node.getDOMNode(nodes.item(k))); * Insert HTML at the current cursor position and return a Node instance of the newly inserted element. * @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. * @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.Selection.DEFAULT_TAG + ' class="yui-
non"></' + Y.Selection.DEFAULT_TAG + '>'), inHTML, txt, txt2, newNode, range = this.createRange(), b; if (node && node.test('body')) { b = Y.Node.create('<span></span>'); newNode = Y.Node.create(html); range.pasteHTML('<span id="rte-
insert"></span>'); inHTML = Y.one('#rte-insert'); Y.on('available', function() { //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))); 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))); newNode = Y.Node.create(html); if (newNode.get('nodeType') === 11) { b = Y.Node.create('<span></span>'); txt.insert(newNode, 'after'); //if (txt2 && txt2.get('length')) { newNode.insert(cur, 'after'); cur.insert(txt2, 'after'); this.selectNode(cur, collapse); if (node.get('nodeType') === 3) { node = node.get('parentNode'); newNode = Y.Node.create(html); html = node.get('innerHTML').replace(/\n/gi, ''); if (html == '' || html == '<br>') { node.insert(newNode, 'before'); if (node.get('firstChild').test('br')) { node.get('firstChild').remove(); * Get all elements inside a selection and wrap them with a new element and return a NodeList of all elements touched. * @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.Selection.DEFAULT_TAG; Y.log('Wrapping selection with: ' + tag, 'info', 'selection'); var items = this.getSelected(), changed = [], range, last, first, range2; items.each(function(n, k) { var t = n.get('tagName').toLowerCase(); changed.push(this._swap(items.item(k), tag)); changed.push(this._wrap(items.item(k), tag)); range = this.createRange(); 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); range.moveToElementText(Y.Node.getDOMNode(first)); range2 = this.createRange(); range2.moveToElementText(Y.Node.getDOMNode(last)); range.setEndPoint('EndToEnd', range2); changed = Y.all(changed); Y.log('Returning NodeList with (' + changed.size() + ') item(s)' , 'info', 'selection'); Y.log('Can not wrap a collapsed selection, use insertContent', 'error', 'selection'); * 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. * @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; index = range.getBookmark(); txt = this.anchorNode.get('innerHTML').replace(se, re); this.anchorNode.set('innerHTML', txt); range.moveToBookmark(index); newNode = Y.one(range.parentElement()); node = this.anchorTextNode; txt = node.get(textContent); txt = txt.replace(se, ''); node.set(textContent, txt); newNode = this.insertAtCursor(re, node, index, true); this._selection.removeAllRanges(); * Wrapper for the different range creation methods. createRange: function() { return Y.config.doc.selection.createRange(); return Y.config.doc.createRange(); * Select a Node (hilighting it). * @param {Node} node The node to select * @param {Boolean} collapse Should the range be collapsed after insertion. default: false selectNode: function(node, collapse, end) { node = Y.Node.getDOMNode(node); var range = this.createRange(); this._selection.removeAllRanges(); this._selection.addRange(range); this._selection.collapse(node, end); this._selection.collapse(node, 0); if (node.nodeType === 3) { range.moveToElementText(node); range.collapse(((end) ? false : true)); * Put a placeholder in the DOM at the current cursor position. this.removeCursor(false); return this.insertContent(Y.Selection.CURSOR); * Get the placeholder in the DOM at the current cursor position. return Y.all('#' + Y.Selection.CURID); * Remove the cursor placeholder from the DOM. * @param {Boolean} keep Setting this to true will keep the node, but remove the unique parts that make it the cursor. removeCursor: function(keep) { var cur = this.getCursor(); cur.removeAttribute('id'); cur.set('innerHTML', '<span id="' + Y.Selection.CUR_WRAPID + '"> </span>'); * Gets a stored cursor and focuses it for editing, must be called sometime after setCursor focusCursor: function(collapse, end) { if (collapse !== false) { var cur = this.removeCursor(true); this.selectNode(c, collapse, end); * Generic toString for logging. return 'Selection Object';