elementFinder.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  3. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. define(["util", "jquery"], function (util, $) {
  5. var elementFinder = util.Module("elementFinder");
  6. var assert = util.assert;
  7. elementFinder.ignoreElement = function ignoreElement(el) {
  8. if (el instanceof $) {
  9. el = el[0];
  10. }
  11. while (el) {
  12. if ($(el).hasClass("togetherjs")) {
  13. return true;
  14. }
  15. el = el.parentNode;
  16. }
  17. return false;
  18. };
  19. elementFinder.elementLocation = function elementLocation(el) {
  20. assert(el !== null, "Got null element");
  21. if (el instanceof $) {
  22. // a jQuery element
  23. el = el[0];
  24. }
  25. if (el[0] && el.attr && el[0].nodeType == 1) {
  26. // Or a jQuery element not made by us
  27. el = el[0];
  28. }
  29. if (el.id) {
  30. return "#" + el.id;
  31. }
  32. if (el.tagName == "BODY") {
  33. return "body";
  34. }
  35. if (el.tagName == "HEAD") {
  36. return "head";
  37. }
  38. if (el === document) {
  39. return "document";
  40. }
  41. var parent = el.parentNode;
  42. if ((! parent) || parent == el) {
  43. console.warn("elementLocation(", el, ") has null parent");
  44. throw new Error("No locatable parent found");
  45. }
  46. var parentLocation = elementLocation(parent);
  47. var children = parent.childNodes;
  48. var _len = children.length;
  49. var index = 0;
  50. for (var i=0; i<_len; i++) {
  51. if (children[i] == el) {
  52. break;
  53. }
  54. if (children[i].nodeType == document.ELEMENT_NODE) {
  55. if (children[i].className.indexOf("togetherjs") != -1) {
  56. // Don't count our UI
  57. continue;
  58. }
  59. // Don't count text or comments
  60. index++;
  61. }
  62. }
  63. return parentLocation + ":nth-child(" + (index+1) + ")";
  64. };
  65. elementFinder.CannotFind = util.Class({
  66. constructor: function CannotFind(location, reason, context) {
  67. this.prefix = "";
  68. this.location = location;
  69. this.reason = reason;
  70. this.context = context;
  71. },
  72. toString: function () {
  73. var loc;
  74. try {
  75. loc = elementFinder.elementLocation(this.context);
  76. } catch (e) {
  77. loc = this.context;
  78. }
  79. return (
  80. "[CannotFind " + this.prefix +
  81. "(" + this.location + "): " +
  82. this.reason + " in " +
  83. loc + "]");
  84. }
  85. });
  86. elementFinder.findElement = function findElement(loc, container) {
  87. // FIXME: should this all just be done with document.querySelector()?
  88. // But no! We can't ignore togetherjs elements with querySelector.
  89. // But maybe! We *could* make togetherjs elements less obtrusive?
  90. container = container || document;
  91. var el, rest;
  92. if (loc === "body") {
  93. return document.body;
  94. } else if (loc === "head") {
  95. return document.head;
  96. } else if (loc === "document") {
  97. return document;
  98. } else if (loc.indexOf("body") === 0) {
  99. el = document.body;
  100. try {
  101. return findElement(loc.substr(("body").length), el);
  102. } catch (e) {
  103. if (e instanceof elementFinder.CannotFind) {
  104. e.prefix = "body" + e.prefix;
  105. }
  106. throw e;
  107. }
  108. } else if (loc.indexOf("head") === 0) {
  109. el = document.head;
  110. try {
  111. return findElement(loc.substr(("head").length), el);
  112. } catch (e) {
  113. if (e instanceof elementFinder.CannotFind) {
  114. e.prefix = "head" + e.prefix;
  115. }
  116. throw e;
  117. }
  118. } else if (loc.indexOf("#") === 0) {
  119. var id;
  120. loc = loc.substr(1);
  121. if (loc.indexOf(":") === -1) {
  122. id = loc;
  123. rest = "";
  124. } else {
  125. id = loc.substr(0, loc.indexOf(":"));
  126. rest = loc.substr(loc.indexOf(":"));
  127. }
  128. el = document.getElementById(id);
  129. if (! el) {
  130. throw elementFinder.CannotFind("#" + id, "No element by that id", container);
  131. }
  132. if (rest) {
  133. try {
  134. return findElement(rest, el);
  135. } catch (e) {
  136. if (e instanceof elementFinder.CannotFind) {
  137. e.prefix = "#" + id + e.prefix;
  138. }
  139. throw e;
  140. }
  141. } else {
  142. return el;
  143. }
  144. } else if (loc.indexOf(":nth-child(") === 0) {
  145. loc = loc.substr((":nth-child(").length);
  146. if (loc.indexOf(")") == -1) {
  147. throw "Invalid location, missing ): " + loc;
  148. }
  149. var num = loc.substr(0, loc.indexOf(")"));
  150. num = parseInt(num, 10);
  151. var count = num;
  152. loc = loc.substr(loc.indexOf(")") + 1);
  153. var children = container.childNodes;
  154. el = null;
  155. for (var i=0; i<children.length; i++) {
  156. var child = children[i];
  157. if (child.nodeType == document.ELEMENT_NODE) {
  158. if (child.className.indexOf("togetherjs") != -1) {
  159. continue;
  160. }
  161. count--;
  162. if (count === 0) {
  163. // this is the element
  164. el = child;
  165. break;
  166. }
  167. }
  168. }
  169. if (! el) {
  170. throw elementFinder.CannotFind(":nth-child(" + num + ")", "container only has " + (num - count) + " elements", container);
  171. }
  172. if (loc) {
  173. try {
  174. return elementFinder.findElement(loc, el);
  175. } catch (e) {
  176. if (e instanceof elementFinder.CannotFind) {
  177. e.prefix = ":nth-child(" + num + ")" + e.prefix;
  178. }
  179. throw e;
  180. }
  181. } else {
  182. return el;
  183. }
  184. } else {
  185. throw elementFinder.CannotFind(loc, "Malformed location", container);
  186. }
  187. };
  188. elementFinder.elementByPixel = function (height) {
  189. /* Returns {location: "...", offset: pixels}
  190. To get the pixel position back, you'd do:
  191. $(location).offset().top + offset
  192. */
  193. function search(start, height) {
  194. var last = null;
  195. var children = start.children();
  196. children.each(function () {
  197. var el = $(this);
  198. if (el.hasClass("togetherjs") || el.css("position") == "fixed" || ! el.is(":visible")) {
  199. return;
  200. }
  201. if (el.offset().top > height) {
  202. return false;
  203. }
  204. last = el;
  205. });
  206. if ((! children.length) || (! last)) {
  207. // There are no children, or only inapplicable children
  208. return {
  209. location: elementFinder.elementLocation(start[0]),
  210. offset: height - start.offset().top,
  211. absoluteTop: height,
  212. documentHeight: $(document).height()
  213. };
  214. }
  215. return search(last, height);
  216. }
  217. return search($(document.body), height);
  218. };
  219. elementFinder.pixelForPosition = function (position) {
  220. /* Inverse of elementFinder.elementByPixel */
  221. if (position.location == "body") {
  222. return position.offset;
  223. }
  224. var el;
  225. try {
  226. el = elementFinder.findElement(position.location);
  227. } catch (e) {
  228. if (e instanceof elementFinder.CannotFind && position.absoluteTop) {
  229. // We don't trust absoluteTop to be quite right locally, so we adjust
  230. // for the total document height differences:
  231. var percent = position.absoluteTop / position.documentHeight;
  232. return $(document).height() * percent;
  233. }
  234. throw e;
  235. }
  236. var top = $(el).offset().top;
  237. // FIXME: maybe here we should test for sanity, like if an element is
  238. // hidden. We can use position.absoluteTop to get a sense of where the
  239. // element roughly should be. If the sanity check failed we'd use
  240. // absoluteTop
  241. return top + position.offset;
  242. };
  243. return elementFinder;
  244. });