@qooxdoo/framework
Version:
The JS Framework for Coders
664 lines (563 loc) • 20.9 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2004-2008 1&1 Internet AG, Germany, http://www.1und1.de
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
Authors:
* Alexander Steitz (aback)
************************************************************************ */
/**
* Low-level selection API to select elements like input and textarea elements
* as well as text nodes or elements which their child nodes.
*
* @ignore(qx.bom.Element, qx.bom.Element.blur)
*/
qx.Bootstrap.define("qx.bom.Selection",
{
/*
*****************************************************************************
STATICS
*****************************************************************************
*/
statics :
{
/**
* Returns the native selection object.
*
* @signature function(documentNode)
* @param documentNode {document} Document node to retrieve the connected selection from
* @return {Selection} native selection object
*/
getSelectionObject : qx.core.Environment.select("html.selection",
{
"selection" : function(documentNode) {
return documentNode.selection;
},
// suitable for gecko, opera, webkit and mshtml >= 9
"default" : function(documentNode) {
return qx.dom.Node.getWindow(documentNode).getSelection();
}
}),
/**
* Returns the current selected text.
*
* @signature function(node)
* @param node {Node} node to retrieve the selection for
* @return {String|null} selected text as string
*/
get : qx.core.Environment.select("html.selection",
{
"selection" : function(node)
{
// to get the selected text in legacy IE you have to work with the TextRange
// of the selection object. So always pass the document node to the
// Range class to get this TextRange object.
var rng = qx.bom.Range.get(qx.dom.Node.getDocument(node));
return rng.text;
},
// suitable for gecko, opera and webkit
"default" : function(node)
{
if (this.__isInputOrTextarea(node)) {
return node.value.substring(node.selectionStart, node.selectionEnd);
} else {
return this.getSelectionObject(qx.dom.Node.getDocument(node)).toString();
}
}
}),
/**
* Returns the length of the selection
*
* @signature function(node)
* @param node {Node} Form node or document/window to check.
* @return {Integer|null} length of the selection or null
*/
getLength : qx.core.Environment.select("html.selection",
{
"selection" : function(node)
{
var selectedValue = this.get(node);
// get the selected part and split it by linebreaks
var split = qx.util.StringSplit.split(selectedValue, /\r\n/);
// return the length substracted by the count of linebreaks
// legacy IE counts linebreaks as two chars
// -> harmonize this to one char per linebreak
return selectedValue.length - (split.length - 1);
},
"default" : function(node)
{
if (qx.core.Environment.get("engine.name") == "opera") {
var selectedValue, selectedLength, split;
if (this.__isInputOrTextarea(node))
{
var start = node.selectionStart;
var end = node.selectionEnd;
selectedValue = node.value.substring(start, end);
selectedLength = end - start;
}
else
{
selectedValue = qx.bom.Selection.get(node);
selectedLength = selectedValue.length;
}
// get the selected part and split it by linebreaks
split = qx.util.StringSplit.split(selectedValue, /\r\n/);
// substract the count of linebreaks
// Opera counts each linebreak as two chars
// -> harmonize this to one char per linebreak
return selectedLength - (split.length - 1);
}
// suitable for gecko and webkit
if (this.__isInputOrTextarea(node)) {
return node.selectionEnd - node.selectionStart;
} else {
return this.get(node).length;
}
}
}),
/**
* Returns the start of the selection
*
* @signature function(node)
* @param node {Node} node to check for
* @return {Integer} start of current selection or "-1" if the current
* selection is not within the given node
*/
getStart : qx.core.Environment.select("html.selection",
{
"selection" : function(node)
{
if (this.__isInputOrTextarea(node))
{
var documentRange = qx.bom.Range.get();
// Check if the document.selection is the text range inside the input element
if (!node.contains(documentRange.parentElement())) {
return -1;
}
var range = qx.bom.Range.get(node);
var len = node.value.length;
// Synchronize range start and end points
range.moveToBookmark(documentRange.getBookmark());
range.moveEnd('character', len);
return len - range.text.length;
}
else
{
var range = qx.bom.Range.get(node);
var parentElement = range.parentElement();
// get a range which holds the text of the parent element
var elementRange = qx.bom.Range.get();
try {
// IE throws an invalid argument error when the document has no selection
elementRange.moveToElementText(parentElement);
} catch(ex) {
return 0;
}
// Move end points of full range so it starts at the user selection
// and ends at the end of the element text.
var bodyRange = qx.bom.Range.get(qx.dom.Node.getBodyElement(node));
bodyRange.setEndPoint("StartToStart", range);
bodyRange.setEndPoint("EndToEnd", elementRange);
// selection is at beginning
if (elementRange.compareEndPoints("StartToStart", bodyRange) == 0) {
return 0;
}
var moved;
var steps = 0;
while (true)
{
moved = bodyRange.moveStart("character", -1);
// Starting points of both ranges are equal
if (elementRange.compareEndPoints("StartToStart", bodyRange) == 0) {
break;
}
// Moving had no effect -> range is at begin of body
if (moved == 0) {
break;
} else {
steps++;
}
}
return ++steps;
}
},
"default" : function(node)
{
if (qx.core.Environment.get("engine.name") === "gecko" ||
qx.core.Environment.get("engine.name") === "webkit")
{
if (this.__isInputOrTextarea(node)) {
return node.selectionStart;
}
else
{
var documentElement = qx.dom.Node.getDocument(node);
var documentSelection = this.getSelectionObject(documentElement);
// gecko and webkit do differ how the user selected the text
// "left-to-right" or "right-to-left"
if (documentSelection.anchorOffset < documentSelection.focusOffset) {
return documentSelection.anchorOffset;
} else {
return documentSelection.focusOffset;
}
}
}
if (this.__isInputOrTextarea(node)) {
return node.selectionStart;
} else {
return qx.bom.Selection.getSelectionObject(qx.dom.Node.getDocument(node)).anchorOffset;
}
}
}),
/**
* Returns the end of the selection
*
* @signature function(node)
* @param node {Node} node to check
* @return {Integer} end of current selection
*/
getEnd : qx.core.Environment.select("html.selection",
{
"selection" : function(node)
{
if (this.__isInputOrTextarea(node))
{
var documentRange = qx.bom.Range.get();
// Check if the document.selection is the text range inside the input element
if (!node.contains(documentRange.parentElement())) {
return -1;
}
var range = qx.bom.Range.get(node);
var len = node.value.length;
// Synchronize range start and end points
range.moveToBookmark(documentRange.getBookmark());
range.moveStart('character', -len);
return range.text.length;
}
else
{
var range = qx.bom.Range.get(node);
var parentElement = range.parentElement();
// get a range which holds the text of the parent element
var elementRange = qx.bom.Range.get();
try {
// IE throws an invalid argument error when the document has no selection
elementRange.moveToElementText(parentElement);
} catch(ex) {
return 0;
}
var len = elementRange.text.length;
// Move end points of full range so it ends at the user selection
// and starts at the start of the element text.
var bodyRange = qx.bom.Range.get(qx.dom.Node.getBodyElement(node));
bodyRange.setEndPoint("EndToEnd", range);
bodyRange.setEndPoint("StartToStart", elementRange);
// selection is at beginning
if (elementRange.compareEndPoints("EndToEnd", bodyRange) == 0) {
return len-1;
}
var moved;
var steps = 0;
while (true)
{
moved = bodyRange.moveEnd("character", 1);
// Ending points of both ranges are equal
if (elementRange.compareEndPoints("EndToEnd", bodyRange) == 0) {
break;
}
// Moving had no effect -> range is at begin of body
if (moved == 0) {
break;
} else {
steps++;
}
}
return len - (++steps);
}
},
"default" : function(node)
{
if (qx.core.Environment.get("engine.name") === "gecko" ||
qx.core.Environment.get("engine.name") === "webkit")
{
if (this.__isInputOrTextarea(node)) {
return node.selectionEnd;
}
else
{
var documentElement = qx.dom.Node.getDocument(node);
var documentSelection = this.getSelectionObject(documentElement);
// gecko and webkit do differ how the user selected the text
// "left-to-right" or "right-to-left"
if (documentSelection.focusOffset > documentSelection.anchorOffset) {
return documentSelection.focusOffset;
} else {
return documentSelection.anchorOffset;
}
}
}
if (this.__isInputOrTextarea(node)) {
return node.selectionEnd;
} else {
return qx.bom.Selection.getSelectionObject(qx.dom.Node.getDocument(node)).focusOffset;
}
}
}),
/**
* Utility method to check for an input or textarea element
*
* @param node {Node} node to check
* @return {Boolean} Whether the given node is an input or textarea element
*/
__isInputOrTextarea : function(node) {
return qx.dom.Node.isElement(node) &&
(node.nodeName.toLowerCase() == "input" ||
node.nodeName.toLowerCase() == "textarea");
},
/**
* Sets a selection at the given node with the given start and end.
* For text nodes, input and textarea elements the start and end parameters
* set the boundaries at the text.
* For element nodes the start and end parameters are used to select the
* childNodes of the given element.
*
* @signature function(node, start, end)
* @param node {Node} node to set the selection at
* @param start {Integer} start of the selection
* @param end {Integer} end of the selection
* @return {Boolean} whether a selection is drawn
*/
set : qx.core.Environment.select("html.selection",
{
"selection" : function(node, start, end)
{
var rng;
// if the node is the document itself then work on with the body element
if (qx.dom.Node.isDocument(node)) {
node = node.body;
}
if (qx.dom.Node.isElement(node) || qx.dom.Node.isText(node))
{
switch(node.nodeName.toLowerCase())
{
case "input":
case "textarea":
case "button":
if (end === undefined)
{
end = node.value.length;
}
if (start >= 0 && start <= node.value.length && end >= 0 && end <= node.value.length)
{
rng = qx.bom.Range.get(node);
rng.collapse(true);
rng.moveStart("character", start);
rng.moveEnd("character", end - start);
rng.select();
return true;
}
break;
case "#text":
if (end === undefined)
{
end = node.nodeValue.length;
}
if (start >= 0 && start <= node.nodeValue.length && end >= 0 && end <= node.nodeValue.length)
{
// get a range of the body element
rng = qx.bom.Range.get(qx.dom.Node.getBodyElement(node));
// use the parent node -> "moveToElementText" expects an element
rng.moveToElementText(node.parentNode);
rng.collapse(true);
rng.moveStart("character", start);
rng.moveEnd("character", end - start);
rng.select();
return true;
}
break;
default:
if (end === undefined)
{
end = node.childNodes.length - 1;
}
// check start and end -> childNodes
if (node.childNodes[start] && node.childNodes[end])
{
// get the TextRange of the body element
// IMPORTANT: only with a range of the body the method "moveElementToText" is available
rng = qx.bom.Range.get(qx.dom.Node.getBodyElement(node));
// position it at the given node
rng.moveToElementText(node.childNodes[start]);
rng.collapse(true);
// create helper range
var newRng = qx.bom.Range.get(qx.dom.Node.getBodyElement(node));
newRng.moveToElementText(node.childNodes[end]);
// set the end of the range to the end of the helper range
rng.setEndPoint("EndToEnd", newRng);
rng.select();
return true;
}
}
}
return false;
},
// suitable for gecko, opera, webkit and mshtml >=9
"default" : function(node, start, end)
{
// special handling for input and textarea elements
var nodeName = node.nodeName.toLowerCase();
if (qx.dom.Node.isElement(node) && (nodeName == "input" || nodeName == "textarea"))
{
// if "end" is not given set it to the end
if (end === undefined) {
end = node.value.length;
}
// check boundaries
if (start >= 0 && start <= node.value.length && end >= 0 && end <= node.value.length)
{
node.focus();
node.select();
node.setSelectionRange(start, end);
return true;
}
}
else
{
var validBoundaries = false;
var sel = qx.dom.Node.getWindow(node).getSelection();
var rng = qx.bom.Range.get(node);
// element or text node?
// for elements nodes the offsets are applied to childNodes
// for text nodes the offsets are applied to the text content
if (qx.dom.Node.isText(node))
{
if (end === undefined) {
end = node.length;
}
if (start >= 0 && start < node.length && end >= 0 && end <= node.length) {
validBoundaries = true;
}
}
else if (qx.dom.Node.isElement(node))
{
if (end === undefined) {
end = node.childNodes.length - 1;
}
if (start >= 0 && node.childNodes[start] && end >= 0 && node.childNodes[end]) {
validBoundaries = true;
}
}
else if (qx.dom.Node.isDocument(node))
{
// work on with the body element
node = node.body;
if (end === undefined) {
end = node.childNodes.length - 1;
}
if (start >= 0 && node.childNodes[start] && end >= 0 && node.childNodes[end]) {
validBoundaries = true;
}
}
if (validBoundaries)
{
// collapse the selection if needed
if (!sel.isCollapsed) {
sel.collapseToStart();
}
// set start and end of the range
rng.setStart(node, start);
// for element nodes set the end after the childNode
if (qx.dom.Node.isText(node)) {
rng.setEnd(node, end);
} else {
rng.setEndAfter(node.childNodes[end]);
}
// remove all existing ranges and add the new one
if (sel.rangeCount > 0) {
sel.removeAllRanges();
}
sel.addRange(rng);
return true;
}
}
return false;
}
}),
/**
* Selects all content/childNodes of the given node
*
* @param node {Node} text, element or document node
* @return {Boolean} whether a selection is drawn
*/
setAll : function(node) {
return qx.bom.Selection.set(node, 0);
},
/**
* Clears the selection on the given node.
*
* @param node {Node} node to clear the selection for
*/
clear : qx.core.Environment.select("html.selection",
{
"selection" : function(node)
{
var rng = qx.bom.Range.get(node);
var parent = rng.parentElement();
var documentRange = qx.bom.Range.get(qx.dom.Node.getDocument(node));
// only collapse if the selection is really on the given node
// -> compare the two parent elements of the ranges with each other and
// the given node
if (qx.dom.Node.isText(node)) {
node = node.parentNode;
}
if (parent == documentRange.parentElement() && parent == node) {
var sel = qx.bom.Selection.getSelectionObject(qx.dom.Node.getDocument(node));
sel.empty();
}
},
"default" : function(node)
{
var sel = qx.bom.Selection.getSelectionObject(qx.dom.Node.getDocument(node));
var nodeName = node.nodeName.toLowerCase();
// if the node is an input or textarea element use the specialized methods
if (qx.dom.Node.isElement(node) && (nodeName == "input" || nodeName == "textarea"))
{
node.setSelectionRange(0, 0);
if (qx.bom.Element && qx.bom.Element.blur) {
qx.bom.Element.blur(node);
}
}
// if the given node is the body/document node -> collapse the selection
else if (qx.dom.Node.isDocument(node) || nodeName == "body")
{
sel.collapse(node.body ? node.body : node, 0);
}
// if an element/text node is given the current selection has to
// encompass the node. Only then the selection is cleared.
else
{
var rng = qx.bom.Range.get(node);
if (!rng.collapsed)
{
var compareNode;
var commonAncestor = rng.commonAncestorContainer;
// compare the parentNode of the textNode with the given node
// (if this node is an element) to decide whether the selection
// is cleared or not.
if (qx.dom.Node.isElement(node) && qx.dom.Node.isText(commonAncestor)) {
compareNode = commonAncestor.parentNode;
} else {
compareNode = commonAncestor;
}
if (compareNode == node) {
sel.collapse(node,0);
}
}
}
}
})
}
});