browser/metro/base/content/contenthandlers/FormHelper.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 // -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
michael@0 2 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 3 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 5
michael@0 6 let Ci = Components.interfaces;
michael@0 7 let Cc = Components.classes;
michael@0 8
michael@0 9 dump("### FormHelper.js loaded\n");
michael@0 10
michael@0 11 let HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement;
michael@0 12 let HTMLInputElement = Ci.nsIDOMHTMLInputElement;
michael@0 13 let HTMLSelectElement = Ci.nsIDOMHTMLSelectElement;
michael@0 14 let HTMLIFrameElement = Ci.nsIDOMHTMLIFrameElement;
michael@0 15 let HTMLDocument = Ci.nsIDOMHTMLDocument;
michael@0 16 let HTMLHtmlElement = Ci.nsIDOMHTMLHtmlElement;
michael@0 17 let HTMLBodyElement = Ci.nsIDOMHTMLBodyElement;
michael@0 18 let HTMLLabelElement = Ci.nsIDOMHTMLLabelElement;
michael@0 19 let HTMLButtonElement = Ci.nsIDOMHTMLButtonElement;
michael@0 20 let HTMLOptGroupElement = Ci.nsIDOMHTMLOptGroupElement;
michael@0 21 let HTMLOptionElement = Ci.nsIDOMHTMLOptionElement;
michael@0 22 let XULMenuListElement = Ci.nsIDOMXULMenuListElement;
michael@0 23
michael@0 24 /**
michael@0 25 * Responsible of navigation between forms fields and of the opening of the assistant
michael@0 26 */
michael@0 27 function FormAssistant() {
michael@0 28 addMessageListener("FormAssist:Closed", this);
michael@0 29 addMessageListener("FormAssist:ChoiceSelect", this);
michael@0 30 addMessageListener("FormAssist:ChoiceChange", this);
michael@0 31 addMessageListener("FormAssist:AutoComplete", this);
michael@0 32 addMessageListener("FormAssist:Update", this);
michael@0 33
michael@0 34 /* Listen text events in order to update the autocomplete suggestions as soon
michael@0 35 * a key is entered on device
michael@0 36 */
michael@0 37 addEventListener("text", this, false);
michael@0 38 addEventListener("focus", this, true);
michael@0 39 addEventListener("blur", this, true);
michael@0 40 addEventListener("pageshow", this, false);
michael@0 41 addEventListener("pagehide", this, false);
michael@0 42 addEventListener("submit", this, false);
michael@0 43 }
michael@0 44
michael@0 45 FormAssistant.prototype = {
michael@0 46 _els: Cc["@mozilla.org/eventlistenerservice;1"].getService(Ci.nsIEventListenerService),
michael@0 47 _open: false,
michael@0 48 _focusSync: false,
michael@0 49 _debugEvents: false,
michael@0 50 _selectWrapper: null,
michael@0 51 _currentElement: null,
michael@0 52 invalidSubmit: false,
michael@0 53
michael@0 54 get focusSync() {
michael@0 55 return this._focusSync;
michael@0 56 },
michael@0 57
michael@0 58 set focusSync(aVal) {
michael@0 59 this._focusSync = aVal;
michael@0 60 },
michael@0 61
michael@0 62 get currentElement() {
michael@0 63 return this._currentElement;
michael@0 64 },
michael@0 65
michael@0 66 set currentElement(aElement) {
michael@0 67 if (!aElement || !this._isVisibleElement(aElement)) {
michael@0 68 return null;
michael@0 69 }
michael@0 70
michael@0 71 this._currentElement = aElement;
michael@0 72 gFocusManager.setFocus(this._currentElement, Ci.nsIFocusManager.FLAG_NOSCROLL);
michael@0 73
michael@0 74 // To ensure we get the current caret positionning of the focused
michael@0 75 // element we need to delayed a bit the event
michael@0 76 this._executeDelayed(function(self) {
michael@0 77 // Bug 640870
michael@0 78 // Sometimes the element inner frame get destroyed while the element
michael@0 79 // receive the focus because the display is turned to 'none' for
michael@0 80 // example, in this "fun" case just do nothing if the element is hidden
michael@0 81 if (self._isVisibleElement(gFocusManager.focusedElement)) {
michael@0 82 self._sendJsonMsgWrapper("FormAssist:Show");
michael@0 83 }
michael@0 84 });
michael@0 85 return this._currentElement;
michael@0 86 },
michael@0 87
michael@0 88 open: function formHelperOpen(aElement, aEvent) {
michael@0 89 // If the click is on an option element we want to check if the parent
michael@0 90 // is a valid target.
michael@0 91 if (aElement instanceof HTMLOptionElement &&
michael@0 92 aElement.parentNode instanceof HTMLSelectElement &&
michael@0 93 !aElement.disabled) {
michael@0 94 aElement = aElement.parentNode;
michael@0 95 }
michael@0 96
michael@0 97 // Don't show the formhelper popup for multi-select boxes, except for touch.
michael@0 98 if (aElement instanceof HTMLSelectElement && aEvent) {
michael@0 99 if ((aElement.multiple || aElement.size > 1) &&
michael@0 100 aEvent.mozInputSource != Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH) {
michael@0 101 return false;
michael@0 102 }
michael@0 103 // Don't fire mouse events on selects; see bug 685197.
michael@0 104 aEvent.preventDefault();
michael@0 105 aEvent.stopPropagation();
michael@0 106 }
michael@0 107
michael@0 108 // The form assistant will close if a click happen:
michael@0 109 // * outside of the scope of the form helper
michael@0 110 // * hover a button of type=[image|submit]
michael@0 111 // * hover a disabled element
michael@0 112 if (!this._isValidElement(aElement)) {
michael@0 113 let passiveButtons = { button: true, checkbox: true, file: true, radio: true, reset: true };
michael@0 114 if ((aElement instanceof HTMLInputElement || aElement instanceof HTMLButtonElement) &&
michael@0 115 passiveButtons[aElement.type] && !aElement.disabled)
michael@0 116 return false;
michael@0 117 return this.close();
michael@0 118 }
michael@0 119
michael@0 120 // Look for a top editable element
michael@0 121 if (this._isEditable(aElement)) {
michael@0 122 aElement = this._getTopLevelEditable(aElement);
michael@0 123 }
michael@0 124
michael@0 125 // We only work with choice lists or elements with autocomplete suggestions
michael@0 126 if (!this._isSelectElement(aElement) &&
michael@0 127 !this._isAutocomplete(aElement)) {
michael@0 128 return this.close();
michael@0 129 }
michael@0 130
michael@0 131 // Don't re-open when navigating to avoid repopulating list when changing selection.
michael@0 132 if (this._isAutocomplete(aElement) && this._open && Util.isNavigationKey(aEvent.keyCode)) {
michael@0 133 return false;
michael@0 134 }
michael@0 135
michael@0 136 // Enable the assistant
michael@0 137 this.currentElement = aElement;
michael@0 138 return this._open = true;
michael@0 139 },
michael@0 140
michael@0 141 close: function close() {
michael@0 142 if (this._open) {
michael@0 143 this._currentElement = null;
michael@0 144 sendAsyncMessage("FormAssist:Hide", { });
michael@0 145 this._open = false;
michael@0 146 }
michael@0 147
michael@0 148 return this._open;
michael@0 149 },
michael@0 150
michael@0 151 receiveMessage: function receiveMessage(aMessage) {
michael@0 152 if (this._debugEvents) Util.dumpLn(aMessage.name);
michael@0 153
michael@0 154 let currentElement = this.currentElement;
michael@0 155 if ((!this._isAutocomplete(currentElement) &&
michael@0 156 !getWrapperForElement(currentElement)) ||
michael@0 157 !currentElement) {
michael@0 158 return;
michael@0 159 }
michael@0 160
michael@0 161 let json = aMessage.json;
michael@0 162
michael@0 163 switch (aMessage.name) {
michael@0 164 case "FormAssist:ChoiceSelect": {
michael@0 165 this._selectWrapper = getWrapperForElement(currentElement);
michael@0 166 this._selectWrapper.select(json.index, json.selected);
michael@0 167 break;
michael@0 168 }
michael@0 169
michael@0 170 case "FormAssist:ChoiceChange": {
michael@0 171 // ChoiceChange could happened once we have move to another element or
michael@0 172 // to nothing, so we should keep the used wrapper in mind.
michael@0 173 this._selectWrapper = getWrapperForElement(currentElement);
michael@0 174 this._selectWrapper.fireOnChange();
michael@0 175
michael@0 176 // New elements can be shown when a select is updated so we need to
michael@0 177 // reconstruct the inner elements array and to take care of possible
michael@0 178 // focus change, this is why we use "self.currentElement" instead of
michael@0 179 // using directly "currentElement".
michael@0 180 this._executeDelayed(function(self) {
michael@0 181 let currentElement = self.currentElement;
michael@0 182 if (!currentElement)
michael@0 183 return;
michael@0 184 self._currentElement = currentElement;
michael@0 185 });
michael@0 186 break;
michael@0 187 }
michael@0 188
michael@0 189 case "FormAssist:AutoComplete": {
michael@0 190 try {
michael@0 191 currentElement = currentElement.QueryInterface(Ci.nsIDOMNSEditableElement);
michael@0 192 let imeEditor = currentElement.editor.QueryInterface(Ci.nsIEditorIMESupport);
michael@0 193 if (imeEditor.composing)
michael@0 194 imeEditor.forceCompositionEnd();
michael@0 195 }
michael@0 196 catch(e) {}
michael@0 197
michael@0 198 currentElement.value = json.value;
michael@0 199
michael@0 200 let event = currentElement.ownerDocument.createEvent("Events");
michael@0 201 event.initEvent("DOMAutoComplete", true, true);
michael@0 202 currentElement.dispatchEvent(event);
michael@0 203 break;
michael@0 204 }
michael@0 205
michael@0 206 case "FormAssist:Closed":
michael@0 207 currentElement.blur();
michael@0 208 this._open = false;
michael@0 209 break;
michael@0 210
michael@0 211 case "FormAssist:Update":
michael@0 212 this._sendJsonMsgWrapper("FormAssist:Show");
michael@0 213 break;
michael@0 214 }
michael@0 215 },
michael@0 216
michael@0 217 handleEvent: function formHelperHandleEvent(aEvent) {
michael@0 218 if (this._debugEvents) Util.dumpLn(aEvent.type, this.currentElement);
michael@0 219 // focus changes should be taken into account only if the user has done a
michael@0 220 // manual operation like manually clicking
michael@0 221 let shouldIgnoreFocus = (aEvent.type == "focus" && !this._open && !this.focusSync);
michael@0 222 if ((!this._open && aEvent.type != "focus") || shouldIgnoreFocus) {
michael@0 223 return;
michael@0 224 }
michael@0 225
michael@0 226 let currentElement = this.currentElement;
michael@0 227 switch (aEvent.type) {
michael@0 228 case "submit":
michael@0 229 // submit is a final action and the form assistant should be closed
michael@0 230 this.close();
michael@0 231 break;
michael@0 232
michael@0 233 case "pagehide":
michael@0 234 case "pageshow":
michael@0 235 // When reacting to a page show/hide, if the focus is different this
michael@0 236 // could mean the web page has dramatically changed because of
michael@0 237 // an Ajax change based on fragment identifier
michael@0 238 if (gFocusManager.focusedElement != currentElement)
michael@0 239 this.close();
michael@0 240 break;
michael@0 241
michael@0 242 case "focus":
michael@0 243 let focusedElement =
michael@0 244 gFocusManager.getFocusedElementForWindow(content, true, {}) ||
michael@0 245 aEvent.target;
michael@0 246
michael@0 247 // If a body element is editable and the body is the child of an
michael@0 248 // iframe we can assume this is an advanced HTML editor, so let's
michael@0 249 // redirect the form helper selection to the iframe element
michael@0 250 if (focusedElement && this._isEditable(focusedElement)) {
michael@0 251 let editableElement = this._getTopLevelEditable(focusedElement);
michael@0 252 if (this._isValidElement(editableElement)) {
michael@0 253 this._executeDelayed(function(self) {
michael@0 254 self.open(editableElement);
michael@0 255 });
michael@0 256 }
michael@0 257 return;
michael@0 258 }
michael@0 259
michael@0 260 // if an element is focused while we're closed but the element can be handle
michael@0 261 // by the assistant, try to activate it (only during mouseup)
michael@0 262 if (!currentElement) {
michael@0 263 if (focusedElement && this._isValidElement(focusedElement)) {
michael@0 264 this._executeDelayed(function(self) {
michael@0 265 self.open(focusedElement);
michael@0 266 });
michael@0 267 }
michael@0 268 return;
michael@0 269 }
michael@0 270
michael@0 271 if (this._currentElement != focusedElement)
michael@0 272 this.currentElement = focusedElement;
michael@0 273 break;
michael@0 274
michael@0 275 case "blur":
michael@0 276 content.setTimeout(function(self) {
michael@0 277 if (!self._open)
michael@0 278 return;
michael@0 279
michael@0 280 // If the blurring causes focus be in no other element,
michael@0 281 // we should close the form assistant.
michael@0 282 let focusedElement = gFocusManager.getFocusedElementForWindow(content, true, {});
michael@0 283 if (!focusedElement)
michael@0 284 self.close();
michael@0 285 }, 0, this);
michael@0 286 break;
michael@0 287
michael@0 288 case "text":
michael@0 289 if (this._isAutocomplete(aEvent.target)) {
michael@0 290 this._sendJsonMsgWrapper("FormAssist:AutoComplete");
michael@0 291 }
michael@0 292 break;
michael@0 293 }
michael@0 294 },
michael@0 295
michael@0 296 _executeDelayed: function formHelperExecuteSoon(aCallback) {
michael@0 297 let self = this;
michael@0 298 let timer = new Util.Timeout(function() {
michael@0 299 aCallback(self);
michael@0 300 });
michael@0 301 timer.once(0);
michael@0 302 },
michael@0 303
michael@0 304 _isEditable: function formHelperIsEditable(aElement) {
michael@0 305 if (!aElement)
michael@0 306 return false;
michael@0 307 let canEdit = false;
michael@0 308
michael@0 309 if (aElement.isContentEditable || aElement.designMode == "on") {
michael@0 310 canEdit = true;
michael@0 311 } else if (aElement instanceof HTMLIFrameElement &&
michael@0 312 (aElement.contentDocument.body.isContentEditable ||
michael@0 313 aElement.contentDocument.designMode == "on")) {
michael@0 314 canEdit = true;
michael@0 315 } else {
michael@0 316 canEdit = aElement.ownerDocument && aElement.ownerDocument.designMode == "on";
michael@0 317 }
michael@0 318
michael@0 319 return canEdit;
michael@0 320 },
michael@0 321
michael@0 322 _getTopLevelEditable: function formHelperGetTopLevelEditable(aElement) {
michael@0 323 if (!(aElement instanceof HTMLIFrameElement)) {
michael@0 324 let element = aElement;
michael@0 325
michael@0 326 // Retrieve the top element that is editable
michael@0 327 if (element instanceof HTMLHtmlElement)
michael@0 328 element = element.ownerDocument.body;
michael@0 329 else if (element instanceof HTMLDocument)
michael@0 330 element = element.body;
michael@0 331
michael@0 332 while (element && !this._isEditable(element))
michael@0 333 element = element.parentNode;
michael@0 334
michael@0 335 // Return the container frame if we are into a nested editable frame
michael@0 336 if (element && element instanceof HTMLBodyElement && element.ownerDocument.defaultView != content.document.defaultView)
michael@0 337 return element.ownerDocument.defaultView.frameElement;
michael@0 338 }
michael@0 339
michael@0 340 return aElement;
michael@0 341 },
michael@0 342
michael@0 343 _isAutocomplete: function formHelperIsAutocomplete(aElement) {
michael@0 344 if (aElement instanceof HTMLInputElement) {
michael@0 345 if (aElement.getAttribute("type") == "password")
michael@0 346 return false;
michael@0 347
michael@0 348 let autocomplete = aElement.getAttribute("autocomplete");
michael@0 349 let allowedValues = ["off", "false", "disabled"];
michael@0 350 if (allowedValues.indexOf(autocomplete) == -1)
michael@0 351 return true;
michael@0 352 }
michael@0 353
michael@0 354 return false;
michael@0 355 },
michael@0 356
michael@0 357 /*
michael@0 358 * This function is similar to getListSuggestions from
michael@0 359 * components/satchel/src/nsInputListAutoComplete.js but sadly this one is
michael@0 360 * used by the autocomplete.xml binding which is not in used in fennec
michael@0 361 */
michael@0 362 _getListSuggestions: function formHelperGetListSuggestions(aElement) {
michael@0 363 if (!(aElement instanceof HTMLInputElement) || !aElement.list)
michael@0 364 return [];
michael@0 365
michael@0 366 let suggestions = [];
michael@0 367 let filter = !aElement.hasAttribute("mozNoFilter");
michael@0 368 let lowerFieldValue = aElement.value.toLowerCase();
michael@0 369
michael@0 370 let options = aElement.list.options;
michael@0 371 let length = options.length;
michael@0 372 for (let i = 0; i < length; i++) {
michael@0 373 let item = options.item(i);
michael@0 374
michael@0 375 let label = item.value;
michael@0 376 if (item.label)
michael@0 377 label = item.label;
michael@0 378 else if (item.text)
michael@0 379 label = item.text;
michael@0 380
michael@0 381 if (filter && label.toLowerCase().indexOf(lowerFieldValue) == -1)
michael@0 382 continue;
michael@0 383 suggestions.push({ label: label, value: item.value });
michael@0 384 }
michael@0 385
michael@0 386 return suggestions;
michael@0 387 },
michael@0 388
michael@0 389 _isValidElement: function formHelperIsValidElement(aElement) {
michael@0 390 if (!aElement.getAttribute)
michael@0 391 return false;
michael@0 392
michael@0 393 let formExceptions = { button: true, checkbox: true, file: true, image: true, radio: true, reset: true, submit: true };
michael@0 394 if (aElement instanceof HTMLInputElement && formExceptions[aElement.type])
michael@0 395 return false;
michael@0 396
michael@0 397 if (aElement instanceof HTMLButtonElement ||
michael@0 398 (aElement.getAttribute("role") == "button" && aElement.hasAttribute("tabindex")))
michael@0 399 return false;
michael@0 400
michael@0 401 return this._isNavigableElement(aElement) && this._isVisibleElement(aElement);
michael@0 402 },
michael@0 403
michael@0 404 _isNavigableElement: function formHelperIsNavigableElement(aElement) {
michael@0 405 if (aElement.disabled || aElement.getAttribute("tabindex") == "-1")
michael@0 406 return false;
michael@0 407
michael@0 408 if (aElement.getAttribute("role") == "button" && aElement.hasAttribute("tabindex"))
michael@0 409 return true;
michael@0 410
michael@0 411 if (this._isSelectElement(aElement) || aElement instanceof HTMLTextAreaElement)
michael@0 412 return true;
michael@0 413
michael@0 414 if (aElement instanceof HTMLInputElement || aElement instanceof HTMLButtonElement)
michael@0 415 return !(aElement.type == "hidden");
michael@0 416
michael@0 417 return this._isEditable(aElement);
michael@0 418 },
michael@0 419
michael@0 420 _isVisibleElement: function formHelperIsVisibleElement(aElement) {
michael@0 421 if (!aElement || !aElement.ownerDocument) {
michael@0 422 return false;
michael@0 423 }
michael@0 424 let style = aElement.ownerDocument.defaultView.getComputedStyle(aElement, null);
michael@0 425 if (!style)
michael@0 426 return false;
michael@0 427
michael@0 428 let isVisible = (style.getPropertyValue("visibility") != "hidden");
michael@0 429 let isOpaque = (style.getPropertyValue("opacity") != 0);
michael@0 430
michael@0 431 let rect = aElement.getBoundingClientRect();
michael@0 432
michael@0 433 // Since the only way to show a drop-down menu for a select when the form
michael@0 434 // assistant is enabled is to return true here, a select is allowed to have
michael@0 435 // an opacity to 0 in order to let web developpers add a custom design on
michael@0 436 // top of it. This is less important to use the form assistant for the
michael@0 437 // other types of fields because even if the form assistant won't fired,
michael@0 438 // the focus will be in and a VKB will popup if needed
michael@0 439 return isVisible && (isOpaque || this._isSelectElement(aElement)) && (rect.height != 0 || rect.width != 0);
michael@0 440 },
michael@0 441
michael@0 442 _isSelectElement: function formHelperIsSelectElement(aElement) {
michael@0 443 return (aElement instanceof HTMLSelectElement || aElement instanceof XULMenuListElement);
michael@0 444 },
michael@0 445
michael@0 446 /** Caret is used to input text for this element. */
michael@0 447 _getCaretRect: function _formHelperGetCaretRect() {
michael@0 448 let element = this.currentElement;
michael@0 449 let focusedElement = gFocusManager.getFocusedElementForWindow(content, true, {});
michael@0 450 if (element && (element.mozIsTextField && element.mozIsTextField(false) ||
michael@0 451 element instanceof HTMLTextAreaElement) && focusedElement == element && this._isVisibleElement(element)) {
michael@0 452 let utils = Util.getWindowUtils(element.ownerDocument.defaultView);
michael@0 453 let rect = utils.sendQueryContentEvent(utils.QUERY_CARET_RECT, element.selectionEnd, 0, 0, 0,
michael@0 454 utils.QUERY_CONTENT_FLAG_USE_XP_LINE_BREAK);
michael@0 455 if (rect) {
michael@0 456 let scroll = ContentScroll.getScrollOffset(element.ownerDocument.defaultView);
michael@0 457 return new Rect(scroll.x + rect.left, scroll.y + rect.top, rect.width, rect.height);
michael@0 458 }
michael@0 459 }
michael@0 460
michael@0 461 return new Rect(0, 0, 0, 0);
michael@0 462 },
michael@0 463
michael@0 464 /** Gets a rect bounding important parts of the element that must be seen when assisting. */
michael@0 465 _getRect: function _formHelperGetRect(aOptions={}) {
michael@0 466 const kDistanceMax = 100;
michael@0 467 let element = this.currentElement;
michael@0 468 let elRect = getBoundingContentRect(element);
michael@0 469
michael@0 470 if (aOptions.alignToLabel) {
michael@0 471 let labels = this._getLabels();
michael@0 472 for (let i=0; i<labels.length; i++) {
michael@0 473 let labelRect = labels[i].rect;
michael@0 474 if (labelRect.left < elRect.left) {
michael@0 475 let isClose = Math.abs(labelRect.left - elRect.left) - labelRect.width < kDistanceMax &&
michael@0 476 Math.abs(labelRect.top - elRect.top) - labelRect.height < kDistanceMax;
michael@0 477 if (isClose) {
michael@0 478 let width = labelRect.width + elRect.width + (elRect.left - labelRect.left - labelRect.width);
michael@0 479 return new Rect(labelRect.left, labelRect.top, width, elRect.height).expandToIntegers();
michael@0 480 }
michael@0 481 }
michael@0 482 }
michael@0 483 }
michael@0 484 return elRect;
michael@0 485 },
michael@0 486
michael@0 487 _getLabels: function formHelperGetLabels() {
michael@0 488 let associatedLabels = [];
michael@0 489 if (!this.currentElement)
michael@0 490 return associatedLabels;
michael@0 491 let element = this.currentElement;
michael@0 492 let labels = element.ownerDocument.getElementsByTagName("label");
michael@0 493 for (let i=0; i<labels.length; i++) {
michael@0 494 let label = labels[i];
michael@0 495 if ((label.control == element || label.getAttribute("for") == element.id) && this._isVisibleElement(label)) {
michael@0 496 associatedLabels.push({
michael@0 497 rect: getBoundingContentRect(label),
michael@0 498 title: label.textContent
michael@0 499 });
michael@0 500 }
michael@0 501 }
michael@0 502
michael@0 503 return associatedLabels;
michael@0 504 },
michael@0 505
michael@0 506 _sendJsonMsgWrapper: function (aMsg) {
michael@0 507 let json = this._getJSON();
michael@0 508 if (json) {
michael@0 509 sendAsyncMessage(aMsg, json);
michael@0 510 }
michael@0 511 },
michael@0 512
michael@0 513 _getJSON: function() {
michael@0 514 let element = this.currentElement;
michael@0 515 if (!element) {
michael@0 516 return null;
michael@0 517 }
michael@0 518 let choices = getListForElement(element);
michael@0 519 let editable = (element instanceof HTMLInputElement && element.mozIsTextField(false)) || this._isEditable(element);
michael@0 520
michael@0 521 let labels = this._getLabels();
michael@0 522 return {
michael@0 523 current: {
michael@0 524 id: element.id,
michael@0 525 name: element.name,
michael@0 526 title: labels.length ? labels[0].title : "",
michael@0 527 value: element.value,
michael@0 528 maxLength: element.maxLength,
michael@0 529 type: (element.getAttribute("type") || "").toLowerCase(),
michael@0 530 choices: choices,
michael@0 531 isAutocomplete: this._isAutocomplete(element),
michael@0 532 list: this._getListSuggestions(element),
michael@0 533 rect: this._getRect(),
michael@0 534 caretRect: this._getCaretRect(),
michael@0 535 editable: editable
michael@0 536 },
michael@0 537 };
michael@0 538 },
michael@0 539
michael@0 540 /**
michael@0 541 * For each radio button group, remove all but the checked button
michael@0 542 * if there is one, or the first button otherwise.
michael@0 543 */
michael@0 544 _filterRadioButtons: function(aNodes) {
michael@0 545 // First pass: Find the checked or first element in each group.
michael@0 546 let chosenRadios = {};
michael@0 547 for (let i=0; i < aNodes.length; i++) {
michael@0 548 let node = aNodes[i];
michael@0 549 if (node.type == "radio" && (!chosenRadios.hasOwnProperty(node.name) || node.checked))
michael@0 550 chosenRadios[node.name] = node;
michael@0 551 }
michael@0 552
michael@0 553 // Second pass: Exclude all other radio buttons from the list.
michael@0 554 let result = [];
michael@0 555 for (let i=0; i < aNodes.length; i++) {
michael@0 556 let node = aNodes[i];
michael@0 557 if (node.type == "radio" && chosenRadios[node.name] != node)
michael@0 558 continue;
michael@0 559 result.push(node);
michael@0 560 }
michael@0 561 return result;
michael@0 562 }
michael@0 563 };
michael@0 564 this.FormAssistant = FormAssistant;
michael@0 565
michael@0 566
michael@0 567 /******************************************************************************
michael@0 568 * The next classes wraps some forms elements such as different type of list to
michael@0 569 * abstract the difference between html and xul element while manipulating them
michael@0 570 * - SelectWrapper : <html:select>
michael@0 571 * - MenulistWrapper : <xul:menulist>
michael@0 572 *****************************************************************************/
michael@0 573
michael@0 574 function getWrapperForElement(aElement) {
michael@0 575 let wrapper = null;
michael@0 576 if (aElement instanceof HTMLSelectElement) {
michael@0 577 wrapper = new SelectWrapper(aElement);
michael@0 578 }
michael@0 579 else if (aElement instanceof XULMenuListElement) {
michael@0 580 wrapper = new MenulistWrapper(aElement);
michael@0 581 }
michael@0 582
michael@0 583 return wrapper;
michael@0 584 }
michael@0 585
michael@0 586 function getListForElement(aElement) {
michael@0 587 let wrapper = getWrapperForElement(aElement);
michael@0 588 if (!wrapper)
michael@0 589 return null;
michael@0 590
michael@0 591 let optionIndex = 0;
michael@0 592 let result = {
michael@0 593 multiple: wrapper.getMultiple(),
michael@0 594 choices: []
michael@0 595 };
michael@0 596
michael@0 597 // Build up a flat JSON array of the choices. In HTML, it's possible for select element choices
michael@0 598 // to be under a group header (but not recursively). We distinguish between headers and entries
michael@0 599 // using the boolean "list.group".
michael@0 600 // XXX If possible, this would be a great candidate for tracing.
michael@0 601 let children = wrapper.getChildren();
michael@0 602 for (let i = 0; i < children.length; i++) {
michael@0 603 let child = children[i];
michael@0 604 if (wrapper.isGroup(child)) {
michael@0 605 // This is the group element. Add an entry in the choices that says that the following
michael@0 606 // elements are a member of this group.
michael@0 607 result.choices.push({ group: true,
michael@0 608 text: child.label || child.firstChild.data,
michael@0 609 disabled: child.disabled
michael@0 610 });
michael@0 611 let subchildren = child.children;
michael@0 612 for (let j = 0; j < subchildren.length; j++) {
michael@0 613 let subchild = subchildren[j];
michael@0 614 result.choices.push({
michael@0 615 group: false,
michael@0 616 inGroup: true,
michael@0 617 text: wrapper.getText(subchild),
michael@0 618 disabled: child.disabled || subchild.disabled,
michael@0 619 selected: subchild.selected,
michael@0 620 optionIndex: optionIndex++
michael@0 621 });
michael@0 622 }
michael@0 623 }
michael@0 624 else if (wrapper.isOption(child)) {
michael@0 625 // This is a regular choice under no group.
michael@0 626 result.choices.push({
michael@0 627 group: false,
michael@0 628 inGroup: false,
michael@0 629 text: wrapper.getText(child),
michael@0 630 disabled: child.disabled,
michael@0 631 selected: child.selected,
michael@0 632 optionIndex: optionIndex++
michael@0 633 });
michael@0 634 }
michael@0 635 }
michael@0 636
michael@0 637 return result;
michael@0 638 }
michael@0 639
michael@0 640
michael@0 641 function SelectWrapper(aControl) {
michael@0 642 this._control = aControl;
michael@0 643 }
michael@0 644
michael@0 645 SelectWrapper.prototype = {
michael@0 646 getSelectedIndex: function() {
michael@0 647 return this._control.selectedIndex;
michael@0 648 },
michael@0 649
michael@0 650 getMultiple: function() {
michael@0 651 return this._control.multiple;
michael@0 652 },
michael@0 653
michael@0 654 getOptions: function() {
michael@0 655 return this._control.options;
michael@0 656 },
michael@0 657
michael@0 658 getChildren: function() {
michael@0 659 return this._control.children;
michael@0 660 },
michael@0 661
michael@0 662 getText: function(aChild) {
michael@0 663 return aChild.text;
michael@0 664 },
michael@0 665
michael@0 666 isOption: function(aChild) {
michael@0 667 return aChild instanceof HTMLOptionElement;
michael@0 668 },
michael@0 669
michael@0 670 isGroup: function(aChild) {
michael@0 671 return aChild instanceof HTMLOptGroupElement;
michael@0 672 },
michael@0 673
michael@0 674 select: function(aIndex, aSelected) {
michael@0 675 let options = this._control.options;
michael@0 676 if (this.getMultiple())
michael@0 677 options[aIndex].selected = aSelected;
michael@0 678 else
michael@0 679 options.selectedIndex = aIndex;
michael@0 680 },
michael@0 681
michael@0 682 fireOnChange: function() {
michael@0 683 let control = this._control;
michael@0 684 let evt = this._control.ownerDocument.createEvent("Events");
michael@0 685 evt.initEvent("change", true, true, this._control.ownerDocument.defaultView, 0,
michael@0 686 false, false,
michael@0 687 false, false, null);
michael@0 688 content.setTimeout(function() {
michael@0 689 control.dispatchEvent(evt);
michael@0 690 }, 0);
michael@0 691 }
michael@0 692 };
michael@0 693 this.SelectWrapper = SelectWrapper;
michael@0 694
michael@0 695
michael@0 696 // bug 559792
michael@0 697 // Use wrappedJSObject when control is in content for extra protection
michael@0 698 function MenulistWrapper(aControl) {
michael@0 699 this._control = aControl;
michael@0 700 }
michael@0 701
michael@0 702 MenulistWrapper.prototype = {
michael@0 703 getSelectedIndex: function() {
michael@0 704 let control = this._control.wrappedJSObject || this._control;
michael@0 705 let result = control.selectedIndex;
michael@0 706 return ((typeof result == "number" && !isNaN(result)) ? result : -1);
michael@0 707 },
michael@0 708
michael@0 709 getMultiple: function() {
michael@0 710 return false;
michael@0 711 },
michael@0 712
michael@0 713 getOptions: function() {
michael@0 714 let control = this._control.wrappedJSObject || this._control;
michael@0 715 return control.menupopup.children;
michael@0 716 },
michael@0 717
michael@0 718 getChildren: function() {
michael@0 719 let control = this._control.wrappedJSObject || this._control;
michael@0 720 return control.menupopup.children;
michael@0 721 },
michael@0 722
michael@0 723 getText: function(aChild) {
michael@0 724 return aChild.label;
michael@0 725 },
michael@0 726
michael@0 727 isOption: function(aChild) {
michael@0 728 return aChild instanceof Ci.nsIDOMXULSelectControlItemElement;
michael@0 729 },
michael@0 730
michael@0 731 isGroup: function(aChild) {
michael@0 732 return false;
michael@0 733 },
michael@0 734
michael@0 735 select: function(aIndex, aSelected) {
michael@0 736 let control = this._control.wrappedJSObject || this._control;
michael@0 737 control.selectedIndex = aIndex;
michael@0 738 },
michael@0 739
michael@0 740 fireOnChange: function() {
michael@0 741 let control = this._control;
michael@0 742 let evt = document.createEvent("XULCommandEvent");
michael@0 743 evt.initCommandEvent("command", true, true, window, 0,
michael@0 744 false, false,
michael@0 745 false, false, null);
michael@0 746 content.setTimeout(function() {
michael@0 747 control.dispatchEvent(evt);
michael@0 748 }, 0);
michael@0 749 }
michael@0 750 };
michael@0 751 this.MenulistWrapper = MenulistWrapper;

mercurial