/* Copyright (c) 2010, Yahoo! Inc. All rights reserved. Code licensed under the BSD License: http://developer.yahoo.com/yui/license.html version: 3.2.0 build: 2676 */ YUI.add('selection', function(Y) { /** * Wraps some common Selection/Range functionality into a simple object * @module editor * @submodule selection */ /** * Wraps some common Selection/Range functionality into a simple object * @class Selection * @for Selection * @constructor */ //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) { textContent = 'nodeValue'; } Y.Selection = function(domEvent) { var sel, par, ieNode, nodes, rng, i; if (Y.config.win.getSelection) { sel = Y.config.win.getSelection(); } else if (Y.config.doc.selection) { sel = Y.config.doc.selection.createRange(); } this._selection = sel; 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); } if (!ieNode) { par = sel.parentElement(); nodes = par.childNodes; rng = sel.duplicate(); 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)) { ieNode = nodes[i]; } } } this.ieNode = ieNode; if (ieNode) { if (ieNode.nodeType !== 3) { if (ieNode.firstChild) { ieNode = ieNode.firstChild; } } this.anchorNode = this.focusNode = Y.Selection.resolve(ieNode); this.anchorOffset = this.focusOffset = (this.anchorNode.nodeValue) ? this.anchorNode.nodeValue.length : 0 ; this.anchorTextNode = this.focusTextNode = Y.one(ieNode); } } //var self = this; //debugger; } else { this.isCollapsed = sel.isCollapsed; this.anchorNode = Y.Selection.resolve(sel.anchorNode); this.focusNode = Y.Selection.resolve(sel.focusNode); this.anchorOffset = sel.anchorOffset; this.focusOffset = sel.focusOffset; this.anchorTextNode = Y.one(sel.anchorNode); this.focusTextNode = Y.one(sel.focusNode); } if (Y.Lang.isString(sel.text)) { this.text = sel.text; } else { if (sel.toString) { this.text = sel.toString(); } else { this.text = ''; } } }; /** * 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.Selection.filter = function(blocks) { var startTime = (new Date()).getTime(); Y.log('Filtering nodes', 'info', 'selection'); var nodes = Y.all(Y.Selection.ALL), baseNodes = Y.all('strong,em'), doc = Y.config.doc, hrs = doc.getElementsByTagName('hr'), classNames = {}, cssString = '', ls; var startTime1 = (new Date()).getTime(); 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); raw.style[FONT_FAMILY] = 'inherit'; raw.removeAttribute('face'); if (raw.getAttribute('style') === '') { raw.removeAttribute('style'); } //This is for IE if (raw.getAttribute('style')) { if (raw.getAttribute('style').toLowerCase() === 'font-family: ') { raw.removeAttribute('style'); } } } /* if (n.getStyle(FONT_FAMILY)) { classNames['.' + n._yuid] = n.getStyle(FONT_FAMILY); n.addClass(n._yuid); n.removeAttribute('face'); n.setStyle(FONT_FAMILY, ''); if (n.getAttribute('style') === '') { n.removeAttribute('style'); } //This is for IE if (n.getAttribute('style').toLowerCase() === 'font-family: ') { n.removeAttribute('style'); } } */ }); var endTime1 = (new Date()).getTime(); Y.log('Node Filter Timer: ' + (endTime1 - startTime1) + 'ms', 'info', 'selection'); Y.each(hrs, function(hr) { var el = doc.createElement('div'); el.className = 'hr yui-non'; el.setAttribute('style', 'border: 1px solid #ccc; line-height: 0; font-size: 0;margin-top: 5px; margin-bottom: 5px;'); el.setAttribute('readonly', true); el.setAttribute('contenteditable', false); //Keep it from being Edited if (hr.parentNode) { hr.parentNode.replaceChild(el, hr); } }); 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'; } Y.Selection.prototype._swap(baseNodes.item(k), newTag); }); //Filter out all the empty UL/OL's ls = Y.all('ol,ul'); ls.each(function(v, k) { var lis = v.all('li'); if (!lis.size()) { v.remove(); } }); if (blocks) { 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
. Called from filter. * @static * @method filterBlocks */ 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; if (childs) { for (i = 0; i < childs.length; i++) { node = Y.one(childs[i]); if (!node.test(Y.Selection.BLOCKS)) { doit = true; if (childs[i].nodeType == 3) { c = childs[i][textContent].match(Y.Selection.REG_CHAR); s = childs[i][textContent].match(Y.Selection.REG_NON); if (c === null && s) { doit = false; } } if (doit) { if (!wrapped) { wrapped = []; } wrapped.push(childs[i]); } } else { wrapped = Y.Selection._wrapBlock(wrapped); } } wrapped = Y.Selection._wrapBlock(wrapped); } single = Y.all('p'); if (single.size() === 1) { Y.log('Only One Paragragh, focus it..', 'info', 'selection'); br = single.item(0).all('br'); if (br.size() === 1) { br.item(0).remove(); var html = single.item(0).get('innerHTML'); if (html == '' || html == ' ') { single.set('innerHTML', Y.Selection.CURSOR); sel = new Y.Selection(); sel.focusCursor(true, true); } } } else { single.each(function(p) { var html = p.get('innerHTML'); if (html === '') { Y.log('Empty Paragraph Tag Found, Removing It', 'info', 'selection'); p.remove(); } }); } if (!Y.UA.ie) { divs = Y.all('div, p'); divs.each(function(d) { if (d.hasClass('yui-non')) { return; } var html = d.get('innerHTML'); if (html === '') { //Y.log('Empty DIV/P Tag Found, Removing It', 'info', 'selection'); d.remove(); } else { //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'); if (d.ancestor('p')) { //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'); spans.each(function(s) { 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 * @static * @property REG_CHAR */ Y.Selection.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.Selection.REG_NON = /[\s\S|\n|\t]/gi; /** * Wraps an array of elements in a Block level tag * @static * @private * @method _wrapBlock */ Y.Selection._wrapBlock = function(wrapped) { if (wrapped) { var newChild = Y.Node.create('
'), 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.Selection.unfilter = function() { var nodes = Y.all('body [class]'), html = '', nons, ids; Y.log('UnFiltering nodes', 'info', '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 = Y.all('.yui-non'); nons.each(function(n) { if (n.get('innerHTML') === '') { n.remove(); } else { n.removeClass('yui-non'); } }); ids = Y.all('body [id]'); ids.each(function(n) { if (n.get('id').indexOf('yui_3_') === 0) { n.removeAttribute('id'); n.removeAttribute('_yuid'); } }); html = Y.one('body').get('innerHTML'); 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.Selection.resolve = function(n) { if (n && n.nodeType === 3) { n = n.parentNode; } 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.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); if (c === null && s) { t = ''; } return t; }; /** * The selector to use when looking for Nodes to cache the value of: [style],font[face] * @static * @property ALL */ Y.Selection.ALL = '[style],font[face]'; /** * RegExp used to strip HTML tags from a string * @static * @property STRIP_HTML */ Y.Selection.STRIP_HTML = /<\S[^><]*>/g; /** * The selector to use when looking for block level items. * @static * @property BLOCKS */ Y.Selection.BLOCKS = 'p,div,ul,ol,table,style'; /** * The temporary fontname applied to a selection to retrieve their values: yui-tmp * @static * @property TMP */ Y.Selection.TMP = 'yui-tmp'; /** * The default tag to use when creating elements: span * @static * @property DEFAULT_TAG */ Y.Selection.DEFAULT_TAG = 'span'; /** * The id of the outer cursor wrapper * @static * @property DEFAULT_TAG */ Y.Selection.CURID = 'yui-cursor'; /** * The id used to wrap the inner space of the cursor position * @static * @property CUR_WRAPID */ Y.Selection.CUR_WRAPID = 'yui-cursor-wrapper'; /** * The default HTML used to focus the cursor.. * @static * @property CURSOR */ Y.Selection.CURSOR = ' '; /** * Called from Editor keydown to remove the "extra" space before the cursor. * @static * @method cleanCursor */ Y.Selection.cleanCursor = function() { /* var cur = Y.config.doc.getElementById(Y.Selection.CUR_WRAPID); if (cur) { cur.id = ''; if (cur.innerHTML == ' ' || cur.innerHTML == '