mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
ef10672364
git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@92488 467b73ca-7a2a-4603-9d3b-597d59a354a9
748 lines
18 KiB
JavaScript
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 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);
|