PageRenderTime 53ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/framework/source/class/qx/bom/Selection.js

https://github.com/StephanHoyer/DJsAdmin
JavaScript | 445 lines | 254 code | 62 blank | 129 comment | 86 complexity | 73966d7fb518829b31fc0c14772a4d94 MD5 | raw file
Possible License(s): Unlicense, CC-BY-SA-3.0
  1. /* ************************************************************************
  2. qooxdoo - the new era of web development
  3. http://qooxdoo.org
  4. Copyright:
  5. 2004-2008 1&1 Internet AG, Germany, http://www.1und1.de
  6. License:
  7. LGPL: http://www.gnu.org/licenses/lgpl.html
  8. EPL: http://www.eclipse.org/org/documents/epl-v10.php
  9. See the LICENSE file in the project's top-level directory for details.
  10. Authors:
  11. * Alexander Back (aback)
  12. ************************************************************************ */
  13. /**
  14. * Low-level selection API to select elements like input and textarea elements
  15. * as well as text nodes or elements which their child nodes.
  16. */
  17. qx.Class.define("qx.bom.Selection",
  18. {
  19. /*
  20. *****************************************************************************
  21. STATICS
  22. *****************************************************************************
  23. */
  24. statics :
  25. {
  26. /**
  27. * Returns the native selection object.
  28. *
  29. * @signature documentNode {document} Document node to retrieve the connected selection
  30. * @param documentNode {Object} The document node
  31. * @return {Selection} native selection object
  32. */
  33. getSelectionObject : qx.core.Variant.select("qx.client",
  34. {
  35. "mshtml" : function(documentNode) {
  36. return documentNode.selection;
  37. },
  38. // suitable for gecko, opera and webkit
  39. "default" : function(documentNode) {
  40. return qx.dom.Node.getWindow(documentNode).getSelection();
  41. }
  42. }),
  43. /**
  44. * Returns the current selected text.
  45. *
  46. * @signature function(node)
  47. * @param node {Node} node to retrieve the selection for
  48. * @return {String?null) selected text as string
  49. */
  50. get : qx.core.Variant.select("qx.client",
  51. {
  52. "mshtml" : function(node)
  53. {
  54. // to get the selected text in IE you have to work with the TextRange
  55. // of the selection object. So always pass the document node to the
  56. // Range class to get this TextRange object.
  57. var rng = qx.bom.Range.get(qx.dom.Node.getDocument(node));
  58. return rng.text;
  59. },
  60. // suitable for gecko, opera and webkit
  61. "default" : function(node)
  62. {
  63. if (qx.dom.Node.isElement(node) && (node.nodeName.toLowerCase() == "input" || node.nodeName.toLowerCase() == "textarea"))
  64. {
  65. return node.value.substring(node.selectionStart, node.selectionEnd);
  66. }
  67. else
  68. {
  69. return qx.bom.Selection.getSelectionObject(qx.dom.Node.getDocument(node)).toString();
  70. }
  71. return null;
  72. }
  73. }),
  74. /**
  75. * Returns the length of the selection
  76. *
  77. * @signature function(node)
  78. * @param node {node} Form node or document/window to check.
  79. * @return {Integer|null} length of the selection or null
  80. */
  81. getLength : qx.core.Variant.select("qx.client",
  82. {
  83. "mshtml" : function(node)
  84. {
  85. var selectedValue = qx.bom.Selection.get(node);
  86. // get the selected part and split it by linebreaks
  87. var split = qx.util.StringSplit.split(selectedValue, /\r\n/);
  88. // return the length substracted by the count of linebreaks
  89. // IE counts linebreaks as two chars
  90. // -> harmonize this to one char per linebreak
  91. return selectedValue.length - (split.length - 1);
  92. },
  93. "opera" : function(node)
  94. {
  95. var selectedValue, selectedLength, split;
  96. if (qx.dom.Node.isElement(node) && (node.nodeName.toLowerCase() == "input" || node.nodeName.toLowerCase() == "textarea"))
  97. {
  98. var start = node.selectionStart;
  99. var end = node.selectionEnd;
  100. selectedValue = node.value.substring(start, end);
  101. selectedLength = end - start;
  102. }
  103. else
  104. {
  105. selectedValue = qx.bom.Selection.get(node);
  106. selectedLength = selectedValue.length;
  107. }
  108. // get the selected part and split it by linebreaks
  109. split = qx.util.StringSplit.split(selectedValue, /\r\n/);
  110. // substract the count of linebreaks
  111. // Opera counts each linebreak as two chars
  112. // -> harmonize this to one char per linebreak
  113. return selectedLength - (split.length - 1);
  114. },
  115. // suitable for gecko and webkit
  116. "default" : function(node)
  117. {
  118. if (qx.dom.Node.isElement(node) && (node.nodeName.toLowerCase() == "input" || node.nodeName.toLowerCase() == "textarea")) {
  119. return node.selectionEnd - node.selectionStart;
  120. } else {
  121. return qx.bom.Selection.get(node).length;
  122. }
  123. return null;
  124. }
  125. }),
  126. /**
  127. * Sets a selection at the given node with the given start and end.
  128. * For text nodes, input and textarea elements the start and end parameters
  129. * set the boundaries at the text.
  130. * For element nodes the start and end parameters are used to select the
  131. * childNodes of the given element.
  132. *
  133. * @signature function(node, start, end)
  134. * @param node {Node} node to set the selection at
  135. * @param start {Integer} start of the selection
  136. * @param end {Integer} end of the selection
  137. * @return {Boolean} whether a selection is drawn
  138. */
  139. set : qx.core.Variant.select("qx.client",
  140. {
  141. "mshtml" : function(node, start, end)
  142. {
  143. var rng;
  144. // if the node is the document itself then work on with the body element
  145. if (qx.dom.Node.isDocument(node))
  146. {
  147. node = node.body;
  148. }
  149. if (qx.dom.Node.isElement(node) || qx.dom.Node.isText(node))
  150. {
  151. switch(node.nodeName.toLowerCase())
  152. {
  153. case "input":
  154. case "textarea":
  155. case "button":
  156. if (end === undefined)
  157. {
  158. end = node.value.length;
  159. }
  160. if (start >= 0 && start <= node.value.length && end >= 0 && end <= node.value.length)
  161. {
  162. rng = qx.bom.Range.get(node);
  163. rng.collapse(true);
  164. rng.moveStart("character", start);
  165. rng.moveEnd("character", end);
  166. rng.select();
  167. return true;
  168. }
  169. break;
  170. case "#text":
  171. if (end === undefined)
  172. {
  173. end = node.nodeValue.length;
  174. }
  175. if (start >= 0 && start <= node.nodeValue.length && end >= 0 && end <= node.nodeValue.length)
  176. {
  177. // get a range of the body element
  178. rng = qx.bom.Range.get(qx.dom.Node.getBodyElement(node));
  179. // use the parent node -> "moveToElementText" expects an element
  180. rng.moveToElementText(node.parentNode);
  181. rng.collapse(true);
  182. rng.moveStart("character", start);
  183. rng.moveEnd("character", end);
  184. rng.select();
  185. return true;
  186. }
  187. break;
  188. default:
  189. if (end === undefined)
  190. {
  191. end = node.childNodes.length - 1;
  192. }
  193. // check start and end -> childNodes
  194. if (node.childNodes[start] && node.childNodes[end])
  195. {
  196. // get the TextRange of the body element
  197. // IMPORTANT: only with a range of the body the method "moveElementToText" is available
  198. rng = qx.bom.Range.get(qx.dom.Node.getBodyElement(node));
  199. // position it at the given node
  200. rng.moveToElementText(node.childNodes[start]);
  201. rng.collapse(true);
  202. // create helper range
  203. var newRng = qx.bom.Range.get(qx.dom.Node.getBodyElement(node));
  204. newRng.moveToElementText(node.childNodes[end]);
  205. // set the end of the range to the end of the helper range
  206. rng.setEndPoint("EndToEnd", newRng);
  207. rng.select();
  208. return true;
  209. }
  210. }
  211. }
  212. return false;
  213. },
  214. // suitable for gecko, opera and webkit
  215. "default" : function(node, start, end)
  216. {
  217. // special handling for input and textarea elements
  218. var nodeName = node.nodeName.toLowerCase();
  219. if (qx.dom.Node.isElement(node) && (nodeName == "input" || nodeName == "textarea"))
  220. {
  221. // if "end" is not given set it to the end
  222. if (end === undefined)
  223. {
  224. end = node.value.length;
  225. }
  226. // check boundaries
  227. if (start >= 0 && start <= node.value.length && end >= 0 && end <= node.value.length)
  228. {
  229. node.select();
  230. node.setSelectionRange(start, end);
  231. return true;
  232. }
  233. }
  234. else
  235. {
  236. var validBoundaries = false;
  237. var sel = qx.dom.Node.getWindow(node).getSelection();
  238. var rng = qx.bom.Range.get(node);
  239. // element or text node?
  240. // for elements nodes the offsets are applied to childNodes
  241. // for text nodes the offsets are applied to the text content
  242. if (qx.dom.Node.isText(node))
  243. {
  244. if (end === undefined) {
  245. end = node.length;
  246. }
  247. if (start >= 0 && start < node.length && end >= 0 && end <= node.length) {
  248. validBoundaries = true;
  249. }
  250. }
  251. else if (qx.dom.Node.isElement(node))
  252. {
  253. if (end === undefined) {
  254. end = node.childNodes.length - 1;
  255. }
  256. if (start >= 0 && node.childNodes[start] && end >= 0 && node.childNodes[end]) {
  257. validBoundaries = true;
  258. }
  259. }
  260. else if (qx.dom.Node.isDocument(node))
  261. {
  262. // work on with the body element
  263. node = node.body;
  264. if (end === undefined) {
  265. end = node.childNodes.length - 1;
  266. }
  267. if (start >= 0 && node.childNodes[start] && end >= 0 && node.childNodes[end]) {
  268. validBoundaries = true;
  269. }
  270. }
  271. if (validBoundaries)
  272. {
  273. // collapse the selection if needed
  274. if (!sel.isCollapsed) {
  275. sel.collapseToStart();
  276. }
  277. // set start and end of the range
  278. rng.setStart(node, start);
  279. // for element nodes set the end after the childNode
  280. if (qx.dom.Node.isText(node)) {
  281. rng.setEnd(node, end);
  282. } else {
  283. rng.setEndAfter(node.childNodes[end]);
  284. }
  285. // remove all existing ranges and add the new one
  286. if (sel.rangeCount > 0) {
  287. sel.removeAllRanges();
  288. }
  289. sel.addRange(rng);
  290. return true;
  291. }
  292. }
  293. return false;
  294. }
  295. }),
  296. /**
  297. * Selects all content/childNodes of the given node
  298. *
  299. * @param node {Node} text, element or document node
  300. * @return {Boolean} whether a selection is drawn
  301. */
  302. setAll : function(node) {
  303. return qx.bom.Selection.set(node, 0);
  304. },
  305. /**
  306. * Clears the selection on the given node.
  307. *
  308. * @param node {Node} node to clear the selection for
  309. * @return {void}
  310. */
  311. clear : qx.core.Variant.select("qx.client",
  312. {
  313. "mshtml" : function(node)
  314. {
  315. var sel = qx.bom.Selection.getSelectionObject(qx.dom.Node.getDocument(node));
  316. var rng = qx.bom.Range.get(node);
  317. var parent = rng.parentElement();
  318. var documentRange = qx.bom.Range.get(qx.dom.Node.getDocument(node));
  319. // only collapse if the selection is really on the given node
  320. // -> compare the two parent elements of the ranges with each other and
  321. // the given node
  322. if (parent == documentRange.parentElement() && parent == node) {
  323. sel.empty();
  324. }
  325. },
  326. "default" : function(node)
  327. {
  328. var sel = qx.bom.Selection.getSelectionObject(qx.dom.Node.getDocument(node));
  329. var nodeName = node.nodeName.toLowerCase();
  330. // if the node is an input or textarea element use the specialized methods
  331. if (qx.dom.Node.isElement(node) && (nodeName == "input" || nodeName == "textarea"))
  332. {
  333. // TODO: this leads Webkit to also focus the input/textarea element
  334. // which is NOT desired.
  335. // Additionally there is a bug in webkit with input/textarea elements
  336. // concerning the native selection and range object.
  337. // -> getting e.g. the startContainer/endContainer of the range returns
  338. // the text element (as expected) but webkit does embed this text node
  339. // into a lonely DIV element, so there us no chance to check if the
  340. // selection is currently at the input/textarea element to only perform
  341. // the "setSelectionRange" in the case the given node is REALLY selected.
  342. // Webkit bugzilla: https://bugs.webkit.org/show_bug.cgi?id=15903
  343. // qooxdoo bugzilla: http://bugzilla.qooxdoo.org/show_bug.cgi?id=1087
  344. node.setSelectionRange(0, 0);
  345. qx.bom.Element.blur(node);
  346. }
  347. // if the given node is the body/document node -> collapse the selection
  348. else if (qx.dom.Node.isDocument(node) || nodeName == "body")
  349. {
  350. sel.collapse(node.body ? node.body : node, 0);
  351. }
  352. // if an element/text node is given the current selection has to
  353. // encompass the node. Only then the selection is cleared.
  354. else
  355. {
  356. var rng = qx.bom.Range.get(node);
  357. if (!rng.collapsed)
  358. {
  359. var compareNode;
  360. var commonAncestor = rng.commonAncestorContainer;
  361. // compare the parentNode of the textNode with the given node
  362. // (if this node is an element) to decide whether the selection
  363. // is cleared or not.
  364. if (qx.dom.Node.isElement(node) && qx.dom.Node.isText(commonAncestor)) {
  365. compareNode = commonAncestor.parentNode;
  366. } else {
  367. compareNode = commonAncestor;
  368. }
  369. if (compareNode == node) {
  370. sel.collapse(node,0);
  371. }
  372. }
  373. }
  374. }
  375. })
  376. }
  377. });