silverstripe-framework/thirdparty/tinymce/classes/dom/Selection.js
2009-11-21 02:26:09 +00:00

748 lines
18 KiB
JavaScript

/**
* $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 += '<span id="__caret">_</span>';
// 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 &nbsp; 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);