/** * $Id: Selection.js 1076 2009-04-04 15:01:25Z spocke $ * * @author Moxiecode * @copyright Copyright © 2004-2008, Moxiecode Systems AB, All rights reserved. */ (function(tinymce) { function trimNl(s) { return s.replace(/[\n\r]+/g, ''); }; // Shorten names var is = tinymce.is, isIE = tinymce.isIE, each = tinymce.each; /**#@+ * @class This class handles text and control selection it's an crossbrowser utility class. * Consult the TinyMCE Wiki API for more details and examples on how to use this class. * @member tinymce.dom.Selection */ tinymce.create('tinymce.dom.Selection', { /** * Constructs a new selection instance. * * @constructor * @param {tinymce.dom.DOMUtils} dom DOMUtils object reference. * @param {Window} win Window to bind the selection object to. * @param {tinymce.dom.Serializer} serializer DOM serialization class to use for getContent. */ Selection : function(dom, win, serializer) { var t = this; t.dom = dom; t.win = win; t.serializer = serializer; // Add events each([ 'onBeforeSetContent', 'onBeforeGetContent', 'onSetContent', 'onGetContent' ], function(e) { t[e] = new tinymce.util.Dispatcher(t); }); // No W3C Range support if (!t.win.getSelection) t.tridentSel = new tinymce.dom.TridentSelection(t); // Prevent leaks tinymce.addUnload(t.destroy, t); }, /**#@+ * @method */ /** * Returns the selected contents using the DOM serializer passed in to this class. * * @param {Object} s Optional settings class with for example output format text or html. * @return {String} Selected contents in for example HTML format. */ getContent : function(s) { var t = this, r = t.getRng(), e = t.dom.create("body"), se = t.getSel(), wb, wa, n; s = s || {}; wb = wa = ''; s.get = true; s.format = s.format || 'html'; t.onBeforeGetContent.dispatch(t, s); if (s.format == 'text') return t.isCollapsed() ? '' : (r.text || (se.toString ? se.toString() : '')); if (r.cloneContents) { n = r.cloneContents(); if (n) e.appendChild(n); } else if (is(r.item) || is(r.htmlText)) e.innerHTML = r.item ? r.item(0).outerHTML : r.htmlText; else e.innerHTML = r.toString(); // Keep whitespace before and after if (/^\s/.test(e.innerHTML)) wb = ' '; if (/\s+$/.test(e.innerHTML)) wa = ' '; s.getInner = true; s.content = t.isCollapsed() ? '' : wb + t.serializer.serialize(e, s) + wa; t.onGetContent.dispatch(t, s); return s.content; }, /** * Sets the current selection to the specified content. If any contents is selected it will be replaced * with the contents passed in to this function. If there is no selection the contents will be inserted * where the caret is placed in the editor/page. * * @param {String} h HTML contents to set could also be other formats depending on settings. * @param {Object} s Optional settings object with for example data format. */ setContent : function(h, s) { var t = this, r = t.getRng(), c, d = t.win.document; s = s || {format : 'html'}; s.set = true; h = s.content = t.dom.processHTML(h); // Dispatch before set content event t.onBeforeSetContent.dispatch(t, s); h = s.content; if (r.insertNode) { // Make caret marker since insertNode places the caret in the beginning of text after insert h += '_'; // Delete and insert new node r.deleteContents(); r.insertNode(t.getRng().createContextualFragment(h)); // Move to caret marker c = t.dom.get('__caret'); // Make sure we wrap it compleatly, Opera fails with a simple select call r = d.createRange(); r.setStartBefore(c); r.setEndAfter(c); t.setRng(r); // Delete the marker, and hopefully the caret gets placed in the right location // Removed this since it seems to remove   in FF and simply deleting it // doesn't seem to affect the caret position in any browser //d.execCommand('Delete', false, null); // Remove the caret position t.dom.remove('__caret'); } else { if (r.item) { // Delete content and get caret text selection d.execCommand('Delete', false, null); r = t.getRng(); } r.pasteHTML(h); } // Dispatch set content event t.onSetContent.dispatch(t, s); }, /** * Returns the start element of a selection range. If the start is in a text * node the parent element will be returned. * * @return {Element} Start element of selection range. */ getStart : function() { var t = this, r = t.getRng(), e; if (isIE) { if (r.item) return r.item(0); r = r.duplicate(); r.collapse(1); e = r.parentElement(); if (e && e.nodeName == 'BODY') return e.firstChild; return e; } else { e = r.startContainer; if (e.nodeName == 'BODY') return e.firstChild; return t.dom.getParent(e, '*'); } }, /** * Returns the end element of a selection range. If the end is in a text * node the parent element will be returned. * * @return {Element} End element of selection range. */ getEnd : function() { var t = this, r = t.getRng(), e; if (isIE) { if (r.item) return r.item(0); r = r.duplicate(); r.collapse(0); e = r.parentElement(); if (e && e.nodeName == 'BODY') return e.lastChild; return e; } else { e = r.endContainer; if (e.nodeName == 'BODY') return e.lastChild; return t.dom.getParent(e, '*'); } }, /** * Returns a bookmark location for the current selection. This bookmark object * can then be used to restore the selection after some content modification to the document. * * @param {bool} si Optional state if the bookmark should be simple or not. Default is complex. * @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection. */ getBookmark : function(si) { var t = this, r = t.getRng(), tr, sx, sy, vp = t.dom.getViewPort(t.win), e, sp, bp, le, c = -0xFFFFFF, s, ro = t.dom.getRoot(), wb = 0, wa = 0, nv; sx = vp.x; sy = vp.y; // Simple bookmark fast but not as persistent if (si == 'simple') return {rng : r, scrollX : sx, scrollY : sy}; // Handle IE if (isIE) { // Control selection if (r.item) { e = r.item(0); each(t.dom.select(e.nodeName), function(n, i) { if (e == n) { sp = i; return false; } }); return { tag : e.nodeName, index : sp, scrollX : sx, scrollY : sy }; } // Text selection tr = t.dom.doc.body.createTextRange(); tr.moveToElementText(ro); tr.collapse(true); bp = Math.abs(tr.move('character', c)); tr = r.duplicate(); tr.collapse(true); sp = Math.abs(tr.move('character', c)); tr = r.duplicate(); tr.collapse(false); le = Math.abs(tr.move('character', c)) - sp; return { start : sp - bp, length : le, scrollX : sx, scrollY : sy }; } // Handle W3C e = t.getNode(); s = t.getSel(); if (!s) return null; // Image selection if (e && e.nodeName == 'IMG') { return { scrollX : sx, scrollY : sy }; } // Text selection function getPos(r, sn, en) { var w = t.dom.doc.createTreeWalker(r, NodeFilter.SHOW_TEXT, null, false), n, p = 0, d = {}; while ((n = w.nextNode()) != null) { if (n == sn) d.start = p; if (n == en) { d.end = p; return d; } p += trimNl(n.nodeValue || '').length; } return null; }; // Caret or selection if (s.anchorNode == s.focusNode && s.anchorOffset == s.focusOffset) { e = getPos(ro, s.anchorNode, s.focusNode); if (!e) return {scrollX : sx, scrollY : sy}; // Count whitespace before trimNl(s.anchorNode.nodeValue || '').replace(/^\s+/, function(a) {wb = a.length;}); return { start : Math.max(e.start + s.anchorOffset - wb, 0), end : Math.max(e.end + s.focusOffset - wb, 0), scrollX : sx, scrollY : sy, beg : s.anchorOffset - wb == 0 }; } else { e = getPos(ro, r.startContainer, r.endContainer); // Count whitespace before start and end container //(r.startContainer.nodeValue || '').replace(/^\s+/, function(a) {wb = a.length;}); //(r.endContainer.nodeValue || '').replace(/^\s+/, function(a) {wa = a.length;}); if (!e) return {scrollX : sx, scrollY : sy}; return { start : Math.max(e.start + r.startOffset - wb, 0), end : Math.max(e.end + r.endOffset - wa, 0), scrollX : sx, scrollY : sy, beg : r.startOffset - wb == 0 }; } }, /** * Restores the selection to the specified bookmark. * * @param {Object} bookmark Bookmark to restore selection from. * @return {bool} true/false if it was successful or not. */ moveToBookmark : function(b) { var t = this, r = t.getRng(), s = t.getSel(), ro = t.dom.getRoot(), sd, nvl, nv; function getPos(r, sp, ep) { var w = t.dom.doc.createTreeWalker(r, NodeFilter.SHOW_TEXT, null, false), n, p = 0, d = {}, o, v, wa, wb; while ((n = w.nextNode()) != null) { wa = wb = 0; nv = n.nodeValue || ''; //nv.replace(/^\s+[^\s]/, function(a) {wb = a.length - 1;}); //nv.replace(/[^\s]\s+$/, function(a) {wa = a.length - 1;}); nvl = trimNl(nv).length; p += nvl; if (p >= sp && !d.startNode) { o = sp - (p - nvl); // Fix for odd quirk in FF if (b.beg && o >= nvl) continue; d.startNode = n; d.startOffset = o + wb; } if (p >= ep) { d.endNode = n; d.endOffset = ep - (p - nvl) + wb; return d; } } return null; }; if (!b) return false; t.win.scrollTo(b.scrollX, b.scrollY); // Handle explorer if (isIE) { // Handle simple if (r = b.rng) { try { r.select(); } catch (ex) { // Ignore } return true; } t.win.focus(); // Handle control bookmark if (b.tag) { r = ro.createControlRange(); each(t.dom.select(b.tag), function(n, i) { if (i == b.index) r.addElement(n); }); } else { // Try/catch needed since this operation breaks when TinyMCE is placed in hidden divs/tabs try { // Incorrect bookmark if (b.start < 0) return true; r = s.createRange(); r.moveToElementText(ro); r.collapse(true); r.moveStart('character', b.start); r.moveEnd('character', b.length); } catch (ex2) { return true; } } try { r.select(); } catch (ex) { // Needed for some odd IE bug #1843306 } return true; } // Handle W3C if (!s) return false; // Handle simple if (b.rng) { s.removeAllRanges(); s.addRange(b.rng); } else { if (is(b.start) && is(b.end)) { try { sd = getPos(ro, b.start, b.end); if (sd) { r = t.dom.doc.createRange(); r.setStart(sd.startNode, sd.startOffset); r.setEnd(sd.endNode, sd.endOffset); s.removeAllRanges(); s.addRange(r); } if (!tinymce.isOpera) t.win.focus(); } catch (ex) { // Ignore } } } }, /** * Selects the specified element. This will place the start and end of the selection range around the element. * * @param {Element} n HMTL DOM element to select. * @param {} c Bool state if the contents should be selected or not on non IE browser. * @return {Element} Selected element the same element as the one that got passed in. */ select : function(n, c) { var t = this, r = t.getRng(), s = t.getSel(), b, fn, ln, d = t.win.document; function find(n, start) { var walker, o; if (n) { walker = d.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, false); // Find first/last non empty text node while (n = walker.nextNode()) { o = n; if (tinymce.trim(n.nodeValue).length != 0) { if (start) return n; else o = n; } } } return o; }; if (isIE) { try { b = d.body; if (/^(IMG|TABLE)$/.test(n.nodeName)) { r = b.createControlRange(); r.addElement(n); } else { r = b.createTextRange(); r.moveToElementText(n); } r.select(); } catch (ex) { // Throws illigal agrument in IE some times } } else { if (c) { fn = find(n, 1) || t.dom.select('br:first', n)[0]; ln = find(n, 0) || t.dom.select('br:last', n)[0]; if (fn && ln) { r = d.createRange(); if (fn.nodeName == 'BR') r.setStartBefore(fn); else r.setStart(fn, 0); if (ln.nodeName == 'BR') r.setEndBefore(ln); else r.setEnd(ln, ln.nodeValue.length); } else r.selectNode(n); } else r.selectNode(n); t.setRng(r); } return n; }, /** * Returns true/false if the selection range is collapsed or not. Collapsed means if it's a caret or a larger selection. * * @return {bool} true/false state if the selection range is collapsed or not. Collapsed means if it's a caret or a larger selection. */ isCollapsed : function() { var t = this, r = t.getRng(), s = t.getSel(); if (!r || r.item) return false; return !s || r.boundingWidth == 0 || r.collapsed; }, /** * Collapse the selection to start or end of range. * * @param {bool} b Optional boolean state if to collapse to end or not. Defaults to start. */ collapse : function(b) { var t = this, r = t.getRng(), n; // Control range on IE if (r.item) { n = r.item(0); r = this.win.document.body.createTextRange(); r.moveToElementText(n); } r.collapse(!!b); t.setRng(r); }, /** * Returns the browsers internal selection object. * * @return {Selection} Internal browser selection object. */ getSel : function() { var t = this, w = this.win; return w.getSelection ? w.getSelection() : w.document.selection; }, /** * Returns the browsers internal range object. * * @param {bool} w3c Forces a compatible W3C range on IE. * @return {Range} Internal browser range object. */ getRng : function(w3c) { var t = this, s, r; // Found tridentSel object then we need to use that one if (w3c && t.tridentSel) return t.tridentSel.getRangeAt(0); try { if (s = t.getSel()) r = s.rangeCount > 0 ? s.getRangeAt(0) : (s.createRange ? s.createRange() : t.win.document.createRange()); } catch (ex) { // IE throws unspecified error here if TinyMCE is placed in a frame/iframe } // No range found then create an empty one // This can occur when the editor is placed in a hidden container element on Gecko // Or on IE when there was an exception if (!r) r = isIE ? t.win.document.body.createTextRange() : t.win.document.createRange(); return r; }, /** * Changes the selection to the specified DOM range. * * @param {Range} r Range to select. */ setRng : function(r) { var s, t = this; if (!t.tridentSel) { s = t.getSel(); if (s) { s.removeAllRanges(); s.addRange(r); } } else { // Is W3C Range if (r.cloneRange) { t.tridentSel.addRange(r); return; } // Is IE specific range try { r.select(); } catch (ex) { // Needed for some odd IE bug #1843306 } } }, /** * Sets the current selection to the specified DOM element. * * @param {Element} n Element to set as the contents of the selection. * @return {Element} Returns the element that got passed in. */ setNode : function(n) { var t = this; t.setContent(t.dom.getOuterHTML(n)); return n; }, /** * Returns the currently selected element or the common ancestor element for both start and end of the selection. * * @return {Element} Currently selected element or common ancestor element. */ getNode : function() { var t = this, r = t.getRng(), s = t.getSel(), e; if (!isIE) { // Range maybe lost after the editor is made visible again if (!r) return t.dom.getRoot(); e = r.commonAncestorContainer; // Handle selection a image or other control like element such as anchors if (!r.collapsed) { // If the anchor node is a element instead of a text node then return this element if (tinymce.isWebKit && s.anchorNode && s.anchorNode.nodeType == 1) return s.anchorNode.childNodes[s.anchorOffset]; if (r.startContainer == r.endContainer) { if (r.startOffset - r.endOffset < 2) { if (r.startContainer.hasChildNodes()) e = r.startContainer.childNodes[r.startOffset]; } } } return t.dom.getParent(e, '*'); } return r.item ? r.item(0) : r.parentElement(); }, getSelectedBlocks : function(st, en) { var t = this, dom = t.dom, sb, eb, n, bl = []; sb = dom.getParent(st || t.getStart(), dom.isBlock); eb = dom.getParent(en || t.getEnd(), dom.isBlock); if (sb) bl.push(sb); if (sb && eb && sb != eb) { n = sb; while ((n = n.nextSibling) && n != eb) { if (dom.isBlock(n)) bl.push(n); } } if (eb && sb != eb) bl.push(eb); return bl; }, destroy : function(s) { var t = this; t.win = null; if (t.tridentSel) t.tridentSel.destroy(); // Manual destroy then remove unload handler if (!s) tinymce.removeUnload(t.destroy); } /**#@-*/ }); })(tinymce);