PageRenderTime 43ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/b2g/chrome/content/forms.js

https://github.com/marcussaad/firefox
JavaScript | 540 lines | 361 code | 87 blank | 92 comment | 81 complexity | bb5903446f7670f194b39d485bc692f1 MD5 | raw file
Possible License(s): JSON, LGPL-2.1, AGPL-1.0, MPL-2.0-no-copyleft-exception, MPL-2.0, BSD-3-Clause, LGPL-3.0, BSD-2-Clause, MIT, Apache-2.0, GPL-2.0, 0BSD
  1. /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
  2. /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
  3. /* This Source Code Form is subject to the terms of the Mozilla Public
  4. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  5. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  6. "use strict";
  7. dump("###################################### forms.js loaded\n");
  8. let Ci = Components.interfaces;
  9. let Cc = Components.classes;
  10. let Cu = Components.utils;
  11. Cu.import("resource://gre/modules/Services.jsm");
  12. Cu.import('resource://gre/modules/XPCOMUtils.jsm');
  13. XPCOMUtils.defineLazyServiceGetter(Services, "fm",
  14. "@mozilla.org/focus-manager;1",
  15. "nsIFocusManager");
  16. XPCOMUtils.defineLazyGetter(this, "domWindowUtils", function () {
  17. return content.QueryInterface(Ci.nsIInterfaceRequestor)
  18. .getInterface(Ci.nsIDOMWindowUtils);
  19. });
  20. const RESIZE_SCROLL_DELAY = 20;
  21. let HTMLDocument = Ci.nsIDOMHTMLDocument;
  22. let HTMLHtmlElement = Ci.nsIDOMHTMLHtmlElement;
  23. let HTMLBodyElement = Ci.nsIDOMHTMLBodyElement;
  24. let HTMLIFrameElement = Ci.nsIDOMHTMLIFrameElement;
  25. let HTMLInputElement = Ci.nsIDOMHTMLInputElement;
  26. let HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement;
  27. let HTMLSelectElement = Ci.nsIDOMHTMLSelectElement;
  28. let HTMLOptGroupElement = Ci.nsIDOMHTMLOptGroupElement;
  29. let HTMLOptionElement = Ci.nsIDOMHTMLOptionElement;
  30. let FormVisibility = {
  31. /**
  32. * Searches upwards in the DOM for an element that has been scrolled.
  33. *
  34. * @param {HTMLElement} node element to start search at.
  35. * @return {Window|HTMLElement|Null} null when none are found window/element otherwise.
  36. */
  37. findScrolled: function fv_findScrolled(node) {
  38. let win = node.ownerDocument.defaultView;
  39. while (!(node instanceof HTMLBodyElement)) {
  40. // We can skip elements that have not been scrolled.
  41. // We only care about top now remember to add the scrollLeft
  42. // check if we decide to care about the X axis.
  43. if (node.scrollTop !== 0) {
  44. // the element has been scrolled so we may need to adjust
  45. // where we think the root element is located.
  46. //
  47. // Otherwise it may seem visible but be scrolled out of the viewport
  48. // inside this scrollable node.
  49. return node;
  50. } else {
  51. // this node does not effect where we think
  52. // the node is even if it is scrollable it has not hidden
  53. // the element we are looking for.
  54. node = node.parentNode;
  55. continue;
  56. }
  57. }
  58. // we also care about the window this is the more
  59. // common case where the content is larger then
  60. // the viewport/screen.
  61. if (win.scrollMaxX || win.scrollMaxY) {
  62. return win;
  63. }
  64. return null;
  65. },
  66. /**
  67. * Checks if "top and "bottom" points of the position is visible.
  68. *
  69. * @param {Number} top position.
  70. * @param {Number} height of the element.
  71. * @param {Number} maxHeight of the window.
  72. * @return {Boolean} true when visible.
  73. */
  74. yAxisVisible: function fv_yAxisVisible(top, height, maxHeight) {
  75. return (top > 0 && (top + height) < maxHeight);
  76. },
  77. /**
  78. * Searches up through the dom for scrollable elements
  79. * which are not currently visible (relative to the viewport).
  80. *
  81. * @param {HTMLElement} element to start search at.
  82. * @param {Object} pos .top, .height and .width of element.
  83. */
  84. scrollablesVisible: function fv_scrollablesVisible(element, pos) {
  85. while ((element = this.findScrolled(element))) {
  86. if (element.window && element.self === element)
  87. break;
  88. // remember getBoundingClientRect does not care
  89. // about scrolling only where the element starts
  90. // in the document.
  91. let offset = element.getBoundingClientRect();
  92. // the top of both the scrollable area and
  93. // the form element itself are in the same document.
  94. // We adjust the "top" so if the elements coordinates
  95. // are relative to the viewport in the current document.
  96. let adjustedTop = pos.top - offset.top;
  97. let visible = this.yAxisVisible(
  98. adjustedTop,
  99. pos.height,
  100. pos.width
  101. );
  102. if (!visible)
  103. return false;
  104. element = element.parentNode;
  105. }
  106. return true;
  107. },
  108. /**
  109. * Verifies the element is visible in the viewport.
  110. * Handles scrollable areas, frames and scrollable viewport(s) (windows).
  111. *
  112. * @param {HTMLElement} element to verify.
  113. * @return {Boolean} true when visible.
  114. */
  115. isVisible: function fv_isVisible(element) {
  116. // scrollable frames can be ignored we just care about iframes...
  117. let rect = element.getBoundingClientRect();
  118. let parent = element.ownerDocument.defaultView;
  119. // used to calculate the inner position of frames / scrollables.
  120. // The intent was to use this information to scroll either up or down.
  121. // scrollIntoView(true) will _break_ some web content so we can't do
  122. // this today. If we want that functionality we need to manually scroll
  123. // the individual elements.
  124. let pos = {
  125. top: rect.top,
  126. height: rect.height,
  127. width: rect.width
  128. };
  129. let visible = true;
  130. do {
  131. let frame = parent.frameElement;
  132. visible = visible &&
  133. this.yAxisVisible(pos.top, pos.height, parent.innerHeight) &&
  134. this.scrollablesVisible(element, pos);
  135. // nothing we can do about this now...
  136. // In the future we can use this information to scroll
  137. // only the elements we need to at this point as we should
  138. // have all the details we need to figure out how to scroll.
  139. if (!visible)
  140. return false;
  141. if (frame) {
  142. let frameRect = frame.getBoundingClientRect();
  143. pos.top += frameRect.top + frame.clientTop;
  144. }
  145. } while (
  146. (parent !== parent.parent) &&
  147. (parent = parent.parent)
  148. );
  149. return visible;
  150. }
  151. };
  152. let FormAssistant = {
  153. init: function fa_init() {
  154. addEventListener("focus", this, true, false);
  155. addEventListener("blur", this, true, false);
  156. addEventListener("resize", this, true, false);
  157. addEventListener("submit", this, true, false);
  158. addEventListener("pagehide", this, true, false);
  159. addMessageListener("Forms:Select:Choice", this);
  160. addMessageListener("Forms:Input:Value", this);
  161. addMessageListener("Forms:Select:Blur", this);
  162. },
  163. ignoredInputTypes: new Set([
  164. 'button', 'file', 'checkbox', 'radio', 'reset', 'submit', 'image'
  165. ]),
  166. isKeyboardOpened: false,
  167. selectionStart: 0,
  168. selectionEnd: 0,
  169. scrollIntoViewTimeout: null,
  170. _focusedElement: null,
  171. get focusedElement() {
  172. if (this._focusedElement && Cu.isDeadWrapper(this._focusedElement))
  173. this._focusedElement = null;
  174. return this._focusedElement;
  175. },
  176. set focusedElement(val) {
  177. this._focusedElement = val;
  178. },
  179. setFocusedElement: function fa_setFocusedElement(element) {
  180. if (element === this.focusedElement)
  181. return;
  182. if (this.focusedElement) {
  183. this.focusedElement.removeEventListener('mousedown', this);
  184. this.focusedElement.removeEventListener('mouseup', this);
  185. if (!element) {
  186. this.focusedElement.blur();
  187. }
  188. }
  189. if (element) {
  190. element.addEventListener('mousedown', this);
  191. element.addEventListener('mouseup', this);
  192. }
  193. this.focusedElement = element;
  194. },
  195. handleEvent: function fa_handleEvent(evt) {
  196. let focusedElement = this.focusedElement;
  197. let target = evt.target;
  198. switch (evt.type) {
  199. case "focus":
  200. if (target && isContentEditable(target)) {
  201. this.showKeyboard(this.getTopLevelEditable(target));
  202. break;
  203. }
  204. if (target && this.isFocusableElement(target))
  205. this.showKeyboard(target);
  206. break;
  207. case "blur":
  208. case "submit":
  209. case "pagehide":
  210. if (this.focusedElement)
  211. this.hideKeyboard();
  212. break;
  213. case 'mousedown':
  214. // We only listen for this event on the currently focused element.
  215. // When the mouse goes down, note the cursor/selection position
  216. this.selectionStart = this.focusedElement.selectionStart;
  217. this.selectionEnd = this.focusedElement.selectionEnd;
  218. break;
  219. case 'mouseup':
  220. // We only listen for this event on the currently focused element.
  221. // When the mouse goes up, see if the cursor has moved (or the
  222. // selection changed) since the mouse went down. If it has, we
  223. // need to tell the keyboard about it
  224. if (this.focusedElement.selectionStart !== this.selectionStart ||
  225. this.focusedElement.selectionEnd !== this.selectionEnd) {
  226. this.sendKeyboardState(this.focusedElement);
  227. }
  228. break;
  229. case "resize":
  230. if (!this.isKeyboardOpened)
  231. return;
  232. if (this.scrollIntoViewTimeout) {
  233. content.clearTimeout(this.scrollIntoViewTimeout);
  234. this.scrollIntoViewTimeout = null;
  235. }
  236. // We may receive multiple resize events in quick succession, so wait
  237. // a bit before scrolling the input element into view.
  238. if (this.focusedElement) {
  239. this.scrollIntoViewTimeout = content.setTimeout(function () {
  240. this.scrollIntoViewTimeout = null;
  241. if (this.focusedElement && !FormVisibility.isVisible(this.focusedElement)) {
  242. this.focusedElement.scrollIntoView(false);
  243. }
  244. }.bind(this), RESIZE_SCROLL_DELAY);
  245. }
  246. break;
  247. }
  248. },
  249. receiveMessage: function fa_receiveMessage(msg) {
  250. let target = this.focusedElement;
  251. if (!target) {
  252. return;
  253. }
  254. let json = msg.json;
  255. switch (msg.name) {
  256. case "Forms:Input:Value": {
  257. target.value = json.value;
  258. let event = content.document.createEvent('HTMLEvents');
  259. event.initEvent('input', true, false);
  260. target.dispatchEvent(event);
  261. break;
  262. }
  263. case "Forms:Select:Choice":
  264. let options = target.options;
  265. let valueChanged = false;
  266. if ("index" in json) {
  267. if (options.selectedIndex != json.index) {
  268. options.selectedIndex = json.index;
  269. valueChanged = true;
  270. }
  271. } else if ("indexes" in json) {
  272. for (let i = 0; i < options.length; i++) {
  273. let newValue = (json.indexes.indexOf(i) != -1);
  274. if (options.item(i).selected != newValue) {
  275. options.item(i).selected = newValue;
  276. valueChanged = true;
  277. }
  278. }
  279. }
  280. // only fire onchange event if any selected option is changed
  281. if (valueChanged) {
  282. let event = content.document.createEvent('HTMLEvents');
  283. event.initEvent('change', true, true);
  284. target.dispatchEvent(event);
  285. }
  286. break;
  287. case "Forms:Select:Blur": {
  288. this.setFocusedElement(null);
  289. break;
  290. }
  291. }
  292. },
  293. showKeyboard: function fa_showKeyboard(target) {
  294. if (this.isKeyboardOpened)
  295. return;
  296. if (target instanceof HTMLOptionElement)
  297. target = target.parentNode;
  298. let kbOpened = this.sendKeyboardState(target);
  299. if (this.isTextInputElement(target))
  300. this.isKeyboardOpened = kbOpened;
  301. this.setFocusedElement(target);
  302. },
  303. hideKeyboard: function fa_hideKeyboard() {
  304. sendAsyncMessage("Forms:Input", { "type": "blur" });
  305. this.isKeyboardOpened = false;
  306. this.setFocusedElement(null);
  307. },
  308. isFocusableElement: function fa_isFocusableElement(element) {
  309. if (element instanceof HTMLSelectElement ||
  310. element instanceof HTMLTextAreaElement)
  311. return true;
  312. if (element instanceof HTMLOptionElement &&
  313. element.parentNode instanceof HTMLSelectElement)
  314. return true;
  315. return (element instanceof HTMLInputElement &&
  316. !this.ignoredInputTypes.has(element.type));
  317. },
  318. isTextInputElement: function fa_isTextInputElement(element) {
  319. return element instanceof HTMLInputElement ||
  320. element instanceof HTMLTextAreaElement ||
  321. isContentEditable(element);
  322. },
  323. getTopLevelEditable: function fa_getTopLevelEditable(element) {
  324. function retrieveTopLevelEditable(element) {
  325. // Retrieve the top element that is editable
  326. if (element instanceof HTMLHtmlElement)
  327. element = element.ownerDocument.body;
  328. else if (element instanceof HTMLDocument)
  329. element = element.body;
  330. while (element && !isContentEditable(element))
  331. element = element.parentNode;
  332. // Return the container frame if we are into a nested editable frame
  333. if (element &&
  334. element instanceof HTMLBodyElement &&
  335. element.ownerDocument.defaultView != content.document.defaultView)
  336. return element.ownerDocument.defaultView.frameElement;
  337. }
  338. if (element instanceof HTMLIFrameElement)
  339. return element;
  340. return retrieveTopLevelEditable(element) || element;
  341. },
  342. sendKeyboardState: function(element) {
  343. // FIXME/bug 729623: work around apparent bug in the IME manager
  344. // in gecko.
  345. let readonly = element.getAttribute("readonly");
  346. if (readonly) {
  347. return false;
  348. }
  349. sendAsyncMessage("Forms:Input", getJSON(element));
  350. return true;
  351. }
  352. };
  353. FormAssistant.init();
  354. function isContentEditable(element) {
  355. if (element.isContentEditable || element.designMode == "on")
  356. return true;
  357. // If a body element is editable and the body is the child of an
  358. // iframe we can assume this is an advanced HTML editor
  359. if (element instanceof HTMLIFrameElement &&
  360. element.contentDocument &&
  361. (element.contentDocument.body.isContentEditable ||
  362. element.contentDocument.designMode == "on"))
  363. return true;
  364. return element.ownerDocument && element.ownerDocument.designMode == "on";
  365. }
  366. function getJSON(element) {
  367. let type = element.type || "";
  368. let value = element.value || ""
  369. // Treat contenteditble element as a special text field
  370. if (isContentEditable(element)) {
  371. type = "text";
  372. value = element.textContent;
  373. }
  374. // Until the input type=date/datetime/range have been implemented
  375. // let's return their real type even if the platform returns 'text'
  376. let attributeType = element.getAttribute("type") || "";
  377. if (attributeType) {
  378. var typeLowerCase = attributeType.toLowerCase();
  379. switch (typeLowerCase) {
  380. case "datetime":
  381. case "datetime-local":
  382. case "range":
  383. type = typeLowerCase;
  384. break;
  385. }
  386. }
  387. // Gecko has some support for @inputmode but behind a preference and
  388. // it is disabled by default.
  389. // Gaia is then using @x-inputmode has its proprietary way to set
  390. // inputmode for fields. This shouldn't be used outside of pre-installed
  391. // apps because the attribute is going to disappear as soon as a definitive
  392. // solution will be find.
  393. let inputmode = element.getAttribute('x-inputmode');
  394. if (inputmode) {
  395. inputmode = inputmode.toLowerCase();
  396. } else {
  397. inputmode = '';
  398. }
  399. return {
  400. "type": type.toLowerCase(),
  401. "choices": getListForElement(element),
  402. "value": value,
  403. "inputmode": inputmode,
  404. "selectionStart": element.selectionStart,
  405. "selectionEnd": element.selectionEnd
  406. };
  407. }
  408. function getListForElement(element) {
  409. if (!(element instanceof HTMLSelectElement))
  410. return null;
  411. let optionIndex = 0;
  412. let result = {
  413. "multiple": element.multiple,
  414. "choices": []
  415. };
  416. // Build up a flat JSON array of the choices.
  417. // In HTML, it's possible for select element choices to be under a
  418. // group header (but not recursively). We distinguish between headers
  419. // and entries using the boolean "list.group".
  420. let children = element.children;
  421. for (let i = 0; i < children.length; i++) {
  422. let child = children[i];
  423. if (child instanceof HTMLOptGroupElement) {
  424. result.choices.push({
  425. "group": true,
  426. "text": child.label || child.firstChild.data,
  427. "disabled": child.disabled
  428. });
  429. let subchildren = child.children;
  430. for (let j = 0; j < subchildren.length; j++) {
  431. let subchild = subchildren[j];
  432. result.choices.push({
  433. "group": false,
  434. "inGroup": true,
  435. "text": subchild.text,
  436. "disabled": child.disabled || subchild.disabled,
  437. "selected": subchild.selected,
  438. "optionIndex": optionIndex++
  439. });
  440. }
  441. } else if (child instanceof HTMLOptionElement) {
  442. result.choices.push({
  443. "group": false,
  444. "inGroup": false,
  445. "text": child.text,
  446. "disabled": child.disabled,
  447. "selected": child.selected,
  448. "optionIndex": optionIndex++
  449. });
  450. }
  451. }
  452. return result;
  453. };