summaryrefslogtreecommitdiffstats
path: root/toolkit/content/widgets
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /toolkit/content/widgets
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'toolkit/content/widgets')
-rw-r--r--toolkit/content/widgets/autocomplete.xml2515
-rw-r--r--toolkit/content/widgets/browser.xml1571
-rw-r--r--toolkit/content/widgets/button.xml389
-rw-r--r--toolkit/content/widgets/checkbox.xml84
-rw-r--r--toolkit/content/widgets/colorpicker.xml565
-rw-r--r--toolkit/content/widgets/datetimebox.css45
-rw-r--r--toolkit/content/widgets/datetimebox.xml807
-rw-r--r--toolkit/content/widgets/datetimepicker.xml1301
-rw-r--r--toolkit/content/widgets/datetimepopup.xml181
-rw-r--r--toolkit/content/widgets/dialog.xml448
-rw-r--r--toolkit/content/widgets/editor.xml195
-rw-r--r--toolkit/content/widgets/expander.xml86
-rw-r--r--toolkit/content/widgets/filefield.xml96
-rw-r--r--toolkit/content/widgets/findbar.xml1397
-rw-r--r--toolkit/content/widgets/general.xml231
-rw-r--r--toolkit/content/widgets/groupbox.xml44
-rw-r--r--toolkit/content/widgets/listbox.xml1144
-rw-r--r--toolkit/content/widgets/menu.xml286
-rw-r--r--toolkit/content/widgets/menulist.xml606
-rw-r--r--toolkit/content/widgets/notification.xml551
-rw-r--r--toolkit/content/widgets/numberbox.xml304
-rw-r--r--toolkit/content/widgets/optionsDialog.xml43
-rw-r--r--toolkit/content/widgets/popup.xml650
-rw-r--r--toolkit/content/widgets/preferences.xml1411
-rw-r--r--toolkit/content/widgets/progressmeter.xml116
-rw-r--r--toolkit/content/widgets/radio.xml526
-rw-r--r--toolkit/content/widgets/remote-browser.xml591
-rw-r--r--toolkit/content/widgets/resizer.xml39
-rw-r--r--toolkit/content/widgets/richlistbox.xml589
-rw-r--r--toolkit/content/widgets/scale.xml232
-rw-r--r--toolkit/content/widgets/scrollbar.xml35
-rw-r--r--toolkit/content/widgets/scrollbox.xml908
-rw-r--r--toolkit/content/widgets/spinbuttons.xml96
-rw-r--r--toolkit/content/widgets/spinner.js514
-rw-r--r--toolkit/content/widgets/splitter.xml37
-rw-r--r--toolkit/content/widgets/stringbundle.xml96
-rw-r--r--toolkit/content/widgets/tabbox.xml892
-rw-r--r--toolkit/content/widgets/text.xml386
-rw-r--r--toolkit/content/widgets/textbox.xml646
-rw-r--r--toolkit/content/widgets/timekeeper.js418
-rw-r--r--toolkit/content/widgets/timepicker.js277
-rw-r--r--toolkit/content/widgets/toolbar.xml590
-rw-r--r--toolkit/content/widgets/toolbarbutton.xml115
-rw-r--r--toolkit/content/widgets/tree.xml1561
-rw-r--r--toolkit/content/widgets/videocontrols.css128
-rw-r--r--toolkit/content/widgets/videocontrols.xml2027
-rw-r--r--toolkit/content/widgets/wizard.xml607
47 files changed, 26376 insertions, 0 deletions
diff --git a/toolkit/content/widgets/autocomplete.xml b/toolkit/content/widgets/autocomplete.xml
new file mode 100644
index 000000000..da2bf678d
--- /dev/null
+++ b/toolkit/content/widgets/autocomplete.xml
@@ -0,0 +1,2515 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<bindings id="autocompleteBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="autocomplete" role="xul:combobox"
+ extends="chrome://global/content/bindings/textbox.xml#textbox">
+ <resources>
+ <stylesheet src="chrome://global/content/autocomplete.css"/>
+ <stylesheet src="chrome://global/skin/autocomplete.css"/>
+ </resources>
+
+ <content sizetopopup="pref">
+ <xul:hbox class="autocomplete-textbox-container" flex="1" xbl:inherits="focused">
+ <children includes="image|deck|stack|box">
+ <xul:image class="autocomplete-icon" allowevents="true"/>
+ </children>
+
+ <xul:hbox anonid="textbox-input-box" class="textbox-input-box" flex="1" xbl:inherits="tooltiptext=inputtooltiptext">
+ <children/>
+ <html:input anonid="input" class="autocomplete-textbox textbox-input"
+ allowevents="true"
+ xbl:inherits="tooltiptext=inputtooltiptext,value,type=inputtype,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey,mozactionhint"/>
+ </xul:hbox>
+ <children includes="hbox"/>
+ </xul:hbox>
+
+ <xul:dropmarker anonid="historydropmarker" class="autocomplete-history-dropmarker"
+ allowevents="true"
+ xbl:inherits="open,enablehistory,parentfocused=focused"/>
+
+ <xul:popupset anonid="popupset" class="autocomplete-result-popupset"/>
+
+ <children includes="toolbarbutton"/>
+ </content>
+
+ <implementation implements="nsIAutoCompleteInput, nsIDOMXULMenuListElement">
+ <field name="mController">null</field>
+ <field name="mSearchNames">null</field>
+ <field name="mIgnoreInput">false</field>
+
+ <field name="_searchBeginHandler">null</field>
+ <field name="_searchCompleteHandler">null</field>
+ <field name="_textEnteredHandler">null</field>
+ <field name="_textRevertedHandler">null</field>
+
+ <constructor><![CDATA[
+ this.mController = Components.classes["@mozilla.org/autocomplete/controller;1"].
+ getService(Components.interfaces.nsIAutoCompleteController);
+
+ this._searchBeginHandler = this.initEventHandler("searchbegin");
+ this._searchCompleteHandler = this.initEventHandler("searchcomplete");
+ this._textEnteredHandler = this.initEventHandler("textentered");
+ this._textRevertedHandler = this.initEventHandler("textreverted");
+
+ // For security reasons delay searches on pasted values.
+ this.inputField.controllers.insertControllerAt(0, this._pasteController);
+ ]]></constructor>
+
+ <destructor><![CDATA[
+ this.inputField.controllers.removeController(this._pasteController);
+ ]]></destructor>
+
+ <!-- =================== nsIAutoCompleteInput =================== -->
+
+ <field name="_popup">null</field>
+ <property name="popup" readonly="true">
+ <getter><![CDATA[
+ // Memoize the result in a field rather than replacing this property,
+ // so that it can be reset along with the binding.
+ if (this._popup) {
+ return this._popup;
+ }
+
+ let popup = null;
+ let popupId = this.getAttribute("autocompletepopup");
+ if (popupId) {
+ popup = document.getElementById(popupId);
+ }
+ if (!popup) {
+ popup = document.createElement("panel");
+ popup.setAttribute("type", "autocomplete");
+ popup.setAttribute("noautofocus", "true");
+
+ let popupset = document.getAnonymousElementByAttribute(this, "anonid", "popupset");
+ popupset.appendChild(popup);
+ }
+ popup.mInput = this;
+
+ return this._popup = popup;
+ ]]></getter>
+ </property>
+
+ <property name="controller" onget="return this.mController;" readonly="true"/>
+
+ <property name="popupOpen"
+ onget="return this.popup.popupOpen;"
+ onset="if (val) this.openPopup(); else this.closePopup();"/>
+
+ <property name="disableAutoComplete"
+ onset="this.setAttribute('disableautocomplete', val); return val;"
+ onget="return this.getAttribute('disableautocomplete') == 'true';"/>
+
+ <property name="completeDefaultIndex"
+ onset="this.setAttribute('completedefaultindex', val); return val;"
+ onget="return this.getAttribute('completedefaultindex') == 'true';"/>
+
+ <property name="completeSelectedIndex"
+ onset="this.setAttribute('completeselectedindex', val); return val;"
+ onget="return this.getAttribute('completeselectedindex') == 'true';"/>
+
+ <property name="forceComplete"
+ onset="this.setAttribute('forcecomplete', val); return val;"
+ onget="return this.getAttribute('forcecomplete') == 'true';"/>
+
+ <property name="minResultsForPopup"
+ onset="this.setAttribute('minresultsforpopup', val); return val;"
+ onget="var m = parseInt(this.getAttribute('minresultsforpopup')); return isNaN(m) ? 1 : m;"/>
+
+ <property name="showCommentColumn"
+ onset="this.setAttribute('showcommentcolumn', val); return val;"
+ onget="return this.getAttribute('showcommentcolumn') == 'true';"/>
+
+ <property name="showImageColumn"
+ onset="this.setAttribute('showimagecolumn', val); return val;"
+ onget="return this.getAttribute('showimagecolumn') == 'true';"/>
+
+ <property name="timeout"
+ onset="this.setAttribute('timeout', val); return val;">
+ <getter><![CDATA[
+ // For security reasons delay searches on pasted values.
+ if (this._valueIsPasted) {
+ let t = parseInt(this.getAttribute('pastetimeout'));
+ return isNaN(t) ? 1000 : t;
+ }
+
+ let t = parseInt(this.getAttribute('timeout'));
+ return isNaN(t) ? 50 : t;
+ ]]></getter>
+ </property>
+
+ <property name="searchParam"
+ onget="return this.getAttribute('autocompletesearchparam') || '';"
+ onset="this.setAttribute('autocompletesearchparam', val); return val;"/>
+
+ <property name="searchCount" readonly="true"
+ onget="this.initSearchNames(); return this.mSearchNames.length;"/>
+
+ <field name="shrinkDelay" readonly="true">
+ parseInt(this.getAttribute("shrinkdelay")) || 0
+ </field>
+
+ <property name="PrivateBrowsingUtils" readonly="true">
+ <getter><![CDATA[
+ let module = {};
+ Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm", module);
+ Object.defineProperty(this, "PrivateBrowsingUtils", {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: module.PrivateBrowsingUtils
+ });
+ return module.PrivateBrowsingUtils;
+ ]]></getter>
+ </property>
+
+ <property name="inPrivateContext" readonly="true"
+ onget="return this.PrivateBrowsingUtils.isWindowPrivate(window);"/>
+
+ <property name="noRollupOnCaretMove" readonly="true"
+ onget="return this.popup.getAttribute('norolluponanchor') == 'true'"/>
+
+ <!-- This is the maximum number of drop-down rows we get when we
+ hit the drop marker beside fields that have it (like the URLbar).-->
+ <field name="maxDropMarkerRows" readonly="true">14</field>
+
+ <method name="getSearchAt">
+ <parameter name="aIndex"/>
+ <body><![CDATA[
+ this.initSearchNames();
+ return this.mSearchNames[aIndex];
+ ]]></body>
+ </method>
+
+ <method name="setTextValueWithReason">
+ <parameter name="aValue"/>
+ <parameter name="aReason"/>
+ <body><![CDATA[
+ if (aReason == Components.interfaces.nsIAutoCompleteInput
+ .TEXTVALUE_REASON_COMPLETEDEFAULT) {
+ this._disableTrim = true;
+ }
+ this.textValue = aValue;
+ this._disableTrim = false;
+ ]]></body>
+ </method>
+
+ <property name="textValue">
+ <getter><![CDATA[
+ if (typeof this.onBeforeTextValueGet == "function") {
+ let result = this.onBeforeTextValueGet();
+ if (result) {
+ return result.value;
+ }
+ }
+ return this.value;
+ ]]></getter>
+ <setter><![CDATA[
+ if (typeof this.onBeforeTextValueSet == "function")
+ val = this.onBeforeTextValueSet(val);
+
+ this.value = val;
+
+ // Completing a result should simulate the user typing the result, so
+ // fire an input event.
+ let evt = document.createEvent("UIEvents");
+ evt.initUIEvent("input", true, false, window, 0);
+ this.mIgnoreInput = true;
+ this.dispatchEvent(evt);
+ this.mIgnoreInput = false;
+
+ return this.value;
+ ]]></setter>
+ </property>
+
+ <method name="selectTextRange">
+ <parameter name="aStartIndex"/>
+ <parameter name="aEndIndex"/>
+ <body><![CDATA[
+ this.inputField.setSelectionRange(aStartIndex, aEndIndex);
+ ]]></body>
+ </method>
+
+ <method name="onSearchBegin">
+ <body><![CDATA[
+ if (this.popup && typeof this.popup.onSearchBegin == "function")
+ this.popup.onSearchBegin();
+ if (this._searchBeginHandler)
+ this._searchBeginHandler();
+ ]]></body>
+ </method>
+
+ <method name="onSearchComplete">
+ <body><![CDATA[
+ if (this.mController.matchCount == 0)
+ this.setAttribute("nomatch", "true");
+ else
+ this.removeAttribute("nomatch");
+
+ if (this.ignoreBlurWhileSearching && !this.focused) {
+ this.handleEnter();
+ this.detachController();
+ }
+
+ if (this._searchCompleteHandler)
+ this._searchCompleteHandler();
+ ]]></body>
+ </method>
+
+ <method name="onTextEntered">
+ <parameter name="event"/>
+ <body><![CDATA[
+ let rv = false;
+ if (this._textEnteredHandler) {
+ rv = this._textEnteredHandler(event);
+ }
+ return rv;
+ ]]></body>
+ </method>
+
+ <method name="onTextReverted">
+ <body><![CDATA[
+ if (this._textRevertedHandler)
+ return this._textRevertedHandler();
+ return false;
+ ]]></body>
+ </method>
+
+ <!-- =================== nsIDOMXULMenuListElement =================== -->
+
+ <property name="editable" readonly="true"
+ onget="return true;" />
+
+ <property name="crop"
+ onset="this.setAttribute('crop',val); return val;"
+ onget="return this.getAttribute('crop');"/>
+
+ <property name="open"
+ onget="return this.getAttribute('open') == 'true';">
+ <setter><![CDATA[
+ if (val)
+ this.showHistoryPopup();
+ else
+ this.closePopup();
+ ]]></setter>
+ </property>
+
+ <!-- =================== PUBLIC MEMBERS =================== -->
+
+ <field name="valueIsTyped">false</field>
+ <field name="_disableTrim">false</field>
+ <property name="value">
+ <getter><![CDATA[
+ if (typeof this.onBeforeValueGet == "function") {
+ var result = this.onBeforeValueGet();
+ if (result)
+ return result.value;
+ }
+ return this.inputField.value;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mIgnoreInput = true;
+
+ if (typeof this.onBeforeValueSet == "function")
+ val = this.onBeforeValueSet(val);
+
+ if (typeof this.trimValue == "function" && !this._disableTrim)
+ val = this.trimValue(val);
+
+ this.valueIsTyped = false;
+ this.inputField.value = val;
+
+ if (typeof this.formatValue == "function")
+ this.formatValue();
+
+ this.mIgnoreInput = false;
+ var event = document.createEvent('Events');
+ event.initEvent('ValueChange', true, true);
+ this.inputField.dispatchEvent(event);
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="focused" readonly="true"
+ onget="return this.getAttribute('focused') == 'true';"/>
+
+ <!-- maximum number of rows to display at a time -->
+ <property name="maxRows"
+ onset="this.setAttribute('maxrows', val); return val;"
+ onget="return parseInt(this.getAttribute('maxrows')) || 0;"/>
+
+ <!-- option to allow scrolling through the list via the tab key, rather than
+ tab moving focus out of the textbox -->
+ <property name="tabScrolling"
+ onset="this.setAttribute('tabscrolling', val); return val;"
+ onget="return this.getAttribute('tabscrolling') == 'true';"/>
+
+ <!-- option to completely ignore any blur events while searches are
+ still going on. -->
+ <property name="ignoreBlurWhileSearching"
+ onset="this.setAttribute('ignoreblurwhilesearching', val); return val;"
+ onget="return this.getAttribute('ignoreblurwhilesearching') == 'true';"/>
+
+ <!-- disable key navigation handling in the popup results -->
+ <property name="disableKeyNavigation"
+ onset="this.setAttribute('disablekeynavigation', val); return val;"
+ onget="return this.getAttribute('disablekeynavigation') == 'true';"/>
+
+ <!-- option to highlight entries that don't have any matches -->
+ <property name="highlightNonMatches"
+ onset="this.setAttribute('highlightnonmatches', val); return val;"
+ onget="return this.getAttribute('highlightnonmatches') == 'true';"/>
+
+ <!-- =================== PRIVATE MEMBERS =================== -->
+
+ <!-- ::::::::::::: autocomplete controller ::::::::::::: -->
+
+ <method name="attachController">
+ <body><![CDATA[
+ this.mController.input = this;
+ ]]></body>
+ </method>
+
+ <method name="detachController">
+ <body><![CDATA[
+ if (this.mController.input == this)
+ this.mController.input = null;
+ ]]></body>
+ </method>
+
+ <!-- ::::::::::::: popup opening ::::::::::::: -->
+
+ <method name="openPopup">
+ <body><![CDATA[
+ if (this.focused)
+ this.popup.openAutocompletePopup(this, this);
+ ]]></body>
+ </method>
+
+ <method name="closePopup">
+ <body><![CDATA[
+ this.popup.closePopup();
+ ]]></body>
+ </method>
+
+ <method name="showHistoryPopup">
+ <body><![CDATA[
+ // history dropmarker pushed state
+ function cleanup(popup) {
+ popup.removeEventListener("popupshowing", onShow, false);
+ }
+ function onShow(event) {
+ var popup = event.target, input = popup.input;
+ cleanup(popup);
+ input.setAttribute("open", "true");
+ function onHide() {
+ input.removeAttribute("open");
+ popup.removeEventListener("popuphiding", onHide, false);
+ }
+ popup.addEventListener("popuphiding", onHide, false);
+ }
+ this.popup.addEventListener("popupshowing", onShow, false);
+ setTimeout(cleanup, 1000, this.popup);
+
+ // Store our "normal" maxRows on the popup, so that it can reset the
+ // value when the popup is hidden.
+ this.popup._normalMaxRows = this.maxRows;
+
+ // Increase our maxRows temporarily, since we want the dropdown to
+ // be bigger in this case. The popup's popupshowing/popuphiding
+ // handlers will take care of resetting this.
+ this.maxRows = this.maxDropMarkerRows;
+
+ // Ensure that we have focus.
+ if (!this.focused)
+ this.focus();
+ this.attachController();
+ this.mController.startSearch("");
+ ]]></body>
+ </method>
+
+ <method name="toggleHistoryPopup">
+ <body><![CDATA[
+ // If this method is called on the same event tick as the popup gets
+ // hidden, do nothing to avoid re-opening the popup when the drop
+ // marker is clicked while the popup is still open.
+ if (!this.popup.isPopupHidingTick && !this.popup.popupOpen)
+ this.showHistoryPopup();
+ else
+ this.closePopup();
+ ]]></body>
+ </method>
+
+ <!-- ::::::::::::: event dispatching ::::::::::::: -->
+
+ <method name="initEventHandler">
+ <parameter name="aEventType"/>
+ <body><![CDATA[
+ let handlerString = this.getAttribute("on" + aEventType);
+ if (handlerString) {
+ return (new Function("eventType", "param", handlerString)).bind(this, aEventType);
+ }
+ return null;
+ ]]></body>
+ </method>
+
+ <!-- ::::::::::::: key handling ::::::::::::: -->
+
+ <field name="_selectionDetails">null</field>
+ <method name="onKeyPress">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ return this.handleKeyPress(aEvent);
+ ]]></body>
+ </method>
+
+ <method name="handleKeyPress">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ if (aEvent.target.localName != "textbox")
+ return true; // Let child buttons of autocomplete take input
+
+ // XXXpch this is so bogus...
+ if (aEvent.defaultPrevented)
+ return false;
+
+ var cancel = false;
+
+ // Catch any keys that could potentially move the caret. Ctrl can be
+ // used in combination with these keys on Windows and Linux; and Alt
+ // can be used on OS X, so make sure the unused one isn't used.
+ let metaKey = /Mac/.test(navigator.platform) ? aEvent.ctrlKey : aEvent.altKey;
+ if (!this.disableKeyNavigation && !metaKey) {
+ switch (aEvent.keyCode) {
+ case KeyEvent.DOM_VK_LEFT:
+ case KeyEvent.DOM_VK_RIGHT:
+ case KeyEvent.DOM_VK_HOME:
+ cancel = this.mController.handleKeyNavigation(aEvent.keyCode);
+ break;
+ }
+ }
+
+ // Handle keys that are not part of a keyboard shortcut (no Ctrl or Alt)
+ if (!this.disableKeyNavigation && !aEvent.ctrlKey && !aEvent.altKey) {
+ switch (aEvent.keyCode) {
+ case KeyEvent.DOM_VK_TAB:
+ if (this.tabScrolling && this.popup.popupOpen)
+ cancel = this.mController.handleKeyNavigation(aEvent.shiftKey ?
+ KeyEvent.DOM_VK_UP :
+ KeyEvent.DOM_VK_DOWN);
+ else if (this.forceComplete && this.mController.matchCount >= 1)
+ this.mController.handleTab();
+ break;
+ case KeyEvent.DOM_VK_UP:
+ case KeyEvent.DOM_VK_DOWN:
+ case KeyEvent.DOM_VK_PAGE_UP:
+ case KeyEvent.DOM_VK_PAGE_DOWN:
+ cancel = this.mController.handleKeyNavigation(aEvent.keyCode);
+ break;
+ }
+ }
+
+ // Handle keys we know aren't part of a shortcut, even with Alt or
+ // Ctrl.
+ switch (aEvent.keyCode) {
+ case KeyEvent.DOM_VK_ESCAPE:
+ cancel = this.mController.handleEscape();
+ break;
+ case KeyEvent.DOM_VK_RETURN:
+ if (/Mac/.test(navigator.platform)) {
+ // Prevent the default action, since it will beep on Mac
+ if (aEvent.metaKey)
+ aEvent.preventDefault();
+ }
+ if (this.mController.selection) {
+ this._selectionDetails = {
+ index: this.mController.selection.currentIndex,
+ kind: "key"
+ };
+ }
+ cancel = this.handleEnter(aEvent);
+ break;
+ case KeyEvent.DOM_VK_DELETE:
+ if (/Mac/.test(navigator.platform) && !aEvent.shiftKey) {
+ break;
+ }
+ cancel = this.handleDelete();
+ break;
+ case KeyEvent.DOM_VK_BACK_SPACE:
+ if (/Mac/.test(navigator.platform) && aEvent.shiftKey) {
+ cancel = this.handleDelete();
+ }
+ break;
+ case KeyEvent.DOM_VK_DOWN:
+ case KeyEvent.DOM_VK_UP:
+ if (aEvent.altKey)
+ this.toggleHistoryPopup();
+ break;
+ case KeyEvent.DOM_VK_F4:
+ if (!/Mac/.test(navigator.platform)) {
+ this.toggleHistoryPopup();
+ }
+ break;
+ }
+
+ if (cancel) {
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ }
+
+ return true;
+ ]]></body>
+ </method>
+
+ <method name="handleEnter">
+ <parameter name="event"/>
+ <body><![CDATA[
+ return this.mController.handleEnter(false, event || null);
+ ]]></body>
+ </method>
+
+ <method name="handleDelete">
+ <body><![CDATA[
+ return this.mController.handleDelete();
+ ]]></body>
+ </method>
+
+ <!-- ::::::::::::: miscellaneous ::::::::::::: -->
+
+ <method name="initSearchNames">
+ <body><![CDATA[
+ if (!this.mSearchNames) {
+ var names = this.getAttribute("autocompletesearch");
+ if (!names)
+ this.mSearchNames = [];
+ else
+ this.mSearchNames = names.split(" ");
+ }
+ ]]></body>
+ </method>
+
+ <method name="_focus">
+ <!-- doesn't reset this.mController -->
+ <body><![CDATA[
+ this._dontBlur = true;
+ this.focus();
+ this._dontBlur = false;
+ ]]></body>
+ </method>
+
+ <method name="resetActionType">
+ <body><![CDATA[
+ if (this.mIgnoreInput)
+ return;
+ this.removeAttribute("actiontype");
+ ]]></body>
+ </method>
+
+ <field name="_valueIsPasted">false</field>
+ <field name="_pasteController"><![CDATA[
+ ({
+ _autocomplete: this,
+ _kGlobalClipboard: Components.interfaces.nsIClipboard.kGlobalClipboard,
+ supportsCommand: aCommand => aCommand == "cmd_paste",
+ doCommand: function(aCommand) {
+ this._autocomplete._valueIsPasted = true;
+ this._autocomplete.editor.paste(this._kGlobalClipboard);
+ this._autocomplete._valueIsPasted = false;
+ },
+ isCommandEnabled: function(aCommand) {
+ return this._autocomplete.editor.isSelectionEditable &&
+ this._autocomplete.editor.canPaste(this._kGlobalClipboard);
+ },
+ onEvent: function() {}
+ })
+ ]]></field>
+
+ <method name="onInput">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ if (!this.mIgnoreInput && this.mController.input == this) {
+ this.valueIsTyped = true;
+ this.mController.handleText();
+ }
+ this.resetActionType();
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="input"><![CDATA[
+ this.onInput(event);
+ ]]></handler>
+
+ <handler event="keypress" phase="capturing"
+ action="return this.onKeyPress(event);"/>
+
+ <handler event="compositionstart" phase="capturing"
+ action="if (this.mController.input == this) this.mController.handleStartComposition();"/>
+
+ <handler event="compositionend" phase="capturing"
+ action="if (this.mController.input == this) this.mController.handleEndComposition();"/>
+
+ <handler event="focus" phase="capturing"><![CDATA[
+ this.attachController();
+ if (window.gBrowser && window.gBrowser.selectedBrowser.hasAttribute("usercontextid")) {
+ this.userContextId = parseInt(window.gBrowser.selectedBrowser.getAttribute("usercontextid"));
+ } else {
+ this.userContextId = 0;
+ }
+ ]]></handler>
+
+ <handler event="blur" phase="capturing"><![CDATA[
+ if (!this._dontBlur) {
+ if (this.forceComplete && this.mController.matchCount >= 1) {
+ // mousemove sets selected index. Don't blindly use that selected
+ // index in this blur handler since if the popup is open you can
+ // easily "select" another match just by moving the mouse over it.
+ let filledVal = this.value.replace(/.+ >> /, "").toLowerCase();
+ let selectedVal = null;
+ if (this.popup.selectedIndex >= 0) {
+ selectedVal = this.mController.getFinalCompleteValueAt(
+ this.popup.selectedIndex);
+ }
+ if (selectedVal && filledVal != selectedVal.toLowerCase()) {
+ for (let i = 0; i < this.mController.matchCount; i++) {
+ let matchVal = this.mController.getFinalCompleteValueAt(i);
+ if (matchVal.toLowerCase() == filledVal) {
+ this.popup.selectedIndex = i;
+ break;
+ }
+ }
+ }
+ this.mController.handleEnter(false);
+ }
+ if (!this.ignoreBlurWhileSearching)
+ this.detachController();
+ }
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="autocomplete-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-base-popup">
+ <resources>
+ <stylesheet src="chrome://global/content/autocomplete.css"/>
+ <stylesheet src="chrome://global/skin/tree.css"/>
+ <stylesheet src="chrome://global/skin/autocomplete.css"/>
+ </resources>
+
+ <content ignorekeys="true" level="top" consumeoutsideclicks="never">
+ <xul:tree anonid="tree" class="autocomplete-tree plain" hidecolumnpicker="true" flex="1" seltype="single">
+ <xul:treecols anonid="treecols">
+ <xul:treecol id="treecolAutoCompleteValue" class="autocomplete-treecol" flex="1" overflow="true"/>
+ </xul:treecols>
+ <xul:treechildren class="autocomplete-treebody"/>
+ </xul:tree>
+ </content>
+
+ <implementation>
+ <field name="mShowCommentColumn">false</field>
+ <field name="mShowImageColumn">false</field>
+
+ <property name="showCommentColumn"
+ onget="return this.mShowCommentColumn;">
+ <setter>
+ <![CDATA[
+ if (!val && this.mShowCommentColumn) {
+ // reset the flex on the value column and remove the comment column
+ document.getElementById("treecolAutoCompleteValue").setAttribute("flex", 1);
+ this.removeColumn("treecolAutoCompleteComment");
+ } else if (val && !this.mShowCommentColumn) {
+ // reset the flex on the value column and add the comment column
+ document.getElementById("treecolAutoCompleteValue").setAttribute("flex", 2);
+ this.addColumn({id: "treecolAutoCompleteComment", flex: 1});
+ }
+ this.mShowCommentColumn = val;
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="showImageColumn"
+ onget="return this.mShowImageColumn;">
+ <setter>
+ <![CDATA[
+ if (!val && this.mShowImageColumn) {
+ // remove the image column
+ this.removeColumn("treecolAutoCompleteImage");
+ } else if (val && !this.mShowImageColumn) {
+ // add the image column
+ this.addColumn({id: "treecolAutoCompleteImage", flex: 1});
+ }
+ this.mShowImageColumn = val;
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+
+ <method name="addColumn">
+ <parameter name="aAttrs"/>
+ <body>
+ <![CDATA[
+ var col = document.createElement("treecol");
+ col.setAttribute("class", "autocomplete-treecol");
+ for (var name in aAttrs)
+ col.setAttribute(name, aAttrs[name]);
+ this.treecols.appendChild(col);
+ return col;
+ ]]>
+ </body>
+ </method>
+
+ <method name="removeColumn">
+ <parameter name="aColId"/>
+ <body>
+ <![CDATA[
+ return this.treecols.removeChild(document.getElementById(aColId));
+ ]]>
+ </body>
+ </method>
+
+ <property name="selectedIndex"
+ onget="return this.tree.currentIndex;">
+ <setter>
+ <![CDATA[
+ this.tree.view.selection.select(val);
+ if (this.tree.treeBoxObject.height > 0)
+ this.tree.treeBoxObject.ensureRowIsVisible(val < 0 ? 0 : val);
+ // Fire select event on xul:tree so that accessibility API
+ // support layer can fire appropriate accessibility events.
+ var event = document.createEvent('Events');
+ event.initEvent("select", true, true);
+ this.tree.dispatchEvent(event);
+ return val;
+ ]]></setter>
+ </property>
+
+ <method name="adjustHeight">
+ <body>
+ <![CDATA[
+ // detect the desired height of the tree
+ var bx = this.tree.treeBoxObject;
+ var view = this.tree.view;
+ if (!view)
+ return;
+ var rows = this.maxRows;
+ if (!view.rowCount || (rows && view.rowCount < rows))
+ rows = view.rowCount;
+
+ var height = rows * bx.rowHeight;
+
+ if (height == 0) {
+ this.tree.setAttribute("collapsed", "true");
+ } else {
+ if (this.tree.hasAttribute("collapsed"))
+ this.tree.removeAttribute("collapsed");
+
+ this.tree.setAttribute("height", height);
+ }
+ this.tree.setAttribute("hidescrollbar", view.rowCount <= rows);
+ ]]>
+ </body>
+ </method>
+
+ <method name="openAutocompletePopup">
+ <parameter name="aInput"/>
+ <parameter name="aElement"/>
+ <body><![CDATA[
+ // until we have "baseBinding", (see bug #373652) this allows
+ // us to override openAutocompletePopup(), but still call
+ // the method on the base class
+ this._openAutocompletePopup(aInput, aElement);
+ ]]></body>
+ </method>
+
+ <method name="_openAutocompletePopup">
+ <parameter name="aInput"/>
+ <parameter name="aElement"/>
+ <body><![CDATA[
+ if (!this.mPopupOpen) {
+ this.mInput = aInput;
+ this.view = aInput.controller.QueryInterface(Components.interfaces.nsITreeView);
+ this.invalidate();
+
+ this.showCommentColumn = this.mInput.showCommentColumn;
+ this.showImageColumn = this.mInput.showImageColumn;
+
+ var rect = aElement.getBoundingClientRect();
+ var nav = aElement.ownerDocument.defaultView.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIWebNavigation);
+ var docShell = nav.QueryInterface(Components.interfaces.nsIDocShell);
+ var docViewer = docShell.contentViewer;
+ var width = (rect.right - rect.left) * docViewer.fullZoom;
+ this.setAttribute("width", width > 100 ? width : 100);
+
+ // Adjust the direction of the autocomplete popup list based on the textbox direction, bug 649840
+ var popupDirection = aElement.ownerDocument.defaultView.getComputedStyle(aElement).direction;
+ this.style.direction = popupDirection;
+
+ this.openPopup(aElement, "after_start", 0, 0, false, false);
+ }
+ ]]></body>
+ </method>
+
+ <method name="invalidate">
+ <body><![CDATA[
+ this.adjustHeight();
+ this.tree.treeBoxObject.invalidate();
+ ]]></body>
+ </method>
+
+ <method name="selectBy">
+ <parameter name="aReverse"/>
+ <parameter name="aPage"/>
+ <body><![CDATA[
+ try {
+ var amount = aPage ? 5 : 1;
+ this.selectedIndex = this.getNextIndex(aReverse, amount, this.selectedIndex, this.tree.view.rowCount-1);
+ if (this.selectedIndex == -1) {
+ this.input._focus();
+ }
+ } catch (ex) {
+ // do nothing - occasionally timer-related js errors happen here
+ // e.g. "this.selectedIndex has no properties", when you type fast and hit a
+ // navigation key before this popup has opened
+ }
+ ]]></body>
+ </method>
+
+ <!-- =================== PUBLIC MEMBERS =================== -->
+
+ <field name="tree">
+ document.getAnonymousElementByAttribute(this, "anonid", "tree");
+ </field>
+
+ <field name="treecols">
+ document.getAnonymousElementByAttribute(this, "anonid", "treecols");
+ </field>
+
+ <property name="view"
+ onget="return this.mView;">
+ <setter><![CDATA[
+ // We must do this by hand because the tree binding may not be ready yet
+ this.mView = val;
+ this.tree.boxObject.view = val;
+ ]]></setter>
+ </property>
+
+ </implementation>
+ </binding>
+
+ <binding id="autocomplete-base-popup" role="none"
+extends="chrome://global/content/bindings/popup.xml#popup">
+ <implementation implements="nsIAutoCompletePopup">
+ <field name="mInput">null</field>
+ <field name="mPopupOpen">false</field>
+ <field name="mIsPopupHidingTick">false</field>
+
+ <!-- =================== nsIAutoCompletePopup =================== -->
+
+ <property name="input" readonly="true"
+ onget="return this.mInput"/>
+
+ <property name="overrideValue" readonly="true"
+ onget="return null;"/>
+
+ <property name="popupOpen" readonly="true"
+ onget="return this.mPopupOpen;"/>
+
+ <property name="isPopupHidingTick" readonly="true"
+ onget="return this.mIsPopupHidingTick;"/>
+
+ <method name="closePopup">
+ <body>
+ <![CDATA[
+ if (this.mPopupOpen) {
+ this.hidePopup();
+ this.removeAttribute("width");
+ }
+ ]]>
+ </body>
+ </method>
+
+ <!-- This is the default number of rows that we give the autocomplete
+ popup when the textbox doesn't have a "maxrows" attribute
+ for us to use. -->
+ <field name="defaultMaxRows" readonly="true">6</field>
+
+ <!-- In some cases (e.g. when the input's dropmarker button is clicked),
+ the input wants to display a popup with more rows. In that case, it
+ should increase its maxRows property and store the "normal" maxRows
+ in this field. When the popup is hidden, we restore the input's
+ maxRows to the value stored in this field.
+
+ This field is set to -1 between uses so that we can tell when it's
+ been set by the input and when we need to set it in the popupshowing
+ handler. -->
+ <field name="_normalMaxRows">-1</field>
+
+ <property name="maxRows" readonly="true">
+ <getter>
+ <![CDATA[
+ return (this.mInput && this.mInput.maxRows) || this.defaultMaxRows;
+ ]]>
+ </getter>
+ </property>
+
+ <method name="getNextIndex">
+ <parameter name="aReverse"/>
+ <parameter name="aAmount"/>
+ <parameter name="aIndex"/>
+ <parameter name="aMaxRow"/>
+ <body><![CDATA[
+ if (aMaxRow < 0)
+ return -1;
+
+ var newIdx = aIndex + (aReverse?-1:1)*aAmount;
+ if (aReverse && aIndex == -1 || newIdx > aMaxRow && aIndex != aMaxRow)
+ newIdx = aMaxRow;
+ else if (!aReverse && aIndex == -1 || newIdx < 0 && aIndex != 0)
+ newIdx = 0;
+
+ if (newIdx < 0 && aIndex == 0 || newIdx > aMaxRow && aIndex == aMaxRow)
+ aIndex = -1;
+ else
+ aIndex = newIdx;
+
+ return aIndex;
+ ]]></body>
+ </method>
+
+ <method name="onPopupClick">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ var controller = this.view.QueryInterface(Components.interfaces.nsIAutoCompleteController);
+ controller.handleEnter(true, aEvent);
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="popupshowing"><![CDATA[
+ // If normalMaxRows wasn't already set by the input, then set it here
+ // so that we restore the correct number when the popup is hidden.
+
+ // Null-check this.mInput; see bug 1017914
+ if (this._normalMaxRows < 0 && this.mInput) {
+ this._normalMaxRows = this.mInput.maxRows;
+ }
+
+ // Set an attribute for styling the popup based on the input.
+ let inputID = "";
+ if (this.mInput && this.mInput.ownerDocument &&
+ this.mInput.ownerDocument.documentURIObject.schemeIs("chrome")) {
+ inputID = this.mInput.id;
+ // Take care of elements with no id that are inside xbl bindings
+ if (!inputID) {
+ let bindingParent = this.mInput.ownerDocument.getBindingParent(this.mInput);
+ if (bindingParent) {
+ inputID = bindingParent.id;
+ }
+ }
+ }
+ this.setAttribute("autocompleteinput", inputID);
+
+ this.mPopupOpen = true;
+ ]]></handler>
+
+ <handler event="popuphiding"><![CDATA[
+ var isListActive = true;
+ if (this.selectedIndex == -1)
+ isListActive = false;
+ var controller = this.view.QueryInterface(Components.interfaces.nsIAutoCompleteController);
+ controller.stopSearch();
+
+ this.removeAttribute("autocompleteinput");
+ this.mPopupOpen = false;
+
+ // Prevent opening popup from historydropmarker mousedown handler
+ // on the same event tick the popup is hidden by the same mousedown
+ // event.
+ this.mIsPopupHidingTick = true;
+ setTimeout(() => {
+ this.mIsPopupHidingTick = false;
+ }, 0);
+
+ // Reset the maxRows property to the cached "normal" value, and reset
+ // _normalMaxRows so that we can detect whether it was set by the input
+ // when the popupshowing handler runs.
+
+ // Null-check this.mInput; see bug 1017914
+ if (this.mInput)
+ this.mInput.maxRows = this._normalMaxRows;
+ this._normalMaxRows = -1;
+ // If the list was being navigated and then closed, make sure
+ // we fire accessible focus event back to textbox
+
+ // Null-check this.mInput; see bug 1017914
+ if (isListActive && this.mInput) {
+ this.mInput.mIgnoreFocus = true;
+ this.mInput._focus();
+ this.mInput.mIgnoreFocus = false;
+ }
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="autocomplete-rich-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-base-popup">
+ <resources>
+ <stylesheet src="chrome://global/content/autocomplete.css"/>
+ <stylesheet src="chrome://global/skin/autocomplete.css"/>
+ </resources>
+
+ <content ignorekeys="true" level="top" consumeoutsideclicks="never">
+ <xul:richlistbox anonid="richlistbox" class="autocomplete-richlistbox" flex="1"/>
+ <xul:hbox>
+ <children/>
+ </xul:hbox>
+ </content>
+
+ <implementation implements="nsIAutoCompletePopup">
+ <field name="_currentIndex">0</field>
+ <field name="_rlbAnimated">false</field>
+
+ <!-- =================== nsIAutoCompletePopup =================== -->
+
+ <property name="selectedIndex"
+ onget="return this.richlistbox.selectedIndex;">
+ <setter>
+ <![CDATA[
+ this.richlistbox.selectedIndex = val;
+
+ // when clearing the selection (val == -1, so selectedItem will be
+ // null), we want to scroll back to the top. see bug #406194
+ this.richlistbox.ensureElementIsVisible(
+ this.richlistbox.selectedItem || this.richlistbox.firstChild);
+
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <method name="onSearchBegin">
+ <body><![CDATA[
+ this.richlistbox.mouseSelectedIndex = -1;
+
+ if (typeof this._onSearchBegin == "function") {
+ this._onSearchBegin();
+ }
+ ]]></body>
+ </method>
+
+ <method name="openAutocompletePopup">
+ <parameter name="aInput"/>
+ <parameter name="aElement"/>
+ <body>
+ <![CDATA[
+ // until we have "baseBinding", (see bug #373652) this allows
+ // us to override openAutocompletePopup(), but still call
+ // the method on the base class
+ this._openAutocompletePopup(aInput, aElement);
+ ]]>
+ </body>
+ </method>
+
+ <method name="_openAutocompletePopup">
+ <parameter name="aInput"/>
+ <parameter name="aElement"/>
+ <body>
+ <![CDATA[
+ if (!this.mPopupOpen) {
+ // It's possible that the panel is hidden initially
+ // to avoid impacting startup / new window performance
+ aInput.popup.hidden = false;
+
+ this.mInput = aInput;
+ // clear any previous selection, see bugs 400671 and 488357
+ this.selectedIndex = -1;
+
+ var width = aElement.getBoundingClientRect().width;
+ this.setAttribute("width", width > 100 ? width : 100);
+ // invalidate() depends on the width attribute
+ this._invalidate();
+
+ this.openPopup(aElement, "after_start", 0, 0, false, false);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="invalidate">
+ <parameter name="reason"/>
+ <body>
+ <![CDATA[
+ // Don't bother doing work if we're not even showing
+ if (!this.mPopupOpen)
+ return;
+
+ this._invalidate(reason);
+ ]]>
+ </body>
+ </method>
+
+ <method name="_invalidate">
+ <parameter name="reason"/>
+ <body>
+ <![CDATA[
+ // collapsed if no matches
+ this.richlistbox.collapsed = (this._matchCount == 0);
+
+ // Update the richlistbox height.
+ if (this._adjustHeightTimeout) {
+ clearTimeout(this._adjustHeightTimeout);
+ }
+ if (this._shrinkTimeout) {
+ clearTimeout(this._shrinkTimeout);
+ }
+
+ if (this.mPopupOpen) {
+ delete this._adjustHeightOnPopupShown;
+ this._adjustHeightTimeout = setTimeout(() => this.adjustHeight(), 0);
+ } else {
+ this._adjustHeightOnPopupShown = true;
+ }
+
+ this._currentIndex = 0;
+ if (this._appendResultTimeout) {
+ clearTimeout(this._appendResultTimeout);
+ }
+ this._appendCurrentResult(reason);
+ ]]>
+ </body>
+ </method>
+
+ <property name="maxResults" readonly="true">
+ <getter>
+ <![CDATA[
+ // this is how many richlistitems will be kept around
+ // (note, this getter may be overridden)
+ return 20;
+ ]]>
+ </getter>
+ </property>
+
+ <property name="_matchCount" readonly="true">
+ <getter>
+ <![CDATA[
+ return Math.min(this.mInput.controller.matchCount, this.maxResults);
+ ]]>
+ </getter>
+ </property>
+
+ <method name="_collapseUnusedItems">
+ <body>
+ <![CDATA[
+ let existingItemsCount = this.richlistbox.childNodes.length;
+ for (let i = this._matchCount; i < existingItemsCount; ++i) {
+ this.richlistbox.childNodes[i].collapsed = true;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="adjustHeight">
+ <body>
+ <![CDATA[
+ // Figure out how many rows to show
+ let rows = this.richlistbox.childNodes;
+ let numRows = Math.min(this._matchCount, this.maxRows, rows.length);
+
+ this.removeAttribute("height");
+
+ // Default the height to 0 if we have no rows to show
+ let height = 0;
+ if (numRows) {
+ let firstRowRect = rows[0].getBoundingClientRect();
+ if (this._rlbPadding == undefined) {
+ let style = window.getComputedStyle(this.richlistbox);
+
+ let transition = style.transitionProperty;
+ this._rlbAnimated = transition && transition != "none";
+
+ let paddingTop = parseInt(style.paddingTop) || 0;
+ let paddingBottom = parseInt(style.paddingBottom) || 0;
+ this._rlbPadding = paddingTop + paddingBottom;
+ }
+
+ if (numRows > this.maxRows) {
+ // Set a fixed max-height to avoid flicker when growing the panel.
+ let lastVisibleRowRect = rows[this.maxRows - 1].getBoundingClientRect();
+ let visibleHeight = lastVisibleRowRect.bottom - firstRowRect.top;
+ this.richlistbox.style.maxHeight =
+ visibleHeight + this._rlbPadding + "px";
+ }
+
+ // The class `forceHandleUnderflow` is for the item might need to
+ // handle OverUnderflow or Overflow when the height of an item will
+ // be changed dynamically.
+ for (let i = 0; i < numRows; i++) {
+ if (rows[i].classList.contains("forceHandleUnderflow")) {
+ rows[i].handleOverUnderflow();
+ }
+ }
+
+ let lastRowRect = rows[numRows - 1].getBoundingClientRect();
+ // Calculate the height to have the first row to last row shown
+ height = lastRowRect.bottom - firstRowRect.top +
+ this._rlbPadding;
+ }
+
+ let animate = this._rlbAnimated &&
+ this.getAttribute("dontanimate") != "true";
+ let currentHeight = this.richlistbox.getBoundingClientRect().height;
+ if (height > currentHeight) {
+ // Grow immediately.
+ if (animate) {
+ this.richlistbox.removeAttribute("height");
+ this.richlistbox.style.height = height + "px";
+ } else {
+ this.richlistbox.style.removeProperty("height");
+ this.richlistbox.height = height;
+ }
+ } else {
+ // Delay shrinking to avoid flicker.
+ this._shrinkTimeout = setTimeout(() => {
+ this._collapseUnusedItems();
+ if (animate) {
+ this.richlistbox.removeAttribute("height");
+ this.richlistbox.style.height = height + "px";
+ } else {
+ this.richlistbox.style.removeProperty("height");
+ this.richlistbox.height = height;
+ }
+ }, this.mInput.shrinkDelay);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="_appendCurrentResult">
+ <parameter name="invalidateReason"/>
+ <body>
+ <![CDATA[
+ var controller = this.mInput.controller;
+ var matchCount = this._matchCount;
+ var existingItemsCount = this.richlistbox.childNodes.length;
+
+ // Process maxRows per chunk to improve performance and user experience
+ for (let i = 0; i < this.maxRows; i++) {
+ if (this._currentIndex >= matchCount)
+ break;
+
+ var item;
+
+ // trim the leading/trailing whitespace
+ var trimmedSearchString = controller.searchString.replace(/^\s+/, "").replace(/\s+$/, "");
+
+ let url = controller.getValueAt(this._currentIndex);
+
+ if (this._currentIndex < existingItemsCount) {
+ // re-use the existing item
+ item = this.richlistbox.childNodes[this._currentIndex];
+ item.setAttribute("dir", this.style.direction);
+
+ // Completely reuse the existing richlistitem for invalidation
+ // due to new results, but only when: the item is the same, *OR*
+ // we are about to replace the currently mouse-selected item, to
+ // avoid surprising the user.
+ let iface = Components.interfaces.nsIAutoCompletePopup;
+ if (item.getAttribute("text") == trimmedSearchString &&
+ invalidateReason == iface.INVALIDATE_REASON_NEW_RESULT &&
+ (item.getAttribute("url") == url ||
+ this.richlistbox.mouseSelectedIndex === this._currentIndex)) {
+ // Additionally, if the item is a searchengine action, then it
+ // should only be reused if the engine name is the same as the
+ // popup's override engine name, if any.
+ let action = item._parseActionUrl(url);
+ if (!action ||
+ action.type != "searchengine" ||
+ !this.overrideSearchEngineName ||
+ action.params.engineName == this.overrideSearchEngineName) {
+ item.collapsed = false;
+ // Call adjustSiteIconStart only after setting collapsed=
+ // false. The calculations it does may be wrong otherwise.
+ item.adjustSiteIconStart(this._siteIconStart);
+ // The popup may have changed size between now and the last
+ // time the item was shown, so always handle over/underflow.
+ item.handleOverUnderflow();
+ this._currentIndex++;
+ continue;
+ }
+ }
+ }
+ else {
+ // need to create a new item
+ item = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "richlistitem");
+ item.setAttribute("dir", this.style.direction);
+ }
+
+ // set these attributes before we set the class
+ // so that we can use them from the constructor
+ let iconURI = controller.getImageAt(this._currentIndex);
+ item.setAttribute("image", iconURI);
+ item.setAttribute("url", url);
+ item.setAttribute("title", controller.getCommentAt(this._currentIndex));
+ item.setAttribute("originaltype", controller.getStyleAt(this._currentIndex));
+ item.setAttribute("text", trimmedSearchString);
+
+ if (this._currentIndex < existingItemsCount) {
+ // re-use the existing item
+ item._adjustAcItem();
+ item.collapsed = false;
+ }
+ else {
+ // set the class at the end so we can use the attributes
+ // in the xbl constructor
+ item.className = "autocomplete-richlistitem";
+ this.richlistbox.appendChild(item);
+ }
+
+ // The binding may have not been applied yet.
+ setTimeout(() => {
+ let changed = item.adjustSiteIconStart(this._siteIconStart);
+ if (changed) {
+ item.handleOverUnderflow();
+ }
+ }, 0);
+
+ this._currentIndex++;
+ }
+
+ if (typeof this.onResultsAdded == "function")
+ this.onResultsAdded();
+
+ if (this._currentIndex < matchCount) {
+ // yield after each batch of items so that typing the url bar is
+ // responsive
+ this._appendResultTimeout = setTimeout(() => this._appendCurrentResult(), 0);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <!-- The x-coordinate relative to the leading edge of the window of the
+ items' site icons (favicons). -->
+ <property name="siteIconStart"
+ onget="return this._siteIconStart;">
+ <setter>
+ <![CDATA[
+ if (val != this._siteIconStart) {
+ this._siteIconStart = val;
+ for (let item of this.richlistbox.childNodes) {
+ let changed = item.adjustSiteIconStart(val);
+ if (changed) {
+ item.handleOverUnderflow();
+ }
+ }
+ }
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="overflowPadding"
+ onget="return Number(this.getAttribute('overflowpadding'))"
+ readonly="true" />
+
+ <method name="selectBy">
+ <parameter name="aReverse"/>
+ <parameter name="aPage"/>
+ <body>
+ <![CDATA[
+ try {
+ var amount = aPage ? 5 : 1;
+
+ // because we collapsed unused items, we can't use this.richlistbox.getRowCount(), we need to use the matchCount
+ this.selectedIndex = this.getNextIndex(aReverse, amount, this.selectedIndex, this._matchCount - 1);
+ if (this.selectedIndex == -1) {
+ this.input._focus();
+ }
+ } catch (ex) {
+ // do nothing - occasionally timer-related js errors happen here
+ // e.g. "this.selectedIndex has no properties", when you type fast and hit a
+ // navigation key before this popup has opened
+ }
+ ]]>
+ </body>
+ </method>
+
+ <field name="richlistbox">
+ document.getAnonymousElementByAttribute(this, "anonid", "richlistbox");
+ </field>
+
+ <property name="view"
+ onget="return this.mInput.controller;"
+ onset="return val;"/>
+
+ </implementation>
+ <handlers>
+ <handler event="popupshown">
+ <![CDATA[
+ if (this._adjustHeightOnPopupShown) {
+ delete this._adjustHeightOnPopupShown;
+ this.adjustHeight();
+ }
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="autocomplete-richlistitem-insecure-field" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-richlistitem">
+ <content align="center"
+ onoverflow="this._onOverflow();"
+ onunderflow="this._onUnderflow();">
+ <xul:image anonid="type-icon"
+ class="ac-type-icon"
+ xbl:inherits="selected,current,type"/>
+ <xul:image anonid="site-icon"
+ class="ac-site-icon"
+ xbl:inherits="src=image,selected,type"/>
+ <xul:vbox class="ac-title"
+ align="left"
+ xbl:inherits="">
+ <xul:description class="ac-text-overflow-container">
+ <xul:description anonid="title-text"
+ class="ac-title-text"
+ xbl:inherits="selected"/>
+ </xul:description>
+ </xul:vbox>
+ <xul:hbox anonid="tags"
+ class="ac-tags"
+ align="center"
+ xbl:inherits="selected">
+ <xul:description class="ac-text-overflow-container">
+ <xul:description anonid="tags-text"
+ class="ac-tags-text"
+ xbl:inherits="selected"/>
+ </xul:description>
+ </xul:hbox>
+ <xul:hbox anonid="separator"
+ class="ac-separator"
+ align="center"
+ xbl:inherits="selected,actiontype,type">
+ <xul:description class="ac-separator-text">—</xul:description>
+ </xul:hbox>
+ <xul:hbox class="ac-url"
+ align="center"
+ xbl:inherits="selected,actiontype">
+ <xul:description class="ac-text-overflow-container">
+ <xul:description anonid="url-text"
+ class="ac-url-text"
+ xbl:inherits="selected"/>
+ </xul:description>
+ </xul:hbox>
+ <xul:hbox class="ac-action"
+ align="center"
+ xbl:inherits="selected,actiontype">
+ <xul:description class="ac-text-overflow-container">
+ <xul:description anonid="action-text"
+ class="ac-action-text"
+ xbl:inherits="selected"/>
+ </xul:description>
+ </xul:hbox>
+ </content>
+
+ <handlers>
+ <handler event="click" button="0"><![CDATA[
+ let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
+ window.openUILinkIn(baseURL + "insecure-password", "tab", {
+ relatedToCurrent: true,
+ });
+ ]]></handler>
+ </handlers>
+
+ <implementation>
+ <constructor><![CDATA[
+ // Unlike other autocomplete items, the height of the insecure warning
+ // increases by wrapping. So "forceHandleUnderflow" is for container to
+ // recalculate an item's height and width.
+ this.classList.add("forceHandleUnderflow");
+ ]]></constructor>
+
+ <property name="_learnMoreString">
+ <getter><![CDATA[
+ if (!this.__learnMoreString) {
+ this.__learnMoreString =
+ Services.strings.createBundle("chrome://passwordmgr/locale/passwordmgr.properties").
+ GetStringFromName("insecureFieldWarningLearnMore");
+ }
+ return this.__learnMoreString;
+ ]]></getter>
+ </property>
+
+ <method name="_getSearchTokens">
+ <parameter name="aSearch"/>
+ <body>
+ <![CDATA[
+ return [this._learnMoreString.toLowerCase()];
+ ]]>
+ </body>
+ </method>
+
+ </implementation>
+ </binding>
+
+ <binding id="autocomplete-richlistitem" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
+
+ <content align="center"
+ onoverflow="this._onOverflow();"
+ onunderflow="this._onUnderflow();">
+ <xul:image anonid="type-icon"
+ class="ac-type-icon"
+ xbl:inherits="selected,current,type"/>
+ <xul:image anonid="site-icon"
+ class="ac-site-icon"
+ xbl:inherits="src=image,selected,type"/>
+ <xul:hbox class="ac-title"
+ align="center"
+ xbl:inherits="selected">
+ <xul:description class="ac-text-overflow-container">
+ <xul:description anonid="title-text"
+ class="ac-title-text"
+ xbl:inherits="selected"/>
+ </xul:description>
+ </xul:hbox>
+ <xul:hbox anonid="tags"
+ class="ac-tags"
+ align="center"
+ xbl:inherits="selected">
+ <xul:description class="ac-text-overflow-container">
+ <xul:description anonid="tags-text"
+ class="ac-tags-text"
+ xbl:inherits="selected"/>
+ </xul:description>
+ </xul:hbox>
+ <xul:hbox anonid="separator"
+ class="ac-separator"
+ align="center"
+ xbl:inherits="selected,actiontype,type">
+ <xul:description class="ac-separator-text">—</xul:description>
+ </xul:hbox>
+ <xul:hbox class="ac-url"
+ align="center"
+ xbl:inherits="selected,actiontype">
+ <xul:description class="ac-text-overflow-container">
+ <xul:description anonid="url-text"
+ class="ac-url-text"
+ xbl:inherits="selected"/>
+ </xul:description>
+ </xul:hbox>
+ <xul:hbox class="ac-action"
+ align="center"
+ xbl:inherits="selected,actiontype">
+ <xul:description class="ac-text-overflow-container">
+ <xul:description anonid="action-text"
+ class="ac-action-text"
+ xbl:inherits="selected"/>
+ </xul:description>
+ </xul:hbox>
+ </content>
+
+ <implementation implements="nsIDOMXULSelectControlItemElement">
+ <constructor>
+ <![CDATA[
+ this._typeIcon = document.getAnonymousElementByAttribute(
+ this, "anonid", "type-icon"
+ );
+ this._siteIcon = document.getAnonymousElementByAttribute(
+ this, "anonid", "site-icon"
+ );
+ this._titleText = document.getAnonymousElementByAttribute(
+ this, "anonid", "title-text"
+ );
+ this._tags = document.getAnonymousElementByAttribute(
+ this, "anonid", "tags"
+ );
+ this._tagsText = document.getAnonymousElementByAttribute(
+ this, "anonid", "tags-text"
+ );
+ this._separator = document.getAnonymousElementByAttribute(
+ this, "anonid", "separator"
+ );
+ this._urlText = document.getAnonymousElementByAttribute(
+ this, "anonid", "url-text"
+ );
+ this._actionText = document.getAnonymousElementByAttribute(
+ this, "anonid", "action-text"
+ );
+ this._adjustAcItem();
+ ]]>
+ </constructor>
+
+ <property name="label" readonly="true">
+ <getter>
+ <![CDATA[
+ // This property is a string that is read aloud by screen readers,
+ // so it must not contain anything that should not be user-facing.
+
+ let parts = [
+ this.getAttribute("title"),
+ this.getAttribute("displayurl"),
+ ];
+ let label = parts.filter(str => str).join(" ")
+
+ // allow consumers that have extended popups to override
+ // the label values for the richlistitems
+ let panel = this.parentNode.parentNode;
+ if (panel.createResultLabel) {
+ return panel.createResultLabel(this, label);
+ }
+
+ return label;
+ ]]>
+ </getter>
+ </property>
+
+ <property name="_stringBundle">
+ <getter><![CDATA[
+ if (!this.__stringBundle) {
+ this.__stringBundle = Services.strings.createBundle("chrome://global/locale/autocomplete.properties");
+ }
+ return this.__stringBundle;
+ ]]></getter>
+ </property>
+
+ <field name="_boundaryCutoff">null</field>
+
+ <property name="boundaryCutoff" readonly="true">
+ <getter>
+ <![CDATA[
+ if (!this._boundaryCutoff) {
+ this._boundaryCutoff =
+ Components.classes["@mozilla.org/preferences-service;1"].
+ getService(Components.interfaces.nsIPrefBranch).
+ getIntPref("toolkit.autocomplete.richBoundaryCutoff");
+ }
+ return this._boundaryCutoff;
+ ]]>
+ </getter>
+ </property>
+
+ <field name="_inOverflow">false</field>
+
+ <method name="_onOverflow">
+ <body>
+ <![CDATA[
+ this._inOverflow = true;
+ this._handleOverflow();
+ ]]>
+ </body>
+ </method>
+
+ <method name="_onUnderflow">
+ <body>
+ <![CDATA[
+ this._inOverflow = false;
+ this._handleOverflow();
+ ]]>
+ </body>
+ </method>
+
+ <method name="_getBoundaryIndices">
+ <parameter name="aText"/>
+ <parameter name="aSearchTokens"/>
+ <body>
+ <![CDATA[
+ // Short circuit for empty search ([""] == "")
+ if (aSearchTokens == "")
+ return [0, aText.length];
+
+ // Find which regions of text match the search terms
+ let regions = [];
+ for (let search of Array.prototype.slice.call(aSearchTokens)) {
+ let matchIndex = -1;
+ let searchLen = search.length;
+
+ // Find all matches of the search terms, but stop early for perf
+ let lowerText = aText.substr(0, this.boundaryCutoff).toLowerCase();
+ while ((matchIndex = lowerText.indexOf(search, matchIndex + 1)) >= 0) {
+ regions.push([matchIndex, matchIndex + searchLen]);
+ }
+ }
+
+ // Sort the regions by start position then end position
+ regions = regions.sort((a, b) => {
+ let start = a[0] - b[0];
+ return (start == 0) ? a[1] - b[1] : start;
+ });
+
+ // Generate the boundary indices from each region
+ let start = 0;
+ let end = 0;
+ let boundaries = [];
+ let len = regions.length;
+ for (let i = 0; i < len; i++) {
+ // We have a new boundary if the start of the next is past the end
+ let region = regions[i];
+ if (region[0] > end) {
+ // First index is the beginning of match
+ boundaries.push(start);
+ // Second index is the beginning of non-match
+ boundaries.push(end);
+
+ // Track the new region now that we've stored the previous one
+ start = region[0];
+ }
+
+ // Push back the end index for the current or new region
+ end = Math.max(end, region[1]);
+ }
+
+ // Add the last region
+ boundaries.push(start);
+ boundaries.push(end);
+
+ // Put on the end boundary if necessary
+ if (end < aText.length)
+ boundaries.push(aText.length);
+
+ // Skip the first item because it's always 0
+ return boundaries.slice(1);
+ ]]>
+ </body>
+ </method>
+
+ <method name="_getSearchTokens">
+ <parameter name="aSearch"/>
+ <body>
+ <![CDATA[
+ let search = aSearch.toLowerCase();
+ return search.split(/\s+/);
+ ]]>
+ </body>
+ </method>
+
+ <method name="_setUpDescription">
+ <parameter name="aDescriptionElement"/>
+ <parameter name="aText"/>
+ <parameter name="aNoEmphasis"/>
+ <body>
+ <![CDATA[
+ // Get rid of all previous text
+ if (!aDescriptionElement) {
+ return;
+ }
+ while (aDescriptionElement.hasChildNodes())
+ aDescriptionElement.removeChild(aDescriptionElement.firstChild);
+
+ // If aNoEmphasis is specified, don't add any emphasis
+ if (aNoEmphasis) {
+ aDescriptionElement.appendChild(document.createTextNode(aText));
+ return;
+ }
+
+ // Get the indices that separate match and non-match text
+ let search = this.getAttribute("text");
+ let tokens = this._getSearchTokens(search);
+ let indices = this._getBoundaryIndices(aText, tokens);
+
+ this._appendDescriptionSpans(indices, aText, aDescriptionElement,
+ aDescriptionElement);
+ ]]>
+ </body>
+ </method>
+
+ <method name="_appendDescriptionSpans">
+ <parameter name="indices"/>
+ <parameter name="text"/>
+ <parameter name="spansParentElement"/>
+ <parameter name="descriptionElement"/>
+ <body>
+ <![CDATA[
+ let next;
+ let start = 0;
+ let len = indices.length;
+ // Even indexed boundaries are matches, so skip the 0th if it's empty
+ for (let i = indices[0] == 0 ? 1 : 0; i < len; i++) {
+ next = indices[i];
+ let spanText = text.substr(start, next - start);
+ start = next;
+
+ if (i % 2 == 0) {
+ // Emphasize the text for even indices
+ let span = spansParentElement.appendChild(
+ document.createElementNS("http://www.w3.org/1999/xhtml", "span"));
+ this._setUpEmphasisSpan(span, descriptionElement);
+ span.textContent = spanText;
+ } else {
+ // Otherwise, it's plain text
+ spansParentElement.appendChild(document.createTextNode(spanText));
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="_setUpTags">
+ <parameter name="tags"/>
+ <body>
+ <![CDATA[
+ while (this._tagsText.hasChildNodes()) {
+ this._tagsText.firstChild.remove();
+ }
+
+ let anyTagsMatch = false;
+
+ // Include only tags that match the search string.
+ for (let tag of tags) {
+ // Check if the tag matches the search string.
+ let search = this.getAttribute("text");
+ let tokens = this._getSearchTokens(search);
+ let indices = this._getBoundaryIndices(tag, tokens);
+
+ if (indices.length == 2 &&
+ indices[0] == 0 &&
+ indices[1] == tag.length) {
+ // The tag doesn't match the search string, so don't include it.
+ continue;
+ }
+
+ anyTagsMatch = true;
+
+ let tagSpan =
+ document.createElementNS("http://www.w3.org/1999/xhtml", "span");
+ tagSpan.classList.add("ac-tag");
+ this._tagsText.appendChild(tagSpan);
+
+ this._appendDescriptionSpans(indices, tag, tagSpan, this._tagsText);
+ }
+
+ return anyTagsMatch;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_setUpEmphasisSpan">
+ <parameter name="aSpan"/>
+ <parameter name="aDescriptionElement"/>
+ <body>
+ <![CDATA[
+ aSpan.classList.add("ac-emphasize-text");
+ switch (aDescriptionElement) {
+ case this._titleText:
+ aSpan.classList.add("ac-emphasize-text-title");
+ break;
+ case this._tagsText:
+ aSpan.classList.add("ac-emphasize-text-tag");
+ break;
+ case this._urlText:
+ aSpan.classList.add("ac-emphasize-text-url");
+ break;
+ case this._actionText:
+ aSpan.classList.add("ac-emphasize-text-action");
+ break;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <!--
+ This will generate an array of emphasis pairs for use with
+ _setUpEmphasisedSections(). Each pair is a tuple (array) that
+ represents a block of text - containing the text of that block, and a
+ boolean for whether that block should have an emphasis styling applied
+ to it.
+
+ These pairs are generated by parsing a localised string (aSourceString)
+ with parameters, in the format that is used by
+ nsIStringBundle.formatStringFromName():
+
+ "textA %1$S textB textC %2$S"
+
+ Or:
+
+ "textA %S"
+
+ Where "%1$S", "%2$S", and "%S" are intended to be replaced by provided
+ replacement strings. These are specified an array of tuples
+ (aReplacements), each containing the replacement text and a boolean for
+ whether that text should have an emphasis styling applied. This is used
+ as a 1-based array - ie, "%1$S" is replaced by the item in the first
+ index of aReplacements, "%2$S" by the second, etc. "%S" will always
+ match the first index.
+ -->
+ <method name="_generateEmphasisPairs">
+ <parameter name="aSourceString"/>
+ <parameter name="aReplacements"/>
+ <body>
+ <![CDATA[
+ let pairs = [];
+
+ // Split on %S, %1$S, %2$S, etc. ie:
+ // "textA %S"
+ // becomes ["textA ", "%S"]
+ // "textA %1$S textB textC %2$S"
+ // becomes ["textA ", "%1$S", " textB textC ", "%2$S"]
+ let parts = aSourceString.split(/(%(?:[0-9]+\$)?S)/);
+
+ for (let part of parts) {
+ // The above regex will actually give us an empty string at the
+ // end - we don't want that, as we don't want to later generate an
+ // empty text node for it.
+ if (part.length === 0)
+ continue;
+
+ // Determine if this token is a replacement token or a normal text
+ // token. If it is a replacement token, we want to extract the
+ // numerical number. However, we still want to match on "$S".
+ let match = part.match(/^%(?:([0-9]+)\$)?S$/);
+
+ if (match) {
+ // "%S" doesn't have a numerical number in it, but will always
+ // be assumed to be 1. Furthermore, the input string specifies
+ // these with a 1-based index, but we want a 0-based index.
+ let index = (match[1] || 1) - 1;
+
+ if (index >= 0 && index < aReplacements.length) {
+ pairs.push([...aReplacements[index]]);
+ }
+ } else {
+ pairs.push([part]);
+ }
+ }
+
+ return pairs;
+ ]]>
+ </body>
+ </method>
+
+ <!--
+ _setUpEmphasisedSections() has the same use as _setUpDescription,
+ except instead of taking a string and highlighting given tokens, it takes
+ an array of pairs generated by _generateEmphasisPairs(). This allows
+ control over emphasising based on specific blocks of text, rather than
+ search for substrings.
+ -->
+ <method name="_setUpEmphasisedSections">
+ <parameter name="aDescriptionElement"/>
+ <parameter name="aTextPairs"/>
+ <body>
+ <![CDATA[
+ // Get rid of all previous text
+ while (aDescriptionElement.hasChildNodes())
+ aDescriptionElement.firstChild.remove();
+
+ for (let [text, emphasise] of aTextPairs) {
+ if (emphasise) {
+ let span = aDescriptionElement.appendChild(
+ document.createElementNS("http://www.w3.org/1999/xhtml", "span"));
+ span.textContent = text;
+ switch (emphasise) {
+ case "match":
+ this._setUpEmphasisSpan(span, aDescriptionElement);
+ break;
+ }
+ } else {
+ aDescriptionElement.appendChild(document.createTextNode(text));
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <field name="_textToSubURI">null</field>
+ <method name="_unescapeUrl">
+ <parameter name="url"/>
+ <body>
+ <![CDATA[
+ if (!this._textToSubURI) {
+ this._textToSubURI =
+ Components.classes["@mozilla.org/intl/texttosuburi;1"]
+ .getService(Components.interfaces.nsITextToSubURI);
+ }
+ return this._textToSubURI.unEscapeURIForUI("UTF-8", url);
+ ]]>
+ </body>
+ </method>
+
+ <method name="_adjustAcItem">
+ <body>
+ <![CDATA[
+ let popup = this.parentNode.parentNode;
+ if (!popup.popupOpen) {
+ // Removing the max-width and resetting it later when overflow is
+ // handled is jarring when the item is visible, so skip this when
+ // the popup is open.
+ this._removeMaxWidths();
+ }
+
+ let title = this.getAttribute("title");
+ let titleLooksLikeUrl = false;
+
+ let displayUrl;
+ let originalUrl = this.getAttribute("url");
+ let emphasiseUrl = true;
+
+ let type = this.getAttribute("originaltype");
+ let types = new Set(type.split(/\s+/));
+ let initialTypes = new Set(types);
+ // Remove types that should ultimately not be in the `type` string.
+ types.delete("action");
+ types.delete("autofill");
+ types.delete("heuristic");
+ type = [...types][0] || "";
+
+ let action;
+
+ if (initialTypes.has("autofill")) {
+ // Treat autofills as visiturl actions.
+ action = {
+ type: "visiturl",
+ params: {
+ url: originalUrl,
+ },
+ };
+ }
+
+ this.removeAttribute("actiontype");
+ this.classList.remove("overridable-action");
+
+ // If the type includes an action, set up the item appropriately.
+ if (initialTypes.has("action") || action) {
+ action = action || this._parseActionUrl(originalUrl);
+ this.setAttribute("actiontype", action.type);
+
+ if (action.type == "switchtab") {
+ this.classList.add("overridable-action");
+ displayUrl = this._unescapeUrl(action.params.url);
+ let desc = this._stringBundle.GetStringFromName("switchToTab2");
+ this._setUpDescription(this._actionText, desc, true);
+ } else if (action.type == "remotetab") {
+ displayUrl = this._unescapeUrl(action.params.url);
+ let desc = action.params.deviceName;
+ this._setUpDescription(this._actionText, desc, true);
+ } else if (action.type == "searchengine") {
+ emphasiseUrl = false;
+
+ // The order here is not localizable, we default to appending
+ // "- Search with Engine" to the search string, to be able to
+ // properly generate emphasis pairs. That said, no localization
+ // changed the order while it was possible, so doesn't look like
+ // there's a strong need for that.
+ let {engineName, searchSuggestion, searchQuery} = action.params;
+
+ // Override the engine name if the popup defines an override.
+ let override = popup.overrideSearchEngineName;
+ if (override && override != engineName) {
+ engineName = override;
+ action.params.engineName = override;
+ let newURL =
+ PlacesUtils.mozActionURI(action.type, action.params);
+ this.setAttribute("url", newURL);
+ }
+
+ let engineStr =
+ this._stringBundle.formatStringFromName("searchWithEngine",
+ [engineName], 1);
+ this._setUpDescription(this._actionText, engineStr, true);
+
+ // Make the title by generating an array of pairs and its
+ // corresponding interpolation string (e.g., "%1$S") to pass to
+ // _generateEmphasisPairs.
+ let pairs;
+ if (searchSuggestion) {
+ // Check if the search query appears in the suggestion. It may
+ // not. If it does, then emphasize the query in the suggestion
+ // and otherwise just include the suggestion without emphasis.
+ let idx = searchSuggestion.indexOf(searchQuery);
+ if (idx >= 0) {
+ pairs = [
+ [searchSuggestion.substring(0, idx), ""],
+ [searchQuery, "match"],
+ [searchSuggestion.substring(idx + searchQuery.length), ""],
+ ];
+ } else {
+ pairs = [
+ [searchSuggestion, ""],
+ ];
+ }
+ } else {
+ pairs = [
+ [searchQuery, ""],
+ ];
+ }
+ let interpStr = pairs.map((pair, i) => `%${i + 1}$S`).join("");
+ title = this._generateEmphasisPairs(interpStr, pairs);
+
+ // If this is a default search match, we remove the image so we
+ // can style it ourselves with a generic search icon.
+ // We don't do this when matching an aliased search engine,
+ // because the icon helps with recognising which engine will be
+ // used (when using the default engine, we don't need that
+ // recognition).
+ if (!action.params.alias && !initialTypes.has("favicon")) {
+ this.removeAttribute("image");
+ }
+ } else if (action.type == "visiturl") {
+ emphasiseUrl = false;
+ displayUrl = this._unescapeUrl(action.params.url);
+ title = displayUrl;
+ titleLooksLikeUrl = true;
+ let visitStr = this._stringBundle.GetStringFromName("visit");
+ this._setUpDescription(this._actionText, visitStr, true);
+ } else if (action.type == "extension") {
+ let content = action.params.content;
+ displayUrl = content;
+ this._setUpDescription(this._actionText, content, true);
+ }
+ }
+
+ if (!displayUrl) {
+ let input = popup.input;
+ let url = typeof(input.trimValue) == "function" ?
+ input.trimValue(originalUrl) :
+ originalUrl;
+ displayUrl = this._unescapeUrl(url);
+ }
+ // For performance reasons we may want to limit the displayUrl size.
+ if (popup.textRunsMaxLen) {
+ displayUrl = displayUrl.substr(0, popup.textRunsMaxLen);
+ }
+ this.setAttribute("displayurl", displayUrl);
+
+ // Show the domain as the title if we don't have a title.
+ if (!title) {
+ title = displayUrl;
+ titleLooksLikeUrl = true;
+ try {
+ let uri = Services.io.newURI(originalUrl, null, null);
+ // Not all valid URLs have a domain.
+ if (uri.host)
+ title = uri.host;
+ } catch (e) {}
+ }
+
+ this._tags.setAttribute("empty", "true");
+
+ if (type == "tag" || type == "bookmark-tag") {
+ // The title is separated from the tags by an endash
+ let tags;
+ [, title, tags] = title.match(/^(.+) \u2013 (.+)$/);
+
+ // Each tag is split by a comma in an undefined order, so sort it
+ let sortedTags = tags.split(/\s*,\s*/).sort((a, b) => {
+ return a.localeCompare(a);
+ });
+
+ let anyTagsMatch = this._setUpTags(sortedTags);
+ if (anyTagsMatch) {
+ this._tags.removeAttribute("empty");
+ }
+ if (type == "bookmark-tag") {
+ type = "bookmark";
+ }
+ } else if (type == "keyword") {
+ // Note that this is a moz-action with action.type == keyword.
+ emphasiseUrl = false;
+ let keywordArg = this.getAttribute("text").replace(/^[^\s]+\s*/, "");
+ if (!keywordArg) {
+ // Treat keyword searches without arguments as visiturl actions.
+ type = "visiturl";
+ this.setAttribute("actiontype", "visiturl");
+ let visitStr = this._stringBundle.GetStringFromName("visit");
+ this._setUpDescription(this._actionText, visitStr, true);
+ } else {
+ let pairs = [[title, ""], [keywordArg, "match"]];
+ let interpStr =
+ this._stringBundle.GetStringFromName("bookmarkKeywordSearch");
+ title = this._generateEmphasisPairs(interpStr, pairs);
+ // The action box will be visible since this is a moz-action, but
+ // we want it to appear as if it were not visible, so set its text
+ // to the empty string.
+ this._setUpDescription(this._actionText, "", false);
+ }
+ }
+
+ this.setAttribute("type", type);
+
+ if (titleLooksLikeUrl) {
+ this._titleText.setAttribute("lookslikeurl", "true");
+ } else {
+ this._titleText.removeAttribute("lookslikeurl");
+ }
+
+ if (Array.isArray(title)) {
+ // For performance reasons we may want to limit the title size.
+ if (popup.textRunsMaxLen) {
+ title = title.map(t => t.substr(0, popup.textRunsMaxLen));
+ }
+ this._setUpEmphasisedSections(this._titleText, title);
+ } else {
+ // For performance reasons we may want to limit the title size.
+ if (popup.textRunsMaxLen) {
+ title = title.substr(0, popup.textRunsMaxLen);
+ }
+ this._setUpDescription(this._titleText, title, false);
+ }
+ this._setUpDescription(this._urlText, displayUrl, !emphasiseUrl);
+
+ if (this._inOverflow) {
+ this._handleOverflow();
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="_removeMaxWidths">
+ <body>
+ <![CDATA[
+ this._titleText.style.removeProperty("max-width");
+ this._tagsText.style.removeProperty("max-width");
+ this._urlText.style.removeProperty("max-width");
+ this._actionText.style.removeProperty("max-width");
+ ]]>
+ </body>
+ </method>
+
+ <!-- Sets the x-coordinate of the leading edge of the site icon (favicon)
+ relative the the leading edge of the window.
+ @param newStart The new x-coordinate, relative to the leading edge of
+ the window. Pass undefined to reset the icon's position to
+ whatever is specified in CSS.
+ @return True if the icon's position changed, false if not. -->
+ <method name="adjustSiteIconStart">
+ <parameter name="newStart"/>
+ <body>
+ <![CDATA[
+ if (typeof(newStart) != "number") {
+ this._typeIcon.style.removeProperty("margin-inline-start");
+ return true;
+ }
+ let rect = this._siteIcon.getBoundingClientRect();
+ let dir = this.getAttribute("dir");
+ let delta = dir == "rtl" ? rect.right - newStart
+ : newStart - rect.left;
+ let px = this._typeIcon.style.marginInlineStart;
+ if (!px) {
+ // Allow margin-inline-start not to be specified in CSS initially.
+ let style = window.getComputedStyle(this._typeIcon);
+ px = dir == "rtl" ? style.marginRight : style.marginLeft;
+ }
+ let typeIconStart = Number(px.substr(0, px.length - 2));
+ this._typeIcon.style.marginInlineStart = (typeIconStart + delta) + "px";
+ return delta > 0;
+ ]]>
+ </body>
+ </method>
+
+ <!-- This method truncates the displayed strings as necessary. -->
+ <method name="_handleOverflow">
+ <body><![CDATA[
+ let itemRect = this.parentNode.getBoundingClientRect();
+ let titleRect = this._titleText.getBoundingClientRect();
+ let tagsRect = this._tagsText.getBoundingClientRect();
+ let separatorRect = this._separator.getBoundingClientRect();
+ let urlRect = this._urlText.getBoundingClientRect();
+ let actionRect = this._actionText.getBoundingClientRect();
+ let separatorURLActionWidth =
+ separatorRect.width + Math.max(urlRect.width, actionRect.width);
+
+ // Total width for the title and URL/action is the width of the item
+ // minus the start of the title text minus a little optional extra padding.
+ // This extra padding amount is basically arbitrary but keeps the text
+ // from getting too close to the popup's edge.
+ let dir = this.getAttribute("dir");
+ let titleStart = dir == "rtl" ? itemRect.right - titleRect.right
+ : titleRect.left - itemRect.left;
+
+ let popup = this.parentNode.parentNode;
+ let itemWidth = itemRect.width - titleStart - popup.overflowPadding;
+
+ if (this._tags.hasAttribute("empty")) {
+ // The tags box is not displayed in this case.
+ tagsRect.width = 0;
+ }
+
+ let titleTagsWidth = titleRect.width + tagsRect.width;
+ if (titleTagsWidth + separatorURLActionWidth > itemWidth) {
+ // Title + tags + URL/action overflows the item width.
+
+ // The percentage of the item width allocated to the title and tags.
+ let titleTagsPct = 0.66;
+
+ let titleTagsAvailable = itemWidth - separatorURLActionWidth;
+ let titleTagsMaxWidth = Math.max(
+ titleTagsAvailable,
+ itemWidth * titleTagsPct
+ );
+ if (titleTagsWidth > titleTagsMaxWidth) {
+ // Title + tags overflows the max title + tags width.
+
+ // The percentage of the title + tags width allocated to the
+ // title.
+ let titlePct = 0.33;
+
+ let titleAvailable = titleTagsMaxWidth - tagsRect.width;
+ let titleMaxWidth = Math.max(
+ titleAvailable,
+ titleTagsMaxWidth * titlePct
+ );
+ let tagsAvailable = titleTagsMaxWidth - titleRect.width;
+ let tagsMaxWidth = Math.max(
+ tagsAvailable,
+ titleTagsMaxWidth * (1 - titlePct)
+ );
+ this._titleText.style.maxWidth = titleMaxWidth + "px";
+ this._tagsText.style.maxWidth = tagsMaxWidth + "px";
+ }
+ let urlActionMaxWidth = Math.max(
+ itemWidth - titleTagsWidth,
+ itemWidth * (1 - titleTagsPct)
+ );
+ urlActionMaxWidth -= separatorRect.width;
+ this._urlText.style.maxWidth = urlActionMaxWidth + "px";
+ this._actionText.style.maxWidth = urlActionMaxWidth + "px";
+ }
+ ]]></body>
+ </method>
+
+ <method name="handleOverUnderflow">
+ <body>
+ <![CDATA[
+ this._removeMaxWidths();
+ this._handleOverflow();
+ ]]>
+ </body>
+ </method>
+
+ <method name="_parseActionUrl">
+ <parameter name="aUrl"/>
+ <body><![CDATA[
+ if (!aUrl.startsWith("moz-action:"))
+ return null;
+
+ // URL is in the format moz-action:ACTION,PARAMS
+ // Where PARAMS is a JSON encoded object.
+ let [, type, params] = aUrl.match(/^moz-action:([^,]+),(.*)$/);
+
+ let action = {
+ type: type,
+ };
+
+ try {
+ action.params = JSON.parse(params);
+ for (let key in action.params) {
+ action.params[key] = decodeURIComponent(action.params[key]);
+ }
+ } catch (e) {
+ // If this failed, we assume that params is not a JSON object, and
+ // is instead just a flat string. This may happen for legacy
+ // search components.
+ action.params = {
+ url: params,
+ }
+ }
+
+ return action;
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="autocomplete-tree" extends="chrome://global/content/bindings/tree.xml#tree">
+ <content>
+ <children includes="treecols"/>
+ <xul:treerows class="autocomplete-treerows tree-rows" xbl:inherits="hidescrollbar" flex="1">
+ <children/>
+ </xul:treerows>
+ </content>
+ </binding>
+
+ <binding id="autocomplete-richlistbox" extends="chrome://global/content/bindings/richlistbox.xml#richlistbox">
+ <implementation>
+ <field name="mLastMoveTime">Date.now()</field>
+ <field name="mouseSelectedIndex">-1</field>
+ </implementation>
+ <handlers>
+ <handler event="mouseup">
+ <![CDATA[
+ // don't call onPopupClick for the scrollbar buttons, thumb, slider, etc.
+ let item = event.originalTarget;
+ while (item && item.localName != "richlistitem") {
+ item = item.parentNode;
+ }
+
+ if (!item)
+ return;
+
+ this.parentNode.onPopupClick(event);
+ ]]>
+ </handler>
+
+ <handler event="mousemove">
+ <![CDATA[
+ if (Date.now() - this.mLastMoveTime > 30) {
+ let item = event.target;
+ while (item && item.localName != "richlistitem") {
+ item = item.parentNode;
+ }
+
+ if (!item)
+ return;
+
+ let index = this.getIndexOfItem(item);
+ if (index != this.selectedIndex) {
+ this.mouseSelectedIndex = this.selectedIndex = index;
+ }
+
+ this.mLastMoveTime = Date.now();
+ }
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="autocomplete-treebody">
+ <implementation>
+ <field name="mLastMoveTime">Date.now()</field>
+ </implementation>
+
+ <handlers>
+ <handler event="mouseup" action="this.parentNode.parentNode.onPopupClick(event);"/>
+
+ <handler event="mousedown"><![CDATA[
+ var rc = this.parentNode.treeBoxObject.getRowAt(event.clientX, event.clientY);
+ if (rc != this.parentNode.currentIndex)
+ this.parentNode.view.selection.select(rc);
+ ]]></handler>
+
+ <handler event="mousemove"><![CDATA[
+ if (Date.now() - this.mLastMoveTime > 30) {
+ var rc = this.parentNode.treeBoxObject.getRowAt(event.clientX, event.clientY);
+ if (rc != this.parentNode.currentIndex)
+ this.parentNode.view.selection.select(rc);
+ this.mLastMoveTime = Date.now();
+ }
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="autocomplete-treerows">
+ <content>
+ <xul:hbox flex="1" class="tree-bodybox">
+ <children/>
+ </xul:hbox>
+ <xul:scrollbar xbl:inherits="collapsed=hidescrollbar" orient="vertical" class="tree-scrollbar"/>
+ </content>
+ </binding>
+
+ <binding id="history-dropmarker" extends="chrome://global/content/bindings/general.xml#dropmarker">
+ <handlers>
+ <handler event="mousedown" button="0"><![CDATA[
+ document.getBindingParent(this).toggleHistoryPopup();
+ ]]></handler>
+ </handlers>
+ </binding>
+
+</bindings>
diff --git a/toolkit/content/widgets/browser.xml b/toolkit/content/widgets/browser.xml
new file mode 100644
index 000000000..a5f37b62a
--- /dev/null
+++ b/toolkit/content/widgets/browser.xml
@@ -0,0 +1,1571 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<bindings id="browserBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <binding id="browser" extends="xul:browser" role="outerdoc">
+ <content clickthrough="never">
+ <children/>
+ </content>
+ <implementation type="application/javascript" implements="nsIObserver, nsIDOMEventListener, nsIMessageListener, nsIBrowser">
+ <property name="autoscrollEnabled">
+ <getter>
+ <![CDATA[
+ if (this.getAttribute("autoscroll") == "false")
+ return false;
+
+ var enabled = true;
+ try {
+ enabled = this.mPrefs.getBoolPref("general.autoScroll");
+ }
+ catch (ex) {
+ }
+
+ return enabled;
+ ]]>
+ </getter>
+ </property>
+
+ <property name="canGoBack"
+ onget="return this.webNavigation.canGoBack;"
+ readonly="true"/>
+
+ <property name="canGoForward"
+ onget="return this.webNavigation.canGoForward;"
+ readonly="true"/>
+
+ <method name="_wrapURIChangeCall">
+ <parameter name="fn"/>
+ <body>
+ <![CDATA[
+ if (!this.isRemoteBrowser) {
+ this.inLoadURI = true;
+ try {
+ fn();
+ } finally {
+ this.inLoadURI = false;
+ }
+ } else {
+ fn();
+ }
+ ]]>
+ </body>
+ </method>
+
+
+ <method name="goBack">
+ <body>
+ <![CDATA[
+ var webNavigation = this.webNavigation;
+ if (webNavigation.canGoBack)
+ this._wrapURIChangeCall(() => webNavigation.goBack());
+ ]]>
+ </body>
+ </method>
+
+ <method name="goForward">
+ <body>
+ <![CDATA[
+ var webNavigation = this.webNavigation;
+ if (webNavigation.canGoForward)
+ this._wrapURIChangeCall(() => webNavigation.goForward());
+ ]]>
+ </body>
+ </method>
+
+ <method name="reload">
+ <body>
+ <![CDATA[
+ const nsIWebNavigation = Components.interfaces.nsIWebNavigation;
+ const flags = nsIWebNavigation.LOAD_FLAGS_NONE;
+ this.reloadWithFlags(flags);
+ ]]>
+ </body>
+ </method>
+
+ <method name="reloadWithFlags">
+ <parameter name="aFlags"/>
+ <body>
+ <![CDATA[
+ this.webNavigation.reload(aFlags);
+ ]]>
+ </body>
+ </method>
+
+ <method name="stop">
+ <body>
+ <![CDATA[
+ const nsIWebNavigation = Components.interfaces.nsIWebNavigation;
+ const flags = nsIWebNavigation.STOP_ALL;
+ this.webNavigation.stop(flags);
+ ]]>
+ </body>
+ </method>
+
+ <!-- throws exception for unknown schemes -->
+ <method name="loadURI">
+ <parameter name="aURI"/>
+ <parameter name="aReferrerURI"/>
+ <parameter name="aCharset"/>
+ <body>
+ <![CDATA[
+ const nsIWebNavigation = Components.interfaces.nsIWebNavigation;
+ const flags = nsIWebNavigation.LOAD_FLAGS_NONE;
+ this._wrapURIChangeCall(() =>
+ this.loadURIWithFlags(aURI, flags, aReferrerURI, aCharset));
+ ]]>
+ </body>
+ </method>
+
+ <!-- throws exception for unknown schemes -->
+ <method name="loadURIWithFlags">
+ <parameter name="aURI"/>
+ <parameter name="aFlags"/>
+ <parameter name="aReferrerURI"/>
+ <parameter name="aCharset"/>
+ <parameter name="aPostData"/>
+ <body>
+ <![CDATA[
+ if (!aURI)
+ aURI = "about:blank";
+
+ var aReferrerPolicy = Components.interfaces.nsIHttpChannel.REFERRER_POLICY_DEFAULT;
+
+ // Check for loadURIWithFlags(uri, { ... });
+ var params = arguments[1];
+ if (params && typeof(params) == "object") {
+ aFlags = params.flags;
+ aReferrerURI = params.referrerURI;
+ if ('referrerPolicy' in params) {
+ aReferrerPolicy = params.referrerPolicy;
+ }
+ aCharset = params.charset;
+ aPostData = params.postData;
+ }
+
+ this._wrapURIChangeCall(() =>
+ this.webNavigation.loadURIWithOptions(
+ aURI, aFlags, aReferrerURI, aReferrerPolicy,
+ aPostData, null, null));
+ ]]>
+ </body>
+ </method>
+
+ <method name="goHome">
+ <body>
+ <![CDATA[
+ try {
+ this.loadURI(this.homePage);
+ }
+ catch (e) {
+ }
+ ]]>
+ </body>
+ </method>
+
+ <property name="homePage">
+ <getter>
+ <![CDATA[
+ var uri;
+
+ if (this.hasAttribute("homepage"))
+ uri = this.getAttribute("homepage");
+ else
+ uri = "http://www.mozilla.org/"; // widget pride
+
+ return uri;
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ this.setAttribute("homepage", val);
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <method name="gotoIndex">
+ <parameter name="aIndex"/>
+ <body>
+ <![CDATA[
+ this._wrapURIChangeCall(() => this.webNavigation.gotoIndex(aIndex));
+ ]]>
+ </body>
+ </method>
+
+ <property name="currentURI" readonly="true">
+ <getter><![CDATA[
+ if (this.webNavigation) {
+ return this.webNavigation.currentURI;
+ }
+ return null;
+ ]]>
+ </getter>
+ </property>
+
+ <!--
+ Used by session restore to ensure that currentURI is set so
+ that switch-to-tab works before the tab is fully
+ restored. This function also invokes onLocationChanged
+ listeners in tabbrowser.xml.
+ -->
+ <method name="_setCurrentURI">
+ <parameter name="aURI"/>
+ <body><![CDATA[
+ this.docShell.setCurrentURI(aURI);
+ ]]></body>
+ </method>
+
+ <property name="documentURI"
+ onget="return this.contentDocument.documentURIObject;"
+ readonly="true"/>
+
+ <property name="documentContentType"
+ onget="return this.contentDocument ? this.contentDocument.contentType : null;"
+ readonly="true"/>
+
+ <property name="preferences"
+ onget="return this.mPrefs.QueryInterface(Components.interfaces.nsIPrefService);"
+ readonly="true"/>
+
+ <!--
+ Weak reference to the related browser (see
+ nsIBrowser.getRelatedBrowser).
+ -->
+ <field name="_relatedBrowser">null</field>
+ <property name="relatedBrowser">
+ <getter><![CDATA[
+ return this._relatedBrowser && this._relatedBrowser.get();
+ ]]></getter>
+ <setter><![CDATA[
+ this._relatedBrowser = Cu.getWeakReference(val);
+ ]]></setter>
+ </property>
+
+ <field name="_docShell">null</field>
+
+ <property name="docShell" readonly="true">
+ <getter><![CDATA[
+ if (this._docShell)
+ return this._docShell;
+
+ let frameLoader = this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader;
+ if (!frameLoader)
+ return null;
+ this._docShell = frameLoader.docShell;
+ return this._docShell;
+ ]]></getter>
+ </property>
+
+ <field name="_loadContext">null</field>
+
+ <property name="loadContext" readonly="true">
+ <getter><![CDATA[
+ if (this._loadContext)
+ return this._loadContext;
+
+ let frameLoader = this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader;
+ if (!frameLoader)
+ return null;
+ this._loadContext = frameLoader.loadContext;
+ return this._loadContext;
+ ]]></getter>
+ </property>
+
+ <property name="autoCompletePopup"
+ onget="return document.getElementById(this.getAttribute('autocompletepopup'))"
+ readonly="true"/>
+
+ <property name="dateTimePicker"
+ onget="return document.getElementById(this.getAttribute('datetimepicker'))"
+ readonly="true"/>
+
+ <property name="docShellIsActive">
+ <getter>
+ <![CDATA[
+ return this.docShell && this.docShell.isActive;
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ if (this.docShell)
+ return this.docShell.isActive = val;
+ return false;
+ ]]>
+ </setter>
+ </property>
+
+ <method name="preserveLayers">
+ <parameter name="preserve"/>
+ <body>
+ // Only useful for remote browsers.
+ </body>
+ </method>
+
+ <method name="makePrerenderedBrowserActive">
+ <body>
+ <![CDATA[
+ let frameLoader = this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader;
+ if (frameLoader) {
+ frameLoader.makePrerenderedLoaderActive();
+ }
+ ]]>
+ </body>
+ </method>
+
+ <property name="imageDocument"
+ readonly="true">
+ <getter>
+ <![CDATA[
+ var document = this.contentDocument;
+ if (!document || !(document instanceof Components.interfaces.nsIImageDocument))
+ return null;
+
+ try {
+ return {width: document.imageRequest.image.width, height: document.imageRequest.image.height };
+ } catch (e) {}
+ return null;
+ ]]>
+ </getter>
+ </property>
+
+ <property name="isRemoteBrowser"
+ onget="return (this.getAttribute('remote') == 'true');"
+ readonly="true"/>
+
+ <property name="messageManager"
+ readonly="true">
+ <getter>
+ <![CDATA[
+ var owner = this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner);
+ if (!owner.frameLoader) {
+ return null;
+ }
+ return owner.frameLoader.messageManager;
+ ]]>
+ </getter>
+
+ </property>
+
+ <field name="_webNavigation">null</field>
+
+ <property name="webNavigation"
+ readonly="true">
+ <getter>
+ <![CDATA[
+ if (!this._webNavigation) {
+ if (!this.docShell) {
+ return null;
+ }
+ this._webNavigation = this.docShell.QueryInterface(Components.interfaces.nsIWebNavigation);
+ }
+ return this._webNavigation;
+ ]]>
+ </getter>
+ </property>
+
+ <field name="_webBrowserFind">null</field>
+
+ <property name="webBrowserFind"
+ readonly="true">
+ <getter>
+ <![CDATA[
+ if (!this._webBrowserFind)
+ this._webBrowserFind = this.docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIWebBrowserFind);
+ return this._webBrowserFind;
+ ]]>
+ </getter>
+ </property>
+
+ <method name="getTabBrowser">
+ <body>
+ <![CDATA[
+ var tabBrowser = this.parentNode;
+ while (tabBrowser && tabBrowser.localName != "tabbrowser")
+ tabBrowser = tabBrowser.parentNode;
+ return tabBrowser;
+ ]]>
+ </body>
+ </method>
+
+ <field name="_finder">null</field>
+
+ <property name="finder" readonly="true">
+ <getter><![CDATA[
+ if (!this._finder) {
+ if (!this.docShell)
+ return null;
+
+ let Finder = Components.utils.import("resource://gre/modules/Finder.jsm", {}).Finder;
+ this._finder = new Finder(this.docShell);
+ }
+ return this._finder;
+ ]]></getter>
+ </property>
+
+ <field name="_fastFind">null</field>
+ <property name="fastFind" readonly="true">
+ <getter><![CDATA[
+ if (!this._fastFind) {
+ if (!("@mozilla.org/typeaheadfind;1" in Components.classes))
+ return null;
+
+ var tabBrowser = this.getTabBrowser();
+ if (tabBrowser && "fastFind" in tabBrowser)
+ return this._fastFind = tabBrowser.fastFind;
+
+ if (!this.docShell)
+ return null;
+
+ this._fastFind = Components.classes["@mozilla.org/typeaheadfind;1"]
+ .createInstance(Components.interfaces.nsITypeAheadFind);
+ this._fastFind.init(this.docShell);
+ }
+ return this._fastFind;
+ ]]></getter>
+ </property>
+
+ <property name="outerWindowID" readonly="true">
+ <getter><![CDATA[
+ return this.contentWindow
+ .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils)
+ .outerWindowID;
+ ]]></getter>
+ </property>
+
+ <property name="innerWindowID" readonly="true">
+ <getter><![CDATA[
+ try {
+ return this.contentWindow
+ .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils)
+ .currentInnerWindowID;
+ } catch (e) {
+ if (e.result != Cr.NS_ERROR_NOT_AVAILABLE) {
+ throw e;
+ }
+ return null;
+ }
+ ]]></getter>
+ </property>
+
+ <field name="_lastSearchString">null</field>
+
+ <property name="webProgress"
+ readonly="true"
+ onget="return this.docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIWebProgress);"/>
+
+ <field name="_contentWindow">null</field>
+
+ <property name="contentWindow"
+ readonly="true"
+ onget="return this._contentWindow || (this._contentWindow = this.docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindow));"/>
+
+ <property name="contentWindowAsCPOW"
+ readonly="true"
+ onget="return this.contentWindow;"/>
+
+ <property name="sessionHistory"
+ onget="return this.webNavigation.sessionHistory;"
+ readonly="true"/>
+
+ <property name="markupDocumentViewer"
+ onget="return this.docShell.contentViewer;"
+ readonly="true"/>
+
+ <property name="contentViewerEdit"
+ onget="return this.docShell.contentViewer.QueryInterface(Components.interfaces.nsIContentViewerEdit);"
+ readonly="true"/>
+
+ <property name="contentViewerFile"
+ onget="return this.docShell.contentViewer.QueryInterface(Components.interfaces.nsIContentViewerFile);"
+ readonly="true"/>
+
+ <property name="contentDocument"
+ onget="return this.webNavigation.document;"
+ readonly="true"/>
+
+ <property name="contentDocumentAsCPOW"
+ onget="return this.contentDocument;"
+ readonly="true"/>
+
+ <property name="contentTitle"
+ onget="return this.contentDocument.title;"
+ readonly="true"/>
+
+ <property name="characterSet"
+ onget="return this.docShell.charset;">
+ <setter><![CDATA[
+ this.docShell.charset = val;
+ this.docShell.gatherCharsetMenuTelemetry();
+ ]]></setter>
+ </property>
+
+ <property name="mayEnableCharacterEncodingMenu"
+ onget="return this.docShell.mayEnableCharacterEncodingMenu;"
+ readonly="true"/>
+
+ <property name="contentPrincipal"
+ onget="return this.contentDocument.nodePrincipal;"
+ readonly="true"/>
+
+ <property name="showWindowResizer"
+ onset="if (val) this.setAttribute('showresizer', 'true');
+ else this.removeAttribute('showresizer');
+ return val;"
+ onget="return this.getAttribute('showresizer') == 'true';"/>
+
+ <property name="manifestURI"
+ readonly="true">
+ <getter><![CDATA[
+ return this.contentDocument.documentElement &&
+ this.contentDocument.documentElement.getAttribute("manifest");
+ ]]></getter>
+ </property>
+
+ <property name="fullZoom">
+ <getter><![CDATA[
+ return this.markupDocumentViewer.fullZoom;
+ ]]></getter>
+ <setter><![CDATA[
+ this.markupDocumentViewer.fullZoom = val;
+ ]]></setter>
+ </property>
+
+ <property name="textZoom">
+ <getter><![CDATA[
+ return this.markupDocumentViewer.textZoom;
+ ]]></getter>
+ <setter><![CDATA[
+ this.markupDocumentViewer.textZoom = val;
+ ]]></setter>
+ </property>
+
+ <property name="isSyntheticDocument">
+ <getter><![CDATA[
+ return this.contentDocument.mozSyntheticDocument;
+ ]]></getter>
+ </property>
+
+ <property name="hasContentOpener">
+ <getter><![CDATA[
+ return !!this.contentWindow.opener;
+ ]]></getter>
+ </property>
+
+ <field name="mPrefs" readonly="true">
+ Components.classes['@mozilla.org/preferences-service;1']
+ .getService(Components.interfaces.nsIPrefBranch);
+ </field>
+
+ <field name="_mStrBundle">null</field>
+
+ <property name="mStrBundle">
+ <getter>
+ <![CDATA[
+ if (!this._mStrBundle) {
+ // need to create string bundle manually instead of using <xul:stringbundle/>
+ // see bug 63370 for details
+ this._mStrBundle = Components.classes["@mozilla.org/intl/stringbundle;1"]
+ .getService(Components.interfaces.nsIStringBundleService)
+ .createBundle("chrome://global/locale/browser.properties");
+ }
+ return this._mStrBundle;
+ ]]></getter>
+ </property>
+
+ <method name="addProgressListener">
+ <parameter name="aListener"/>
+ <parameter name="aNotifyMask"/>
+ <body>
+ <![CDATA[
+ if (!aNotifyMask) {
+ aNotifyMask = Components.interfaces.nsIWebProgress.NOTIFY_ALL;
+ }
+ this.webProgress.addProgressListener(aListener, aNotifyMask);
+ ]]>
+ </body>
+ </method>
+
+ <method name="removeProgressListener">
+ <parameter name="aListener"/>
+ <body>
+ <![CDATA[
+ this.webProgress.removeProgressListener(aListener);
+ ]]>
+ </body>
+ </method>
+
+ <method name="findChildShell">
+ <parameter name="aDocShell"/>
+ <parameter name="aSoughtURI"/>
+ <body>
+ <![CDATA[
+ if (aDocShell.QueryInterface(Components.interfaces.nsIWebNavigation)
+ .currentURI.spec == aSoughtURI.spec)
+ return aDocShell;
+ var node = aDocShell.QueryInterface(
+ Components.interfaces.nsIDocShellTreeItem);
+ for (var i = 0; i < node.childCount; ++i) {
+ var docShell = node.getChildAt(i);
+ docShell = this.findChildShell(docShell, aSoughtURI);
+ if (docShell)
+ return docShell;
+ }
+ return null;
+ ]]>
+ </body>
+ </method>
+
+ <method name="onPageHide">
+ <parameter name="aEvent"/>
+ <body>
+ <![CDATA[
+ // Delete the feeds cache if we're hiding the topmost page
+ // (as opposed to one of its iframes).
+ if (this.feeds && aEvent.target == this.contentDocument)
+ this.feeds = null;
+ if (!this.docShell || !this.fastFind)
+ return;
+ var tabBrowser = this.getTabBrowser();
+ if (!tabBrowser || !("fastFind" in tabBrowser) ||
+ tabBrowser.selectedBrowser == this)
+ this.fastFind.setDocShell(this.docShell);
+ ]]>
+ </body>
+ </method>
+
+ <method name="updateBlockedPopups">
+ <body>
+ <![CDATA[
+ let event = document.createEvent("Events");
+ event.initEvent("DOMUpdatePageReport", true, true);
+ this.dispatchEvent(event);
+ ]]>
+ </body>
+ </method>
+
+ <method name="retrieveListOfBlockedPopups">
+ <body>
+ <![CDATA[
+ this.messageManager.sendAsyncMessage("PopupBlocking:GetBlockedPopupList", null);
+ return new Promise(resolve => {
+ let self = this;
+ this.messageManager.addMessageListener("PopupBlocking:ReplyGetBlockedPopupList",
+ function replyReceived(msg) {
+ self.messageManager.removeMessageListener("PopupBlocking:ReplyGetBlockedPopupList",
+ replyReceived);
+ resolve(msg.data.popupData);
+ }
+ );
+ });
+ ]]>
+ </body>
+ </method>
+
+ <method name="unblockPopup">
+ <parameter name="aPopupIndex"/>
+ <body><![CDATA[
+ this.messageManager.sendAsyncMessage("PopupBlocking:UnblockPopup",
+ {index: aPopupIndex});
+ ]]></body>
+ </method>
+
+ <field name="blockedPopups">null</field>
+
+ <!-- Obsolete name for blockedPopups. Used by android. -->
+ <property name="pageReport"
+ onget="return this.blockedPopups;"
+ readonly="true"/>
+
+ <method name="audioPlaybackStarted">
+ <body>
+ <![CDATA[
+ let event = document.createEvent("Events");
+ event.initEvent("DOMAudioPlaybackStarted", true, false);
+ this.dispatchEvent(event);
+ if (this._audioBlocked) {
+ this._audioBlocked = false;
+ event = document.createEvent("Events");
+ event.initEvent("DOMAudioPlaybackBlockStopped", true, false);
+ this.dispatchEvent(event);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="audioPlaybackStopped">
+ <body>
+ <![CDATA[
+ let event = document.createEvent("Events");
+ event.initEvent("DOMAudioPlaybackStopped", true, false);
+ this.dispatchEvent(event);
+ ]]>
+ </body>
+ </method>
+
+ <method name="audioPlaybackBlocked">
+ <body>
+ <![CDATA[
+ this._audioBlocked = true;
+ let event = document.createEvent("Events");
+ event.initEvent("DOMAudioPlaybackBlockStarted", true, false);
+ this.dispatchEvent(event);
+ ]]>
+ </body>
+ </method>
+
+ <field name="_audioMuted">false</field>
+ <property name="audioMuted"
+ onget="return this._audioMuted;"
+ readonly="true"/>
+
+
+ <field name="_audioBlocked">false</field>
+ <property name="audioBlocked"
+ onget="return this._audioBlocked;"
+ readonly="true"/>
+
+ <method name="mute">
+ <body>
+ <![CDATA[
+ this._audioMuted = true;
+ this.messageManager.sendAsyncMessage("AudioPlayback",
+ {type: "mute"});
+ ]]>
+ </body>
+ </method>
+
+ <method name="unmute">
+ <body>
+ <![CDATA[
+ this._audioMuted = false;
+ this.messageManager.sendAsyncMessage("AudioPlayback",
+ {type: "unmute"});
+ ]]>
+ </body>
+ </method>
+
+ <method name="pauseMedia">
+ <parameter name="disposable"/>
+ <body>
+ <![CDATA[
+ let suspendedReason;
+ if (disposable) {
+ suspendedReason = "mediaControlPaused";
+ } else {
+ suspendedReason = "lostAudioFocusTransiently";
+ }
+
+ this.messageManager.sendAsyncMessage("AudioPlayback",
+ {type: suspendedReason});
+ ]]>
+ </body>
+ </method>
+
+ <method name="stopMedia">
+ <body>
+ <![CDATA[
+ this.messageManager.sendAsyncMessage("AudioPlayback",
+ {type: "mediaControlStopped"});
+ ]]>
+ </body>
+ </method>
+
+ <method name="blockMedia">
+ <body>
+ <![CDATA[
+ this._audioBlocked = true;
+ this.messageManager.sendAsyncMessage("AudioPlayback",
+ {type: "blockInactivePageMedia"});
+ ]]>
+ </body>
+ </method>
+
+ <method name="resumeMedia">
+ <body>
+ <![CDATA[
+ this._audioBlocked = false;
+ this.messageManager.sendAsyncMessage("AudioPlayback",
+ {type: "resumeMedia"});
+ let event = document.createEvent("Events");
+ event.initEvent("DOMAudioPlaybackBlockStopped", true, false);
+ this.dispatchEvent(event);
+ ]]>
+ </body>
+ </method>
+
+ <property name="securityUI">
+ <getter>
+ <![CDATA[
+ if (!this.docShell.securityUI) {
+ const SECUREBROWSERUI_CONTRACTID = "@mozilla.org/secure_browser_ui;1";
+ if (!this.hasAttribute("disablesecurity") &&
+ SECUREBROWSERUI_CONTRACTID in Components.classes) {
+ var securityUI = Components.classes[SECUREBROWSERUI_CONTRACTID]
+ .createInstance(Components.interfaces.nsISecureBrowserUI);
+ securityUI.init(this.contentWindow);
+ }
+ }
+
+ return this.docShell.securityUI;
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ this.docShell.securityUI = val;
+ ]]>
+ </setter>
+ </property>
+
+ <!-- increases or decreases the browser's network priority -->
+ <method name="adjustPriority">
+ <parameter name="adjustment"/>
+ <body><![CDATA[
+ let loadGroup = this.webNavigation.QueryInterface(Components.interfaces.nsIDocumentLoader)
+ .loadGroup.QueryInterface(Components.interfaces.nsISupportsPriority);
+ loadGroup.adjustPriority(adjustment);
+ ]]></body>
+ </method>
+
+ <!-- sets the browser's network priority to a discrete value -->
+ <method name="setPriority">
+ <parameter name="priority"/>
+ <body><![CDATA[
+ let loadGroup = this.webNavigation.QueryInterface(Components.interfaces.nsIDocumentLoader)
+ .loadGroup.QueryInterface(Components.interfaces.nsISupportsPriority);
+ loadGroup.priority = priority;
+ ]]></body>
+ </method>
+
+ <field name="urlbarChangeTracker">
+ ({
+ _startedLoadSinceLastUserTyping: false,
+
+ startedLoad() {
+ this._startedLoadSinceLastUserTyping = true;
+ },
+ finishedLoad() {
+ this._startedLoadSinceLastUserTyping = false;
+ },
+ userTyped() {
+ this._startedLoadSinceLastUserTyping = false;
+ },
+ })
+ </field>
+
+ <method name="didStartLoadSinceLastUserTyping">
+ <body><![CDATA[
+ return !this.inLoadURI &&
+ this.urlbarChangeTracker._startedLoadSinceLastUserTyping;
+ ]]></body>
+ </method>
+
+ <field name="_userTypedValue">
+ null
+ </field>
+
+ <property name="userTypedValue"
+ onget="return this._userTypedValue;">
+ <setter><![CDATA[
+ this.urlbarChangeTracker.userTyped();
+ this._userTypedValue = val;
+ return val;
+ ]]></setter>
+ </property>
+
+ <field name="mFormFillAttached">
+ false
+ </field>
+
+ <field name="isShowingMessage">
+ false
+ </field>
+
+ <field name="droppedLinkHandler">
+ null
+ </field>
+
+ <field name="mIconURL">null</field>
+
+ <!-- This is managed by the tabbrowser -->
+ <field name="lastURI">null</field>
+
+ <field name="mDestroyed">false</field>
+
+ <constructor>
+ <![CDATA[
+ try {
+ // |webNavigation.sessionHistory| will have been set by the frame
+ // loader when creating the docShell as long as this xul:browser
+ // doesn't have the 'disablehistory' attribute set.
+ if (this.docShell && this.webNavigation.sessionHistory) {
+ var os = Components.classes["@mozilla.org/observer-service;1"]
+ .getService(Components.interfaces.nsIObserverService);
+ os.addObserver(this, "browser:purge-session-history", true);
+
+ // enable global history if we weren't told otherwise
+ if (!this.hasAttribute("disableglobalhistory") && !this.isRemoteBrowser) {
+ try {
+ this.docShell.useGlobalHistory = true;
+ } catch (ex) {
+ // This can occur if the Places database is locked
+ Components.utils.reportError("Error enabling browser global history: " + ex);
+ }
+ }
+ }
+ }
+ catch (e) {
+ Components.utils.reportError(e);
+ }
+ try {
+ var securityUI = this.securityUI;
+ }
+ catch (e) {
+ }
+
+ // XXX tabbrowser.xml sets "relatedBrowser" as a direct property on
+ // some browsers before they are put into a DOM (and get a binding).
+ // This hack makes sure that we hold a weak reference to the other
+ // browser (and go through the proper getter and setter).
+ if (this.hasOwnProperty("relatedBrowser")) {
+ var relatedBrowser = this.relatedBrowser;
+ delete this.relatedBrowser;
+ this.relatedBrowser = relatedBrowser;
+ }
+
+ if (!this.isRemoteBrowser) {
+ this.addEventListener("pagehide", this.onPageHide, true);
+ }
+
+ if (this.messageManager) {
+ this.messageManager.addMessageListener("PopupBlocking:UpdateBlockedPopups", this);
+ this.messageManager.addMessageListener("Autoscroll:Start", this);
+ this.messageManager.addMessageListener("Autoscroll:Cancel", this);
+ this.messageManager.addMessageListener("AudioPlayback:Start", this);
+ this.messageManager.addMessageListener("AudioPlayback:Stop", this);
+ this.messageManager.addMessageListener("AudioPlayback:Block", this);
+ }
+ ]]>
+ </constructor>
+
+ <destructor>
+ <![CDATA[
+ this.destroy();
+ ]]>
+ </destructor>
+
+ <!-- This is necessary because the destructor doesn't always get called when
+ we are removed from a tabbrowser. This will be explicitly called by tabbrowser.
+
+ Note: this function is overriden in remote-browser.xml, so any clean-up that
+ also applies to browser.isRemoteBrowser = true must be duplicated there. -->
+ <method name="destroy">
+ <body>
+ <![CDATA[
+ if (this.mDestroyed)
+ return;
+ this.mDestroyed = true;
+
+ if (this.docShell && this.webNavigation.sessionHistory) {
+ var os = Components.classes["@mozilla.org/observer-service;1"]
+ .getService(Components.interfaces.nsIObserverService);
+ try {
+ os.removeObserver(this, "browser:purge-session-history");
+ } catch (ex) {
+ // It's not clear why this sometimes throws an exception.
+ }
+ }
+
+ this._fastFind = null;
+ this._webBrowserFind = null;
+
+ // The feeds cache can keep the document inside this browser alive.
+ this.feeds = null;
+
+ this.lastURI = null;
+
+ if (!this.isRemoteBrowser) {
+ this.removeEventListener("pagehide", this.onPageHide, true);
+ }
+
+ if (this._autoScrollNeedsCleanup) {
+ // we polluted the global scope, so clean it up
+ this._autoScrollPopup.parentNode.removeChild(this._autoScrollPopup);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <!--
+ We call this _receiveMessage (and alias receiveMessage to it) so that
+ bindings that inherit from this one can delegate to it.
+ -->
+ <method name="_receiveMessage">
+ <parameter name="aMessage"/>
+ <body><![CDATA[
+ let data = aMessage.data;
+ switch (aMessage.name) {
+ case "PopupBlocking:UpdateBlockedPopups": {
+ this.blockedPopups = {
+ length: data.count,
+ reported: !data.freshPopup,
+ };
+
+ this.updateBlockedPopups();
+ break;
+ }
+ case "Autoscroll:Start": {
+ if (!this.autoscrollEnabled) {
+ return false;
+ }
+ this.startScroll(data.scrolldir, data.screenX, data.screenY);
+ return true;
+ }
+ case "Autoscroll:Cancel":
+ this._autoScrollPopup.hidePopup();
+ break;
+ case "AudioPlayback:Start":
+ this.audioPlaybackStarted();
+ break;
+ case "AudioPlayback:Stop":
+ this.audioPlaybackStopped();
+ break;
+ case "AudioPlayback:Block":
+ this.audioPlaybackBlocked();
+ break;
+ }
+ return undefined;
+ ]]></body>
+ </method>
+
+ <method name="receiveMessage">
+ <parameter name="aMessage"/>
+ <body><![CDATA[
+ return this._receiveMessage(aMessage);
+ ]]></body>
+ </method>
+
+ <method name="observe">
+ <parameter name="aSubject"/>
+ <parameter name="aTopic"/>
+ <parameter name="aState"/>
+ <body>
+ <![CDATA[
+ if (aTopic != "browser:purge-session-history")
+ return;
+
+ this.purgeSessionHistory();
+ ]]>
+ </body>
+ </method>
+
+ <method name="purgeSessionHistory">
+ <body>
+ <![CDATA[
+ this.messageManager.sendAsyncMessage("Browser:PurgeSessionHistory");
+ ]]>
+ </body>
+ </method>
+
+ <method name="createAboutBlankContentViewer">
+ <parameter name="aPrincipal"/>
+ <body>
+ <![CDATA[
+ let principal = BrowserUtils.principalWithMatchingOA(aPrincipal, this.contentPrincipal);
+ this.docShell.createAboutBlankContentViewer(principal);
+ ]]>
+ </body>
+ </method>
+
+ <field name="_AUTOSCROLL_SNAP">10</field>
+ <field name="_scrolling">false</field>
+ <field name="_startX">null</field>
+ <field name="_startY">null</field>
+ <field name="_autoScrollPopup">null</field>
+ <field name="_autoScrollNeedsCleanup">false</field>
+
+ <method name="stopScroll">
+ <body>
+ <![CDATA[
+ if (this._scrolling) {
+ this._scrolling = false;
+ window.removeEventListener("mousemove", this, true);
+ window.removeEventListener("mousedown", this, true);
+ window.removeEventListener("mouseup", this, true);
+ window.removeEventListener("DOMMouseScroll", this, true);
+ window.removeEventListener("contextmenu", this, true);
+ window.removeEventListener("keydown", this, true);
+ window.removeEventListener("keypress", this, true);
+ window.removeEventListener("keyup", this, true);
+ this.messageManager.sendAsyncMessage("Autoscroll:Stop");
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="_createAutoScrollPopup">
+ <body>
+ <![CDATA[
+ const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ var popup = document.createElementNS(XUL_NS, "panel");
+ popup.className = "autoscroller";
+ // We set this attribute on the element so that mousemove
+ // events can be handled by browser-content.js.
+ popup.setAttribute("mousethrough", "always");
+ popup.setAttribute("rolluponmousewheel", "true");
+ return popup;
+ ]]>
+ </body>
+ </method>
+
+ <method name="startScroll">
+ <parameter name="scrolldir"/>
+ <parameter name="screenX"/>
+ <parameter name="screenY"/>
+ <body><![CDATA[
+ if (!this._autoScrollPopup) {
+ if (this.hasAttribute("autoscrollpopup")) {
+ // our creator provided a popup to share
+ this._autoScrollPopup = document.getElementById(this.getAttribute("autoscrollpopup"));
+ }
+ else {
+ // we weren't provided a popup; we have to use the global scope
+ this._autoScrollPopup = this._createAutoScrollPopup();
+ document.documentElement.appendChild(this._autoScrollPopup);
+ this._autoScrollNeedsCleanup = true;
+ }
+ }
+
+ // we need these attributes so themers don't need to create per-platform packages
+ if (screen.colorDepth > 8) { // need high color for transparency
+ // Exclude second-rate platforms
+ this._autoScrollPopup.setAttribute("transparent", !/BeOS|OS\/2/.test(navigator.appVersion));
+ // Enable translucency on Windows and Mac
+ this._autoScrollPopup.setAttribute("translucent", /Win|Mac/.test(navigator.platform));
+ }
+
+ this._autoScrollPopup.setAttribute("noautofocus", "true");
+ this._autoScrollPopup.setAttribute("scrolldir", scrolldir);
+ this._autoScrollPopup.addEventListener("popuphidden", this, true);
+ this._autoScrollPopup.showPopup(document.documentElement,
+ screenX,
+ screenY,
+ "popup", null, null);
+ this._ignoreMouseEvents = true;
+ this._scrolling = true;
+ this._startX = screenX;
+ this._startY = screenY;
+
+ window.addEventListener("mousemove", this, true);
+ window.addEventListener("mousedown", this, true);
+ window.addEventListener("mouseup", this, true);
+ window.addEventListener("DOMMouseScroll", this, true);
+ window.addEventListener("contextmenu", this, true);
+ window.addEventListener("keydown", this, true);
+ window.addEventListener("keypress", this, true);
+ window.addEventListener("keyup", this, true);
+ ]]>
+ </body>
+ </method>
+
+ <method name="handleEvent">
+ <parameter name="aEvent"/>
+ <body>
+ <![CDATA[
+ if (this._scrolling) {
+ switch (aEvent.type) {
+ case "mousemove": {
+ var x = aEvent.screenX - this._startX;
+ var y = aEvent.screenY - this._startY;
+
+ if ((x > this._AUTOSCROLL_SNAP || x < -this._AUTOSCROLL_SNAP) ||
+ (y > this._AUTOSCROLL_SNAP || y < -this._AUTOSCROLL_SNAP))
+ this._ignoreMouseEvents = false;
+ break;
+ }
+ case "mouseup":
+ case "mousedown":
+ case "contextmenu": {
+ if (!this._ignoreMouseEvents) {
+ // Use a timeout to prevent the mousedown from opening the popup again.
+ // Ideally, we could use preventDefault here, but contenteditable
+ // and middlemouse paste don't interact well. See bug 1188536.
+ setTimeout(() => this._autoScrollPopup.hidePopup(), 0);
+ }
+ this._ignoreMouseEvents = false;
+ break;
+ }
+ case "DOMMouseScroll": {
+ this._autoScrollPopup.hidePopup();
+ event.preventDefault();
+ break;
+ }
+ case "popuphidden": {
+ this._autoScrollPopup.removeEventListener("popuphidden", this, true);
+ this.stopScroll();
+ break;
+ }
+ case "keydown": {
+ if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) {
+ // the escape key will be processed by
+ // nsXULPopupManager::KeyDown and the panel will be closed.
+ // So, don't consume the key event here.
+ break;
+ }
+ // don't break here. we need to eat keydown events.
+ }
+ case "keypress":
+ case "keyup": {
+ // All keyevents should be eaten here during autoscrolling.
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ break;
+ }
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="closeBrowser">
+ <body>
+ <![CDATA[
+ // The request comes from a XPCOM component, we'd want to redirect
+ // the request to tabbrowser.
+ let tabbrowser = this.getTabBrowser();
+ if (tabbrowser) {
+ let tab = tabbrowser.getTabForBrowser(this);
+ if (tab) {
+ tabbrowser.removeTab(tab);
+ return;
+ }
+ }
+
+ throw new Error("Closing a browser which was not attached to a tabbrowser is unsupported.");
+ ]]>
+ </body>
+ </method>
+
+ <method name="swapBrowsers">
+ <parameter name="aOtherBrowser"/>
+ <body>
+ <![CDATA[
+ // The request comes from a XPCOM component, we'd want to redirect
+ // the request to tabbrowser so tabbrowser will be setup correctly,
+ // and it will eventually call swapDocShells.
+ let tabbrowser = this.getTabBrowser();
+ if (tabbrowser) {
+ let tab = tabbrowser.getTabForBrowser(this);
+ if (tab) {
+ tabbrowser.swapBrowsers(tab, aOtherBrowser);
+ return;
+ }
+ }
+
+ // If we're not attached to a tabbrowser, just swap.
+ this.swapDocShells(aOtherBrowser);
+ ]]>
+ </body>
+ </method>
+
+ <method name="swapDocShells">
+ <parameter name="aOtherBrowser"/>
+ <body>
+ <![CDATA[
+ if (this.isRemoteBrowser != aOtherBrowser.isRemoteBrowser)
+ throw new Error("Can only swap docshells between browsers in the same process.");
+
+ // Give others a chance to swap state.
+ // IMPORTANT: Since a swapDocShells call does not swap the messageManager
+ // instances attached to a browser to aOtherBrowser, others
+ // will need to add the message listeners to the new
+ // messageManager.
+ // This is not a bug in swapDocShells or the FrameLoader,
+ // merely a design decision: If message managers were swapped,
+ // so that no new listeners were needed, the new
+ // aOtherBrowser.messageManager would have listeners pointing
+ // to the JS global of the current browser, which would rather
+ // easily create leaks while swapping.
+ // IMPORTANT2: When the current browser element is removed from DOM,
+ // which is quite common after a swpDocShells call, its
+ // frame loader is destroyed, and that destroys the relevant
+ // message manager, which will remove the listeners.
+ let event = new CustomEvent("SwapDocShells", {"detail": aOtherBrowser});
+ this.dispatchEvent(event);
+ event = new CustomEvent("SwapDocShells", {"detail": this});
+ aOtherBrowser.dispatchEvent(event);
+
+ // We need to swap fields that are tied to our docshell or related to
+ // the loaded page
+ // Fields which are built as a result of notifactions (pageshow/hide,
+ // DOMLinkAdded/Removed, onStateChange) should not be swapped here,
+ // because these notifications are dispatched again once the docshells
+ // are swapped.
+ var fieldsToSwap = [
+ "_docShell",
+ "_webBrowserFind",
+ "_contentWindow",
+ "_webNavigation"
+ ];
+
+ if (this.isRemoteBrowser) {
+ fieldsToSwap.push(...[
+ "_remoteWebNavigation",
+ "_remoteWebNavigationImpl",
+ "_remoteWebProgressManager",
+ "_remoteWebProgress",
+ "_remoteFinder",
+ "_securityUI",
+ "_documentURI",
+ "_documentContentType",
+ "_contentTitle",
+ "_characterSet",
+ "_contentPrincipal",
+ "_imageDocument",
+ "_fullZoom",
+ "_textZoom",
+ "_isSyntheticDocument",
+ "_innerWindowID",
+ "_manifestURI",
+ ]);
+ }
+
+ var ourFieldValues = {};
+ var otherFieldValues = {};
+ for (let field of fieldsToSwap) {
+ ourFieldValues[field] = this[field];
+ otherFieldValues[field] = aOtherBrowser[field];
+ }
+
+ if (window.PopupNotifications)
+ PopupNotifications._swapBrowserNotifications(aOtherBrowser, this);
+
+ try {
+ this.swapFrameLoaders(aOtherBrowser);
+ } catch (ex) {
+ // This may not be implemented for browser elements that are not
+ // attached to a BrowserDOMWindow.
+ }
+
+ for (let field of fieldsToSwap) {
+ this[field] = otherFieldValues[field];
+ aOtherBrowser[field] = ourFieldValues[field];
+ }
+
+ if (!this.isRemoteBrowser) {
+ // Null the current nsITypeAheadFind instances so that they're
+ // lazily re-created on access. We need to do this because they
+ // might have attached the wrong docShell.
+ this._fastFind = aOtherBrowser._fastFind = null;
+ }
+ else {
+ // Rewire the remote listeners
+ this._remoteWebNavigationImpl.swapBrowser(this);
+ aOtherBrowser._remoteWebNavigationImpl.swapBrowser(aOtherBrowser);
+
+ if (this._remoteWebProgressManager && aOtherBrowser._remoteWebProgressManager) {
+ this._remoteWebProgressManager.swapBrowser(this);
+ aOtherBrowser._remoteWebProgressManager.swapBrowser(aOtherBrowser);
+ }
+
+ if (this._remoteFinder)
+ this._remoteFinder.swapBrowser(this);
+ if (aOtherBrowser._remoteFinder)
+ aOtherBrowser._remoteFinder.swapBrowser(aOtherBrowser);
+ }
+
+ event = new CustomEvent("EndSwapDocShells", {"detail": aOtherBrowser});
+ this.dispatchEvent(event);
+ event = new CustomEvent("EndSwapDocShells", {"detail": this});
+ aOtherBrowser.dispatchEvent(event);
+ ]]>
+ </body>
+ </method>
+
+ <method name="getInPermitUnload">
+ <parameter name="aCallback"/>
+ <body>
+ <![CDATA[
+ if (!this.docShell || !this.docShell.contentViewer) {
+ aCallback(false);
+ return;
+ }
+ aCallback(this.docShell.contentViewer.inPermitUnload);
+ ]]>
+ </body>
+ </method>
+
+ <method name="permitUnload">
+ <body>
+ <![CDATA[
+ if (!this.docShell || !this.docShell.contentViewer) {
+ return {permitUnload: true, timedOut: false};
+ }
+ return {permitUnload: this.docShell.contentViewer.permitUnload(), timedOut: false};
+ ]]>
+ </body>
+ </method>
+
+ <method name="print">
+ <parameter name="aOuterWindowID"/>
+ <parameter name="aPrintSettings"/>
+ <parameter name="aPrintProgressListener"/>
+ <body>
+ <![CDATA[
+ var owner = this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner);
+ if (!owner.frameLoader) {
+ throw Components.Exception("No frame loader.",
+ Components.results.NS_ERROR_FAILURE);
+ }
+
+ owner.frameLoader.print(aOuterWindowID, aPrintSettings,
+ aPrintProgressListener);
+ ]]>
+ </body>
+ </method>
+
+ <method name="dropLinks">
+ <parameter name="aLinksCount"/>
+ <parameter name="aLinks"/>
+ <body><![CDATA[
+ if (!this.droppedLinkHandler) {
+ return false;
+ }
+ let links = [];
+ for (let i = 0; i < aLinksCount; i += 3) {
+ links.push({
+ url: aLinks[i],
+ name: aLinks[i + 1],
+ type: aLinks[i + 2],
+ });
+ }
+ this.droppedLinkHandler(null, links);
+ return true;
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="keypress" keycode="VK_F7" group="system">
+ <![CDATA[
+ if (event.defaultPrevented || !event.isTrusted)
+ return;
+
+ const kPrefShortcutEnabled = "accessibility.browsewithcaret_shortcut.enabled";
+ const kPrefWarnOnEnable = "accessibility.warn_on_browsewithcaret";
+ const kPrefCaretBrowsingOn = "accessibility.browsewithcaret";
+
+ var isEnabled = this.mPrefs.getBoolPref(kPrefShortcutEnabled);
+ if (!isEnabled)
+ return;
+
+ // Toggle browse with caret mode
+ var browseWithCaretOn = false;
+ var warn = true;
+
+ try {
+ warn = this.mPrefs.getBoolPref(kPrefWarnOnEnable);
+ } catch (ex) {
+ }
+
+ try {
+ browseWithCaretOn = this.mPrefs.getBoolPref(kPrefCaretBrowsingOn);
+ } catch (ex) {
+ }
+ if (warn && !browseWithCaretOn) {
+ var checkValue = {value:false};
+ var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
+ .getService(Components.interfaces.nsIPromptService);
+
+ var buttonPressed = promptService.confirmEx(window,
+ this.mStrBundle.GetStringFromName('browsewithcaret.checkWindowTitle'),
+ this.mStrBundle.GetStringFromName('browsewithcaret.checkLabel'),
+ // Make "No" the default:
+ promptService.STD_YES_NO_BUTTONS | promptService.BUTTON_POS_1_DEFAULT,
+ null, null, null, this.mStrBundle.GetStringFromName('browsewithcaret.checkMsg'),
+ checkValue);
+ if (buttonPressed != 0) {
+ if (checkValue.value) {
+ try {
+ this.mPrefs.setBoolPref(kPrefShortcutEnabled, false);
+ } catch (ex) {
+ }
+ }
+ return;
+ }
+ if (checkValue.value) {
+ try {
+ this.mPrefs.setBoolPref(kPrefWarnOnEnable, false);
+ }
+ catch (ex) {
+ }
+ }
+ }
+
+ // Toggle the pref
+ try {
+ this.mPrefs.setBoolPref(kPrefCaretBrowsingOn, !browseWithCaretOn);
+ } catch (ex) {
+ }
+ ]]>
+ </handler>
+ <handler event="dragover" group="system">
+ <![CDATA[
+ if (!this.droppedLinkHandler || event.defaultPrevented)
+ return;
+
+ // For drags that appear to be internal text (for example, tab drags),
+ // set the dropEffect to 'none'. This prevents the drop even if some
+ // other listener cancelled the event.
+ var types = event.dataTransfer.types;
+ if (types.includes("text/x-moz-text-internal") && !types.includes("text/plain")) {
+ event.dataTransfer.dropEffect = "none";
+ event.stopPropagation();
+ event.preventDefault();
+ }
+
+ // No need to handle "dragover" in e10s, since nsDocShellTreeOwner.cpp in the child process
+ // handles that case using "@mozilla.org/content/dropped-link-handler;1" service.
+ if (this.isRemoteBrowser)
+ return;
+
+ let linkHandler = Components.classes["@mozilla.org/content/dropped-link-handler;1"].
+ getService(Components.interfaces.nsIDroppedLinkHandler);
+ if (linkHandler.canDropLink(event, false))
+ event.preventDefault();
+ ]]>
+ </handler>
+ <handler event="drop" group="system">
+ <![CDATA[
+ // No need to handle "drop" in e10s, since nsDocShellTreeOwner.cpp in the child process
+ // handles that case using "@mozilla.org/content/dropped-link-handler;1" service.
+ if (!this.droppedLinkHandler || event.defaultPrevented || this.isRemoteBrowser)
+ return;
+
+ let name = { };
+ let linkHandler = Components.classes["@mozilla.org/content/dropped-link-handler;1"].
+ getService(Components.interfaces.nsIDroppedLinkHandler);
+ try {
+ // Pass true to prevent the dropping of javascript:/data: URIs
+ var links = linkHandler.dropLinks(event, true);
+ } catch (ex) {
+ return;
+ }
+
+ if (links.length) {
+ this.droppedLinkHandler(event, links);
+ }
+ ]]>
+ </handler>
+ </handlers>
+
+ </binding>
+
+</bindings>
diff --git a/toolkit/content/widgets/button.xml b/toolkit/content/widgets/button.xml
new file mode 100644
index 000000000..89d9d86c6
--- /dev/null
+++ b/toolkit/content/widgets/button.xml
@@ -0,0 +1,389 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<bindings id="buttonBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="button-base" extends="chrome://global/content/bindings/general.xml#basetext" role="xul:button">
+ <implementation implements="nsIDOMXULButtonElement">
+ <property name="type"
+ onget="return this.getAttribute('type');"
+ onset="this.setAttribute('type', val); return val;"/>
+
+ <property name="dlgType"
+ onget="return this.getAttribute('dlgtype');"
+ onset="this.setAttribute('dlgtype', val); return val;"/>
+
+ <property name="group"
+ onget="return this.getAttribute('group');"
+ onset="this.setAttribute('group', val); return val;"/>
+
+ <property name="open" onget="return this.hasAttribute('open');">
+ <setter><![CDATA[
+ if (this.boxObject instanceof MenuBoxObject) {
+ this.boxObject.openMenu(val);
+ } else if (val) {
+ // Fall back to just setting the attribute
+ this.setAttribute('open', 'true');
+ } else {
+ this.removeAttribute('open');
+ }
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="checked" onget="return this.hasAttribute('checked');">
+ <setter><![CDATA[
+ if (this.type == "checkbox") {
+ this.checkState = val ? 1 : 0;
+ } else if (this.type == "radio" && val) {
+ var sibs = this.parentNode.getElementsByAttribute("group", this.group);
+ for (var i = 0; i < sibs.length; ++i)
+ sibs[i].removeAttribute("checked");
+ }
+
+ if (val)
+ this.setAttribute("checked", "true");
+ else
+ this.removeAttribute("checked");
+
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="checkState">
+ <getter><![CDATA[
+ var state = this.getAttribute("checkState");
+ if (state == "")
+ return this.checked ? 1 : 0;
+ if (state == "0")
+ return 0;
+ if (state == "2")
+ return 2;
+ return 1;
+ ]]></getter>
+ <setter><![CDATA[
+ this.setAttribute("checkState", val);
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="autoCheck"
+ onget="return this.getAttribute('autoCheck') == 'true';"
+ onset="this.setAttribute('autoCheck', val); return val;"/>
+
+ <method name ="filterButtons">
+ <parameter name="node"/>
+ <body>
+ <![CDATA[
+ // if the node isn't visible, don't descend into it.
+ var cs = node.ownerDocument.defaultView.getComputedStyle(node, null);
+ if (cs.visibility != "visible" || cs.display == "none") {
+ return NodeFilter.FILTER_REJECT;
+ }
+ // but it may be a popup element, in which case we look at "state"...
+ if (cs.display == "-moz-popup" && node.state != "open") {
+ return NodeFilter.FILTER_REJECT;
+ }
+ // OK - the node seems visible, so it is a candidate.
+ if (node.localName == "button" && node.accessKey && !node.disabled)
+ return NodeFilter.FILTER_ACCEPT;
+ return NodeFilter.FILTER_SKIP;
+ ]]>
+ </body>
+ </method>
+
+ <method name="fireAccessKeyButton">
+ <parameter name="aSubtree"/>
+ <parameter name="aAccessKeyLower"/>
+ <body>
+ <![CDATA[
+ var iterator = aSubtree.ownerDocument.createTreeWalker(aSubtree,
+ NodeFilter.SHOW_ELEMENT,
+ this.filterButtons);
+ while (iterator.nextNode()) {
+ var test = iterator.currentNode;
+ if (test.accessKey.toLowerCase() == aAccessKeyLower &&
+ !test.disabled && !test.collapsed && !test.hidden) {
+ test.focus();
+ test.click();
+ return true;
+ }
+ }
+ return false;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_handleClick">
+ <body>
+ <![CDATA[
+ if (!this.disabled &&
+ (this.autoCheck || !this.hasAttribute("autoCheck"))) {
+
+ if (this.type == "checkbox") {
+ this.checked = !this.checked;
+ } else if (this.type == "radio") {
+ this.checked = true;
+ }
+ }
+ ]]>
+ </body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <!-- While it would seem we could do this by handling oncommand, we can't
+ because any external oncommand handlers might get called before ours,
+ and then they would see the incorrect value of checked. Additionally
+ a command attribute would redirect the command events anyway.-->
+ <handler event="click" button="0" action="this._handleClick();"/>
+ <handler event="keypress" key=" ">
+ <![CDATA[
+ this._handleClick();
+ // Prevent page from scrolling on the space key.
+ event.preventDefault();
+ ]]>
+ </handler>
+
+ <handler event="keypress">
+ <![CDATA[
+ if (this.boxObject instanceof MenuBoxObject) {
+ if (this.open)
+ return;
+ } else {
+ if (event.keyCode == KeyEvent.DOM_VK_UP ||
+ (event.keyCode == KeyEvent.DOM_VK_LEFT &&
+ document.defaultView.getComputedStyle(this.parentNode, "")
+ .direction == "ltr") ||
+ (event.keyCode == KeyEvent.DOM_VK_RIGHT &&
+ document.defaultView.getComputedStyle(this.parentNode, "")
+ .direction == "rtl")) {
+ event.preventDefault();
+ window.document.commandDispatcher.rewindFocus();
+ return;
+ }
+
+ if (event.keyCode == KeyEvent.DOM_VK_DOWN ||
+ (event.keyCode == KeyEvent.DOM_VK_RIGHT &&
+ document.defaultView.getComputedStyle(this.parentNode, "")
+ .direction == "ltr") ||
+ (event.keyCode == KeyEvent.DOM_VK_LEFT &&
+ document.defaultView.getComputedStyle(this.parentNode, "")
+ .direction == "rtl")) {
+ event.preventDefault();
+ window.document.commandDispatcher.advanceFocus();
+ return;
+ }
+ }
+
+ if (event.keyCode || event.charCode <= 32 || event.altKey ||
+ event.ctrlKey || event.metaKey)
+ return; // No printable char pressed, not a potential accesskey
+
+ // Possible accesskey pressed
+ var charPressedLower = String.fromCharCode(event.charCode).toLowerCase();
+
+ // If the accesskey of the current button is pressed, just activate it
+ if (this.accessKey.toLowerCase() == charPressedLower) {
+ this.click();
+ return;
+ }
+
+ // Search for accesskey in the list of buttons for this doc and each subdoc
+ // Get the buttons for the main document and all sub-frames
+ for (var frameCount = -1; frameCount < window.top.frames.length; frameCount++) {
+ var doc = (frameCount == -1)? window.top.document:
+ window.top.frames[frameCount].document
+ if (this.fireAccessKeyButton(doc.documentElement, charPressedLower))
+ return;
+ }
+
+ // Test anonymous buttons
+ var dlg = window.top.document;
+ var buttonBox = dlg.getAnonymousElementByAttribute(dlg.documentElement,
+ "anonid", "buttons");
+ if (buttonBox)
+ this.fireAccessKeyButton(buttonBox, charPressedLower);
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="button" display="xul:button"
+ extends="chrome://global/content/bindings/button.xml#button-base">
+ <resources>
+ <stylesheet src="chrome://global/skin/button.css"/>
+ </resources>
+
+ <content>
+ <children includes="observes|template|menupopup|panel|tooltip"/>
+ <xul:hbox class="box-inherit button-box" xbl:inherits="align,dir,pack,orient"
+ align="center" pack="center" flex="1" anonid="button-box">
+ <children>
+ <xul:image class="button-icon" xbl:inherits="src=image"/>
+ <xul:label class="button-text" xbl:inherits="value=label,accesskey,crop"/>
+ </children>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="menu" display="xul:menu"
+ extends="chrome://global/content/bindings/button.xml#button">
+ <content>
+ <children includes="observes|template|menupopup|panel|tooltip"/>
+ <xul:hbox class="box-inherit button-box" xbl:inherits="align,dir,pack,orient"
+ align="center" pack="center" flex="1">
+ <children>
+ <xul:hbox class="box-inherit" xbl:inherits="align,dir,pack,orient"
+ align="center" pack="center" flex="1">
+ <xul:image class="button-icon" xbl:inherits="src=image"/>
+ <xul:label class="button-text" xbl:inherits="value=label,accesskey,crop"/>
+ </xul:hbox>
+ <xul:dropmarker class="button-menu-dropmarker" xbl:inherits="open,disabled,label"/>
+ </children>
+ </xul:hbox>
+ </content>
+
+ <handlers>
+ <handler event="keypress" keycode="VK_RETURN" action="this.open = true;"/>
+ <handler event="keypress" key=" ">
+ <![CDATA[
+ this.open = true;
+ // Prevent page from scrolling on the space key.
+ event.preventDefault();
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="menu-button-base"
+ extends="chrome://global/content/bindings/button.xml#button-base">
+ <implementation implements="nsIDOMEventListener">
+ <constructor>
+ this.init();
+ </constructor>
+
+ <method name="init">
+ <body>
+ <![CDATA[
+ var btn = document.getAnonymousElementByAttribute(this, "anonid", "button");
+ if (!btn)
+ throw "XBL binding for <button type=\"menu-button\"/> binding must contain an element with anonid=\"button\"";
+
+ var menubuttonParent = this;
+ btn.addEventListener("mouseover", function() {
+ if (!this.disabled)
+ menubuttonParent.buttonover = true;
+ }, true);
+ btn.addEventListener("mouseout", function() {
+ menubuttonParent.buttonover = false;
+ }, true);
+ btn.addEventListener("mousedown", function() {
+ if (!this.disabled) {
+ menubuttonParent.buttondown = true;
+ document.addEventListener("mouseup", menubuttonParent, true);
+ }
+ }, true);
+ ]]>
+ </body>
+ </method>
+
+ <property name="buttonover" onget="return this.getAttribute('buttonover');">
+ <setter>
+ <![CDATA[
+ var v = val || val == "true";
+ if (!v && this.buttondown) {
+ this.buttondown = false;
+ this._pendingActive = true;
+ }
+ else if (this._pendingActive) {
+ this.buttondown = true;
+ this._pendingActive = false;
+ }
+
+ if (v)
+ this.setAttribute("buttonover", "true");
+ else
+ this.removeAttribute("buttonover");
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="buttondown" onget="return this.getAttribute('buttondown') == 'true';">
+ <setter>
+ <![CDATA[
+ if (val || val == "true")
+ this.setAttribute("buttondown", "true");
+ else
+ this.removeAttribute("buttondown");
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <field name="_pendingActive">false</field>
+
+ <method name="handleEvent">
+ <parameter name="aEvent"/>
+ <body>
+ <![CDATA[
+ this._pendingActive = false;
+ this.buttondown = false;
+ document.removeEventListener("mouseup", this, true);
+ ]]>
+ </body>
+ </method>
+
+ </implementation>
+
+ <handlers>
+ <handler event="keypress" keycode="VK_RETURN">
+ if (event.originalTarget == this)
+ this.open = true;
+ </handler>
+ <handler event="keypress" key=" ">
+ if (event.originalTarget == this) {
+ this.open = true;
+ // Prevent page from scrolling on the space key.
+ event.preventDefault();
+ }
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="menu-button" display="xul:menu"
+ extends="chrome://global/content/bindings/button.xml#menu-button-base">
+ <resources>
+ <stylesheet src="chrome://global/skin/button.css"/>
+ </resources>
+
+ <content>
+ <children includes="observes|template|menupopup|panel|tooltip"/>
+ <xul:button class="box-inherit button-menubutton-button"
+ anonid="button" flex="1" allowevents="true"
+ xbl:inherits="disabled,crop,image,label,accesskey,command,
+ buttonover,buttondown,align,dir,pack,orient">
+ <children/>
+ </xul:button>
+ <xul:dropmarker class="button-menubutton-dropmarker" xbl:inherits="open,disabled,label"/>
+ </content>
+ </binding>
+
+ <binding id="button-image" display="xul:button"
+ extends="chrome://global/content/bindings/button.xml#button">
+ <content>
+ <xul:image class="button-image-icon" xbl:inherits="src=image"/>
+ </content>
+ </binding>
+
+ <binding id="button-repeat" display="xul:autorepeatbutton"
+ extends="chrome://global/content/bindings/button.xml#button"/>
+
+</bindings>
diff --git a/toolkit/content/widgets/checkbox.xml b/toolkit/content/widgets/checkbox.xml
new file mode 100644
index 000000000..c6a5babfd
--- /dev/null
+++ b/toolkit/content/widgets/checkbox.xml
@@ -0,0 +1,84 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<bindings id="checkboxBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="checkbox" extends="chrome://global/content/bindings/checkbox.xml#checkbox-baseline">
+ <resources>
+ <stylesheet src="chrome://global/skin/checkbox.css"/>
+ </resources>
+ </binding>
+
+ <binding id="checkbox-baseline" role="xul:checkbox"
+ extends="chrome://global/content/bindings/general.xml#basetext">
+ <content>
+ <xul:image class="checkbox-check" xbl:inherits="checked,disabled"/>
+ <xul:hbox class="checkbox-label-box" flex="1">
+ <xul:image class="checkbox-icon" xbl:inherits="src"/>
+ <xul:label class="checkbox-label" xbl:inherits="xbl:text=label,accesskey,crop" flex="1"/>
+ </xul:hbox>
+ </content>
+
+ <implementation implements="nsIDOMXULCheckboxElement">
+ <method name="setChecked">
+ <parameter name="aValue"/>
+ <body>
+ <![CDATA[
+ var change = (aValue != (this.getAttribute('checked') == 'true'));
+ if (aValue)
+ this.setAttribute('checked', 'true');
+ else
+ this.removeAttribute('checked');
+ if (change) {
+ var event = document.createEvent('Events');
+ event.initEvent('CheckboxStateChange', true, true);
+ this.dispatchEvent(event);
+ }
+ return aValue;
+ ]]>
+ </body>
+ </method>
+
+ <!-- public implementation -->
+ <property name="checked" onset="return this.setChecked(val);"
+ onget="return this.getAttribute('checked') == 'true';"/>
+ </implementation>
+
+ <handlers>
+ <!-- While it would seem we could do this by handling oncommand, we need can't
+ because any external oncommand handlers might get called before ours, and
+ then they would see the incorrect value of checked. -->
+ <handler event="click" button="0" action="if (!this.disabled) this.checked = !this.checked;"/>
+ <handler event="keypress" key=" ">
+ <![CDATA[
+ this.checked = !this.checked;
+ // Prevent page from scrolling on the space key.
+ event.preventDefault();
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="checkbox-with-spacing"
+ extends="chrome://global/content/bindings/checkbox.xml#checkbox">
+
+ <content>
+ <xul:hbox class="checkbox-spacer-box">
+ <xul:image class="checkbox-check" xbl:inherits="checked,disabled"/>
+ </xul:hbox>
+ <xul:hbox class="checkbox-label-center-box" flex="1">
+ <xul:hbox class="checkbox-label-box" flex="1">
+ <xul:image class="checkbox-icon" xbl:inherits="src"/>
+ <xul:label class="checkbox-label" xbl:inherits="xbl:text=label,accesskey,crop" flex="1"/>
+ </xul:hbox>
+ </xul:hbox>
+ </content>
+ </binding>
+
+</bindings>
diff --git a/toolkit/content/widgets/colorpicker.xml b/toolkit/content/widgets/colorpicker.xml
new file mode 100644
index 000000000..30f8a6354
--- /dev/null
+++ b/toolkit/content/widgets/colorpicker.xml
@@ -0,0 +1,565 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<bindings id="colorpickerBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="colorpicker" extends="chrome://global/content/bindings/general.xml#basecontrol">
+ <resources>
+ <stylesheet src="chrome://global/skin/colorpicker.css"/>
+ </resources>
+
+ <content>
+ <xul:vbox flex="1">
+
+ <xul:hbox>
+ <xul:image class="colorpickertile cp-light" color="#FFFFFF"/>
+ <xul:image class="colorpickertile cp-light" color="#FFCCCC"/>
+ <xul:image class="colorpickertile cp-light" color="#FFCC99"/>
+ <xul:image class="colorpickertile cp-light" color="#FFFF99"/>
+ <xul:image class="colorpickertile cp-light" color="#FFFFCC"/>
+ <xul:image class="colorpickertile cp-light" color="#99FF99"/>
+ <xul:image class="colorpickertile cp-light" color="#99FFFF"/>
+ <xul:image class="colorpickertile cp-light" color="#CCFFFF"/>
+ <xul:image class="colorpickertile cp-light" color="#CCCCFF"/>
+ <xul:image class="colorpickertile cp-light" color="#FFCCFF"/>
+ </xul:hbox>
+ <xul:hbox>
+ <xul:image class="colorpickertile" color="#CCCCCC"/>
+ <xul:image class="colorpickertile" color="#FF6666"/>
+ <xul:image class="colorpickertile" color="#FF9966"/>
+ <xul:image class="colorpickertile cp-light" color="#FFFF66"/>
+ <xul:image class="colorpickertile cp-light" color="#FFFF33"/>
+ <xul:image class="colorpickertile cp-light" color="#66FF99"/>
+ <xul:image class="colorpickertile cp-light" color="#33FFFF"/>
+ <xul:image class="colorpickertile cp-light" color="#66FFFF"/>
+ <xul:image class="colorpickertile" color="#9999FF"/>
+ <xul:image class="colorpickertile" color="#FF99FF"/>
+ </xul:hbox>
+ <xul:hbox>
+ <xul:image class="colorpickertile" color="#C0C0C0"/>
+ <xul:image class="colorpickertile" color="#FF0000"/>
+ <xul:image class="colorpickertile" color="#FF9900"/>
+ <xul:image class="colorpickertile" color="#FFCC66"/>
+ <xul:image class="colorpickertile cp-light" color="#FFFF00"/>
+ <xul:image class="colorpickertile cp-light" color="#33FF33"/>
+ <xul:image class="colorpickertile" color="#66CCCC"/>
+ <xul:image class="colorpickertile" color="#33CCFF"/>
+ <xul:image class="colorpickertile" color="#6666CC"/>
+ <xul:image class="colorpickertile" color="#CC66CC"/>
+ </xul:hbox>
+ <xul:hbox>
+ <xul:image class="colorpickertile" color="#999999"/>
+ <xul:image class="colorpickertile" color="#CC0000"/>
+ <xul:image class="colorpickertile" color="#FF6600"/>
+ <xul:image class="colorpickertile" color="#FFCC33"/>
+ <xul:image class="colorpickertile" color="#FFCC00"/>
+ <xul:image class="colorpickertile" color="#33CC00"/>
+ <xul:image class="colorpickertile" color="#00CCCC"/>
+ <xul:image class="colorpickertile" color="#3366FF"/>
+ <xul:image class="colorpickertile" color="#6633FF"/>
+ <xul:image class="colorpickertile" color="#CC33CC"/>
+ </xul:hbox>
+ <xul:hbox>
+ <xul:image class="colorpickertile" color="#666666"/>
+ <xul:image class="colorpickertile" color="#990000"/>
+ <xul:image class="colorpickertile" color="#CC6600"/>
+ <xul:image class="colorpickertile" color="#CC9933"/>
+ <xul:image class="colorpickertile" color="#999900"/>
+ <xul:image class="colorpickertile" color="#009900"/>
+ <xul:image class="colorpickertile" color="#339999"/>
+ <xul:image class="colorpickertile" color="#3333FF"/>
+ <xul:image class="colorpickertile" color="#6600CC"/>
+ <xul:image class="colorpickertile" color="#993399"/>
+ </xul:hbox>
+ <xul:hbox>
+ <xul:image class="colorpickertile" color="#333333"/>
+ <xul:image class="colorpickertile" color="#660000"/>
+ <xul:image class="colorpickertile" color="#993300"/>
+ <xul:image class="colorpickertile" color="#996633"/>
+ <xul:image class="colorpickertile" color="#666600"/>
+ <xul:image class="colorpickertile" color="#006600"/>
+ <xul:image class="colorpickertile" color="#336666"/>
+ <xul:image class="colorpickertile" color="#000099"/>
+ <xul:image class="colorpickertile" color="#333399"/>
+ <xul:image class="colorpickertile" color="#663366"/>
+ </xul:hbox>
+ <xul:hbox>
+ <xul:image class="colorpickertile" color="#000000"/>
+ <xul:image class="colorpickertile" color="#330000"/>
+ <xul:image class="colorpickertile" color="#663300"/>
+ <xul:image class="colorpickertile" color="#663333"/>
+ <xul:image class="colorpickertile" color="#333300"/>
+ <xul:image class="colorpickertile" color="#003300"/>
+ <xul:image class="colorpickertile" color="#003333"/>
+ <xul:image class="colorpickertile" color="#000066"/>
+ <xul:image class="colorpickertile" color="#330099"/>
+ <xul:image class="colorpickertile" color="#330033"/>
+ </xul:hbox>
+ </xul:vbox>
+ <!-- Something to take tab focus
+ <button style="border : 0px; width: 0px; height: 0px;"/>
+ -->
+ </content>
+
+ <implementation implements="nsIDOMEventListener">
+ <property name="color">
+ <getter><![CDATA[
+ return this.mSelectedCell ? this.mSelectedCell.getAttribute("color") : null;
+ ]]></getter>
+ <setter><![CDATA[
+ if (!val)
+ return val;
+ var uppercaseVal = val.toUpperCase();
+ // Translate standard HTML color strings:
+ if (uppercaseVal[0] != "#") {
+ switch (uppercaseVal) {
+ case "GREEN":
+ uppercaseVal = "#008000";
+ break;
+ case "LIME":
+ uppercaseVal = "#00FF00";
+ break;
+ case "OLIVE":
+ uppercaseVal = "#808000";
+ break;
+ case "TEAL":
+ uppercaseVal = "#008080";
+ break;
+ case "YELLOW":
+ uppercaseVal = "#FFFF00";
+ break;
+ case "RED":
+ uppercaseVal = "#FF0000";
+ break;
+ case "MAROON":
+ uppercaseVal = "#800000";
+ break;
+ case "PURPLE":
+ uppercaseVal = "#800080";
+ break;
+ case "FUCHSIA":
+ uppercaseVal = "#FF00FF";
+ break;
+ case "NAVY":
+ uppercaseVal = "#000080";
+ break;
+ case "BLUE":
+ uppercaseVal = "#0000FF";
+ break;
+ case "AQUA":
+ uppercaseVal = "#00FFFF";
+ break;
+ case "WHITE":
+ uppercaseVal = "#FFFFFF";
+ break;
+ case "SILVER":
+ uppercaseVal = "#C0C0C0";
+ break;
+ case "GRAY":
+ uppercaseVal = "#808080";
+ break;
+ default: // BLACK
+ uppercaseVal = "#000000";
+ break;
+ }
+ }
+ var cells = this.mBox.getElementsByAttribute("color", uppercaseVal);
+ if (cells.item(0)) {
+ this.selectCell(cells[0]);
+ this.hoverCell(this.mSelectedCell);
+ }
+ return val;
+ ]]></setter>
+ </property>
+
+ <method name="initColor">
+ <parameter name="aColor"/>
+ <body><![CDATA[
+ // Use this to initialize color without
+ // triggering the "onselect" handler,
+ // which closes window when it's a popup
+ this.mDoOnSelect = false;
+ this.color = aColor;
+ this.mDoOnSelect = true;
+ ]]></body>
+ </method>
+
+ <method name="initialize">
+ <body><![CDATA[
+ this.mSelectedCell = null;
+ this.mHoverCell = null;
+ this.mBox = document.getAnonymousNodes(this)[0];
+ this.mIsPopup = false;
+ this.mDoOnSelect = true;
+
+ let imageEls = this.mBox.querySelectorAll("image");
+ // We set the background of the picker tiles here using images in
+ // order for the color to show up even when author colors are
+ // disabled or the user is using high contrast mode.
+ for (let el of imageEls) {
+ let dataURI = "data:image/svg+xml,<svg style='background-color: " +
+ encodeURIComponent(el.getAttribute("color")) +
+ "' xmlns='http://www.w3.org/2000/svg' />";
+ el.setAttribute("src", dataURI);
+ }
+
+ this.hoverCell(this.mBox.childNodes[0].childNodes[0]);
+
+ // used to capture keydown at the document level
+ this.mPickerKeyDown = function(aEvent)
+ {
+ document._focusedPicker.pickerKeyDown(aEvent);
+ }
+
+ ]]></body>
+ </method>
+
+ <method name="_fireEvent">
+ <parameter name="aTarget"/>
+ <parameter name="aEventName"/>
+ <body>
+ <![CDATA[
+ try {
+ var event = document.createEvent("Events");
+ event.initEvent(aEventName, true, true);
+ var cancel = !aTarget.dispatchEvent(event);
+ if (aTarget.hasAttribute("on" + aEventName)) {
+ var fn = new Function ("event", aTarget.getAttribute("on" + aEventName));
+ var rv = fn.call(aTarget, event);
+ if (rv == false)
+ cancel = true;
+ }
+ return !cancel;
+ }
+ catch (e) {
+ Components.utils.reportError(e);
+ }
+ return false;
+ ]]>
+ </body>
+ </method>
+
+ <method name="resetHover">
+ <body><![CDATA[
+ if (this.mHoverCell)
+ this.mHoverCell.removeAttribute("hover");
+ ]]></body>
+ </method>
+
+ <method name="getColIndex">
+ <parameter name="aCell"/>
+ <body><![CDATA[
+ var cell = aCell;
+ var idx;
+ for (idx = -1; cell; idx++)
+ cell = cell.previousSibling;
+
+ return idx;
+ ]]></body>
+ </method>
+
+ <method name="isColorCell">
+ <parameter name="aCell"/>
+ <body><![CDATA[
+ return aCell && aCell.hasAttribute("color");
+ ]]></body>
+ </method>
+
+ <method name="hoverLeft">
+ <body><![CDATA[
+ var cell = this.mHoverCell.previousSibling;
+ this.hoverCell(cell);
+ ]]></body>
+ </method>
+
+ <method name="hoverRight">
+ <body><![CDATA[
+ var cell = this.mHoverCell.nextSibling;
+ this.hoverCell(cell);
+ ]]></body>
+ </method>
+
+ <method name="hoverUp">
+ <body><![CDATA[
+ var row = this.mHoverCell.parentNode.previousSibling;
+ if (row) {
+ var colIdx = this.getColIndex(this.mHoverCell);
+ var cell = row.childNodes[colIdx];
+ this.hoverCell(cell);
+ }
+ ]]></body>
+ </method>
+
+ <method name="hoverDown">
+ <body><![CDATA[
+ var row = this.mHoverCell.parentNode.nextSibling;
+ if (row) {
+ var colIdx = this.getColIndex(this.mHoverCell);
+ var cell = row.childNodes[colIdx];
+ this.hoverCell(cell);
+ }
+ ]]></body>
+ </method>
+
+ <method name="hoverTo">
+ <parameter name="aRow"/>
+ <parameter name="aCol"/>
+
+ <body><![CDATA[
+ var row = this.mBox.childNodes[aRow];
+ if (!row) return;
+ var cell = row.childNodes[aCol];
+ if (!cell) return;
+ this.hoverCell(cell);
+ ]]></body>
+ </method>
+
+ <method name="hoverCell">
+ <parameter name="aCell"/>
+
+ <body><![CDATA[
+ if (this.isColorCell(aCell)) {
+ this.resetHover();
+ aCell.setAttribute("hover", "true");
+ this.mHoverCell = aCell;
+ var event = document.createEvent('Events');
+ event.initEvent('DOMMenuItemActive', true, true);
+ aCell.dispatchEvent(event);
+ }
+ ]]></body>
+ </method>
+
+ <method name="selectHoverCell">
+ <body><![CDATA[
+ this.selectCell(this.mHoverCell);
+ ]]></body>
+ </method>
+
+ <method name="selectCell">
+ <parameter name="aCell"/>
+
+ <body><![CDATA[
+ if (this.isColorCell(aCell)) {
+ if (this.mSelectedCell)
+ this.mSelectedCell.removeAttribute("selected");
+
+ this.mSelectedCell = aCell;
+ aCell.setAttribute("selected", "true");
+
+ if (this.mDoOnSelect)
+ this._fireEvent(this, "select");
+ }
+ ]]></body>
+ </method>
+
+ <method name="handleEvent">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ switch (aEvent.keyCode) {
+ case 37: // left
+ this.hoverLeft();
+ break;
+ case 38: // up
+ this.hoverUp();
+ break;
+ case 39: // right
+ this.hoverRight();
+ break;
+ case 40: // down
+ this.hoverDown();
+ break;
+ case 13: // enter
+ case 32: // space
+ this.selectHoverCell();
+ break;
+ }
+ ]]></body>
+ </method>
+
+ <constructor><![CDATA[
+ this.initialize();
+ ]]></constructor>
+
+ </implementation>
+
+ <handlers>
+ <handler event="mouseover"><![CDATA[
+ this.hoverCell(event.originalTarget);
+ ]]></handler>
+
+ <handler event="click"><![CDATA[
+ if (event.originalTarget.hasAttribute("color")) {
+ this.selectCell(event.originalTarget);
+ this.hoverCell(this.mSelectedCell);
+ }
+ ]]></handler>
+
+
+ <handler event="focus" phase="capturing">
+ <![CDATA[
+ if (!mIsPopup && this.getAttribute('focused') != 'true') {
+ this.setAttribute('focused', 'true');
+ document.addEventListener("keydown", this, true);
+ if (this.mSelectedCell)
+ this.hoverCell(this.mSelectedCell);
+ }
+ ]]>
+ </handler>
+
+ <handler event="blur" phase="capturing">
+ <![CDATA[
+ if (!mIsPopup && this.getAttribute('focused') == 'true') {
+ document.removeEventListener("keydown", this, true);
+ this.removeAttribute('focused');
+ this.resetHover();
+ }
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="colorpicker-button" display="xul:menu" role="xul:colorpicker"
+ extends="chrome://global/content/bindings/general.xml#basecontrol">
+ <resources>
+ <stylesheet src="chrome://global/skin/colorpicker.css"/>
+ </resources>
+
+ <content>
+ <xul:image class="colorpicker-button-colorbox" anonid="colorbox" flex="1" xbl:inherits="disabled"/>
+
+ <xul:panel class="colorpicker-button-menupopup"
+ anonid="colorpopup" noautofocus="true" level="top"
+ onmousedown="event.stopPropagation()"
+ onpopupshowing="this._colorPicker.onPopupShowing()"
+ onpopuphiding="this._colorPicker.onPopupHiding()"
+ onselect="this._colorPicker.pickerChange()">
+ <xul:colorpicker xbl:inherits="palettename,disabled" allowevents="true" anonid="colorpicker"/>
+ </xul:panel>
+ </content>
+
+ <implementation>
+ <property name="open"
+ onget="return this.getAttribute('open') == 'true'"
+ onset="this.showPopup();"/>
+ <property name="color">
+ <getter><![CDATA[
+ return this.getAttribute("color");
+ ]]></getter>
+ <setter><![CDATA[
+ this.mColorBox.setAttribute("src",
+ "data:image/svg+xml,<svg style='background-color: " +
+ encodeURIComponent(val) +
+ "' xmlns='http://www.w3.org/2000/svg' />");
+ this.setAttribute("color", val);
+ return val;
+ ]]></setter>
+ </property>
+
+ <method name="initialize">
+ <body><![CDATA[
+ this.mColorBox = document.getAnonymousElementByAttribute(this, "anonid", "colorbox");
+ this.mColorBox.setAttribute("src",
+ "data:image/svg+xml,<svg style='background-color: " +
+ encodeURIComponent(this.color) +
+ "' xmlns='http://www.w3.org/2000/svg' />");
+
+ var popup = document.getAnonymousElementByAttribute(this, "anonid", "colorpopup")
+ popup._colorPicker = this;
+
+ this.mPicker = document.getAnonymousElementByAttribute(this, "anonid", "colorpicker")
+ ]]></body>
+ </method>
+
+ <method name="_fireEvent">
+ <parameter name="aTarget"/>
+ <parameter name="aEventName"/>
+ <body>
+ <![CDATA[
+ try {
+ var event = document.createEvent("Events");
+ event.initEvent(aEventName, true, true);
+ var cancel = !aTarget.dispatchEvent(event);
+ if (aTarget.hasAttribute("on" + aEventName)) {
+ var fn = new Function ("event", aTarget.getAttribute("on" + aEventName));
+ var rv = fn.call(aTarget, event);
+ if (rv == false)
+ cancel = true;
+ }
+ return !cancel;
+ }
+ catch (e) {
+ dump(e);
+ }
+ return false;
+ ]]>
+ </body>
+ </method>
+
+ <method name="showPopup">
+ <body><![CDATA[
+ this.mPicker.parentNode.openPopup(this, "after_start", 0, 0, false, false);
+ ]]></body>
+ </method>
+
+ <method name="hidePopup">
+ <body><![CDATA[
+ this.mPicker.parentNode.hidePopup();
+ ]]></body>
+ </method>
+
+ <method name="onPopupShowing">
+ <body><![CDATA[
+ if ("resetHover" in this.mPicker)
+ this.mPicker.resetHover();
+ document.addEventListener("keydown", this.mPicker, true);
+ this.mPicker.mIsPopup = true;
+ // Initialize to current button's color
+ this.mPicker.initColor(this.color);
+ ]]></body>
+ </method>
+
+ <method name="onPopupHiding">
+ <body><![CDATA[
+ // Removes the key listener
+ document.removeEventListener("keydown", this.mPicker, true);
+ this.mPicker.mIsPopup = false;
+ ]]></body>
+ </method>
+
+ <method name="pickerChange">
+ <body><![CDATA[
+ this.color = this.mPicker.color;
+ setTimeout(function(aPopup) { aPopup.hidePopup() }, 1, this.mPicker.parentNode);
+
+ this._fireEvent(this, "change");
+ ]]></body>
+ </method>
+
+ <constructor><![CDATA[
+ this.initialize();
+ ]]></constructor>
+
+ </implementation>
+
+ <handlers>
+ <handler event="keydown"><![CDATA[
+ // open popup if key is space/up/left/right/down and popup is closed
+ if ( (event.keyCode == 32 || (event.keyCode > 36 && event.keyCode < 41)) && !this.open)
+ this.showPopup();
+ else if ( (event.keyCode == 27) && this.open)
+ this.hidePopup();
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="colorpickertile" role="xul:colorpickertile">
+ </binding>
+
+</bindings>
+
diff --git a/toolkit/content/widgets/datetimebox.css b/toolkit/content/widgets/datetimebox.css
new file mode 100644
index 000000000..4a9593a69
--- /dev/null
+++ b/toolkit/content/widgets/datetimebox.css
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@namespace url("http://www.w3.org/1999/xhtml");
+@namespace xul url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+
+.datetime-input-box-wrapper {
+ -moz-appearance: none;
+ display: inline-flex;
+ cursor: default;
+ background-color: inherit;
+ color: inherit;
+}
+
+.datetime-input {
+ -moz-appearance: none;
+ text-align: center;
+ padding: 0;
+ border: 0;
+ margin: 0;
+ ime-mode: disabled;
+}
+
+.datetime-separator {
+ margin: 0 !important;
+}
+
+.datetime-input[readonly],
+.datetime-input[disabled] {
+ color: GrayText;
+ -moz-user-select: none;
+}
+
+.datetime-reset-button {
+ background-image: url(chrome://global/skin/icons/input-clear.svg);
+ background-color: transparent;
+ background-repeat: no-repeat;
+ background-size: 12px, 12px;
+ border: none;
+ height: 12px;
+ width: 12px;
+ align-self: center;
+ justify-content: flex-end;
+}
diff --git a/toolkit/content/widgets/datetimebox.xml b/toolkit/content/widgets/datetimebox.xml
new file mode 100644
index 000000000..05591e65a
--- /dev/null
+++ b/toolkit/content/widgets/datetimebox.xml
@@ -0,0 +1,807 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<bindings id="datetimeboxBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="time-input"
+ extends="chrome://global/content/bindings/datetimebox.xml#datetime-input-base">
+ <resources>
+ <stylesheet src="chrome://global/content/textbox.css"/>
+ <stylesheet src="chrome://global/skin/textbox.css"/>
+ <stylesheet src="chrome://global/content/bindings/datetimebox.css"/>
+ </resources>
+
+ <implementation>
+ <constructor>
+ <![CDATA[
+ // TODO: Bug 1301312 - localization for input type=time input.
+ this.mHour12 = true;
+ this.mAMIndicator = "AM";
+ this.mPMIndicator = "PM";
+ this.mPlaceHolder = "--";
+ this.mSeparatorText = ":";
+ this.mMillisecSeparatorText = ".";
+ this.mMaxLength = 2;
+ this.mMillisecMaxLength = 3;
+ this.mDefaultStep = 60 * 1000; // in milliseconds
+
+ this.mMinHourInHour12 = 1;
+ this.mMaxHourInHour12 = 12;
+ this.mMinMinute = 0;
+ this.mMaxMinute = 59;
+ this.mMinSecond = 0;
+ this.mMaxSecond = 59;
+ this.mMinMillisecond = 0;
+ this.mMaxMillisecond = 999;
+
+ this.mHourPageUpDownInterval = 3;
+ this.mMinSecPageUpDownInterval = 10;
+
+ this.mHourField =
+ document.getAnonymousElementByAttribute(this, "anonid", "input-one");
+ this.mHourField.setAttribute("typeBuffer", "");
+ this.mMinuteField =
+ document.getAnonymousElementByAttribute(this, "anonid", "input-two");
+ this.mMinuteField.setAttribute("typeBuffer", "");
+ this.mDayPeriodField =
+ document.getAnonymousElementByAttribute(this, "anonid", "input-three");
+ this.mDayPeriodField.classList.remove("numeric");
+
+ this.mHourField.placeholder = this.mPlaceHolder;
+ this.mMinuteField.placeholder = this.mPlaceHolder;
+ this.mDayPeriodField.placeholder = this.mPlaceHolder;
+
+ this.mHourField.setAttribute("min", this.mMinHourInHour12);
+ this.mHourField.setAttribute("max", this.mMaxHourInHour12);
+ this.mMinuteField.setAttribute("min", this.mMinMinute);
+ this.mMinuteField.setAttribute("max", this.mMaxMinute);
+
+ this.mMinuteSeparator =
+ document.getAnonymousElementByAttribute(this, "anonid", "sep-first");
+ this.mMinuteSeparator.textContent = this.mSeparatorText;
+ this.mSpaceSeparator =
+ document.getAnonymousElementByAttribute(this, "anonid", "sep-second");
+ // space between time and am/pm field
+ this.mSpaceSeparator.textContent = " ";
+
+ this.mSecondSeparator = null;
+ this.mSecondField = null;
+ this.mMillisecSeparator = null;
+ this.mMillisecField = null;
+
+ if (this.mInputElement.value) {
+ this.setFieldsFromInputValue();
+ }
+ ]]>
+ </constructor>
+
+ <method name="insertSeparator">
+ <parameter name="aSeparatorText"/>
+ <body>
+ <![CDATA[
+ let container = this.mHourField.parentNode;
+ const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+ let separator = document.createElementNS(HTML_NS, "span");
+ separator.textContent = aSeparatorText;
+ separator.setAttribute("class", "datetime-separator");
+ container.insertBefore(separator, this.mSpaceSeparator);
+
+ return separator;
+ ]]>
+ </body>
+ </method>
+
+ <method name="insertAdditionalField">
+ <parameter name="aPlaceHolder"/>
+ <parameter name="aMin"/>
+ <parameter name="aMax"/>
+ <parameter name="aSize"/>
+ <parameter name="aMaxLength"/>
+ <body>
+ <![CDATA[
+ let container = this.mHourField.parentNode;
+ const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+ let field = document.createElementNS(HTML_NS, "input");
+ field.classList.add("textbox-input", "datetime-input", "numeric");
+ field.setAttribute("size", aSize);
+ field.setAttribute("maxlength", aMaxLength);
+ field.setAttribute("min", aMin);
+ field.setAttribute("max", aMax);
+ field.setAttribute("typeBuffer", "");
+ field.disabled = this.mInputElement.disabled;
+ field.readOnly = this.mInputElement.readOnly;
+ field.tabIndex = this.mInputElement.tabIndex;
+ field.placeholder = aPlaceHolder;
+ container.insertBefore(field, this.mSpaceSeparator);
+
+ return field;
+ ]]>
+ </body>
+ </method>
+
+ <method name="setFieldsFromInputValue">
+ <body>
+ <![CDATA[
+ let value = this.mInputElement.value;
+ if (!value) {
+ this.clearInputFields(true);
+ return;
+ }
+
+ this.log("setFieldsFromInputValue: " + value);
+ let [hour, minute, second] = value.split(':');
+
+ this.setFieldValue(this.mHourField, hour);
+ this.setFieldValue(this.mMinuteField, minute);
+ if (this.mHour12) {
+ this.mDayPeriodField.value = (hour >= this.mMaxHourInHour12) ?
+ this.mPMIndicator : this.mAMIndicator;
+ }
+
+ if (!this.isEmpty(second)) {
+ let index = second.indexOf(".");
+ let millisecond;
+ if (index != -1) {
+ millisecond = second.substring(index + 1);
+ second = second.substring(0, index);
+ }
+
+ if (!this.mSecondField) {
+ this.mSecondSeparator = this.insertSeparator(this.mSeparatorText);
+ this.mSecondField = this.insertAdditionalField(this.mPlaceHolder,
+ this.mMinSecond, this.mMaxSecond, this.mMaxLength,
+ this.mMaxLength);
+ }
+ this.setFieldValue(this.mSecondField, second);
+
+ if (!this.isEmpty(millisecond)) {
+ if (!this.mMillisecField) {
+ this.mMillisecSeparator = this.insertSeparator(
+ this.mMillisecSeparatorText);
+ this.mMillisecField = this.insertAdditionalField(
+ this.mPlaceHolder, this.mMinMillisecond, this.mMaxMillisecond,
+ this.mMillisecMaxLength, this.mMillisecMaxLength);
+ }
+ this.setFieldValue(this.mMillisecField, millisecond);
+ } else if (this.mMillisecField) {
+ this.mMillisecField.remove();
+ this.mMillisecField = null;
+
+ this.mMillisecSeparator.remove();
+ this.mMillisecSeparator = null;
+ }
+ } else {
+ if (this.mSecondField) {
+ this.mSecondField.remove();
+ this.mSecondField = null;
+
+ this.mSecondSeparator.remove();
+ this.mSecondSeparator = null;
+ }
+
+ if (this.mMillisecField) {
+ this.mMillisecField.remove();
+ this.mMillisecField = null;
+
+ this.mMillisecSeparator.remove();
+ this.mMillisecSeparator = null;
+ }
+ }
+ this.notifyPicker();
+ ]]>
+ </body>
+ </method>
+
+ <method name="setInputValueFromFields">
+ <body>
+ <![CDATA[
+ if (this.isEmpty(this.mHourField.value) ||
+ this.isEmpty(this.mMinuteField.value) ||
+ (this.mDayPeriodField && this.isEmpty(this.mDayPeriodField.value)) ||
+ (this.mSecondField && this.isEmpty(this.mSecondField.value)) ||
+ (this.mMillisecField && this.isEmpty(this.mMillisecField.value))) {
+ // We still need to notify picker in case any of the field has
+ // changed. If we can set input element value, then notifyPicker
+ // will be called in setFieldsFromInputValue().
+ this.notifyPicker();
+ return;
+ }
+
+ let hour = Number(this.mHourField.value);
+ if (this.mHour12) {
+ let dayPeriod = this.mDayPeriodField.value;
+ if (dayPeriod == this.mPMIndicator &&
+ hour < this.mMaxHourInHour12) {
+ hour += this.mMaxHourInHour12;
+ } else if (dayPeriod == this.mAMIndicator &&
+ hour == this.mMaxHourInHour12) {
+ hour = 0;
+ }
+ }
+
+ hour = (hour < 10) ? ("0" + hour) : hour;
+
+ let time = hour + ":" + this.mMinuteField.value;
+ if (this.mSecondField) {
+ time += ":" + this.mSecondField.value;
+ }
+
+ if (this.mMillisecField) {
+ time += "." + this.mMillisecField.value;
+ }
+
+ this.log("setInputValueFromFields: " + time);
+ this.mInputElement.setUserInput(time);
+ ]]>
+ </body>
+ </method>
+
+ <method name="setFieldsFromPicker">
+ <parameter name="aValue"/>
+ <body>
+ <![CDATA[
+ let hour = aValue.hour;
+ let minute = aValue.minute;
+ this.log("setFieldsFromPicker: " + hour + ":" + minute);
+
+ if (!this.isEmpty(hour)) {
+ this.setFieldValue(this.mHourField, hour);
+ if (this.mHour12) {
+ this.mDayPeriodField.value =
+ (hour >= this.mMaxHourInHour12) ? this.mPMIndicator
+ : this.mAMIndicator;
+ }
+ }
+
+ if (!this.isEmpty(minute)) {
+ this.setFieldValue(this.mMinuteField, minute);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="clearInputFields">
+ <parameter name="aFromInputElement"/>
+ <body>
+ <![CDATA[
+ this.log("clearInputFields");
+
+ if (this.isDisabled() || this.isReadonly()) {
+ return;
+ }
+
+ if (this.mHourField && !this.mHourField.disabled &&
+ !this.mHourField.readOnly) {
+ this.mHourField.value = "";
+ }
+
+ if (this.mMinuteField && !this.mMinuteField.disabled &&
+ !this.mMinuteField.readOnly) {
+ this.mMinuteField.value = "";
+ }
+
+ if (this.mSecondField && !this.mSecondField.disabled &&
+ !this.mSecondField.readOnly) {
+ this.mSecondField.value = "";
+ }
+
+ if (this.mMillisecField && !this.mMillisecField.disabled &&
+ !this.mMillisecField.readOnly) {
+ this.mMillisecField.value = "";
+ }
+
+ if (this.mDayPeriodField && !this.mDayPeriodField.disabled &&
+ !this.mDayPeriodField.readOnly) {
+ this.mDayPeriodField.value = "";
+ }
+
+ if (!aFromInputElement) {
+ this.mInputElement.setUserInput("");
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="incrementFieldValue">
+ <parameter name="aTargetField"/>
+ <parameter name="aTimes"/>
+ <body>
+ <![CDATA[
+ let value;
+
+ // Use current time if field is empty.
+ if (this.isEmpty(aTargetField.value)) {
+ let now = new Date();
+
+ if (aTargetField == this.mHourField) {
+ value = now.getHours() % this.mMaxHourInHour12 ||
+ this.mMaxHourInHour12;
+ } else if (aTargetField == this.mMinuteField) {
+ value = now.getMinutes();
+ } else if (aTargetField == this.mSecondField) {
+ value = now.getSeconds();
+ } else if (aTargetField == this.mMillisecField) {
+ value = now.getMilliseconds();
+ } else {
+ this.log("Field not supported in incrementFieldValue.");
+ return;
+ }
+ } else {
+ value = Number(aTargetField.value);
+ }
+
+ let min = aTargetField.getAttribute("min");
+ let max = aTargetField.getAttribute("max");
+
+ value += aTimes;
+ if (value > max) {
+ value -= (max - min + 1);
+ } else if (value < min) {
+ value += (max - min + 1);
+ }
+ this.setFieldValue(aTargetField, value);
+ aTargetField.select();
+ ]]>
+ </body>
+ </method>
+
+ <method name="handleKeyboardNav">
+ <parameter name="aEvent"/>
+ <body>
+ <![CDATA[
+ if (this.isDisabled() || this.isReadonly()) {
+ return;
+ }
+
+ let targetField = aEvent.originalTarget;
+ let key = aEvent.key;
+
+ if (this.mDayPeriodField &&
+ targetField == this.mDayPeriodField) {
+ // Home/End key does nothing on AM/PM field.
+ if (key == "Home" || key == "End") {
+ return;
+ }
+
+ this.mDayPeriodField.value =
+ this.mDayPeriodField.value == this.mAMIndicator ?
+ this.mPMIndicator : this.mAMIndicator;
+ this.mDayPeriodField.select();
+ this.setInputValueFromFields();
+ return;
+ }
+
+ switch (key) {
+ case "ArrowUp":
+ this.incrementFieldValue(targetField, 1);
+ break;
+ case "ArrowDown":
+ this.incrementFieldValue(targetField, -1);
+ break;
+ case "PageUp":
+ this.incrementFieldValue(targetField,
+ targetField == this.mHourField ? this.mHourPageUpDownInterval
+ : this.mMinSecPageUpDownInterval);
+ break;
+ case "PageDown":
+ this.incrementFieldValue(targetField,
+ targetField == this.mHourField ? (0 - this.mHourPageUpDownInterval)
+ : (0 - this.mMinSecPageUpDownInterval));
+ break;
+ case "Home":
+ let min = targetField.getAttribute("min");
+ this.setFieldValue(targetField, min);
+ targetField.select();
+ break;
+ case "End":
+ let max = targetField.getAttribute("max");
+ this.setFieldValue(targetField, max);
+ targetField.select();
+ break;
+ }
+ this.setInputValueFromFields();
+ ]]>
+ </body>
+ </method>
+
+ <method name="handleKeypress">
+ <parameter name="aEvent"/>
+ <body>
+ <![CDATA[
+ if (this.isDisabled() || this.isReadonly()) {
+ return;
+ }
+
+ let targetField = aEvent.originalTarget;
+ let key = aEvent.key;
+
+ if (this.mDayPeriodField &&
+ targetField == this.mDayPeriodField) {
+ if (key == "a" || key == "A") {
+ this.mDayPeriodField.value = this.mAMIndicator;
+ this.mDayPeriodField.select();
+ } else if (key == "p" || key == "P") {
+ this.mDayPeriodField.value = this.mPMIndicator;
+ this.mDayPeriodField.select();
+ }
+ return;
+ }
+
+ if (targetField.classList.contains("numeric") && key.match(/[0-9]/)) {
+ let buffer = targetField.getAttribute("typeBuffer") || "";
+
+ buffer = buffer.concat(key);
+ this.setFieldValue(targetField, buffer);
+ targetField.select();
+
+ let n = Number(buffer);
+ let max = targetField.getAttribute("max");
+ if (buffer.length >= targetField.maxLength || n * 10 > max) {
+ buffer = "";
+ this.advanceToNextField();
+ }
+ targetField.setAttribute("typeBuffer", buffer);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="setFieldValue">
+ <parameter name="aField"/>
+ <parameter name="aValue"/>
+ <body>
+ <![CDATA[
+ let value = Number(aValue);
+ if (isNaN(value)) {
+ this.log("NaN on setFieldValue!");
+ return;
+ }
+
+ if (aField.maxLength == this.mMaxLength) { // For hour, minute and second
+ if (aField == this.mHourField && this.mHour12) {
+ value = (value > this.mMaxHourInHour12) ?
+ value - this.mMaxHourInHour12 : value;
+ if (aValue == "00") {
+ value = this.mMaxHourInHour12;
+ }
+ }
+ // prepend zero
+ if (value < 10) {
+ value = "0" + value;
+ }
+ } else if (aField.maxLength == this.mMillisecMaxLength) {
+ // prepend zeroes
+ if (value < 10) {
+ value = "00" + value;
+ } else if (value < 100) {
+ value = "0" + value;
+ }
+ }
+
+ aField.value = value;
+ ]]>
+ </body>
+ </method>
+
+ <method name="isValueAvailable">
+ <body>
+ <![CDATA[
+ // Picker only cares about hour:minute.
+ return !this.isEmpty(this.mHourField.value) ||
+ !this.isEmpty(this.mMinuteField.value);
+ ]]>
+ </body>
+ </method>
+
+ <method name="getCurrentValue">
+ <body>
+ <![CDATA[
+ let hour;
+ if (!this.isEmpty(this.mHourField.value)) {
+ hour = Number(this.mHourField.value);
+ if (this.mHour12) {
+ let dayPeriod = this.mDayPeriodField.value;
+ if (dayPeriod == this.mPMIndicator &&
+ hour < this.mMaxHourInHour12) {
+ hour += this.mMaxHourInHour12;
+ } else if (dayPeriod == this.mAMIndicator &&
+ hour == this.mMaxHourInHour12) {
+ hour = 0;
+ }
+ }
+ }
+
+ let minute;
+ if (!this.isEmpty(this.mMinuteField.value)) {
+ minute = Number(this.mMinuteField.value);
+ }
+
+ // Picker only needs hour/minute.
+ let time = { hour, minute };
+
+ this.log("getCurrentValue: " + JSON.stringify(time));
+ return time;
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="datetime-input-base">
+ <resources>
+ <stylesheet src="chrome://global/content/textbox.css"/>
+ <stylesheet src="chrome://global/skin/textbox.css"/>
+ <stylesheet src="chrome://global/content/bindings/datetimebox.css"/>
+ </resources>
+
+ <content>
+ <html:div class="datetime-input-box-wrapper"
+ xbl:inherits="context,disabled,readonly">
+ <html:span>
+ <html:input anonid="input-one"
+ class="textbox-input datetime-input numeric"
+ size="2" maxlength="2"
+ xbl:inherits="disabled,readonly,tabindex"/>
+ <html:span anonid="sep-first" class="datetime-separator"></html:span>
+ <html:input anonid="input-two"
+ class="textbox-input datetime-input numeric"
+ size="2" maxlength="2"
+ xbl:inherits="disabled,readonly,tabindex"/>
+ <html:span anonid="sep-second" class="datetime-separator"></html:span>
+ <html:input anonid="input-three"
+ class="textbox-input datetime-input numeric"
+ size="2" maxlength="2"
+ xbl:inherits="disabled,readonly,tabindex"/>
+ </html:span>
+
+ <html:button class="datetime-reset-button" anoid="reset-button"
+ tabindex="-1" xbl:inherits="disabled"
+ onclick="document.getBindingParent(this).clearInputFields(false);"/>
+ </html:div>
+ </content>
+
+ <implementation implements="nsIDateTimeInputArea">
+ <constructor>
+ <![CDATA[
+ this.DEBUG = false;
+ this.mInputElement = this.parentNode;
+
+ this.mMin = this.mInputElement.min;
+ this.mMax = this.mInputElement.max;
+ this.mStep = this.mInputElement.step;
+ this.mIsPickerOpen = false;
+ ]]>
+ </constructor>
+
+ <method name="log">
+ <parameter name="aMsg"/>
+ <body>
+ <![CDATA[
+ if (this.DEBUG) {
+ dump("[DateTimeBox] " + aMsg + "\n");
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="focusInnerTextBox">
+ <body>
+ <![CDATA[
+ this.log("focusInnerTextBox");
+ document.getAnonymousElementByAttribute(this, "anonid", "input-one").focus();
+ ]]>
+ </body>
+ </method>
+
+ <method name="blurInnerTextBox">
+ <body>
+ <![CDATA[
+ this.log("blurInnerTextBox");
+ if (this.mLastFocusedField) {
+ this.mLastFocusedField.blur();
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="notifyInputElementValueChanged">
+ <body>
+ <![CDATA[
+ this.log("inputElementValueChanged");
+ this.setFieldsFromInputValue();
+ ]]>
+ </body>
+ </method>
+
+ <method name="setValueFromPicker">
+ <parameter name="aValue"/>
+ <body>
+ <![CDATA[
+ this.setFieldsFromPicker(aValue);
+ ]]>
+ </body>
+ </method>
+
+ <method name="advanceToNextField">
+ <parameter name="aReverse"/>
+ <body>
+ <![CDATA[
+ this.log("advanceToNextField");
+
+ let focusedInput = this.mLastFocusedField;
+ let next = aReverse ? focusedInput.previousElementSibling
+ : focusedInput.nextElementSibling;
+ if (!next && !aReverse) {
+ this.setInputValueFromFields();
+ return;
+ }
+
+ while (next) {
+ if (next.type == "text" && !next.disabled) {
+ next.focus();
+ break;
+ }
+ next = aReverse ? next.previousElementSibling
+ : next.nextElementSibling;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="setPickerState">
+ <parameter name="aIsOpen"/>
+ <body>
+ <![CDATA[
+ this.log("picker is now " + (aIsOpen ? "opened" : "closed"));
+ this.mIsPickerOpen = aIsOpen;
+ ]]>
+ </body>
+ </method>
+
+ <method name="isEmpty">
+ <parameter name="aValue"/>
+ <body>
+ return (aValue == undefined || 0 === aValue.length);
+ </body>
+ </method>
+
+ <method name="clearInputFields">
+ <body>
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ </body>
+ </method>
+
+ <method name="setFieldsFromInputValue">
+ <body>
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ </body>
+ </method>
+
+ <method name="setInputValueFromFields">
+ <body>
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ </body>
+ </method>
+
+ <method name="setFieldsFromPicker">
+ <body>
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ </body>
+ </method>
+
+ <method name="handleKeypress">
+ <body>
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ </body>
+ </method>
+
+ <method name="handleKeyboardNav">
+ <body>
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ </body>
+ </method>
+
+ <method name="notifyPicker">
+ <body>
+ <![CDATA[
+ if (this.mIsPickerOpen && this.isValueAvailable()) {
+ this.mInputElement.updateDateTimePicker(this.getCurrentValue());
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="isDisabled">
+ <body>
+ <![CDATA[
+ return this.hasAttribute("disabled");
+ ]]>
+ </body>
+ </method>
+
+ <method name="isReadonly">
+ <body>
+ <![CDATA[
+ return this.hasAttribute("readonly");
+ ]]>
+ </body>
+ </method>
+
+ </implementation>
+
+ <handlers>
+ <handler event="focus">
+ <![CDATA[
+ this.log("focus on: " + event.originalTarget);
+
+ let target = event.originalTarget;
+ if (target.type == "text") {
+ this.mLastFocusedField = target;
+ target.select();
+ }
+ ]]>
+ </handler>
+
+ <handler event="blur">
+ <![CDATA[
+ this.setInputValueFromFields();
+ ]]>
+ </handler>
+
+ <handler event="click">
+ <![CDATA[
+ // XXX: .originalTarget is not expected.
+ // When clicking on one of the inner text boxes, the .originalTarget is
+ // a HTMLDivElement and when clicking on the reset button, it's a
+ // HTMLButtonElement but it's not equal to our reset-button.
+ this.log("click on: " + event.originalTarget);
+ if (event.defaultPrevented || this.isDisabled() || this.isReadonly()) {
+ return;
+ }
+
+ if (!(event.originalTarget instanceof HTMLButtonElement)) {
+ this.mInputElement.openDateTimePicker(this.getCurrentValue());
+ }
+ ]]>
+ </handler>
+
+ <handler event="keypress" phase="capturing">
+ <![CDATA[
+ let key = event.key;
+ this.log("keypress: " + key);
+
+ if (key == "Backspace" || key == "Tab") {
+ return;
+ }
+
+ if (key == "Enter" || key == " ") {
+ // Close picker on Enter and Space.
+ this.mInputElement.closeDateTimePicker();
+ }
+
+ if (key == "ArrowUp" || key == "ArrowDown" ||
+ key == "PageUp" || key == "PageDown" ||
+ key == "Home" || key == "End") {
+ this.handleKeyboardNav(event);
+ } else if (key == "ArrowRight" || key == "ArrowLeft") {
+ this.advanceToNextField((key == "ArrowRight" ? false : true));
+ } else {
+ this.handleKeypress(event);
+ }
+
+ event.preventDefault();
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+</bindings>
diff --git a/toolkit/content/widgets/datetimepicker.xml b/toolkit/content/widgets/datetimepicker.xml
new file mode 100644
index 000000000..5f16f1ff0
--- /dev/null
+++ b/toolkit/content/widgets/datetimepicker.xml
@@ -0,0 +1,1301 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE bindings [
+ <!ENTITY % datetimepickerDTD SYSTEM "chrome://global/locale/datetimepicker.dtd">
+ %datetimepickerDTD;
+]>
+
+<bindings id="timepickerBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="datetimepicker-base"
+ extends="chrome://global/content/bindings/general.xml#basecontrol">
+
+ <resources>
+ <stylesheet src="chrome://global/content/textbox.css"/>
+ <stylesheet src="chrome://global/skin/textbox.css"/>
+ <stylesheet src="chrome://global/skin/dropmarker.css"/>
+ <stylesheet src="chrome://global/skin/datetimepicker.css"/>
+ </resources>
+
+ <content align="center">
+ <xul:hbox class="datetimepicker-input-box" align="center"
+ xbl:inherits="context,disabled,readonly">
+ <xul:hbox class="textbox-input-box datetimepicker-input-subbox" align="center">
+ <html:input class="datetimepicker-input textbox-input" anonid="input-one"
+ size="2" maxlength="2"
+ xbl:inherits="disabled,readonly"/>
+ </xul:hbox>
+ <xul:label anonid="sep-first" class="datetimepicker-separator" value=":"/>
+ <xul:hbox class="textbox-input-box datetimepicker-input-subbox" align="center">
+ <html:input class="datetimepicker-input textbox-input" anonid="input-two"
+ size="2" maxlength="2"
+ xbl:inherits="disabled,readonly"/>
+ </xul:hbox>
+ <xul:label anonid="sep-second" class="datetimepicker-separator" value=":"/>
+ <xul:hbox class="textbox-input-box datetimepicker-input-subbox" align="center">
+ <html:input class="datetimepicker-input textbox-input" anonid="input-three"
+ size="2" maxlength="2"
+ xbl:inherits="disabled,readonly"/>
+ </xul:hbox>
+ <xul:hbox class="textbox-input-box datetimepicker-input-subbox" align="center">
+ <html:input class="datetimepicker-input textbox-input" anonid="input-ampm"
+ size="2" maxlength="2"
+ xbl:inherits="disabled,readonly"/>
+ </xul:hbox>
+ </xul:hbox>
+ <xul:spinbuttons anonid="buttons" xbl:inherits="disabled"
+ onup="this.parentNode._increaseOrDecrease(1);"
+ ondown="this.parentNode._increaseOrDecrease(-1);"/>
+ </content>
+
+ <implementation>
+ <field name="_dateValue">null</field>
+ <field name="_fieldOne">
+ document.getAnonymousElementByAttribute(this, "anonid", "input-one");
+ </field>
+ <field name="_fieldTwo">
+ document.getAnonymousElementByAttribute(this, "anonid", "input-two");
+ </field>
+ <field name="_fieldThree">
+ document.getAnonymousElementByAttribute(this, "anonid", "input-three");
+ </field>
+ <field name="_fieldAMPM">
+ document.getAnonymousElementByAttribute(this, "anonid", "input-ampm");
+ </field>
+ <field name="_separatorFirst">
+ document.getAnonymousElementByAttribute(this, "anonid", "sep-first");
+ </field>
+ <field name="_separatorSecond">
+ document.getAnonymousElementByAttribute(this, "anonid", "sep-second");
+ </field>
+ <field name="_lastFocusedField">null</field>
+ <field name="_hasEntry">true</field>
+ <field name="_valueEntered">false</field>
+ <field name="attachedControl">null</field>
+
+ <property name="_currentField" readonly="true">
+ <getter>
+ var focusedInput = document.activeElement;
+ if (focusedInput == this._fieldOne ||
+ focusedInput == this._fieldTwo ||
+ focusedInput == this._fieldThree ||
+ focusedInput == this._fieldAMPM)
+ return focusedInput;
+ return this._lastFocusedField || this._fieldOne;
+ </getter>
+ </property>
+
+ <property name="dateValue" onget="return new Date(this._dateValue);">
+ <setter>
+ <![CDATA[
+ if (!(val instanceof Date))
+ throw "Invalid Date";
+
+ this._setValueNoSync(val);
+ if (this.attachedControl)
+ this.attachedControl._setValueNoSync(val);
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="readOnly" onset="if (val) this.setAttribute('readonly', 'true');
+ else this.removeAttribute('readonly'); return val;"
+ onget="return this.getAttribute('readonly') == 'true';"/>
+
+ <method name="_fireEvent">
+ <parameter name="aEventName"/>
+ <parameter name="aTarget"/>
+ <body>
+ var event = document.createEvent("Events");
+ event.initEvent(aEventName, true, true);
+ return !aTarget.dispatchEvent(event);
+ </body>
+ </method>
+
+ <method name="_setValueOnChange">
+ <parameter name="aField"/>
+ <body>
+ <![CDATA[
+ if (!this._hasEntry)
+ return;
+
+ if (aField == this._fieldOne ||
+ aField == this._fieldTwo ||
+ aField == this._fieldThree) {
+ var value = Number(aField.value);
+ if (isNaN(value))
+ value = 0;
+
+ value = this._constrainValue(aField, value, true);
+ this._setFieldValue(aField, value);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="_init">
+ <body/>
+ </method>
+
+ <constructor>
+ this._init();
+
+ var cval = this.getAttribute("value");
+ if (cval) {
+ try {
+ this.value = cval;
+ return;
+ } catch (ex) { }
+ }
+ this.dateValue = new Date();
+ </constructor>
+
+ <destructor>
+ if (this.attachedControl) {
+ this.attachedControl.attachedControl = null;
+ this.attachedControl = null;
+ }
+ </destructor>
+
+ </implementation>
+
+ <handlers>
+ <handler event="focus" phase="capturing">
+ <![CDATA[
+ var target = event.originalTarget;
+ if (target == this._fieldOne ||
+ target == this._fieldTwo ||
+ target == this._fieldThree ||
+ target == this._fieldAMPM)
+ this._lastFocusedField = target;
+ ]]>
+ </handler>
+
+ <handler event="keypress">
+ <![CDATA[
+ if (this._hasEntry && event.charCode &&
+ this._currentField != this._fieldAMPM &&
+ ! (event.altKey || event.ctrlKey || event.metaKey) &&
+ (event.charCode < 48 || event.charCode > 57))
+ event.preventDefault();
+ ]]>
+ </handler>
+
+ <handler event="keypress" keycode="VK_UP">
+ if (this._hasEntry)
+ this._increaseOrDecrease(1);
+ </handler>
+ <handler event="keypress" keycode="VK_DOWN">
+ if (this._hasEntry)
+ this._increaseOrDecrease(-1);
+ </handler>
+
+ <handler event="input">
+ this._valueEntered = true;
+ </handler>
+
+ <handler event="change">
+ this._setValueOnChange(event.originalTarget);
+ </handler>
+ </handlers>
+
+ </binding>
+
+ <binding id="timepicker"
+ extends="chrome://global/content/bindings/datetimepicker.xml#datetimepicker-base">
+
+ <implementation>
+ <field name="is24HourClock">false</field>
+ <field name="hourLeadingZero">false</field>
+ <field name="minuteLeadingZero">true</field>
+ <field name="secondLeadingZero">true</field>
+ <field name="amIndicator">"AM"</field>
+ <field name="pmIndicator">"PM"</field>
+
+ <field name="hourField">null</field>
+ <field name="minuteField">null</field>
+ <field name="secondField">null</field>
+
+ <property name="value">
+ <getter>
+ <![CDATA[
+ var minute = this._dateValue.getMinutes();
+ if (minute < 10)
+ minute = "0" + minute;
+
+ var second = this._dateValue.getSeconds();
+ if (second < 10)
+ second = "0" + second;
+ return this._dateValue.getHours() + ":" + minute + ":" + second;
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ var items = val.match(/^([0-9]{1,2})\:([0-9]{1,2})\:?([0-9]{1,2})?$/);
+ if (!items)
+ throw "Invalid Time";
+
+ var dt = this.dateValue;
+ dt.setHours(items[1]);
+ dt.setMinutes(items[2]);
+ dt.setSeconds(items[3] ? items[3] : 0);
+ this.dateValue = dt;
+ return val;
+ ]]>
+ </setter>
+ </property>
+ <property name="hour" onget="return this._dateValue.getHours();">
+ <setter>
+ <![CDATA[
+ var valnum = Number(val);
+ if (isNaN(valnum) || valnum < 0 || valnum > 23)
+ throw "Invalid Hour";
+ this._setFieldValue(this.hourField, valnum);
+ return val;
+ ]]>
+ </setter>
+ </property>
+ <property name="minute" onget="return this._dateValue.getMinutes();">
+ <setter>
+ <![CDATA[
+ var valnum = Number(val);
+ if (isNaN(valnum) || valnum < 0 || valnum > 59)
+ throw "Invalid Minute";
+ this._setFieldValue(this.minuteField, valnum);
+ return val;
+ ]]>
+ </setter>
+ </property>
+ <property name="second" onget="return this._dateValue.getSeconds();">
+ <setter>
+ <![CDATA[
+ var valnum = Number(val);
+ if (isNaN(valnum) || valnum < 0 || valnum > 59)
+ throw "Invalid Second";
+ this._setFieldValue(this.secondField, valnum);
+ return val;
+ ]]>
+ </setter>
+ </property>
+ <property name="isPM">
+ <getter>
+ <![CDATA[
+ return (this.hour >= 12);
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ if (val) {
+ if (this.hour < 12)
+ this.hour += 12;
+ }
+ else if (this.hour >= 12)
+ this.hour -= 12;
+ return val;
+ ]]>
+ </setter>
+ </property>
+ <property name="hideSeconds">
+ <getter>
+ return (this.getAttribute("hideseconds") == "true");
+ </getter>
+ <setter>
+ if (val)
+ this.setAttribute("hideseconds", "true");
+ else
+ this.removeAttribute("hideseconds");
+ if (this.secondField)
+ this.secondField.parentNode.collapsed = val;
+ this._separatorSecond.collapsed = val;
+ return val;
+ </setter>
+ </property>
+ <property name="increment">
+ <getter>
+ <![CDATA[
+ var increment = this.getAttribute("increment");
+ increment = Number(increment);
+ if (isNaN(increment) || increment <= 0 || increment >= 60)
+ return 1;
+ return increment;
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ if (typeof val == "number")
+ this.setAttribute("increment", val);
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <method name="_setValueNoSync">
+ <parameter name="aValue"/>
+ <body>
+ <![CDATA[
+ var dt = new Date(aValue);
+ if (!isNaN(dt)) {
+ this._dateValue = dt;
+ this.setAttribute("value", this.value);
+ this._updateUI(this.hourField, this.hour);
+ this._updateUI(this.minuteField, this.minute);
+ this._updateUI(this.secondField, this.second);
+ }
+ ]]>
+ </body>
+ </method>
+ <method name="_increaseOrDecrease">
+ <parameter name="aDir"/>
+ <body>
+ <![CDATA[
+ if (this.disabled || this.readOnly)
+ return;
+
+ var field = this._currentField;
+ if (this._valueEntered)
+ this._setValueOnChange(field);
+
+ if (field == this._fieldAMPM) {
+ this.isPM = !this.isPM;
+ this._fireEvent("change", this);
+ }
+ else {
+ var oldval;
+ var change = aDir;
+ if (field == this.hourField) {
+ oldval = this.hour;
+ }
+ else if (field == this.minuteField) {
+ oldval = this.minute;
+ change *= this.increment;
+ }
+ else if (field == this.secondField) {
+ oldval = this.second;
+ }
+
+ var newval = this._constrainValue(field, oldval + change, false);
+
+ if (field == this.hourField)
+ this.hour = newval;
+ else if (field == this.minuteField)
+ this.minute = newval;
+ else if (field == this.secondField)
+ this.second = newval;
+
+ if (oldval != newval)
+ this._fireEvent("change", this);
+ }
+ field.select();
+ ]]>
+ </body>
+ </method>
+ <method name="_setFieldValue">
+ <parameter name="aField"/>
+ <parameter name="aValue"/>
+ <body>
+ <![CDATA[
+ if (aField == this.hourField)
+ this._dateValue.setHours(aValue);
+ else if (aField == this.minuteField)
+ this._dateValue.setMinutes(aValue);
+ else if (aField == this.secondField)
+ this._dateValue.setSeconds(aValue);
+
+ this.setAttribute("value", this.value);
+ this._updateUI(aField, aValue);
+
+ if (this.attachedControl)
+ this.attachedControl._setValueNoSync(this._dateValue);
+ ]]>
+ </body>
+ </method>
+ <method name="_updateUI">
+ <parameter name="aField"/>
+ <parameter name="aValue"/>
+ <body>
+ <![CDATA[
+ this._valueEntered = false;
+
+ var prependZero = false;
+ if (aField == this.hourField) {
+ prependZero = this.hourLeadingZero;
+ if (!this.is24HourClock) {
+ if (aValue >= 12) {
+ if (aValue > 12)
+ aValue -= 12;
+ this._fieldAMPM.value = this.pmIndicator;
+ }
+ else {
+ if (aValue == 0)
+ aValue = 12;
+ this._fieldAMPM.value = this.amIndicator;
+ }
+ }
+ }
+ else if (aField == this.minuteField) {
+ prependZero = this.minuteLeadingZero;
+ }
+ else if (aField == this.secondField) {
+ prependZero = this.secondLeadingZero;
+ }
+
+ if (prependZero && aValue < 10)
+ aField.value = "0" + aValue;
+ else
+ aField.value = aValue;
+ ]]>
+ </body>
+ </method>
+ <method name="_constrainValue">
+ <parameter name="aField"/>
+ <parameter name="aValue"/>
+ <parameter name="aNoWrap"/>
+ <body>
+ <![CDATA[
+ // aNoWrap is true when the user entered a value, so just
+ // constrain within limits. If false, the value is being
+ // incremented or decremented, so wrap around values
+ var max = (aField == this.hourField) ? 24 : 60;
+ if (aValue < 0)
+ return aNoWrap ? 0 : max + aValue;
+ if (aValue >= max)
+ return aNoWrap ? max - 1 : aValue - max;
+ return aValue;
+ ]]>
+ </body>
+ </method>
+ <method name="_init">
+ <body>
+ <![CDATA[
+ this.hourField = this._fieldOne;
+ this.minuteField = this._fieldTwo;
+ this.secondField = this._fieldThree;
+
+ var numberOrder = /^(\D*)\s*(\d+)(\D*)(\d+)(\D*)(\d+)\s*(\D*)$/;
+
+ var locale = Intl.DateTimeFormat().resolvedOptions().locale + "-u-ca-gregory-nu-latn";
+
+ var pmTime = new Date(2000, 0, 1, 16, 7, 9).toLocaleTimeString(locale);
+ var numberFields = pmTime.match(numberOrder);
+ if (numberFields) {
+ this._separatorFirst.value = numberFields[3];
+ this._separatorSecond.value = numberFields[5];
+ if (Number(numberFields[2]) > 12)
+ this.is24HourClock = true;
+ else
+ this.pmIndicator = numberFields[1] || numberFields[7];
+ }
+
+ var amTime = new Date(2000, 0, 1, 1, 7, 9).toLocaleTimeString(locale);
+ numberFields = amTime.match(numberOrder);
+ if (numberFields) {
+ this.hourLeadingZero = (numberFields[2].length > 1);
+ this.minuteLeadingZero = (numberFields[4].length > 1);
+ this.secondLeadingZero = (numberFields[6].length > 1);
+
+ if (!this.is24HourClock) {
+ this.amIndicator = numberFields[1] || numberFields[7];
+ if (numberFields[1]) {
+ var mfield = this._fieldAMPM.parentNode;
+ var mcontainer = mfield.parentNode;
+ mcontainer.insertBefore(mfield, mcontainer.firstChild);
+ }
+ var size = (numberFields[1] || numberFields[7]).length;
+ if (this.pmIndicator.length > size)
+ size = this.pmIndicator.length;
+ this._fieldAMPM.size = size;
+ this._fieldAMPM.maxLength = size;
+ }
+ else {
+ this._fieldAMPM.parentNode.collapsed = true;
+ }
+ }
+
+ this.hideSeconds = this.hideSeconds;
+ ]]>
+ </body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="keypress">
+ <![CDATA[
+ // just allow any printable character to switch the AM/PM state
+ if (event.charCode && !this.disabled && !this.readOnly &&
+ this._currentField == this._fieldAMPM) {
+ this.isPM = !this.isPM;
+ this._fieldAMPM.select();
+ this._fireEvent("change", this);
+ event.preventDefault();
+ }
+ ]]>
+ </handler>
+ </handlers>
+
+ </binding>
+
+ <binding id="datepicker"
+ extends="chrome://global/content/bindings/datetimepicker.xml#datetimepicker-base">
+
+ <implementation>
+ <field name="yearLeadingZero">false</field>
+ <field name="monthLeadingZero">true</field>
+ <field name="dateLeadingZero">true</field>
+
+ <field name="yearField"/>
+ <field name="monthField"/>
+ <field name="dateField"/>
+
+ <property name="value">
+ <getter>
+ <![CDATA[
+ var month = this._dateValue.getMonth();
+ month = (month < 9) ? month = "0" + ++month : month + 1;
+
+ var date = this._dateValue.getDate();
+ if (date < 10)
+ date = "0" + date;
+ return this._dateValue.getFullYear() + "-" + month + "-" + date;
+ ]]>
+
+ </getter>
+ <setter>
+ <![CDATA[
+ var results = val.match(/^([0-9]{1,4})\-([0-9]{1,2})\-([0-9]{1,2})$/);
+ if (!results)
+ throw "Invalid Date";
+
+ this.dateValue = new Date(results[1] + "/" + results[2] + "/" + results[3]);
+ this.setAttribute("value", this.value);
+ return val;
+ ]]>
+ </setter>
+ </property>
+ <property name="year" onget="return this._dateValue.getFullYear();">
+ <setter>
+ <![CDATA[
+ var valnum = Number(val);
+ if (isNaN(valnum) || valnum < 1 || valnum > 9999)
+ throw "Invalid Year";
+ this._setFieldValue(this.yearField, valnum);
+ return val;
+ ]]>
+ </setter>
+ </property>
+ <property name="month" onget="return this._dateValue.getMonth();">
+ <setter>
+ <![CDATA[
+ var valnum = Number(val);
+ if (isNaN(valnum) || valnum < 0 || valnum > 11)
+ throw "Invalid Month";
+ this._setFieldValue(this.monthField, valnum);
+ return val;
+ ]]>
+ </setter>
+ </property>
+ <property name="date" onget="return this._dateValue.getDate();">
+ <setter>
+ <![CDATA[
+ var valnum = Number(val);
+ if (isNaN(valnum) || valnum < 1 || valnum > 31)
+ throw "Invalid Date";
+ this._setFieldValue(this.dateField, valnum);
+ return val;
+ ]]>
+ </setter>
+ </property>
+ <property name="open" onget="return false;" onset="return val;"/>
+
+ <property name="displayedMonth" onget="return this.month;"
+ onset="this.month = val; return val;"/>
+ <property name="displayedYear" onget="return this.year;"
+ onset="this.year = val; return val;"/>
+
+ <method name="_setValueNoSync">
+ <parameter name="aValue"/>
+ <body>
+ <![CDATA[
+ var dt = new Date(aValue);
+ if (!isNaN(dt)) {
+ this._dateValue = dt;
+ this.setAttribute("value", this.value);
+ this._updateUI(this.yearField, this.year);
+ this._updateUI(this.monthField, this.month);
+ this._updateUI(this.dateField, this.date);
+ }
+ ]]>
+ </body>
+ </method>
+ <method name="_increaseOrDecrease">
+ <parameter name="aDir"/>
+ <body>
+ <![CDATA[
+ if (this.disabled || this.readOnly)
+ return;
+
+ var field = this._currentField;
+ if (this._valueEntered)
+ this._setValueOnChange(field);
+
+ var oldval;
+ if (field == this.yearField)
+ oldval = this.year;
+ else if (field == this.monthField)
+ oldval = this.month;
+ else if (field == this.dateField)
+ oldval = this.date;
+
+ var newval = this._constrainValue(field, oldval + aDir, false);
+
+ if (field == this.yearField)
+ this.year = newval;
+ else if (field == this.monthField)
+ this.month = newval;
+ else if (field == this.dateField)
+ this.date = newval;
+
+ if (oldval != newval)
+ this._fireEvent("change", this);
+ field.select();
+ ]]>
+ </body>
+ </method>
+ <method name="_setFieldValue">
+ <parameter name="aField"/>
+ <parameter name="aValue"/>
+ <body>
+ <![CDATA[
+ if (aField == this.yearField) {
+ let oldDate = this.date;
+ this._dateValue.setFullYear(aValue);
+ if (oldDate != this.date) {
+ this._dateValue.setDate(0);
+ this._updateUI(this.dateField, this.date);
+ }
+ }
+ else if (aField == this.monthField) {
+ let oldDate = this.date;
+ this._dateValue.setMonth(aValue);
+ if (oldDate != this.date) {
+ this._dateValue.setDate(0);
+ this._updateUI(this.dateField, this.date);
+ }
+ }
+ else if (aField == this.dateField) {
+ this._dateValue.setDate(aValue);
+ }
+
+ this.setAttribute("value", this.value);
+ this._updateUI(aField, aValue);
+
+ if (this.attachedControl)
+ this.attachedControl._setValueNoSync(this._dateValue);
+ ]]>
+ </body>
+ </method>
+ <method name="_updateUI">
+ <parameter name="aField"/>
+ <parameter name="aValue"/>
+ <body>
+ <![CDATA[
+ this._valueEntered = false;
+
+ var prependZero = false;
+ if (aField == this.yearField) {
+ if (this.yearLeadingZero) {
+ aField.value = ("000" + aValue).slice(-4);
+ return;
+ }
+ }
+ else if (aField == this.monthField) {
+ aValue++;
+ prependZero = this.monthLeadingZero;
+ }
+ else if (aField == this.dateField) {
+ prependZero = this.dateLeadingZero;
+ }
+ if (prependZero && aValue < 10)
+ aField.value = "0" + aValue;
+ else
+ aField.value = aValue;
+ ]]>
+ </body>
+ </method>
+ <method name="_constrainValue">
+ <parameter name="aField"/>
+ <parameter name="aValue"/>
+ <parameter name="aNoWrap"/>
+ <body>
+ <![CDATA[
+ // the month will be 1 to 12 if entered by the user, so subtract 1
+ if (aNoWrap && aField == this.monthField)
+ aValue--;
+
+ if (aField == this.dateField) {
+ if (aValue < 1)
+ return new Date(this.year, this.month + 1, 0).getDate();
+
+ var currentMonth = this.month;
+ var dt = new Date(this.year, currentMonth, aValue);
+ return (dt.getMonth() != currentMonth ? 1 : aValue);
+ }
+ var min = (aField == this.monthField) ? 0 : 1;
+ var max = (aField == this.monthField) ? 11 : 9999;
+ if (aValue < min)
+ return aNoWrap ? min : max;
+ if (aValue > max)
+ return aNoWrap ? max : min;
+ return aValue;
+ ]]>
+ </body>
+ </method>
+ <method name="_init">
+ <body>
+ <![CDATA[
+ // We'll default to YYYY/MM/DD to start.
+ var yfield = "input-one";
+ var mfield = "input-two";
+ var dfield = "input-three";
+ var twoDigitYear = false;
+ this.yearLeadingZero = true;
+ this.monthLeadingZero = true;
+ this.dateLeadingZero = true;
+
+ var numberOrder = /^(\D*)\s*(\d+)(\D*)(\d+)(\D*)(\d+)\s*(\D*)$/;
+
+ var locale = Intl.DateTimeFormat().resolvedOptions().locale + "-u-ca-gregory-nu-latn";
+
+ var dt = new Date(2002, 9, 4).toLocaleDateString(locale);
+ var numberFields = dt.match(numberOrder);
+ if (numberFields) {
+ this._separatorFirst.value = numberFields[3];
+ this._separatorSecond.value = numberFields[5];
+
+ var yi = 2, mi = 4, di = 6;
+
+ function fieldForNumber(i) {
+ if (i == 2)
+ return "input-one";
+ if (i == 4)
+ return "input-two";
+ return "input-three";
+ }
+
+ for (var i = 1; i < numberFields.length; i++) {
+ switch (Number(numberFields[i])) {
+ case 2:
+ twoDigitYear = true; // fall through
+ case 2002:
+ yi = i;
+ yfield = fieldForNumber(i);
+ break;
+ case 9, 10:
+ mi = i;
+ mfield = fieldForNumber(i);
+ break;
+ case 4:
+ di = i;
+ dfield = fieldForNumber(i);
+ break;
+ }
+ }
+
+ this.yearLeadingZero = (numberFields[yi].length > 1);
+ this.monthLeadingZero = (numberFields[mi].length > 1);
+ this.dateLeadingZero = (numberFields[di].length > 1);
+ }
+
+ this.yearField = document.getAnonymousElementByAttribute(this, "anonid", yfield);
+ if (!twoDigitYear)
+ this.yearField.parentNode.classList.add("datetimepicker-input-subbox", "datetimepicker-year");
+ this.monthField = document.getAnonymousElementByAttribute(this, "anonid", mfield);
+ this.dateField = document.getAnonymousElementByAttribute(this, "anonid", dfield);
+
+ this._fieldAMPM.parentNode.collapsed = true;
+ this.yearField.size = twoDigitYear ? 2 : 4;
+ this.yearField.maxLength = twoDigitYear ? 2 : 4;
+ ]]>
+ </body>
+ </method>
+ </implementation>
+
+ </binding>
+
+ <binding id="datepicker-grid"
+ extends="chrome://global/content/bindings/datetimepicker.xml#datepicker">
+
+ <content>
+ <vbox class="datepicker-mainbox"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <hbox class="datepicker-monthbox" align="center">
+ <button class="datepicker-previous datepicker-button" type="repeat"
+ xbl:inherits="disabled"
+ oncommand="document.getBindingParent(this)._increaseOrDecreaseMonth(-1);"/>
+ <spacer flex="1"/>
+ <deck anonid="monthlabeldeck">
+ <label class="datepicker-gridlabel" value=""/>
+ <label class="datepicker-gridlabel" value=""/>
+ <label class="datepicker-gridlabel" value=""/>
+ <label class="datepicker-gridlabel" value=""/>
+ <label class="datepicker-gridlabel" value=""/>
+ <label class="datepicker-gridlabel" value=""/>
+ <label class="datepicker-gridlabel" value=""/>
+ <label class="datepicker-gridlabel" value=""/>
+ <label class="datepicker-gridlabel" value=""/>
+ <label class="datepicker-gridlabel" value=""/>
+ <label class="datepicker-gridlabel" value=""/>
+ <label class="datepicker-gridlabel" value=""/>
+ </deck>
+ <label anonid="yearlabel" class="datepicker-gridlabel"/>
+ <spacer flex="1"/>
+ <button class="datepicker-next datepicker-button" type="repeat"
+ xbl:inherits="disabled"
+ oncommand="document.getBindingParent(this)._increaseOrDecreaseMonth(1);"/>
+ </hbox>
+ <grid class="datepicker-grid" role="grid">
+ <columns>
+ <column class="datepicker-gridrow" flex="1"/>
+ <column class="datepicker-gridrow" flex="1"/>
+ <column class="datepicker-gridrow" flex="1"/>
+ <column class="datepicker-gridrow" flex="1"/>
+ <column class="datepicker-gridrow" flex="1"/>
+ <column class="datepicker-gridrow" flex="1"/>
+ <column class="datepicker-gridrow" flex="1"/>
+ </columns>
+ <rows anonid="datebox">
+ <row anonid="dayofweekbox">
+ <label class="datepicker-weeklabel" role="columnheader"/>
+ <label class="datepicker-weeklabel" role="columnheader"/>
+ <label class="datepicker-weeklabel" role="columnheader"/>
+ <label class="datepicker-weeklabel" role="columnheader"/>
+ <label class="datepicker-weeklabel" role="columnheader"/>
+ <label class="datepicker-weeklabel" role="columnheader"/>
+ <label class="datepicker-weeklabel" role="columnheader"/>
+ </row>
+ <row>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ </row>
+ <row>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ </row>
+ <row>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ </row>
+ <row>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ </row>
+ <row>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ </row>
+ <row>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ <label class="datepicker-gridlabel" role="gridcell"/>
+ </row>
+ </rows>
+ </grid>
+ </vbox>
+ </content>
+
+ <implementation>
+ <field name="_hasEntry">false</field>
+ <field name="_weekStart">&firstdayofweek.default;</field>
+ <field name="_displayedDate">null</field>
+ <field name="_todayItem">null</field>
+
+ <field name="yearField">
+ document.getAnonymousElementByAttribute(this, "anonid", "yearlabel");
+ </field>
+ <field name="monthField">
+ document.getAnonymousElementByAttribute(this, "anonid", "monthlabeldeck");
+ </field>
+ <field name="dateField">
+ document.getAnonymousElementByAttribute(this, "anonid", "datebox");
+ </field>
+
+ <field name="_selectedItem">null</field>
+
+ <property name="selectedItem" onget="return this._selectedItem">
+ <setter>
+ <![CDATA[
+ if (!val.value)
+ return val;
+ if (val.parentNode.parentNode != this.dateField)
+ return val;
+
+ if (this._selectedItem)
+ this._selectedItem.removeAttribute("selected");
+ this._selectedItem = val;
+ val.setAttribute("selected", "true");
+ this._displayedDate.setDate(val.value);
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="displayedMonth">
+ <getter>
+ return this._displayedDate.getMonth();
+ </getter>
+ <setter>
+ this._updateUI(this.monthField, val, true);
+ return val;
+ </setter>
+ </property>
+ <property name="displayedYear">
+ <getter>
+ return this._displayedDate.getFullYear();
+ </getter>
+ <setter>
+ this._updateUI(this.yearField, val, true);
+ return val;
+ </setter>
+ </property>
+
+ <method name="_init">
+ <body>
+ <![CDATA[
+ var locale = Intl.DateTimeFormat().resolvedOptions().locale + "-u-ca-gregory";
+ var dtfMonth = Intl.DateTimeFormat(locale, {month: "long"});
+ var dtfWeekday = Intl.DateTimeFormat(locale, {weekday: "narrow"});
+
+ var monthLabel = this.monthField.firstChild;
+ var tempDate = new Date(2005, 0, 1);
+ for (var month = 0; month < 12; month++) {
+ tempDate.setMonth(month);
+ monthLabel.setAttribute("value", dtfMonth.format(tempDate));
+ monthLabel = monthLabel.nextSibling;
+ }
+
+ var fdow = Number(this.getAttribute("firstdayofweek"));
+ if (!isNaN(fdow) && fdow >= 0 && fdow <= 6)
+ this._weekStart = fdow;
+
+ var weekbox = document.getAnonymousElementByAttribute(this, "anonid", "dayofweekbox").childNodes;
+ var date = new Date();
+ date.setDate(date.getDate() - (date.getDay() - this._weekStart));
+ for (var i = 0; i < weekbox.length; i++) {
+ weekbox[i].value = dtfWeekday.format(date);
+ date.setDate(date.getDate() + 1);
+ }
+ ]]>
+ </body>
+ </method>
+ <method name="_setValueNoSync">
+ <parameter name="aValue"/>
+ <body>
+ <![CDATA[
+ var dt = new Date(aValue);
+ if (!isNaN(dt)) {
+ this._dateValue = dt;
+ this.setAttribute("value", this.value);
+ this._updateUI();
+ }
+ ]]>
+ </body>
+ </method>
+ <method name="_updateUI">
+ <parameter name="aField"/>
+ <parameter name="aValue"/>
+ <parameter name="aCheckMonth"/>
+ <body>
+ <![CDATA[
+ var date;
+ var currentMonth;
+ if (aCheckMonth) {
+ if (!this._displayedDate)
+ this._displayedDate = this.dateValue;
+
+ var expectedMonth = aValue;
+ if (aField == this.monthField) {
+ this._displayedDate.setMonth(aValue);
+ }
+ else {
+ expectedMonth = this._displayedDate.getMonth();
+ this._displayedDate.setFullYear(aValue);
+ }
+
+ if (expectedMonth != -1 && expectedMonth != 12 &&
+ expectedMonth != this._displayedDate.getMonth()) {
+ // If the month isn't what was expected, then the month overflowed.
+ // Setting the date to 0 will go back to the last day of the right month.
+ this._displayedDate.setDate(0);
+ }
+
+ date = new Date(this._displayedDate);
+ currentMonth = this._displayedDate.getMonth();
+ }
+ else {
+ var samemonth = (this._displayedDate &&
+ this._displayedDate.getMonth() == this.month &&
+ this._displayedDate.getFullYear() == this.year);
+ if (samemonth) {
+ var items = this.dateField.getElementsByAttribute("value", this.date);
+ if (items.length)
+ this.selectedItem = items[0];
+ return;
+ }
+
+ date = this.dateValue;
+ this._displayedDate = new Date(date);
+ currentMonth = this.month;
+ }
+
+ if (this._todayItem) {
+ this._todayItem.removeAttribute("today");
+ this._todayItem = null;
+ }
+
+ if (this._selectedItem) {
+ this._selectedItem.removeAttribute("selected");
+ this._selectedItem = null;
+ }
+
+ // Update the month and year title
+ this.monthField.selectedIndex = currentMonth;
+ this.yearField.setAttribute("value", date.getFullYear());
+
+ date.setDate(1);
+ var firstWeekday = (7 + date.getDay() - this._weekStart) % 7;
+ date.setDate(date.getDate() - firstWeekday);
+
+ var today = new Date();
+ var datebox = this.dateField;
+ for (var k = 1; k < datebox.childNodes.length; k++) {
+ var row = datebox.childNodes[k];
+ for (var i = 0; i < 7; i++) {
+ var item = row.childNodes[i];
+
+ if (currentMonth == date.getMonth()) {
+ item.value = date.getDate();
+
+ // highlight today
+ if (this._isSameDay(today, date)) {
+ this._todayItem = item;
+ item.setAttribute("today", "true");
+ }
+
+ // highlight the selected date
+ if (this._isSameDay(this._dateValue, date)) {
+ this._selectedItem = item;
+ item.setAttribute("selected", "true");
+ }
+ }
+ else {
+ item.value = "";
+ }
+
+ date.setDate(date.getDate() + 1);
+ }
+ }
+
+ this._fireEvent("monthchange", this);
+ if (this.hasAttribute("monthchange")) {
+ var fn = new Function("event", aTarget.getAttribute("onmonthchange"));
+ fn.call(aTarget, event);
+ }
+ ]]>
+ </body>
+ </method>
+ <method name="_increaseOrDecreaseDateFromEvent">
+ <parameter name="aEvent"/>
+ <parameter name="aDiff"/>
+ <body>
+ <![CDATA[
+ if (aEvent.originalTarget == this && !this.disabled && !this.readOnly) {
+ var newdate = this.dateValue;
+ newdate.setDate(newdate.getDate() + aDiff);
+ this.dateValue = newdate;
+ this._fireEvent("change", this);
+ }
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ ]]>
+ </body>
+ </method>
+ <method name="_increaseOrDecreaseMonth">
+ <parameter name="aDir"/>
+ <body>
+ <![CDATA[
+ if (!this.disabled) {
+ var month = this._displayedDate ? this._displayedDate.getMonth() :
+ this.month;
+ this._updateUI(this.monthField, month + aDir, true);
+ }
+ ]]>
+ </body>
+ </method>
+ <method name="_isSameDay">
+ <parameter name="aDate1"/>
+ <parameter name="aDate2"/>
+ <body>
+ <![CDATA[
+ return (aDate1 && aDate2 &&
+ aDate1.getDate() == aDate2.getDate() &&
+ aDate1.getMonth() == aDate2.getMonth() &&
+ aDate1.getFullYear() == aDate2.getFullYear());
+ ]]>
+ </body>
+ </method>
+
+ </implementation>
+
+ <handlers>
+ <handler event="click">
+ <![CDATA[
+ if (event.button != 0 || this.disabled || this.readOnly)
+ return;
+
+ var target = event.originalTarget;
+ if (target.classList.contains("datepicker-gridlabel") &&
+ target != this.selectedItem) {
+ this.selectedItem = target;
+ this._dateValue = new Date(this._displayedDate);
+ if (this.attachedControl)
+ this.attachedControl._setValueNoSync(this._dateValue);
+ this._fireEvent("change", this);
+
+ if (this.attachedControl && "open" in this.attachedControl)
+ this.attachedControl.open = false; // close the popup
+ }
+ ]]>
+ </handler>
+ <handler event="MozMousePixelScroll" preventdefault="true"/>
+ <handler event="DOMMouseScroll" preventdefault="true">
+ <![CDATA[
+ this._increaseOrDecreaseMonth(event.detail < 0 ? -1 : 1);
+ ]]>
+ </handler>
+ <handler event="keypress" keycode="VK_LEFT"
+ action="this._increaseOrDecreaseDateFromEvent(event, -1);"/>
+ <handler event="keypress" keycode="VK_RIGHT"
+ action="this._increaseOrDecreaseDateFromEvent(event, 1);"/>
+ <handler event="keypress" keycode="VK_UP"
+ action="this._increaseOrDecreaseDateFromEvent(event, -7);"/>
+ <handler event="keypress" keycode="VK_DOWN"
+ action="this._increaseOrDecreaseDateFromEvent(event, 7);"/>
+ <handler event="keypress" keycode="VK_PAGE_UP" preventdefault="true"
+ action="this._increaseOrDecreaseMonth(-1);"/>
+ <handler event="keypress" keycode="VK_PAGE_DOWN" preventdefault="true"
+ action="this._increaseOrDecreaseMonth(1);"/>
+ </handlers>
+ </binding>
+
+ <binding id="datepicker-popup" display="xul:menu"
+ extends="chrome://global/content/bindings/datetimepicker.xml#datepicker">
+ <content align="center">
+ <xul:hbox class="textbox-input-box datetimepicker-input-box" align="center"
+ allowevents="true" xbl:inherits="context,disabled,readonly">
+ <xul:hbox class="datetimepicker-input-subbox" align="baseline">
+ <html:input class="datetimepicker-input textbox-input" anonid="input-one"
+ size="2" maxlength="2"
+ xbl:inherits="disabled,readonly"/>
+ </xul:hbox>
+ <xul:label anonid="sep-first" class="datetimepicker-separator" value=":"/>
+ <xul:hbox class="datetimepicker-input-subbox" align="baseline">
+ <html:input class="datetimepicker-input textbox-input" anonid="input-two"
+ size="2" maxlength="2"
+ xbl:inherits="disabled,readonly"/>
+ </xul:hbox>
+ <xul:label anonid="sep-second" class="datetimepicker-separator" value=":"/>
+ <xul:hbox class="datetimepicker-input-subbox" align="center">
+ <html:input class="datetimepicker-input textbox-input" anonid="input-three"
+ size="2" maxlength="2"
+ xbl:inherits="disabled,readonly"/>
+ </xul:hbox>
+ <xul:hbox class="datetimepicker-input-subbox" align="center">
+ <html:input class="datetimepicker-input textbox-input" anonid="input-ampm"
+ size="2" maxlength="2"
+ xbl:inherits="disabled,readonly"/>
+ </xul:hbox>
+ </xul:hbox>
+ <xul:spinbuttons anonid="buttons" xbl:inherits="disabled" allowevents="true"
+ onup="this.parentNode._increaseOrDecrease(1);"
+ ondown="this.parentNode._increaseOrDecrease(-1);"/>
+ <xul:dropmarker class="datepicker-dropmarker" xbl:inherits="disabled"/>
+ <xul:panel onpopupshown="this.firstChild.focus();" level="top">
+ <xul:datepicker anonid="grid" type="grid" class="datepicker-popupgrid"
+ xbl:inherits="disabled,readonly,firstdayofweek"/>
+ </xul:panel>
+ </content>
+ <implementation>
+ <constructor>
+ var grid = document.getAnonymousElementByAttribute(this, "anonid", "grid");
+ this.attachedControl = grid;
+ grid.attachedControl = this;
+ grid._setValueNoSync(this._dateValue);
+ </constructor>
+ <property name="open" onget="return this.hasAttribute('open');">
+ <setter>
+ <![CDATA[
+ if (this.boxObject instanceof MenuBoxObject)
+ this.boxObject.openMenu(val);
+ return val;
+ ]]>
+ </setter>
+ </property>
+ <property name="displayedMonth">
+ <getter>
+ return document.getAnonymousElementByAttribute(this, "anonid", "grid").displayedMonth;
+ </getter>
+ <setter>
+ document.getAnonymousElementByAttribute(this, "anonid", "grid").displayedMonth = val;
+ return val;
+ </setter>
+ </property>
+ <property name="displayedYear">
+ <getter>
+ return document.getAnonymousElementByAttribute(this, "anonid", "grid").displayedYear;
+ </getter>
+ <setter>
+ document.getAnonymousElementByAttribute(this, "anonid", "grid").displayedYear = val;
+ return val;
+ </setter>
+ </property>
+ </implementation>
+ </binding>
+
+</bindings>
diff --git a/toolkit/content/widgets/datetimepopup.xml b/toolkit/content/widgets/datetimepopup.xml
new file mode 100644
index 000000000..327f45368
--- /dev/null
+++ b/toolkit/content/widgets/datetimepopup.xml
@@ -0,0 +1,181 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<bindings id="dateTimePopupBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+ <binding id="datetime-popup"
+ extends="chrome://global/content/bindings/popup.xml#arrowpanel">
+ <implementation>
+ <field name="dateTimePopupFrame">
+ this.querySelector("#dateTimePopupFrame");
+ </field>
+ <field name="TIME_PICKER_WIDTH" readonly="true">"12em"</field>
+ <field name="TIME_PICKER_HEIGHT" readonly="true">"21em"</field>
+ <method name="loadPicker">
+ <parameter name="type"/>
+ <parameter name="detail"/>
+ <body><![CDATA[
+ this.hidden = false;
+ this.type = type;
+ this.pickerState = {};
+ // TODO: Resize picker according to content zoom level
+ this.style.fontSize = "10px";
+ switch (type) {
+ case "time": {
+ this.detail = detail;
+ this.dateTimePopupFrame.addEventListener("load", this, true);
+ this.dateTimePopupFrame.setAttribute("src", "chrome://global/content/timepicker.xhtml");
+ this.dateTimePopupFrame.style.width = this.TIME_PICKER_WIDTH;
+ this.dateTimePopupFrame.style.height = this.TIME_PICKER_HEIGHT;
+ break;
+ }
+ }
+ ]]></body>
+ </method>
+ <method name="closePicker">
+ <body><![CDATA[
+ this.hidden = true;
+ this.setInputBoxValue(true);
+ this.pickerState = {};
+ this.type = undefined;
+ this.dateTimePopupFrame.removeEventListener("load", this, true);
+ this.dateTimePopupFrame.contentDocument.removeEventListener("TimePickerPopupChanged", this, false);
+ this.dateTimePopupFrame.setAttribute("src", "");
+ ]]></body>
+ </method>
+ <method name="setPopupValue">
+ <parameter name="data"/>
+ <body><![CDATA[
+ switch (this.type) {
+ case "time": {
+ this.postMessageToPicker({
+ name: "TimePickerSetValue",
+ detail: data.value
+ });
+ break;
+ }
+ }
+ ]]></body>
+ </method>
+ <method name="initPicker">
+ <parameter name="detail"/>
+ <body><![CDATA[
+ switch (this.type) {
+ case "time": {
+ const { hour, minute } = detail.value;
+ const format = detail.format || "12";
+ const locale = Components.classes["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIXULChromeRegistry).getSelectedLocale("global");
+
+ this.postMessageToPicker({
+ name: "TimePickerInit",
+ detail: {
+ hour,
+ minute,
+ format,
+ locale,
+ min: detail.min,
+ max: detail.max,
+ step: detail.step,
+ }
+ });
+ break;
+ }
+ }
+ ]]></body>
+ </method>
+ <method name="setInputBoxValue">
+ <parameter name="passAllValues"/>
+ <body><![CDATA[
+ /**
+ * @param {Boolean} passAllValues: Pass spinner values regardless if they've been set/changed or not
+ */
+ switch (this.type) {
+ case "time": {
+ const { hour, minute, isHourSet, isMinuteSet, isDayPeriodSet } = this.pickerState;
+ const isAnyValueSet = isHourSet || isMinuteSet || isDayPeriodSet;
+ if (passAllValues && isAnyValueSet) {
+ this.sendPickerValueChanged({ hour, minute });
+ } else {
+ this.sendPickerValueChanged({
+ hour: isHourSet || isDayPeriodSet ? hour : undefined,
+ minute: isMinuteSet ? minute : undefined
+ });
+ }
+ break;
+ }
+ }
+ ]]></body>
+ </method>
+ <method name="sendPickerValueChanged">
+ <parameter name="value"/>
+ <body><![CDATA[
+ switch (this.type) {
+ case "time": {
+ this.dispatchEvent(new CustomEvent("DateTimePickerValueChanged", {
+ detail: {
+ hour: value.hour,
+ minute: value.minute
+ }
+ }));
+ break;
+ }
+ }
+ ]]></body>
+ </method>
+ <method name="handleEvent">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ switch (aEvent.type) {
+ case "load": {
+ this.initPicker(this.detail);
+ this.dateTimePopupFrame.contentWindow.addEventListener("message", this, false);
+ break;
+ }
+ case "message": {
+ this.handleMessage(aEvent);
+ break;
+ }
+ }
+ ]]></body>
+ </method>
+ <method name="handleMessage">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ if (!this.dateTimePopupFrame.contentDocument.nodePrincipal.isSystemPrincipal) {
+ return;
+ }
+
+ switch (aEvent.data.name) {
+ case "TimePickerPopupChanged": {
+ this.pickerState = aEvent.data.detail;
+ this.setInputBoxValue();
+ break;
+ }
+ }
+ ]]></body>
+ </method>
+ <method name="postMessageToPicker">
+ <parameter name="data"/>
+ <body><![CDATA[
+ if (this.dateTimePopupFrame.contentDocument.nodePrincipal.isSystemPrincipal) {
+ this.dateTimePopupFrame.contentWindow.postMessage(data, "*");
+ }
+ ]]></body>
+ </method>
+
+ </implementation>
+ <handlers>
+ <handler event="popuphiding">
+ <![CDATA[
+ this.closePicker();
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+</bindings>
diff --git a/toolkit/content/widgets/dialog.xml b/toolkit/content/widgets/dialog.xml
new file mode 100644
index 000000000..d83570ac0
--- /dev/null
+++ b/toolkit/content/widgets/dialog.xml
@@ -0,0 +1,448 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<bindings id="dialogBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="dialog" extends="chrome://global/content/bindings/general.xml#root-element">
+ <resources>
+ <stylesheet src="chrome://global/skin/dialog.css"/>
+ </resources>
+ <content>
+ <xul:vbox class="box-inherit dialog-content-box" flex="1">
+ <children/>
+ </xul:vbox>
+
+ <xul:hbox class="dialog-button-box" anonid="buttons"
+ xbl:inherits="pack=buttonpack,align=buttonalign,dir=buttondir,orient=buttonorient"
+#ifdef XP_UNIX
+ >
+ <xul:button dlgtype="disclosure" class="dialog-button" hidden="true"/>
+ <xul:button dlgtype="help" class="dialog-button" hidden="true"/>
+ <xul:button dlgtype="extra2" class="dialog-button" hidden="true"/>
+ <xul:button dlgtype="extra1" class="dialog-button" hidden="true"/>
+ <xul:spacer anonid="spacer" flex="1"/>
+ <xul:button dlgtype="cancel" class="dialog-button"/>
+ <xul:button dlgtype="accept" class="dialog-button" xbl:inherits="disabled=buttondisabledaccept"/>
+#else
+ pack="end">
+ <xul:button dlgtype="extra2" class="dialog-button" hidden="true"/>
+ <xul:spacer anonid="spacer" flex="1" hidden="true"/>
+ <xul:button dlgtype="accept" class="dialog-button" xbl:inherits="disabled=buttondisabledaccept"/>
+ <xul:button dlgtype="extra1" class="dialog-button" hidden="true"/>
+ <xul:button dlgtype="cancel" class="dialog-button"/>
+ <xul:button dlgtype="help" class="dialog-button" hidden="true"/>
+ <xul:button dlgtype="disclosure" class="dialog-button" hidden="true"/>
+#endif
+ </xul:hbox>
+ </content>
+
+ <implementation>
+ <field name="_mStrBundle">null</field>
+ <field name="_closeHandler">(function(event) {
+ if (!document.documentElement.cancelDialog())
+ event.preventDefault();
+ })</field>
+
+ <property name="buttons"
+ onget="return this.getAttribute('buttons');"
+ onset="this._configureButtons(val); return val;"/>
+
+ <property name="defaultButton">
+ <getter>
+ <![CDATA[
+ if (this.hasAttribute("defaultButton"))
+ return this.getAttribute("defaultButton");
+ return "accept"; // default to the accept button
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ this._setDefaultButton(val);
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <method name="acceptDialog">
+ <body>
+ <![CDATA[
+ return this._doButtonCommand("accept");
+ ]]>
+ </body>
+ </method>
+
+ <method name="cancelDialog">
+ <body>
+ <![CDATA[
+ return this._doButtonCommand("cancel");
+ ]]>
+ </body>
+ </method>
+
+ <method name="getButton">
+ <parameter name="aDlgType"/>
+ <body>
+ <![CDATA[
+ return this._buttons[aDlgType];
+ ]]>
+ </body>
+ </method>
+
+ <method name="moveToAlertPosition">
+ <body>
+ <![CDATA[
+ // hack. we need this so the window has something like its final size
+ if (window.outerWidth == 1) {
+ dump("Trying to position a sizeless window; caller should have called sizeToContent() or sizeTo(). See bug 75649.\n");
+ sizeToContent();
+ }
+
+ if (opener) {
+ var xOffset = (opener.outerWidth - window.outerWidth) / 2;
+ var yOffset = opener.outerHeight / 5;
+
+ var newX = opener.screenX + xOffset;
+ var newY = opener.screenY + yOffset;
+ } else {
+ newX = (screen.availWidth - window.outerWidth) / 2;
+ newY = (screen.availHeight - window.outerHeight) / 2;
+ }
+
+ // ensure the window is fully onscreen (if smaller than the screen)
+ if (newX < screen.availLeft)
+ newX = screen.availLeft + 20;
+ if ((newX + window.outerWidth) > (screen.availLeft + screen.availWidth))
+ newX = (screen.availLeft + screen.availWidth) - window.outerWidth - 20;
+
+ if (newY < screen.availTop)
+ newY = screen.availTop + 20;
+ if ((newY + window.outerHeight) > (screen.availTop + screen.availHeight))
+ newY = (screen.availTop + screen.availHeight) - window.outerHeight - 60;
+
+ window.moveTo( newX, newY );
+ ]]>
+ </body>
+ </method>
+
+ <method name="centerWindowOnScreen">
+ <body>
+ <![CDATA[
+ var xOffset = screen.availWidth/2 - window.outerWidth/2;
+ var yOffset = screen.availHeight/2 - window.outerHeight/2;
+
+ xOffset = xOffset > 0 ? xOffset : 0;
+ yOffset = yOffset > 0 ? yOffset : 0;
+ window.moveTo(xOffset, yOffset);
+ ]]>
+ </body>
+ </method>
+
+ <constructor>
+ <![CDATA[
+ this._configureButtons(this.buttons);
+
+ // listen for when window is closed via native close buttons
+ window.addEventListener("close", this._closeHandler, false);
+
+ // for things that we need to initialize after onload fires
+ window.addEventListener("load", this.postLoadInit, false);
+
+ window.moveToAlertPosition = this.moveToAlertPosition;
+ window.centerWindowOnScreen = this.centerWindowOnScreen;
+ ]]>
+ </constructor>
+
+ <method name="postLoadInit">
+ <parameter name="aEvent"/>
+ <body>
+ <![CDATA[
+ function focusInit() {
+ const dialog = document.documentElement;
+ const defaultButton = dialog.getButton(dialog.defaultButton);
+ // give focus to the first focusable element in the dialog
+ if (!document.commandDispatcher.focusedElement) {
+ document.commandDispatcher.advanceFocusIntoSubtree(dialog);
+
+ var focusedElt = document.commandDispatcher.focusedElement;
+ if (focusedElt) {
+ var initialFocusedElt = focusedElt;
+ while (focusedElt.localName == "tab" ||
+ focusedElt.getAttribute("noinitialfocus") == "true") {
+ document.commandDispatcher.advanceFocusIntoSubtree(focusedElt);
+ focusedElt = document.commandDispatcher.focusedElement;
+ if (focusedElt == initialFocusedElt)
+ break;
+ }
+
+ if (initialFocusedElt.localName == "tab") {
+ if (focusedElt.hasAttribute("dlgtype")) {
+ // We don't want to focus on anonymous OK, Cancel, etc. buttons,
+ // so return focus to the tab itself
+ initialFocusedElt.focus();
+ }
+ }
+ else if (!/Mac/.test(navigator.platform) &&
+ focusedElt.hasAttribute("dlgtype") && focusedElt != defaultButton) {
+ defaultButton.focus();
+ }
+ }
+ }
+
+ try {
+ if (defaultButton)
+ window.notifyDefaultButtonLoaded(defaultButton);
+ } catch (e) { }
+ }
+
+ // Give focus after onload completes, see bug 103197.
+ setTimeout(focusInit, 0);
+ ]]>
+ </body>
+ </method>
+
+ <property name="mStrBundle">
+ <getter>
+ <![CDATA[
+ if (!this._mStrBundle) {
+ // need to create string bundle manually instead of using <xul:stringbundle/>
+ // see bug 63370 for details
+ this._mStrBundle = Components.classes["@mozilla.org/intl/stringbundle;1"]
+ .getService(Components.interfaces.nsIStringBundleService)
+ .createBundle("chrome://global/locale/dialog.properties");
+ }
+ return this._mStrBundle;
+ ]]></getter>
+ </property>
+
+ <method name="_configureButtons">
+ <parameter name="aButtons"/>
+ <body>
+ <![CDATA[
+ // by default, get all the anonymous button elements
+ var buttons = {};
+ this._buttons = buttons;
+ buttons.accept = document.getAnonymousElementByAttribute(this, "dlgtype", "accept");
+ buttons.cancel = document.getAnonymousElementByAttribute(this, "dlgtype", "cancel");
+ buttons.extra1 = document.getAnonymousElementByAttribute(this, "dlgtype", "extra1");
+ buttons.extra2 = document.getAnonymousElementByAttribute(this, "dlgtype", "extra2");
+ buttons.help = document.getAnonymousElementByAttribute(this, "dlgtype", "help");
+ buttons.disclosure = document.getAnonymousElementByAttribute(this, "dlgtype", "disclosure");
+
+ // look for any overriding explicit button elements
+ var exBtns = this.getElementsByAttribute("dlgtype", "*");
+ var dlgtype;
+ var i;
+ for (i = 0; i < exBtns.length; ++i) {
+ dlgtype = exBtns[i].getAttribute("dlgtype");
+ buttons[dlgtype].hidden = true; // hide the anonymous button
+ buttons[dlgtype] = exBtns[i];
+ }
+
+ // add the label and oncommand handler to each button
+ for (dlgtype in buttons) {
+ var button = buttons[dlgtype];
+ button.addEventListener("command", this._handleButtonCommand, true);
+
+ // don't override custom labels with pre-defined labels on explicit buttons
+ if (!button.hasAttribute("label")) {
+ // dialog attributes override the default labels in dialog.properties
+ if (this.hasAttribute("buttonlabel"+dlgtype)) {
+ button.setAttribute("label", this.getAttribute("buttonlabel"+dlgtype));
+ if (this.hasAttribute("buttonaccesskey"+dlgtype))
+ button.setAttribute("accesskey", this.getAttribute("buttonaccesskey"+dlgtype));
+ } else if (dlgtype != "extra1" && dlgtype != "extra2") {
+ button.setAttribute("label", this.mStrBundle.GetStringFromName("button-"+dlgtype));
+ var accessKey = this.mStrBundle.GetStringFromName("accesskey-"+dlgtype);
+ if (accessKey)
+ button.setAttribute("accesskey", accessKey);
+ }
+ }
+ // allow specifying alternate icons in the dialog header
+ if (!button.hasAttribute("icon")) {
+ // if there's an icon specified, use that
+ if (this.hasAttribute("buttonicon"+dlgtype))
+ button.setAttribute("icon", this.getAttribute("buttonicon"+dlgtype));
+ // otherwise set defaults
+ else
+ switch (dlgtype) {
+ case "accept":
+ button.setAttribute("icon", "accept");
+ break;
+ case "cancel":
+ button.setAttribute("icon", "cancel");
+ break;
+ case "disclosure":
+ button.setAttribute("icon", "properties");
+ break;
+ case "help":
+ button.setAttribute("icon", "help");
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ // ensure that hitting enter triggers the default button command
+ this.defaultButton = this.defaultButton;
+
+ // if there is a special button configuration, use it
+ if (aButtons) {
+ // expect a comma delimited list of dlgtype values
+ var list = aButtons.split(",");
+
+ // mark shown dlgtypes as true
+ var shown = { accept: false, cancel: false, help: false,
+ disclosure: false, extra1: false, extra2: false };
+ for (i = 0; i < list.length; ++i)
+ shown[list[i].replace(/ /g, "")] = true;
+
+ // hide/show the buttons we want
+ for (dlgtype in buttons)
+ buttons[dlgtype].hidden = !shown[dlgtype];
+
+ // show the spacer on Windows only when the extra2 button is present
+ if (/Win/.test(navigator.platform)) {
+ var spacer = document.getAnonymousElementByAttribute(this, "anonid", "spacer");
+ spacer.removeAttribute("hidden");
+ spacer.setAttribute("flex", shown["extra2"]?"1":"0");
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="_setDefaultButton">
+ <parameter name="aNewDefault"/>
+ <body>
+ <![CDATA[
+ // remove the default attribute from the previous default button, if any
+ var oldDefaultButton = this.getButton(this.defaultButton);
+ if (oldDefaultButton)
+ oldDefaultButton.removeAttribute("default");
+
+ var newDefaultButton = this.getButton(aNewDefault);
+ if (newDefaultButton) {
+ this.setAttribute("defaultButton", aNewDefault);
+ newDefaultButton.setAttribute("default", "true");
+ }
+ else {
+ this.setAttribute("defaultButton", "none");
+ if (aNewDefault != "none")
+ dump("invalid new default button: " + aNewDefault + ", assuming: none\n");
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="_handleButtonCommand">
+ <parameter name="aEvent"/>
+ <body>
+ <![CDATA[
+ return document.documentElement._doButtonCommand(
+ aEvent.target.getAttribute("dlgtype"));
+ ]]>
+ </body>
+ </method>
+
+ <method name="_doButtonCommand">
+ <parameter name="aDlgType"/>
+ <body>
+ <![CDATA[
+ var button = this.getButton(aDlgType);
+ if (!button.disabled) {
+ var noCancel = this._fireButtonEvent(aDlgType);
+ if (noCancel) {
+ if (aDlgType == "accept" || aDlgType == "cancel") {
+ var closingEvent = new CustomEvent("dialogclosing", {
+ bubbles: true,
+ detail: { button: aDlgType },
+ });
+ this.dispatchEvent(closingEvent);
+ window.close();
+ }
+ }
+ return noCancel;
+ }
+ return true;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_fireButtonEvent">
+ <parameter name="aDlgType"/>
+ <body>
+ <![CDATA[
+ var event = document.createEvent("Events");
+ event.initEvent("dialog"+aDlgType, true, true);
+
+ // handle dom event handlers
+ var noCancel = this.dispatchEvent(event);
+
+ // handle any xml attribute event handlers
+ var handler = this.getAttribute("ondialog"+aDlgType);
+ if (handler != "") {
+ var fn = new Function("event", handler);
+ var returned = fn(event);
+ if (returned == false)
+ noCancel = false;
+ }
+
+ return noCancel;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_hitEnter">
+ <parameter name="evt"/>
+ <body>
+ <![CDATA[
+ if (evt.defaultPrevented)
+ return;
+
+ var btn = this.getButton(this.defaultButton);
+ if (btn)
+ this._doButtonCommand(this.defaultButton);
+ ]]>
+ </body>
+ </method>
+
+ </implementation>
+
+ <handlers>
+ <handler event="keypress" keycode="VK_RETURN"
+ group="system" action="this._hitEnter(event);"/>
+ <handler event="keypress" keycode="VK_ESCAPE" group="system">
+ if (!event.defaultPrevented)
+ this.cancelDialog();
+ </handler>
+#ifdef XP_MACOSX
+ <handler event="keypress" key="." modifiers="meta" phase="capturing" action="this.cancelDialog();"/>
+#else
+ <handler event="focus" phase="capturing">
+ var btn = this.getButton(this.defaultButton);
+ if (btn)
+ btn.setAttribute("default", event.originalTarget == btn || !(event.originalTarget instanceof Components.interfaces.nsIDOMXULButtonElement));
+ </handler>
+#endif
+ </handlers>
+
+ </binding>
+
+ <binding id="dialogheader">
+ <resources>
+ <stylesheet src="chrome://global/skin/dialog.css"/>
+ </resources>
+ <content>
+ <xul:label class="dialogheader-title" xbl:inherits="value=title,crop" crop="right" flex="1"/>
+ <xul:label class="dialogheader-description" xbl:inherits="value=description"/>
+ </content>
+ </binding>
+
+</bindings>
diff --git a/toolkit/content/widgets/editor.xml b/toolkit/content/widgets/editor.xml
new file mode 100644
index 000000000..637586dc2
--- /dev/null
+++ b/toolkit/content/widgets/editor.xml
@@ -0,0 +1,195 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<bindings id="editorBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="editor" role="outerdoc">
+ <implementation type="application/javascript">
+ <constructor>
+ <![CDATA[
+ // Make window editable immediately only
+ // if the "editortype" attribute is supplied
+ // This allows using same contentWindow for different editortypes,
+ // where the type is determined during the apps's window.onload handler.
+ if (this.editortype)
+ this.makeEditable(this.editortype, true);
+ ]]>
+ </constructor>
+ <destructor/>
+
+ <field name="_editorContentListener">
+ <![CDATA[
+ ({
+ QueryInterface: function(iid)
+ {
+ if (iid.equals(Components.interfaces.nsIURIContentListener) ||
+ iid.equals(Components.interfaces.nsISupportsWeakReference) ||
+ iid.equals(Components.interfaces.nsISupports))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ },
+ onStartURIOpen: function(uri)
+ {
+ return false;
+ },
+ doContent: function(contentType, isContentPreferred, request, contentHandler)
+ {
+ return false;
+ },
+ isPreferred: function(contentType, desiredContentType)
+ {
+ return false;
+ },
+ canHandleContent: function(contentType, isContentPreferred, desiredContentType)
+ {
+ return false;
+ },
+ loadCookie: null,
+ parentContentListener: null
+ })
+ ]]>
+ </field>
+ <method name="makeEditable">
+ <parameter name="editortype"/>
+ <parameter name="waitForUrlLoad"/>
+ <body>
+ <![CDATA[
+ this.editingSession.makeWindowEditable(this.contentWindow, editortype, waitForUrlLoad, true, false);
+ this.setAttribute("editortype", editortype);
+
+ this.docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIURIContentListener)
+ .parentContentListener = this._editorContentListener;
+ ]]>
+ </body>
+ </method>
+ <method name="getEditor">
+ <parameter name="containingWindow"/>
+ <body>
+ <![CDATA[
+ return this.editingSession.getEditorForWindow(containingWindow);
+ ]]>
+ </body>
+ </method>
+ <method name="getHTMLEditor">
+ <parameter name="containingWindow"/>
+ <body>
+ <![CDATA[
+ var editor = this.editingSession.getEditorForWindow(containingWindow);
+ return editor.QueryInterface(Components.interfaces.nsIHTMLEditor);
+ ]]>
+ </body>
+ </method>
+
+ <field name="_finder">null</field>
+ <property name="finder" readonly="true">
+ <getter><![CDATA[
+ if (!this._finder) {
+ if (!this.docShell)
+ return null;
+
+ let Finder = Components.utils.import("resource://gre/modules/Finder.jsm", {}).Finder;
+ this._finder = new Finder(this.docShell);
+ }
+ return this._finder;
+ ]]></getter>
+ </property>
+
+ <field name="_fastFind">null</field>
+ <property name="fastFind"
+ readonly="true">
+ <getter>
+ <![CDATA[
+ if (!this._fastFind) {
+ if (!("@mozilla.org/typeaheadfind;1" in Components.classes))
+ return null;
+
+ if (!this.docShell)
+ return null;
+
+ this._fastFind = Components.classes["@mozilla.org/typeaheadfind;1"]
+ .createInstance(Components.interfaces.nsITypeAheadFind);
+ this._fastFind.init(this.docShell);
+ }
+ return this._fastFind;
+ ]]>
+ </getter>
+ </property>
+
+ <field name="_lastSearchString">null</field>
+
+ <property name="editortype"
+ onget="return this.getAttribute('editortype');"
+ onset="this.setAttribute('editortype', val); return val;"/>
+ <property name="webNavigation"
+ onget="return this.docShell.QueryInterface(Components.interfaces.nsIWebNavigation);"
+ readonly="true"/>
+ <property name="contentDocument" readonly="true"
+ onget="return this.webNavigation.document;"/>
+ <property name="docShell" readonly="true">
+ <getter><![CDATA[
+ let frameLoader = this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader;
+ return frameLoader ? frameLoader.docShell : null;
+ ]]></getter>
+ </property>
+ <property name="currentURI"
+ readonly="true"
+ onget="return this.webNavigation.currentURI;"/>
+ <property name="contentWindow"
+ readonly="true"
+ onget="return this.docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindow);"/>
+ <property name="contentWindowAsCPOW"
+ readonly="true"
+ onget="return this.contentWindow;"/>
+ <property name="webBrowserFind"
+ readonly="true"
+ onget="return this.docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIWebBrowserFind);"/>
+ <property name="markupDocumentViewer"
+ readonly="true"
+ onget="return this.docShell.contentViewer;"/>
+ <property name="editingSession"
+ readonly="true"
+ onget="return this.webNavigation.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIEditingSession);"/>
+ <property name="commandManager"
+ readonly="true"
+ onget="return this.webNavigation.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsICommandManager);"/>
+ <property name="fullZoom"
+ onget="return this.markupDocumentViewer.fullZoom;"
+ onset="this.markupDocumentViewer.fullZoom = val;"/>
+ <property name="textZoom"
+ onget="return this.markupDocumentViewer.textZoom;"
+ onset="this.markupDocumentViewer.textZoom = val;"/>
+ <property name="isSyntheticDocument"
+ onget="return this.contentDocument.isSyntheticDocument;"
+ readonly="true"/>
+ <property name="messageManager"
+ readonly="true">
+ <getter>
+ <![CDATA[
+ var owner = this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner);
+ if (!owner.frameLoader) {
+ return null;
+ }
+ return owner.frameLoader.messageManager;
+ ]]>
+ </getter>
+ </property>
+ <property name="outerWindowID" readonly="true">
+ <getter><![CDATA[
+ return this.contentWindow
+ .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils)
+ .outerWindowID;
+ ]]></getter>
+ </property>
+ </implementation>
+ </binding>
+
+</bindings>
diff --git a/toolkit/content/widgets/expander.xml b/toolkit/content/widgets/expander.xml
new file mode 100644
index 000000000..a4ffea313
--- /dev/null
+++ b/toolkit/content/widgets/expander.xml
@@ -0,0 +1,86 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<bindings id="expanderBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xbl="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <binding id="expander" display="xul:vbox">
+ <resources>
+ <stylesheet src="chrome://global/skin/expander.css"/>
+ </resources>
+ <content>
+ <xul:hbox align="center">
+ <xul:button type="disclosure" class="expanderButton" anonid="disclosure" xbl:inherits="disabled" mousethrough="always"/>
+ <xul:label class="header expanderButton" anonid="label" xbl:inherits="value=label,disabled" mousethrough="always" flex="1"/>
+ <xul:button anonid="clear-button" xbl:inherits="label=clearlabel,disabled=cleardisabled,hidden=clearhidden" mousethrough="always" icon="clear"/>
+ </xul:hbox>
+ <xul:vbox flex="1" anonid="settings" class="settingsContainer" collapsed="true" xbl:inherits="align">
+ <children/>
+ </xul:vbox>
+ </content>
+ <implementation>
+ <constructor><![CDATA[
+ var settings = document.getAnonymousElementByAttribute(this, "anonid", "settings");
+ var expander = document.getAnonymousElementByAttribute(this, "anonid", "disclosure");
+ var open = this.getAttribute("open") == "true";
+ settings.collapsed = !open;
+ expander.open = open;
+ ]]></constructor>
+ <property name="open">
+ <setter>
+ <![CDATA[
+ var settings = document.getAnonymousElementByAttribute(this, "anonid", "settings");
+ var expander = document.getAnonymousElementByAttribute(this, "anonid", "disclosure");
+ settings.collapsed = !val;
+ expander.open = val;
+ if (val)
+ this.setAttribute("open", "true");
+ else
+ this.setAttribute("open", "false");
+ return val;
+ ]]>
+ </setter>
+ <getter>
+ return this.getAttribute("open");
+ </getter>
+ </property>
+ <method name="onCommand">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ var element = aEvent.originalTarget;
+ var button = element.getAttribute("anonid");
+ switch (button) {
+ case "disclosure":
+ case "label":
+ if (this.open == "true")
+ this.open = false;
+ else
+ this.open = true;
+ break;
+ case "clear-button":
+ var event = document.createEvent("Events");
+ event.initEvent("clear", true, true);
+ this.dispatchEvent(event);
+ break;
+ }
+ ]]></body>
+ </method>
+ </implementation>
+ <handlers>
+ <handler event="command"><![CDATA[
+ this.onCommand(event);
+ ]]></handler>
+ <handler event="click"><![CDATA[
+ if (event.originalTarget.localName == "label")
+ this.onCommand(event);
+ ]]></handler>
+ </handlers>
+ </binding>
+
+</bindings>
+
+
diff --git a/toolkit/content/widgets/filefield.xml b/toolkit/content/widgets/filefield.xml
new file mode 100644
index 000000000..f81761eb5
--- /dev/null
+++ b/toolkit/content/widgets/filefield.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<bindings id="filefieldBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xbl="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <binding id="filefield" extends="chrome://global/content/bindings/general.xml#basetext">
+ <resources>
+ <stylesheet src="chrome://global/skin/filefield.css"/>
+ </resources>
+ <content>
+ <xul:stringbundle anonid="bundle" src="chrome://global/locale/filefield.properties"/>
+ <xul:hbox class="fileFieldContentBox" align="center" flex="1" xbl:inherits="disabled">
+ <xul:image class="fileFieldIcon" xbl:inherits="src=image,disabled"/>
+ <xul:textbox class="fileFieldLabel" xbl:inherits="value=label,disabled,accesskey,tabindex,aria-labelledby" flex="1" readonly="true"/>
+ </xul:hbox>
+ </content>
+ <implementation implements="nsIDOMXULLabeledControlElement">
+ <property name="label" onget="return this.getAttribute('label');">
+ <setter>
+ this.setAttribute('label', val);
+ var elt = document.getAnonymousElementByAttribute(this, "class", "fileFieldLabel");
+ return (elt.value = val);
+ </setter>
+ </property>
+
+ <field name="_file">null</field>
+ <property name="file" onget="return this._file">
+ <setter>
+ <![CDATA[
+ this._file = val;
+ if (val) {
+ this.image = this._getIconURLForFile(val);
+ this.label = this._getDisplayNameForFile(val);
+ }
+ else {
+ this.removeAttribute("image");
+ var bundle = document.getAnonymousElementByAttribute(this, "anonid", "bundle");
+ this.label = bundle.getString("downloadHelperNoneSelected");
+ }
+ return val;
+ ]]>
+ </setter>
+ </property>
+ <method name="_getDisplayNameForFile">
+ <parameter name="aFile"/>
+ <body>
+ <![CDATA[
+ if (/Win/.test(navigator.platform)) {
+ var lfw = aFile.QueryInterface(Components.interfaces.nsILocalFileWin);
+ try {
+ return lfw.getVersionInfoField("FileDescription");
+ }
+ catch (e) {
+ // fall through to the filename
+ }
+ } else if (/Mac/.test(navigator.platform)) {
+ var lfm = aFile.QueryInterface(Components.interfaces.nsILocalFileMac);
+ try {
+ return lfm.bundleDisplayName;
+ }
+ catch (e) {
+ // fall through to the file name
+ }
+ }
+ var ios = Components.classes["@mozilla.org/network/io-service;1"]
+ .getService(Components.interfaces.nsIIOService);
+ var url = ios.newFileURI(aFile).QueryInterface(Components.interfaces.nsIURL);
+ return url.fileName;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_getIconURLForFile">
+ <parameter name="aFile"/>
+ <body>
+ <![CDATA[
+ if (!aFile)
+ return "";
+ var ios = Components.classes["@mozilla.org/network/io-service;1"]
+ .getService(Components.interfaces.nsIIOService);
+ var fph = ios.getProtocolHandler("file")
+ .QueryInterface(Components.interfaces.nsIFileProtocolHandler);
+ var urlspec = fph.getURLSpecFromFile(aFile);
+ return "moz-icon://" + urlspec + "?size=16";
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+</bindings>
diff --git a/toolkit/content/widgets/findbar.xml b/toolkit/content/widgets/findbar.xml
new file mode 100644
index 000000000..f90d41227
--- /dev/null
+++ b/toolkit/content/widgets/findbar.xml
@@ -0,0 +1,1397 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE bindings [
+<!ENTITY % findBarDTD SYSTEM "chrome://global/locale/findbar.dtd" >
+%findBarDTD;
+]>
+
+<bindings id="findbarBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <!-- Private binding -->
+ <binding id="findbar-textbox"
+ extends="chrome://global/content/bindings/textbox.xml#textbox">
+ <implementation>
+
+ <field name="_findbar">null</field>
+ <property name="findbar" readonly="true">
+ <getter>
+ return this._findbar ?
+ this._findbar : this._findbar = document.getBindingParent(this);
+ </getter>
+ </property>
+
+ <method name="_handleEnter">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ if (this.findbar._findMode == this.findbar.FIND_NORMAL) {
+ let findString = this.findbar._findField;
+ if (!findString.value)
+ return;
+ if (aEvent.getModifierState("Accel")) {
+ this.findbar.getElement("highlight").click();
+ return;
+ }
+
+ this.findbar.onFindAgainCommand(aEvent.shiftKey);
+ } else {
+ this.findbar._finishFAYT(aEvent);
+ }
+ ]]></body>
+ </method>
+
+ <method name="_handleTab">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ let shouldHandle = !aEvent.altKey && !aEvent.ctrlKey &&
+ !aEvent.metaKey;
+ if (shouldHandle &&
+ this.findbar._findMode != this.findbar.FIND_NORMAL) {
+
+ this.findbar._finishFAYT(aEvent);
+ }
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="input"><![CDATA[
+ // We should do nothing during composition. E.g., composing string
+ // before converting may matches a forward word of expected word.
+ // After that, even if user converts the composition string to the
+ // expected word, it may find second or later searching word in the
+ // document.
+ if (this.findbar._isIMEComposing) {
+ return;
+ }
+
+ if (this._hadValue && !this.value) {
+ this._willfullyDeleted = true;
+ this._hadValue = false;
+ } else if (this.value.trim()) {
+ this._hadValue = true;
+ this._willfullyDeleted = false;
+ }
+ this.findbar._find(this.value);
+ ]]></handler>
+
+ <handler event="keypress"><![CDATA[
+ let shouldHandle = !event.altKey && !event.ctrlKey &&
+ !event.metaKey && !event.shiftKey;
+
+ switch (event.keyCode) {
+ case KeyEvent.DOM_VK_RETURN:
+ this._handleEnter(event);
+ break;
+ case KeyEvent.DOM_VK_TAB:
+ this._handleTab(event);
+ break;
+ case KeyEvent.DOM_VK_PAGE_UP:
+ case KeyEvent.DOM_VK_PAGE_DOWN:
+ if (shouldHandle) {
+ this.findbar.browser.finder.keyPress(event);
+ event.preventDefault();
+ }
+ break;
+ case KeyEvent.DOM_VK_UP:
+ case KeyEvent.DOM_VK_DOWN:
+ this.findbar.browser.finder.keyPress(event);
+ event.preventDefault();
+ break;
+ }
+ ]]></handler>
+
+ <handler event="blur"><![CDATA[
+ let findbar = this.findbar;
+ // Note: This code used to remove the selection
+ // if it matched an editable.
+ findbar.browser.finder.enableSelection();
+ ]]></handler>
+
+ <handler event="focus"><![CDATA[
+ if (/Mac/.test(navigator.platform)) {
+ let findbar = this.findbar;
+ findbar._onFindFieldFocus();
+ }
+ ]]></handler>
+
+ <handler event="compositionstart"><![CDATA[
+ // Don't close the find toolbar while IME is composing.
+ let findbar = this.findbar;
+ findbar._isIMEComposing = true;
+ if (findbar._quickFindTimeout) {
+ clearTimeout(findbar._quickFindTimeout);
+ findbar._quickFindTimeout = null;
+ }
+ ]]></handler>
+
+ <handler event="compositionend"><![CDATA[
+ let findbar = this.findbar;
+ findbar._isIMEComposing = false;
+ if (findbar._findMode != findbar.FIND_NORMAL)
+ findbar._setFindCloseTimeout();
+ ]]></handler>
+
+ <handler event="dragover"><![CDATA[
+ if (event.dataTransfer.types.includes("text/plain"))
+ event.preventDefault();
+ ]]></handler>
+
+ <handler event="drop"><![CDATA[
+ let value = event.dataTransfer.getData("text/plain");
+ this.value = value;
+ this.findbar._find(value);
+ event.stopPropagation();
+ event.preventDefault();
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="findbar"
+ extends="chrome://global/content/bindings/toolbar.xml#toolbar">
+ <resources>
+ <stylesheet src="chrome://global/skin/findBar.css"/>
+ </resources>
+
+ <content hidden="true">
+ <xul:hbox anonid="findbar-container" class="findbar-container" flex="1" align="center">
+ <xul:hbox anonid="findbar-textbox-wrapper" align="stretch">
+ <xul:textbox anonid="findbar-textbox"
+ class="findbar-textbox findbar-find-fast"
+ xbl:inherits="flash"/>
+ <xul:toolbarbutton anonid="find-previous"
+ class="findbar-find-previous tabbable"
+ tooltiptext="&previous.tooltip;"
+ oncommand="onFindAgainCommand(true);"
+ disabled="true"
+ xbl:inherits="accesskey=findpreviousaccesskey"/>
+ <xul:toolbarbutton anonid="find-next"
+ class="findbar-find-next tabbable"
+ tooltiptext="&next.tooltip;"
+ oncommand="onFindAgainCommand(false);"
+ disabled="true"
+ xbl:inherits="accesskey=findnextaccesskey"/>
+ </xul:hbox>
+ <xul:toolbarbutton anonid="highlight"
+ class="findbar-highlight findbar-button tabbable"
+ label="&highlightAll.label;"
+ accesskey="&highlightAll.accesskey;"
+ tooltiptext="&highlightAll.tooltiptext;"
+ oncommand="toggleHighlight(this.checked);"
+ type="checkbox"
+ xbl:inherits="accesskey=highlightaccesskey"/>
+ <xul:toolbarbutton anonid="find-case-sensitive"
+ class="findbar-case-sensitive findbar-button tabbable"
+ label="&caseSensitive.label;"
+ accesskey="&caseSensitive.accesskey;"
+ tooltiptext="&caseSensitive.tooltiptext;"
+ oncommand="_setCaseSensitivity(this.checked ? 1 : 0);"
+ type="checkbox"
+ xbl:inherits="accesskey=matchcaseaccesskey"/>
+ <xul:toolbarbutton anonid="find-entire-word"
+ class="findbar-entire-word findbar-button tabbable"
+ label="&entireWord.label;"
+ accesskey="&entireWord.accesskey;"
+ tooltiptext="&entireWord.tooltiptext;"
+ oncommand="toggleEntireWord(this.checked);"
+ type="checkbox"
+ xbl:inherits="accesskey=entirewordaccesskey"/>
+ <xul:label anonid="match-case-status" class="findbar-find-fast"/>
+ <xul:label anonid="entire-word-status" class="findbar-find-fast"/>
+ <xul:label anonid="found-matches" class="findbar-find-fast found-matches" hidden="true"/>
+ <xul:image anonid="find-status-icon" class="findbar-find-fast find-status-icon"/>
+ <xul:description anonid="find-status"
+ control="findbar-textbox"
+ class="findbar-find-fast findbar-find-status">
+ <!-- Do not use value, first child is used because it provides a11y with text change events -->
+ </xul:description>
+ </xul:hbox>
+ <xul:toolbarbutton anonid="find-closebutton"
+ class="findbar-closebutton close-icon"
+ tooltiptext="&findCloseButton.tooltip;"
+ oncommand="close();"/>
+ </content>
+
+ <implementation implements="nsIMessageListener, nsIEditActionListener">
+ <!-- Please keep in sync with toolkit/content/browser-content.js -->
+ <field name="FIND_NORMAL">0</field>
+ <field name="FIND_TYPEAHEAD">1</field>
+ <field name="FIND_LINKS">2</field>
+
+ <field name="__findMode">0</field>
+ <property name="_findMode" onget="return this.__findMode;"
+ onset="this.__findMode = val; this._updateBrowserWithState(); return val;"/>
+
+ <field name="_flashFindBar">0</field>
+ <field name="_initialFlashFindBarCount">6</field>
+
+ <!--
+ - For tests that need to know when the find bar is finished
+ - initializing, we store a promise to notify on.
+ -->
+ <field name="_startFindDeferred">null</field>
+
+ <property name="prefillWithSelection"
+ onget="return this.getAttribute('prefillwithselection') != 'false'"
+ onset="this.setAttribute('prefillwithselection', val); return val;"/>
+
+ <method name="getElement">
+ <parameter name="aAnonymousID"/>
+ <body><![CDATA[
+ return document.getAnonymousElementByAttribute(this,
+ "anonid",
+ aAnonymousID)
+ ]]></body>
+ </method>
+
+ <property name="findMode"
+ readonly="true"
+ onget="return this._findMode;"/>
+
+ <property name="hasTransactions" readonly="true">
+ <getter><![CDATA[
+ if (this._findField.value)
+ return true;
+
+ // Watch out for lazy editor init
+ if (this._findField.editor) {
+ let tm = this._findField.editor.transactionManager;
+ return !!(tm.numberOfUndoItems || tm.numberOfRedoItems);
+ }
+ return false;
+ ]]></getter>
+ </property>
+
+ <field name="_browser">null</field>
+ <property name="browser">
+ <getter><![CDATA[
+ if (!this._browser) {
+ this._browser =
+ document.getElementById(this.getAttribute("browserid"));
+ }
+ return this._browser;
+ ]]></getter>
+ <setter><![CDATA[
+ if (this._browser) {
+ if (this._browser.messageManager) {
+ this._browser.messageManager.removeMessageListener("Findbar:Keypress", this);
+ this._browser.messageManager.removeMessageListener("Findbar:Mouseup", this);
+ }
+ let finder = this._browser.finder;
+ if (finder)
+ finder.removeResultListener(this);
+ }
+
+ this._browser = val;
+ if (this._browser) {
+ // Need to do this to ensure the correct initial state.
+ this._updateBrowserWithState();
+ this._browser.messageManager.addMessageListener("Findbar:Keypress", this);
+ this._browser.messageManager.addMessageListener("Findbar:Mouseup", this);
+ this._browser.finder.addResultListener(this);
+
+ this._findField.value = this._browser._lastSearchString;
+ }
+ return val;
+ ]]></setter>
+ </property>
+
+ <field name="__prefsvc">null</field>
+ <property name="_prefsvc">
+ <getter><![CDATA[
+ if (!this.__prefsvc) {
+ this.__prefsvc = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch);
+ }
+ return this.__prefsvc;
+ ]]></getter>
+ </property>
+
+ <field name="_observer"><![CDATA[({
+ _self: this,
+
+ QueryInterface: function(aIID) {
+ if (aIID.equals(Components.interfaces.nsIObserver) ||
+ aIID.equals(Components.interfaces.nsISupportsWeakReference) ||
+ aIID.equals(Components.interfaces.nsISupports))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ },
+
+ observe: function(aSubject, aTopic, aPrefName) {
+ if (aTopic != "nsPref:changed")
+ return;
+
+ let prefsvc = this._self._prefsvc;
+
+ switch (aPrefName) {
+ case "accessibility.typeaheadfind":
+ this._self._findAsYouType = prefsvc.getBoolPref(aPrefName);
+ break;
+ case "accessibility.typeaheadfind.linksonly":
+ this._self._typeAheadLinksOnly = prefsvc.getBoolPref(aPrefName);
+ break;
+ case "accessibility.typeaheadfind.casesensitive":
+ this._self._setCaseSensitivity(prefsvc.getIntPref(aPrefName));
+ break;
+ case "findbar.entireword":
+ this._self._entireWord = prefsvc.getBoolPref(aPrefName);
+ this._self.toggleEntireWord(this._self._entireWord, true);
+ break;
+ case "findbar.highlightAll":
+ this._self.toggleHighlight(prefsvc.getBoolPref(aPrefName), true);
+ break;
+ case "findbar.modalHighlight":
+ this._self._useModalHighlight = prefsvc.getBoolPref(aPrefName);
+ if (this._self.browser.finder)
+ this._self.browser.finder.onModalHighlightChange(this._self._useModalHighlight);
+ break;
+ }
+ }
+ })]]></field>
+
+ <field name="_destroyed">false</field>
+
+ <constructor><![CDATA[
+ // These elements are accessed frequently and are therefore cached
+ this._findField = this.getElement("findbar-textbox");
+ this._foundMatches = this.getElement("found-matches");
+ this._findStatusIcon = this.getElement("find-status-icon");
+ this._findStatusDesc = this.getElement("find-status");
+
+ this._foundURL = null;
+
+ let prefsvc = this._prefsvc;
+
+ this._quickFindTimeoutLength =
+ prefsvc.getIntPref("accessibility.typeaheadfind.timeout");
+ this._flashFindBar =
+ prefsvc.getIntPref("accessibility.typeaheadfind.flashBar");
+ this._useModalHighlight = prefsvc.getBoolPref("findbar.modalHighlight");
+
+ prefsvc.addObserver("accessibility.typeaheadfind",
+ this._observer, false);
+ prefsvc.addObserver("accessibility.typeaheadfind.linksonly",
+ this._observer, false);
+ prefsvc.addObserver("accessibility.typeaheadfind.casesensitive",
+ this._observer, false);
+ prefsvc.addObserver("findbar.entireword", this._observer, false);
+ prefsvc.addObserver("findbar.highlightAll", this._observer, false);
+ prefsvc.addObserver("findbar.modalHighlight", this._observer, false);
+
+ this._findAsYouType =
+ prefsvc.getBoolPref("accessibility.typeaheadfind");
+ this._typeAheadLinksOnly =
+ prefsvc.getBoolPref("accessibility.typeaheadfind.linksonly");
+ this._typeAheadCaseSensitive =
+ prefsvc.getIntPref("accessibility.typeaheadfind.casesensitive");
+ this._entireWord = prefsvc.getBoolPref("findbar.entireword");
+ this._highlightAll = prefsvc.getBoolPref("findbar.highlightAll");
+
+ // Convenience
+ this.nsITypeAheadFind = Components.interfaces.nsITypeAheadFind;
+ this.nsISelectionController = Components.interfaces.nsISelectionController;
+ this._findSelection = this.nsISelectionController.SELECTION_FIND;
+
+ this._findResetTimeout = -1;
+
+ // Make sure the FAYT keypress listener is attached by initializing the
+ // browser property
+ if (this.getAttribute("browserid"))
+ setTimeout(function(aSelf) { aSelf.browser = aSelf.browser; }, 0, this);
+ ]]></constructor>
+
+ <destructor><![CDATA[
+ this.destroy();
+ ]]></destructor>
+
+ <!-- This is necessary because the destructor isn't called when
+ we are removed from a document that is not destroyed. This
+ needs to be explicitly called in this case -->
+ <method name="destroy">
+ <body><![CDATA[
+ if (this._destroyed)
+ return;
+ this._destroyed = true;
+
+ if (this.browser.finder)
+ this.browser.finder.destroy();
+
+ this.browser = null;
+
+ let prefsvc = this._prefsvc;
+ prefsvc.removeObserver("accessibility.typeaheadfind",
+ this._observer);
+ prefsvc.removeObserver("accessibility.typeaheadfind.linksonly",
+ this._observer);
+ prefsvc.removeObserver("accessibility.typeaheadfind.casesensitive",
+ this._observer);
+ prefsvc.removeObserver("findbar.entireword", this._observer);
+ prefsvc.removeObserver("findbar.highlightAll", this._observer);
+ prefsvc.removeObserver("findbar.modalHighlight", this._observer);
+
+ // Clear all timers that might still be running.
+ this._cancelTimers();
+ ]]></body>
+ </method>
+
+ <method name="_cancelTimers">
+ <body><![CDATA[
+ if (this._flashFindBarTimeout) {
+ clearInterval(this._flashFindBarTimeout);
+ this._flashFindBarTimeout = null;
+ }
+ if (this._quickFindTimeout) {
+ clearTimeout(this._quickFindTimeout);
+ this._quickFindTimeout = null;
+ }
+ if (this._findResetTimeout) {
+ clearTimeout(this._findResetTimeout);
+ this._findResetTimeout = null;
+ }
+ ]]></body>
+ </method>
+
+ <method name="_setFindCloseTimeout">
+ <body><![CDATA[
+ if (this._quickFindTimeout)
+ clearTimeout(this._quickFindTimeout);
+
+ // Don't close the find toolbar while IME is composing OR when the
+ // findbar is already hidden.
+ if (this._isIMEComposing || this.hidden) {
+ this._quickFindTimeout = null;
+ return;
+ }
+
+ this._quickFindTimeout = setTimeout(() => {
+ if (this._findMode != this.FIND_NORMAL)
+ this.close();
+ this._quickFindTimeout = null;
+ }, this._quickFindTimeoutLength);
+ ]]></body>
+ </method>
+
+ <field name="_pluralForm">null</field>
+ <property name="pluralForm">
+ <getter><![CDATA[
+ if (!this._pluralForm) {
+ this._pluralForm = Components.utils.import(
+ "resource://gre/modules/PluralForm.jsm", {}).PluralForm;
+ }
+ return this._pluralForm;
+ ]]></getter>
+ </property>
+
+ <!--
+ - Updates the search match count after each find operation on a new string.
+ - @param aRes
+ - the result of the find operation
+ -->
+ <method name="_updateMatchesCount">
+ <body><![CDATA[
+ if (!this._dispatchFindEvent("matchescount"))
+ return;
+
+ this.browser.finder.requestMatchesCount(this._findField.value,
+ this._findMode == this.FIND_LINKS);
+ ]]></body>
+ </method>
+
+ <!--
+ - Turns highlight on or off.
+ - @param aHighlight (boolean)
+ - Whether to turn the highlight on or off
+ - @param aFromPrefObserver (boolean)
+ - Whether the callee is the pref observer, which means we should
+ - not set the same pref again.
+ -->
+ <method name="toggleHighlight">
+ <parameter name="aHighlight"/>
+ <parameter name="aFromPrefObserver"/>
+ <body><![CDATA[
+ if (aHighlight === this._highlightAll) {
+ return;
+ }
+
+ this.browser.finder.onHighlightAllChange(aHighlight);
+
+ this._setHighlightAll(aHighlight, aFromPrefObserver);
+
+ if (!this._dispatchFindEvent("highlightallchange")) {
+ return;
+ }
+
+ let word = this._findField.value;
+ // Bug 429723. Don't attempt to highlight ""
+ if (aHighlight && !word)
+ return;
+
+ this.browser.finder.highlight(aHighlight, word,
+ this._findMode == this.FIND_LINKS);
+
+ // Update the matches count
+ this._updateMatchesCount(this.nsITypeAheadFind.FIND_FOUND);
+ ]]></body>
+ </method>
+
+ <!--
+ - Updates the highlight-all mode of the findbar and its UI.
+ - @param aHighlight (boolean)
+ - Whether to turn the highlight on or off.
+ - @param aFromPrefObserver (boolean)
+ - Whether the callee is the pref observer, which means we should
+ - not set the same pref again.
+ -->
+ <method name="_setHighlightAll">
+ <parameter name="aHighlight"/>
+ <parameter name="aFromPrefObserver"/>
+ <body><![CDATA[
+ if (typeof aHighlight != "boolean") {
+ aHighlight = this._highlightAll;
+ }
+ if (aHighlight !== this._highlightAll && !aFromPrefObserver) {
+ this._prefsvc.setBoolPref("findbar.highlightAll", aHighlight);
+ }
+ this._highlightAll = aHighlight;
+ let checkbox = this.getElement("highlight");
+ checkbox.checked = this._highlightAll;
+ ]]></body>
+ </method>
+
+ <method name="_maybeHighlightAll">
+ <body><![CDATA[
+ let word = this._findField.value;
+ // Bug 429723. Don't attempt to highlight ""
+ if (!this._highlightAll || !word)
+ return;
+
+ this.browser.finder.highlight(true, word,
+ this._findMode == this.FIND_LINKS);
+ ]]></body>
+ </method>
+
+ <!--
+ - Updates the case-sensitivity mode of the findbar and its UI.
+ - @param [optional] aString
+ - The string for which case sensitivity might be turned on.
+ - This only used when case-sensitivity is in auto mode,
+ - @see _shouldBeCaseSensitive. The default value for this
+ - parameter is the find-field value.
+ -->
+ <method name="_updateCaseSensitivity">
+ <parameter name="aString"/>
+ <body><![CDATA[
+ let val = aString || this._findField.value;
+
+ let caseSensitive = this._shouldBeCaseSensitive(val);
+ let checkbox = this.getElement("find-case-sensitive");
+ let statusLabel = this.getElement("match-case-status");
+ checkbox.checked = caseSensitive;
+
+ statusLabel.value = caseSensitive ? this._caseSensitiveStr : "";
+
+ // Show the checkbox on the full Find bar in non-auto mode.
+ // Show the label in all other cases.
+ let hideCheckbox = this._findMode != this.FIND_NORMAL ||
+ (this._typeAheadCaseSensitive != 0 &&
+ this._typeAheadCaseSensitive != 1);
+ checkbox.hidden = hideCheckbox;
+ statusLabel.hidden = !hideCheckbox;
+
+ this.browser.finder.caseSensitive = caseSensitive;
+ ]]></body>
+ </method>
+
+ <!--
+ - Sets the findbar case-sensitivity mode
+ - @param aCaseSensitivity (int)
+ - 0 - case insensitive
+ - 1 - case sensitive
+ - 2 - auto = case sensitive iff match string contains upper case letters
+ - @see _shouldBeCaseSensitive
+ -->
+ <method name="_setCaseSensitivity">
+ <parameter name="aCaseSensitivity"/>
+ <body><![CDATA[
+ this._typeAheadCaseSensitive = aCaseSensitivity;
+ this._updateCaseSensitivity();
+ this._findFailedString = null;
+ this._find();
+
+ this._dispatchFindEvent("casesensitivitychange");
+ ]]></body>
+ </method>
+
+ <!--
+ - Updates the entire-word mode of the findbar and its UI.
+ -->
+ <method name="_setEntireWord">
+ <body><![CDATA[
+ let entireWord = this._entireWord;
+ let checkbox = this.getElement("find-entire-word");
+ let statusLabel = this.getElement("entire-word-status");
+ checkbox.checked = entireWord;
+
+ statusLabel.value = entireWord ? this._entireWordStr : "";
+
+ // Show the checkbox on the full Find bar in non-auto mode.
+ // Show the label in all other cases.
+ let hideCheckbox = this._findMode != this.FIND_NORMAL;
+ checkbox.hidden = hideCheckbox;
+ statusLabel.hidden = !hideCheckbox;
+
+ this.browser.finder.entireWord = entireWord;
+ ]]></body>
+ </method>
+
+ <!--
+ - Sets the findbar entire-word mode
+ - @param aEntireWord (boolean)
+ - Whether or not entire-word mode should be turned on.
+ -->
+ <method name="toggleEntireWord">
+ <parameter name="aEntireWord"/>
+ <parameter name="aFromPrefObserver"/>
+ <body><![CDATA[
+ if (!aFromPrefObserver) {
+ // Just set the pref; our observer will change the find bar behavior.
+ this._prefsvc.setBoolPref("findbar.entireword", aEntireWord);
+ return;
+ }
+
+ this._findFailedString = null;
+ this._find();
+ ]]></body>
+ </method>
+
+ <field name="_strBundle">null</field>
+ <property name="strBundle">
+ <getter><![CDATA[
+ if (!this._strBundle) {
+ this._strBundle =
+ Components.classes["@mozilla.org/intl/stringbundle;1"]
+ .getService(Components.interfaces.nsIStringBundleService)
+ .createBundle("chrome://global/locale/findbar.properties");
+ }
+ return this._strBundle;
+ ]]></getter>
+ </property>
+
+ <!--
+ - Opens and displays the find bar.
+ -
+ - @param aMode
+ - the find mode to be used, which is either FIND_NORMAL,
+ - FIND_TYPEAHEAD or FIND_LINKS. If not passed, the last
+ - find mode if any or FIND_NORMAL.
+ - @returns true if the find bar wasn't previously open, false otherwise.
+ -->
+ <method name="open">
+ <parameter name="aMode"/>
+ <body><![CDATA[
+ if (aMode != undefined)
+ this._findMode = aMode;
+
+ if (!this._notFoundStr) {
+ var stringsBundle = this.strBundle;
+ this._notFoundStr = stringsBundle.GetStringFromName("NotFound");
+ this._wrappedToTopStr =
+ stringsBundle.GetStringFromName("WrappedToTop");
+ this._wrappedToBottomStr =
+ stringsBundle.GetStringFromName("WrappedToBottom");
+ this._normalFindStr =
+ stringsBundle.GetStringFromName("NormalFind");
+ this._fastFindStr =
+ stringsBundle.GetStringFromName("FastFind");
+ this._fastFindLinksStr =
+ stringsBundle.GetStringFromName("FastFindLinks");
+ this._caseSensitiveStr =
+ stringsBundle.GetStringFromName("CaseSensitive");
+ this._entireWordStr =
+ stringsBundle.GetStringFromName("EntireWord");
+ }
+
+ this._findFailedString = null;
+
+ this._updateFindUI();
+ if (this.hidden) {
+ this.removeAttribute("noanim");
+ this.hidden = false;
+
+ this._updateStatusUI(this.nsITypeAheadFind.FIND_FOUND);
+
+ let event = document.createEvent("Events");
+ event.initEvent("findbaropen", true, false);
+ this.dispatchEvent(event);
+
+ this.browser.finder.onFindbarOpen();
+
+ return true;
+ }
+ return false;
+ ]]></body>
+ </method>
+
+ <!--
+ - Closes the findbar.
+ -->
+ <method name="close">
+ <parameter name="aNoAnim"/>
+ <body><![CDATA[
+ if (this.hidden)
+ return;
+
+ if (aNoAnim)
+ this.setAttribute("noanim", true);
+ this.hidden = true;
+
+ // 'focusContent()' iterates over all listeners in the chrome
+ // process, so we need to call it from here.
+ this.browser.finder.focusContent();
+ this.browser.finder.onFindbarClose();
+
+ this._cancelTimers();
+
+ this._findFailedString = null;
+ ]]></body>
+ </method>
+
+ <method name="clear">
+ <body><![CDATA[
+ this.browser.finder.removeSelection();
+ this._findField.reset();
+ this.toggleHighlight(false);
+ this._updateStatusUI();
+ this._enableFindButtons(false);
+ ]]></body>
+ </method>
+
+ <method name="_dispatchKeypressEvent">
+ <parameter name="aTarget"/>
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ if (!aTarget)
+ return;
+
+ let event = document.createEvent("KeyboardEvent");
+ event.initKeyEvent(aEvent.type, aEvent.bubbles, aEvent.cancelable,
+ aEvent.view, aEvent.ctrlKey, aEvent.altKey,
+ aEvent.shiftKey, aEvent.metaKey, aEvent.keyCode,
+ aEvent.charCode);
+ aTarget.dispatchEvent(event);
+ ]]></body>
+ </method>
+
+ <field name="_xulBrowserWindow">null</field>
+ <method name="_updateStatusUIBar">
+ <parameter name="aFoundURL"/>
+ <body><![CDATA[
+ if (!this._xulBrowserWindow) {
+ try {
+ this._xulBrowserWindow =
+ window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIWebNavigation)
+ .QueryInterface(Components.interfaces.nsIDocShellTreeItem)
+ .treeOwner
+ .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIXULWindow)
+ .XULBrowserWindow;
+ }
+ catch (ex) { }
+ if (!this._xulBrowserWindow)
+ return false;
+ }
+
+ // Call this has the same effect like hovering over link,
+ // the browser shows the URL as a tooltip.
+ this._xulBrowserWindow.setOverLink(aFoundURL || "", null);
+ return true;
+ ]]></body>
+ </method>
+
+ <method name="_finishFAYT">
+ <parameter name="aKeypressEvent"/>
+ <body><![CDATA[
+ this.browser.finder.focusContent();
+
+ if (aKeypressEvent)
+ aKeypressEvent.preventDefault();
+
+ this.browser.finder.keyPress(aKeypressEvent);
+
+ this.close();
+ return true;
+ ]]></body>
+ </method>
+
+ <method name="_shouldBeCaseSensitive">
+ <parameter name="aString"/>
+ <body><![CDATA[
+ if (this._typeAheadCaseSensitive == 0)
+ return false;
+ if (this._typeAheadCaseSensitive == 1)
+ return true;
+
+ return aString != aString.toLowerCase();
+ ]]></body>
+ </method>
+
+ <!-- We get a fake event object through an IPC message which contains the
+ data we need to make a decision. We then return |true| if and only if
+ the page gets to deal with the event itself. Everywhere we return
+ false, the message sender will take care of calling event.preventDefault
+ on the real event. -->
+ <method name="_onBrowserKeypress">
+ <parameter name="aFakeEvent"/>
+ <parameter name="aShouldFastFind"/>
+ <body><![CDATA[
+ const FAYT_LINKS_KEY = "'";
+ const FAYT_TEXT_KEY = "/";
+
+ // Fast keypresses can stack up when the content process is slow or
+ // hangs when in e10s mode. We make sure the findbar isn't 'opened'
+ // several times in a row, because then the find query is selected
+ // each time, losing characters typed initially.
+ let inputField = this._findField.inputField;
+ if (!this.hidden && document.activeElement == inputField) {
+ this._dispatchKeypressEvent(inputField, aFakeEvent);
+ return false;
+ }
+
+ if (this._findMode != this.FIND_NORMAL && this._quickFindTimeout) {
+ if (!aFakeEvent.charCode)
+ return true;
+
+ this._findField.select();
+ this._findField.focus();
+ this._dispatchKeypressEvent(this._findField.inputField, aFakeEvent);
+ return false;
+ }
+
+ if (!aShouldFastFind)
+ return true;
+
+ let key = aFakeEvent.charCode ? String.fromCharCode(aFakeEvent.charCode) : null;
+ let manualstartFAYT = (key == FAYT_LINKS_KEY || key == FAYT_TEXT_KEY);
+ let autostartFAYT = !manualstartFAYT && this._findAsYouType &&
+ key && key != " ";
+ if (manualstartFAYT || autostartFAYT) {
+ let mode = (key == FAYT_LINKS_KEY ||
+ (autostartFAYT && this._typeAheadLinksOnly)) ?
+ this.FIND_LINKS : this.FIND_TYPEAHEAD;
+
+ // Clear bar first, so that when openFindBar() calls setCaseSensitivity()
+ // it doesn't get confused by a lingering value
+ this._findField.value = "";
+
+ this.open(mode);
+ this._setFindCloseTimeout();
+ this._findField.select();
+ this._findField.focus();
+
+ if (autostartFAYT)
+ this._dispatchKeypressEvent(this._findField.inputField, aFakeEvent);
+ else
+ this._updateStatusUI(this.nsITypeAheadFind.FIND_FOUND);
+
+ return false;
+ }
+ return undefined;
+ ]]></body>
+ </method>
+
+ <!-- See nsIMessageListener -->
+ <method name="receiveMessage">
+ <parameter name="aMessage"/>
+ <body><![CDATA[
+ if (aMessage.target != this._browser) {
+ return undefined;
+ }
+ switch (aMessage.name) {
+ case "Findbar:Mouseup":
+ if (!this.hidden && this._findMode != this.FIND_NORMAL)
+ this.close();
+ break;
+
+ case "Findbar:Keypress":
+ return this._onBrowserKeypress(aMessage.data.fakeEvent,
+ aMessage.data.shouldFastFind);
+ }
+ return undefined;
+ ]]></body>
+ </method>
+
+ <method name="_updateBrowserWithState">
+ <body><![CDATA[
+ if (this._browser && this._browser.messageManager) {
+ this._browser.messageManager.sendAsyncMessage("Findbar:UpdateState", {
+ findMode: this._findMode
+ });
+ }
+ ]]></body>
+ </method>
+
+ <method name="_enableFindButtons">
+ <parameter name="aEnable"/>
+ <body><![CDATA[
+ this.getElement("find-next").disabled =
+ this.getElement("find-previous").disabled = !aEnable;
+ ]]></body>
+ </method>
+
+ <!--
+ - Determines whether minimalist or general-purpose search UI is to be
+ - displayed when the find bar is activated.
+ -->
+ <method name="_updateFindUI">
+ <body><![CDATA[
+ let showMinimalUI = this._findMode != this.FIND_NORMAL;
+
+ let nodes = this.getElement("findbar-container").childNodes;
+ let wrapper = this.getElement("findbar-textbox-wrapper");
+ let foundMatches = this._foundMatches;
+ for (let node of nodes) {
+ if (node == wrapper || node == foundMatches)
+ continue;
+ node.hidden = showMinimalUI;
+ }
+ this.getElement("find-next").hidden =
+ this.getElement("find-previous").hidden = showMinimalUI;
+ foundMatches.hidden = showMinimalUI || !foundMatches.value;
+ this._updateCaseSensitivity();
+ this._setEntireWord();
+ this._setHighlightAll();
+
+ if (showMinimalUI)
+ this._findField.classList.add("minimal");
+ else
+ this._findField.classList.remove("minimal");
+
+ if (this._findMode == this.FIND_TYPEAHEAD)
+ this._findField.placeholder = this._fastFindStr;
+ else if (this._findMode == this.FIND_LINKS)
+ this._findField.placeholder = this._fastFindLinksStr;
+ else
+ this._findField.placeholder = this._normalFindStr;
+ ]]></body>
+ </method>
+
+ <method name="_find">
+ <parameter name="aValue"/>
+ <body><![CDATA[
+ if (!this._dispatchFindEvent(""))
+ return;
+
+ let val = aValue || this._findField.value;
+
+ // We have to carry around an explicit version of this,
+ // because finder.searchString doesn't update on failed
+ // searches.
+ this.browser._lastSearchString = val;
+
+ // Only search on input if we don't have a last-failed string,
+ // or if the current search string doesn't start with it.
+ // In entire-word mode we always attemp a find; since sequential matching
+ // is not guaranteed, the first character typed may not be a word (no
+ // match), but the with the second character it may well be a word,
+ // thus a match.
+ if (!this._findFailedString ||
+ !val.startsWith(this._findFailedString) ||
+ this._entireWord) {
+ // Getting here means the user commanded a find op. Make sure any
+ // initial prefilling is ignored if it hasn't happened yet.
+ if (this._startFindDeferred) {
+ this._startFindDeferred.resolve();
+ this._startFindDeferred = null;
+ }
+
+ this._enableFindButtons(val);
+ this._updateCaseSensitivity(val);
+ this._setEntireWord();
+
+ this.browser.finder.fastFind(val, this._findMode == this.FIND_LINKS,
+ this._findMode != this.FIND_NORMAL);
+ }
+
+ if (this._findMode != this.FIND_NORMAL)
+ this._setFindCloseTimeout();
+
+ if (this._findResetTimeout != -1)
+ clearTimeout(this._findResetTimeout);
+
+ // allow a search to happen on input again after a second has
+ // expired since the previous input, to allow for dynamic
+ // content and/or page loading
+ this._findResetTimeout = setTimeout(() => {
+ this._findFailedString = null;
+ this._findResetTimeout = -1;
+ }, 1000);
+ ]]></body>
+ </method>
+
+ <method name="_flash">
+ <body><![CDATA[
+ if (this._flashFindBarCount === undefined)
+ this._flashFindBarCount = this._initialFlashFindBarCount;
+
+ if (this._flashFindBarCount-- == 0) {
+ clearInterval(this._flashFindBarTimeout);
+ this.removeAttribute("flash");
+ this._flashFindBarCount = 6;
+ return;
+ }
+
+ this.setAttribute("flash",
+ (this._flashFindBarCount % 2 == 0) ?
+ "false" : "true");
+ ]]></body>
+ </method>
+
+ <method name="_findAgain">
+ <parameter name="aFindPrevious"/>
+ <body><![CDATA[
+ this.browser.finder.findAgain(aFindPrevious,
+ this._findMode == this.FIND_LINKS,
+ this._findMode != this.FIND_NORMAL);
+ ]]></body>
+ </method>
+
+ <method name="_updateStatusUI">
+ <parameter name="res"/>
+ <parameter name="aFindPrevious"/>
+ <body><![CDATA[
+ switch (res) {
+ case this.nsITypeAheadFind.FIND_WRAPPED:
+ this._findStatusIcon.setAttribute("status", "wrapped");
+ this._findStatusDesc.textContent =
+ aFindPrevious ? this._wrappedToBottomStr : this._wrappedToTopStr;
+ this._findField.removeAttribute("status");
+ break;
+ case this.nsITypeAheadFind.FIND_NOTFOUND:
+ this._findStatusIcon.setAttribute("status", "notfound");
+ this._findStatusDesc.textContent = this._notFoundStr;
+ this._findField.setAttribute("status", "notfound");
+ break;
+ case this.nsITypeAheadFind.FIND_PENDING:
+ this._findStatusIcon.setAttribute("status", "pending");
+ this._findStatusDesc.textContent = "";
+ this._findField.removeAttribute("status");
+ break;
+ case this.nsITypeAheadFind.FIND_FOUND:
+ default:
+ this._findStatusIcon.removeAttribute("status");
+ this._findStatusDesc.textContent = "";
+ this._findField.removeAttribute("status");
+ break;
+ }
+ ]]></body>
+ </method>
+
+ <method name="updateControlState">
+ <parameter name="aResult"/>
+ <parameter name="aFindPrevious"/>
+ <body><![CDATA[
+ this._updateStatusUI(aResult, aFindPrevious);
+ this._enableFindButtons(aResult !== this.nsITypeAheadFind.FIND_NOTFOUND);
+ ]]></body>
+ </method>
+
+ <method name="_dispatchFindEvent">
+ <parameter name="aType"/>
+ <parameter name="aFindPrevious"/>
+ <body><![CDATA[
+ let event = document.createEvent("CustomEvent");
+ event.initCustomEvent("find" + aType, true, true, {
+ query: this._findField.value,
+ caseSensitive: !!this._typeAheadCaseSensitive,
+ entireWord: this._entireWord,
+ highlightAll: this._highlightAll,
+ findPrevious: aFindPrevious
+ });
+ return this.dispatchEvent(event);
+ ]]></body>
+ </method>
+
+
+ <!--
+ - Opens the findbar, focuses the findfield and selects its contents.
+ - Also flashes the findbar the first time it's used.
+ - @param aMode
+ - the find mode to be used, which is either FIND_NORMAL,
+ - FIND_TYPEAHEAD or FIND_LINKS. If not passed, the last
+ - find mode if any or FIND_NORMAL.
+ -->
+ <method name="startFind">
+ <parameter name="aMode"/>
+ <body><![CDATA[
+ let prefsvc = this._prefsvc;
+ let userWantsPrefill = true;
+ this.open(aMode);
+
+ if (this._flashFindBar) {
+ this._flashFindBarTimeout = setInterval(() => this._flash(), 500);
+ prefsvc.setIntPref("accessibility.typeaheadfind.flashBar",
+ --this._flashFindBar);
+ }
+
+ let {PromiseUtils} =
+ Components.utils.import("resource://gre/modules/PromiseUtils.jsm", {});
+ this._startFindDeferred = PromiseUtils.defer();
+ let startFindPromise = this._startFindDeferred.promise;
+
+ if (this.prefillWithSelection)
+ userWantsPrefill =
+ prefsvc.getBoolPref("accessibility.typeaheadfind.prefillwithselection");
+
+ if (this.prefillWithSelection && userWantsPrefill) {
+ // NB: We have to focus this._findField here so tests that send
+ // key events can open and close the find bar synchronously.
+ this._findField.focus();
+
+ // (e10s) since we focus lets also select it, otherwise that would
+ // only happen in this.onCurrentSelection and, because it is async,
+ // there's a chance keypresses could come inbetween, leading to
+ // jumbled up queries.
+ this._findField.select();
+
+ this.browser.finder.getInitialSelection();
+ return startFindPromise;
+ }
+
+ // If userWantsPrefill is false but prefillWithSelection is true,
+ // then we might need to check the selection clipboard. Call
+ // onCurrentSelection to do so.
+ // Note: this.onCurrentSelection clears this._startFindDeferred.
+ this.onCurrentSelection("", true);
+ return startFindPromise;
+ ]]></body>
+ </method>
+
+ <!--
+ - Convenient alias to startFind(gFindBar.FIND_NORMAL);
+ -
+ - You should generally map the window's find command to this method.
+ - e.g. <command name="cmd_find" oncommand="gFindBar.onFindCommand();"/>
+ -->
+ <method name="onFindCommand">
+ <body><![CDATA[
+ return this.startFind(this.FIND_NORMAL);
+ ]]></body>
+ </method>
+
+ <!--
+ - Stub for find-next and find-previous commands
+ - @param aFindPrevious
+ - true for find-previous, false otherwise.
+ -->
+ <method name="onFindAgainCommand">
+ <parameter name="aFindPrevious"/>
+ <body><![CDATA[
+ let findString = this._browser.finder.searchString || this._findField.value;
+ if (!findString)
+ return this.startFind();
+
+ // We dispatch the findAgain event here instead of in _findAgain since
+ // if there is a find event handler that prevents the default then
+ // finder.searchString will never get updated which in turn means
+ // there would never be findAgain events because of the logic below.
+ if (!this._dispatchFindEvent("again", aFindPrevious))
+ return undefined;
+
+ // user explicitly requested another search, so do it even if we think it'll fail
+ this._findFailedString = null;
+
+ // Ensure the stored SearchString is in sync with what we want to find
+ if (this._findField.value != this._browser.finder.searchString) {
+ this._find(this._findField.value);
+ } else {
+ this._findAgain(aFindPrevious);
+ if (this._useModalHighlight) {
+ this.open();
+ this._findField.select();
+ this._findField.focus();
+ }
+ }
+
+ return undefined;
+ ]]></body>
+ </method>
+
+#ifdef XP_MACOSX
+ <!--
+ - Fetches the currently selected text and sets that as the text to search
+ - next. This is a MacOS specific feature.
+ -->
+ <method name="onFindSelectionCommand">
+ <body><![CDATA[
+ let searchString = this.browser.finder.setSearchStringToSelection();
+ if (searchString)
+ this._findField.value = searchString;
+ ]]></body>
+ </method>
+
+ <method name="_onFindFieldFocus">
+ <body><![CDATA[
+ let prefsvc = this._prefsvc;
+ const kPref = "accessibility.typeaheadfind.prefillwithselection";
+ if (this.prefillWithSelection && prefsvc.getBoolPref(kPref))
+ return;
+
+ let clipboardSearchString = this._browser.finder.clipboardSearchString;
+ if (clipboardSearchString && this._findField.value != clipboardSearchString &&
+ !this._findField._willfullyDeleted) {
+ this._findField.value = clipboardSearchString;
+ this._findField._hadValue = true;
+ // Changing the search string makes the previous status invalid, so
+ // we better clear it here.
+ this._updateStatusUI();
+ }
+ ]]></body>
+ </method>
+#endif
+
+ <!--
+ - This handles all the result changes for both
+ - type-ahead-find and highlighting.
+ - @param aResult
+ - One of the nsITypeAheadFind.FIND_* constants
+ - indicating the result of a search operation.
+ - @param aFindBackwards
+ - If the search was done from the bottom to
+ - the top. This is used for right error messages
+ - when reaching "the end of the page".
+ - @param aLinkURL
+ - When a link matched then its URK. Always null
+ - when not in FIND_LINKS mode.
+ -->
+ <method name="onFindResult">
+ <parameter name="aData"/>
+ <body><![CDATA[
+ if (aData.result == this.nsITypeAheadFind.FIND_NOTFOUND) {
+ // If an explicit Find Again command fails, re-open the toolbar.
+ if (aData.storeResult && this.open()) {
+ this._findField.select();
+ this._findField.focus();
+ }
+ this._findFailedString = aData.searchString;
+ } else {
+ this._findFailedString = null;
+ }
+
+ this._updateStatusUI(aData.result, aData.findBackwards);
+ this._updateStatusUIBar(aData.linkURL);
+
+ if (this._findMode != this.FIND_NORMAL)
+ this._setFindCloseTimeout();
+ ]]></body>
+ </method>
+
+ <!--
+ - This handles all the result changes for matches counts.
+ - @param aResult
+ - Result Object, containing the total amount of matches and a vector
+ - of the current result.
+ -->
+ <method name="onMatchesCountResult">
+ <parameter name="aResult"/>
+ <body><![CDATA[
+ if (aResult.total !== 0) {
+ if (aResult.total == -1) {
+ this._foundMatches.value = this.pluralForm.get(
+ aResult.limit,
+ this.strBundle.GetStringFromName("FoundMatchesCountLimit")
+ ).replace("#1", aResult.limit);
+ } else {
+ this._foundMatches.value = this.pluralForm.get(
+ aResult.total,
+ this.strBundle.GetStringFromName("FoundMatches")
+ ).replace("#1", aResult.current)
+ .replace("#2", aResult.total);
+ }
+ this._foundMatches.hidden = false;
+ } else {
+ this._foundMatches.hidden = true;
+ this._foundMatches.value = "";
+ }
+ ]]></body>
+ </method>
+
+ <method name="onHighlightFinished">
+ <parameter name="result"/>
+ <body><![CDATA[
+ // Noop.
+ ]]></body>
+ </method>
+
+ <method name="onCurrentSelection">
+ <parameter name="aSelectionString" />
+ <parameter name="aIsInitialSelection" />
+ <body><![CDATA[
+ // Ignore the prefill if the user has already typed in the findbar,
+ // it would have been overwritten anyway. See bug 1198465.
+ if (aIsInitialSelection && !this._startFindDeferred)
+ return;
+
+ if (/Mac/.test(navigator.platform) && aIsInitialSelection && !aSelectionString) {
+ let clipboardSearchString = this.browser.finder.clipboardSearchString;
+ if (clipboardSearchString)
+ aSelectionString = clipboardSearchString;
+ }
+
+ if (aSelectionString)
+ this._findField.value = aSelectionString;
+
+ if (aIsInitialSelection) {
+ this._enableFindButtons(!!this._findField.value);
+ this._findField.select();
+ this._findField.focus();
+
+ this._startFindDeferred.resolve();
+ this._startFindDeferred = null;
+ }
+ ]]></body>
+ </method>
+
+ <!--
+ - This handler may cancel a request to focus content by returning |false|
+ - explicitly.
+ -->
+ <method name="shouldFocusContent">
+ <body><![CDATA[
+ const fm = Components.classes["@mozilla.org/focus-manager;1"]
+ .getService(Components.interfaces.nsIFocusManager);
+ if (fm.focusedWindow != window)
+ return false;
+
+ let focusedElement = fm.focusedElement;
+ if (!focusedElement)
+ return false;
+
+ let bindingParent = document.getBindingParent(focusedElement);
+ if (bindingParent != this && bindingParent != this._findField)
+ return false;
+
+ return true;
+ ]]></body>
+ </method>
+
+ </implementation>
+
+ <handlers>
+ <!--
+ - We have to guard against `this.close` being |null| due to an unknown
+ - issue, which is tracked in bug 957999.
+ -->
+ <handler event="keypress" keycode="VK_ESCAPE" phase="capturing"
+ action="if (this.close) this.close();" preventdefault="true"/>
+ </handlers>
+ </binding>
+</bindings>
diff --git a/toolkit/content/widgets/general.xml b/toolkit/content/widgets/general.xml
new file mode 100644
index 000000000..b4538e41d
--- /dev/null
+++ b/toolkit/content/widgets/general.xml
@@ -0,0 +1,231 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<bindings id="generalBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="basecontrol">
+ <implementation implements="nsIDOMXULControlElement">
+ <!-- public implementation -->
+ <property name="disabled" onset="if (val) this.setAttribute('disabled', 'true');
+ else this.removeAttribute('disabled');
+ return val;"
+ onget="return this.getAttribute('disabled') == 'true';"/>
+ <property name="tabIndex" onget="return parseInt(this.getAttribute('tabindex')) || 0"
+ onset="if (val) this.setAttribute('tabindex', val);
+ else this.removeAttribute('tabindex'); return val;"/>
+ </implementation>
+ </binding>
+
+ <binding id="basetext" extends="chrome://global/content/bindings/general.xml#basecontrol">
+ <implementation implements="nsIDOMXULLabeledControlElement">
+ <!-- public implementation -->
+ <property name="label" onset="this.setAttribute('label',val); return val;"
+ onget="return this.getAttribute('label');"/>
+ <property name="crop" onset="this.setAttribute('crop',val); return val;"
+ onget="return this.getAttribute('crop');"/>
+ <property name="image" onset="this.setAttribute('image',val); return val;"
+ onget="return this.getAttribute('image');"/>
+ <property name="command" onset="this.setAttribute('command',val); return val;"
+ onget="return this.getAttribute('command');"/>
+ <property name="accessKey">
+ <getter>
+ <![CDATA[
+ return this.labelElement ? this.labelElement.accessKey : this.getAttribute('accesskey');
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ // Always store on the control
+ this.setAttribute('accesskey', val);
+ // If there is a label, change the accesskey on the labelElement
+ // if it's also set there
+ if (this.labelElement) {
+ this.labelElement.accessKey = val;
+ }
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <field name="labelElement"/>
+ </implementation>
+ </binding>
+
+ <binding id="control-item" extends="chrome://global/content/bindings/general.xml#basetext">
+ <implementation>
+ <property name="value" onset="this.setAttribute('value', val); return val;"
+ onget="return this.getAttribute('value');"/>
+ </implementation>
+ </binding>
+
+ <binding id="root-element">
+ <implementation>
+ <field name="_lightweightTheme">null</field>
+ <constructor><![CDATA[
+ if (this.hasAttribute("lightweightthemes")) {
+ let temp = {};
+ Components.utils.import("resource://gre/modules/LightweightThemeConsumer.jsm", temp);
+ this._lightweightTheme = new temp.LightweightThemeConsumer(this.ownerDocument);
+ }
+ ]]></constructor>
+ <destructor><![CDATA[
+ if (this._lightweightTheme) {
+ this._lightweightTheme.destroy();
+ this._lightweightTheme = null;
+ }
+ ]]></destructor>
+ </implementation>
+ </binding>
+
+ <binding id="iframe" role="outerdoc">
+ <implementation>
+ <property name="docShell" readonly="true">
+ <getter><![CDATA[
+ let frameLoader = this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader;
+ return frameLoader ? frameLoader.docShell : null;
+ ]]></getter>
+ </property>
+ <property name="contentWindow"
+ readonly="true"
+ onget="return this.docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindow);"/>
+ <property name="webNavigation"
+ onget="return this.docShell.QueryInterface(Components.interfaces.nsIWebNavigation);"
+ readonly="true"/>
+ <property name="contentDocument" readonly="true"
+ onget="return this.webNavigation.document;"/>
+ </implementation>
+ </binding>
+
+ <binding id="statusbarpanel" display="xul:button">
+ <content>
+ <children>
+ <xul:label class="statusbarpanel-text" xbl:inherits="value=label,crop" crop="right" flex="1"/>
+ </children>
+ </content>
+
+ <implementation>
+ <property name="label"
+ onget="return this.getAttribute('label');"
+ onset="this.setAttribute('label',val); return val;"/>
+ <property name="image"
+ onget="return this.getAttribute('image');"
+ onset="this.setAttribute('image',val); return val;"/>
+ <property name="src"
+ onget="return this.getAttribute('src');"
+ onset="this.setAttribute('src',val); return val;"/>
+ </implementation>
+ </binding>
+
+ <binding id="statusbarpanel-menu-iconic" display="xul:menu"
+ extends="chrome://global/content/bindings/general.xml#statusbarpanel">
+ <content>
+ <xul:image class="statusbarpanel-icon" xbl:inherits="src,src=image"/>
+ <children/>
+ </content>
+ </binding>
+
+ <binding id="statusbar" role="xul:statusbar">
+ <content>
+ <children/>
+ <xul:statusbarpanel class="statusbar-resizerpanel">
+ <xul:resizer dir="bottomend"/>
+ </xul:statusbarpanel>
+ </content>
+ </binding>
+
+ <binding id="statusbarpanel-iconic" display="xul:button" role="xul:button"
+ extends="chrome://global/content/bindings/general.xml#statusbarpanel">
+ <content>
+ <xul:image class="statusbarpanel-icon" xbl:inherits="src,src=image"/>
+ </content>
+ </binding>
+
+ <binding id="statusbarpanel-iconic-text" display="xul:button" role="xul:button"
+ extends="chrome://global/content/bindings/general.xml#statusbarpanel">
+ <content>
+ <xul:image class="statusbarpanel-icon" xbl:inherits="src,src=image"/>
+ <xul:label class="statusbarpanel-text" xbl:inherits="value=label,crop"/>
+ </content>
+ </binding>
+
+ <binding id="image" role="xul:image">
+ <implementation implements="nsIDOMXULImageElement">
+ <property name="src"
+ onget="return this.getAttribute('src');"
+ onset="this.setAttribute('src',val); return val;"/>
+ </implementation>
+ </binding>
+
+ <binding id="deck">
+ <implementation>
+ <property name="selectedIndex"
+ onget="return this.getAttribute('selectedIndex') || '0'">
+ <setter>
+ <![CDATA[
+ if (this.selectedIndex == val)
+ return val;
+ this.setAttribute("selectedIndex", val);
+ var event = document.createEvent('Events');
+ event.initEvent('select', true, true);
+ this.dispatchEvent(event);
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="selectedPanel">
+ <getter>
+ <![CDATA[
+ return this.childNodes[this.selectedIndex];
+ ]]>
+ </getter>
+
+ <setter>
+ <![CDATA[
+ var selectedIndex = -1;
+ for (var panel = val; panel != null; panel = panel.previousSibling)
+ ++selectedIndex;
+ this.selectedIndex = selectedIndex;
+ return val;
+ ]]>
+ </setter>
+ </property>
+ </implementation>
+ </binding>
+
+ <binding id="dropmarker" extends="xul:button" role="xul:dropmarker">
+ <resources>
+ <stylesheet src="chrome://global/skin/dropmarker.css"/>
+ </resources>
+
+ <content>
+ <xul:image class="dropmarker-icon"/>
+ </content>
+ </binding>
+
+ <binding id="windowdragbox">
+ <implementation>
+ <field name="_dragBindingAlive">true</field>
+ <constructor>
+ if (!this._draggableStarted) {
+ this._draggableStarted = true;
+ try {
+ let tmp = {};
+ Components.utils.import("resource://gre/modules/WindowDraggingUtils.jsm", tmp);
+ let draghandle = new tmp.WindowDraggingElement(this);
+ draghandle.mouseDownCheck = function () {
+ return this._dragBindingAlive;
+ };
+ } catch (e) {}
+ }
+ </constructor>
+ </implementation>
+ </binding>
+
+</bindings>
diff --git a/toolkit/content/widgets/groupbox.xml b/toolkit/content/widgets/groupbox.xml
new file mode 100644
index 000000000..7cd3276b4
--- /dev/null
+++ b/toolkit/content/widgets/groupbox.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<bindings id="groupboxBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="groupbox-base">
+ <resources>
+ <stylesheet src="chrome://global/skin/groupbox.css"/>
+ </resources>
+ </binding>
+
+ <binding id="groupbox" role="xul:groupbox"
+ extends="chrome://global/content/bindings/groupbox.xml#groupbox-base">
+ <content>
+ <xul:hbox class="groupbox-title" align="center" pack="start">
+ <children includes="caption"/>
+ </xul:hbox>
+ <xul:box flex="1" class="groupbox-body" xbl:inherits="orient,align,pack">
+ <children/>
+ </xul:box>
+ </content>
+ </binding>
+
+ <binding id="caption" extends="chrome://global/content/bindings/general.xml#basetext">
+ <resources>
+ <stylesheet src="chrome://global/skin/groupbox.css"/>
+ </resources>
+
+ <content>
+ <children>
+ <xul:image class="caption-icon" xbl:inherits="src=image"/>
+ <xul:label class="caption-text" flex="1"
+ xbl:inherits="default,value=label,crop,accesskey"/>
+ </children>
+ </content>
+ </binding>
+
+</bindings>
diff --git a/toolkit/content/widgets/listbox.xml b/toolkit/content/widgets/listbox.xml
new file mode 100644
index 000000000..9fae61669
--- /dev/null
+++ b/toolkit/content/widgets/listbox.xml
@@ -0,0 +1,1144 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<bindings id="listboxBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <!--
+ Interface binding that is base for bindings of xul:listbox and
+ xul:richlistbox elements. This binding assumes that successors bindings
+ will implement the following properties and methods:
+
+ /** Return the number of items */
+ readonly itemCount
+
+ /** Return index of given item
+ * @param aItem - given item element */
+ getIndexOfItem(aItem)
+
+ /** Return item at given index
+ * @param aIndex - index of item element */
+ getItemAtIndex(aIndex)
+
+ /** Return count of item elements */
+ getRowCount()
+
+ /** Return count of visible item elements */
+ getNumberOfVisibleRows()
+
+ /** Return index of first visible item element */
+ getIndexOfFirstVisibleRow()
+
+ /** Return true if item of given index is visible
+ * @param aIndex - index of item element
+ *
+ * @note XXX: this method should be removed after bug 364612 is fixed
+ */
+ ensureIndexIsVisible(aIndex)
+
+ /** Return true if item element is visible
+ * @param aElement - given item element */
+ ensureElementIsVisible(aElement)
+
+ /** Scroll list control to make visible item of given index
+ * @param aIndex - index of item element
+ *
+ * @note XXX: this method should be removed after bug 364612 is fixed
+ */
+ scrollToIndex(aIndex)
+
+ /** Create item element and append it to the end of listbox
+ * @param aLabel - label of new item element
+ * @param aValue - value of new item element */
+ appendItem(aLabel, aValue)
+
+ /** Create item element and insert it to given position
+ * @param aIndex - insertion position
+ * @param aLabel - label of new item element
+ * @param aValue - value of new item element */
+ insertItemAt(aIndex, aLabel, aValue)
+
+ /** Scroll up/down one page
+ * @param aDirection - specifies scrolling direction, should be either -1 or 1
+ * @return the number of elements the selection scrolled
+ */
+ scrollOnePage(aDirection)
+
+ /** Fire "select" event */
+ _fireOnSelect()
+ -->
+ <binding id="listbox-base" role="xul:listbox"
+ extends="chrome://global/content/bindings/general.xml#basecontrol">
+
+ <implementation implements="nsIDOMXULMultiSelectControlElement">
+ <field name="_lastKeyTime">0</field>
+ <field name="_incrementalString">""</field>
+
+ <!-- nsIDOMXULSelectControlElement -->
+ <property name="selectedItem"
+ onset="this.selectItem(val);">
+ <getter>
+ <![CDATA[
+ return this.selectedItems.length > 0 ? this.selectedItems[0] : null;
+ ]]>
+ </getter>
+ </property>
+
+ <property name="selectedIndex">
+ <getter>
+ <![CDATA[
+ if (this.selectedItems.length > 0)
+ return this.getIndexOfItem(this.selectedItems[0]);
+ return -1;
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ if (val >= 0) {
+ this.selectItem(this.getItemAtIndex(val));
+ } else {
+ this.clearSelection();
+ this.currentItem = null;
+ }
+ ]]>
+ </setter>
+ </property>
+
+ <property name="value">
+ <getter>
+ <![CDATA[
+ if (this.selectedItems.length > 0)
+ return this.selectedItem.value;
+ return null;
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ var kids = this.getElementsByAttribute("value", val);
+ if (kids && kids.item(0))
+ this.selectItem(kids[0]);
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <method name="removeItemAt">
+ <parameter name="index"/>
+ <body>
+ <![CDATA[
+ var remove = this.getItemAtIndex(index);
+ if (remove)
+ this.removeChild(remove);
+ return remove;
+ ]]>
+ </body>
+ </method>
+
+ <!-- nsIDOMXULMultiSelectControlElement -->
+ <property name="selType"
+ onget="return this.getAttribute('seltype');"
+ onset="this.setAttribute('seltype', val); return val;"/>
+
+ <property name="currentItem" onget="return this._currentItem;">
+ <setter>
+ if (this._currentItem == val)
+ return val;
+
+ if (this._currentItem)
+ this._currentItem.current = false;
+ this._currentItem = val;
+
+ if (val)
+ val.current = true;
+
+ return val;
+ </setter>
+ </property>
+
+ <property name="currentIndex">
+ <getter>
+ return this.currentItem ? this.getIndexOfItem(this.currentItem) : -1;
+ </getter>
+ <setter>
+ <![CDATA[
+ if (val >= 0)
+ this.currentItem = this.getItemAtIndex(val);
+ else
+ this.currentItem = null;
+ ]]>
+ </setter>
+ </property>
+
+ <field name="selectedItems">new ChromeNodeList()</field>
+
+ <method name="addItemToSelection">
+ <parameter name="aItem"/>
+ <body>
+ <![CDATA[
+ if (this.selType != "multiple" && this.selectedCount)
+ return;
+
+ if (aItem.selected)
+ return;
+
+ this.selectedItems.append(aItem);
+ aItem.selected = true;
+
+ this._fireOnSelect();
+ ]]>
+ </body>
+ </method>
+
+ <method name="removeItemFromSelection">
+ <parameter name="aItem"/>
+ <body>
+ <![CDATA[
+ if (!aItem.selected)
+ return;
+
+ this.selectedItems.remove(aItem);
+ aItem.selected = false;
+ this._fireOnSelect();
+ ]]>
+ </body>
+ </method>
+
+ <method name="toggleItemSelection">
+ <parameter name="aItem"/>
+ <body>
+ <![CDATA[
+ if (aItem.selected)
+ this.removeItemFromSelection(aItem);
+ else
+ this.addItemToSelection(aItem);
+ ]]>
+ </body>
+ </method>
+
+ <method name="selectItem">
+ <parameter name="aItem"/>
+ <body>
+ <![CDATA[
+ if (!aItem)
+ return;
+
+ if (this.selectedItems.length == 1 && this.selectedItems[0] == aItem)
+ return;
+
+ this._selectionStart = null;
+
+ var suppress = this._suppressOnSelect;
+ this._suppressOnSelect = true;
+
+ this.clearSelection();
+ this.addItemToSelection(aItem);
+ this.currentItem = aItem;
+
+ this._suppressOnSelect = suppress;
+ this._fireOnSelect();
+ ]]>
+ </body>
+ </method>
+
+ <method name="selectItemRange">
+ <parameter name="aStartItem"/>
+ <parameter name="aEndItem"/>
+ <body>
+ <![CDATA[
+ if (this.selType != "multiple")
+ return;
+
+ if (!aStartItem)
+ aStartItem = this._selectionStart ?
+ this._selectionStart : this.currentItem;
+
+ if (!aStartItem)
+ aStartItem = aEndItem;
+
+ var suppressSelect = this._suppressOnSelect;
+ this._suppressOnSelect = true;
+
+ this._selectionStart = aStartItem;
+
+ var currentItem;
+ var startIndex = this.getIndexOfItem(aStartItem);
+ var endIndex = this.getIndexOfItem(aEndItem);
+ if (endIndex < startIndex) {
+ currentItem = aEndItem;
+ aEndItem = aStartItem;
+ aStartItem = currentItem;
+ } else {
+ currentItem = aStartItem;
+ }
+
+ while (currentItem) {
+ this.addItemToSelection(currentItem);
+ if (currentItem == aEndItem) {
+ currentItem = this.getNextItem(currentItem, 1);
+ break;
+ }
+ currentItem = this.getNextItem(currentItem, 1);
+ }
+
+ // Clear around new selection
+ // Don't use clearSelection() because it causes a lot of noise
+ // with respect to selection removed notifications used by the
+ // accessibility API support.
+ var userSelecting = this._userSelecting;
+ this._userSelecting = false; // that's US automatically unselecting
+ for (; currentItem; currentItem = this.getNextItem(currentItem, 1))
+ this.removeItemFromSelection(currentItem);
+
+ for (currentItem = this.getItemAtIndex(0); currentItem != aStartItem;
+ currentItem = this.getNextItem(currentItem, 1))
+ this.removeItemFromSelection(currentItem);
+ this._userSelecting = userSelecting;
+
+ this._suppressOnSelect = suppressSelect;
+
+ this._fireOnSelect();
+ ]]>
+ </body>
+ </method>
+
+ <method name="selectAll">
+ <body>
+ this._selectionStart = null;
+
+ var suppress = this._suppressOnSelect;
+ this._suppressOnSelect = true;
+
+ var item = this.getItemAtIndex(0);
+ while (item) {
+ this.addItemToSelection(item);
+ item = this.getNextItem(item, 1);
+ }
+
+ this._suppressOnSelect = suppress;
+ this._fireOnSelect();
+ </body>
+ </method>
+
+ <method name="invertSelection">
+ <body>
+ this._selectionStart = null;
+
+ var suppress = this._suppressOnSelect;
+ this._suppressOnSelect = true;
+
+ var item = this.getItemAtIndex(0);
+ while (item) {
+ if (item.selected)
+ this.removeItemFromSelection(item);
+ else
+ this.addItemToSelection(item);
+ item = this.getNextItem(item, 1);
+ }
+
+ this._suppressOnSelect = suppress;
+ this._fireOnSelect();
+ </body>
+ </method>
+
+ <method name="clearSelection">
+ <body>
+ <![CDATA[
+ if (this.selectedItems) {
+ while (this.selectedItems.length > 0) {
+ let item = this.selectedItems[0];
+ item.selected = false;
+ this.selectedItems.remove(item);
+ }
+ }
+
+ this._selectionStart = null;
+ this._fireOnSelect();
+ ]]>
+ </body>
+ </method>
+
+ <property name="selectedCount" readonly="true"
+ onget="return this.selectedItems.length;"/>
+
+ <method name="getSelectedItem">
+ <parameter name="aIndex"/>
+ <body>
+ <![CDATA[
+ return aIndex < this.selectedItems.length ?
+ this.selectedItems[aIndex] : null;
+ ]]>
+ </body>
+ </method>
+
+ <!-- Other public members -->
+ <property name="disableKeyNavigation"
+ onget="return this.hasAttribute('disableKeyNavigation');">
+ <setter>
+ if (val)
+ this.setAttribute("disableKeyNavigation", "true");
+ else
+ this.removeAttribute("disableKeyNavigation");
+ return val;
+ </setter>
+ </property>
+
+ <property name="suppressOnSelect"
+ onget="return this.getAttribute('suppressonselect') == 'true';"
+ onset="this.setAttribute('suppressonselect', val);"/>
+
+ <property name="_selectDelay"
+ onset="this.setAttribute('_selectDelay', val);"
+ onget="return this.getAttribute('_selectDelay') || 50;"/>
+
+ <method name="timedSelect">
+ <parameter name="aItem"/>
+ <parameter name="aTimeout"/>
+ <body>
+ <![CDATA[
+ var suppress = this._suppressOnSelect;
+ if (aTimeout != -1)
+ this._suppressOnSelect = true;
+
+ this.selectItem(aItem);
+
+ this._suppressOnSelect = suppress;
+
+ if (aTimeout != -1) {
+ if (this._selectTimeout)
+ window.clearTimeout(this._selectTimeout);
+ this._selectTimeout =
+ window.setTimeout(this._selectTimeoutHandler, aTimeout, this);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="moveByOffset">
+ <parameter name="aOffset"/>
+ <parameter name="aIsSelecting"/>
+ <parameter name="aIsSelectingRange"/>
+ <body>
+ <![CDATA[
+ if ((aIsSelectingRange || !aIsSelecting) &&
+ this.selType != "multiple")
+ return;
+
+ var newIndex = this.currentIndex + aOffset;
+ if (newIndex < 0)
+ newIndex = 0;
+
+ var numItems = this.getRowCount();
+ if (newIndex > numItems - 1)
+ newIndex = numItems - 1;
+
+ var newItem = this.getItemAtIndex(newIndex);
+ // make sure that the item is actually visible/selectable
+ if (this._userSelecting && newItem && !this._canUserSelect(newItem))
+ newItem =
+ aOffset > 0 ? this.getNextItem(newItem, 1) || this.getPreviousItem(newItem, 1) :
+ this.getPreviousItem(newItem, 1) || this.getNextItem(newItem, 1);
+ if (newItem) {
+ this.ensureIndexIsVisible(this.getIndexOfItem(newItem));
+ if (aIsSelectingRange)
+ this.selectItemRange(null, newItem);
+ else if (aIsSelecting)
+ this.selectItem(newItem);
+
+ this.currentItem = newItem;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <!-- Private -->
+ <method name="getNextItem">
+ <parameter name="aStartItem"/>
+ <parameter name="aDelta"/>
+ <body>
+ <![CDATA[
+ while (aStartItem) {
+ aStartItem = aStartItem.nextSibling;
+ if (aStartItem && aStartItem instanceof
+ Components.interfaces.nsIDOMXULSelectControlItemElement &&
+ (!this._userSelecting || this._canUserSelect(aStartItem))) {
+ --aDelta;
+ if (aDelta == 0)
+ return aStartItem;
+ }
+ }
+ return null;
+ ]]></body>
+ </method>
+
+ <method name="getPreviousItem">
+ <parameter name="aStartItem"/>
+ <parameter name="aDelta"/>
+ <body>
+ <![CDATA[
+ while (aStartItem) {
+ aStartItem = aStartItem.previousSibling;
+ if (aStartItem && aStartItem instanceof
+ Components.interfaces.nsIDOMXULSelectControlItemElement &&
+ (!this._userSelecting || this._canUserSelect(aStartItem))) {
+ --aDelta;
+ if (aDelta == 0)
+ return aStartItem;
+ }
+ }
+ return null;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_moveByOffsetFromUserEvent">
+ <parameter name="aOffset"/>
+ <parameter name="aEvent"/>
+ <body>
+ <![CDATA[
+ if (!aEvent.defaultPrevented) {
+ this._userSelecting = true;
+ this._mayReverse = true;
+ this.moveByOffset(aOffset, !aEvent.ctrlKey, aEvent.shiftKey);
+ this._userSelecting = false;
+ this._mayReverse = false;
+ aEvent.preventDefault();
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="_canUserSelect">
+ <parameter name="aItem"/>
+ <body>
+ <![CDATA[
+ var style = document.defaultView.getComputedStyle(aItem, "");
+ return style.display != "none" && style.visibility == "visible";
+ ]]>
+ </body>
+ </method>
+
+ <method name="_selectTimeoutHandler">
+ <parameter name="aMe"/>
+ <body>
+ aMe._fireOnSelect();
+ aMe._selectTimeout = null;
+ </body>
+ </method>
+
+ <field name="_suppressOnSelect">false</field>
+ <field name="_userSelecting">false</field>
+ <field name="_mayReverse">false</field>
+ <field name="_selectTimeout">null</field>
+ <field name="_currentItem">null</field>
+ <field name="_selectionStart">null</field>
+ </implementation>
+
+ <handlers>
+ <handler event="keypress" keycode="VK_UP" modifiers="control shift any"
+ action="this._moveByOffsetFromUserEvent(-1, event);"
+ group="system"/>
+ <handler event="keypress" keycode="VK_DOWN" modifiers="control shift any"
+ action="this._moveByOffsetFromUserEvent(1, event);"
+ group="system"/>
+ <handler event="keypress" keycode="VK_HOME" modifiers="control shift any"
+ group="system">
+ <![CDATA[
+ this._mayReverse = true;
+ this._moveByOffsetFromUserEvent(-this.currentIndex, event);
+ this._mayReverse = false;
+ ]]>
+ </handler>
+ <handler event="keypress" keycode="VK_END" modifiers="control shift any"
+ group="system">
+ <![CDATA[
+ this._mayReverse = true;
+ this._moveByOffsetFromUserEvent(this.getRowCount() - this.currentIndex - 1, event);
+ this._mayReverse = false;
+ ]]>
+ </handler>
+ <handler event="keypress" keycode="VK_PAGE_UP" modifiers="control shift any"
+ group="system">
+ <![CDATA[
+ this._mayReverse = true;
+ this._moveByOffsetFromUserEvent(this.scrollOnePage(-1), event);
+ this._mayReverse = false;
+ ]]>
+ </handler>
+ <handler event="keypress" keycode="VK_PAGE_DOWN" modifiers="control shift any"
+ group="system">
+ <![CDATA[
+ this._mayReverse = true;
+ this._moveByOffsetFromUserEvent(this.scrollOnePage(1), event);
+ this._mayReverse = false;
+ ]]>
+ </handler>
+ <handler event="keypress" key=" " modifiers="control" phase="target">
+ <![CDATA[
+ if (this.currentItem && this.selType == "multiple")
+ this.toggleItemSelection(this.currentItem);
+ ]]>
+ </handler>
+ <handler event="focus">
+ <![CDATA[
+ if (this.getRowCount() > 0) {
+ if (this.currentIndex == -1) {
+ this.currentIndex = this.getIndexOfFirstVisibleRow();
+ }
+ else {
+ this.currentItem._fireEvent("DOMMenuItemActive");
+ }
+ }
+ this._lastKeyTime = 0;
+ ]]>
+ </handler>
+ <handler event="keypress" phase="target">
+ <![CDATA[
+ if (this.disableKeyNavigation || !event.charCode ||
+ event.altKey || event.ctrlKey || event.metaKey)
+ return;
+
+ if (event.timeStamp - this._lastKeyTime > 1000)
+ this._incrementalString = "";
+
+ var key = String.fromCharCode(event.charCode).toLowerCase();
+ this._incrementalString += key;
+ this._lastKeyTime = event.timeStamp;
+
+ // If all letters in the incremental string are the same, just
+ // try to match the first one
+ var incrementalString = /^(.)\1+$/.test(this._incrementalString) ?
+ RegExp.$1 : this._incrementalString;
+ var length = incrementalString.length;
+
+ var rowCount = this.getRowCount();
+ var l = this.selectedItems.length;
+ var start = l > 0 ? this.getIndexOfItem(this.selectedItems[l - 1]) : -1;
+ // start from the first element if none was selected or from the one
+ // following the selected one if it's a new or a repeated-letter search
+ if (start == -1 || length == 1)
+ start++;
+
+ for (var i = 0; i < rowCount; i++) {
+ var k = (start + i) % rowCount;
+ var listitem = this.getItemAtIndex(k);
+ if (!this._canUserSelect(listitem))
+ continue;
+ // allow richlistitems to specify the string being searched for
+ var searchText = "searchLabel" in listitem ? listitem.searchLabel :
+ listitem.getAttribute("label"); // (see also bug 250123)
+ searchText = searchText.substring(0, length).toLowerCase();
+ if (searchText == incrementalString) {
+ this.ensureIndexIsVisible(k);
+ this.timedSelect(listitem, this._selectDelay);
+ break;
+ }
+ }
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+
+ <!-- Binding for xul:listbox element.
+ -->
+ <binding id="listbox"
+ extends="#listbox-base">
+
+ <resources>
+ <stylesheet src="chrome://global/skin/listbox.css"/>
+ </resources>
+
+ <content>
+ <children includes="listcols">
+ <xul:listcols>
+ <xul:listcol flex="1"/>
+ </xul:listcols>
+ </children>
+ <xul:listrows>
+ <children includes="listhead"/>
+ <xul:listboxbody xbl:inherits="rows,size,minheight">
+ <children includes="listitem"/>
+ </xul:listboxbody>
+ </xul:listrows>
+ </content>
+
+ <implementation>
+
+ <!-- ///////////////// public listbox members ///////////////// -->
+
+ <property name="listBoxObject"
+ onget="return this.boxObject;"
+ readonly="true"/>
+
+ <!-- ///////////////// private listbox members ///////////////// -->
+
+ <method name="_fireOnSelect">
+ <body>
+ <![CDATA[
+ if (!this._suppressOnSelect && !this.suppressOnSelect) {
+ var event = document.createEvent("Events");
+ event.initEvent("select", true, true);
+ this.dispatchEvent(event);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <constructor>
+ <![CDATA[
+ var count = this.itemCount;
+ for (var index = 0; index < count; index++) {
+ var item = this.getItemAtIndex(index);
+ if (item.getAttribute("selected") == "true")
+ this.selectedItems.append(item);
+ }
+ ]]>
+ </constructor>
+
+ <!-- ///////////////// nsIDOMXULSelectControlElement ///////////////// -->
+
+ <method name="appendItem">
+ <parameter name="aLabel"/>
+ <parameter name="aValue"/>
+ <body>
+ const XULNS =
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+ var item = this.ownerDocument.createElementNS(XULNS, "listitem");
+ item.setAttribute("label", aLabel);
+ item.setAttribute("value", aValue);
+ this.appendChild(item);
+ return item;
+ </body>
+ </method>
+
+ <method name="insertItemAt">
+ <parameter name="aIndex"/>
+ <parameter name="aLabel"/>
+ <parameter name="aValue"/>
+ <body>
+ const XULNS =
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+ var item = this.ownerDocument.createElementNS(XULNS, "listitem");
+ item.setAttribute("label", aLabel);
+ item.setAttribute("value", aValue);
+ var before = this.getItemAtIndex(aIndex);
+ if (before)
+ this.insertBefore(item, before);
+ else
+ this.appendChild(item);
+ return item;
+ </body>
+ </method>
+
+ <property name="itemCount" readonly="true"
+ onget="return this.listBoxObject.getRowCount()"/>
+
+ <!-- ///////////////// nsIListBoxObject ///////////////// -->
+ <method name="getIndexOfItem">
+ <parameter name="item"/>
+ <body>
+ return this.listBoxObject.getIndexOfItem(item);
+ </body>
+ </method>
+ <method name="getItemAtIndex">
+ <parameter name="index"/>
+ <body>
+ return this.listBoxObject.getItemAtIndex(index);
+ </body>
+ </method>
+ <method name="ensureIndexIsVisible">
+ <parameter name="index"/>
+ <body>
+ return this.listBoxObject.ensureIndexIsVisible(index);
+ </body>
+ </method>
+ <method name="ensureElementIsVisible">
+ <parameter name="element"/>
+ <body>
+ return this.ensureIndexIsVisible(this.listBoxObject.getIndexOfItem(element));
+ </body>
+ </method>
+ <method name="scrollToIndex">
+ <parameter name="index"/>
+ <body>
+ return this.listBoxObject.scrollToIndex(index);
+ </body>
+ </method>
+ <method name="getNumberOfVisibleRows">
+ <body>
+ return this.listBoxObject.getNumberOfVisibleRows();
+ </body>
+ </method>
+ <method name="getIndexOfFirstVisibleRow">
+ <body>
+ return this.listBoxObject.getIndexOfFirstVisibleRow();
+ </body>
+ </method>
+ <method name="getRowCount">
+ <body>
+ return this.listBoxObject.getRowCount();
+ </body>
+ </method>
+
+ <method name="scrollOnePage">
+ <parameter name="direction"/> <!-- Must be -1 or 1 -->
+ <body>
+ <![CDATA[
+ var pageOffset = this.getNumberOfVisibleRows() * direction;
+ // skip over invisible elements - the user won't care about them
+ for (var i = 0; i != pageOffset; i += direction) {
+ var item = this.getItemAtIndex(this.currentIndex + i);
+ if (item && !this._canUserSelect(item))
+ pageOffset += direction;
+ }
+ var newTop = this.getIndexOfFirstVisibleRow() + pageOffset;
+ if (direction == 1) {
+ var maxTop = this.getRowCount() - this.getNumberOfVisibleRows();
+ for (i = this.getRowCount(); i >= 0 && i > maxTop; i--) {
+ item = this.getItemAtIndex(i);
+ if (item && !this._canUserSelect(item))
+ maxTop--;
+ }
+ if (newTop >= maxTop)
+ newTop = maxTop;
+ }
+ if (newTop < 0)
+ newTop = 0;
+ this.scrollToIndex(newTop);
+ return pageOffset;
+ ]]>
+ </body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="keypress" key=" " phase="target">
+ <![CDATA[
+ if (this.currentItem) {
+ if (this.currentItem.getAttribute("type") != "checkbox") {
+ this.addItemToSelection(this.currentItem);
+ // Prevent page from scrolling on the space key.
+ event.preventDefault();
+ }
+ else if (!this.currentItem.disabled) {
+ this.currentItem.checked = !this.currentItem.checked;
+ this.currentItem.doCommand();
+ // Prevent page from scrolling on the space key.
+ event.preventDefault();
+ }
+ }
+ ]]>
+ </handler>
+
+ <handler event="MozSwipeGesture">
+ <![CDATA[
+ // Figure out which index to show
+ let targetIndex = 0;
+
+ // Only handle swipe gestures up and down
+ switch (event.direction) {
+ case event.DIRECTION_DOWN:
+ targetIndex = this.itemCount - 1;
+ // Fall through for actual action
+ case event.DIRECTION_UP:
+ this.ensureIndexIsVisible(targetIndex);
+ break;
+ }
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="listrows">
+
+ <resources>
+ <stylesheet src="chrome://global/skin/listbox.css"/>
+ </resources>
+
+ <handlers>
+ <handler event="DOMMouseScroll" phase="capturing">
+ <![CDATA[
+ if (event.axis == event.HORIZONTAL_AXIS)
+ return;
+
+ var listBox = this.parentNode.listBoxObject;
+ var rows = event.detail;
+ if (rows == UIEvent.SCROLL_PAGE_UP)
+ rows = -listBox.getNumberOfVisibleRows();
+ else if (rows == UIEvent.SCROLL_PAGE_DOWN)
+ rows = listBox.getNumberOfVisibleRows();
+
+ listBox.scrollByLines(rows);
+ event.preventDefault();
+ ]]>
+ </handler>
+
+ <handler event="MozMousePixelScroll" phase="capturing">
+ <![CDATA[
+ if (event.axis == event.HORIZONTAL_AXIS)
+ return;
+
+ // shouldn't be scrolled by pixel scrolling events before a line/page
+ // scrolling event.
+ event.preventDefault();
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="listitem" role="xul:listitem"
+ extends="chrome://global/content/bindings/general.xml#basetext">
+ <resources>
+ <stylesheet src="chrome://global/skin/listbox.css"/>
+ </resources>
+
+ <content>
+ <children>
+ <xul:listcell xbl:inherits="label,crop,disabled,flexlabel"/>
+ </children>
+ </content>
+
+ <implementation implements="nsIDOMXULSelectControlItemElement">
+ <property name="current" onget="return this.getAttribute('current') == 'true';">
+ <setter><![CDATA[
+ if (val)
+ this.setAttribute("current", "true");
+ else
+ this.removeAttribute("current");
+
+ let control = this.control;
+ if (!control || !control.suppressMenuItemEvent) {
+ this._fireEvent(val ? "DOMMenuItemActive" : "DOMMenuItemInactive");
+ }
+
+ return val;
+ ]]></setter>
+ </property>
+
+ <!-- ///////////////// nsIDOMXULSelectControlItemElement ///////////////// -->
+
+ <property name="value" onget="return this.getAttribute('value');"
+ onset="this.setAttribute('value', val); return val;"/>
+ <property name="label" onget="return this.getAttribute('label');"
+ onset="this.setAttribute('label', val); return val;"/>
+
+ <property name="selected" onget="return this.getAttribute('selected') == 'true';">
+ <setter><![CDATA[
+ if (val)
+ this.setAttribute("selected", "true");
+ else
+ this.removeAttribute("selected");
+
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="control">
+ <getter><![CDATA[
+ var parent = this.parentNode;
+ while (parent) {
+ if (parent instanceof Components.interfaces.nsIDOMXULSelectControlElement)
+ return parent;
+ parent = parent.parentNode;
+ }
+ return null;
+ ]]></getter>
+ </property>
+
+ <method name="_fireEvent">
+ <parameter name="name"/>
+ <body>
+ <![CDATA[
+ var event = document.createEvent("Events");
+ event.initEvent(name, true, true);
+ this.dispatchEvent(event);
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ <handlers>
+ <!-- If there is no modifier key, we select on mousedown, not
+ click, so that drags work correctly. -->
+ <handler event="mousedown">
+ <![CDATA[
+ var control = this.control;
+ if (!control || control.disabled)
+ return;
+ if ((!event.ctrlKey || (/Mac/.test(navigator.platform) && event.button == 2)) &&
+ !event.shiftKey && !event.metaKey) {
+ if (!this.selected) {
+ control.selectItem(this);
+ }
+ control.currentItem = this;
+ }
+ ]]>
+ </handler>
+
+ <!-- On a click (up+down on the same item), deselect everything
+ except this item. -->
+ <handler event="click" button="0">
+ <![CDATA[
+ var control = this.control;
+ if (!control || control.disabled)
+ return;
+ control._userSelecting = true;
+ if (control.selType != "multiple") {
+ control.selectItem(this);
+ }
+ else if (event.ctrlKey || event.metaKey) {
+ control.toggleItemSelection(this);
+ control.currentItem = this;
+ }
+ else if (event.shiftKey) {
+ control.selectItemRange(null, this);
+ control.currentItem = this;
+ }
+ else {
+ /* We want to deselect all the selected items except what was
+ clicked, UNLESS it was a right-click. We have to do this
+ in click rather than mousedown so that you can drag a
+ selected group of items */
+
+ // use selectItemRange instead of selectItem, because this
+ // doesn't de- and reselect this item if it is selected
+ control.selectItemRange(this, this);
+ }
+ control._userSelecting = false;
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="listitem-iconic"
+ extends="chrome://global/content/bindings/listbox.xml#listitem">
+ <content>
+ <children>
+ <xul:listcell class="listcell-iconic" xbl:inherits="label,image,crop,disabled,flexlabel"/>
+ </children>
+ </content>
+ </binding>
+
+ <binding id="listitem-checkbox"
+ extends="chrome://global/content/bindings/listbox.xml#listitem">
+ <content>
+ <children>
+ <xul:listcell type="checkbox" xbl:inherits="label,crop,checked,disabled,flexlabel"/>
+ </children>
+ </content>
+
+ <implementation>
+ <property name="checked"
+ onget="return this.getAttribute('checked') == 'true';">
+ <setter><![CDATA[
+ if (val)
+ this.setAttribute('checked', 'true');
+ else
+ this.removeAttribute('checked');
+ var event = document.createEvent('Events');
+ event.initEvent('CheckboxStateChange', true, true);
+ this.dispatchEvent(event);
+ return val;
+ ]]></setter>
+ </property>
+ </implementation>
+
+ <handlers>
+ <handler event="mousedown" button="0">
+ <![CDATA[
+ if (!this.disabled && !this.control.disabled) {
+ this.checked = !this.checked;
+ this.doCommand();
+ }
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="listitem-checkbox-iconic"
+ extends="chrome://global/content/bindings/listbox.xml#listitem-checkbox">
+ <content>
+ <children>
+ <xul:listcell type="checkbox" class="listcell-iconic" xbl:inherits="label,image,crop,checked,disabled,flexlabel"/>
+ </children>
+ </content>
+ </binding>
+
+ <binding id="listcell" role="xul:listcell"
+ extends="chrome://global/content/bindings/general.xml#basecontrol">
+
+ <resources>
+ <stylesheet src="chrome://global/skin/listbox.css"/>
+ </resources>
+
+ <content>
+ <children>
+ <xul:label class="listcell-label" xbl:inherits="value=label,flex=flexlabel,crop,disabled" flex="1" crop="right"/>
+ </children>
+ </content>
+ </binding>
+
+ <binding id="listcell-iconic"
+ extends="chrome://global/content/bindings/listbox.xml#listcell">
+ <content>
+ <children>
+ <xul:image class="listcell-icon" xbl:inherits="src=image"/>
+ <xul:label class="listcell-label" xbl:inherits="value=label,flex=flexlabel,crop,disabled" flex="1" crop="right"/>
+ </children>
+ </content>
+ </binding>
+
+ <binding id="listcell-checkbox"
+ extends="chrome://global/content/bindings/listbox.xml#listcell">
+ <content>
+ <children>
+ <xul:image class="listcell-check" xbl:inherits="checked,disabled"/>
+ <xul:label class="listcell-label" xbl:inherits="value=label,flex=flexlabel,crop,disabled" flex="1" crop="right"/>
+ </children>
+ </content>
+ </binding>
+
+ <binding id="listcell-checkbox-iconic"
+ extends="chrome://global/content/bindings/listbox.xml#listcell-checkbox">
+ <content>
+ <children>
+ <xul:image class="listcell-check" xbl:inherits="checked,disabled"/>
+ <xul:image class="listcell-icon" xbl:inherits="src=image"/>
+ <xul:label class="listcell-label" xbl:inherits="value=label,flex=flexlabel,crop,disabled" flex="1" crop="right"/>
+ </children>
+ </content>
+ </binding>
+
+ <binding id="listhead" role="xul:listhead">
+
+ <resources>
+ <stylesheet src="chrome://global/skin/listbox.css"/>
+ </resources>
+
+ <content>
+ <xul:listheaditem>
+ <children includes="listheader"/>
+ </xul:listheaditem>
+ </content>
+ </binding>
+
+ <binding id="listheader" display="xul:button" role="xul:listheader">
+
+ <resources>
+ <stylesheet src="chrome://global/skin/listbox.css"/>
+ </resources>
+
+ <content>
+ <xul:image class="listheader-icon"/>
+ <xul:label class="listheader-label" xbl:inherits="value=label,crop" flex="1" crop="right"/>
+ <xul:image class="listheader-sortdirection" xbl:inherits="sortDirection"/>
+ </content>
+ </binding>
+
+</bindings>
diff --git a/toolkit/content/widgets/menu.xml b/toolkit/content/widgets/menu.xml
new file mode 100644
index 000000000..26dcad454
--- /dev/null
+++ b/toolkit/content/widgets/menu.xml
@@ -0,0 +1,286 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<bindings id="menuitemBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="menuitem-base" role="xul:menuitem"
+ extends="chrome://global/content/bindings/general.xml#control-item">
+ <resources>
+ <stylesheet src="chrome://global/skin/menu.css"/>
+ </resources>
+ <implementation implements="nsIDOMXULSelectControlItemElement, nsIDOMXULContainerItemElement">
+ <!-- nsIDOMXULSelectControlItemElement -->
+ <property name="selected" readonly="true"
+ onget="return this.getAttribute('selected') == 'true';"/>
+ <property name="control" readonly="true">
+ <getter>
+ <![CDATA[
+ var parent = this.parentNode;
+ if (parent &&
+ parent.parentNode instanceof Components.interfaces.nsIDOMXULSelectControlElement)
+ return parent.parentNode;
+ return null;
+ ]]>
+ </getter>
+ </property>
+
+ <!-- nsIDOMXULContainerItemElement -->
+ <property name="parentContainer" readonly="true">
+ <getter>
+ for (var parent = this.parentNode; parent; parent = parent.parentNode) {
+ if (parent instanceof Components.interfaces.nsIDOMXULContainerElement)
+ return parent;
+ }
+ return null;
+ </getter>
+ </property>
+ </implementation>
+ </binding>
+
+ <binding id="menu-base"
+ extends="chrome://global/content/bindings/menu.xml#menuitem-base">
+
+ <implementation implements="nsIDOMXULContainerElement">
+ <property name="open" onget="return this.hasAttribute('open');">
+ <setter><![CDATA[
+ this.boxObject.openMenu(val);
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="openedWithKey" readonly="true">
+ <getter><![CDATA[
+ return this.boxObject.openedWithKey;
+ ]]></getter>
+ </property>
+
+ <!-- nsIDOMXULContainerElement interface -->
+ <method name="appendItem">
+ <parameter name="aLabel"/>
+ <parameter name="aValue"/>
+ <body>
+ return this.insertItemAt(-1, aLabel, aValue);
+ </body>
+ </method>
+
+ <method name="insertItemAt">
+ <parameter name="aIndex"/>
+ <parameter name="aLabel"/>
+ <parameter name="aValue"/>
+ <body>
+ const XUL_NS =
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+ var menupopup = this.menupopup;
+ if (!menupopup) {
+ menupopup = this.ownerDocument.createElementNS(XUL_NS, "menupopup");
+ this.appendChild(menupopup);
+ }
+
+ var menuitem = this.ownerDocument.createElementNS(XUL_NS, "menuitem");
+ menuitem.setAttribute("label", aLabel);
+ menuitem.setAttribute("value", aValue);
+
+ var before = this.getItemAtIndex(aIndex);
+ if (before)
+ return menupopup.insertBefore(menuitem, before);
+ return menupopup.appendChild(menuitem);
+ </body>
+ </method>
+
+ <method name="removeItemAt">
+ <parameter name="aIndex"/>
+ <body>
+ <![CDATA[
+ var menupopup = this.menupopup;
+ if (menupopup) {
+ var item = this.getItemAtIndex(aIndex);
+ if (item)
+ return menupopup.removeChild(item);
+ }
+ return null;
+ ]]>
+ </body>
+ </method>
+
+ <property name="itemCount" readonly="true">
+ <getter>
+ var menupopup = this.menupopup;
+ return menupopup ? menupopup.childNodes.length : 0;
+ </getter>
+ </property>
+
+ <method name="getIndexOfItem">
+ <parameter name="aItem"/>
+ <body>
+ <![CDATA[
+ var menupopup = this.menupopup;
+ if (menupopup) {
+ var items = menupopup.childNodes;
+ var length = items.length;
+ for (var index = 0; index < length; ++index) {
+ if (items[index] == aItem)
+ return index;
+ }
+ }
+ return -1;
+ ]]>
+ </body>
+ </method>
+
+ <method name="getItemAtIndex">
+ <parameter name="aIndex"/>
+ <body>
+ <![CDATA[
+ var menupopup = this.menupopup;
+ if (!menupopup || aIndex < 0 || aIndex >= menupopup.childNodes.length)
+ return null;
+
+ return menupopup.childNodes[aIndex];
+ ]]>
+ </body>
+ </method>
+
+ <property name="menupopup" readonly="true">
+ <getter>
+ <![CDATA[
+ const XUL_NS =
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+ for (var child = this.firstChild; child; child = child.nextSibling) {
+ if (child.namespaceURI == XUL_NS && child.localName == "menupopup")
+ return child;
+ }
+ return null;
+ ]]>
+ </getter>
+ </property>
+ </implementation>
+ </binding>
+
+ <binding id="menu"
+ extends="chrome://global/content/bindings/menu.xml#menu-base">
+ <content>
+ <xul:label class="menu-text" xbl:inherits="value=label,accesskey,crop" crop="right"/>
+ <xul:hbox class="menu-accel-container" anonid="accel">
+ <xul:label class="menu-accel" xbl:inherits="value=acceltext"/>
+ </xul:hbox>
+ <xul:hbox align="center" class="menu-right" xbl:inherits="_moz-menuactive,disabled">
+ <xul:image/>
+ </xul:hbox>
+ <children includes="menupopup"/>
+ </content>
+ </binding>
+
+ <binding id="menuitem" extends="chrome://global/content/bindings/menu.xml#menuitem-base">
+ <content>
+ <xul:label class="menu-text" xbl:inherits="value=label,accesskey,crop" crop="right"/>
+ <xul:hbox class="menu-accel-container" anonid="accel">
+ <xul:label class="menu-accel" xbl:inherits="value=acceltext"/>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="menucaption" extends="chrome://global/content/bindings/menu.xml#menu-base">
+ <content>
+ <xul:label class="menu-text" xbl:inherits="value=label,crop" crop="right"/>
+ </content>
+ </binding>
+
+ <binding id="menu-menubar"
+ extends="chrome://global/content/bindings/menu.xml#menu-base">
+ <content>
+ <xul:label class="menubar-text" xbl:inherits="value=label,accesskey,crop" crop="right"/>
+ <children includes="menupopup"/>
+ </content>
+ </binding>
+
+ <binding id="menu-menubar-iconic"
+ extends="chrome://global/content/bindings/menu.xml#menu-base">
+ <content>
+ <xul:image class="menubar-left" xbl:inherits="src=image"/>
+ <xul:label class="menubar-text" xbl:inherits="value=label,accesskey,crop" crop="right"/>
+ <children includes="menupopup"/>
+ </content>
+ </binding>
+
+ <binding id="menuitem-iconic" extends="chrome://global/content/bindings/menu.xml#menuitem">
+ <content>
+ <xul:hbox class="menu-iconic-left" align="center" pack="center"
+ xbl:inherits="selected,_moz-menuactive,disabled,checked">
+ <xul:image class="menu-iconic-icon" xbl:inherits="src=image,validate,src"/>
+ </xul:hbox>
+ <xul:label class="menu-iconic-text" flex="1" xbl:inherits="value=label,accesskey,crop" crop="right"/>
+ <children/>
+ <xul:hbox class="menu-accel-container" anonid="accel">
+ <xul:label class="menu-iconic-accel" xbl:inherits="value=acceltext"/>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="menuitem-iconic-noaccel" extends="chrome://global/content/bindings/menu.xml#menuitem">
+ <content>
+ <xul:hbox class="menu-iconic-left" align="center" pack="center"
+ xbl:inherits="selected,disabled,checked">
+ <xul:image class="menu-iconic-icon" xbl:inherits="src=image,validate,src"/>
+ </xul:hbox>
+ <xul:label class="menu-iconic-text" flex="1" xbl:inherits="value=label,accesskey,crop" crop="right"/>
+ </content>
+ </binding>
+
+ <binding id="menucaption-inmenulist" extends="chrome://global/content/bindings/menu.xml#menucaption">
+ <content>
+ <xul:hbox class="menu-iconic-left" align="center" pack="center"
+ xbl:inherits="selected,disabled,checked">
+ <xul:image class="menu-iconic-icon" xbl:inherits="src=image,validate,src"/>
+ </xul:hbox>
+ <xul:label class="menu-iconic-text" flex="1" xbl:inherits="value=label,crop" crop="right"/>
+ </content>
+ </binding>
+
+ <binding id="menuitem-iconic-desc-noaccel" extends="chrome://global/content/bindings/menu.xml#menuitem">
+ <content>
+ <xul:hbox class="menu-iconic-left" align="center" pack="center"
+ xbl:inherits="selected,disabled,checked">
+ <xul:image class="menu-iconic-icon" xbl:inherits="src=image,validate,src"/>
+ </xul:hbox>
+ <xul:label class="menu-iconic-text" xbl:inherits="value=label,accesskey,crop" crop="right" flex="1"/>
+ <xul:label class="menu-iconic-text menu-description" xbl:inherits="value=description" crop="right" flex="10000"/>
+ </content>
+ </binding>
+
+ <binding id="menu-iconic"
+ extends="chrome://global/content/bindings/menu.xml#menu-base">
+ <content>
+ <xul:hbox class="menu-iconic-left" align="center" pack="center">
+ <xul:image class="menu-iconic-icon" xbl:inherits="src=image"/>
+ </xul:hbox>
+ <xul:label class="menu-iconic-text" flex="1" xbl:inherits="value=label,accesskey,crop" crop="right"/>
+ <xul:hbox class="menu-accel-container" anonid="accel">
+ <xul:label class="menu-iconic-accel" xbl:inherits="value=acceltext"/>
+ </xul:hbox>
+ <xul:hbox align="center" class="menu-right" xbl:inherits="_moz-menuactive,disabled">
+ <xul:image/>
+ </xul:hbox>
+ <children includes="menupopup|template"/>
+ </content>
+ </binding>
+
+ <binding id="menubutton-item" extends="chrome://global/content/bindings/menu.xml#menuitem-base">
+ <content>
+ <xul:label class="menubutton-text" flex="1" xbl:inherits="value=label,accesskey,crop" crop="right"/>
+ <children includes="menupopup"/>
+ </content>
+ </binding>
+
+ <binding id="menuseparator" role="xul:menuseparator"
+ extends="chrome://global/content/bindings/menu.xml#menuitem-base">
+ </binding>
+
+</bindings>
diff --git a/toolkit/content/widgets/menulist.xml b/toolkit/content/widgets/menulist.xml
new file mode 100644
index 000000000..ccdf3bd26
--- /dev/null
+++ b/toolkit/content/widgets/menulist.xml
@@ -0,0 +1,606 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<bindings id="menulistBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="menulist-base" extends="chrome://global/content/bindings/general.xml#basecontrol">
+ <resources>
+ <stylesheet src="chrome://global/content/menulist.css"/>
+ <stylesheet src="chrome://global/skin/menulist.css"/>
+ </resources>
+ </binding>
+
+ <binding id="menulist" display="xul:menu" role="xul:menulist"
+ extends="chrome://global/content/bindings/menulist.xml#menulist-base">
+ <content sizetopopup="pref">
+ <xul:hbox class="menulist-label-box" flex="1">
+ <xul:image class="menulist-icon" xbl:inherits="src=image,src"/>
+ <xul:label class="menulist-label" xbl:inherits="value=label,crop,accesskey" crop="right" flex="1"/>
+ </xul:hbox>
+ <xul:dropmarker class="menulist-dropmarker" type="menu" xbl:inherits="disabled,open"/>
+ <children includes="menupopup"/>
+ </content>
+
+ <handlers>
+ <handler event="command" phase="capturing"
+ action="if (event.target.parentNode.parentNode == this) this.selectedItem = event.target;"/>
+
+ <handler event="popupshowing">
+ <![CDATA[
+ if (event.target.parentNode == this) {
+ this.menuBoxObject.activeChild = null;
+ if (this.selectedItem)
+ // Not ready for auto-setting the active child in hierarchies yet.
+ // For now, only do this when the outermost menupopup opens.
+ this.menuBoxObject.activeChild = this.mSelectedInternal;
+ }
+ ]]>
+ </handler>
+
+ <handler event="keypress" modifiers="shift any" group="system">
+ <![CDATA[
+ if (!event.defaultPrevented &&
+ (event.keyCode == KeyEvent.DOM_VK_UP ||
+ event.keyCode == KeyEvent.DOM_VK_DOWN ||
+ event.keyCode == KeyEvent.DOM_VK_PAGE_UP ||
+ event.keyCode == KeyEvent.DOM_VK_PAGE_DOWN ||
+ event.keyCode == KeyEvent.DOM_VK_HOME ||
+ event.keyCode == KeyEvent.DOM_VK_END ||
+ event.keyCode == KeyEvent.DOM_VK_BACK_SPACE ||
+ event.charCode > 0)) {
+ // Moving relative to an item: start from the currently selected item
+ this.menuBoxObject.activeChild = this.mSelectedInternal;
+ if (this.menuBoxObject.handleKeyPress(event)) {
+ this.menuBoxObject.activeChild.doCommand();
+ event.preventDefault();
+ }
+ }
+ ]]>
+ </handler>
+ </handlers>
+
+ <implementation implements="nsIDOMXULMenuListElement">
+ <constructor>
+ this.mInputField = null;
+ this.mSelectedInternal = null;
+ this.mAttributeObserver = null;
+ this.menuBoxObject = this.boxObject;
+ this.setInitialSelection();
+ </constructor>
+
+ <method name="setInitialSelection">
+ <body>
+ <![CDATA[
+ var popup = this.menupopup;
+ if (popup) {
+ var arr = popup.getElementsByAttribute('selected', 'true');
+
+ var editable = this.editable;
+ var value = this.value;
+ if (!arr.item(0) && value)
+ arr = popup.getElementsByAttribute(editable ? 'label' : 'value', value);
+
+ if (arr.item(0))
+ this.selectedItem = arr[0];
+ else if (!editable)
+ this.selectedIndex = 0;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <property name="value" onget="return this.getAttribute('value');">
+ <setter>
+ <![CDATA[
+ // if the new value is null, we still need to remove the old value
+ if (val == null)
+ return this.selectedItem = val;
+
+ var arr = null;
+ var popup = this.menupopup;
+ if (popup)
+ arr = popup.getElementsByAttribute('value', val);
+
+ if (arr && arr.item(0))
+ this.selectedItem = arr[0];
+ else {
+ this.selectedItem = null;
+ this.setAttribute('value', val);
+ }
+
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="inputField" readonly="true" onget="return null;"/>
+
+ <property name="crop" onset="this.setAttribute('crop',val); return val;"
+ onget="return this.getAttribute('crop');"/>
+ <property name="image" onset="this.setAttribute('image',val); return val;"
+ onget="return this.getAttribute('image');"/>
+ <property name="label" readonly="true" onget="return this.getAttribute('label');"/>
+ <property name="description" onset="this.setAttribute('description',val); return val;"
+ onget="return this.getAttribute('description');"/>
+ <property name="editable" onset="this.setAttribute('editable',val); return val;"
+ onget="return this.getAttribute('editable') == 'true';"/>
+
+ <property name="open" onset="this.menuBoxObject.openMenu(val);
+ return val;"
+ onget="return this.hasAttribute('open');"/>
+
+ <property name="itemCount" readonly="true"
+ onget="return this.menupopup ? this.menupopup.childNodes.length : 0"/>
+
+ <property name="menupopup" readonly="true">
+ <getter>
+ <![CDATA[
+ var popup = this.firstChild;
+ while (popup && popup.localName != "menupopup")
+ popup = popup.nextSibling;
+ return popup;
+ ]]>
+ </getter>
+ </property>
+
+ <method name="contains">
+ <parameter name="item"/>
+ <body>
+ <![CDATA[
+ if (!item)
+ return false;
+
+ var parent = item.parentNode;
+ return (parent && parent.parentNode == this);
+ ]]>
+ </body>
+ </method>
+
+ <property name="selectedIndex">
+ <getter>
+ <![CDATA[
+ // Quick and dirty. We won't deal with hierarchical menulists yet.
+ if (!this.selectedItem ||
+ !this.mSelectedInternal.parentNode ||
+ this.mSelectedInternal.parentNode.parentNode != this)
+ return -1;
+
+ var children = this.mSelectedInternal.parentNode.childNodes;
+ var i = children.length;
+ while (i--)
+ if (children[i] == this.mSelectedInternal)
+ break;
+
+ return i;
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ var popup = this.menupopup;
+ if (popup && 0 <= val) {
+ if (val < popup.childNodes.length)
+ this.selectedItem = popup.childNodes[val];
+ }
+ else
+ this.selectedItem = null;
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="selectedItem">
+ <getter>
+ <![CDATA[
+ return this.mSelectedInternal;
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ var oldval = this.mSelectedInternal;
+ if (oldval == val)
+ return val;
+
+ if (val && !this.contains(val))
+ return val;
+
+ if (oldval) {
+ oldval.removeAttribute('selected');
+ this.mAttributeObserver.disconnect();
+ }
+
+ this.mSelectedInternal = val;
+ let attributeFilter = ["value", "label", "image", "description"];
+ if (val) {
+ val.setAttribute('selected', 'true');
+ for (let attr of attributeFilter) {
+ if (val.hasAttribute(attr)) {
+ this.setAttribute(attr, val.getAttribute(attr));
+ }
+ else {
+ this.removeAttribute(attr);
+ }
+ }
+
+ this.mAttributeObserver = new MutationObserver(this.handleMutation.bind(this));
+ this.mAttributeObserver.observe(val, { attributeFilter });
+ }
+ else {
+ for (let attr of attributeFilter) {
+ this.removeAttribute(attr);
+ }
+ }
+
+ var event = document.createEvent("Events");
+ event.initEvent("select", true, true);
+ this.dispatchEvent(event);
+
+ event = document.createEvent("Events");
+ event.initEvent("ValueChange", true, true);
+ this.dispatchEvent(event);
+
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <method name="handleMutation">
+ <parameter name="aRecords"/>
+ <body>
+ <![CDATA[
+ for (let record of aRecords) {
+ let t = record.target;
+ if (t == this.mSelectedInternal) {
+ let attrName = record.attributeName;
+ switch (attrName) {
+ case "value":
+ case "label":
+ case "image":
+ case "description":
+ if (t.hasAttribute(attrName)) {
+ this.setAttribute(attrName, t.getAttribute(attrName));
+ }
+ else {
+ this.removeAttribute(attrName);
+ }
+ }
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="getIndexOfItem">
+ <parameter name="item"/>
+ <body>
+ <![CDATA[
+ var popup = this.menupopup;
+ if (popup) {
+ var children = popup.childNodes;
+ var i = children.length;
+ while (i--)
+ if (children[i] == item)
+ return i;
+ }
+ return -1;
+ ]]>
+ </body>
+ </method>
+
+ <method name="getItemAtIndex">
+ <parameter name="index"/>
+ <body>
+ <![CDATA[
+ var popup = this.menupopup;
+ if (popup) {
+ var children = popup.childNodes;
+ if (index >= 0 && index < children.length)
+ return children[index];
+ }
+ return null;
+ ]]>
+ </body>
+ </method>
+
+ <method name="appendItem">
+ <parameter name="label"/>
+ <parameter name="value"/>
+ <parameter name="description"/>
+ <body>
+ <![CDATA[
+ const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ var popup = this.menupopup ||
+ this.appendChild(document.createElementNS(XULNS, "menupopup"));
+ var item = document.createElementNS(XULNS, "menuitem");
+ item.setAttribute("label", label);
+ item.setAttribute("value", value);
+ if (description)
+ item.setAttribute("description", description);
+
+ popup.appendChild(item);
+ return item;
+ ]]>
+ </body>
+ </method>
+
+ <method name="insertItemAt">
+ <parameter name="index"/>
+ <parameter name="label"/>
+ <parameter name="value"/>
+ <parameter name="description"/>
+ <body>
+ <![CDATA[
+ const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ var popup = this.menupopup ||
+ this.appendChild(document.createElementNS(XULNS, "menupopup"));
+ var item = document.createElementNS(XULNS, "menuitem");
+ item.setAttribute("label", label);
+ item.setAttribute("value", value);
+ if (description)
+ item.setAttribute("description", description);
+
+ if (index >= 0 && index < popup.childNodes.length)
+ popup.insertBefore(item, popup.childNodes[index]);
+ else
+ popup.appendChild(item);
+ return item;
+ ]]>
+ </body>
+ </method>
+
+ <method name="removeItemAt">
+ <parameter name="index"/>
+ <body>
+ <![CDATA[
+ var popup = this.menupopup;
+ if (popup && 0 <= index && index < popup.childNodes.length) {
+ var remove = popup.childNodes[index];
+ popup.removeChild(remove);
+ return remove;
+ }
+ return null;
+ ]]>
+ </body>
+ </method>
+
+ <method name="removeAllItems">
+ <body>
+ <![CDATA[
+ this.selectedItem = null;
+ var popup = this.menupopup;
+ if (popup)
+ this.removeChild(popup);
+ ]]>
+ </body>
+ </method>
+
+ <destructor>
+ <![CDATA[
+ if (this.mAttributeObserver) {
+ this.mAttributeObserver.disconnect();
+ }
+ ]]>
+ </destructor>
+ </implementation>
+ </binding>
+
+ <binding id="menulist-editable" extends="chrome://global/content/bindings/menulist.xml#menulist">
+ <content sizetopopup="pref">
+ <xul:hbox class="menulist-editable-box textbox-input-box" xbl:inherits="context,disabled,readonly,focused" flex="1">
+ <html:input class="menulist-editable-input" anonid="input" allowevents="true"
+ xbl:inherits="value=label,value,disabled,tabindex,readonly,placeholder"/>
+ </xul:hbox>
+ <xul:dropmarker class="menulist-dropmarker" type="menu"
+ xbl:inherits="open,disabled,parentfocused=focused"/>
+ <children includes="menupopup"/>
+ </content>
+
+ <implementation>
+ <method name="_selectInputFieldValueInList">
+ <body>
+ <![CDATA[
+ if (this.hasAttribute("disableautoselect"))
+ return;
+
+ // Find and select the menuitem that matches inputField's "value"
+ var arr = null;
+ var popup = this.menupopup;
+
+ if (popup)
+ arr = popup.getElementsByAttribute('label', this.inputField.value);
+
+ this.setSelectionInternal(arr ? arr.item(0) : null);
+ ]]>
+ </body>
+ </method>
+
+ <method name="setSelectionInternal">
+ <parameter name="val"/>
+ <body>
+ <![CDATA[
+ // This is called internally to set selected item
+ // without triggering infinite loop
+ // when using selectedItem's setter
+ if (this.mSelectedInternal == val)
+ return val;
+
+ if (this.mSelectedInternal)
+ this.mSelectedInternal.removeAttribute('selected');
+
+ this.mSelectedInternal = val;
+
+ if (val)
+ val.setAttribute('selected', 'true');
+
+ // Do NOT change the "value", which is owned by inputField
+ return val;
+ ]]>
+ </body>
+ </method>
+
+ <property name="inputField" readonly="true">
+ <getter><![CDATA[
+ if (!this.mInputField)
+ this.mInputField = document.getAnonymousElementByAttribute(this, "anonid", "input");
+ return this.mInputField;
+ ]]></getter>
+ </property>
+
+ <property name="label" onset="this.inputField.value = val; return val;"
+ onget="return this.inputField.value;"/>
+
+ <property name="value" onget="return this.inputField.value;">
+ <setter>
+ <![CDATA[
+ // Override menulist's value setter to refer to the inputField's value
+ // (Allows using "menulist.value" instead of "menulist.inputField.value")
+ this.inputField.value = val;
+ this.setAttribute('value', val);
+ this.setAttribute('label', val);
+ this._selectInputFieldValueInList();
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="selectedItem">
+ <getter>
+ <![CDATA[
+ // Make sure internally-selected item
+ // is in sync with inputField.value
+ this._selectInputFieldValueInList();
+ return this.mSelectedInternal;
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ var oldval = this.mSelectedInternal;
+ if (oldval == val)
+ return val;
+
+ if (val && !this.contains(val))
+ return val;
+
+ // This doesn't touch inputField.value or "value" and "label" attributes
+ this.setSelectionInternal(val);
+ if (val) {
+ // Editable menulist uses "label" as its "value"
+ var label = val.getAttribute('label');
+ this.inputField.value = label;
+ this.setAttribute('value', label);
+ this.setAttribute('label', label);
+ }
+ else {
+ this.inputField.value = "";
+ this.removeAttribute('value');
+ this.removeAttribute('label');
+ }
+
+ var event = document.createEvent("Events");
+ event.initEvent("select", true, true);
+ this.dispatchEvent(event);
+
+ event = document.createEvent("Events");
+ event.initEvent("ValueChange", true, true);
+ this.dispatchEvent(event);
+
+ return val;
+ ]]>
+ </setter>
+ </property>
+ <property name="disableautoselect"
+ onset="if (val) this.setAttribute('disableautoselect','true');
+ else this.removeAttribute('disableautoselect'); return val;"
+ onget="return this.hasAttribute('disableautoselect');"/>
+
+ <property name="editor" readonly="true">
+ <getter><![CDATA[
+ const nsIDOMNSEditableElement = Components.interfaces.nsIDOMNSEditableElement;
+ return this.inputField.QueryInterface(nsIDOMNSEditableElement).editor;
+ ]]></getter>
+ </property>
+
+ <property name="readOnly" onset="this.inputField.readOnly = val;
+ if (val) this.setAttribute('readonly', 'true');
+ else this.removeAttribute('readonly'); return val;"
+ onget="return this.inputField.readOnly;"/>
+
+ <method name="select">
+ <body>
+ this.inputField.select();
+ </body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="focus" phase="capturing">
+ <![CDATA[
+ this.setAttribute('focused', 'true');
+ ]]>
+ </handler>
+
+ <handler event="blur" phase="capturing">
+ <![CDATA[
+ this.removeAttribute('focused');
+ ]]>
+ </handler>
+
+ <handler event="popupshowing">
+ <![CDATA[
+ // editable menulists elements aren't in the focus order,
+ // so when the popup opens we need to force the focus to the inputField
+ if (event.target.parentNode == this) {
+ if (document.commandDispatcher.focusedElement != this.inputField)
+ this.inputField.focus();
+
+ this.menuBoxObject.activeChild = null;
+ if (this.selectedItem)
+ // Not ready for auto-setting the active child in hierarchies yet.
+ // For now, only do this when the outermost menupopup opens.
+ this.menuBoxObject.activeChild = this.mSelectedInternal;
+ }
+ ]]>
+ </handler>
+
+ <handler event="keypress">
+ <![CDATA[
+ // open popup if key is up arrow, down arrow, or F4
+ if (!event.ctrlKey && !event.shiftKey) {
+ if (event.keyCode == KeyEvent.DOM_VK_UP ||
+ event.keyCode == KeyEvent.DOM_VK_DOWN ||
+ (event.keyCode == KeyEvent.DOM_VK_F4 && !event.altKey)) {
+ event.preventDefault();
+ this.open = true;
+ }
+ }
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="menulist-description" display="xul:menu"
+ extends="chrome://global/content/bindings/menulist.xml#menulist">
+ <content sizetopopup="pref">
+ <xul:hbox class="menulist-label-box" flex="1">
+ <xul:image class="menulist-icon" xbl:inherits="src=image,src"/>
+ <xul:label class="menulist-label" xbl:inherits="value=label,crop,accesskey" crop="right" flex="1"/>
+ <xul:label class="menulist-label menulist-description" xbl:inherits="value=description" crop="right" flex="10000"/>
+ </xul:hbox>
+ <xul:dropmarker class="menulist-dropmarker" type="menu" xbl:inherits="disabled,open"/>
+ <children includes="menupopup"/>
+ </content>
+ </binding>
+
+ <binding id="menulist-popuponly" display="xul:menu"
+ extends="chrome://global/content/bindings/menulist.xml#menulist">
+ <content sizetopopup="pref">
+ <children includes="menupopup"/>
+ </content>
+ </binding>
+</bindings>
diff --git a/toolkit/content/widgets/notification.xml b/toolkit/content/widgets/notification.xml
new file mode 100644
index 000000000..2cc2f4b2c
--- /dev/null
+++ b/toolkit/content/widgets/notification.xml
@@ -0,0 +1,551 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<!DOCTYPE bindings [
+<!ENTITY % notificationDTD SYSTEM "chrome://global/locale/notification.dtd">
+%notificationDTD;
+]>
+
+<bindings id="notificationBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xbl="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <binding id="notificationbox">
+ <content>
+ <xul:stack xbl:inherits="hidden=notificationshidden"
+ class="notificationbox-stack">
+ <xul:spacer/>
+ <children includes="notification"/>
+ </xul:stack>
+ <children/>
+ </content>
+
+ <implementation>
+ <field name="PRIORITY_INFO_LOW" readonly="true">1</field>
+ <field name="PRIORITY_INFO_MEDIUM" readonly="true">2</field>
+ <field name="PRIORITY_INFO_HIGH" readonly="true">3</field>
+ <field name="PRIORITY_WARNING_LOW" readonly="true">4</field>
+ <field name="PRIORITY_WARNING_MEDIUM" readonly="true">5</field>
+ <field name="PRIORITY_WARNING_HIGH" readonly="true">6</field>
+ <field name="PRIORITY_CRITICAL_LOW" readonly="true">7</field>
+ <field name="PRIORITY_CRITICAL_MEDIUM" readonly="true">8</field>
+ <field name="PRIORITY_CRITICAL_HIGH" readonly="true">9</field>
+ <field name="PRIORITY_CRITICAL_BLOCK" readonly="true">10</field>
+
+ <field name="currentNotification">null</field>
+
+ <field name="_closedNotification">null</field>
+ <field name="_blockingCanvas">null</field>
+ <field name="_animating">false</field>
+
+ <property name="notificationsHidden"
+ onget="return this.getAttribute('notificationshidden') == 'true';">
+ <setter>
+ if (val)
+ this.setAttribute('notificationshidden', true);
+ else this.removeAttribute('notificationshidden');
+ return val;
+ </setter>
+ </property>
+
+ <property name="allNotifications" readonly="true">
+ <getter>
+ <![CDATA[
+ var closedNotification = this._closedNotification;
+ var notifications = this.getElementsByTagName('notification');
+ return Array.filter(notifications, n => n != closedNotification);
+ ]]>
+ </getter>
+ </property>
+
+ <method name="getNotificationWithValue">
+ <parameter name="aValue"/>
+ <body>
+ <![CDATA[
+ var notifications = this.allNotifications;
+ for (var n = notifications.length - 1; n >= 0; n--) {
+ if (aValue == notifications[n].getAttribute("value"))
+ return notifications[n];
+ }
+ return null;
+ ]]>
+ </body>
+ </method>
+
+ <method name="appendNotification">
+ <parameter name="aLabel"/>
+ <parameter name="aValue"/>
+ <parameter name="aImage"/>
+ <parameter name="aPriority"/>
+ <parameter name="aButtons"/>
+ <parameter name="aEventCallback"/>
+ <body>
+ <![CDATA[
+ if (aPriority < this.PRIORITY_INFO_LOW ||
+ aPriority > this.PRIORITY_CRITICAL_BLOCK)
+ throw "Invalid notification priority " + aPriority;
+
+ // check for where the notification should be inserted according to
+ // priority. If two are equal, the existing one appears on top.
+ var notifications = this.allNotifications;
+ var insertPos = null;
+ for (var n = notifications.length - 1; n >= 0; n--) {
+ if (notifications[n].priority < aPriority)
+ break;
+ insertPos = notifications[n];
+ }
+
+ const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ var newitem = document.createElementNS(XULNS, "notification");
+ // Can't use instanceof in case this was created from a different document:
+ let labelIsDocFragment = aLabel && typeof aLabel == "object" && aLabel.nodeType &&
+ aLabel.nodeType == aLabel.DOCUMENT_FRAGMENT_NODE;
+ if (!labelIsDocFragment)
+ newitem.setAttribute("label", aLabel);
+ newitem.setAttribute("value", aValue);
+ if (aImage)
+ newitem.setAttribute("image", aImage);
+ newitem.eventCallback = aEventCallback;
+
+ if (aButtons) {
+ // The notification-button-default class is added to the button
+ // with isDefault set to true. If there is no such button, it is
+ // added to the first button (unless that button has isDefault
+ // set to false). There cannot be multiple default buttons.
+ var defaultElem;
+
+ for (var b = 0; b < aButtons.length; b++) {
+ var button = aButtons[b];
+ var buttonElem = document.createElementNS(XULNS, "button");
+ buttonElem.setAttribute("label", button.label);
+ if (typeof button.accessKey == "string")
+ buttonElem.setAttribute("accesskey", button.accessKey);
+ if (typeof button.type == "string") {
+ buttonElem.setAttribute("type", button.type);
+ if ((button.type == "menu-button" || button.type == "menu") &&
+ "popup" in button) {
+ buttonElem.appendChild(button.popup);
+ delete button.popup;
+ }
+ if (typeof button.anchor == "string")
+ buttonElem.setAttribute("anchor", button.anchor);
+ }
+ buttonElem.classList.add("notification-button");
+
+ if (button.isDefault ||
+ b == 0 && !("isDefault" in button))
+ defaultElem = buttonElem;
+
+ newitem.appendChild(buttonElem);
+ buttonElem.buttonInfo = button;
+ }
+
+ if (defaultElem)
+ defaultElem.classList.add("notification-button-default");
+ }
+
+ newitem.setAttribute("priority", aPriority);
+ if (aPriority >= this.PRIORITY_CRITICAL_LOW)
+ newitem.setAttribute("type", "critical");
+ else if (aPriority <= this.PRIORITY_INFO_HIGH)
+ newitem.setAttribute("type", "info");
+ else
+ newitem.setAttribute("type", "warning");
+
+ if (!insertPos) {
+ newitem.style.position = "fixed";
+ newitem.style.top = "100%";
+ newitem.style.marginTop = "-15px";
+ newitem.style.opacity = "0";
+ }
+ this.insertBefore(newitem, insertPos);
+ // Can only insert the document fragment after the item has been created because
+ // otherwise the XBL structure isn't there yet:
+ if (labelIsDocFragment) {
+ document.getAnonymousElementByAttribute(newitem, "anonid", "messageText")
+ .appendChild(aLabel);
+ }
+
+ if (!insertPos)
+ this._showNotification(newitem, true);
+
+ // Fire event for accessibility APIs
+ var event = document.createEvent("Events");
+ event.initEvent("AlertActive", true, true);
+ newitem.dispatchEvent(event);
+
+ return newitem;
+ ]]>
+ </body>
+ </method>
+
+ <method name="removeNotification">
+ <parameter name="aItem"/>
+ <parameter name="aSkipAnimation"/>
+ <body>
+ <![CDATA[
+ if (aItem == this.currentNotification)
+ this.removeCurrentNotification(aSkipAnimation);
+ else if (aItem != this._closedNotification)
+ this._removeNotificationElement(aItem);
+ return aItem;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_removeNotificationElement">
+ <parameter name="aChild"/>
+ <body>
+ <![CDATA[
+ if (aChild.eventCallback)
+ aChild.eventCallback("removed");
+ this.removeChild(aChild);
+
+ // make sure focus doesn't get lost (workaround for bug 570835)
+ let fm = Components.classes["@mozilla.org/focus-manager;1"]
+ .getService(Components.interfaces.nsIFocusManager);
+ if (!fm.getFocusedElementForWindow(window, false, {}))
+ fm.moveFocus(window, this, fm.MOVEFOCUS_FORWARD, 0);
+ ]]>
+ </body>
+ </method>
+
+ <method name="removeCurrentNotification">
+ <parameter name="aSkipAnimation"/>
+ <body>
+ <![CDATA[
+ this._showNotification(this.currentNotification, false, aSkipAnimation);
+ ]]>
+ </body>
+ </method>
+
+ <method name="removeAllNotifications">
+ <parameter name="aImmediate"/>
+ <body>
+ <![CDATA[
+ var notifications = this.allNotifications;
+ for (var n = notifications.length - 1; n >= 0; n--) {
+ if (aImmediate)
+ this._removeNotificationElement(notifications[n]);
+ else
+ this.removeNotification(notifications[n]);
+ }
+ this.currentNotification = null;
+
+ // Must clear up any currently animating notification
+ if (aImmediate)
+ this._finishAnimation();
+ ]]>
+ </body>
+ </method>
+
+ <method name="removeTransientNotifications">
+ <body>
+ <![CDATA[
+ var notifications = this.allNotifications;
+ for (var n = notifications.length - 1; n >= 0; n--) {
+ var notification = notifications[n];
+ if (notification.persistence)
+ notification.persistence--;
+ else if (Date.now() > notification.timeout)
+ this.removeNotification(notification);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="_showNotification">
+ <parameter name="aNotification"/>
+ <parameter name="aSlideIn"/>
+ <parameter name="aSkipAnimation"/>
+ <body>
+ <![CDATA[
+ this._finishAnimation();
+
+ var height = aNotification.boxObject.height;
+ var skipAnimation = aSkipAnimation || (height == 0);
+
+ if (aSlideIn) {
+ this.currentNotification = aNotification;
+ aNotification.style.removeProperty("position");
+ aNotification.style.removeProperty("top");
+ aNotification.style.removeProperty("margin-top");
+ aNotification.style.removeProperty("opacity");
+
+ if (skipAnimation) {
+ this._setBlockingState(this.currentNotification);
+ return;
+ }
+ }
+ else {
+ this._closedNotification = aNotification;
+ var notifications = this.allNotifications;
+ var idx = notifications.length - 1;
+ this.currentNotification = (idx >= 0) ? notifications[idx] : null;
+
+ if (skipAnimation) {
+ this._removeNotificationElement(this._closedNotification);
+ this._closedNotification = null;
+ this._setBlockingState(this.currentNotification);
+ return;
+ }
+
+ aNotification.style.marginTop = -height + "px";
+ aNotification.style.opacity = 0;
+ }
+
+ this._animating = true;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_finishAnimation">
+ <body><![CDATA[
+ if (this._animating) {
+ this._animating = false;
+ if (this._closedNotification) {
+ this._removeNotificationElement(this._closedNotification);
+ this._closedNotification = null;
+ }
+ this._setBlockingState(this.currentNotification);
+ }
+ ]]></body>
+ </method>
+
+ <method name="_setBlockingState">
+ <parameter name="aNotification"/>
+ <body>
+ <![CDATA[
+ var isblock = aNotification &&
+ aNotification.priority == this.PRIORITY_CRITICAL_BLOCK;
+ var canvas = this._blockingCanvas;
+ if (isblock) {
+ if (!canvas)
+ canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+ const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ let content = this.firstChild;
+ if (!content ||
+ content.namespaceURI != XULNS ||
+ content.localName != "browser")
+ return;
+
+ var width = content.boxObject.width;
+ var height = content.boxObject.height;
+ content.collapsed = true;
+
+ canvas.setAttribute("width", width);
+ canvas.setAttribute("height", height);
+ canvas.setAttribute("flex", "1");
+
+ this.appendChild(canvas);
+ this._blockingCanvas = canvas;
+
+ var bgcolor = "white";
+ try {
+ var prefService = Components.classes["@mozilla.org/preferences-service;1"].
+ getService(Components.interfaces.nsIPrefBranch);
+ bgcolor = prefService.getCharPref("browser.display.background_color");
+
+ var win = content.contentWindow;
+ var context = canvas.getContext("2d");
+ context.globalAlpha = 0.5;
+ context.drawWindow(win, win.scrollX, win.scrollY,
+ width, height, bgcolor);
+ }
+ catch (ex) { }
+ }
+ else if (canvas) {
+ canvas.parentNode.removeChild(canvas);
+ this._blockingCanvas = null;
+ let content = this.firstChild;
+ if (content)
+ content.collapsed = false;
+ }
+ ]]>
+ </body>
+ </method>
+
+ </implementation>
+
+ <handlers>
+ <handler event="transitionend"><![CDATA[
+ if (event.target.localName == "notification" &&
+ event.propertyName == "margin-top")
+ this._finishAnimation();
+ ]]></handler>
+ </handlers>
+
+ </binding>
+
+ <binding id="notification" role="xul:alert">
+ <content>
+ <xul:hbox class="notification-inner" flex="1" xbl:inherits="type">
+ <xul:hbox anonid="details" align="center" flex="1"
+ oncommand="this.parentNode.parentNode._doButtonCommand(event);">
+ <xul:image anonid="messageImage" class="messageImage" xbl:inherits="src=image,type,value"/>
+ <xul:description anonid="messageText" class="messageText" flex="1" xbl:inherits="xbl:text=label"/>
+ <xul:spacer flex="1"/>
+ <children/>
+ </xul:hbox>
+ <xul:toolbarbutton ondblclick="event.stopPropagation();"
+ class="messageCloseButton close-icon tabbable"
+ xbl:inherits="hidden=hideclose"
+ tooltiptext="&closeNotification.tooltip;"
+ oncommand="document.getBindingParent(this).dismiss();"/>
+ </xul:hbox>
+ </content>
+ <resources>
+ <stylesheet src="chrome://global/skin/notification.css"/>
+ </resources>
+ <implementation>
+ <property name="label" onset="this.setAttribute('label', val); return val;"
+ onget="return this.getAttribute('label');"/>
+ <property name="value" onset="this.setAttribute('value', val); return val;"
+ onget="return this.getAttribute('value');"/>
+ <property name="image" onset="this.setAttribute('image', val); return val;"
+ onget="return this.getAttribute('image');"/>
+ <property name="type" onset="this.setAttribute('type', val); return val;"
+ onget="return this.getAttribute('type');"/>
+ <property name="priority" onget="return parseInt(this.getAttribute('priority')) || 0;"
+ onset="this.setAttribute('priority', val); return val;"/>
+ <property name="persistence" onget="return parseInt(this.getAttribute('persistence')) || 0;"
+ onset="this.setAttribute('persistence', val); return val;"/>
+ <field name="timeout">0</field>
+
+ <property name="control" readonly="true">
+ <getter>
+ <![CDATA[
+ var parent = this.parentNode;
+ while (parent) {
+ if (parent.localName == "notificationbox")
+ return parent;
+ parent = parent.parentNode;
+ }
+ return null;
+ ]]>
+ </getter>
+ </property>
+
+ <!-- This method should only be called when the user has
+ manually closed the notification. If you want to
+ programmatically close the notification, you should
+ call close() instead. -->
+ <method name="dismiss">
+ <body>
+ <![CDATA[
+ if (this.eventCallback) {
+ this.eventCallback("dismissed");
+ }
+ this.close();
+ ]]>
+ </body>
+ </method>
+
+ <method name="close">
+ <body>
+ <![CDATA[
+ var control = this.control;
+ if (control)
+ control.removeNotification(this);
+ else
+ this.hidden = true;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_doButtonCommand">
+ <parameter name="aEvent"/>
+ <body>
+ <![CDATA[
+ if (!("buttonInfo" in aEvent.target))
+ return;
+
+ var button = aEvent.target.buttonInfo;
+ if (button.popup) {
+ document.getElementById(button.popup).
+ openPopup(aEvent.originalTarget, "after_start", 0, 0, false, false, aEvent);
+ aEvent.stopPropagation();
+ }
+ else {
+ var callback = button.callback;
+ if (callback) {
+ var result = callback(this, button, aEvent.target);
+ if (!result)
+ this.close();
+ aEvent.stopPropagation();
+ }
+ }
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="popup-notification">
+ <content>
+ <xul:vbox>
+ <xul:image class="popup-notification-icon"
+ xbl:inherits="popupid,src=icon,class=iconclass"/>
+ </xul:vbox>
+ <xul:vbox class="popup-notification-body" xbl:inherits="popupid">
+ <xul:hbox align="start">
+ <xul:vbox flex="1">
+ <xul:label class="popup-notification-origin header"
+ xbl:inherits="value=origin,tooltiptext=origin"
+ crop="center"/>
+ <xul:description class="popup-notification-description"
+ xbl:inherits="xbl:text=label,popupid"/>
+ </xul:vbox>
+ <xul:toolbarbutton anonid="closebutton"
+ class="messageCloseButton close-icon popup-notification-closebutton tabbable"
+ xbl:inherits="oncommand=closebuttoncommand"
+ tooltiptext="&closeNotification.tooltip;"/>
+ </xul:hbox>
+ <children includes="popupnotificationcontent"/>
+ <xul:label class="text-link popup-notification-learnmore-link"
+ xbl:inherits="onclick=learnmoreclick,href=learnmoreurl">&learnMore;</xul:label>
+ <xul:checkbox anonid="checkbox"
+ xbl:inherits="hidden=checkboxhidden,checked=checkboxchecked,label=checkboxlabel,oncommand=checkboxcommand" />
+ <xul:description class="popup-notification-warning" xbl:inherits="hidden=warninghidden,xbl:text=warninglabel"/>
+ <xul:spacer flex="1"/>
+ <xul:hbox class="popup-notification-button-container"
+ pack="end" align="center">
+ <children includes="button"/>
+ <xul:button anonid="button"
+ class="popup-notification-menubutton"
+ type="menu-button"
+ xbl:inherits="oncommand=buttoncommand,onpopupshown=buttonpopupshown,label=buttonlabel,accesskey=buttonaccesskey,disabled=mainactiondisabled">
+ <xul:menupopup anonid="menupopup"
+ xbl:inherits="oncommand=menucommand">
+ <children/>
+ <xul:menuitem class="menuitem-iconic popup-notification-closeitem"
+ label="&closeNotificationItem.label;"
+ xbl:inherits="oncommand=closeitemcommand,hidden=hidenotnow"/>
+ </xul:menupopup>
+ </xul:button>
+ </xul:hbox>
+ </xul:vbox>
+ </content>
+ <resources>
+ <stylesheet src="chrome://global/skin/notification.css"/>
+ </resources>
+ <implementation>
+ <field name="checkbox" readonly="true">
+ document.getAnonymousElementByAttribute(this, "anonid", "checkbox");
+ </field>
+ <field name="closebutton" readonly="true">
+ document.getAnonymousElementByAttribute(this, "anonid", "closebutton");
+ </field>
+ <field name="button" readonly="true">
+ document.getAnonymousElementByAttribute(this, "anonid", "button");
+ </field>
+ <field name="menupopup" readonly="true">
+ document.getAnonymousElementByAttribute(this, "anonid", "menupopup");
+ </field>
+ </implementation>
+ </binding>
+</bindings>
diff --git a/toolkit/content/widgets/numberbox.xml b/toolkit/content/widgets/numberbox.xml
new file mode 100644
index 000000000..0e1225f09
--- /dev/null
+++ b/toolkit/content/widgets/numberbox.xml
@@ -0,0 +1,304 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<bindings id="numberboxBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="numberbox"
+ extends="chrome://global/content/bindings/textbox.xml#textbox">
+
+ <resources>
+ <stylesheet src="chrome://global/skin/numberbox.css"/>
+ </resources>
+
+ <content>
+ <xul:hbox class="textbox-input-box numberbox-input-box" flex="1" xbl:inherits="context,disabled,focused">
+ <html:input class="numberbox-input textbox-input" anonid="input"
+ xbl:inherits="value,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey"/>
+ </xul:hbox>
+ <xul:spinbuttons anonid="buttons" xbl:inherits="disabled,hidden=hidespinbuttons"/>
+ </content>
+
+ <implementation>
+ <field name="_valueEntered">false</field>
+ <field name="_spinButtons">null</field>
+ <field name="_value">0</field>
+ <field name="decimalSymbol">"."</field>
+
+ <property name="spinButtons" readonly="true">
+ <getter>
+ <![CDATA[
+ if (!this._spinButtons)
+ this._spinButtons = document.getAnonymousElementByAttribute(this, "anonid", "buttons");
+ return this._spinButtons;
+ ]]>
+ </getter>
+ </property>
+
+ <property name="value" onget="return '' + this.valueNumber"
+ onset="return this.valueNumber = val;"/>
+
+ <property name="valueNumber">
+ <getter>
+ if (this._valueEntered) {
+ var newval = this.inputField.value;
+ newval = newval.replace(this.decimalSymbol, ".");
+ this._validateValue(newval, false);
+ }
+ return this._value;
+ </getter>
+ <setter>
+ this._validateValue(val, false);
+ return val;
+ </setter>
+ </property>
+
+ <property name="wrapAround">
+ <getter>
+ <![CDATA[
+ return (this.getAttribute('wraparound') == 'true')
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ if (val)
+ this.setAttribute('wraparound', 'true');
+ else
+ this.removeAttribute('wraparound');
+ this._enableDisableButtons();
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="min">
+ <getter>
+ var min = this.getAttribute("min");
+ return min ? Number(min) : 0;
+ </getter>
+ <setter>
+ <![CDATA[
+ if (typeof val == "number") {
+ this.setAttribute("min", val);
+ if (this.valueNumber < val)
+ this._validateValue(val, false);
+ }
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="max">
+ <getter>
+ var max = this.getAttribute("max");
+ return max ? Number(max) : Infinity;
+ </getter>
+ <setter>
+ <![CDATA[
+ if (typeof val != "number")
+ return val;
+ var min = this.min;
+ if (val < min)
+ val = min;
+ this.setAttribute("max", val);
+ if (this.valueNumber > val)
+ this._validateValue(val, false);
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="decimalPlaces">
+ <getter>
+ var places = this.getAttribute("decimalplaces");
+ return places ? Number(places) : 0;
+ </getter>
+ <setter>
+ if (typeof val == "number") {
+ this.setAttribute("decimalplaces", val);
+ this._validateValue(this.valueNumber, false);
+ }
+ return val;
+ </setter>
+ </property>
+
+ <property name="increment">
+ <getter>
+ var increment = this.getAttribute("increment");
+ return increment ? Number(increment) : 1;
+ </getter>
+ <setter>
+ <![CDATA[
+ if (typeof val == "number")
+ this.setAttribute("increment", val);
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <method name="decrease">
+ <body>
+ return this._validateValue(this.valueNumber - this.increment, true);
+ </body>
+ </method>
+
+ <method name="increase">
+ <body>
+ return this._validateValue(this.valueNumber + this.increment, true);
+ </body>
+ </method>
+
+ <method name="_modifyUp">
+ <body>
+ <![CDATA[
+ if (this.disabled || this.readOnly)
+ return;
+ var oldval = this.valueNumber;
+ var newval = this.increase();
+ this.inputField.select();
+ if (oldval != newval)
+ this._fireChange();
+ ]]>
+ </body>
+ </method>
+ <method name="_modifyDown">
+ <body>
+ <![CDATA[
+ if (this.disabled || this.readOnly)
+ return;
+ var oldval = this.valueNumber;
+ var newval = this.decrease();
+ this.inputField.select();
+ if (oldval != newval)
+ this._fireChange();
+ ]]>
+ </body>
+ </method>
+
+ <method name="_enableDisableButtons">
+ <body>
+ <![CDATA[
+ var buttons = this.spinButtons;
+ if (this.wrapAround) {
+ buttons.decreaseDisabled = buttons.increaseDisabled = false;
+ }
+ else if (this.disabled || this.readOnly) {
+ buttons.decreaseDisabled = buttons.increaseDisabled = true;
+ }
+ else {
+ buttons.decreaseDisabled = (this.valueNumber <= this.min);
+ buttons.increaseDisabled = (this.valueNumber >= this.max);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="_validateValue">
+ <parameter name="aValue"/>
+ <parameter name="aIsIncDec"/>
+ <body>
+ <![CDATA[
+ aValue = Number(aValue) || 0;
+
+ var min = this.min;
+ var max = this.max;
+ var wrapAround = this.wrapAround &&
+ min != -Infinity && max != Infinity;
+ if (aValue < min)
+ aValue = (aIsIncDec && wrapAround ? max : min);
+ else if (aValue > max)
+ aValue = (aIsIncDec && wrapAround ? min : max);
+
+ var places = this.decimalPlaces;
+ aValue = (places == Infinity) ? "" + aValue : aValue.toFixed(places);
+
+ this._valueEntered = false;
+ this._value = Number(aValue);
+ this.inputField.value = aValue.replace(/\./, this.decimalSymbol);
+
+ if (!wrapAround)
+ this._enableDisableButtons();
+
+ return aValue;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_fireChange">
+ <body>
+ var evt = document.createEvent("Events");
+ evt.initEvent("change", true, true);
+ this.dispatchEvent(evt);
+ </body>
+ </method>
+
+ <constructor><![CDATA[
+ if (this.max < this.min)
+ this.max = this.min;
+
+ var dsymbol = (Number(5.4)).toLocaleString().match(/\D/);
+ if (dsymbol != null)
+ this.decimalSymbol = dsymbol[0];
+
+ var value = this.inputField.value || 0;
+ this._validateValue(value, false);
+ ]]></constructor>
+
+ </implementation>
+
+ <handlers>
+ <handler event="input" phase="capturing">
+ this._valueEntered = true;
+ </handler>
+
+ <handler event="keypress">
+ <![CDATA[
+ if (!event.ctrlKey && !event.metaKey && !event.altKey && event.charCode) {
+ if (event.charCode == this.decimalSymbol.charCodeAt(0) &&
+ this.decimalPlaces &&
+ String(this.inputField.value).indexOf(this.decimalSymbol) == -1)
+ return;
+
+ if (event.charCode == 45 && this.min < 0)
+ return;
+
+ if (event.charCode < 48 || event.charCode > 57)
+ event.preventDefault();
+ }
+ ]]>
+ </handler>
+
+ <handler event="keypress" keycode="VK_UP">
+ this._modifyUp();
+ </handler>
+
+ <handler event="keypress" keycode="VK_DOWN">
+ this._modifyDown();
+ </handler>
+
+ <handler event="up" preventdefault="true">
+ this._modifyUp();
+ </handler>
+
+ <handler event="down" preventdefault="true">
+ this._modifyDown();
+ </handler>
+
+ <handler event="change">
+ if (event.originalTarget == this.inputField) {
+ var newval = this.inputField.value;
+ newval = newval.replace(this.decimalSymbol, ".");
+ this._validateValue(newval, false);
+ }
+ </handler>
+ </handlers>
+
+ </binding>
+
+</bindings>
diff --git a/toolkit/content/widgets/optionsDialog.xml b/toolkit/content/widgets/optionsDialog.xml
new file mode 100644
index 000000000..f0cdba62f
--- /dev/null
+++ b/toolkit/content/widgets/optionsDialog.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<bindings id="optionsDialogBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <binding id="optionsDialog"
+ extends="chrome://global/content/bindings/dialog.xml#dialog">
+ <content>
+#ifndef XP_MACOSX
+ <xul:hbox flex="1">
+ <xul:categoryBox anonid="prefsCategories">
+ <children/>
+ </xul:categoryBox>
+ <xul:vbox flex="1">
+ <xul:dialogheader id="panelHeader"/>
+ <xul:iframe anonid="panelFrame" name="panelFrame" style="width: 0px;" flex="1"/>
+ </xul:vbox>
+ </xul:hbox>
+#else
+ <xul:vbox flex="1">
+ <xul:categoryBox anonid="prefsCategories">
+ <children/>
+ </xul:categoryBox>
+ <xul:vbox flex="1">
+ <xul:iframe anonid="panelFrame" name="panelFrame" style="width: 0px;" flex="1"/>
+ </xul:vbox>
+ </xul:vbox>
+#endif
+ </content>
+
+ <implementation>
+
+
+ </implementation>
+
+ </binding>
+
+</bindings> \ No newline at end of file
diff --git a/toolkit/content/widgets/popup.xml b/toolkit/content/widgets/popup.xml
new file mode 100644
index 000000000..bb1a5eeee
--- /dev/null
+++ b/toolkit/content/widgets/popup.xml
@@ -0,0 +1,650 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<bindings id="popupBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="popup-base">
+ <resources>
+ <stylesheet src="chrome://global/skin/popup.css"/>
+ </resources>
+
+ <implementation implements="nsIDOMXULPopupElement">
+ <property name="label" onget="return this.getAttribute('label');"
+ onset="this.setAttribute('label', val); return val;"/>
+ <property name="position" onget="return this.getAttribute('position');"
+ onset="this.setAttribute('position', val); return val;"/>
+ <property name="popupBoxObject">
+ <getter>
+ return this.boxObject;
+ </getter>
+ </property>
+
+ <property name="state" readonly="true"
+ onget="return this.popupBoxObject.popupState"/>
+
+ <property name="triggerNode" readonly="true"
+ onget="return this.popupBoxObject.triggerNode"/>
+
+ <property name="anchorNode" readonly="true"
+ onget="return this.popupBoxObject.anchorNode"/>
+
+ <method name="openPopup">
+ <parameter name="aAnchorElement"/>
+ <parameter name="aPosition"/>
+ <parameter name="aX"/>
+ <parameter name="aY"/>
+ <parameter name="aIsContextMenu"/>
+ <parameter name="aAttributesOverride"/>
+ <parameter name="aTriggerEvent"/>
+ <body>
+ <![CDATA[
+ try {
+ var popupBox = this.popupBoxObject;
+ if (popupBox)
+ popupBox.openPopup(aAnchorElement, aPosition, aX, aY,
+ aIsContextMenu, aAttributesOverride, aTriggerEvent);
+ } catch (e) {}
+ ]]>
+ </body>
+ </method>
+
+ <method name="openPopupAtScreen">
+ <parameter name="aX"/>
+ <parameter name="aY"/>
+ <parameter name="aIsContextMenu"/>
+ <parameter name="aTriggerEvent"/>
+ <body>
+ <![CDATA[
+ try {
+ var popupBox = this.popupBoxObject;
+ if (popupBox)
+ popupBox.openPopupAtScreen(aX, aY, aIsContextMenu, aTriggerEvent);
+ } catch (e) {}
+ ]]>
+ </body>
+ </method>
+
+ <method name="openPopupAtScreenRect">
+ <parameter name="aPosition"/>
+ <parameter name="aX"/>
+ <parameter name="aY"/>
+ <parameter name="aWidth"/>
+ <parameter name="aHeight"/>
+ <parameter name="aIsContextMenu"/>
+ <parameter name="aAttributesOverride"/>
+ <parameter name="aTriggerEvent"/>
+ <body>
+ <![CDATA[
+ try {
+ var popupBox = this.popupBoxObject;
+ if (popupBox)
+ popupBox.openPopupAtScreenRect(aPosition, aX, aY, aWidth, aHeight,
+ aIsContextMenu, aAttributesOverride, aTriggerEvent);
+ } catch (e) {}
+ ]]>
+ </body>
+ </method>
+
+ <method name="showPopup">
+ <parameter name="element"/>
+ <parameter name="xpos"/>
+ <parameter name="ypos"/>
+ <parameter name="popuptype"/>
+ <parameter name="anchoralignment"/>
+ <parameter name="popupalignment"/>
+ <body>
+ <![CDATA[
+ var popupBox = null;
+ var menuBox = null;
+ try {
+ popupBox = this.popupBoxObject;
+ } catch (e) {}
+ try {
+ menuBox = this.parentNode.boxObject;
+ } catch (e) {}
+ if (menuBox instanceof MenuBoxObject)
+ menuBox.openMenu(true);
+ else if (popupBox)
+ popupBox.showPopup(element, this, xpos, ypos, popuptype, anchoralignment, popupalignment);
+ ]]>
+ </body>
+ </method>
+
+ <method name="hidePopup">
+ <parameter name="cancel"/>
+ <body>
+ <![CDATA[
+ var popupBox = null;
+ var menuBox = null;
+ try {
+ popupBox = this.popupBoxObject;
+ } catch (e) {}
+ try {
+ menuBox = this.parentNode.boxObject;
+ } catch (e) {}
+ if (menuBox instanceof MenuBoxObject)
+ menuBox.openMenu(false);
+ else if (popupBox instanceof PopupBoxObject)
+ popupBox.hidePopup(cancel);
+ ]]>
+ </body>
+ </method>
+
+ <property name="autoPosition">
+ <getter>
+ <![CDATA[
+ return this.popupBoxObject.autoPosition;
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ return this.popupBoxObject.autoPosition = val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="alignmentPosition" readonly="true">
+ <getter>
+ <![CDATA[
+ return this.popupBoxObject.alignmentPosition;
+ ]]>
+ </getter>
+ </property>
+
+ <property name="alignmentOffset" readonly="true">
+ <getter>
+ <![CDATA[
+ return this.popupBoxObject.alignmentOffset;
+ ]]>
+ </getter>
+ </property>
+
+ <method name="enableKeyboardNavigator">
+ <parameter name="aEnableKeyboardNavigator"/>
+ <body>
+ <![CDATA[
+ this.popupBoxObject.enableKeyboardNavigator(aEnableKeyboardNavigator);
+ ]]>
+ </body>
+ </method>
+
+ <method name="enableRollup">
+ <parameter name="aEnableRollup"/>
+ <body>
+ <![CDATA[
+ this.popupBoxObject.enableRollup(aEnableRollup);
+ ]]>
+ </body>
+ </method>
+
+ <method name="sizeTo">
+ <parameter name="aWidth"/>
+ <parameter name="aHeight"/>
+ <body>
+ <![CDATA[
+ this.popupBoxObject.sizeTo(aWidth, aHeight);
+ ]]>
+ </body>
+ </method>
+
+ <method name="moveTo">
+ <parameter name="aLeft"/>
+ <parameter name="aTop"/>
+ <body>
+ <![CDATA[
+ this.popupBoxObject.moveTo(aLeft, aTop);
+ ]]>
+ </body>
+ </method>
+
+ <method name="moveToAnchor">
+ <parameter name="aAnchorElement"/>
+ <parameter name="aPosition"/>
+ <parameter name="aX"/>
+ <parameter name="aY"/>
+ <parameter name="aAttributesOverride"/>
+ <body>
+ <![CDATA[
+ this.popupBoxObject.moveToAnchor(aAnchorElement, aPosition, aX, aY, aAttributesOverride);
+ ]]>
+ </body>
+ </method>
+
+ <method name="getOuterScreenRect">
+ <body>
+ <![CDATA[
+ return this.popupBoxObject.getOuterScreenRect();
+ ]]>
+ </body>
+ </method>
+
+ <method name="setConstraintRect">
+ <parameter name="aRect"/>
+ <body>
+ <![CDATA[
+ this.popupBoxObject.setConstraintRect(aRect);
+ ]]>
+ </body>
+ </method>
+ </implementation>
+
+ </binding>
+
+ <binding id="popup" role="xul:menupopup"
+ extends="chrome://global/content/bindings/popup.xml#popup-base">
+
+ <content>
+ <xul:arrowscrollbox class="popup-internal-box" flex="1" orient="vertical"
+ smoothscroll="false">
+ <children/>
+ </xul:arrowscrollbox>
+ </content>
+
+ <handlers>
+ <handler event="popupshowing" phase="target">
+ <![CDATA[
+ var array = [];
+ var width = 0;
+ for (var menuitem = this.firstChild; menuitem; menuitem = menuitem.nextSibling) {
+ if (menuitem.localName == "menuitem" && menuitem.hasAttribute("acceltext")) {
+ var accel = document.getAnonymousElementByAttribute(menuitem, "anonid", "accel");
+ if (accel && accel.boxObject) {
+ array.push(accel);
+ if (accel.boxObject.width > width)
+ width = accel.boxObject.width;
+ }
+ }
+ }
+ for (var i = 0; i < array.length; i++)
+ array[i].width = width;
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="panel" role="xul:panel"
+ extends="chrome://global/content/bindings/popup.xml#popup-base">
+ <implementation implements="nsIDOMXULPopupElement">
+ <field name="_prevFocus">0</field>
+ <field name="_dragBindingAlive">true</field>
+ <constructor>
+ <![CDATA[
+ if (this.getAttribute("backdrag") == "true" && !this._draggableStarted) {
+ this._draggableStarted = true;
+ try {
+ let tmp = {};
+ Components.utils.import("resource://gre/modules/WindowDraggingUtils.jsm", tmp);
+ let draghandle = new tmp.WindowDraggingElement(this);
+ draghandle.mouseDownCheck = function () {
+ return this._dragBindingAlive;
+ }
+ } catch (e) {}
+ }
+ ]]>
+ </constructor>
+ </implementation>
+
+ <handlers>
+ <handler event="popupshowing"><![CDATA[
+ // Capture the previous focus before has a chance to get set inside the panel
+ try {
+ this._prevFocus = Components.utils
+ .getWeakReference(document.commandDispatcher.focusedElement);
+ if (this._prevFocus.get())
+ return;
+ } catch (ex) { }
+
+ this._prevFocus = Components.utils.getWeakReference(document.activeElement);
+ ]]></handler>
+ <handler event="popupshown"><![CDATA[
+ // Fire event for accessibility APIs
+ var alertEvent = document.createEvent("Events");
+ alertEvent.initEvent("AlertActive", true, true);
+ this.dispatchEvent(alertEvent);
+ ]]></handler>
+ <handler event="popuphiding"><![CDATA[
+ try {
+ this._currentFocus = document.commandDispatcher.focusedElement;
+ } catch (e) {
+ this._currentFocus = document.activeElement;
+ }
+ ]]></handler>
+ <handler event="popuphidden"><![CDATA[
+ function doFocus() {
+ // Focus was set on an element inside this panel,
+ // so we need to move it back to where it was previously
+ try {
+ let fm = Components.classes["@mozilla.org/focus-manager;1"]
+ .getService(Components.interfaces.nsIFocusManager);
+ fm.setFocus(prevFocus, fm.FLAG_NOSCROLL);
+ } catch (e) {
+ prevFocus.focus();
+ }
+ }
+ var currentFocus = this._currentFocus;
+ var prevFocus = this._prevFocus ? this._prevFocus.get() : null;
+ this._currentFocus = null;
+ this._prevFocus = null;
+
+ // Avoid changing focus if focus changed while we hide the popup
+ // (This can happen e.g. if the popup is hiding as a result of a
+ // click/keypress that focused something)
+ let nowFocus;
+ try {
+ nowFocus = document.commandDispatcher.focusedElement;
+ } catch (e) {
+ nowFocus = document.activeElement;
+ }
+ if (nowFocus && nowFocus != currentFocus)
+ return;
+
+ if (prevFocus && this.getAttribute("norestorefocus") != "true") {
+ // Try to restore focus
+ try {
+ if (document.commandDispatcher.focusedWindow != window)
+ return; // Focus has already been set to a window outside of this panel
+ } catch (ex) {}
+
+ if (!currentFocus) {
+ doFocus();
+ return;
+ }
+ while (currentFocus) {
+ if (currentFocus == this) {
+ doFocus();
+ return;
+ }
+ currentFocus = currentFocus.parentNode;
+ }
+ }
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="arrowpanel" extends="chrome://global/content/bindings/popup.xml#panel">
+ <content flip="both" side="top" position="bottomcenter topleft" consumeoutsideclicks="false">
+ <xul:vbox anonid="container" class="panel-arrowcontainer" flex="1"
+ xbl:inherits="side,panelopen">
+ <xul:box anonid="arrowbox" class="panel-arrowbox">
+ <xul:image anonid="arrow" class="panel-arrow" xbl:inherits="side"/>
+ </xul:box>
+ <xul:box class="panel-arrowcontent" xbl:inherits="side,align,dir,orient,pack" flex="1">
+ <children/>
+ </xul:box>
+ </xul:vbox>
+ </content>
+ <implementation>
+ <field name="_fadeTimer">null</field>
+ <method name="sizeTo">
+ <parameter name="aWidth"/>
+ <parameter name="aHeight"/>
+ <body>
+ <![CDATA[
+ this.popupBoxObject.sizeTo(aWidth, aHeight);
+ if (this.state == "open") {
+ this.adjustArrowPosition();
+ }
+ ]]>
+ </body>
+ </method>
+ <method name="moveToAnchor">
+ <parameter name="aAnchorElement"/>
+ <parameter name="aPosition"/>
+ <parameter name="aX"/>
+ <parameter name="aY"/>
+ <parameter name="aAttributesOverride"/>
+ <body>
+ <![CDATA[
+ this.popupBoxObject.moveToAnchor(aAnchorElement, aPosition, aX, aY, aAttributesOverride);
+ ]]>
+ </body>
+ </method>
+ <method name="adjustArrowPosition">
+ <body>
+ <![CDATA[
+ var arrow = document.getAnonymousElementByAttribute(this, "anonid", "arrow");
+
+ var anchor = this.anchorNode;
+ if (!anchor) {
+ return;
+ }
+
+ var container = document.getAnonymousElementByAttribute(this, "anonid", "container");
+ var arrowbox = document.getAnonymousElementByAttribute(this, "anonid", "arrowbox");
+
+ var position = this.alignmentPosition;
+ var offset = this.alignmentOffset;
+
+ this.setAttribute("arrowposition", position);
+
+ if (position.indexOf("start_") == 0 || position.indexOf("end_") == 0) {
+ container.orient = "horizontal";
+ arrowbox.orient = "vertical";
+ if (position.indexOf("_after") > 0) {
+ arrowbox.pack = "end";
+ } else {
+ arrowbox.pack = "start";
+ }
+ arrowbox.style.transform = "translate(0, " + -offset + "px)";
+
+ // The assigned side stays the same regardless of direction.
+ var isRTL = (window.getComputedStyle(this).direction == "rtl");
+
+ if (position.indexOf("start_") == 0) {
+ container.dir = "reverse";
+ this.setAttribute("side", isRTL ? "left" : "right");
+ }
+ else {
+ container.dir = "";
+ this.setAttribute("side", isRTL ? "right" : "left");
+ }
+ }
+ else if (position.indexOf("before_") == 0 || position.indexOf("after_") == 0) {
+ container.orient = "";
+ arrowbox.orient = "";
+ if (position.indexOf("_end") > 0) {
+ arrowbox.pack = "end";
+ } else {
+ arrowbox.pack = "start";
+ }
+ arrowbox.style.transform = "translate(" + -offset + "px, 0)";
+
+ if (position.indexOf("before_") == 0) {
+ container.dir = "reverse";
+ this.setAttribute("side", "bottom");
+ }
+ else {
+ container.dir = "";
+ this.setAttribute("side", "top");
+ }
+ }
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ <handlers>
+ <handler event="popupshowing" phase="target">
+ <![CDATA[
+ var arrow = document.getAnonymousElementByAttribute(this, "anonid", "arrow");
+ arrow.hidden = this.anchorNode == null;
+ document.getAnonymousElementByAttribute(this, "anonid", "arrowbox")
+ .style.removeProperty("transform");
+
+ this.adjustArrowPosition();
+
+ if (this.getAttribute("animate") != "false") {
+ this.setAttribute("animate", "open");
+ }
+
+ // set fading
+ var fade = this.getAttribute("fade");
+ var fadeDelay = 0;
+ if (fade == "fast") {
+ fadeDelay = 1;
+ }
+ else if (fade == "slow") {
+ fadeDelay = 4000;
+ }
+ else {
+ return;
+ }
+
+ this._fadeTimer = setTimeout(() => this.hidePopup(true), fadeDelay, this);
+ ]]>
+ </handler>
+ <handler event="popuphiding" phase="target">
+ let animate = (this.getAttribute("animate") != "false");
+
+ if (this._fadeTimer) {
+ clearTimeout(this._fadeTimer);
+ if (animate) {
+ this.setAttribute("animate", "fade");
+ }
+ }
+ else if (animate) {
+ this.setAttribute("animate", "cancel");
+ }
+ </handler>
+ <handler event="popupshown" phase="target">
+ this.setAttribute("panelopen", "true");
+ </handler>
+ <handler event="popuphidden" phase="target">
+ this.removeAttribute("panelopen");
+ if (this.getAttribute("animate") != "false") {
+ this.removeAttribute("animate");
+ }
+ </handler>
+ <handler event="popuppositioned" phase="target">
+ this.adjustArrowPosition();
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="tooltip" role="xul:tooltip"
+ extends="chrome://global/content/bindings/popup.xml#popup-base">
+ <content>
+ <children>
+ <xul:label class="tooltip-label" xbl:inherits="xbl:text=label" flex="1"/>
+ </children>
+ </content>
+
+ <implementation>
+ <field name="_mouseOutCount">0</field>
+ <field name="_isMouseOver">false</field>
+
+ <property name="label"
+ onget="return this.getAttribute('label');"
+ onset="this.setAttribute('label', val); return val;"/>
+
+ <property name="page" onset="if (val) this.setAttribute('page', 'true');
+ else this.removeAttribute('page');
+ return val;"
+ onget="return this.getAttribute('page') == 'true';"/>
+ <property name="textProvider"
+ readonly="true">
+ <getter>
+ <![CDATA[
+ if (!this._textProvider) {
+ this._textProvider = Components.classes["@mozilla.org/embedcomp/default-tooltiptextprovider;1"]
+ .getService(Components.interfaces.nsITooltipTextProvider);
+ }
+ return this._textProvider;
+ ]]>
+ </getter>
+ </property>
+
+ <!-- Given the supplied element within a page, set the tooltip's text to the text
+ for that element. Returns true if text was assigned, and false if the no text
+ is set, which normally would be used to cancel tooltip display.
+ -->
+ <method name="fillInPageTooltip">
+ <parameter name="tipElement"/>
+ <body>
+ <![CDATA[
+ let tttp = this.textProvider;
+ let textObj = {}, dirObj = {};
+ let shouldChangeText = tttp.getNodeText(tipElement, textObj, dirObj);
+ if (shouldChangeText) {
+ this.style.direction = dirObj.value;
+ this.label = textObj.value;
+ }
+ return shouldChangeText;
+ ]]>
+ </body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="mouseover"><![CDATA[
+ var rel = event.relatedTarget;
+ if (!rel)
+ return;
+
+ // find out if the node we entered from is one of our anonymous children
+ while (rel) {
+ if (rel == this)
+ break;
+ rel = rel.parentNode;
+ }
+
+ // if the exited node is not a descendant of ours, we are entering for the first time
+ if (rel != this)
+ this._isMouseOver = true;
+ ]]></handler>
+
+ <handler event="mouseout"><![CDATA[
+ var rel = event.relatedTarget;
+
+ // relatedTarget is null when the titletip is first shown: a mouseout event fires
+ // because the mouse is exiting the main window and entering the titletip "window".
+ // relatedTarget is also null when the mouse exits the main window completely,
+ // so count how many times relatedTarget was null after titletip is first shown
+ // and hide popup the 2nd time
+ if (!rel) {
+ ++this._mouseOutCount;
+ if (this._mouseOutCount > 1)
+ this.hidePopup();
+ return;
+ }
+
+ // find out if the node we are entering is one of our anonymous children
+ while (rel) {
+ if (rel == this)
+ break;
+ rel = rel.parentNode;
+ }
+
+ // if the entered node is not a descendant of ours, hide the tooltip
+ if (rel != this && this._isMouseOver) {
+ this.hidePopup();
+ }
+ ]]></handler>
+
+ <handler event="popupshowing"><![CDATA[
+ if (this.page && !this.fillInPageTooltip(this.triggerNode)) {
+ event.preventDefault();
+ }
+ ]]></handler>
+
+ <handler event="popuphiding"><![CDATA[
+ this._isMouseOver = false;
+ this._mouseOutCount = 0;
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="popup-scrollbars" extends="chrome://global/content/bindings/popup.xml#popup">
+ <content>
+ <xul:hbox class="popup-internal-box" flex="1" orient="vertical" style="overflow: auto;">
+ <children/>
+ </xul:hbox>
+ </content>
+ </binding>
+
+</bindings>
diff --git a/toolkit/content/widgets/preferences.xml b/toolkit/content/widgets/preferences.xml
new file mode 100644
index 000000000..9fb2ac7ea
--- /dev/null
+++ b/toolkit/content/widgets/preferences.xml
@@ -0,0 +1,1411 @@
+<?xml version="1.0"?>
+
+<!DOCTYPE bindings [
+ <!ENTITY % preferencesDTD SYSTEM "chrome://global/locale/preferences.dtd">
+ %preferencesDTD;
+ <!ENTITY % globalKeysDTD SYSTEM "chrome://global/locale/globalKeys.dtd">
+ %globalKeysDTD;
+]>
+
+<bindings id="preferencesBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xbl="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+#
+# = Preferences Window Framework
+#
+# The syntax for use looks something like:
+#
+# <prefwindow>
+# <prefpane id="prefPaneA">
+# <preferences>
+# <preference id="preference1" name="app.preference1" type="bool" onchange="foo();"/>
+# <preference id="preference2" name="app.preference2" type="bool" useDefault="true"/>
+# </preferences>
+# <checkbox label="Preference" preference="preference1"/>
+# </prefpane>
+# </prefwindow>
+#
+
+ <binding id="preferences">
+ <implementation implements="nsIObserver">
+ <method name="_constructAfterChildren">
+ <body>
+ <![CDATA[
+ // This method will be called after each one of the child
+ // <preference> elements is constructed. Its purpose is to propagate
+ // the values to the associated form elements
+
+ var elements = this.getElementsByTagName("preference");
+ for (let element of elements) {
+ if (!element._constructed) {
+ return;
+ }
+ }
+ for (let element of elements) {
+ element.updateElements();
+ }
+ ]]>
+ </body>
+ </method>
+ <method name="observe">
+ <parameter name="aSubject"/>
+ <parameter name="aTopic"/>
+ <parameter name="aData"/>
+ <body>
+ <![CDATA[
+ for (var i = 0; i < this.childNodes.length; ++i) {
+ var preference = this.childNodes[i];
+ if (preference.name == aData) {
+ preference.value = preference.valueFromPreferences;
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="fireChangedEvent">
+ <parameter name="aPreference"/>
+ <body>
+ <![CDATA[
+ // Value changed, synthesize an event
+ try {
+ var event = document.createEvent("Events");
+ event.initEvent("change", true, true);
+ aPreference.dispatchEvent(event);
+ }
+ catch (e) {
+ Components.utils.reportError(e);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <field name="service">
+ Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefService);
+ </field>
+ <field name="rootBranch">
+ Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch);
+ </field>
+ <field name="defaultBranch">
+ this.service.getDefaultBranch("");
+ </field>
+ <field name="rootBranchInternal">
+ Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranchInternal);
+ </field>
+ <property name="type" readonly="true">
+ <getter>
+ <![CDATA[
+ return document.documentElement.type || "";
+ ]]>
+ </getter>
+ </property>
+ <property name="instantApply" readonly="true">
+ <getter>
+ <![CDATA[
+ var doc = document.documentElement;
+ return this.type == "child" ? doc.instantApply
+ : doc.instantApply || this.rootBranch.getBoolPref("browser.preferences.instantApply");
+ ]]>
+ </getter>
+ </property>
+ </implementation>
+ </binding>
+
+ <binding id="preference">
+ <implementation>
+ <constructor>
+ <![CDATA[
+ this._constructed = true;
+
+ // if the element has been inserted without the name attribute set,
+ // we have nothing to do here
+ if (!this.name)
+ return;
+
+ this.preferences.rootBranchInternal
+ .addObserver(this.name, this.preferences, false);
+ // In non-instant apply mode, we must try and use the last saved state
+ // from any previous opens of a child dialog instead of the value from
+ // preferences, to pick up any edits a user may have made.
+
+ var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
+ .getService(Components.interfaces.nsIScriptSecurityManager);
+ if (this.preferences.type == "child" &&
+ !this.instantApply && window.opener &&
+ secMan.isSystemPrincipal(window.opener.document.nodePrincipal)) {
+ var pdoc = window.opener.document;
+
+ // Try to find a preference element for the same preference.
+ var preference = null;
+ var parentPreferences = pdoc.getElementsByTagName("preferences");
+ for (var k = 0; (k < parentPreferences.length && !preference); ++k) {
+ var parentPrefs = parentPreferences[k]
+ .getElementsByAttribute("name", this.name);
+ for (var l = 0; (l < parentPrefs.length && !preference); ++l) {
+ if (parentPrefs[l].localName == "preference")
+ preference = parentPrefs[l];
+ }
+ }
+
+ // Don't use the value setter here, we don't want updateElements to be prematurely fired.
+ this._value = preference ? preference.value : this.valueFromPreferences;
+ }
+ else
+ this._value = this.valueFromPreferences;
+ this.preferences._constructAfterChildren();
+ ]]>
+ </constructor>
+ <destructor>
+ this.preferences.rootBranchInternal
+ .removeObserver(this.name, this.preferences);
+ </destructor>
+ <field name="_constructed">false</field>
+ <property name="instantApply">
+ <getter>
+ if (this.getAttribute("instantApply") == "false")
+ return false;
+ return this.getAttribute("instantApply") == "true" || this.preferences.instantApply;
+ </getter>
+ </property>
+
+ <property name="preferences" onget="return this.parentNode"/>
+ <property name="name" onget="return this.getAttribute('name');">
+ <setter>
+ if (val == this.name)
+ return val;
+
+ this.preferences.rootBranchInternal
+ .removeObserver(this.name, this.preferences);
+ this.setAttribute('name', val);
+ this.preferences.rootBranchInternal
+ .addObserver(val, this.preferences, false);
+
+ return val;
+ </setter>
+ </property>
+ <property name="type" onget="return this.getAttribute('type');"
+ onset="this.setAttribute('type', val); return val;"/>
+ <property name="inverted" onget="return this.getAttribute('inverted') == 'true';"
+ onset="this.setAttribute('inverted', val); return val;"/>
+ <property name="readonly" onget="return this.getAttribute('readonly') == 'true';"
+ onset="this.setAttribute('readonly', val); return val;"/>
+
+ <field name="_value">null</field>
+ <method name="_setValue">
+ <parameter name="aValue"/>
+ <body>
+ <![CDATA[
+ if (this.value !== aValue) {
+ this._value = aValue;
+ if (this.instantApply)
+ this.valueFromPreferences = aValue;
+ this.preferences.fireChangedEvent(this);
+ }
+ return aValue;
+ ]]>
+ </body>
+ </method>
+ <property name="value" onget="return this._value" onset="return this._setValue(val);"/>
+
+ <property name="locked">
+ <getter>
+ return this.preferences.rootBranch.prefIsLocked(this.name);
+ </getter>
+ </property>
+
+ <property name="disabled">
+ <getter>
+ return this.getAttribute("disabled") == "true";
+ </getter>
+ <setter>
+ <![CDATA[
+ if (val)
+ this.setAttribute("disabled", "true");
+ else
+ this.removeAttribute("disabled");
+
+ if (!this.id)
+ return val;
+
+ var elements = document.getElementsByAttribute("preference", this.id);
+ for (var i = 0; i < elements.length; ++i) {
+ elements[i].disabled = val;
+
+ var labels = document.getElementsByAttribute("control", elements[i].id);
+ for (var j = 0; j < labels.length; ++j)
+ labels[j].disabled = val;
+ }
+
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="tabIndex">
+ <getter>
+ return parseInt(this.getAttribute("tabindex"));
+ </getter>
+ <setter>
+ <![CDATA[
+ if (val)
+ this.setAttribute("tabindex", val);
+ else
+ this.removeAttribute("tabindex");
+
+ if (!this.id)
+ return val;
+
+ var elements = document.getElementsByAttribute("preference", this.id);
+ for (var i = 0; i < elements.length; ++i) {
+ elements[i].tabIndex = val;
+
+ var labels = document.getElementsByAttribute("control", elements[i].id);
+ for (var j = 0; j < labels.length; ++j)
+ labels[j].tabIndex = val;
+ }
+
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="hasUserValue">
+ <getter>
+ <![CDATA[
+ return this.preferences.rootBranch.prefHasUserValue(this.name) &&
+ this.value !== undefined;
+ ]]>
+ </getter>
+ </property>
+
+ <method name="reset">
+ <body>
+ // defer reset until preference update
+ this.value = undefined;
+ </body>
+ </method>
+
+ <field name="_useDefault">false</field>
+ <property name="defaultValue">
+ <getter>
+ <![CDATA[
+ this._useDefault = true;
+ var val = this.valueFromPreferences;
+ this._useDefault = false;
+ return val;
+ ]]>
+ </getter>
+ </property>
+
+ <property name="_branch">
+ <getter>
+ return this._useDefault ? this.preferences.defaultBranch : this.preferences.rootBranch;
+ </getter>
+ </property>
+
+ <field name="batching">false</field>
+
+ <method name="_reportUnknownType">
+ <body>
+ <![CDATA[
+ var consoleService = Components.classes["@mozilla.org/consoleservice;1"]
+ .getService(Components.interfaces.nsIConsoleService);
+ var msg = "<preference> with id='" + this.id + "' and name='" +
+ this.name + "' has unknown type '" + this.type + "'.";
+ consoleService.logStringMessage(msg);
+ ]]>
+ </body>
+ </method>
+
+ <property name="valueFromPreferences">
+ <getter>
+ <![CDATA[
+ try {
+ // Force a resync of value with preferences.
+ switch (this.type) {
+ case "int":
+ return this._branch.getIntPref(this.name);
+ case "bool":
+ var val = this._branch.getBoolPref(this.name);
+ return this.inverted ? !val : val;
+ case "wstring":
+ return this._branch
+ .getComplexValue(this.name, Components.interfaces.nsIPrefLocalizedString)
+ .data;
+ case "string":
+ case "unichar":
+ return this._branch
+ .getComplexValue(this.name, Components.interfaces.nsISupportsString)
+ .data;
+ case "fontname":
+ var family = this._branch
+ .getComplexValue(this.name, Components.interfaces.nsISupportsString)
+ .data;
+ var fontEnumerator = Components.classes["@mozilla.org/gfx/fontenumerator;1"]
+ .createInstance(Components.interfaces.nsIFontEnumerator);
+ return fontEnumerator.getStandardFamilyName(family);
+ case "file":
+ var f = this._branch
+ .getComplexValue(this.name, Components.interfaces.nsILocalFile);
+ return f;
+ default:
+ this._reportUnknownType();
+ }
+ }
+ catch (e) { }
+ return null;
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ // Exit early if nothing to do.
+ if (this.readonly || this.valueFromPreferences == val)
+ return val;
+
+ // The special value undefined means 'reset preference to default'.
+ if (val === undefined) {
+ this.preferences.rootBranch.clearUserPref(this.name);
+ return val;
+ }
+
+ // Force a resync of preferences with value.
+ switch (this.type) {
+ case "int":
+ this.preferences.rootBranch.setIntPref(this.name, val);
+ break;
+ case "bool":
+ this.preferences.rootBranch.setBoolPref(this.name, this.inverted ? !val : val);
+ break;
+ case "wstring":
+ var pls = Components.classes["@mozilla.org/pref-localizedstring;1"]
+ .createInstance(Components.interfaces.nsIPrefLocalizedString);
+ pls.data = val;
+ this.preferences.rootBranch
+ .setComplexValue(this.name, Components.interfaces.nsIPrefLocalizedString, pls);
+ break;
+ case "string":
+ case "unichar":
+ case "fontname":
+ var iss = Components.classes["@mozilla.org/supports-string;1"]
+ .createInstance(Components.interfaces.nsISupportsString);
+ iss.data = val;
+ this.preferences.rootBranch
+ .setComplexValue(this.name, Components.interfaces.nsISupportsString, iss);
+ break;
+ case "file":
+ var lf;
+ if (typeof(val) == "string") {
+ lf = Components.classes["@mozilla.org/file/local;1"]
+ .createInstance(Components.interfaces.nsILocalFile);
+ lf.persistentDescriptor = val;
+ if (!lf.exists())
+ lf.initWithPath(val);
+ }
+ else
+ lf = val.QueryInterface(Components.interfaces.nsILocalFile);
+ this.preferences.rootBranch
+ .setComplexValue(this.name, Components.interfaces.nsILocalFile, lf);
+ break;
+ default:
+ this._reportUnknownType();
+ }
+ if (!this.batching)
+ this.preferences.service.savePrefFile(null);
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <method name="setElementValue">
+ <parameter name="aElement"/>
+ <body>
+ <![CDATA[
+ if (this.locked)
+ aElement.disabled = true;
+
+ if (!this.isElementEditable(aElement))
+ return;
+
+ var rv = undefined;
+ if (aElement.hasAttribute("onsyncfrompreference")) {
+ // Value changed, synthesize an event
+ try {
+ var event = document.createEvent("Events");
+ event.initEvent("syncfrompreference", true, true);
+ var f = new Function ("event",
+ aElement.getAttribute("onsyncfrompreference"));
+ rv = f.call(aElement, event);
+ }
+ catch (e) {
+ Components.utils.reportError(e);
+ }
+ }
+ var val = rv;
+ if (val === undefined)
+ val = this.instantApply ? this.valueFromPreferences : this.value;
+ // if the preference is marked for reset, show default value in UI
+ if (val === undefined)
+ val = this.defaultValue;
+
+ /**
+ * Initialize a UI element property with a value. Handles the case
+ * where an element has not yet had a XBL binding attached for it and
+ * the property setter does not yet exist by setting the same attribute
+ * on the XUL element using DOM apis and assuming the element's
+ * constructor or property getters appropriately handle this state.
+ */
+ function setValue(element, attribute, value) {
+ if (attribute in element)
+ element[attribute] = value;
+ else
+ element.setAttribute(attribute, value);
+ }
+ if (aElement.localName == "checkbox" ||
+ aElement.localName == "listitem")
+ setValue(aElement, "checked", val);
+ else if (aElement.localName == "colorpicker")
+ setValue(aElement, "color", val);
+ else if (aElement.localName == "textbox") {
+ // XXXmano Bug 303998: Avoid a caret placement issue if either the
+ // preference observer or its setter calls updateElements as a result
+ // of the input event handler.
+ if (aElement.value !== val)
+ setValue(aElement, "value", val);
+ }
+ else
+ setValue(aElement, "value", val);
+ ]]>
+ </body>
+ </method>
+
+ <method name="getElementValue">
+ <parameter name="aElement"/>
+ <body>
+ <![CDATA[
+ if (aElement.hasAttribute("onsynctopreference")) {
+ // Value changed, synthesize an event
+ try {
+ var event = document.createEvent("Events");
+ event.initEvent("synctopreference", true, true);
+ var f = new Function ("event",
+ aElement.getAttribute("onsynctopreference"));
+ var rv = f.call(aElement, event);
+ if (rv !== undefined)
+ return rv;
+ }
+ catch (e) {
+ Components.utils.reportError(e);
+ }
+ }
+
+ /**
+ * Read the value of an attribute from an element, assuming the
+ * attribute is a property on the element's node API. If the property
+ * is not present in the API, then assume its value is contained in
+ * an attribute, as is the case before a binding has been attached.
+ */
+ function getValue(element, attribute) {
+ if (attribute in element)
+ return element[attribute];
+ return element.getAttribute(attribute);
+ }
+ if (aElement.localName == "checkbox" ||
+ aElement.localName == "listitem")
+ var value = getValue(aElement, "checked");
+ else if (aElement.localName == "colorpicker")
+ value = getValue(aElement, "color");
+ else
+ value = getValue(aElement, "value");
+
+ switch (this.type) {
+ case "int":
+ return parseInt(value, 10) || 0;
+ case "bool":
+ return typeof(value) == "boolean" ? value : value == "true";
+ }
+ return value;
+ ]]>
+ </body>
+ </method>
+
+ <method name="isElementEditable">
+ <parameter name="aElement"/>
+ <body>
+ <![CDATA[
+ switch (aElement.localName) {
+ case "checkbox":
+ case "colorpicker":
+ case "radiogroup":
+ case "textbox":
+ case "listitem":
+ case "listbox":
+ case "menulist":
+ return true;
+ }
+ return aElement.getAttribute("preference-editable") == "true";
+ ]]>
+ </body>
+ </method>
+
+ <method name="updateElements">
+ <body>
+ <![CDATA[
+ if (!this.id)
+ return;
+
+ // This "change" event handler tracks changes made to preferences by
+ // sources other than the user in this window.
+ var elements = document.getElementsByAttribute("preference", this.id);
+ for (var i = 0; i < elements.length; ++i)
+ this.setElementValue(elements[i]);
+ ]]>
+ </body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="change">
+ this.updateElements();
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="prefwindow"
+ extends="chrome://global/content/bindings/dialog.xml#dialog">
+ <resources>
+ <stylesheet src="chrome://global/skin/preferences.css"/>
+ </resources>
+ <content dlgbuttons="accept,cancel" persist="lastSelected screenX screenY"
+ closebuttonlabel="&preferencesCloseButton.label;"
+ closebuttonaccesskey="&preferencesCloseButton.accesskey;"
+ role="dialog"
+#ifdef XP_WIN
+ title="&preferencesDefaultTitleWin.title;">
+#else
+ title="&preferencesDefaultTitleMac.title;">
+#endif
+ <xul:windowdragbox orient="vertical">
+ <xul:radiogroup anonid="selector" orient="horizontal" class="paneSelector chromeclass-toolbar"
+ role="listbox"/> <!-- Expose to accessibility APIs as a listbox -->
+ </xul:windowdragbox>
+ <xul:hbox flex="1" class="paneDeckContainer">
+ <xul:deck anonid="paneDeck" flex="1">
+ <children includes="prefpane"/>
+ </xul:deck>
+ </xul:hbox>
+ <xul:hbox anonid="dlg-buttons" class="prefWindow-dlgbuttons" pack="end">
+#ifdef XP_UNIX
+ <xul:button dlgtype="disclosure" class="dialog-button" hidden="true"/>
+ <xul:button dlgtype="help" class="dialog-button" hidden="true" icon="help"/>
+ <xul:button dlgtype="extra2" class="dialog-button" hidden="true"/>
+ <xul:button dlgtype="extra1" class="dialog-button" hidden="true"/>
+ <xul:spacer anonid="spacer" flex="1"/>
+ <xul:button dlgtype="cancel" class="dialog-button" icon="cancel"/>
+ <xul:button dlgtype="accept" class="dialog-button" icon="accept"/>
+#else
+ <xul:button dlgtype="extra2" class="dialog-button" hidden="true"/>
+ <xul:spacer anonid="spacer" flex="1"/>
+ <xul:button dlgtype="accept" class="dialog-button" icon="accept"/>
+ <xul:button dlgtype="extra1" class="dialog-button" hidden="true"/>
+ <xul:button dlgtype="cancel" class="dialog-button" icon="cancel"/>
+ <xul:button dlgtype="help" class="dialog-button" hidden="true" icon="help"/>
+ <xul:button dlgtype="disclosure" class="dialog-button" hidden="true"/>
+#endif
+ </xul:hbox>
+ <xul:hbox>
+ <children/>
+ </xul:hbox>
+ </content>
+ <implementation implements="nsITimerCallback">
+ <constructor>
+ <![CDATA[
+ if (this.type != "child") {
+ if (!this._instantApplyInitialized) {
+ let psvc = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch);
+ this.instantApply = psvc.getBoolPref("browser.preferences.instantApply");
+ }
+ if (this.instantApply) {
+ var docElt = document.documentElement;
+ var acceptButton = docElt.getButton("accept");
+ acceptButton.hidden = true;
+ var cancelButton = docElt.getButton("cancel");
+ if (/Mac/.test(navigator.platform)) {
+ // no buttons on Mac except Help
+ cancelButton.hidden = true;
+ // Move Help button to the end
+ document.getAnonymousElementByAttribute(this, "anonid", "spacer").hidden = true;
+ // Also, don't fire onDialogAccept on enter
+ acceptButton.disabled = true;
+ } else {
+ // morph the Cancel button into the Close button
+ cancelButton.setAttribute ("icon", "close");
+ cancelButton.label = docElt.getAttribute("closebuttonlabel");
+ cancelButton.accesskey = docElt.getAttribute("closebuttonaccesskey");
+ }
+ }
+ }
+ this.setAttribute("animated", this._shouldAnimate ? "true" : "false");
+ var panes = this.preferencePanes;
+
+ var lastPane = null;
+ if (this.lastSelected) {
+ lastPane = document.getElementById(this.lastSelected);
+ if (!lastPane) {
+ this.lastSelected = "";
+ }
+ }
+
+ var paneToLoad;
+ if ("arguments" in window && window.arguments[0] && document.getElementById(window.arguments[0]) && document.getElementById(window.arguments[0]).nodeName == "prefpane") {
+ paneToLoad = document.getElementById(window.arguments[0]);
+ this.lastSelected = paneToLoad.id;
+ }
+ else if (lastPane)
+ paneToLoad = lastPane;
+ else
+ paneToLoad = panes[0];
+
+ for (var i = 0; i < panes.length; ++i) {
+ this._makePaneButton(panes[i]);
+ if (panes[i].loaded) {
+ // Inline pane content, fire load event to force initialization.
+ this._fireEvent("paneload", panes[i]);
+ }
+ }
+ this.showPane(paneToLoad);
+
+ if (panes.length == 1)
+ this._selector.setAttribute("collapsed", "true");
+ ]]>
+ </constructor>
+
+ <destructor>
+ <![CDATA[
+ // Release timers to avoid reference cycles.
+ if (this._animateTimer) {
+ this._animateTimer.cancel();
+ this._animateTimer = null;
+ }
+ if (this._fadeTimer) {
+ this._fadeTimer.cancel();
+ this._fadeTimer = null;
+ }
+ ]]>
+ </destructor>
+
+ <!-- Derived bindings can set this to true to cause us to skip
+ reading the browser.preferences.instantApply pref in the constructor.
+ Then they can set instantApply to their wished value. -->
+ <field name="_instantApplyInitialized">false</field>
+ <!-- Controls whether changed pref values take effect immediately. -->
+ <field name="instantApply">false</field>
+
+ <property name="preferencePanes"
+ onget="return this.getElementsByTagName('prefpane');"/>
+
+ <property name="type" onget="return this.getAttribute('type');"/>
+ <property name="_paneDeck"
+ onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'paneDeck');"/>
+ <property name="_paneDeckContainer"
+ onget="return document.getAnonymousElementByAttribute(this, 'class', 'paneDeckContainer');"/>
+ <property name="_selector"
+ onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'selector');"/>
+ <property name="lastSelected"
+ onget="return this.getAttribute('lastSelected');">
+ <setter>
+ this.setAttribute("lastSelected", val);
+ document.persist(this.id, "lastSelected");
+ return val;
+ </setter>
+ </property>
+ <property name="currentPane"
+ onset="return this._currentPane = val;">
+ <getter>
+ if (!this._currentPane)
+ this._currentPane = this.preferencePanes[0];
+
+ return this._currentPane;
+ </getter>
+ </property>
+ <field name="_currentPane">null</field>
+
+
+ <method name="_makePaneButton">
+ <parameter name="aPaneElement"/>
+ <body>
+ <![CDATA[
+ var radio = document.createElement("radio");
+ radio.setAttribute("pane", aPaneElement.id);
+ radio.setAttribute("label", aPaneElement.label);
+ // Expose preference group choice to accessibility APIs as an unchecked list item
+ // The parent group is exposed to accessibility APIs as a list
+ if (aPaneElement.image)
+ radio.setAttribute("src", aPaneElement.image);
+ radio.style.listStyleImage = aPaneElement.style.listStyleImage;
+ this._selector.appendChild(radio);
+ return radio;
+ ]]>
+ </body>
+ </method>
+
+ <method name="showPane">
+ <parameter name="aPaneElement"/>
+ <body>
+ <![CDATA[
+ if (!aPaneElement)
+ return;
+
+ this._selector.selectedItem = document.getAnonymousElementByAttribute(this, "pane", aPaneElement.id);
+ if (!aPaneElement.loaded) {
+ let OverlayLoadObserver = function(aPane)
+ {
+ this._pane = aPane;
+ }
+ OverlayLoadObserver.prototype = {
+ _outer: this,
+ observe: function (aSubject, aTopic, aData)
+ {
+ this._pane.loaded = true;
+ this._outer._fireEvent("paneload", this._pane);
+ this._outer._selectPane(this._pane);
+ }
+ };
+
+ var obs = new OverlayLoadObserver(aPaneElement);
+ document.loadOverlay(aPaneElement.src, obs);
+ }
+ else
+ this._selectPane(aPaneElement);
+ ]]>
+ </body>
+ </method>
+
+ <method name="_fireEvent">
+ <parameter name="aEventName"/>
+ <parameter name="aTarget"/>
+ <body>
+ <![CDATA[
+ // Panel loaded, synthesize a load event.
+ try {
+ var event = document.createEvent("Events");
+ event.initEvent(aEventName, true, true);
+ var cancel = !aTarget.dispatchEvent(event);
+ if (aTarget.hasAttribute("on" + aEventName)) {
+ var fn = new Function ("event", aTarget.getAttribute("on" + aEventName));
+ var rv = fn.call(aTarget, event);
+ if (rv == false)
+ cancel = true;
+ }
+ return !cancel;
+ }
+ catch (e) {
+ Components.utils.reportError(e);
+ }
+ return false;
+ ]]>
+ </body>
+ </method>
+
+ <field name="_initialized">false</field>
+ <method name="_selectPane">
+ <parameter name="aPaneElement"/>
+ <body>
+ <![CDATA[
+ if (/Mac/.test(navigator.platform)) {
+ var paneTitle = aPaneElement.label;
+ if (paneTitle != "")
+ document.title = paneTitle;
+ }
+ var helpButton = document.documentElement.getButton("help");
+ if (aPaneElement.helpTopic)
+ helpButton.hidden = false;
+ else
+ helpButton.hidden = true;
+
+ // Find this pane's index in the deck and set the deck's
+ // selectedIndex to that value to switch to it.
+ var prefpanes = this.preferencePanes;
+ for (var i = 0; i < prefpanes.length; ++i) {
+ if (prefpanes[i] == aPaneElement) {
+ this._paneDeck.selectedIndex = i;
+
+ if (this.type != "child") {
+ if (aPaneElement.hasAttribute("flex") && this._shouldAnimate &&
+ prefpanes.length > 1)
+ aPaneElement.removeAttribute("flex");
+ // Calling sizeToContent after the first prefpane is loaded
+ // will size the windows contents so style information is
+ // available to calculate correct sizing.
+ if (!this._initialized && prefpanes.length > 1) {
+ if (this._shouldAnimate)
+ this.style.minHeight = 0;
+ window.sizeToContent();
+ }
+
+ var oldPane = this.lastSelected ? document.getElementById(this.lastSelected) : this.preferencePanes[0];
+ oldPane.selected = !(aPaneElement.selected = true);
+ this.lastSelected = aPaneElement.id;
+ this.currentPane = aPaneElement;
+ this._initialized = true;
+
+ // Only animate if we've switched between prefpanes
+ if (this._shouldAnimate && oldPane.id != aPaneElement.id) {
+ aPaneElement.style.opacity = 0.0;
+ this.animate(oldPane, aPaneElement);
+ }
+ else if (!this._shouldAnimate && prefpanes.length > 1) {
+ var targetHeight = parseInt(window.getComputedStyle(this._paneDeckContainer, "").height);
+ var verticalPadding = parseInt(window.getComputedStyle(aPaneElement, "").paddingTop);
+ verticalPadding += parseInt(window.getComputedStyle(aPaneElement, "").paddingBottom);
+ if (aPaneElement.contentHeight > targetHeight - verticalPadding) {
+ // To workaround the bottom border of a groupbox from being
+ // cutoff an hbox with a class of bottomBox may enclose it.
+ // This needs to include its padding to resize properly.
+ // See bug 394433
+ var bottomPadding = 0;
+ var bottomBox = aPaneElement.getElementsByAttribute("class", "bottomBox")[0];
+ if (bottomBox)
+ bottomPadding = parseInt(window.getComputedStyle(bottomBox, "").paddingBottom);
+ window.innerHeight += bottomPadding + verticalPadding + aPaneElement.contentHeight - targetHeight;
+ }
+
+ // XXX rstrong - extend the contents of the prefpane to
+ // prevent elements from being cutoff (see bug 349098).
+ if (aPaneElement.contentHeight + verticalPadding < targetHeight)
+ aPaneElement._content.style.height = targetHeight - verticalPadding + "px";
+ }
+ }
+ break;
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <property name="_shouldAnimate">
+ <getter>
+ <![CDATA[
+ var psvc = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch);
+ var animate = /Mac/.test(navigator.platform);
+ try {
+ animate = psvc.getBoolPref("browser.preferences.animateFadeIn");
+ }
+ catch (e) { }
+ return animate;
+ ]]>
+ </getter>
+ </property>
+
+ <method name="animate">
+ <parameter name="aOldPane"/>
+ <parameter name="aNewPane"/>
+ <body>
+ <![CDATA[
+ // if we are already resizing, use currentHeight
+ var oldHeight = this._currentHeight ? this._currentHeight : aOldPane.contentHeight;
+
+ this._multiplier = aNewPane.contentHeight > oldHeight ? 1 : -1;
+ var sizeDelta = Math.abs(oldHeight - aNewPane.contentHeight);
+ this._animateRemainder = sizeDelta % this._animateIncrement;
+
+ this._setUpAnimationTimer(oldHeight);
+ ]]>
+ </body>
+ </method>
+
+ <property name="_sizeIncrement">
+ <getter>
+ <![CDATA[
+ var lastSelectedPane = document.getElementById(this.lastSelected);
+ var increment = this._animateIncrement * this._multiplier;
+ var newHeight = this._currentHeight + increment;
+ if ((this._multiplier > 0 && this._currentHeight >= lastSelectedPane.contentHeight) ||
+ (this._multiplier < 0 && this._currentHeight <= lastSelectedPane.contentHeight))
+ return 0;
+
+ if ((this._multiplier > 0 && newHeight > lastSelectedPane.contentHeight) ||
+ (this._multiplier < 0 && newHeight < lastSelectedPane.contentHeight))
+ increment = this._animateRemainder * this._multiplier;
+ return increment;
+ ]]>
+ </getter>
+ </property>
+
+ <method name="notify">
+ <parameter name="aTimer"/>
+ <body>
+ <![CDATA[
+ if (!document)
+ aTimer.cancel();
+
+ if (aTimer == this._animateTimer) {
+ var increment = this._sizeIncrement;
+ if (increment != 0) {
+ window.innerHeight += increment;
+ this._currentHeight += increment;
+ }
+ else {
+ aTimer.cancel();
+ this._setUpFadeTimer();
+ }
+ } else if (aTimer == this._fadeTimer) {
+ var elt = document.getElementById(this.lastSelected);
+ var newOpacity = parseFloat(window.getComputedStyle(elt, "").opacity) + this._fadeIncrement;
+ if (newOpacity < 1.0)
+ elt.style.opacity = newOpacity;
+ else {
+ aTimer.cancel();
+ elt.style.opacity = 1.0;
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="_setUpAnimationTimer">
+ <parameter name="aStartHeight"/>
+ <body>
+ <![CDATA[
+ if (!this._animateTimer)
+ this._animateTimer = Components.classes["@mozilla.org/timer;1"]
+ .createInstance(Components.interfaces.nsITimer);
+ else
+ this._animateTimer.cancel();
+ this._currentHeight = aStartHeight;
+
+ this._animateTimer.initWithCallback(this, this._animateDelay,
+ Components.interfaces.nsITimer.TYPE_REPEATING_SLACK);
+ ]]>
+ </body>
+ </method>
+
+ <method name="_setUpFadeTimer">
+ <body>
+ <![CDATA[
+ if (!this._fadeTimer)
+ this._fadeTimer = Components.classes["@mozilla.org/timer;1"]
+ .createInstance(Components.interfaces.nsITimer);
+ else
+ this._fadeTimer.cancel();
+
+ this._fadeTimer.initWithCallback(this, this._fadeDelay,
+ Components.interfaces.nsITimer.TYPE_REPEATING_SLACK);
+ ]]>
+ </body>
+ </method>
+
+ <field name="_animateTimer">null</field>
+ <field name="_fadeTimer">null</field>
+ <field name="_animateDelay">15</field>
+ <field name="_animateIncrement">40</field>
+ <field name="_fadeDelay">5</field>
+ <field name="_fadeIncrement">0.40</field>
+ <field name="_animateRemainder">0</field>
+ <field name="_currentHeight">0</field>
+ <field name="_multiplier">0</field>
+
+ <method name="addPane">
+ <parameter name="aPaneElement"/>
+ <body>
+ <![CDATA[
+ this.appendChild(aPaneElement);
+
+ // Set up pane button
+ this._makePaneButton(aPaneElement);
+ ]]>
+ </body>
+ </method>
+
+ <method name="openSubDialog">
+ <parameter name="aURL"/>
+ <parameter name="aFeatures"/>
+ <parameter name="aParams"/>
+ <body>
+ return openDialog(aURL, "", "modal,centerscreen,resizable=no" + (aFeatures != "" ? ("," + aFeatures) : ""), aParams);
+ </body>
+ </method>
+
+ <method name="openWindow">
+ <parameter name="aWindowType"/>
+ <parameter name="aURL"/>
+ <parameter name="aFeatures"/>
+ <parameter name="aParams"/>
+ <body>
+ <![CDATA[
+ var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Components.interfaces.nsIWindowMediator);
+ var win = aWindowType ? wm.getMostRecentWindow(aWindowType) : null;
+ if (win) {
+ if ("initWithParams" in win)
+ win.initWithParams(aParams);
+ win.focus();
+ }
+ else {
+ var features = "resizable,dialog=no,centerscreen" + (aFeatures != "" ? ("," + aFeatures) : "");
+ var parentWindow = (this.instantApply || !window.opener || window.opener.closed) ? window : window.opener;
+ win = parentWindow.openDialog(aURL, "_blank", features, aParams);
+ }
+ return win;
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ <handlers>
+ <handler event="dialogaccept">
+ <![CDATA[
+ if (!this._fireEvent("beforeaccept", this)) {
+ return false;
+ }
+
+ var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
+ .getService(Components.interfaces.nsIScriptSecurityManager);
+ if (this.type == "child" && window.opener &&
+ secMan.isSystemPrincipal(window.opener.document.nodePrincipal)) {
+ let psvc = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch);
+ var pdocEl = window.opener.document.documentElement;
+ if (pdocEl.instantApply) {
+ let panes = this.preferencePanes;
+ for (let i = 0; i < panes.length; ++i)
+ panes[i].writePreferences(true);
+ }
+ else {
+ // Clone all the preferences elements from the child document and
+ // insert them into the pane collection of the parent.
+ var pdoc = window.opener.document;
+ if (pdoc.documentElement.localName == "prefwindow") {
+ var currentPane = pdoc.documentElement.currentPane;
+ var id = window.location.href + "#childprefs";
+ var childPrefs = pdoc.getElementById(id);
+ if (!childPrefs) {
+ childPrefs = pdoc.createElement("preferences");
+ currentPane.appendChild(childPrefs);
+ childPrefs.id = id;
+ }
+ let panes = this.preferencePanes;
+ for (let i = 0; i < panes.length; ++i) {
+ var preferences = panes[i].preferences;
+ for (var j = 0; j < preferences.length; ++j) {
+ // Try to find a preference element for the same preference.
+ var preference = null;
+ var parentPreferences = pdoc.getElementsByTagName("preferences");
+ for (var k = 0; (k < parentPreferences.length && !preference); ++k) {
+ var parentPrefs = parentPreferences[k]
+ .getElementsByAttribute("name", preferences[j].name);
+ for (var l = 0; (l < parentPrefs.length && !preference); ++l) {
+ if (parentPrefs[l].localName == "preference")
+ preference = parentPrefs[l];
+ }
+ }
+ if (!preference) {
+ // No matching preference in the parent window.
+ preference = pdoc.createElement("preference");
+ childPrefs.appendChild(preference);
+ preference.name = preferences[j].name;
+ preference.type = preferences[j].type;
+ preference.inverted = preferences[j].inverted;
+ preference.readonly = preferences[j].readonly;
+ preference.disabled = preferences[j].disabled;
+ }
+ preference.value = preferences[j].value;
+ }
+ }
+ }
+ }
+ }
+ else {
+ let panes = this.preferencePanes;
+ for (var i = 0; i < panes.length; ++i)
+ panes[i].writePreferences(false);
+
+ let psvc = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefService);
+ psvc.savePrefFile(null);
+ }
+
+ return true;
+ ]]>
+ </handler>
+ <handler event="command">
+ if (event.originalTarget.hasAttribute("pane")) {
+ var pane = document.getElementById(event.originalTarget.getAttribute("pane"));
+ this.showPane(pane);
+ }
+ </handler>
+
+ <handler event="keypress" key="&windowClose.key;" modifiers="accel" phase="capturing">
+ <![CDATA[
+ if (this.instantApply)
+ window.close();
+ event.stopPropagation();
+ event.preventDefault();
+ ]]>
+ </handler>
+
+ <handler event="keypress"
+#ifdef XP_MACOSX
+ key="&openHelpMac.commandkey;" modifiers="accel"
+#else
+ keycode="&openHelp.commandkey;"
+#endif
+ phase="capturing">
+ <![CDATA[
+ var helpButton = this.getButton("help");
+ if (helpButton.disabled || helpButton.hidden)
+ return;
+ this._fireEvent("dialoghelp", this);
+ event.stopPropagation();
+ event.preventDefault();
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="prefpane">
+ <resources>
+ <stylesheet src="chrome://global/skin/preferences.css"/>
+ </resources>
+ <content>
+ <xul:vbox class="content-box" xbl:inherits="flex">
+ <children/>
+ </xul:vbox>
+ </content>
+ <implementation>
+ <method name="writePreferences">
+ <parameter name="aFlushToDisk"/>
+ <body>
+ <![CDATA[
+ // Write all values to preferences.
+ if (this._deferredValueUpdateElements.size) {
+ this._finalizeDeferredElements();
+ }
+
+ var preferences = this.preferences;
+ for (var i = 0; i < preferences.length; ++i) {
+ var preference = preferences[i];
+ preference.batching = true;
+ preference.valueFromPreferences = preference.value;
+ preference.batching = false;
+ }
+ if (aFlushToDisk) {
+ var psvc = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefService);
+ psvc.savePrefFile(null);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <property name="src"
+ onget="return this.getAttribute('src');"
+ onset="this.setAttribute('src', val); return val;"/>
+ <property name="selected"
+ onget="return this.getAttribute('selected') == 'true';"
+ onset="this.setAttribute('selected', val); return val;"/>
+ <property name="image"
+ onget="return this.getAttribute('image');"
+ onset="this.setAttribute('image', val); return val;"/>
+ <property name="label"
+ onget="return this.getAttribute('label');"
+ onset="this.setAttribute('label', val); return val;"/>
+
+ <property name="preferenceElements"
+ onget="return this.getElementsByAttribute('preference', '*');"/>
+ <property name="preferences"
+ onget="return this.getElementsByTagName('preference');"/>
+
+ <property name="helpTopic">
+ <getter>
+ <![CDATA[
+ // if there are tabs, and the selected tab provides a helpTopic, return that
+ var box = this.getElementsByTagName("tabbox");
+ if (box[0]) {
+ var tab = box[0].selectedTab;
+ if (tab && tab.hasAttribute("helpTopic"))
+ return tab.getAttribute("helpTopic");
+ }
+
+ // otherwise, return the helpTopic of the current panel
+ return this.getAttribute("helpTopic");
+ ]]>
+ </getter>
+ </property>
+
+ <field name="_loaded">false</field>
+ <property name="loaded"
+ onget="return !this.src ? true : this._loaded;"
+ onset="this._loaded = val; return val;"/>
+
+ <method name="preferenceForElement">
+ <parameter name="aElement"/>
+ <body>
+ return document.getElementById(aElement.getAttribute("preference"));
+ </body>
+ </method>
+
+ <method name="getPreferenceElement">
+ <parameter name="aStartElement"/>
+ <body>
+ <![CDATA[
+ var temp = aStartElement;
+ while (temp && temp.nodeType == Node.ELEMENT_NODE &&
+ !temp.hasAttribute("preference"))
+ temp = temp.parentNode;
+ return temp.nodeType == Node.ELEMENT_NODE ? temp : aStartElement;
+ ]]>
+ </body>
+ </method>
+
+ <property name="DeferredTask" readonly="true">
+ <getter><![CDATA[
+ let module = {};
+ Components.utils.import("resource://gre/modules/DeferredTask.jsm", module);
+ Object.defineProperty(this, "DeferredTask", {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: module.DeferredTask
+ });
+ return module.DeferredTask;
+ ]]></getter>
+ </property>
+ <method name="_deferredValueUpdate">
+ <parameter name="aElement"/>
+ <body>
+ <![CDATA[
+ delete aElement._deferredValueUpdateTask;
+ let preference = document.getElementById(aElement.getAttribute("preference"));
+ let prefVal = preference.getElementValue(aElement);
+ preference.value = prefVal;
+ this._deferredValueUpdateElements.delete(aElement);
+ ]]>
+ </body>
+ </method>
+ <field name="_deferredValueUpdateElements">
+ new Set();
+ </field>
+ <method name="_finalizeDeferredElements">
+ <body>
+ <![CDATA[
+ for (let el of this._deferredValueUpdateElements) {
+ if (el._deferredValueUpdateTask) {
+ el._deferredValueUpdateTask.finalize();
+ }
+ }
+ ]]>
+ </body>
+ </method>
+ <method name="userChangedValue">
+ <parameter name="aElement"/>
+ <body>
+ <![CDATA[
+ let element = this.getPreferenceElement(aElement);
+ if (element.hasAttribute("preference")) {
+ if (element.getAttribute("delayprefsave") != "true") {
+ var preference = document.getElementById(element.getAttribute("preference"));
+ var prefVal = preference.getElementValue(element);
+ preference.value = prefVal;
+ } else {
+ if (!element._deferredValueUpdateTask) {
+ element._deferredValueUpdateTask = new this.DeferredTask(this._deferredValueUpdate.bind(this, element), 1000);
+ this._deferredValueUpdateElements.add(element);
+ } else {
+ // Each time the preference is changed, restart the delay.
+ element._deferredValueUpdateTask.disarm();
+ }
+ element._deferredValueUpdateTask.arm();
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <property name="contentHeight">
+ <getter>
+ var targetHeight = parseInt(window.getComputedStyle(this._content, "").height);
+ targetHeight += parseInt(window.getComputedStyle(this._content, "").marginTop);
+ targetHeight += parseInt(window.getComputedStyle(this._content, "").marginBottom);
+ return targetHeight;
+ </getter>
+ </property>
+ <field name="_content">
+ document.getAnonymousElementByAttribute(this, "class", "content-box");
+ </field>
+ </implementation>
+ <handlers>
+ <handler event="command">
+ // This "command" event handler tracks changes made to preferences by
+ // the user in this window.
+ if (event.sourceEvent)
+ event = event.sourceEvent;
+ this.userChangedValue(event.target);
+ </handler>
+ <handler event="select">
+ // This "select" event handler tracks changes made to colorpicker
+ // preferences by the user in this window.
+ if (event.target.localName == "colorpicker")
+ this.userChangedValue(event.target);
+ </handler>
+ <handler event="change">
+ // This "change" event handler tracks changes made to preferences by
+ // the user in this window.
+ this.userChangedValue(event.target);
+ </handler>
+ <handler event="input">
+ // This "input" event handler tracks changes made to preferences by
+ // the user in this window.
+ this.userChangedValue(event.target);
+ </handler>
+ <handler event="paneload">
+ <![CDATA[
+ // Initialize all values from preferences.
+ var elements = this.preferenceElements;
+ for (var i = 0; i < elements.length; ++i) {
+ try {
+ var preference = this.preferenceForElement(elements[i]);
+ preference.setElementValue(elements[i]);
+ }
+ catch (e) {
+ dump("*** No preference found for " + elements[i].getAttribute("preference") + "\n");
+ }
+ }
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="panebutton" role="xul:listitem"
+ extends="chrome://global/content/bindings/radio.xml#radio">
+ <resources>
+ <stylesheet src="chrome://global/skin/preferences.css"/>
+ </resources>
+ <content>
+ <xul:image class="paneButtonIcon" xbl:inherits="src"/>
+ <xul:label class="paneButtonLabel" xbl:inherits="value=label"/>
+ </content>
+ </binding>
+
+</bindings>
+
+# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#
+# This is PrefWindow 6. The Code Could Well Be Ready, Are You?
+#
+# Historical References:
+# PrefWindow V (February 1, 2003)
+# PrefWindow IV (April 24, 2000)
+# PrefWindow III (January 6, 2000)
+# PrefWindow II (???)
+# PrefWindow I (June 4, 1999)
+#
diff --git a/toolkit/content/widgets/progressmeter.xml b/toolkit/content/widgets/progressmeter.xml
new file mode 100644
index 000000000..82f28ffba
--- /dev/null
+++ b/toolkit/content/widgets/progressmeter.xml
@@ -0,0 +1,116 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<bindings id="progressmeterBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="progressmeter" role="xul:progressmeter">
+ <resources>
+ <stylesheet src="chrome://global/skin/progressmeter.css"/>
+ </resources>
+
+ <content>
+ <xul:spacer class="progress-bar" xbl:inherits="mode"/>
+ <xul:spacer class="progress-remainder" xbl:inherits="mode"/>
+ </content>
+
+ <implementation>
+ <property name="mode" onset="if (this.mode != val) this.setAttribute('mode', val); return val;"
+ onget="return this.getAttribute('mode');"/>
+
+ <property name="value" onget="return this.getAttribute('value') || '0';">
+ <setter><![CDATA[
+ var p = Math.round(val);
+ var max = Math.round(this.max);
+ if (p < 0)
+ p = 0;
+ else if (p > max)
+ p = max;
+ var c = this.value;
+ if (p != c) {
+ var delta = p - c;
+ if (delta < 0)
+ delta = -delta;
+ if (delta > 3 || p == 0 || p == max) {
+ this.setAttribute("value", p);
+ // Fire DOM event so that accessible value change events occur
+ var event = document.createEvent('Events');
+ event.initEvent('ValueChange', true, true);
+ this.dispatchEvent(event);
+ }
+ }
+
+ return val;
+ ]]></setter>
+ </property>
+ <property name="max"
+ onget="return this.getAttribute('max') || '100';"
+ onset="this.setAttribute('max', isNaN(val) ? 100 : Math.max(val, 1));
+ this.value = this.value;
+ return val;" />
+ </implementation>
+ </binding>
+
+ <binding id="progressmeter-undetermined"
+ extends="chrome://global/content/bindings/progressmeter.xml#progressmeter">
+ <content>
+ <xul:stack class="progress-remainder" flex="1" anonid="stack" style="overflow: -moz-hidden-unscrollable;">
+ <xul:spacer class="progress-bar" anonid="spacer" top="0" style="margin-right: -1000px;"/>
+ </xul:stack>
+ </content>
+
+ <implementation>
+ <field name="_alive">true</field>
+ <method name="_init">
+ <body><![CDATA[
+ var stack =
+ document.getAnonymousElementByAttribute(this, "anonid", "stack");
+ var spacer =
+ document.getAnonymousElementByAttribute(this, "anonid", "spacer");
+ var isLTR =
+ document.defaultView.getComputedStyle(this, null).direction == "ltr";
+ var startTime = performance.now();
+ var self = this;
+
+ function nextStep(t) {
+ try {
+ var width = stack.boxObject.width;
+ if (!width) {
+ // Maybe we've been removed from the document.
+ if (self._alive)
+ requestAnimationFrame(nextStep);
+ return;
+ }
+
+ var elapsedTime = t - startTime;
+
+ // Width of chunk is 1/5 (determined by the ratio 2000:400) of the
+ // total width of the progress bar. The left edge of the chunk
+ // starts at -1 and moves all the way to 4. It covers the distance
+ // in 2 seconds.
+ var position = isLTR ? ((elapsedTime % 2000) / 400) - 1 :
+ ((elapsedTime % 2000) / -400) + 4;
+
+ width = width >> 2;
+ spacer.height = stack.boxObject.height;
+ spacer.width = width;
+ spacer.left = width * position;
+
+ requestAnimationFrame(nextStep);
+ } catch (e) {
+ }
+ }
+ requestAnimationFrame(nextStep);
+ ]]></body>
+ </method>
+
+ <constructor>this._init();</constructor>
+ </implementation>
+ </binding>
+
+</bindings>
diff --git a/toolkit/content/widgets/radio.xml b/toolkit/content/widgets/radio.xml
new file mode 100644
index 000000000..de3acfbf6
--- /dev/null
+++ b/toolkit/content/widgets/radio.xml
@@ -0,0 +1,526 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<bindings id="radioBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="radiogroup" role="xul:radiogroup"
+ extends="chrome://global/content/bindings/general.xml#basecontrol">
+ <resources>
+ <stylesheet src="chrome://global/skin/radio.css"/>
+ </resources>
+
+ <implementation implements="nsIDOMXULSelectControlElement">
+ <constructor>
+ <![CDATA[
+ if (this.getAttribute("disabled") == "true")
+ this.disabled = true;
+
+ var children = this._getRadioChildren();
+ var length = children.length;
+ for (var i = 0; i < length; i++) {
+ if (children[i].getAttribute("selected") == "true") {
+ this.selectedIndex = i;
+ return;
+ }
+ }
+
+ var value = this.value;
+ if (value)
+ this.value = value;
+ else
+ this.selectedIndex = 0;
+ ]]>
+ </constructor>
+
+ <property name="value" onget="return this.getAttribute('value');">
+ <setter>
+ <![CDATA[
+ this.setAttribute("value", val);
+ var children = this._getRadioChildren();
+ for (var i = 0; i < children.length; i++) {
+ if (String(children[i].value) == String(val)) {
+ this.selectedItem = children[i];
+ break;
+ }
+ }
+ return val;
+ ]]>
+ </setter>
+ </property>
+ <property name="disabled">
+ <getter>
+ <![CDATA[
+ if (this.getAttribute('disabled') == 'true')
+ return true;
+ var children = this._getRadioChildren();
+ for (var i = 0; i < children.length; ++i) {
+ if (!children[i].hidden && !children[i].collapsed && !children[i].disabled)
+ return false;
+ }
+ return true;
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ if (val)
+ this.setAttribute('disabled', 'true');
+ else
+ this.removeAttribute('disabled');
+ var children = this._getRadioChildren();
+ for (var i = 0; i < children.length; ++i) {
+ children[i].disabled = val;
+ }
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="itemCount" readonly="true"
+ onget="return this._getRadioChildren().length"/>
+
+ <property name="selectedIndex">
+ <getter>
+ <![CDATA[
+ var children = this._getRadioChildren();
+ for (var i = 0; i < children.length; ++i) {
+ if (children[i].selected)
+ return i;
+ }
+ return -1;
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ this.selectedItem = this._getRadioChildren()[val];
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="selectedItem">
+ <getter>
+ <![CDATA[
+ var children = this._getRadioChildren();
+ for (var i = 0; i < children.length; ++i) {
+ if (children[i].selected)
+ return children[i];
+ }
+ return null;
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ var focused = this.getAttribute("focused") == "true";
+ var alreadySelected = false;
+
+ if (val) {
+ alreadySelected = val.getAttribute("selected") == "true";
+ val.setAttribute("focused", focused);
+ val.setAttribute("selected", "true");
+ this.setAttribute("value", val.value);
+ }
+ else {
+ this.removeAttribute("value");
+ }
+
+ // uncheck all other group nodes
+ var children = this._getRadioChildren();
+ var previousItem = null;
+ for (var i = 0; i < children.length; ++i) {
+ if (children[i] != val) {
+ if (children[i].getAttribute("selected") == "true")
+ previousItem = children[i];
+
+ children[i].removeAttribute("selected");
+ children[i].removeAttribute("focused");
+ }
+ }
+
+ var event = document.createEvent("Events");
+ event.initEvent("select", false, true);
+ this.dispatchEvent(event);
+
+ if (!alreadySelected && focused) {
+ // Only report if actual change
+ var myEvent;
+ if (val) {
+ myEvent = document.createEvent("Events");
+ myEvent.initEvent("RadioStateChange", true, true);
+ val.dispatchEvent(myEvent);
+ }
+
+ if (previousItem) {
+ myEvent = document.createEvent("Events");
+ myEvent.initEvent("RadioStateChange", true, true);
+ previousItem.dispatchEvent(myEvent);
+ }
+ }
+
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="focusedItem">
+ <getter>
+ <![CDATA[
+ var children = this._getRadioChildren();
+ for (var i = 0; i < children.length; ++i) {
+ if (children[i].getAttribute("focused") == "true")
+ return children[i];
+ }
+ return null;
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ if (val) val.setAttribute("focused", "true");
+
+ // unfocus all other group nodes
+ var children = this._getRadioChildren();
+ for (var i = 0; i < children.length; ++i) {
+ if (children[i] != val)
+ children[i].removeAttribute("focused");
+ }
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <method name="checkAdjacentElement">
+ <parameter name="aNextFlag"/>
+ <body>
+ <![CDATA[
+ var currentElement = this.focusedItem || this.selectedItem;
+ var i;
+ var children = this._getRadioChildren();
+ for (i = 0; i < children.length; ++i ) {
+ if (children[i] == currentElement)
+ break;
+ }
+ var index = i;
+
+ if (aNextFlag) {
+ do {
+ if (++i == children.length)
+ i = 0;
+ if (i == index)
+ break;
+ }
+ while (children[i].hidden || children[i].collapsed || children[i].disabled);
+ // XXX check for display/visibility props too
+
+ this.selectedItem = children[i];
+ children[i].doCommand();
+ }
+ else {
+ do {
+ if (i == 0)
+ i = children.length;
+ if (--i == index)
+ break;
+ }
+ while (children[i].hidden || children[i].collapsed || children[i].disabled);
+ // XXX check for display/visibility props too
+
+ this.selectedItem = children[i];
+ children[i].doCommand();
+ }
+ ]]>
+ </body>
+ </method>
+ <field name="_radioChildren">null</field>
+ <method name="_getRadioChildren">
+ <body>
+ <![CDATA[
+ if (this._radioChildren)
+ return this._radioChildren;
+
+ var radioChildren = [];
+ var doc = this.ownerDocument;
+
+ if (this.hasChildNodes()) {
+ // Don't store the collected child nodes immediately,
+ // collecting the child nodes could trigger constructors
+ // which would blow away our list.
+
+ const nsIDOMNodeFilter = Components.interfaces.nsIDOMNodeFilter;
+ var iterator = doc.createTreeWalker(this,
+ nsIDOMNodeFilter.SHOW_ELEMENT,
+ this._filterRadioGroup);
+ while (iterator.nextNode())
+ radioChildren.push(iterator.currentNode);
+ return this._radioChildren = radioChildren;
+ }
+
+ // We don't have child nodes.
+ const XUL_NS = "http://www.mozilla.org/keymaster/"
+ + "gatekeeper/there.is.only.xul";
+ var elems = doc.getElementsByAttribute("group", this.id);
+ for (var i = 0; i < elems.length; i++) {
+ if ((elems[i].namespaceURI == XUL_NS) &&
+ (elems[i].localName == "radio")) {
+ radioChildren.push(elems[i]);
+ }
+ }
+ return this._radioChildren = radioChildren;
+ ]]>
+ </body>
+ </method>
+ <method name="_filterRadioGroup">
+ <parameter name="node"/>
+ <body>
+ <![CDATA[
+ switch (node.localName) {
+ case "radio": return NodeFilter.FILTER_ACCEPT;
+ case "template":
+ case "radiogroup": return NodeFilter.FILTER_REJECT;
+ default: return NodeFilter.FILTER_SKIP;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="getIndexOfItem">
+ <parameter name="item"/>
+ <body>
+ return this._getRadioChildren().indexOf(item);
+ </body>
+ </method>
+
+ <method name="getItemAtIndex">
+ <parameter name="index"/>
+ <body>
+ <![CDATA[
+ var children = this._getRadioChildren();
+ return (index >= 0 && index < children.length) ? children[index] : null;
+ ]]>
+ </body>
+ </method>
+
+ <method name="appendItem">
+ <parameter name="label"/>
+ <parameter name="value"/>
+ <body>
+ <![CDATA[
+ var XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ var radio = document.createElementNS(XULNS, "radio");
+ radio.setAttribute("label", label);
+ radio.setAttribute("value", value);
+ this.appendChild(radio);
+ this._radioChildren = null;
+ return radio;
+ ]]>
+ </body>
+ </method>
+
+ <method name="insertItemAt">
+ <parameter name="index"/>
+ <parameter name="label"/>
+ <parameter name="value"/>
+ <body>
+ <![CDATA[
+ var XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ var radio = document.createElementNS(XULNS, "radio");
+ radio.setAttribute("label", label);
+ radio.setAttribute("value", value);
+ var before = this.getItemAtIndex(index);
+ if (before)
+ before.parentNode.insertBefore(radio, before);
+ else
+ this.appendChild(radio);
+ this._radioChildren = null;
+ return radio;
+ ]]>
+ </body>
+ </method>
+
+ <method name="removeItemAt">
+ <parameter name="index"/>
+ <body>
+ <![CDATA[
+ var remove = this.getItemAtIndex(index);
+ if (remove) {
+ remove.parentNode.removeChild(remove);
+ this._radioChildren = null;
+ }
+ return remove;
+ ]]>
+ </body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="mousedown">
+ if (this.disabled)
+ event.preventDefault();
+ </handler>
+
+ <!-- keyboard navigation -->
+ <!-- Here's how keyboard navigation works in radio groups on Windows:
+ The group takes 'focus'
+ The user is then free to navigate around inside the group
+ using the arrow keys. Accessing previous or following radio buttons
+ is done solely through the arrow keys and not the tab button. Tab
+ takes you to the next widget in the tab order -->
+ <handler event="keypress" key=" " phase="target">
+ this.selectedItem = this.focusedItem;
+ this.selectedItem.doCommand();
+ // Prevent page from scrolling on the space key.
+ event.preventDefault();
+ </handler>
+ <handler event="keypress" keycode="VK_UP" phase="target">
+ this.checkAdjacentElement(false);
+ event.stopPropagation();
+ event.preventDefault();
+ </handler>
+ <handler event="keypress" keycode="VK_LEFT" phase="target">
+ // left arrow goes back when we are ltr, forward when we are rtl
+ this.checkAdjacentElement(document.defaultView.getComputedStyle(
+ this, "").direction == "rtl");
+ event.stopPropagation();
+ event.preventDefault();
+ </handler>
+ <handler event="keypress" keycode="VK_DOWN" phase="target">
+ this.checkAdjacentElement(true);
+ event.stopPropagation();
+ event.preventDefault();
+ </handler>
+ <handler event="keypress" keycode="VK_RIGHT" phase="target">
+ // right arrow goes forward when we are ltr, back when we are rtl
+ this.checkAdjacentElement(document.defaultView.getComputedStyle(
+ this, "").direction == "ltr");
+ event.stopPropagation();
+ event.preventDefault();
+ </handler>
+
+ <!-- set a focused attribute on the selected item when the group
+ receives focus so that we can style it as if it were focused even though
+ it is not (Windows platform behaviour is for the group to receive focus,
+ not the item -->
+ <handler event="focus" phase="target">
+ <![CDATA[
+ this.setAttribute("focused", "true");
+ if (this.focusedItem)
+ return;
+
+ var val = this.selectedItem;
+ if (!val || val.disabled || val.hidden || val.collapsed) {
+ var children = this._getRadioChildren();
+ for (var i = 0; i < children.length; ++i) {
+ if (!children[i].hidden && !children[i].collapsed && !children[i].disabled) {
+ val = children[i];
+ break;
+ }
+ }
+ }
+ this.focusedItem = val;
+ ]]>
+ </handler>
+ <handler event="blur" phase="target">
+ this.removeAttribute("focused");
+ this.focusedItem = null;
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="radio" role="xul:radiobutton"
+ extends="chrome://global/content/bindings/general.xml#control-item">
+ <resources>
+ <stylesheet src="chrome://global/skin/radio.css"/>
+ </resources>
+
+ <content>
+ <xul:image class="radio-check" xbl:inherits="disabled,selected"/>
+ <xul:hbox class="radio-label-box" align="center" flex="1">
+ <xul:image class="radio-icon" xbl:inherits="src"/>
+ <xul:label class="radio-label" xbl:inherits="xbl:text=label,accesskey,crop" flex="1"/>
+ </xul:hbox>
+ </content>
+
+ <implementation implements="nsIDOMXULSelectControlItemElement">
+ <constructor>
+ <![CDATA[
+ // Just clear out the parent's cached list of radio children
+ var control = this.control;
+ if (control)
+ control._radioChildren = null;
+ ]]>
+ </constructor>
+ <destructor>
+ <![CDATA[
+ if (!this.control)
+ return;
+
+ var radioList = this.control._radioChildren;
+ if (!radioList)
+ return;
+ for (var i = 0; i < radioList.length; ++i) {
+ if (radioList[i] == this) {
+ radioList.splice(i, 1);
+ return;
+ }
+ }
+ ]]>
+ </destructor>
+ <property name="selected" readonly="true">
+ <getter>
+ <![CDATA[
+ return this.hasAttribute('selected');
+ ]]>
+ </getter>
+ </property>
+ <property name="radioGroup" readonly="true" onget="return this.control"/>
+ <property name="control" readonly="true">
+ <getter>
+ <![CDATA[
+ const XUL_NS = "http://www.mozilla.org/keymaster/"
+ + "gatekeeper/there.is.only.xul";
+ var parent = this.parentNode;
+ while (parent) {
+ if ((parent.namespaceURI == XUL_NS) &&
+ (parent.localName == "radiogroup")) {
+ return parent;
+ }
+ parent = parent.parentNode;
+ }
+
+ var group = this.getAttribute("group");
+ if (!group) {
+ return null;
+ }
+
+ parent = this.ownerDocument.getElementById(group);
+ if (!parent ||
+ (parent.namespaceURI != XUL_NS) ||
+ (parent.localName != "radiogroup")) {
+ parent = null;
+ }
+ return parent;
+ ]]>
+ </getter>
+ </property>
+ </implementation>
+ <handlers>
+ <handler event="click" button="0">
+ <![CDATA[
+ if (!this.disabled)
+ this.control.selectedItem = this;
+ ]]>
+ </handler>
+
+ <handler event="mousedown" button="0">
+ <![CDATA[
+ if (!this.disabled)
+ this.control.focusedItem = this;
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+</bindings>
diff --git a/toolkit/content/widgets/remote-browser.xml b/toolkit/content/widgets/remote-browser.xml
new file mode 100644
index 000000000..b78179944
--- /dev/null
+++ b/toolkit/content/widgets/remote-browser.xml
@@ -0,0 +1,591 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<bindings id="firefoxBrowserBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <binding id="remote-browser" extends="chrome://global/content/bindings/browser.xml#browser">
+
+ <implementation type="application/javascript"
+ implements="nsIObserver, nsIDOMEventListener, nsIMessageListener, nsIRemoteBrowser">
+
+ <field name="_securityUI">null</field>
+
+ <property name="securityUI"
+ readonly="true">
+ <getter><![CDATA[
+ if (!this._securityUI) {
+ // Don't attempt to create the remote web progress if the
+ // messageManager has already gone away
+ if (!this.messageManager)
+ return null;
+
+ let jsm = "resource://gre/modules/RemoteSecurityUI.jsm";
+ let RemoteSecurityUI = Components.utils.import(jsm, {}).RemoteSecurityUI;
+ this._securityUI = new RemoteSecurityUI();
+ }
+
+ // We want to double-wrap the JS implemented interface, so that QI and instanceof works.
+ var ptr = Components.classes["@mozilla.org/supports-interface-pointer;1"]
+ .createInstance(Components.interfaces.nsISupportsInterfacePointer);
+ ptr.data = this._securityUI;
+ return ptr.data.QueryInterface(Components.interfaces.nsISecureBrowserUI);
+ ]]></getter>
+ </property>
+
+ <!-- increases or decreases the browser's network priority -->
+ <method name="adjustPriority">
+ <parameter name="adjustment"/>
+ <body><![CDATA[
+ this.messageManager.sendAsyncMessage("NetworkPrioritizer:AdjustPriority",
+ {adjustment});
+ ]]></body>
+ </method>
+
+ <!-- sets the browser's network priority to a discrete value -->
+ <method name="setPriority">
+ <parameter name="priority"/>
+ <body><![CDATA[
+ this.messageManager.sendAsyncMessage("NetworkPrioritizer:SetPriority",
+ {priority});
+ ]]></body>
+ </method>
+
+ <field name="_controller">null</field>
+
+ <field name="_selectParentHelper">null</field>
+
+ <field name="_remoteWebNavigation">null</field>
+
+ <property name="webNavigation"
+ onget="return this._remoteWebNavigation;"
+ readonly="true"/>
+
+ <field name="_remoteWebProgress">null</field>
+
+ <property name="webProgress" readonly="true">
+ <getter>
+ <![CDATA[
+ if (!this._remoteWebProgress) {
+ // Don't attempt to create the remote web progress if the
+ // messageManager has already gone away
+ if (!this.messageManager)
+ return null;
+
+ let jsm = "resource://gre/modules/RemoteWebProgress.jsm";
+ let { RemoteWebProgressManager } = Components.utils.import(jsm, {});
+ this._remoteWebProgressManager = new RemoteWebProgressManager(this);
+ this._remoteWebProgress = this._remoteWebProgressManager.topLevelWebProgress;
+ }
+ return this._remoteWebProgress;
+ ]]>
+ </getter>
+ </property>
+
+ <field name="_remoteFinder">null</field>
+
+ <property name="finder" readonly="true">
+ <getter><![CDATA[
+ if (!this._remoteFinder) {
+ // Don't attempt to create the remote finder if the
+ // messageManager has already gone away
+ if (!this.messageManager)
+ return null;
+
+ let jsm = "resource://gre/modules/RemoteFinder.jsm";
+ let { RemoteFinder } = Components.utils.import(jsm, {});
+ this._remoteFinder = new RemoteFinder(this);
+ }
+ return this._remoteFinder;
+ ]]></getter>
+ </property>
+
+ <field name="_documentURI">null</field>
+
+ <field name="_documentContentType">null</field>
+
+ <!--
+ Used by session restore to ensure that currentURI is set so
+ that switch-to-tab works before the tab is fully
+ restored. This function also invokes onLocationChanged
+ listeners in tabbrowser.xml.
+ -->
+ <method name="_setCurrentURI">
+ <parameter name="aURI"/>
+ <body><![CDATA[
+ this._remoteWebProgressManager.setCurrentURI(aURI);
+ ]]></body>
+ </method>
+
+ <property name="documentURI"
+ onget="return this._documentURI;"
+ readonly="true"/>
+
+ <property name="documentContentType"
+ onget="return this._documentContentType;"
+ readonly="true"/>
+
+ <field name="_contentTitle">""</field>
+
+ <property name="contentTitle"
+ onget="return this._contentTitle"
+ readonly="true"/>
+
+ <field name="_characterSet">""</field>
+
+ <property name="characterSet"
+ onget="return this._characterSet">
+ <setter><![CDATA[
+ this.messageManager.sendAsyncMessage("UpdateCharacterSet", {value: val});
+ this._characterSet = val;
+ ]]></setter>
+ </property>
+
+ <field name="_mayEnableCharacterEncodingMenu">null</field>
+
+ <property name="mayEnableCharacterEncodingMenu"
+ onget="return this._mayEnableCharacterEncodingMenu;"
+ readonly="true"/>
+
+ <field name="_contentWindow">null</field>
+
+ <property name="contentWindow"
+ onget="return null"
+ readonly="true"/>
+
+ <property name="contentWindowAsCPOW"
+ onget="return this._contentWindow"
+ readonly="true"/>
+
+ <property name="contentDocument"
+ onget="return null"
+ readonly="true"/>
+
+ <field name="_contentPrincipal">null</field>
+
+ <property name="contentPrincipal"
+ onget="return this._contentPrincipal"
+ readonly="true"/>
+
+ <property name="contentDocumentAsCPOW"
+ onget="return this.contentWindowAsCPOW ? this.contentWindowAsCPOW.document : null"
+ readonly="true"/>
+
+ <field name="_imageDocument">null</field>
+
+ <property name="imageDocument"
+ onget="return this._imageDocument"
+ readonly="true"/>
+
+ <field name="_fullZoom">1</field>
+ <property name="fullZoom">
+ <getter><![CDATA[
+ return this._fullZoom;
+ ]]></getter>
+ <setter><![CDATA[
+ let changed = val.toFixed(2) != this._fullZoom.toFixed(2);
+
+ this._fullZoom = val;
+ this.messageManager.sendAsyncMessage("FullZoom", {value: val});
+
+ if (changed) {
+ let event = new Event("FullZoomChange", {bubbles: true});
+ this.dispatchEvent(event);
+ }
+ ]]></setter>
+ </property>
+
+ <field name="_textZoom">1</field>
+ <property name="textZoom">
+ <getter><![CDATA[
+ return this._textZoom;
+ ]]></getter>
+ <setter><![CDATA[
+ let changed = val.toFixed(2) != this._textZoom.toFixed(2);
+
+ this._textZoom = val;
+ this.messageManager.sendAsyncMessage("TextZoom", {value: val});
+
+ if (changed) {
+ let event = new Event("TextZoomChange", {bubbles: true});
+ this.dispatchEvent(event);
+ }
+ ]]></setter>
+ </property>
+
+ <field name="_isSyntheticDocument">false</field>
+ <property name="isSyntheticDocument">
+ <getter><![CDATA[
+ return this._isSyntheticDocument;
+ ]]></getter>
+ </property>
+
+ <property name="hasContentOpener">
+ <getter><![CDATA[
+ let {frameLoader} = this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner);
+ return frameLoader.tabParent.hasContentOpener;
+ ]]></getter>
+ </property>
+
+ <field name="_outerWindowID">null</field>
+ <property name="outerWindowID"
+ onget="return this._outerWindowID"
+ readonly="true"/>
+
+ <field name="_innerWindowID">null</field>
+ <property name="innerWindowID">
+ <getter><![CDATA[
+ return this._innerWindowID;
+ ]]></getter>
+ </property>
+
+ <property name="docShellIsActive">
+ <getter>
+ <![CDATA[
+ let {frameLoader} = this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner);
+ return frameLoader.tabParent.docShellIsActive;
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ let {frameLoader} = this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner);
+ frameLoader.tabParent.docShellIsActive = val;
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <method name="preserveLayers">
+ <parameter name="preserve"/>
+ <body><![CDATA[
+ let {frameLoader} = this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner);
+ if (frameLoader.tabParent) {
+ frameLoader.tabParent.preserveLayers(preserve);
+ }
+ ]]></body>
+ </method>
+
+ <field name="_manifestURI"/>
+ <property name="manifestURI"
+ onget="return this._manifestURI"
+ readonly="true"/>
+
+ <field name="mDestroyed">false</field>
+
+ <field name="_permitUnloadId">0</field>
+
+ <method name="getInPermitUnload">
+ <parameter name="aCallback"/>
+ <body>
+ <![CDATA[
+ let id = this._permitUnloadId++;
+ let mm = this.messageManager;
+ mm.sendAsyncMessage("InPermitUnload", {id});
+ mm.addMessageListener("InPermitUnload", function listener(msg) {
+ if (msg.data.id != id) {
+ return;
+ }
+ aCallback(msg.data.inPermitUnload);
+ });
+ ]]>
+ </body>
+ </method>
+
+ <method name="permitUnload">
+ <body>
+ <![CDATA[
+ const kTimeout = 5000;
+
+ let finished = false;
+ let responded = false;
+ let permitUnload;
+ let id = this._permitUnloadId++;
+ let mm = this.messageManager;
+ let Services = Components.utils.import("resource://gre/modules/Services.jsm", {}).Services;
+
+ let msgListener = msg => {
+ if (msg.data.id != id) {
+ return;
+ }
+ if (msg.data.kind == "start") {
+ responded = true;
+ return;
+ }
+ done(msg.data.permitUnload);
+ };
+
+ let observer = subject => {
+ if (subject == mm) {
+ done(true);
+ }
+ };
+
+ function done(result) {
+ finished = true;
+ permitUnload = result;
+ mm.removeMessageListener("PermitUnload", msgListener);
+ Services.obs.removeObserver(observer, "message-manager-close");
+ }
+
+ mm.sendAsyncMessage("PermitUnload", {id});
+ mm.addMessageListener("PermitUnload", msgListener);
+ Services.obs.addObserver(observer, "message-manager-close", false);
+
+ let timedOut = false;
+ function timeout() {
+ if (!responded) {
+ timedOut = true;
+ }
+
+ // Dispatch something to ensure that the main thread wakes up.
+ Services.tm.mainThread.dispatch(function() {}, Components.interfaces.nsIThread.DISPATCH_NORMAL);
+ }
+
+ let timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer);
+ timer.initWithCallback(timeout, kTimeout, timer.TYPE_ONE_SHOT);
+
+ while (!finished && !timedOut) {
+ Services.tm.currentThread.processNextEvent(true);
+ }
+
+ return {permitUnload, timedOut};
+ ]]>
+ </body>
+ </method>
+
+ <constructor>
+ <![CDATA[
+ /*
+ * Don't try to send messages from this function. The message manager for
+ * the <browser> element may not be initialized yet.
+ */
+
+ this._remoteWebNavigation = Components.classes["@mozilla.org/remote-web-navigation;1"]
+ .createInstance(Components.interfaces.nsIWebNavigation);
+ this._remoteWebNavigationImpl = this._remoteWebNavigation.wrappedJSObject;
+ this._remoteWebNavigationImpl.swapBrowser(this);
+
+ // Initialize contentPrincipal to the about:blank principal for this loadcontext
+ let {Services} = Components.utils.import("resource://gre/modules/Services.jsm", {});
+ let aboutBlank = Services.io.newURI("about:blank", null, null);
+ let ssm = Services.scriptSecurityManager;
+ this._contentPrincipal = ssm.getLoadContextCodebasePrincipal(aboutBlank, this.loadContext);
+
+ this.messageManager.addMessageListener("Browser:Init", this);
+ this.messageManager.addMessageListener("DOMTitleChanged", this);
+ this.messageManager.addMessageListener("ImageDocumentLoaded", this);
+ this.messageManager.addMessageListener("FullZoomChange", this);
+ this.messageManager.addMessageListener("TextZoomChange", this);
+ this.messageManager.addMessageListener("ZoomChangeUsingMouseWheel", this);
+ this.messageManager.addMessageListener("DOMFullscreen:RequestExit", this);
+ this.messageManager.addMessageListener("DOMFullscreen:RequestRollback", this);
+ this.messageManager.addMessageListener("MozApplicationManifest", this);
+ this.messageManager.loadFrameScript("chrome://global/content/browser-child.js", true);
+
+ if (this.hasAttribute("selectmenulist")) {
+ this.messageManager.addMessageListener("Forms:ShowDropDown", this);
+ this.messageManager.addMessageListener("Forms:HideDropDown", this);
+ this.messageManager.loadFrameScript("chrome://global/content/select-child.js", true);
+ }
+
+ if (!this.hasAttribute("disablehistory")) {
+ Services.obs.addObserver(this, "browser:purge-session-history", true);
+ }
+
+ let jsm = "resource://gre/modules/RemoteController.jsm";
+ let RemoteController = Components.utils.import(jsm, {}).RemoteController;
+ this._controller = new RemoteController(this);
+ this.controllers.appendController(this._controller);
+ ]]>
+ </constructor>
+
+ <destructor>
+ <![CDATA[
+ this.destroy();
+ ]]>
+ </destructor>
+
+ <!-- This is necessary because the destructor doesn't always get called when
+ we are removed from a tabbrowser. This will be explicitly called by tabbrowser.
+
+ Note: This overrides the destroy() method from browser.xml. -->
+ <method name="destroy">
+ <body><![CDATA[
+ // Make sure that any open select is closed.
+ if (this._selectParentHelper) {
+ let menulist = document.getElementById(this.getAttribute("selectmenulist"));
+ this._selectParentHelper.hide(menulist, this);
+ }
+
+ if (this.mDestroyed)
+ return;
+ this.mDestroyed = true;
+
+ try {
+ this.controllers.removeController(this._controller);
+ } catch (ex) {
+ // This can fail when this browser element is not attached to a
+ // BrowserDOMWindow.
+ }
+
+ if (!this.hasAttribute("disablehistory")) {
+ let Services = Components.utils.import("resource://gre/modules/Services.jsm", {}).Services;
+ try {
+ Services.obs.removeObserver(this, "browser:purge-session-history");
+ } catch (ex) {
+ // It's not clear why this sometimes throws an exception.
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="receiveMessage">
+ <parameter name="aMessage"/>
+ <body><![CDATA[
+ let data = aMessage.data;
+ switch (aMessage.name) {
+ case "Browser:Init":
+ this._outerWindowID = data.outerWindowID;
+ break;
+ case "DOMTitleChanged":
+ this._contentTitle = data.title;
+ break;
+ case "ImageDocumentLoaded":
+ this._imageDocument = {
+ width: data.width,
+ height: data.height
+ };
+ break;
+
+ case "Forms:ShowDropDown": {
+ if (!this._selectParentHelper) {
+ this._selectParentHelper =
+ Cu.import("resource://gre/modules/SelectParentHelper.jsm", {}).SelectParentHelper;
+ }
+
+ let menulist = document.getElementById(this.getAttribute("selectmenulist"));
+ menulist.menupopup.style.direction = data.direction;
+
+ let zoom = Services.prefs.getBoolPref("browser.zoom.full") ||
+ this.isSyntheticDocument ? this._fullZoom : this._textZoom;
+ this._selectParentHelper.populate(menulist, data.options, data.selectedIndex, zoom);
+ this._selectParentHelper.open(this, menulist, data.rect, data.isOpenedViaTouch);
+ break;
+ }
+
+ case "FullZoomChange": {
+ this._fullZoom = data.value;
+ let event = document.createEvent("Events");
+ event.initEvent("FullZoomChange", true, false);
+ this.dispatchEvent(event);
+ break;
+ }
+
+ case "TextZoomChange": {
+ this._textZoom = data.value;
+ let event = document.createEvent("Events");
+ event.initEvent("TextZoomChange", true, false);
+ this.dispatchEvent(event);
+ break;
+ }
+
+ case "ZoomChangeUsingMouseWheel": {
+ let event = document.createEvent("Events");
+ event.initEvent("ZoomChangeUsingMouseWheel", true, false);
+ this.dispatchEvent(event);
+ break;
+ }
+
+ case "Forms:HideDropDown": {
+ if (this._selectParentHelper) {
+ let menulist = document.getElementById(this.getAttribute("selectmenulist"));
+ this._selectParentHelper.hide(menulist, this);
+ }
+ break;
+ }
+
+ case "DOMFullscreen:RequestExit": {
+ let windowUtils = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils);
+ windowUtils.exitFullscreen();
+ break;
+ }
+
+ case "DOMFullscreen:RequestRollback": {
+ let windowUtils = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils);
+ windowUtils.remoteFrameFullscreenReverted();
+ break;
+ }
+
+ case "MozApplicationManifest":
+ this._manifestURI = aMessage.data.manifest;
+ break;
+
+ default:
+ // Delegate to browser.xml.
+ return this._receiveMessage(aMessage);
+ }
+ return undefined;
+ ]]></body>
+ </method>
+
+ <method name="enableDisableCommands">
+ <parameter name="aAction"/>
+ <parameter name="aEnabledLength"/>
+ <parameter name="aEnabledCommands"/>
+ <parameter name="aDisabledLength"/>
+ <parameter name="aDisabledCommands"/>
+ <body>
+ if (this._controller) {
+ this._controller.enableDisableCommands(aAction,
+ aEnabledLength, aEnabledCommands,
+ aDisabledLength, aDisabledCommands);
+ }
+ </body>
+ </method>
+
+ <method name="purgeSessionHistory">
+ <body>
+ <![CDATA[
+ try {
+ this.messageManager.sendAsyncMessage("Browser:PurgeSessionHistory");
+ } catch (ex) {
+ // This can throw if the browser has started to go away.
+ if (ex.result != Components.results.NS_ERROR_NOT_INITIALIZED) {
+ throw ex;
+ }
+ }
+ this._remoteWebNavigationImpl.canGoBack = false;
+ this._remoteWebNavigationImpl.canGoForward = false;
+ ]]>
+ </body>
+ </method>
+
+ <method name="createAboutBlankContentViewer">
+ <parameter name="aPrincipal"/>
+ <body>
+ <![CDATA[
+ this.messageManager.sendAsyncMessage("Browser:CreateAboutBlank", aPrincipal);
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ <handlers>
+ <handler event="dragstart">
+ <![CDATA[
+ // If we're a remote browser dealing with a dragstart, stop it
+ // from propagating up, since our content process should be dealing
+ // with the mouse movement.
+ event.stopPropagation();
+ ]]>
+ </handler>
+ </handlers>
+
+ </binding>
+
+</bindings>
diff --git a/toolkit/content/widgets/resizer.xml b/toolkit/content/widgets/resizer.xml
new file mode 100644
index 000000000..006877a4f
--- /dev/null
+++ b/toolkit/content/widgets/resizer.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<bindings id="resizerBindings"
+ xmlns="http://www.mozilla.org/xbl">
+
+ <binding id="resizer">
+ <resources>
+ <stylesheet src="chrome://global/skin/resizer.css"/>
+ </resources>
+ <implementation>
+ <constructor>
+ <![CDATA[
+ // don't do this for viewport resizers; causes a crash related to
+ // bugs 563665 and 581536 otherwise
+ if (this.parentNode == this.ownerDocument.documentElement)
+ return;
+
+ // if the direction is rtl, set the rtl attribute so that the
+ // stylesheet can use this to make the cursor appear properly
+ var cs = window.getComputedStyle(this, "");
+ if (cs.writingMode === undefined || cs.writingMode == "horizontal-tb") {
+ if (cs.direction == "rtl") {
+ this.setAttribute("rtl", "true");
+ }
+ } else if (cs.writingMode.endsWith("-rl")) {
+ // writing-modes 'vertical-rl' and 'sideways-rl' want rtl resizers,
+ // as they will appear at the bottom left of the element
+ this.setAttribute("rtl", "true");
+ }
+ ]]>
+ </constructor>
+ </implementation>
+ </binding>
+
+</bindings>
diff --git a/toolkit/content/widgets/richlistbox.xml b/toolkit/content/widgets/richlistbox.xml
new file mode 100644
index 000000000..dd04a0cff
--- /dev/null
+++ b/toolkit/content/widgets/richlistbox.xml
@@ -0,0 +1,589 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<bindings id="richlistboxBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="richlistbox"
+ extends="chrome://global/content/bindings/listbox.xml#listbox-base">
+ <resources>
+ <stylesheet src="chrome://global/skin/richlistbox.css"/>
+ </resources>
+
+ <content>
+ <children includes="listheader"/>
+ <xul:scrollbox allowevents="true" orient="vertical" anonid="main-box"
+ flex="1" style="overflow: auto;" xbl:inherits="dir,pack">
+ <children/>
+ </xul:scrollbox>
+ </content>
+
+ <implementation>
+ <field name="_scrollbox">
+ document.getAnonymousElementByAttribute(this, "anonid", "main-box");
+ </field>
+ <field name="scrollBoxObject">
+ this._scrollbox.boxObject;
+ </field>
+ <constructor>
+ <![CDATA[
+ // add a template build listener
+ if (this.builder)
+ this.builder.addListener(this._builderListener);
+ else
+ this._refreshSelection();
+ ]]>
+ </constructor>
+
+ <destructor>
+ <![CDATA[
+ // remove the template build listener
+ if (this.builder)
+ this.builder.removeListener(this._builderListener);
+ ]]>
+ </destructor>
+
+ <!-- Overriding baselistbox -->
+ <method name="_fireOnSelect">
+ <body>
+ <![CDATA[
+ // make sure not to modify last-selected when suppressing select events
+ // (otherwise we'll lose the selection when a template gets rebuilt)
+ if (this._suppressOnSelect || this.suppressOnSelect)
+ return;
+
+ // remember the current item and all selected items with IDs
+ var state = this.currentItem ? this.currentItem.id : "";
+ if (this.selType == "multiple" && this.selectedCount) {
+ let getId = function getId(aItem) { return aItem.id; }
+ state += " " + [... this.selectedItems].filter(getId).map(getId).join(" ");
+ }
+ if (state)
+ this.setAttribute("last-selected", state);
+ else
+ this.removeAttribute("last-selected");
+
+ // preserve the index just in case no IDs are available
+ if (this.currentIndex > -1)
+ this._currentIndex = this.currentIndex + 1;
+
+ var event = document.createEvent("Events");
+ event.initEvent("select", true, true);
+ this.dispatchEvent(event);
+
+ // always call this (allows a commandupdater without controller)
+ document.commandDispatcher.updateCommands("richlistbox-select");
+ ]]>
+ </body>
+ </method>
+
+ <!-- We override base-listbox here because those methods don't take dir
+ into account on listbox (which doesn't support dir yet) -->
+ <method name="getNextItem">
+ <parameter name="aStartItem"/>
+ <parameter name="aDelta"/>
+ <body>
+ <![CDATA[
+ var prop = this.dir == "reverse" && this._mayReverse ?
+ "previousSibling" :
+ "nextSibling";
+ while (aStartItem) {
+ aStartItem = aStartItem[prop];
+ if (aStartItem && aStartItem instanceof
+ Components.interfaces.nsIDOMXULSelectControlItemElement &&
+ (!this._userSelecting || this._canUserSelect(aStartItem))) {
+ --aDelta;
+ if (aDelta == 0)
+ return aStartItem;
+ }
+ }
+ return null;
+ ]]></body>
+ </method>
+
+ <method name="getPreviousItem">
+ <parameter name="aStartItem"/>
+ <parameter name="aDelta"/>
+ <body>
+ <![CDATA[
+ var prop = this.dir == "reverse" && this._mayReverse ?
+ "nextSibling" :
+ "previousSibling";
+ while (aStartItem) {
+ aStartItem = aStartItem[prop];
+ if (aStartItem && aStartItem instanceof
+ Components.interfaces.nsIDOMXULSelectControlItemElement &&
+ (!this._userSelecting || this._canUserSelect(aStartItem))) {
+ --aDelta;
+ if (aDelta == 0)
+ return aStartItem;
+ }
+ }
+ return null;
+ ]]>
+ </body>
+ </method>
+
+ <method name="appendItem">
+ <parameter name="aLabel"/>
+ <parameter name="aValue"/>
+ <body>
+ return this.insertItemAt(-1, aLabel, aValue);
+ </body>
+ </method>
+
+ <method name="insertItemAt">
+ <parameter name="aIndex"/>
+ <parameter name="aLabel"/>
+ <parameter name="aValue"/>
+ <body>
+ const XULNS =
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+ var item =
+ this.ownerDocument.createElementNS(XULNS, "richlistitem");
+ item.setAttribute("value", aValue);
+
+ var label = this.ownerDocument.createElementNS(XULNS, "label");
+ label.setAttribute("value", aLabel);
+ label.setAttribute("flex", "1");
+ label.setAttribute("crop", "end");
+ item.appendChild(label);
+
+ var before = this.getItemAtIndex(aIndex);
+ if (!before)
+ this.appendChild(item);
+ else
+ this.insertBefore(item, before);
+
+ return item;
+ </body>
+ </method>
+
+ <property name="itemCount" readonly="true"
+ onget="return this.children.length"/>
+
+ <method name="getIndexOfItem">
+ <parameter name="aItem"/>
+ <body>
+ <![CDATA[
+ // don't search the children, if we're looking for none of them
+ if (aItem == null)
+ return -1;
+
+ return this.children.indexOf(aItem);
+ ]]>
+ </body>
+ </method>
+
+ <method name="getItemAtIndex">
+ <parameter name="aIndex"/>
+ <body>
+ return this.children[aIndex] || null;
+ </body>
+ </method>
+
+ <method name="ensureIndexIsVisible">
+ <parameter name="aIndex"/>
+ <body>
+ <![CDATA[
+ // work around missing implementation in scrollBoxObject
+ return this.ensureElementIsVisible(this.getItemAtIndex(aIndex));
+ ]]>
+ </body>
+ </method>
+
+ <method name="ensureElementIsVisible">
+ <parameter name="aElement"/>
+ <body>
+ <![CDATA[
+ if (!aElement)
+ return;
+ var targetRect = aElement.getBoundingClientRect();
+ var scrollRect = this._scrollbox.getBoundingClientRect();
+ var offset = targetRect.top - scrollRect.top;
+ if (offset >= 0) {
+ // scrollRect.bottom wouldn't take a horizontal scroll bar into account
+ let scrollRectBottom = scrollRect.top + this._scrollbox.clientHeight;
+ offset = targetRect.bottom - scrollRectBottom;
+ if (offset <= 0)
+ return;
+ }
+ this._scrollbox.scrollTop += offset;
+ ]]>
+ </body>
+ </method>
+
+ <method name="scrollToIndex">
+ <parameter name="aIndex"/>
+ <body>
+ <![CDATA[
+ var item = this.getItemAtIndex(aIndex);
+ if (item)
+ this.scrollBoxObject.scrollToElement(item);
+ ]]>
+ </body>
+ </method>
+
+ <method name="getNumberOfVisibleRows">
+ <!-- returns the number of currently visible rows -->
+ <!-- don't rely on this function, if the items' height can vary! -->
+ <body>
+ <![CDATA[
+ var children = this.children;
+
+ for (var top = 0; top < children.length && !this._isItemVisible(children[top]); top++);
+ for (var ix = top; ix < children.length && this._isItemVisible(children[ix]); ix++);
+
+ return ix - top;
+ ]]>
+ </body>
+ </method>
+
+ <method name="getIndexOfFirstVisibleRow">
+ <body>
+ <![CDATA[
+ var children = this.children;
+
+ for (var ix = 0; ix < children.length; ix++)
+ if (this._isItemVisible(children[ix]))
+ return ix;
+
+ return -1;
+ ]]>
+ </body>
+ </method>
+
+ <method name="getRowCount">
+ <body>
+ <![CDATA[
+ return this.children.length;
+ ]]>
+ </body>
+ </method>
+
+ <method name="scrollOnePage">
+ <parameter name="aDirection"/> <!-- Must be -1 or 1 -->
+ <body>
+ <![CDATA[
+ var children = this.children;
+
+ if (children.length == 0)
+ return 0;
+
+ // If nothing is selected, we just select the first element
+ // at the extreme we're moving away from
+ if (!this.currentItem)
+ return aDirection == -1 ? children.length : 0;
+
+ // If the current item is visible, scroll by one page so that
+ // the new current item is at approximately the same position as
+ // the existing current item.
+ if (this._isItemVisible(this.currentItem))
+ this.scrollBoxObject.scrollBy(0, this.scrollBoxObject.height * aDirection);
+
+ // Figure out, how many items fully fit into the view port
+ // (including the currently selected one), and determine
+ // the index of the first one lying (partially) outside
+ var height = this.scrollBoxObject.height;
+ var startBorder = this.currentItem.boxObject.y;
+ if (aDirection == -1)
+ startBorder += this.currentItem.boxObject.height;
+
+ var index = this.currentIndex;
+ for (var ix = index; 0 <= ix && ix < children.length; ix += aDirection) {
+ var boxObject = children[ix].boxObject;
+ if (boxObject.height == 0)
+ continue; // hidden children have a y of 0
+ var endBorder = boxObject.y + (aDirection == -1 ? boxObject.height : 0);
+ if ((endBorder - startBorder) * aDirection > height)
+ break; // we've reached the desired distance
+ index = ix;
+ }
+
+ return index != this.currentIndex ? index - this.currentIndex : aDirection;
+ ]]>
+ </body>
+ </method>
+
+ <!-- richlistbox specific -->
+ <property name="children" readonly="true">
+ <getter>
+ <![CDATA[
+ var childNodes = [];
+ var isReverse = this.dir == "reverse" && this._mayReverse;
+ var child = isReverse ? this.lastChild : this.firstChild;
+ var prop = isReverse ? "previousSibling" : "nextSibling";
+ while (child) {
+ if (child instanceof Components.interfaces.nsIDOMXULSelectControlItemElement)
+ childNodes.push(child);
+ child = child[prop];
+ }
+ return childNodes;
+ ]]>
+ </getter>
+ </property>
+
+ <field name="_builderListener" readonly="true">
+ <![CDATA[
+ ({
+ mOuter: this,
+ item: null,
+ willRebuild: function(builder) { },
+ didRebuild: function(builder) {
+ this.mOuter._refreshSelection();
+ }
+ });
+ ]]>
+ </field>
+
+ <method name="_refreshSelection">
+ <body>
+ <![CDATA[
+ // when this method is called, we know that either the currentItem
+ // and selectedItems we have are null (ctor) or a reference to an
+ // element no longer in the DOM (template).
+
+ // first look for the last-selected attribute
+ var state = this.getAttribute("last-selected");
+ if (state) {
+ var ids = state.split(" ");
+
+ var suppressSelect = this._suppressOnSelect;
+ this._suppressOnSelect = true;
+ this.clearSelection();
+ for (let i = 1; i < ids.length; i++) {
+ var selectedItem = document.getElementById(ids[i]);
+ if (selectedItem)
+ this.addItemToSelection(selectedItem);
+ }
+
+ var currentItem = document.getElementById(ids[0]);
+ if (!currentItem && this._currentIndex)
+ currentItem = this.getItemAtIndex(Math.min(
+ this._currentIndex - 1, this.getRowCount()));
+ if (currentItem) {
+ this.currentItem = currentItem;
+ if (this.selType != "multiple" && this.selectedCount == 0)
+ this.selectedItem = currentItem;
+
+ if (this.scrollBoxObject.height) {
+ this.ensureElementIsVisible(currentItem);
+ }
+ else {
+ // XXX hack around a bug in ensureElementIsVisible as it will
+ // scroll beyond the last element, bug 493645.
+ var previousElement = this.dir == "reverse" ? currentItem.nextSibling :
+ currentItem.previousSibling;
+ this.ensureElementIsVisible(previousElement);
+ }
+ }
+ this._suppressOnSelect = suppressSelect;
+ // XXX actually it's just a refresh, but at least
+ // the Extensions manager expects this:
+ this._fireOnSelect();
+ return;
+ }
+
+ // try to restore the selected items according to their IDs
+ // (applies after a template rebuild, if last-selected was not set)
+ if (this.selectedItems) {
+ let itemIds = [];
+ for (let i = this.selectedCount - 1; i >= 0; i--) {
+ let selectedItem = this.selectedItems[i];
+ itemIds.push(selectedItem.id);
+ this.selectedItems.remove(selectedItem);
+ }
+ for (let i = 0; i < itemIds.length; i++) {
+ let selectedItem = document.getElementById(itemIds[i]);
+ if (selectedItem) {
+ this.selectedItems.append(selectedItem);
+ }
+ }
+ }
+ if (this.currentItem && this.currentItem.id)
+ this.currentItem = document.getElementById(this.currentItem.id);
+ else
+ this.currentItem = null;
+
+ // if we have no previously current item or if the above check fails to
+ // find the previous nodes (which causes it to clear selection)
+ if (!this.currentItem && this.selectedCount == 0) {
+ this.currentIndex = this._currentIndex ? this._currentIndex - 1 : 0;
+
+ // cf. listbox constructor:
+ // select items according to their attributes
+ var children = this.children;
+ for (let i = 0; i < children.length; ++i) {
+ if (children[i].getAttribute("selected") == "true")
+ this.selectedItems.append(children[i]);
+ }
+ }
+
+ if (this.selType != "multiple" && this.selectedCount == 0)
+ this.selectedItem = this.currentItem;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_isItemVisible">
+ <parameter name="aItem"/>
+ <body>
+ <![CDATA[
+ if (!aItem)
+ return false;
+
+ var y = this.scrollBoxObject.positionY + this.scrollBoxObject.y;
+
+ // Partially visible items are also considered visible
+ return (aItem.boxObject.y + aItem.boxObject.height > y) &&
+ (aItem.boxObject.y < y + this.scrollBoxObject.height);
+ ]]>
+ </body>
+ </method>
+
+ <field name="_currentIndex">null</field>
+
+ <!-- For backwards-compatibility and for convenience.
+ Use getIndexOfItem instead. -->
+ <method name="getIndexOf">
+ <parameter name="aElement"/>
+ <body>
+ <![CDATA[
+ return this.getIndexOfItem(aElement);
+ ]]>
+ </body>
+ </method>
+
+ <!-- For backwards-compatibility and for convenience.
+ Use ensureElementIsVisible instead -->
+ <method name="ensureSelectedElementIsVisible">
+ <body>
+ <![CDATA[
+ return this.ensureElementIsVisible(this.selectedItem);
+ ]]>
+ </body>
+ </method>
+
+ <!-- For backwards-compatibility and for convenience.
+ Use moveByOffset instead. -->
+ <method name="goUp">
+ <body>
+ <![CDATA[
+ var index = this.currentIndex;
+ this.moveByOffset(-1, true, false);
+ return index != this.currentIndex;
+ ]]>
+ </body>
+ </method>
+ <method name="goDown">
+ <body>
+ <![CDATA[
+ var index = this.currentIndex;
+ this.moveByOffset(1, true, false);
+ return index != this.currentIndex;
+ ]]>
+ </body>
+ </method>
+
+ <!-- deprecated (is implied by currentItem and selectItem) -->
+ <method name="fireActiveItemEvent"><body/></method>
+ </implementation>
+
+ <handlers>
+ <handler event="click">
+ <![CDATA[
+ // clicking into nothing should unselect
+ if (event.originalTarget == this._scrollbox) {
+ this.clearSelection();
+ this.currentItem = null;
+ }
+ ]]>
+ </handler>
+
+ <handler event="MozSwipeGesture">
+ <![CDATA[
+ // Only handle swipe gestures up and down
+ switch (event.direction) {
+ case event.DIRECTION_DOWN:
+ this._scrollbox.scrollTop = this._scrollbox.scrollHeight;
+ break;
+ case event.DIRECTION_UP:
+ this._scrollbox.scrollTop = 0;
+ break;
+ }
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="richlistitem"
+ extends="chrome://global/content/bindings/listbox.xml#listitem">
+ <content>
+ <children/>
+ </content>
+
+ <resources>
+ <stylesheet src="chrome://global/skin/richlistbox.css"/>
+ </resources>
+
+ <implementation>
+ <destructor>
+ <![CDATA[
+ var control = this.control;
+ if (!control)
+ return;
+ // When we are destructed and we are current or selected, unselect ourselves
+ // so that richlistbox's selection doesn't point to something not in the DOM.
+ // We don't want to reset last-selected, so we set _suppressOnSelect.
+ if (this.selected) {
+ var suppressSelect = control._suppressOnSelect;
+ control._suppressOnSelect = true;
+ control.removeItemFromSelection(this);
+ control._suppressOnSelect = suppressSelect;
+ }
+ if (this.current)
+ control.currentItem = null;
+ ]]>
+ </destructor>
+
+ <property name="label" readonly="true">
+ <!-- Setter purposely not implemented; the getter returns a
+ concatentation of label text to expose via accessibility APIs -->
+ <getter>
+ <![CDATA[
+ const XULNS =
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ return Array.map(this.getElementsByTagNameNS(XULNS, "label"),
+ label => label.value)
+ .join(" ");
+ ]]>
+ </getter>
+ </property>
+
+ <property name="searchLabel">
+ <getter>
+ <![CDATA[
+ return this.hasAttribute("searchlabel") ?
+ this.getAttribute("searchlabel") : this.label;
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ if (val !== null)
+ this.setAttribute("searchlabel", val);
+ else
+ // fall back to the label property (default value)
+ this.removeAttribute("searchlabel");
+ return val;
+ ]]>
+ </setter>
+ </property>
+ </implementation>
+ </binding>
+</bindings>
diff --git a/toolkit/content/widgets/scale.xml b/toolkit/content/widgets/scale.xml
new file mode 100644
index 000000000..3e5f5aeb2
--- /dev/null
+++ b/toolkit/content/widgets/scale.xml
@@ -0,0 +1,232 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<bindings id="scaleBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="scalethumb" extends="xul:button" role="xul:thumb">
+ <resources>
+ <stylesheet src="chrome://global/skin/scale.css"/>
+ </resources>
+ </binding>
+
+ <binding id="scaleslider" display="xul:slider"
+ extends="chrome://global/content/bindings/general.xml#basecontrol">
+ <resources>
+ <stylesheet src="chrome://global/skin/scale.css"/>
+ </resources>
+ </binding>
+
+ <binding id="scale" role="xul:scale"
+ extends="chrome://global/content/bindings/general.xml#basecontrol">
+ <resources>
+ <stylesheet src="chrome://global/skin/scale.css"/>
+ </resources>
+
+ <content align="center" pack="center">
+ <xul:slider anonid="slider" class="scale-slider" snap="true" flex="1"
+ xbl:inherits="disabled,orient,dir,curpos=value,minpos=min,maxpos=max,increment,pageincrement,movetoclick">
+ <xul:thumb class="scale-thumb" xbl:inherits="disabled,orient"/>
+ </xul:slider>
+ </content>
+
+ <implementation implements="nsISliderListener">
+ <property name="value" onget="return this._getIntegerAttribute('curpos', 0);"
+ onset="return this._setIntegerAttribute('curpos', val);"/>
+ <property name="min" onget="return this._getIntegerAttribute('minpos', 0);"
+ onset="return this._setIntegerAttribute('minpos', val);"/>
+ <property name="max" onget="return this._getIntegerAttribute('maxpos', 100);"
+ onset="return this._setIntegerAttribute('maxpos', val);"/>
+ <property name="increment" onget="return this._getIntegerAttribute('increment', 1);"
+ onset="return this._setIntegerAttribute('increment', val);"/>
+ <property name="pageIncrement" onget="return this._getIntegerAttribute('pageincrement', 10);"
+ onset="return this._setIntegerAttribute('pageincrement', val);"/>
+
+ <property name="_slider" readonly="true">
+ <getter>
+ if (!this._sliderElement)
+ this._sliderElement = document.getAnonymousElementByAttribute(this, "anonid", "slider");
+ return this._sliderElement;
+ </getter>
+ </property>
+
+ <constructor>
+ <![CDATA[
+ this._userChanged = false;
+ var value = parseInt(this.getAttribute("value"), 10);
+ if (!isNaN(value))
+ this.value = value;
+ else if (this.min > 0)
+ this.value = this.min;
+ else if (this.max < 0)
+ this.value = this.max;
+ ]]>
+ </constructor>
+
+ <method name="_getIntegerAttribute">
+ <parameter name="aAttr"/>
+ <parameter name="aDefaultValue"/>
+ <body>
+ var value = this._slider.getAttribute(aAttr);
+ var intvalue = parseInt(value, 10);
+ if (!isNaN(intvalue))
+ return intvalue;
+ return aDefaultValue;
+ </body>
+ </method>
+
+ <method name="_setIntegerAttribute">
+ <parameter name="aAttr"/>
+ <parameter name="aValue"/>
+ <body>
+ <![CDATA[
+ var intvalue = parseInt(aValue, 10);
+ if (!isNaN(intvalue)) {
+ if (aAttr == "curpos") {
+ if (intvalue < this.min)
+ intvalue = this.min;
+ else if (intvalue > this.max)
+ intvalue = this.max;
+ }
+ this._slider.setAttribute(aAttr, intvalue);
+ }
+ return aValue;
+ ]]>
+ </body>
+ </method>
+
+ <method name="decrease">
+ <body>
+ <![CDATA[
+ var newpos = this.value - this.increment;
+ var startpos = this.min;
+ this.value = (newpos > startpos) ? newpos : startpos;
+ ]]>
+ </body>
+ </method>
+ <method name="increase">
+ <body>
+ <![CDATA[
+ var newpos = this.value + this.increment;
+ var endpos = this.max;
+ this.value = (newpos < endpos) ? newpos : endpos;
+ ]]>
+ </body>
+ </method>
+
+ <method name="decreasePage">
+ <body>
+ <![CDATA[
+ var newpos = this.value - this.pageIncrement;
+ var startpos = this.min;
+ this.value = (newpos > startpos) ? newpos : startpos;
+ ]]>
+ </body>
+ </method>
+ <method name="increasePage">
+ <body>
+ <![CDATA[
+ var newpos = this.value + this.pageIncrement;
+ var endpos = this.max;
+ this.value = (newpos < endpos) ? newpos : endpos;
+ ]]>
+ </body>
+ </method>
+
+ <method name="valueChanged">
+ <parameter name="which"/>
+ <parameter name="newValue"/>
+ <parameter name="userChanged"/>
+ <body>
+ <![CDATA[
+ switch (which) {
+ case "curpos":
+ this.setAttribute("value", newValue);
+
+ // in the future, only fire the change event when userChanged
+ // or _userChanged is true
+ var changeEvent = document.createEvent("Events");
+ changeEvent.initEvent("change", true, true);
+ this.dispatchEvent(changeEvent);
+ break;
+
+ case "minpos":
+ this.setAttribute("min", newValue);
+ break;
+
+ case "maxpos":
+ this.setAttribute("max", newValue);
+ break;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="dragStateChanged">
+ <parameter name="isDragging"/>
+ <body/>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="keypress" keycode="VK_LEFT" preventdefault="true">
+ <![CDATA[
+ this._userChanged = true;
+ (this.orient != "vertical" && this.dir == "reverse") ? this.increase() : this.decrease();
+ this._userChanged = false;
+ ]]>
+ </handler>
+ <handler event="keypress" keycode="VK_RIGHT" preventdefault="true">
+ <![CDATA[
+ this._userChanged = true;
+ (this.orient != "vertical" && this.dir == "reverse") ? this.decrease() : this.increase();
+ this._userChanged = false;
+ ]]>
+ </handler>
+ <handler event="keypress" keycode="VK_UP" preventdefault="true">
+ <![CDATA[
+ this._userChanged = true;
+ (this.orient == "vertical" && this.dir != "reverse") ? this.decrease() : this.increase();
+ this._userChanged = false;
+ ]]>
+ </handler>
+ <handler event="keypress" keycode="VK_DOWN" preventdefault="true">
+ <![CDATA[
+ this._userChanged = true;
+ (this.orient == "vertical" && this.dir != "reverse") ? this.increase() : this.decrease();
+ this._userChanged = false;
+ ]]>
+ </handler>
+ <handler event="keypress" keycode="VK_PAGE_UP" preventdefault="true">
+ <![CDATA[
+ this._userChanged = true;
+ (this.orient == "vertical" && this.dir != "reverse") ? this.decreasePage() : this.increasePage();
+ this._userChanged = false;
+ ]]>
+ </handler>
+ <handler event="keypress" keycode="VK_PAGE_DOWN" preventdefault="true">
+ <![CDATA[
+ this._userChanged = true;
+ (this.orient == "vertical" && this.dir != "reverse") ? this.increasePage() : this.decreasePage();
+ this._userChanged = false;
+ ]]>
+ </handler>
+ <handler event="keypress" keycode="VK_HOME" preventdefault="true">
+ this._userChanged = true;
+ this.value = (this.dir == "reverse") ? this.max : this.min;
+ this._userChanged = false;
+ </handler>
+ <handler event="keypress" keycode="VK_END" preventdefault="true">
+ this._userChanged = true;
+ this.value = (this.dir == "reverse") ? this.min : this.max;
+ this._userChanged = false;
+ </handler>
+ </handlers>
+
+ </binding>
+</bindings>
diff --git a/toolkit/content/widgets/scrollbar.xml b/toolkit/content/widgets/scrollbar.xml
new file mode 100644
index 000000000..ce2eff11b
--- /dev/null
+++ b/toolkit/content/widgets/scrollbar.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<bindings id="scrollbarBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="thumb" extends="xul:button" />
+
+ <binding id="scrollbar-base" bindToUntrustedContent="true">
+ <handlers>
+ <handler event="contextmenu" preventdefault="true" action="event.stopPropagation();"/>
+ <handler event="click" preventdefault="true" action="event.stopPropagation();"/>
+ <handler event="dblclick" action="event.stopPropagation();"/>
+ <handler event="command" action="event.stopPropagation();"/>
+ </handlers>
+ </binding>
+
+ <binding id="scrollbar" bindToUntrustedContent="true" extends="chrome://global/content/bindings/scrollbar.xml#scrollbar-base">
+ <content clickthrough="always">
+ <xul:scrollbarbutton sbattr="scrollbar-up-top" type="decrement" xbl:inherits="curpos,maxpos,disabled,sborient=orient"/>
+ <xul:scrollbarbutton sbattr="scrollbar-down-top" type="increment" xbl:inherits="curpos,maxpos,disabled,sborient=orient"/>
+ <xul:slider flex="1" xbl:inherits="disabled,curpos,maxpos,pageincrement,increment,orient,sborient=orient">
+ <xul:thumb sbattr="scrollbar-thumb" xbl:inherits="orient,sborient=orient,collapsed=disabled"
+ align="center" pack="center"/>
+ </xul:slider>
+ <xul:scrollbarbutton sbattr="scrollbar-up-bottom" type="decrement" xbl:inherits="curpos,maxpos,disabled,sborient=orient"/>
+ <xul:scrollbarbutton sbattr="scrollbar-down-bottom" type="increment" xbl:inherits="curpos,maxpos,disabled,sborient=orient"/>
+ </content>
+ </binding>
+</bindings>
diff --git a/toolkit/content/widgets/scrollbox.xml b/toolkit/content/widgets/scrollbox.xml
new file mode 100644
index 000000000..ff57a5911
--- /dev/null
+++ b/toolkit/content/widgets/scrollbox.xml
@@ -0,0 +1,908 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<bindings id="arrowscrollboxBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="scrollbox-base" extends="chrome://global/content/bindings/general.xml#basecontrol">
+ <resources>
+ <stylesheet src="chrome://global/skin/scrollbox.css"/>
+ </resources>
+ </binding>
+
+ <binding id="scrollbox" extends="chrome://global/content/bindings/scrollbox.xml#scrollbox-base">
+ <content>
+ <xul:box class="box-inherit scrollbox-innerbox" xbl:inherits="orient,align,pack,dir" flex="1">
+ <children/>
+ </xul:box>
+ </content>
+ </binding>
+
+ <binding id="arrowscrollbox" extends="chrome://global/content/bindings/scrollbox.xml#scrollbox-base">
+ <content>
+ <xul:autorepeatbutton class="autorepeatbutton-up"
+ anonid="scrollbutton-up"
+ xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtostart"
+ oncommand="_autorepeatbuttonScroll(event);"/>
+ <xul:spacer class="arrowscrollbox-overflow-start-indicator"
+ xbl:inherits="collapsed=scrolledtostart"/>
+ <xul:scrollbox class="arrowscrollbox-scrollbox"
+ anonid="scrollbox"
+ flex="1"
+ xbl:inherits="orient,align,pack,dir">
+ <children/>
+ </xul:scrollbox>
+ <xul:spacer class="arrowscrollbox-overflow-end-indicator"
+ xbl:inherits="collapsed=scrolledtoend"/>
+ <xul:autorepeatbutton class="autorepeatbutton-down"
+ anonid="scrollbutton-down"
+ xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtoend"
+ oncommand="_autorepeatbuttonScroll(event);"/>
+ </content>
+
+ <implementation>
+ <constructor><![CDATA[
+ this.setAttribute("notoverflowing", "true");
+ this._updateScrollButtonsDisabledState();
+ ]]></constructor>
+
+ <destructor><![CDATA[
+ this._stopSmoothScroll();
+ ]]></destructor>
+
+ <field name="_scrollbox">
+ document.getAnonymousElementByAttribute(this, "anonid", "scrollbox");
+ </field>
+ <field name="_scrollButtonUp">
+ document.getAnonymousElementByAttribute(this, "anonid", "scrollbutton-up");
+ </field>
+ <field name="_scrollButtonDown">
+ document.getAnonymousElementByAttribute(this, "anonid", "scrollbutton-down");
+ </field>
+
+ <field name="__prefBranch">null</field>
+ <property name="_prefBranch" readonly="true">
+ <getter><![CDATA[
+ if (this.__prefBranch === null) {
+ this.__prefBranch = Components.classes['@mozilla.org/preferences-service;1']
+ .getService(Components.interfaces.nsIPrefBranch);
+ }
+ return this.__prefBranch;
+ ]]></getter>
+ </property>
+
+ <field name="_scrollIncrement">null</field>
+ <property name="scrollIncrement" readonly="true">
+ <getter><![CDATA[
+ if (this._scrollIncrement === null) {
+ try {
+ this._scrollIncrement = this._prefBranch
+ .getIntPref("toolkit.scrollbox.scrollIncrement");
+ }
+ catch (ex) {
+ this._scrollIncrement = 20;
+ }
+ }
+ return this._scrollIncrement;
+ ]]></getter>
+ </property>
+
+ <field name="_smoothScroll">null</field>
+ <property name="smoothScroll">
+ <getter><![CDATA[
+ if (this._smoothScroll === null) {
+ if (this.hasAttribute("smoothscroll")) {
+ this._smoothScroll = (this.getAttribute("smoothscroll") == "true");
+ } else {
+ try {
+ this._smoothScroll = this._prefBranch
+ .getBoolPref("toolkit.scrollbox.smoothScroll");
+ }
+ catch (ex) {
+ this._smoothScroll = true;
+ }
+ }
+ }
+ return this._smoothScroll;
+ ]]></getter>
+ <setter><![CDATA[
+ this._smoothScroll = val;
+ return val;
+ ]]></setter>
+ </property>
+
+ <field name="_scrollBoxObject">null</field>
+ <property name="scrollBoxObject" readonly="true">
+ <getter><![CDATA[
+ if (!this._scrollBoxObject) {
+ this._scrollBoxObject = this._scrollbox.boxObject;
+ }
+ return this._scrollBoxObject;
+ ]]></getter>
+ </property>
+
+ <property name="scrollClientRect" readonly="true">
+ <getter><![CDATA[
+ return this._scrollbox.getBoundingClientRect();
+ ]]></getter>
+ </property>
+
+ <property name="scrollClientSize" readonly="true">
+ <getter><![CDATA[
+ return this.orient == "vertical" ?
+ this._scrollbox.clientHeight :
+ this._scrollbox.clientWidth;
+ ]]></getter>
+ </property>
+
+ <property name="scrollSize" readonly="true">
+ <getter><![CDATA[
+ return this.orient == "vertical" ?
+ this._scrollbox.scrollHeight :
+ this._scrollbox.scrollWidth;
+ ]]></getter>
+ </property>
+ <property name="scrollPaddingRect" readonly="true">
+ <getter><![CDATA[
+ // This assumes that this._scrollbox doesn't have any border.
+ var outerRect = this.scrollClientRect;
+ var innerRect = {};
+ innerRect.left = outerRect.left - this._scrollbox.scrollLeft;
+ innerRect.top = outerRect.top - this._scrollbox.scrollTop;
+ innerRect.right = innerRect.left + this._scrollbox.scrollWidth;
+ innerRect.bottom = innerRect.top + this._scrollbox.scrollHeight;
+ return innerRect;
+ ]]></getter>
+ </property>
+ <property name="scrollboxPaddingStart" readonly="true">
+ <getter><![CDATA[
+ var ltr = (window.getComputedStyle(this, null).direction == "ltr");
+ var paddingStartName = ltr ? "padding-left" : "padding-right";
+ var scrollboxStyle = window.getComputedStyle(this._scrollbox, null);
+ return parseFloat(scrollboxStyle.getPropertyValue(paddingStartName));
+ ]]></getter>
+ </property>
+ <property name="scrollPosition">
+ <getter><![CDATA[
+ return this.orient == "vertical" ?
+ this._scrollbox.scrollTop :
+ this._scrollbox.scrollLeft;
+ ]]></getter>
+ <setter><![CDATA[
+ if (this.orient == "vertical")
+ this._scrollbox.scrollTop = val;
+ else
+ this._scrollbox.scrollLeft = val;
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="_startEndProps" readonly="true">
+ <getter><![CDATA[
+ return this.orient == "vertical" ?
+ ["top", "bottom"] : ["left", "right"];
+ ]]></getter>
+ </property>
+
+ <field name="_isRTLScrollbox"><![CDATA[
+ this.orient != "vertical" &&
+ document.defaultView.getComputedStyle(this._scrollbox, "").direction == "rtl";
+ ]]></field>
+
+ <field name="_scrollTarget">null</field>
+
+ <method name="_canScrollToElement">
+ <parameter name="element"/>
+ <body><![CDATA[
+ return window.getComputedStyle(element).display != "none";
+ ]]></body>
+ </method>
+
+ <method name="ensureElementIsVisible">
+ <parameter name="element"/>
+ <parameter name="aSmoothScroll"/>
+ <body><![CDATA[
+ if (!this._canScrollToElement(element))
+ return;
+
+ var vertical = this.orient == "vertical";
+ var rect = this.scrollClientRect;
+ var containerStart = vertical ? rect.top : rect.left;
+ var containerEnd = vertical ? rect.bottom : rect.right;
+ rect = element.getBoundingClientRect();
+ var elementStart = vertical ? rect.top : rect.left;
+ var elementEnd = vertical ? rect.bottom : rect.right;
+
+ var scrollPaddingRect = this.scrollPaddingRect;
+ let style = window.getComputedStyle(this._scrollbox, null);
+ var scrollContentRect = {
+ left: scrollPaddingRect.left + parseFloat(style.paddingLeft),
+ top: scrollPaddingRect.top + parseFloat(style.paddingTop),
+ right: scrollPaddingRect.right - parseFloat(style.paddingRight),
+ bottom: scrollPaddingRect.bottom - parseFloat(style.paddingBottom)
+ };
+
+ // Provide an entry point for derived bindings to adjust these values.
+ if (this._adjustElementStartAndEnd) {
+ [elementStart, elementEnd] =
+ this._adjustElementStartAndEnd(element, elementStart, elementEnd);
+ }
+
+ if (elementStart <= (vertical ? scrollContentRect.top : scrollContentRect.left)) {
+ elementStart = vertical ? scrollPaddingRect.top : scrollPaddingRect.left;
+ }
+ if (elementEnd >= (vertical ? scrollContentRect.bottom : scrollContentRect.right)) {
+ elementEnd = vertical ? scrollPaddingRect.bottom : scrollPaddingRect.right;
+ }
+
+ var amountToScroll;
+
+ if (elementStart < containerStart) {
+ amountToScroll = elementStart - containerStart;
+ } else if (containerEnd < elementEnd) {
+ amountToScroll = elementEnd - containerEnd;
+ } else if (this._isScrolling) {
+ // decelerate if a currently-visible element is selected during the scroll
+ const STOP_DISTANCE = 15;
+ if (this._isScrolling == -1 && elementStart - STOP_DISTANCE < containerStart)
+ amountToScroll = elementStart - containerStart;
+ else if (this._isScrolling == 1 && containerEnd - STOP_DISTANCE < elementEnd)
+ amountToScroll = elementEnd - containerEnd;
+ else
+ amountToScroll = this._isScrolling * STOP_DISTANCE;
+ } else {
+ return;
+ }
+
+ this._stopSmoothScroll();
+
+ if (aSmoothScroll != false && this.smoothScroll) {
+ this._smoothScrollByPixels(amountToScroll, element);
+ } else {
+ this.scrollByPixels(amountToScroll);
+ }
+ ]]></body>
+ </method>
+
+ <method name="_smoothScrollByPixels">
+ <parameter name="amountToScroll"/>
+ <parameter name="element"/><!-- optional -->
+ <body><![CDATA[
+ this._stopSmoothScroll();
+ if (amountToScroll == 0)
+ return;
+
+ this._scrollTarget = element;
+ // Positive amountToScroll makes us scroll right (elements fly left), negative scrolls left.
+ this._isScrolling = amountToScroll < 0 ? -1 : 1;
+
+ this._scrollAnim.start(amountToScroll);
+ ]]></body>
+ </method>
+
+ <field name="_scrollAnim"><![CDATA[({
+ scrollbox: this,
+ requestHandle: 0, /* 0 indicates there is no pending request */
+ start: function scrollAnim_start(distance) {
+ this.distance = distance;
+ this.startPos = this.scrollbox.scrollPosition;
+ this.duration = Math.min(1000, Math.round(50 * Math.sqrt(Math.abs(distance))));
+ this.startTime = window.performance.now();
+
+ if (!this.requestHandle)
+ this.requestHandle = window.requestAnimationFrame(this.sample.bind(this));
+ },
+ stop: function scrollAnim_stop() {
+ window.cancelAnimationFrame(this.requestHandle);
+ this.requestHandle = 0;
+ },
+ sample: function scrollAnim_handleEvent(timeStamp) {
+ const timePassed = timeStamp - this.startTime;
+ const pos = timePassed >= this.duration ? 1 :
+ 1 - Math.pow(1 - timePassed / this.duration, 4);
+
+ this.scrollbox.scrollPosition = this.startPos + (this.distance * pos);
+
+ if (pos == 1)
+ this.scrollbox._stopSmoothScroll();
+ else
+ this.requestHandle = window.requestAnimationFrame(this.sample.bind(this));
+ }
+ })]]></field>
+
+ <method name="scrollByIndex">
+ <parameter name="index"/>
+ <parameter name="aSmoothScroll"/>
+ <body><![CDATA[
+ if (index == 0)
+ return;
+
+ // Each scrollByIndex call is expected to scroll the given number of
+ // items. If a previous call is still in progress because of smooth
+ // scrolling, we need to complete it before starting a new one.
+ if (this._scrollTarget) {
+ let elements = this._getScrollableElements();
+ if (this._scrollTarget != elements[0] &&
+ this._scrollTarget != elements[elements.length - 1])
+ this.ensureElementIsVisible(this._scrollTarget, false);
+ }
+
+ var rect = this.scrollClientRect;
+ var [start, end] = this._startEndProps;
+ var x = index > 0 ? rect[end] + 1 : rect[start] - 1;
+ var nextElement = this._elementFromPoint(x, index);
+ if (!nextElement)
+ return;
+
+ var targetElement;
+ if (this._isRTLScrollbox)
+ index *= -1;
+ while (index < 0 && nextElement) {
+ if (this._canScrollToElement(nextElement))
+ targetElement = nextElement;
+ nextElement = nextElement.previousSibling;
+ index++;
+ }
+ while (index > 0 && nextElement) {
+ if (this._canScrollToElement(nextElement))
+ targetElement = nextElement;
+ nextElement = nextElement.nextSibling;
+ index--;
+ }
+ if (!targetElement)
+ return;
+
+ this.ensureElementIsVisible(targetElement, aSmoothScroll);
+ ]]></body>
+ </method>
+
+ <method name="scrollByPage">
+ <parameter name="pageDelta"/>
+ <parameter name="aSmoothScroll"/>
+ <body><![CDATA[
+ if (pageDelta == 0)
+ return;
+
+ // If a previous call is still in progress because of smooth
+ // scrolling, we need to complete it before starting a new one.
+ if (this._scrollTarget) {
+ let elements = this._getScrollableElements();
+ if (this._scrollTarget != elements[0] &&
+ this._scrollTarget != elements[elements.length - 1])
+ this.ensureElementIsVisible(this._scrollTarget, false);
+ }
+
+ var [start, end] = this._startEndProps;
+ var rect = this.scrollClientRect;
+ var containerEdge = pageDelta > 0 ? rect[end] + 1 : rect[start] - 1;
+ var pixelDelta = pageDelta * (rect[end] - rect[start]);
+ var destinationPosition = containerEdge + pixelDelta;
+ var nextElement = this._elementFromPoint(containerEdge, pageDelta);
+ if (!nextElement)
+ return;
+
+ // We need to iterate over our elements in the direction of pageDelta.
+ // pageDelta is the physical direction, so in a horizontal scroll box,
+ // positive values scroll to the right no matter if the scrollbox is
+ // LTR or RTL. But RTL changes how we need to advance the iteration
+ // (whether to get the next or the previous sibling of the current
+ // element).
+ var logicalAdvanceDir = pageDelta * (this._isRTLScrollbox ? -1 : 1);
+ var advance = logicalAdvanceDir > 0 ? (e => e.nextSibling) : (e => e.previousSibling);
+
+ var extendsPastTarget = (pageDelta > 0)
+ ? (e => e.getBoundingClientRect()[end] > destinationPosition)
+ : (e => e.getBoundingClientRect()[start] < destinationPosition);
+
+ // We want to scroll to the last element we encounter before we find
+ // an element which extends past destinationPosition.
+ var targetElement;
+ do {
+ if (this._canScrollToElement(nextElement))
+ targetElement = nextElement;
+ nextElement = advance(nextElement);
+ } while (nextElement && !extendsPastTarget(nextElement));
+
+ if (!targetElement)
+ return;
+
+ this.ensureElementIsVisible(targetElement, aSmoothScroll);
+ ]]></body>
+ </method>
+
+ <method name="_getScrollableElements">
+ <body><![CDATA[
+ var nodes = this.childNodes;
+ if (nodes.length == 1 &&
+ nodes[0].localName == "children" &&
+ nodes[0].namespaceURI == "http://www.mozilla.org/xbl") {
+ nodes = document.getBindingParent(this).childNodes;
+ }
+
+ return Array.filter(nodes, this._canScrollToElement, this);
+ ]]></body>
+ </method>
+
+ <method name="_elementFromPoint">
+ <parameter name="aX"/>
+ <parameter name="aPhysicalScrollDir"/>
+ <body><![CDATA[
+ var elements = this._getScrollableElements();
+ if (!elements.length)
+ return null;
+
+ if (this._isRTLScrollbox)
+ elements.reverse();
+
+ var [start, end] = this._startEndProps;
+ var low = 0;
+ var high = elements.length - 1;
+
+ if (aX < elements[low].getBoundingClientRect()[start] ||
+ aX > elements[high].getBoundingClientRect()[end])
+ return null;
+
+ var mid, rect;
+ while (low <= high) {
+ mid = Math.floor((low + high) / 2);
+ rect = elements[mid].getBoundingClientRect();
+ if (rect[start] > aX)
+ high = mid - 1;
+ else if (rect[end] < aX)
+ low = mid + 1;
+ else
+ return elements[mid];
+ }
+
+ // There's no element at the requested coordinate, but the algorithm
+ // from above yields an element next to it, in a random direction.
+ // The desired scrolling direction leads to the correct element.
+
+ if (!aPhysicalScrollDir)
+ return null;
+
+ if (aPhysicalScrollDir < 0 && rect[start] > aX)
+ mid = Math.max(mid - 1, 0);
+ else if (aPhysicalScrollDir > 0 && rect[end] < aX)
+ mid = Math.min(mid + 1, elements.length - 1);
+
+ return elements[mid];
+ ]]></body>
+ </method>
+
+ <method name="_autorepeatbuttonScroll">
+ <parameter name="event"/>
+ <body><![CDATA[
+ var dir = event.originalTarget == this._scrollButtonUp ? -1 : 1;
+ if (this._isRTLScrollbox)
+ dir *= -1;
+
+ this.scrollByPixels(this.scrollIncrement * dir);
+
+ event.stopPropagation();
+ ]]></body>
+ </method>
+
+ <method name="scrollByPixels">
+ <parameter name="px"/>
+ <body><![CDATA[
+ this.scrollPosition += px;
+ ]]></body>
+ </method>
+
+ <!-- 0: idle
+ 1: scrolling right
+ -1: scrolling left -->
+ <field name="_isScrolling">0</field>
+ <field name="_prevMouseScrolls">[null, null]</field>
+
+ <field name="_touchStart">-1</field>
+
+ <method name="_stopSmoothScroll">
+ <body><![CDATA[
+ if (this._isScrolling) {
+ this._scrollAnim.stop();
+ this._isScrolling = 0;
+ this._scrollTarget = null;
+ }
+ ]]></body>
+ </method>
+
+ <method name="_updateScrollButtonsDisabledState">
+ <body><![CDATA[
+ var scrolledToStart = false;
+ var scrolledToEnd = false;
+
+ if (this.hasAttribute("notoverflowing")) {
+ scrolledToStart = true;
+ scrolledToEnd = true;
+ }
+ else if (this.scrollPosition == 0) {
+ // In the RTL case, this means the _last_ element in the
+ // scrollbox is visible
+ if (this._isRTLScrollbox)
+ scrolledToEnd = true;
+ else
+ scrolledToStart = true;
+ }
+ else if (this.scrollClientSize + this.scrollPosition == this.scrollSize) {
+ // In the RTL case, this means the _first_ element in the
+ // scrollbox is visible
+ if (this._isRTLScrollbox)
+ scrolledToStart = true;
+ else
+ scrolledToEnd = true;
+ }
+
+ if (scrolledToEnd)
+ this.setAttribute("scrolledtoend", "true");
+ else
+ this.removeAttribute("scrolledtoend");
+
+ if (scrolledToStart)
+ this.setAttribute("scrolledtostart", "true");
+ else
+ this.removeAttribute("scrolledtostart");
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="wheel"><![CDATA[
+ if (this.orient == "vertical") {
+ if (event.deltaMode == event.DOM_DELTA_PIXEL)
+ this.scrollByPixels(event.deltaY);
+ else if (event.deltaMode == event.DOM_DELTA_PAGE)
+ this.scrollByPage(event.deltaY);
+ else
+ this.scrollByIndex(event.deltaY);
+ }
+ // We allow vertical scrolling to scroll a horizontal scrollbox
+ // because many users have a vertical scroll wheel but no
+ // horizontal support.
+ // Because of this, we need to avoid scrolling chaos on trackpads
+ // and mouse wheels that support simultaneous scrolling in both axes.
+ // We do this by scrolling only when the last two scroll events were
+ // on the same axis as the current scroll event.
+ // For diagonal scroll events we only respect the dominant axis.
+ else {
+ let isVertical = Math.abs(event.deltaY) > Math.abs(event.deltaX);
+ let delta = isVertical ? event.deltaY : event.deltaX;
+ let scrollByDelta = isVertical && this._isRTLScrollbox ? -delta : delta;
+
+ if (this._prevMouseScrolls.every(prev => prev == isVertical)) {
+ if (event.deltaMode == event.DOM_DELTA_PIXEL)
+ this.scrollByPixels(scrollByDelta);
+ else if (event.deltaMode == event.DOM_DELTA_PAGE)
+ this.scrollByPage(scrollByDelta);
+ else
+ this.scrollByIndex(scrollByDelta);
+ }
+
+ if (this._prevMouseScrolls.length > 1)
+ this._prevMouseScrolls.shift();
+ this._prevMouseScrolls.push(isVertical);
+ }
+
+ event.stopPropagation();
+ event.preventDefault();
+ ]]></handler>
+
+ <handler event="touchstart"><![CDATA[
+ if (event.touches.length > 1) {
+ // Multiple touch points detected, abort. In particular this aborts
+ // the panning gesture when the user puts a second finger down after
+ // already panning with one finger. Aborting at this point prevents
+ // the pan gesture from being resumed until all fingers are lifted
+ // (as opposed to when the user is back down to one finger).
+ this._touchStart = -1;
+ } else {
+ this._touchStart = (this.orient == "vertical"
+ ? event.touches[0].screenY
+ : event.touches[0].screenX);
+ }
+ ]]></handler>
+
+ <handler event="touchmove"><![CDATA[
+ if (event.touches.length == 1 &&
+ this._touchStart >= 0) {
+ var touchPoint = (this.orient == "vertical"
+ ? event.touches[0].screenY
+ : event.touches[0].screenX);
+ var delta = this._touchStart - touchPoint;
+ if (Math.abs(delta) > 0) {
+ this.scrollByPixels(delta);
+ this._touchStart = touchPoint;
+ }
+ event.preventDefault();
+ }
+ ]]></handler>
+
+ <handler event="touchend"><![CDATA[
+ this._touchStart = -1;
+ ]]></handler>
+
+ <handler event="underflow" phase="capturing"><![CDATA[
+ // filter underflow events which were dispatched on nested scrollboxes
+ if (event.target != this)
+ return;
+
+ // Ignore events that doesn't match our orientation.
+ // Scrollport event orientation:
+ // 0: vertical
+ // 1: horizontal
+ // 2: both
+ if (this.orient == "vertical") {
+ if (event.detail == 1)
+ return;
+ }
+ else if (event.detail == 0) {
+ // horizontal scrollbox
+ return;
+ }
+
+ this.setAttribute("notoverflowing", "true");
+
+ try {
+ // See bug 341047 and comments in overflow handler as to why
+ // try..catch is needed here
+ this._updateScrollButtonsDisabledState();
+
+ let childNodes = this._getScrollableElements();
+ if (childNodes && childNodes.length)
+ this.ensureElementIsVisible(childNodes[0], false);
+ }
+ catch (e) {
+ this.removeAttribute("notoverflowing");
+ }
+ ]]></handler>
+
+ <handler event="overflow" phase="capturing"><![CDATA[
+ // filter underflow events which were dispatched on nested scrollboxes
+ if (event.target != this)
+ return;
+
+ // Ignore events that doesn't match our orientation.
+ // Scrollport event orientation:
+ // 0: vertical
+ // 1: horizontal
+ // 2: both
+ if (this.orient == "vertical") {
+ if (event.detail == 1)
+ return;
+ }
+ else if (event.detail == 0) {
+ // horizontal scrollbox
+ return;
+ }
+
+ this.removeAttribute("notoverflowing");
+
+ try {
+ // See bug 341047, the overflow event is dispatched when the
+ // scrollbox already is mostly destroyed. This causes some code in
+ // _updateScrollButtonsDisabledState() to throw an error. It also
+ // means that the notoverflowing attribute was removed erroneously,
+ // as the whole overflow event should not be happening in that case.
+ this._updateScrollButtonsDisabledState();
+ }
+ catch (e) {
+ this.setAttribute("notoverflowing", "true");
+ }
+ ]]></handler>
+
+ <handler event="scroll" action="this._updateScrollButtonsDisabledState()"/>
+ </handlers>
+ </binding>
+
+ <binding id="autorepeatbutton" extends="chrome://global/content/bindings/scrollbox.xml#scrollbox-base">
+ <content repeat="hover">
+ <xul:image class="autorepeatbutton-icon"/>
+ </content>
+ </binding>
+
+ <binding id="arrowscrollbox-clicktoscroll" extends="chrome://global/content/bindings/scrollbox.xml#arrowscrollbox">
+ <content>
+ <xul:toolbarbutton class="scrollbutton-up"
+ xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtostart"
+ anonid="scrollbutton-up"
+ onclick="_distanceScroll(event);"
+ onmousedown="if (event.button == 0) _startScroll(-1);"
+ onmouseup="if (event.button == 0) _stopScroll();"
+ onmouseover="_continueScroll(-1);"
+ onmouseout="_pauseScroll();"/>
+ <xul:spacer class="arrowscrollbox-overflow-start-indicator"
+ xbl:inherits="collapsed=scrolledtostart"/>
+ <xul:scrollbox class="arrowscrollbox-scrollbox"
+ anonid="scrollbox"
+ flex="1"
+ xbl:inherits="orient,align,pack,dir">
+ <children/>
+ </xul:scrollbox>
+ <xul:spacer class="arrowscrollbox-overflow-end-indicator"
+ xbl:inherits="collapsed=scrolledtoend"/>
+ <xul:toolbarbutton class="scrollbutton-down"
+ xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtoend"
+ anonid="scrollbutton-down"
+ onclick="_distanceScroll(event);"
+ onmousedown="if (event.button == 0) _startScroll(1);"
+ onmouseup="if (event.button == 0) _stopScroll();"
+ onmouseover="_continueScroll(1);"
+ onmouseout="_pauseScroll();"/>
+ </content>
+ <implementation implements="nsITimerCallback, nsIDOMEventListener">
+ <constructor><![CDATA[
+ try {
+ this._scrollDelay = this._prefBranch
+ .getIntPref("toolkit.scrollbox.clickToScroll.scrollDelay");
+ }
+ catch (ex) {
+ }
+ ]]></constructor>
+
+ <destructor><![CDATA[
+ // Release timer to avoid reference cycles.
+ if (this._scrollTimer) {
+ this._scrollTimer.cancel();
+ this._scrollTimer = null;
+ }
+ ]]></destructor>
+
+ <field name="_scrollIndex">0</field>
+ <field name="_scrollDelay">150</field>
+
+ <method name="notify">
+ <parameter name="aTimer"/>
+ <body>
+ <![CDATA[
+ if (!document)
+ aTimer.cancel();
+
+ this.scrollByIndex(this._scrollIndex);
+ ]]>
+ </body>
+ </method>
+
+ <field name="_arrowScrollAnim"><![CDATA[({
+ scrollbox: this,
+ requestHandle: 0, /* 0 indicates there is no pending request */
+ start: function arrowSmoothScroll_start() {
+ this.lastFrameTime = window.performance.now();
+ if (!this.requestHandle)
+ this.requestHandle = window.requestAnimationFrame(this.sample.bind(this));
+ },
+ stop: function arrowSmoothScroll_stop() {
+ window.cancelAnimationFrame(this.requestHandle);
+ this.requestHandle = 0;
+ },
+ sample: function arrowSmoothScroll_handleEvent(timeStamp) {
+ const scrollIndex = this.scrollbox._scrollIndex;
+ const timePassed = timeStamp - this.lastFrameTime;
+ this.lastFrameTime = timeStamp;
+
+ const scrollDelta = 0.5 * timePassed * scrollIndex;
+ this.scrollbox.scrollPosition += scrollDelta;
+
+ this.requestHandle = window.requestAnimationFrame(this.sample.bind(this));
+ }
+ })]]></field>
+
+ <method name="_startScroll">
+ <parameter name="index"/>
+ <body><![CDATA[
+ if (this._isRTLScrollbox)
+ index *= -1;
+ this._scrollIndex = index;
+ this._mousedown = true;
+ if (this.smoothScroll) {
+ this._arrowScrollAnim.start();
+ return;
+ }
+
+ if (!this._scrollTimer)
+ this._scrollTimer =
+ Components.classes["@mozilla.org/timer;1"]
+ .createInstance(Components.interfaces.nsITimer);
+ else
+ this._scrollTimer.cancel();
+
+ this._scrollTimer.initWithCallback(this, this._scrollDelay,
+ this._scrollTimer.TYPE_REPEATING_SLACK);
+ this.notify(this._scrollTimer);
+ ]]>
+ </body>
+ </method>
+
+ <method name="_stopScroll">
+ <body><![CDATA[
+ if (this._scrollTimer)
+ this._scrollTimer.cancel();
+ this._mousedown = false;
+ if (!this._scrollIndex || !this.smoothScroll)
+ return;
+
+ this.scrollByIndex(this._scrollIndex);
+ this._scrollIndex = 0;
+ this._arrowScrollAnim.stop();
+ ]]></body>
+ </method>
+
+ <method name="_pauseScroll">
+ <body><![CDATA[
+ if (this._mousedown) {
+ this._stopScroll();
+ this._mousedown = true;
+ document.addEventListener("mouseup", this, false);
+ document.addEventListener("blur", this, true);
+ }
+ ]]></body>
+ </method>
+
+ <method name="_continueScroll">
+ <parameter name="index"/>
+ <body><![CDATA[
+ if (this._mousedown)
+ this._startScroll(index);
+ ]]></body>
+ </method>
+
+ <method name="handleEvent">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ if (aEvent.type == "mouseup" ||
+ aEvent.type == "blur" && aEvent.target == document) {
+ this._mousedown = false;
+ document.removeEventListener("mouseup", this, false);
+ document.removeEventListener("blur", this, true);
+ }
+ ]]></body>
+ </method>
+
+ <method name="_distanceScroll">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ if (aEvent.detail < 2 || aEvent.detail > 3)
+ return;
+
+ var scrollBack = (aEvent.originalTarget == this._scrollButtonUp);
+ var scrollLeftOrUp = this._isRTLScrollbox ? !scrollBack : scrollBack;
+ var targetElement;
+
+ if (aEvent.detail == 2) {
+ // scroll by the size of the scrollbox
+ let [start, end] = this._startEndProps;
+ let x;
+ if (scrollLeftOrUp)
+ x = this.scrollClientRect[start] - this.scrollClientSize;
+ else
+ x = this.scrollClientRect[end] + this.scrollClientSize;
+ targetElement = this._elementFromPoint(x, scrollLeftOrUp ? -1 : 1);
+
+ // the next partly-hidden element will become fully visible,
+ // so don't scroll too far
+ if (targetElement)
+ targetElement = scrollBack ?
+ targetElement.nextSibling :
+ targetElement.previousSibling;
+ }
+
+ if (!targetElement) {
+ // scroll to the first resp. last element
+ let elements = this._getScrollableElements();
+ targetElement = scrollBack ?
+ elements[0] :
+ elements[elements.length - 1];
+ }
+
+ this.ensureElementIsVisible(targetElement);
+ ]]></body>
+ </method>
+
+ </implementation>
+ </binding>
+</bindings>
diff --git a/toolkit/content/widgets/spinbuttons.xml b/toolkit/content/widgets/spinbuttons.xml
new file mode 100644
index 000000000..3a695beac
--- /dev/null
+++ b/toolkit/content/widgets/spinbuttons.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<bindings id="spinbuttonsBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="spinbuttons"
+ extends="chrome://global/content/bindings/general.xml#basecontrol">
+
+ <resources>
+ <stylesheet src="chrome://global/skin/spinbuttons.css"/>
+ </resources>
+
+ <content>
+ <xul:vbox class="spinbuttons-box" flex="1">
+ <xul:button anonid="increaseButton" type="repeat" flex="1"
+ class="spinbuttons-button spinbuttons-up"
+ xbl:inherits="disabled,disabled=increasedisabled"/>
+ <xul:button anonid="decreaseButton" type="repeat" flex="1"
+ class="spinbuttons-button spinbuttons-down"
+ xbl:inherits="disabled,disabled=decreasedisabled"/>
+ </xul:vbox>
+ </content>
+
+ <implementation>
+ <property name="_increaseButton" readonly="true">
+ <getter>
+ return document.getAnonymousElementByAttribute(this, "anonid", "increaseButton");
+ </getter>
+ </property>
+ <property name="_decreaseButton" readonly="true">
+ <getter>
+ return document.getAnonymousElementByAttribute(this, "anonid", "decreaseButton");
+ </getter>
+ </property>
+
+ <property name="increaseDisabled"
+ onget="return this._increaseButton.getAttribute('disabled') == 'true';"
+ onset="if (val) this._increaseButton.setAttribute('disabled', 'true');
+ else this._increaseButton.removeAttribute('disabled'); return val;"/>
+ <property name="decreaseDisabled"
+ onget="return this._decreaseButton.getAttribute('disabled') == 'true';"
+ onset="if (val) this._decreaseButton.setAttribute('disabled', 'true');
+ else this._decreaseButton.removeAttribute('disabled'); return val;"/>
+ </implementation>
+
+ <handlers>
+ <handler event="mousedown">
+ <![CDATA[
+ // on the Mac, the native theme draws the spinbutton as a single widget
+ // so a state attribute is set based on where the mouse button was pressed
+ if (event.originalTarget == this._increaseButton)
+ this.setAttribute("state", "up");
+ else if (event.originalTarget == this._decreaseButton)
+ this.setAttribute("state", "down");
+ ]]>
+ </handler>
+
+ <handler event="mouseup">
+ this.removeAttribute("state");
+ </handler>
+ <handler event="mouseout">
+ this.removeAttribute("state");
+ </handler>
+
+ <handler event="command">
+ <![CDATA[
+ var eventname;
+ if (event.originalTarget == this._increaseButton)
+ eventname = "up";
+ else if (event.originalTarget == this._decreaseButton)
+ eventname = "down";
+
+ var evt = document.createEvent("Events");
+ evt.initEvent(eventname, true, true);
+ var cancel = this.dispatchEvent(evt);
+
+ if (this.hasAttribute("on" + eventname)) {
+ var fn = new Function("event", this.getAttribute("on" + eventname));
+ if (fn.call(this, event) == false)
+ cancel = true;
+ }
+
+ return !cancel;
+ ]]>
+ </handler>
+
+ </handlers>
+ </binding>
+
+</bindings>
diff --git a/toolkit/content/widgets/spinner.js b/toolkit/content/widgets/spinner.js
new file mode 100644
index 000000000..208ab1931
--- /dev/null
+++ b/toolkit/content/widgets/spinner.js
@@ -0,0 +1,514 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/*
+ * The spinner is responsible for displaying the items, and does
+ * not care what the values represent. The setValue function is called
+ * when it detects a change in value triggered by scroll event.
+ * Supports scrolling, clicking on up or down, clicking on item, and
+ * dragging.
+ */
+
+function Spinner(props, context) {
+ this.context = context;
+ this._init(props);
+}
+
+{
+ const debug = 0 ? console.log.bind(console, "[spinner]") : function() {};
+
+ const ITEM_HEIGHT = 2.5,
+ VIEWPORT_SIZE = 7,
+ VIEWPORT_COUNT = 5,
+ SCROLL_TIMEOUT = 100;
+
+ Spinner.prototype = {
+ /**
+ * Initializes a spinner. Set the default states and properties, cache
+ * element references, create the HTML markup, and add event listeners.
+ *
+ * @param {Object} props [Properties passed in from parent]
+ * {
+ * {Function} setValue: Takes a value and set the state to
+ * the parent component.
+ * {Function} getDisplayString: Takes a value, and output it
+ * as localized strings.
+ * {Number} viewportSize [optional]: Number of items in a
+ * viewport.
+ * {Boolean} hideButtons [optional]: Hide up & down buttons
+ * {Number} rootFontSize [optional]: Used to support zoom in/out
+ * }
+ */
+ _init(props) {
+ const { setValue, getDisplayString, hideButtons, rootFontSize = 10 } = props;
+
+ const spinnerTemplate = document.getElementById("spinner-template");
+ const spinnerElement = document.importNode(spinnerTemplate.content, true);
+
+ // Make sure viewportSize is an odd number because we want to have the selected
+ // item in the center. If it's an even number, use the default size instead.
+ const viewportSize = props.viewportSize % 2 ? props.viewportSize : VIEWPORT_SIZE;
+
+ this.state = {
+ items: [],
+ isScrolling: false
+ };
+ this.props = {
+ setValue, getDisplayString, viewportSize, rootFontSize,
+ // We can assume that the viewportSize is an odd number. Calculate how many
+ // items we need to insert on top of the spinner so that the selected is at
+ // the center. Ex: if viewportSize is 5, we need 2 items on top.
+ viewportTopOffset: (viewportSize - 1) / 2
+ };
+ this.elements = {
+ container: spinnerElement.querySelector(".spinner-container"),
+ spinner: spinnerElement.querySelector(".spinner"),
+ up: spinnerElement.querySelector(".up"),
+ down: spinnerElement.querySelector(".down"),
+ itemsViewElements: []
+ };
+
+ this.elements.spinner.style.height = (ITEM_HEIGHT * viewportSize) + "rem";
+
+ if (hideButtons) {
+ this.elements.container.classList.add("hide-buttons");
+ }
+
+ this.context.appendChild(spinnerElement);
+ this._attachEventListeners();
+ },
+
+ /**
+ * Only the parent component calls setState on the spinner.
+ * It checks if the items have changed and updates the spinner.
+ * If only the value has changed, smooth scrolls to the new value.
+ *
+ * @param {Object} newState [The new spinner state]
+ * {
+ * {Number/String} value: The centered value
+ * {Array} items: The list of items for display
+ * {Boolean} isInfiniteScroll: Whether or not the spinner should
+ * have infinite scroll capability
+ * {Boolean} isValueSet: true if user has selected a value
+ * }
+ */
+ setState(newState) {
+ const { spinner } = this.elements;
+ const { value, items } = this.state;
+ const { value: newValue, items: newItems, isValueSet, isInvalid } = newState;
+
+ if (this._isArrayDiff(newItems, items)) {
+ this.state = Object.assign(this.state, newState);
+ this._updateItems();
+ this._scrollTo(newValue, true);
+ } else if (newValue != value) {
+ this.state = Object.assign(this.state, newState);
+ this._smoothScrollTo(newValue);
+ }
+
+ if (isValueSet) {
+ if (isInvalid) {
+ this._removeSelection();
+ } else {
+ this._updateSelection();
+ }
+ }
+ },
+
+ /**
+ * Whenever scroll event is detected:
+ * - Update the index state
+ * - If a smooth scroll has reached its destination, set [isScrolling] state
+ * to false
+ * - If the value has changed, update the [value] state and call [setValue]
+ * - If infinite scrolling is on, reset the scrolling position if necessary
+ */
+ _onScroll() {
+ const { items, itemsView, isInfiniteScroll } = this.state;
+ const { viewportSize, viewportTopOffset } = this.props;
+ const { spinner, itemsViewElements } = this.elements;
+
+ this.state.index = this._getIndexByOffset(spinner.scrollTop);
+
+ const value = itemsView[this.state.index + viewportTopOffset].value;
+
+ // Check if smooth scrolling has reached its destination.
+ // This prevents input box jump when input box changes values.
+ if (this.state.value == value && this.state.isScrolling) {
+ this.state.isScrolling = false;
+ }
+
+ // Call setValue if value has changed, and is not smooth scrolling
+ if (this.state.value != value && !this.state.isScrolling) {
+ this.state.value = value;
+ this.props.setValue(value);
+ }
+
+ // Do infinite scroll when items length is bigger or equal to viewport
+ // and isInfiniteScroll is not false.
+ if (items.length >= viewportSize && isInfiniteScroll) {
+ // If the scroll position is near the top or bottom, jump back to the middle
+ // so user can keep scrolling up or down.
+ if (this.state.index < viewportSize ||
+ this.state.index > itemsView.length - viewportSize) {
+ this._scrollTo(this.state.value, true);
+ }
+ }
+
+ // Use a timer to detect if a scroll event has not fired within some time
+ // (defined in SCROLL_TIMEOUT). This is required because we need to hide
+ // highlight and hover state when user is scrolling.
+ clearTimeout(this.state.scrollTimer);
+ this.elements.spinner.classList.add("scrolling");
+ this.state.scrollTimer = setTimeout(() => {
+ this.elements.spinner.classList.remove("scrolling");
+ this.elements.spinner.dispatchEvent(new CustomEvent("ScrollStop"));
+ }, SCROLL_TIMEOUT);
+ },
+
+ /**
+ * Updates the spinner items to the current states.
+ */
+ _updateItems() {
+ const { viewportSize, viewportTopOffset } = this.props;
+ const { items, isInfiniteScroll } = this.state;
+
+ // Prepends null elements so the selected value is centered in spinner
+ let itemsView = new Array(viewportTopOffset).fill({}).concat(items);
+
+ if (items.length >= viewportSize && isInfiniteScroll) {
+ // To achieve infinite scroll, we move the scroll position back to the
+ // center when it is near the top or bottom. The scroll momentum could
+ // be lost in the process, so to minimize that, we need at least 2 sets
+ // of items to act as buffer: one for the top and one for the bottom.
+ // But if the number of items is small ( < viewportSize * viewport count)
+ // we should add more sets.
+ let count = Math.ceil(viewportSize * VIEWPORT_COUNT / items.length) * 2;
+ for (let i = 0; i < count; i += 1) {
+ itemsView.push(...items);
+ }
+ }
+
+ // Reuse existing DOM nodes when possible. Create or remove
+ // nodes based on how big itemsView is.
+ this._prepareNodes(itemsView.length, this.elements.spinner);
+ // Once DOM nodes are ready, set display strings using textContent
+ this._setDisplayStringAndClass(itemsView, this.elements.itemsViewElements);
+
+ this.state.itemsView = itemsView;
+ },
+
+ /**
+ * Make sure the number or child elements is the same as length
+ * and keep the elements' references for updating textContent
+ *
+ * @param {Number} length [The number of child elements]
+ * @param {DOMElement} parent [The parent element reference]
+ */
+ _prepareNodes(length, parent) {
+ const diff = length - parent.childElementCount;
+
+ if (!diff) {
+ return;
+ }
+
+ if (diff > 0) {
+ // Add more elements if length is greater than current
+ let frag = document.createDocumentFragment();
+
+ // Remove margin bottom on the last element before appending
+ if (parent.lastChild) {
+ parent.lastChild.style.marginBottom = "";
+ }
+
+ for (let i = 0; i < diff; i++) {
+ let el = document.createElement("div");
+ frag.appendChild(el);
+ this.elements.itemsViewElements.push(el);
+ }
+ parent.appendChild(frag);
+ } else if (diff < 0) {
+ // Remove elements if length is less than current
+ for (let i = 0; i < Math.abs(diff); i++) {
+ parent.removeChild(parent.lastChild);
+ }
+ this.elements.itemsViewElements.splice(diff);
+ }
+
+ parent.lastChild.style.marginBottom =
+ (ITEM_HEIGHT * this.props.viewportTopOffset) + "rem";
+ },
+
+ /**
+ * Set the display string and class name to the elements.
+ *
+ * @param {Array<Object>} items
+ * [{
+ * {Number/String} value: The value in its original form
+ * {Boolean} enabled: Whether or not the item is enabled
+ * }]
+ * @param {Array<DOMElement>} elements
+ */
+ _setDisplayStringAndClass(items, elements) {
+ const { getDisplayString } = this.props;
+
+ items.forEach((item, index) => {
+ elements[index].textContent =
+ item.value != undefined ? getDisplayString(item.value) : "";
+ elements[index].className = item.enabled ? "" : "disabled";
+ });
+ },
+
+ /**
+ * Attach event listeners to the spinner and buttons.
+ */
+ _attachEventListeners() {
+ const { spinner } = this.elements;
+
+ spinner.addEventListener("scroll", this, { passive: true });
+ document.addEventListener("mouseup", this, { passive: true });
+ document.addEventListener("mousedown", this);
+ },
+
+ /**
+ * Handle events
+ * @param {DOMEvent} event
+ */
+ handleEvent(event) {
+ const { mouseState = {}, index, itemsView } = this.state;
+ const { viewportTopOffset, setValue } = this.props;
+ const { spinner, up, down } = this.elements;
+
+ switch (event.type) {
+ case "scroll": {
+ this._onScroll();
+ break;
+ }
+ case "mousedown": {
+ // Use preventDefault to keep focus on input boxes
+ event.preventDefault();
+ event.target.setCapture();
+ this.state.mouseState = {
+ down: true,
+ layerX: event.layerX,
+ layerY: event.layerY
+ };
+ if (event.target == up) {
+ // An "active" class is needed to simulate :active pseudo-class
+ // because element is not focused.
+ event.target.classList.add("active");
+ this._smoothScrollToIndex(index + 1);
+ }
+ if (event.target == down) {
+ event.target.classList.add("active");
+ this._smoothScrollToIndex(index - 1);
+ }
+ if (event.target.parentNode == spinner) {
+ // Listen to dragging events
+ spinner.addEventListener("mousemove", this, { passive: true });
+ spinner.addEventListener("mouseleave", this, { passive: true });
+ }
+ break;
+ }
+ case "mouseup": {
+ this.state.mouseState.down = false;
+ if (event.target == up || event.target == down) {
+ event.target.classList.remove("active");
+ }
+ if (event.target.parentNode == spinner) {
+ // Check if user clicks or drags, scroll to the item if clicked,
+ // otherwise get the current index and smooth scroll there.
+ if (event.layerX == mouseState.layerX && event.layerY == mouseState.layerY) {
+ const newIndex = this._getIndexByOffset(event.target.offsetTop) - viewportTopOffset;
+ if (index == newIndex) {
+ // Set value manually if the clicked element is already centered.
+ // This happens when the picker first opens, and user pick the
+ // default value.
+ setValue(itemsView[index + viewportTopOffset].value);
+ } else {
+ this._smoothScrollToIndex(newIndex);
+ }
+ } else {
+ this._smoothScrollToIndex(this._getIndexByOffset(spinner.scrollTop));
+ }
+ // Stop listening to dragging
+ spinner.removeEventListener("mousemove", this, { passive: true });
+ spinner.removeEventListener("mouseleave", this, { passive: true });
+ }
+ break;
+ }
+ case "mouseleave": {
+ if (event.target == spinner) {
+ // Stop listening to drag event if mouse is out of the spinner
+ this._smoothScrollToIndex(this._getIndexByOffset(spinner.scrollTop));
+ spinner.removeEventListener("mousemove", this, { passive: true });
+ spinner.removeEventListener("mouseleave", this, { passive: true });
+ }
+ break;
+ }
+ case "mousemove": {
+ // Change spinner position on drag
+ spinner.scrollTop -= event.movementY;
+ break;
+ }
+ }
+ },
+
+ /**
+ * Find the index by offset
+ * @param {Number} offset: Offset value in pixel.
+ * @return {Number} Index number
+ */
+ _getIndexByOffset(offset) {
+ return Math.round(offset / (ITEM_HEIGHT * this.props.rootFontSize));
+ },
+
+ /**
+ * Find the index of a value that is the closest to the current position.
+ * If centering is true, find the index closest to the center.
+ *
+ * @param {Number/String} value: The value to find
+ * @param {Boolean} centering: Whether or not to find the value closest to center
+ * @return {Number} index of the value, returns -1 if value is not found
+ */
+ _getScrollIndex(value, centering) {
+ const { itemsView } = this.state;
+ const { viewportTopOffset } = this.props;
+
+ // If index doesn't exist, or centering is true, start from the middle point
+ let currentIndex = centering || (this.state.index == undefined) ?
+ Math.round((itemsView.length - viewportTopOffset) / 2) :
+ this.state.index;
+ let closestIndex = itemsView.length;
+ let indexes = [];
+ let diff = closestIndex;
+ let isValueFound = false;
+
+ // Find indexes of items match the value
+ itemsView.forEach((item, index) => {
+ if (item.value == value) {
+ indexes.push(index);
+ }
+ });
+
+ // Find the index closest to currentIndex
+ indexes.forEach(index => {
+ let d = Math.abs(index - currentIndex);
+ if (d < diff) {
+ diff = d;
+ closestIndex = index;
+ isValueFound = true;
+ }
+ });
+
+ return isValueFound ? (closestIndex - viewportTopOffset) : -1;
+ },
+
+ /**
+ * Scroll to a value.
+ *
+ * @param {Number/String} value: Value to scroll to
+ * @param {Boolean} centering: Whether or not to scroll to center location
+ */
+ _scrollTo(value, centering) {
+ const index = this._getScrollIndex(value, centering);
+ // Do nothing if the value is not found
+ if (index > -1) {
+ this.state.index = index;
+ this.elements.spinner.scrollTop = this.state.index * ITEM_HEIGHT * this.props.rootFontSize;
+ }
+ },
+
+ /**
+ * Smooth scroll to a value.
+ *
+ * @param {Number/String} value: Value to scroll to
+ */
+ _smoothScrollTo(value) {
+ const index = this._getScrollIndex(value);
+ // Do nothing if the value is not found
+ if (index > -1) {
+ this.state.index = index;
+ this._smoothScrollToIndex(this.state.index);
+ }
+ },
+
+ /**
+ * Smooth scroll to a value based on the index
+ *
+ * @param {Number} index: Index number
+ */
+ _smoothScrollToIndex(index) {
+ const element = this.elements.spinner.children[index];
+ if (element) {
+ // Set the isScrolling flag before smooth scrolling begins
+ // and remove it when it has reached the destination.
+ // This prevents input box jump when input box changes values
+ this.state.isScrolling = true;
+ element.scrollIntoView({
+ behavior: "smooth", block: "start"
+ });
+ }
+ },
+
+ /**
+ * Update the selection state.
+ */
+ _updateSelection() {
+ const { itemsViewElements, selected } = this.elements;
+ const { itemsView, index } = this.state;
+ const { viewportTopOffset } = this.props;
+ const currentItemIndex = index + viewportTopOffset;
+
+ if (selected && selected != itemsViewElements[currentItemIndex]) {
+ this._removeSelection();
+ }
+
+ this.elements.selected = itemsViewElements[currentItemIndex];
+ if (itemsView[currentItemIndex] && itemsView[currentItemIndex].enabled) {
+ this.elements.selected.classList.add("selection");
+ }
+ },
+
+ /**
+ * Remove selection if selected exists and different from current
+ */
+ _removeSelection() {
+ const { selected } = this.elements;
+ if (selected) {
+ selected.classList.remove("selection");
+ }
+ },
+
+ /**
+ * Compares arrays of objects. It assumes the structure is an array of
+ * objects, and objects in a and b have the same number of properties.
+ *
+ * @param {Array<Object>} a
+ * @param {Array<Object>} b
+ * @return {Boolean} Returns true if a and b are different
+ */
+ _isArrayDiff(a, b) {
+ // Check reference first, exit early if reference is the same.
+ if (a == b) {
+ return false;
+ }
+
+ if (a.length != b.length) {
+ return true;
+ }
+
+ for (let i = 0; i < a.length; i++) {
+ for (let prop in a[i]) {
+ if (a[i][prop] != b[i][prop]) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ };
+}
diff --git a/toolkit/content/widgets/splitter.xml b/toolkit/content/widgets/splitter.xml
new file mode 100644
index 000000000..d23631fbe
--- /dev/null
+++ b/toolkit/content/widgets/splitter.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<bindings id="splitterBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <binding id="splitter" extends="xul:splitter">
+ <resources>
+ <stylesheet src="chrome://global/skin/splitter.css"/>
+ </resources>
+ </binding>
+
+ <binding id="grippy" extends="xul:button">
+ <resources>
+ <stylesheet src="chrome://global/skin/splitter.css"/>
+ </resources>
+ <handlers>
+ <handler event="command">
+ <![CDATA[
+ var splitter = this.parentNode;
+ if (splitter) {
+ var state = splitter.getAttribute("state");
+ if (state == "collapsed")
+ splitter.setAttribute("state", "open");
+ else
+ splitter.setAttribute("state", "collapsed");
+ }
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+</bindings>
diff --git a/toolkit/content/widgets/stringbundle.xml b/toolkit/content/widgets/stringbundle.xml
new file mode 100644
index 000000000..3365bd61f
--- /dev/null
+++ b/toolkit/content/widgets/stringbundle.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<bindings id="stringBundleBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <binding id="stringbundleset" extends="xul:box"/>
+
+ <binding id="stringbundle" extends="xul:spacer">
+ <implementation name="XStringBundle">
+
+ <method name="getString">
+ <parameter name="aStringKey"/>
+ <body>
+ <![CDATA[
+ try {
+ return this.stringBundle.GetStringFromName(aStringKey);
+ }
+ catch (e) {
+ dump("*** Failed to get string " + aStringKey + " in bundle: " + this.src + "\n");
+ throw e;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="getFormattedString">
+ <parameter name="aStringKey"/>
+ <parameter name="aStringsArray"/>
+ <body>
+ <![CDATA[
+ try {
+ return this.stringBundle.formatStringFromName(aStringKey, aStringsArray, aStringsArray.length);
+ }
+ catch (e) {
+ dump("*** Failed to format string " + aStringKey + " in bundle: " + this.src + "\n");
+ throw e;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <property name="stringBundle" readonly="true">
+ <getter>
+ <![CDATA[
+ if (!this._bundle) {
+ try {
+ this._bundle = Components.classes["@mozilla.org/intl/stringbundle;1"]
+ .getService(Components.interfaces.nsIStringBundleService)
+ .createBundle(this.src);
+ }
+ catch (e) {
+ dump("Failed to get stringbundle:\n");
+ dump(e + "\n");
+ }
+ }
+ return this._bundle;
+ ]]>
+ </getter>
+ </property>
+
+ <property name="src">
+ <getter>
+ <![CDATA[
+ return this.getAttribute("src");
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ this._bundle = null;
+ this.setAttribute("src", val);
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="strings">
+ <getter>
+ <![CDATA[
+ // Note: this is a sucky method name! Should be:
+ // readonly attribute nsISimpleEnumerator strings;
+ return this.stringBundle.getSimpleEnumeration();
+ ]]>
+ </getter>
+ </property>
+
+ <field name="_bundle">null</field>
+
+ </implementation>
+ </binding>
+
+</bindings>
diff --git a/toolkit/content/widgets/tabbox.xml b/toolkit/content/widgets/tabbox.xml
new file mode 100644
index 000000000..02adb70b3
--- /dev/null
+++ b/toolkit/content/widgets/tabbox.xml
@@ -0,0 +1,892 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<bindings id="tabBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="tab-base">
+ <resources>
+ <stylesheet src="chrome://global/skin/tabbox.css"/>
+ </resources>
+ </binding>
+
+ <binding id="tabbox"
+ extends="chrome://global/content/bindings/tabbox.xml#tab-base">
+ <implementation implements="nsIDOMEventListener">
+ <property name="handleCtrlTab">
+ <setter>
+ <![CDATA[
+ this.setAttribute("handleCtrlTab", val);
+ return val;
+ ]]>
+ </setter>
+ <getter>
+ <![CDATA[
+ return (this.getAttribute("handleCtrlTab") != "false");
+ ]]>
+ </getter>
+ </property>
+
+ <property name="handleCtrlPageUpDown">
+ <setter>
+ <![CDATA[
+ this.setAttribute("handleCtrlPageUpDown", val);
+ return val;
+ ]]>
+ </setter>
+ <getter>
+ <![CDATA[
+ return (this.getAttribute("handleCtrlPageUpDown") != "false");
+ ]]>
+ </getter>
+ </property>
+
+ <field name="_handleMetaAltArrows" readonly="true">
+ /Mac/.test(navigator.platform)
+ </field>
+
+ <!-- _tabs and _tabpanels are deprecated, they exist only for
+ backwards compatibility. -->
+ <property name="_tabs" readonly="true" onget="return this.tabs;"/>
+ <property name="_tabpanels" readonly="true" onget="return this.tabpanels;"/>
+
+ <property name="tabs" readonly="true">
+ <getter>
+ <![CDATA[
+ return this.getElementsByTagNameNS(
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+ "tabs").item(0);
+ ]]>
+ </getter>
+ </property>
+
+ <property name="tabpanels" readonly="true">
+ <getter>
+ <![CDATA[
+ return this.getElementsByTagNameNS(
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+ "tabpanels").item(0);
+ ]]>
+ </getter>
+ </property>
+
+ <property name="selectedIndex">
+ <getter>
+ <![CDATA[
+ var tabs = this.tabs;
+ return tabs ? tabs.selectedIndex : -1;
+ ]]>
+ </getter>
+
+ <setter>
+ <![CDATA[
+ var tabs = this.tabs;
+ if (tabs)
+ tabs.selectedIndex = val;
+ this.setAttribute("selectedIndex", val);
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="selectedTab">
+ <getter>
+ <![CDATA[
+ var tabs = this.tabs;
+ return tabs && tabs.selectedItem;
+ ]]>
+ </getter>
+
+ <setter>
+ <![CDATA[
+ if (val) {
+ var tabs = this.tabs;
+ if (tabs)
+ tabs.selectedItem = val;
+ }
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="selectedPanel">
+ <getter>
+ <![CDATA[
+ var tabpanels = this.tabpanels;
+ return tabpanels && tabpanels.selectedPanel;
+ ]]>
+ </getter>
+
+ <setter>
+ <![CDATA[
+ if (val) {
+ var tabpanels = this.tabpanels;
+ if (tabpanels)
+ tabpanels.selectedPanel = val;
+ }
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <method name="handleEvent">
+ <parameter name="event"/>
+ <body>
+ <![CDATA[
+ if (!event.isTrusted) {
+ // Don't let untrusted events mess with tabs.
+ return;
+ }
+
+ // Don't check if the event was already consumed because tab
+ // navigation should always work for better user experience.
+
+ switch (event.keyCode) {
+ case event.DOM_VK_TAB:
+ if (event.ctrlKey && !event.altKey && !event.metaKey)
+ if (this.tabs && this.handleCtrlTab) {
+ this.tabs.advanceSelectedTab(event.shiftKey ? -1 : 1, true);
+ event.preventDefault();
+ }
+ break;
+ case event.DOM_VK_PAGE_UP:
+ if (event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey)
+ if (this.tabs && this.handleCtrlPageUpDown) {
+ this.tabs.advanceSelectedTab(-1, true);
+ event.preventDefault();
+ }
+ break;
+ case event.DOM_VK_PAGE_DOWN:
+ if (event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey)
+ if (this.tabs && this.handleCtrlPageUpDown) {
+ this.tabs.advanceSelectedTab(1, true);
+ event.preventDefault();
+ }
+ break;
+ case event.DOM_VK_LEFT:
+ if (event.metaKey && event.altKey && !event.shiftKey && !event.ctrlKey)
+ if (this.tabs && this._handleMetaAltArrows) {
+ var offset = window.getComputedStyle(this, "")
+ .direction == "ltr" ? -1 : 1;
+ this.tabs.advanceSelectedTab(offset, true);
+ event.preventDefault();
+ }
+ break;
+ case event.DOM_VK_RIGHT:
+ if (event.metaKey && event.altKey && !event.shiftKey && !event.ctrlKey)
+ if (this.tabs && this._handleMetaAltArrows) {
+ offset = window.getComputedStyle(this, "")
+ .direction == "ltr" ? 1 : -1;
+ this.tabs.advanceSelectedTab(offset, true);
+ event.preventDefault();
+ }
+ break;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <field name="_eventNode">this</field>
+
+ <property name="eventNode" onget="return this._eventNode;">
+ <setter>
+ <![CDATA[
+ if (val != this._eventNode) {
+ const nsIEventListenerService =
+ Components.interfaces.nsIEventListenerService;
+ let els = Components.classes["@mozilla.org/eventlistenerservice;1"]
+ .getService(nsIEventListenerService);
+ els.addSystemEventListener(val, "keydown", this, false);
+ els.removeSystemEventListener(this._eventNode, "keydown", this, false);
+ this._eventNode = val;
+ }
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <constructor>
+ switch (this.getAttribute("eventnode")) {
+ case "parent": this._eventNode = this.parentNode; break;
+ case "window": this._eventNode = window; break;
+ case "document": this._eventNode = document; break;
+ }
+ const nsIEventListenerService =
+ Components.interfaces.nsIEventListenerService;
+ let els = Components.classes["@mozilla.org/eventlistenerservice;1"]
+ .getService(nsIEventListenerService);
+ els.addSystemEventListener(this._eventNode, "keydown", this, false);
+ </constructor>
+
+ <destructor>
+ const nsIEventListenerService =
+ Components.interfaces.nsIEventListenerService;
+ let els = Components.classes["@mozilla.org/eventlistenerservice;1"]
+ .getService(nsIEventListenerService);
+ els.removeSystemEventListener(this._eventNode, "keydown", this, false);
+ </destructor>
+ </implementation>
+ </binding>
+
+ <binding id="tabs" role="xul:tabs"
+ extends="chrome://global/content/bindings/general.xml#basecontrol">
+ <resources>
+ <stylesheet src="chrome://global/skin/tabbox.css"/>
+ </resources>
+
+ <content>
+ <xul:spacer class="tabs-left"/>
+ <children/>
+ <xul:spacer class="tabs-right" flex="1"/>
+ </content>
+
+ <implementation implements="nsIDOMXULSelectControlElement, nsIDOMXULRelatedElement">
+ <constructor>
+ <![CDATA[
+ // first and last tabs need to be able to have unique styles
+ // and also need to select first tab on startup.
+ if (this.firstChild)
+ this.firstChild.setAttribute("first-tab", "true");
+ if (this.lastChild)
+ this.lastChild.setAttribute("last-tab", "true");
+
+ if (!this.hasAttribute("orient"))
+ this.setAttribute("orient", "horizontal");
+
+ if (this.tabbox && this.tabbox.hasAttribute("selectedIndex")) {
+ let selectedIndex = parseInt(this.tabbox.getAttribute("selectedIndex"));
+ this.selectedIndex = selectedIndex > 0 ? selectedIndex : 0;
+ return;
+ }
+
+ var children = this.childNodes;
+ var length = children.length;
+ for (var i = 0; i < length; i++) {
+ if (children[i].getAttribute("selected") == "true") {
+ this.selectedIndex = i;
+ return;
+ }
+ }
+
+ var value = this.value;
+ if (value)
+ this.value = value;
+ else
+ this.selectedIndex = 0;
+ ]]>
+ </constructor>
+
+ <!-- nsIDOMXULRelatedElement -->
+ <method name="getRelatedElement">
+ <parameter name="aTabElm"/>
+ <body>
+ <![CDATA[
+ if (!aTabElm)
+ return null;
+
+ let tabboxElm = this.tabbox;
+ if (!tabboxElm)
+ return null;
+
+ let tabpanelsElm = tabboxElm.tabpanels;
+ if (!tabpanelsElm)
+ return null;
+
+ // Get linked tab panel by 'linkedpanel' attribute on the given tab
+ // element.
+ let linkedPanelElm = null;
+
+ let linkedPanelId = aTabElm.linkedPanel;
+ if (linkedPanelId) {
+ let ownerDoc = this.ownerDocument;
+
+ // XXX bug 565858: if XUL tab element is anonymous element then
+ // suppose linked tab panel is hosted within the same XBL binding
+ // and search it by ID attribute inside an anonymous content of
+ // the binding. This is not robust assumption since tab elements may
+ // live outside a tabbox element so that for example tab elements
+ // can be explicit content but tab panels can be anonymous.
+
+ let bindingParent = ownerDoc.getBindingParent(aTabElm);
+ if (bindingParent)
+ return ownerDoc.getAnonymousElementByAttribute(bindingParent,
+ "id",
+ linkedPanelId);
+
+ return ownerDoc.getElementById(linkedPanelId);
+ }
+
+ // otherwise linked tabpanel element has the same index as the given
+ // tab element.
+ let tabElmIdx = this.getIndexOfItem(aTabElm);
+ return tabpanelsElm.childNodes[tabElmIdx];
+ ]]>
+ </body>
+ </method>
+
+ <!-- nsIDOMXULSelectControlElement -->
+ <property name="itemCount" readonly="true"
+ onget="return this.childNodes.length"/>
+
+ <property name="value" onget="return this.getAttribute('value');">
+ <setter>
+ <![CDATA[
+ this.setAttribute("value", val);
+ var children = this.childNodes;
+ for (var c = children.length - 1; c >= 0; c--) {
+ if (children[c].value == val) {
+ this.selectedIndex = c;
+ break;
+ }
+ }
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <field name="_tabbox">null</field>
+ <property name="tabbox" readonly="true">
+ <getter><![CDATA[
+ // Memoize the result in a field rather than replacing this property,
+ // so that it can be reset along with the binding.
+ if (this._tabbox) {
+ return this._tabbox;
+ }
+
+ let parent = this.parentNode;
+ while (parent) {
+ if (parent.localName == "tabbox") {
+ break;
+ }
+ parent = parent.parentNode;
+ }
+
+ return this._tabbox = parent;
+ ]]></getter>
+ </property>
+
+ <!-- _tabbox is deprecated, it exists only for backwards compatibility. -->
+ <field name="_tabbox" readonly="true"><![CDATA[
+ this.tabbox;
+ ]]></field>
+
+ <property name="selectedIndex">
+ <getter>
+ <![CDATA[
+ const tabs = this.childNodes;
+ for (var i = 0; i < tabs.length; i++) {
+ if (tabs[i].selected)
+ return i;
+ }
+ return -1;
+ ]]>
+ </getter>
+
+ <setter>
+ <![CDATA[
+ var tab = this.getItemAtIndex(val);
+ if (tab) {
+ var alreadySelected = tab.selected;
+
+ Array.forEach(this.childNodes, function (aTab) {
+ if (aTab.selected && aTab != tab)
+ aTab._selected = false;
+ });
+ tab._selected = true;
+
+ this.setAttribute("value", tab.value);
+
+ let linkedPanel = this.getRelatedElement(tab);
+ if (linkedPanel) {
+ this.tabbox.setAttribute("selectedIndex", val);
+
+ // This will cause an onselect event to fire for the tabpanel
+ // element.
+ this.tabbox.tabpanels.selectedPanel = linkedPanel;
+ }
+
+ if (!alreadySelected) {
+ // Fire an onselect event for the tabs element.
+ var event = document.createEvent('Events');
+ event.initEvent('select', true, true);
+ this.dispatchEvent(event);
+ }
+ }
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="selectedItem">
+ <getter>
+ <![CDATA[
+ const tabs = this.childNodes;
+ for (var i = 0; i < tabs.length; i++) {
+ if (tabs[i].selected)
+ return tabs[i];
+ }
+ return null;
+ ]]>
+ </getter>
+
+ <setter>
+ <![CDATA[
+ if (val && !val.selected)
+ // The selectedIndex setter ignores invalid values
+ // such as -1 if |val| isn't one of our child nodes.
+ this.selectedIndex = this.getIndexOfItem(val);
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <method name="getIndexOfItem">
+ <parameter name="item"/>
+ <body>
+ <![CDATA[
+ return Array.indexOf(this.childNodes, item);
+ ]]>
+ </body>
+ </method>
+
+ <method name="getItemAtIndex">
+ <parameter name="index"/>
+ <body>
+ <![CDATA[
+ return this.childNodes.item(index);
+ ]]>
+ </body>
+ </method>
+
+ <method name="_selectNewTab">
+ <parameter name="aNewTab"/>
+ <parameter name="aFallbackDir"/>
+ <parameter name="aWrap"/>
+ <body>
+ <![CDATA[
+ var requestedTab = aNewTab;
+ while (aNewTab.hidden || aNewTab.disabled || !this._canAdvanceToTab(aNewTab)) {
+ aNewTab = aFallbackDir == -1 ? aNewTab.previousSibling : aNewTab.nextSibling;
+ if (!aNewTab && aWrap)
+ aNewTab = aFallbackDir == -1 ? this.childNodes[this.childNodes.length - 1] :
+ this.childNodes[0];
+ if (!aNewTab || aNewTab == requestedTab)
+ return;
+ }
+
+ var isTabFocused = false;
+ try {
+ isTabFocused =
+ (document.commandDispatcher.focusedElement == this.selectedItem);
+ } catch (e) {}
+ this.selectedItem = aNewTab;
+ if (isTabFocused) {
+ aNewTab.focus();
+ }
+ else if (this.getAttribute("setfocus") != "false") {
+ let selectedPanel = this.tabbox.selectedPanel;
+ document.commandDispatcher.advanceFocusIntoSubtree(selectedPanel);
+
+ // Make sure that the focus doesn't move outside the tabbox
+ if (this.tabbox) {
+ try {
+ let el = document.commandDispatcher.focusedElement;
+ while (el && el != this.tabbox.tabpanels) {
+ if (el == this.tabbox || el == selectedPanel)
+ return;
+ el = el.parentNode;
+ }
+ aNewTab.focus();
+ } catch (e) {
+ }
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="_canAdvanceToTab">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ return true;
+ ]]>
+ </body>
+ </method>
+
+ <method name="advanceSelectedTab">
+ <parameter name="aDir"/>
+ <parameter name="aWrap"/>
+ <body>
+ <![CDATA[
+ var startTab = this.selectedItem;
+ var next = startTab[aDir == -1 ? "previousSibling" : "nextSibling"];
+ if (!next && aWrap) {
+ next = aDir == -1 ? this.childNodes[this.childNodes.length - 1] :
+ this.childNodes[0];
+ }
+ if (next && next != startTab) {
+ this._selectNewTab(next, aDir, aWrap);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="appendItem">
+ <parameter name="label"/>
+ <parameter name="value"/>
+ <body>
+ <![CDATA[
+ var XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ var tab = document.createElementNS(XULNS, "tab");
+ tab.setAttribute("label", label);
+ tab.setAttribute("value", value);
+ this.appendChild(tab);
+ return tab;
+ ]]>
+ </body>
+ </method>
+
+ <method name="insertItemAt">
+ <parameter name="index"/>
+ <parameter name="label"/>
+ <parameter name="value"/>
+ <body>
+ <![CDATA[
+ var XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ var tab = document.createElementNS(XULNS, "tab");
+ tab.setAttribute("label", label);
+ tab.setAttribute("value", value);
+ var before = this.getItemAtIndex(index);
+ if (before)
+ this.insertBefore(tab, before);
+ else
+ this.appendChild(tab);
+ return tab;
+ ]]>
+ </body>
+ </method>
+
+ <method name="removeItemAt">
+ <parameter name="index"/>
+ <body>
+ <![CDATA[
+ var remove = this.getItemAtIndex(index);
+ if (remove)
+ this.removeChild(remove);
+ return remove;
+ ]]>
+ </body>
+ </method>
+ </implementation>
+
+#ifdef MOZ_WIDGET_GTK
+ <handlers>
+ <handler event="DOMMouseScroll">
+ <![CDATA[
+ if (event.detail > 0)
+ this.advanceSelectedTab(1, false);
+ else
+ this.advanceSelectedTab(-1, false);
+
+ event.stopPropagation();
+ ]]>
+ </handler>
+ </handlers>
+#endif
+ </binding>
+
+ <binding id="tabpanels" role="xul:tabpanels"
+ extends="chrome://global/content/bindings/tabbox.xml#tab-base">
+ <implementation implements="nsIDOMXULRelatedElement">
+ <!-- nsIDOMXULRelatedElement -->
+ <method name="getRelatedElement">
+ <parameter name="aTabPanelElm"/>
+ <body>
+ <![CDATA[
+ if (!aTabPanelElm)
+ return null;
+
+ let tabboxElm = this.tabbox;
+ if (!tabboxElm)
+ return null;
+
+ let tabsElm = tabboxElm.tabs;
+ if (!tabsElm)
+ return null;
+
+ // Return tab element having 'linkedpanel' attribute equal to the id
+ // of the tab panel or the same index as the tab panel element.
+ let tabpanelIdx = Array.indexOf(this.childNodes, aTabPanelElm);
+ if (tabpanelIdx == -1)
+ return null;
+
+ let tabElms = tabsElm.childNodes;
+ let tabElmFromIndex = tabElms[tabpanelIdx];
+
+ let tabpanelId = aTabPanelElm.id;
+ if (tabpanelId) {
+ for (let idx = 0; idx < tabElms.length; idx++) {
+ var tabElm = tabElms[idx];
+ if (tabElm.linkedPanel == tabpanelId)
+ return tabElm;
+ }
+ }
+
+ return tabElmFromIndex;
+ ]]>
+ </body>
+ </method>
+
+ <!-- public -->
+ <field name="_tabbox">null</field>
+ <property name="tabbox" readonly="true">
+ <getter><![CDATA[
+ // Memoize the result in a field rather than replacing this property,
+ // so that it can be reset along with the binding.
+ if (this._tabbox) {
+ return this._tabbox;
+ }
+
+ let parent = this.parentNode;
+ while (parent) {
+ if (parent.localName == "tabbox") {
+ break;
+ }
+ parent = parent.parentNode;
+ }
+
+ return this._tabbox = parent;
+ ]]></getter>
+ </property>
+
+ <field name="_selectedPanel">this.childNodes.item(this.selectedIndex)</field>
+
+ <property name="selectedIndex">
+ <getter>
+ <![CDATA[
+ var indexStr = this.getAttribute("selectedIndex");
+ return indexStr ? parseInt(indexStr) : -1;
+ ]]>
+ </getter>
+
+ <setter>
+ <![CDATA[
+ if (val < 0 || val >= this.childNodes.length)
+ return val;
+ var panel = this._selectedPanel;
+ this._selectedPanel = this.childNodes[val];
+ this.setAttribute("selectedIndex", val);
+ if (this._selectedPanel != panel) {
+ var event = document.createEvent("Events");
+ event.initEvent("select", true, true);
+ this.dispatchEvent(event);
+ }
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="selectedPanel">
+ <getter>
+ <![CDATA[
+ return this._selectedPanel;
+ ]]>
+ </getter>
+
+ <setter>
+ <![CDATA[
+ var selectedIndex = -1;
+ for (var panel = val; panel != null; panel = panel.previousSibling)
+ ++selectedIndex;
+ this.selectedIndex = selectedIndex;
+ return val;
+ ]]>
+ </setter>
+ </property>
+ </implementation>
+ </binding>
+
+ <binding id="tab" display="xul:button" role="xul:tab"
+ extends="chrome://global/content/bindings/general.xml#control-item">
+ <resources>
+ <stylesheet src="chrome://global/skin/tabbox.css"/>
+ </resources>
+
+ <content>
+ <xul:hbox class="tab-middle box-inherit" xbl:inherits="align,dir,pack,orient,selected,visuallyselected" flex="1">
+ <xul:image class="tab-icon"
+ xbl:inherits="validate,src=image"
+ role="presentation"/>
+ <xul:label class="tab-text"
+ xbl:inherits="value=label,accesskey,crop,disabled"
+ flex="1"
+ role="presentation"/>
+ </xul:hbox>
+ </content>
+
+ <implementation implements="nsIDOMXULSelectControlItemElement">
+ <property name="control" readonly="true">
+ <getter>
+ <![CDATA[
+ var parent = this.parentNode;
+ if (parent instanceof Components.interfaces.nsIDOMXULSelectControlElement)
+ return parent;
+ return null;
+ ]]>
+ </getter>
+ </property>
+
+ <property name="selected" readonly="true"
+ onget="return this.getAttribute('selected') == 'true';"/>
+
+ <property name="_selected">
+ <setter><![CDATA[
+ if (val) {
+ this.setAttribute("selected", "true");
+ this.setAttribute("visuallyselected", "true");
+ } else {
+ this.removeAttribute("selected");
+ this.removeAttribute("visuallyselected");
+ }
+
+ this._setPositionAttributes(val);
+
+ return val;
+ ]]></setter>
+ </property>
+
+ <method name="_setPositionAttributes">
+ <parameter name="aSelected"/>
+ <body><![CDATA[
+ if (this.previousSibling && this.previousSibling.localName == "tab") {
+ if (aSelected)
+ this.previousSibling.setAttribute("beforeselected", "true");
+ else
+ this.previousSibling.removeAttribute("beforeselected");
+ this.removeAttribute("first-tab");
+ } else {
+ this.setAttribute("first-tab", "true");
+ }
+
+ if (this.nextSibling && this.nextSibling.localName == "tab") {
+ if (aSelected)
+ this.nextSibling.setAttribute("afterselected", "true");
+ else
+ this.nextSibling.removeAttribute("afterselected");
+ this.removeAttribute("last-tab");
+ } else {
+ this.setAttribute("last-tab", "true");
+ }
+ ]]></body>
+ </method>
+
+ <property name="linkedPanel" onget="return this.getAttribute('linkedpanel')"
+ onset="this.setAttribute('linkedpanel', val); return val;"/>
+
+ <field name="arrowKeysShouldWrap" readonly="true">
+ /Mac/.test(navigator.platform)
+ </field>
+ <property name="TelemetryStopwatch" readonly="true">
+ <getter><![CDATA[
+ let module = {};
+ Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", module);
+ Object.defineProperty(this, "TelemetryStopwatch", {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: module.TelemetryStopwatch
+ });
+ return module.TelemetryStopwatch;
+ ]]></getter>
+ </property>
+ </implementation>
+
+ <handlers>
+ <handler event="mousedown" button="0">
+ <![CDATA[
+ if (this.disabled)
+ return;
+
+ if (this != this.parentNode.selectedItem) { // Not selected yet
+ let stopwatchid = this.parentNode.getAttribute("stopwatchid");
+ if (stopwatchid) {
+ this.TelemetryStopwatch.start(stopwatchid);
+ }
+
+ // Call this before setting the 'ignorefocus' attribute because this
+ // will pass on focus if the formerly selected tab was focused as well.
+ this.parentNode._selectNewTab(this);
+
+ var isTabFocused = false;
+ try {
+ isTabFocused = (document.commandDispatcher.focusedElement == this);
+ } catch (e) {}
+
+ // Set '-moz-user-focus' to 'ignore' so that PostHandleEvent() can't
+ // focus the tab; we only want tabs to be focusable by the mouse if
+ // they are already focused. After a short timeout we'll reset
+ // '-moz-user-focus' so that tabs can be focused by keyboard again.
+ if (!isTabFocused) {
+ this.setAttribute("ignorefocus", "true");
+ setTimeout(tab => tab.removeAttribute("ignorefocus"), 0, this);
+ }
+
+ if (stopwatchid) {
+ this.TelemetryStopwatch.finish(stopwatchid);
+ }
+ }
+ // Otherwise this tab is already selected and we will fall
+ // through to mousedown behavior which sets focus on the current tab,
+ // Only a click on an already selected tab should focus the tab itself.
+ ]]>
+ </handler>
+
+ <handler event="keydown" keycode="VK_LEFT" group="system" preventdefault="true">
+ <![CDATA[
+ var direction = window.getComputedStyle(this.parentNode, null).direction;
+ this.parentNode.advanceSelectedTab(direction == 'ltr' ? -1 : 1, this.arrowKeysShouldWrap);
+ ]]>
+ </handler>
+
+ <handler event="keydown" keycode="VK_RIGHT" group="system" preventdefault="true">
+ <![CDATA[
+ var direction = window.getComputedStyle(this.parentNode, null).direction;
+ this.parentNode.advanceSelectedTab(direction == 'ltr' ? 1 : -1, this.arrowKeysShouldWrap);
+ ]]>
+ </handler>
+
+ <handler event="keydown" keycode="VK_UP" group="system" preventdefault="true">
+ <![CDATA[
+ this.parentNode.advanceSelectedTab(-1, this.arrowKeysShouldWrap);
+ ]]>
+ </handler>
+
+ <handler event="keydown" keycode="VK_DOWN" group="system" preventdefault="true">
+ <![CDATA[
+ this.parentNode.advanceSelectedTab(1, this.arrowKeysShouldWrap);
+ ]]>
+ </handler>
+
+ <handler event="keydown" keycode="VK_HOME" group="system" preventdefault="true">
+ <![CDATA[
+ this.parentNode._selectNewTab(this.parentNode.childNodes[0]);
+ ]]>
+ </handler>
+
+ <handler event="keydown" keycode="VK_END" group="system" preventdefault="true">
+ <![CDATA[
+ var tabs = this.parentNode.childNodes;
+ this.parentNode._selectNewTab(tabs[tabs.length - 1], -1);
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+</bindings>
+
diff --git a/toolkit/content/widgets/text.xml b/toolkit/content/widgets/text.xml
new file mode 100644
index 000000000..ed998cee4
--- /dev/null
+++ b/toolkit/content/widgets/text.xml
@@ -0,0 +1,386 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<bindings id="textBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+
+ <!-- bound to <description>s -->
+ <binding id="text-base" role="xul:text">
+ <implementation implements="nsIDOMXULDescriptionElement">
+ <property name="disabled" onset="if (val) this.setAttribute('disabled', 'true');
+ else this.removeAttribute('disabled');
+ return val;"
+ onget="return this.getAttribute('disabled') == 'true';"/>
+ <property name="value" onget="return this.getAttribute('value');"
+ onset="this.setAttribute('value', val); return val;"/>
+ <property name="crop" onget="return this.getAttribute('crop');"
+ onset="this.setAttribute('crop', val); return val;"/>
+ </implementation>
+ </binding>
+
+ <binding id="text-label" extends="chrome://global/content/bindings/text.xml#text-base">
+ <implementation implements="nsIDOMXULLabelElement">
+ <property name="accessKey">
+ <getter>
+ <![CDATA[
+ var accessKey = this.getAttribute('accesskey');
+ return accessKey ? accessKey[0] : null;
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ this.setAttribute('accesskey', val);
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="control" onget="return getAttribute('control');">
+ <setter>
+ <![CDATA[
+ // After this gets set, the label will use the binding #label-control
+ this.setAttribute('control', val);
+ return val;
+ ]]>
+ </setter>
+ </property>
+ </implementation>
+ </binding>
+
+ <binding id="label-control" extends="chrome://global/content/bindings/text.xml#text-label">
+ <content>
+ <children/><html:span anonid="accessKeyParens"></html:span>
+ </content>
+ <implementation implements="nsIDOMXULLabelElement">
+ <constructor>
+ <![CDATA[
+ this.formatAccessKey(true);
+ ]]>
+ </constructor>
+
+ <method name="formatAccessKey">
+ <parameter name="firstTime"/>
+ <body>
+ <![CDATA[
+ var control = this.labeledControlElement;
+ if (!control) {
+ var bindingParent = document.getBindingParent(this);
+ if (bindingParent instanceof Components.interfaces.nsIDOMXULLabeledControlElement) {
+ control = bindingParent; // For controls that make the <label> an anon child
+ }
+ }
+ if (control) {
+ control.labelElement = this;
+ }
+
+ var accessKey = this.accessKey;
+ // No need to remove existing formatting the first time.
+ if (firstTime && !accessKey)
+ return;
+
+ if (this.mInsertSeparator === undefined) {
+ try {
+ var prefs = Components.classes["@mozilla.org/preferences-service;1"].
+ getService(Components.interfaces.nsIPrefBranch);
+ this.mUnderlineAccesskey = (prefs.getIntPref("ui.key.menuAccessKey") != 0);
+
+ const nsIPrefLocalizedString =
+ Components.interfaces.nsIPrefLocalizedString;
+
+ const prefNameInsertSeparator =
+ "intl.menuitems.insertseparatorbeforeaccesskeys";
+ const prefNameAlwaysAppendAccessKey =
+ "intl.menuitems.alwaysappendaccesskeys";
+
+ var val = prefs.getComplexValue(prefNameInsertSeparator,
+ nsIPrefLocalizedString).data;
+ this.mInsertSeparator = (val == "true");
+
+ val = prefs.getComplexValue(prefNameAlwaysAppendAccessKey,
+ nsIPrefLocalizedString).data;
+ this.mAlwaysAppendAccessKey = (val == "true");
+ }
+ catch (e) {
+ this.mInsertSeparator = true;
+ }
+ }
+
+ if (!this.mUnderlineAccesskey)
+ return;
+
+ var afterLabel = document.getAnonymousElementByAttribute(this, "anonid", "accessKeyParens");
+ afterLabel.textContent = "";
+
+ var oldAccessKey = this.getElementsByAttribute('class', 'accesskey').item(0);
+ if (oldAccessKey) { // Clear old accesskey
+ this.mergeElement(oldAccessKey);
+ }
+
+ var oldHiddenSpan =
+ this.getElementsByAttribute('class', 'hiddenColon').item(0);
+ if (oldHiddenSpan) {
+ this.mergeElement(oldHiddenSpan);
+ }
+
+ var labelText = this.textContent;
+ if (!accessKey || !labelText || !control) {
+ return;
+ }
+ var accessKeyIndex = -1;
+ if (!this.mAlwaysAppendAccessKey) {
+ accessKeyIndex = labelText.indexOf(accessKey);
+ if (accessKeyIndex < 0) { // Try again in upper case
+ accessKeyIndex =
+ labelText.toUpperCase().indexOf(accessKey.toUpperCase());
+ }
+ }
+
+ const HTML_NS = "http://www.w3.org/1999/xhtml";
+ var span = document.createElementNS(HTML_NS, "span");
+ span.className = "accesskey";
+
+ // Note that if you change the following code, see the comment of
+ // nsTextBoxFrame::UpdateAccessTitle.
+
+ // If accesskey is not in string, append in parentheses
+ if (accessKeyIndex < 0) {
+ // If end is colon, we should insert before colon.
+ // i.e., "label:" -> "label(X):"
+ var colonHidden = false;
+ if (/:$/.test(labelText)) {
+ labelText = labelText.slice(0, -1);
+ var hiddenSpan = document.createElementNS(HTML_NS, "span");
+ hiddenSpan.className = "hiddenColon";
+ hiddenSpan.style.display = "none";
+ // Hide the last colon by using span element.
+ // I.e., label<span style="display:none;">:</span>
+ this.wrapChar(hiddenSpan, labelText.length);
+ colonHidden = true;
+ }
+ // If end is space(U+20),
+ // we should not add space before parentheses.
+ var endIsSpace = false;
+ if (/ $/.test(labelText)) {
+ endIsSpace = true;
+ }
+ if (this.mInsertSeparator && !endIsSpace)
+ afterLabel.textContent = " (";
+ else
+ afterLabel.textContent = "(";
+ span.textContent = accessKey.toUpperCase();
+ afterLabel.appendChild(span);
+ if (!colonHidden)
+ afterLabel.appendChild(document.createTextNode(")"));
+ else
+ afterLabel.appendChild(document.createTextNode("):"));
+ return;
+ }
+ this.wrapChar(span, accessKeyIndex);
+ ]]>
+ </body>
+ </method>
+
+ <method name="wrapChar">
+ <parameter name="element"/>
+ <parameter name="index"/>
+ <body>
+ <![CDATA[
+ var treeWalker = document.createTreeWalker(this,
+ NodeFilter.SHOW_TEXT,
+ null);
+ var node = treeWalker.nextNode();
+ while (index >= node.length) {
+ index -= node.length;
+ node = treeWalker.nextNode();
+ }
+ if (index) {
+ node = node.splitText(index);
+ }
+ node.parentNode.insertBefore(element, node);
+ if (node.length > 1) {
+ node.splitText(1);
+ }
+ element.appendChild(node);
+ ]]>
+ </body>
+ </method>
+
+ <method name="mergeElement">
+ <parameter name="element"/>
+ <body>
+ <![CDATA[
+ if (element.previousSibling instanceof Text) {
+ element.previousSibling.appendData(element.textContent)
+ }
+ else {
+ element.parentNode.insertBefore(element.firstChild, element);
+ }
+ element.parentNode.removeChild(element);
+ ]]>
+ </body>
+ </method>
+
+ <field name="mUnderlineAccesskey">
+ !/Mac/.test(navigator.platform)
+ </field>
+ <field name="mInsertSeparator"/>
+ <field name="mAlwaysAppendAccessKey">false</field>
+
+ <property name="accessKey">
+ <getter>
+ <![CDATA[
+ var accessKey = null;
+ var labeledEl = this.labeledControlElement;
+ if (labeledEl) {
+ accessKey = labeledEl.getAttribute('accesskey');
+ }
+ if (!accessKey) {
+ accessKey = this.getAttribute('accesskey');
+ }
+ return accessKey ? accessKey[0] : null;
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ // If this label already has an accesskey attribute store it here as well
+ if (this.hasAttribute('accesskey')) {
+ this.setAttribute('accesskey', val);
+ }
+ var control = this.labeledControlElement;
+ if (control) {
+ control.setAttribute('accesskey', val);
+ }
+ this.formatAccessKey(false);
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="labeledControlElement" readonly="true"
+ onget="var control = this.control; return control ? document.getElementById(control) : null;" />
+
+ <property name="control" onget="return this.getAttribute('control');">
+ <setter>
+ <![CDATA[
+ var control = this.labeledControlElement;
+ if (control) {
+ control.labelElement = null; // No longer pointed to be this label
+ }
+ this.setAttribute('control', val);
+ this.formatAccessKey(false);
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ </implementation>
+
+ <handlers>
+ <handler event="click" action="if (this.disabled) return;
+ var controlElement = this.labeledControlElement;
+ if(controlElement)
+ controlElement.focus();
+ "/>
+ </handlers>
+ </binding>
+
+ <binding id="text-link" extends="chrome://global/content/bindings/text.xml#text-label" role="xul:link">
+ <implementation>
+ <property name="href" onget="return this.getAttribute('href');"
+ onset="this.setAttribute('href', val); return val;" />
+ <method name="open">
+ <parameter name="aEvent"/>
+ <body>
+ <![CDATA[
+ var href = this.href;
+ if (!href || this.disabled || aEvent.defaultPrevented)
+ return;
+
+ var uri = null;
+ try {
+ const nsISSM = Components.interfaces.nsIScriptSecurityManager;
+ const secMan =
+ Components.classes["@mozilla.org/scriptsecuritymanager;1"]
+ .getService(nsISSM);
+
+ const ioService =
+ Components.classes["@mozilla.org/network/io-service;1"]
+ .getService(Components.interfaces.nsIIOService);
+
+ uri = ioService.newURI(href, null, null);
+
+ let principal;
+ if (this.getAttribute("useoriginprincipal") == "true") {
+ principal = this.nodePrincipal;
+ } else {
+ principal = secMan.createNullPrincipal({});
+ }
+ try {
+ secMan.checkLoadURIWithPrincipal(principal, uri,
+ nsISSM.DISALLOW_INHERIT_PRINCIPAL);
+ }
+ catch (ex) {
+ var msg = "Error: Cannot open a " + uri.scheme + ": link using \
+ the text-link binding.";
+ Components.utils.reportError(msg);
+ return;
+ }
+
+ const cID = "@mozilla.org/uriloader/external-protocol-service;1";
+ const nsIEPS = Components.interfaces.nsIExternalProtocolService;
+ var protocolSvc = Components.classes[cID].getService(nsIEPS);
+
+ // if the scheme is not an exposed protocol, then opening this link
+ // should be deferred to the system's external protocol handler
+ if (!protocolSvc.isExposedProtocol(uri.scheme)) {
+ protocolSvc.loadUrl(uri);
+ aEvent.preventDefault()
+ return;
+ }
+
+ }
+ catch (ex) {
+ Components.utils.reportError(ex);
+ }
+
+ aEvent.preventDefault();
+ href = uri ? uri.spec : href;
+
+ // Try handing off the link to the host application, e.g. for
+ // opening it in a tabbed browser.
+ var linkHandled = Components.classes["@mozilla.org/supports-PRBool;1"]
+ .createInstance(Components.interfaces.nsISupportsPRBool);
+ linkHandled.data = false;
+ let {shiftKey, ctrlKey, metaKey, altKey, button} = aEvent;
+ let data = {shiftKey, ctrlKey, metaKey, altKey, button, href};
+ Components.classes["@mozilla.org/observer-service;1"]
+ .getService(Components.interfaces.nsIObserverService)
+ .notifyObservers(linkHandled, "handle-xul-text-link", JSON.stringify(data));
+ if (linkHandled.data)
+ return;
+
+ // otherwise, fall back to opening the anchor directly
+ var win = window;
+ if (window instanceof Components.interfaces.nsIDOMChromeWindow) {
+ while (win.opener && !win.opener.closed)
+ win = win.opener;
+ }
+ win.open(href);
+ ]]>
+ </body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="click" phase="capturing" button="0" action="this.open(event)"/>
+ <handler event="click" phase="capturing" button="1" action="this.open(event)"/>
+ <handler event="keypress" preventdefault="true" keycode="VK_RETURN" action="this.click()" />
+ </handlers>
+ </binding>
+
+</bindings>
diff --git a/toolkit/content/widgets/textbox.xml b/toolkit/content/widgets/textbox.xml
new file mode 100644
index 000000000..f166fb78a
--- /dev/null
+++ b/toolkit/content/widgets/textbox.xml
@@ -0,0 +1,646 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<!DOCTYPE bindings [
+ <!ENTITY % textcontextDTD SYSTEM "chrome://global/locale/textcontext.dtd" >
+ %textcontextDTD;
+]>
+
+<bindings id="textboxBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="textbox" extends="xul:box" role="xul:textbox">
+ <resources>
+ <stylesheet src="chrome://global/content/textbox.css"/>
+ <stylesheet src="chrome://global/skin/textbox.css"/>
+ </resources>
+
+ <content>
+ <children/>
+ <xul:hbox class="textbox-input-box" flex="1" xbl:inherits="context,spellcheck">
+ <html:input class="textbox-input" anonid="input"
+ xbl:inherits="value,type,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey,noinitialfocus,mozactionhint,spellcheck"/>
+ </xul:hbox>
+ </content>
+
+ <implementation implements="nsIDOMXULTextBoxElement, nsIDOMXULLabeledControlElement">
+ <!-- nsIDOMXULLabeledControlElement -->
+ <field name="crop">""</field>
+ <field name="image">""</field>
+ <field name="command">""</field>
+ <field name="accessKey">""</field>
+
+ <field name="mInputField">null</field>
+ <field name="mIgnoreClick">false</field>
+ <field name="mIgnoreFocus">false</field>
+ <field name="mEditor">null</field>
+
+ <property name="inputField" readonly="true">
+ <getter><![CDATA[
+ if (!this.mInputField)
+ this.mInputField = document.getAnonymousElementByAttribute(this, "anonid", "input");
+ return this.mInputField;
+ ]]></getter>
+ </property>
+
+ <property name="value" onset="this.inputField.value = val; return val;"
+ onget="return this.inputField.value;"/>
+ <property name="defaultValue" onset="this.inputField.defaultValue = val; return val;"
+ onget="return this.inputField.defaultValue;"/>
+ <property name="label" onset="this.setAttribute('label', val); return val;"
+ onget="return this.getAttribute('label') ||
+ (this.labelElement ? this.labelElement.value :
+ this.placeholder);"/>
+ <property name="placeholder" onset="this.inputField.placeholder = val; return val;"
+ onget="return this.inputField.placeholder;"/>
+ <property name="emptyText" onset="this.placeholder = val; return val;"
+ onget="return this.placeholder;"/>
+ <property name="type" onset="if (val) this.setAttribute('type', val);
+ else this.removeAttribute('type'); return val;"
+ onget="return this.getAttribute('type');"/>
+ <property name="maxLength" onset="this.inputField.maxLength = val; return val;"
+ onget="return this.inputField.maxLength;"/>
+ <property name="disabled" onset="this.inputField.disabled = val;
+ if (val) this.setAttribute('disabled', 'true');
+ else this.removeAttribute('disabled'); return val;"
+ onget="return this.inputField.disabled;"/>
+ <property name="tabIndex" onget="return parseInt(this.getAttribute('tabindex'));"
+ onset="this.inputField.tabIndex = val;
+ if (val) this.setAttribute('tabindex', val);
+ else this.removeAttribute('tabindex'); return val;"/>
+ <property name="size" onset="this.inputField.size = val; return val;"
+ onget="return this.inputField.size;"/>
+ <property name="readOnly" onset="this.inputField.readOnly = val;
+ if (val) this.setAttribute('readonly', 'true');
+ else this.removeAttribute('readonly'); return val;"
+ onget="return this.inputField.readOnly;"/>
+ <property name="clickSelectsAll"
+ onget="return this.getAttribute('clickSelectsAll') == 'true';"
+ onset="if (val) this.setAttribute('clickSelectsAll', 'true');
+ else this.removeAttribute('clickSelectsAll'); return val;" />
+
+ <property name="editor" readonly="true">
+ <getter><![CDATA[
+ if (!this.mEditor) {
+ const nsIDOMNSEditableElement = Components.interfaces.nsIDOMNSEditableElement;
+ this.mEditor = this.inputField.QueryInterface(nsIDOMNSEditableElement).editor;
+ }
+ return this.mEditor;
+ ]]></getter>
+ </property>
+
+ <method name="reset">
+ <body><![CDATA[
+ this.value = this.defaultValue;
+ try {
+ this.editor.transactionManager.clear();
+ return true;
+ }
+ catch (e) {}
+ return false;
+ ]]></body>
+ </method>
+
+ <method name="select">
+ <body>
+ this.inputField.select();
+ </body>
+ </method>
+
+ <property name="controllers" readonly="true" onget="return this.inputField.controllers"/>
+ <property name="textLength" readonly="true"
+ onget="return this.inputField.textLength;"/>
+ <property name="selectionStart" onset="this.inputField.selectionStart = val; return val;"
+ onget="return this.inputField.selectionStart;"/>
+ <property name="selectionEnd" onset="this.inputField.selectionEnd = val; return val;"
+ onget="return this.inputField.selectionEnd;"/>
+
+ <method name="setSelectionRange">
+ <parameter name="aSelectionStart"/>
+ <parameter name="aSelectionEnd"/>
+ <body>
+ this.inputField.setSelectionRange( aSelectionStart, aSelectionEnd );
+ </body>
+ </method>
+
+ <method name="_setNewlineHandling">
+ <body><![CDATA[
+ var str = this.getAttribute("newlines");
+ if (str && this.editor) {
+ const nsIPlaintextEditor = Components.interfaces.nsIPlaintextEditor;
+ for (var x in nsIPlaintextEditor) {
+ if (/^eNewlines/.test(x)) {
+ if (str == RegExp.rightContext.toLowerCase()) {
+ this.editor.QueryInterface(nsIPlaintextEditor)
+ .newlineHandling = nsIPlaintextEditor[x];
+ break;
+ }
+ }
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="_maybeSelectAll">
+ <body><![CDATA[
+ if (!this.mIgnoreClick && this.clickSelectsAll &&
+ document.activeElement == this.inputField &&
+ this.inputField.selectionStart == this.inputField.selectionEnd)
+ this.editor.selectAll();
+ ]]></body>
+ </method>
+
+ <constructor><![CDATA[
+ var str = this.boxObject.getProperty("value");
+ if (str) {
+ this.inputField.value = str;
+ this.boxObject.removeProperty("value");
+ }
+
+ this._setNewlineHandling();
+
+ if (this.hasAttribute("emptytext"))
+ this.placeholder = this.getAttribute("emptytext");
+ ]]></constructor>
+
+ <destructor>
+ <![CDATA[
+ var field = this.inputField;
+ if (field && field.value)
+ this.boxObject.setProperty('value', field.value);
+ this.mInputField = null;
+ ]]>
+ </destructor>
+
+ </implementation>
+
+ <handlers>
+ <handler event="focus" phase="capturing">
+ <![CDATA[
+ if (this.hasAttribute("focused"))
+ return;
+
+ switch (event.originalTarget) {
+ case this:
+ // Forward focus to actual HTML input
+ this.inputField.focus();
+ break;
+ case this.inputField:
+ if (this.mIgnoreFocus) {
+ this.mIgnoreFocus = false;
+ } else if (this.clickSelectsAll) {
+ try {
+ const nsIEditorIMESupport =
+ Components.interfaces.nsIEditorIMESupport;
+ let imeEditor = this.editor.QueryInterface(nsIEditorIMESupport);
+ if (!imeEditor || !imeEditor.composing)
+ this.editor.selectAll();
+ } catch (e) {}
+ }
+ break;
+ default:
+ // Allow other children (e.g. URL bar buttons) to get focus
+ return;
+ }
+ this.setAttribute("focused", "true");
+ ]]>
+ </handler>
+
+ <handler event="blur" phase="capturing">
+ <![CDATA[
+ this.removeAttribute("focused");
+
+ // don't trigger clickSelectsAll when switching application windows
+ if (window == window.top &&
+ window.constructor == ChromeWindow &&
+ document.activeElement == this.inputField)
+ this.mIgnoreFocus = true;
+ ]]>
+ </handler>
+
+ <handler event="mousedown">
+ <![CDATA[
+ this.mIgnoreClick = this.hasAttribute("focused");
+
+ if (!this.mIgnoreClick) {
+ this.mIgnoreFocus = true;
+ this.inputField.setSelectionRange(0, 0);
+ if (event.originalTarget == this ||
+ event.originalTarget == this.inputField.parentNode)
+ this.inputField.focus();
+ }
+ ]]>
+ </handler>
+
+ <handler event="click" action="this._maybeSelectAll();"/>
+
+#ifndef XP_WIN
+ <handler event="contextmenu">
+ // Only care about context clicks on the textbox itself.
+ if (event.target != this)
+ return;
+
+ if (!event.button) // context menu opened via keyboard shortcut
+ return;
+ this._maybeSelectAll();
+ // see bug 576135 comment 4
+ let box = this.inputField.parentNode;
+ let menu = document.getAnonymousElementByAttribute(box, "anonid", "input-box-contextmenu");
+ box._doPopupItemEnabling(menu);
+ </handler>
+#endif
+ </handlers>
+ </binding>
+
+ <binding id="timed-textbox" extends="chrome://global/content/bindings/textbox.xml#textbox">
+ <implementation>
+ <constructor><![CDATA[
+ try {
+ var consoleService = Components.classes["@mozilla.org/consoleservice;1"]
+ .getService(Components.interfaces.nsIConsoleService);
+ var scriptError = Components.classes["@mozilla.org/scripterror;1"]
+ .createInstance(Components.interfaces.nsIScriptError);
+ scriptError.init("Timed textboxes are deprecated. Consider using type=\"search\" instead.",
+ this.ownerDocument.location.href, null, null,
+ null, scriptError.warningFlag, "XUL Widgets");
+ consoleService.logMessage(scriptError);
+ } catch (e) {}
+ ]]></constructor>
+ <field name="_timer">null</field>
+ <property name="timeout"
+ onset="this.setAttribute('timeout', val); return val;"
+ onget="return parseInt(this.getAttribute('timeout')) || 0;"/>
+ <property name="value"
+ onget="return this.inputField.value;">
+ <setter><![CDATA[
+ this.inputField.value = val;
+ if (this._timer)
+ clearTimeout(this._timer);
+ return val;
+ ]]></setter>
+ </property>
+ <method name="_fireCommand">
+ <parameter name="me"/>
+ <body>
+ <![CDATA[
+ me._timer = null;
+ me.doCommand();
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ <handlers>
+ <handler event="input">
+ <![CDATA[
+ if (this._timer)
+ clearTimeout(this._timer);
+ this._timer = this.timeout && setTimeout(this._fireCommand, this.timeout, this);
+ ]]>
+ </handler>
+ <handler event="keypress" keycode="VK_RETURN">
+ <![CDATA[
+ if (this._timer)
+ clearTimeout(this._timer);
+ this._fireCommand(this);
+ event.preventDefault();
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="search-textbox" extends="chrome://global/content/bindings/textbox.xml#textbox">
+ <content>
+ <children/>
+ <xul:hbox class="textbox-input-box" flex="1" xbl:inherits="context,spellcheck" align="center">
+ <html:input class="textbox-input" anonid="input" mozactionhint="search"
+ xbl:inherits="value,type,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey,mozactionhint,spellcheck"/>
+ <xul:deck class="textbox-search-icons" anonid="search-icons">
+ <xul:image class="textbox-search-icon" anonid="searchbutton-icon"
+ xbl:inherits="src=image,label=searchbuttonlabel,searchbutton,disabled"/>
+ <xul:image class="textbox-search-clear"
+ onclick="document.getBindingParent(this)._clearSearch();"
+ label="&searchTextBox.clear.label;"
+ xbl:inherits="disabled"/>
+ </xul:deck>
+ </xul:hbox>
+ </content>
+ <implementation>
+ <field name="_timer">null</field>
+ <field name="_searchIcons">
+ document.getAnonymousElementByAttribute(this, "anonid", "search-icons");
+ </field>
+ <field name="_searchButtonIcon">
+ document.getAnonymousElementByAttribute(this, "anonid", "searchbutton-icon");
+ </field>
+ <property name="timeout"
+ onset="this.setAttribute('timeout', val); return val;"
+ onget="return parseInt(this.getAttribute('timeout')) || 500;"/>
+ <property name="searchButton"
+ onget="return this.getAttribute('searchbutton') == 'true';">
+ <setter><![CDATA[
+ if (val) {
+ this.setAttribute("searchbutton", "true");
+ this.removeAttribute("aria-autocomplete");
+ // Hack for the button to get the right accessible:
+ this._searchButtonIcon.setAttribute("onclick", "true");
+ } else {
+ this.removeAttribute("searchbutton");
+ this._searchButtonIcon.removeAttribute("onclick");
+ this.setAttribute("aria-autocomplete", "list");
+ }
+ return val;
+ ]]></setter>
+ </property>
+ <property name="value"
+ onget="return this.inputField.value;">
+ <setter><![CDATA[
+ this.inputField.value = val;
+
+ if (val)
+ this._searchIcons.selectedIndex = this.searchButton ? 0 : 1;
+ else
+ this._searchIcons.selectedIndex = 0;
+
+ if (this._timer)
+ clearTimeout(this._timer);
+
+ return val;
+ ]]></setter>
+ </property>
+ <constructor><![CDATA[
+ // Ensure the button state is up to date:
+ this.searchButton = this.searchButton;
+ this._searchButtonIcon.addEventListener("click", (e) => this._iconClick(e), false);
+ ]]></constructor>
+ <method name="_fireCommand">
+ <parameter name="me"/>
+ <body><![CDATA[
+ if (me._timer)
+ clearTimeout(me._timer);
+ me._timer = null;
+ me.doCommand();
+ ]]></body>
+ </method>
+ <method name="_iconClick">
+ <body><![CDATA[
+ if (this.searchButton)
+ this._enterSearch();
+ else
+ this.focus();
+ ]]></body>
+ </method>
+ <method name="_enterSearch">
+ <body><![CDATA[
+ if (this.disabled)
+ return;
+ if (this.searchButton && this.value && !this.readOnly)
+ this._searchIcons.selectedIndex = 1;
+ this._fireCommand(this);
+ ]]></body>
+ </method>
+ <method name="_clearSearch">
+ <body><![CDATA[
+ if (!this.disabled && !this.readOnly && this.value) {
+ this.value = "";
+ this._fireCommand(this);
+ this._searchIcons.selectedIndex = 0;
+ return true;
+ }
+ return false;
+ ]]></body>
+ </method>
+ </implementation>
+ <handlers>
+ <handler event="input">
+ <![CDATA[
+ if (this.searchButton) {
+ this._searchIcons.selectedIndex = 0;
+ return;
+ }
+ if (this._timer)
+ clearTimeout(this._timer);
+ this._timer = this.timeout && setTimeout(this._fireCommand, this.timeout, this);
+ this._searchIcons.selectedIndex = this.value ? 1 : 0;
+ ]]>
+ </handler>
+ <handler event="keypress" keycode="VK_ESCAPE">
+ <![CDATA[
+ if (this._clearSearch()) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ ]]>
+ </handler>
+ <handler event="keypress" keycode="VK_RETURN">
+ <![CDATA[
+ this._enterSearch();
+ event.preventDefault();
+ event.stopPropagation();
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="textarea" extends="chrome://global/content/bindings/textbox.xml#textbox">
+ <content>
+ <xul:hbox class="textbox-input-box" flex="1" xbl:inherits="context,spellcheck">
+ <html:textarea class="textbox-textarea" anonid="input"
+ xbl:inherits="xbl:text=value,disabled,tabindex,rows,cols,readonly,wrap,placeholder,mozactionhint,spellcheck"><children/></html:textarea>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="input-box">
+ <content context="_child">
+ <children/>
+ <xul:menupopup anonid="input-box-contextmenu"
+ class="textbox-contextmenu"
+ onpopupshowing="var input =
+ this.parentNode.getElementsByAttribute('anonid', 'input')[0];
+ if (document.commandDispatcher.focusedElement != input)
+ input.focus();
+ this.parentNode._doPopupItemEnabling(this);"
+ oncommand="var cmd = event.originalTarget.getAttribute('cmd'); if(cmd) { this.parentNode.doCommand(cmd); event.stopPropagation(); }">
+ <xul:menuitem label="&undoCmd.label;" accesskey="&undoCmd.accesskey;" cmd="cmd_undo"/>
+ <xul:menuseparator/>
+ <xul:menuitem label="&cutCmd.label;" accesskey="&cutCmd.accesskey;" cmd="cmd_cut"/>
+ <xul:menuitem label="&copyCmd.label;" accesskey="&copyCmd.accesskey;" cmd="cmd_copy"/>
+ <xul:menuitem label="&pasteCmd.label;" accesskey="&pasteCmd.accesskey;" cmd="cmd_paste"/>
+ <xul:menuitem label="&deleteCmd.label;" accesskey="&deleteCmd.accesskey;" cmd="cmd_delete"/>
+ <xul:menuseparator/>
+ <xul:menuitem label="&selectAllCmd.label;" accesskey="&selectAllCmd.accesskey;" cmd="cmd_selectAll"/>
+ </xul:menupopup>
+ </content>
+
+ <implementation>
+ <method name="_doPopupItemEnabling">
+ <parameter name="popupNode"/>
+ <body>
+ <![CDATA[
+ var children = popupNode.childNodes;
+ for (var i = 0; i < children.length; i++) {
+ var command = children[i].getAttribute("cmd");
+ if (command) {
+ var controller = document.commandDispatcher.getControllerForCommand(command);
+ var enabled = controller.isCommandEnabled(command);
+ if (enabled)
+ children[i].removeAttribute("disabled");
+ else
+ children[i].setAttribute("disabled", "true");
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="_setMenuItemVisibility">
+ <parameter name="anonid"/>
+ <parameter name="visible"/>
+ <body><![CDATA[
+ document.getAnonymousElementByAttribute(this, "anonid", anonid).
+ hidden = ! visible;
+ ]]></body>
+ </method>
+
+ <method name="doCommand">
+ <parameter name="command"/>
+ <body>
+ <![CDATA[
+ var controller = document.commandDispatcher.getControllerForCommand(command);
+ controller.doCommand(command);
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="input-box-spell" extends="chrome://global/content/bindings/textbox.xml#input-box">
+ <content context="_child">
+ <children/>
+ <xul:menupopup anonid="input-box-contextmenu"
+ class="textbox-contextmenu"
+ onpopupshowing="var input =
+ this.parentNode.getElementsByAttribute('anonid', 'input')[0];
+ if (document.commandDispatcher.focusedElement != input)
+ input.focus();
+ this.parentNode._doPopupItemEnablingSpell(this);"
+ onpopuphiding="this.parentNode._doPopupItemDisabling(this);"
+ oncommand="var cmd = event.originalTarget.getAttribute('cmd'); if(cmd) { this.parentNode.doCommand(cmd); event.stopPropagation(); }">
+ <xul:menuitem label="&spellNoSuggestions.label;" anonid="spell-no-suggestions" disabled="true"/>
+ <xul:menuitem label="&spellAddToDictionary.label;" accesskey="&spellAddToDictionary.accesskey;" anonid="spell-add-to-dictionary"
+ oncommand="this.parentNode.parentNode.spellCheckerUI.addToDictionary();"/>
+ <xul:menuitem label="&spellUndoAddToDictionary.label;" accesskey="&spellUndoAddToDictionary.accesskey;" anonid="spell-undo-add-to-dictionary"
+ oncommand="this.parentNode.parentNode.spellCheckerUI.undoAddToDictionary();"/>
+ <xul:menuseparator anonid="spell-suggestions-separator"/>
+ <xul:menuitem label="&undoCmd.label;" accesskey="&undoCmd.accesskey;" cmd="cmd_undo"/>
+ <xul:menuseparator/>
+ <xul:menuitem label="&cutCmd.label;" accesskey="&cutCmd.accesskey;" cmd="cmd_cut"/>
+ <xul:menuitem label="&copyCmd.label;" accesskey="&copyCmd.accesskey;" cmd="cmd_copy"/>
+ <xul:menuitem label="&pasteCmd.label;" accesskey="&pasteCmd.accesskey;" cmd="cmd_paste"/>
+ <xul:menuitem label="&deleteCmd.label;" accesskey="&deleteCmd.accesskey;" cmd="cmd_delete"/>
+ <xul:menuseparator/>
+ <xul:menuitem label="&selectAllCmd.label;" accesskey="&selectAllCmd.accesskey;" cmd="cmd_selectAll"/>
+ <xul:menuseparator anonid="spell-check-separator"/>
+ <xul:menuitem label="&spellCheckToggle.label;" type="checkbox" accesskey="&spellCheckToggle.accesskey;" anonid="spell-check-enabled"
+ oncommand="this.parentNode.parentNode.spellCheckerUI.toggleEnabled();"/>
+ <xul:menu label="&spellDictionaries.label;" accesskey="&spellDictionaries.accesskey;" anonid="spell-dictionaries">
+ <xul:menupopup anonid="spell-dictionaries-menu"
+ onpopupshowing="event.stopPropagation();"
+ onpopuphiding="event.stopPropagation();"/>
+ </xul:menu>
+ </xul:menupopup>
+ </content>
+
+ <implementation>
+ <field name="_spellCheckInitialized">false</field>
+ <field name="_enabledCheckbox">
+ document.getAnonymousElementByAttribute(this, "anonid", "spell-check-enabled");
+ </field>
+ <field name="_suggestionsSeparator">
+ document.getAnonymousElementByAttribute(this, "anonid", "spell-no-suggestions");
+ </field>
+ <field name="_dictionariesMenu">
+ document.getAnonymousElementByAttribute(this, "anonid", "spell-dictionaries-menu");
+ </field>
+
+ <property name="spellCheckerUI" readonly="true">
+ <getter><![CDATA[
+ if (!this._spellCheckInitialized) {
+ this._spellCheckInitialized = true;
+
+ const CI = Components.interfaces;
+ if (!(document instanceof CI.nsIDOMXULDocument))
+ return null;
+
+ var textbox = document.getBindingParent(this);
+ if (!textbox || !(textbox instanceof CI.nsIDOMXULTextBoxElement))
+ return null;
+
+ try {
+ Components.utils.import("resource://gre/modules/InlineSpellChecker.jsm", this);
+ this.InlineSpellCheckerUI = new this.InlineSpellChecker(textbox.editor);
+ } catch (ex) { }
+ }
+
+ return this.InlineSpellCheckerUI;
+ ]]></getter>
+ </property>
+
+ <method name="_doPopupItemEnablingSpell">
+ <parameter name="popupNode"/>
+ <body>
+ <![CDATA[
+ var spellui = this.spellCheckerUI;
+ if (!spellui || !spellui.canSpellCheck) {
+ this._setMenuItemVisibility("spell-no-suggestions", false);
+ this._setMenuItemVisibility("spell-check-enabled", false);
+ this._setMenuItemVisibility("spell-check-separator", false);
+ this._setMenuItemVisibility("spell-add-to-dictionary", false);
+ this._setMenuItemVisibility("spell-undo-add-to-dictionary", false);
+ this._setMenuItemVisibility("spell-suggestions-separator", false);
+ this._setMenuItemVisibility("spell-dictionaries", false);
+ return;
+ }
+
+ spellui.initFromEvent(document.popupRangeParent,
+ document.popupRangeOffset);
+
+ var enabled = spellui.enabled;
+ var showUndo = spellui.canSpellCheck && spellui.canUndo();
+ this._enabledCheckbox.setAttribute("checked", enabled);
+
+ var overMisspelling = spellui.overMisspelling;
+ this._setMenuItemVisibility("spell-add-to-dictionary", overMisspelling);
+ this._setMenuItemVisibility("spell-undo-add-to-dictionary", showUndo);
+ this._setMenuItemVisibility("spell-suggestions-separator", overMisspelling || showUndo);
+
+ // suggestion list
+ var numsug = spellui.addSuggestionsToMenu(popupNode, this._suggestionsSeparator, 5);
+ this._setMenuItemVisibility("spell-no-suggestions", overMisspelling && numsug == 0);
+
+ // dictionary list
+ var numdicts = spellui.addDictionaryListToMenu(this._dictionariesMenu, null);
+ this._setMenuItemVisibility("spell-dictionaries", enabled && numdicts > 1);
+
+ this._doPopupItemEnabling(popupNode);
+ ]]>
+ </body>
+ </method>
+ <method name="_doPopupItemDisabling">
+ <body><![CDATA[
+ if (this.spellCheckerUI) {
+ this.spellCheckerUI.clearSuggestionsFromMenu();
+ this.spellCheckerUI.clearDictionaryListFromMenu();
+ }
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+</bindings>
diff --git a/toolkit/content/widgets/timekeeper.js b/toolkit/content/widgets/timekeeper.js
new file mode 100644
index 000000000..2234c9e50
--- /dev/null
+++ b/toolkit/content/widgets/timekeeper.js
@@ -0,0 +1,418 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * TimeKeeper keeps track of the time states. Given min, max, step, and
+ * format (12/24hr), TimeKeeper will determine the ranges of possible
+ * selections, and whether or not the current time state is out of range
+ * or off step.
+ *
+ * @param {Object} props
+ * {
+ * {Date} min
+ * {Date} max
+ * {Number} stepInMs
+ * {String} format: Either "12" or "24"
+ * }
+ */
+function TimeKeeper(props) {
+ this.props = props;
+ this.state = { time: new Date(0), ranges: {} };
+}
+
+{
+ const debug = 0 ? console.log.bind(console, '[timekeeper]') : function() {};
+
+ const DAY_PERIOD_IN_HOURS = 12,
+ SECOND_IN_MS = 1000,
+ MINUTE_IN_MS = 60000,
+ HOUR_IN_MS = 3600000,
+ DAY_PERIOD_IN_MS = 43200000,
+ DAY_IN_MS = 86400000,
+ TIME_FORMAT_24 = "24";
+
+ TimeKeeper.prototype = {
+ /**
+ * Getters for different time units.
+ * @return {Number}
+ */
+ get hour() {
+ return this.state.time.getUTCHours();
+ },
+ get minute() {
+ return this.state.time.getUTCMinutes();
+ },
+ get second() {
+ return this.state.time.getUTCSeconds();
+ },
+ get millisecond() {
+ return this.state.time.getUTCMilliseconds();
+ },
+ get dayPeriod() {
+ // 0 stands for AM and 12 for PM
+ return this.state.time.getUTCHours() < DAY_PERIOD_IN_HOURS ? 0 : DAY_PERIOD_IN_HOURS;
+ },
+
+ /**
+ * Get the ranges of different time units.
+ * @return {Object}
+ * {
+ * {Array<Number>} dayPeriod
+ * {Array<Number>} hours
+ * {Array<Number>} minutes
+ * {Array<Number>} seconds
+ * {Array<Number>} milliseconds
+ * }
+ */
+ get ranges() {
+ return this.state.ranges;
+ },
+
+ /**
+ * Set new time, check if the current state is valid, and set ranges.
+ *
+ * @param {Object} timeState: The new time
+ * {
+ * {Number} hour [optional]
+ * {Number} minute [optional]
+ * {Number} second [optional]
+ * {Number} millisecond [optional]
+ * }
+ */
+ setState(timeState) {
+ const { min, max } = this.props;
+ const { hour, minute, second, millisecond } = timeState;
+
+ if (hour != undefined) {
+ this.state.time.setUTCHours(hour);
+ }
+ if (minute != undefined) {
+ this.state.time.setUTCMinutes(minute);
+ }
+ if (second != undefined) {
+ this.state.time.setUTCSeconds(second);
+ }
+ if (millisecond != undefined) {
+ this.state.time.setUTCMilliseconds(millisecond);
+ }
+
+ this.state.isOffStep = this._isOffStep(this.state.time);
+ this.state.isOutOfRange = (this.state.time < min || this.state.time > max);
+ this.state.isInvalid = this.state.isOutOfRange || this.state.isOffStep;
+
+ this._setRanges(this.dayPeriod, this.hour, this.minute, this.second);
+ },
+
+ /**
+ * Set day-period (AM/PM)
+ * @param {Number} dayPeriod: 0 as AM, 12 as PM
+ */
+ setDayPeriod(dayPeriod) {
+ if (dayPeriod == this.dayPeriod) {
+ return;
+ }
+
+ if (dayPeriod == 0) {
+ this.setState({ hour: this.hour - DAY_PERIOD_IN_HOURS });
+ } else {
+ this.setState({ hour: this.hour + DAY_PERIOD_IN_HOURS });
+ }
+ },
+
+ /**
+ * Set hour in 24hr format (0 ~ 23)
+ * @param {Number} hour
+ */
+ setHour(hour) {
+ this.setState({ hour });
+ },
+
+ /**
+ * Set minute (0 ~ 59)
+ * @param {Number} minute
+ */
+ setMinute(minute) {
+ this.setState({ minute });
+ },
+
+ /**
+ * Set second (0 ~ 59)
+ * @param {Number} second
+ */
+ setSecond(second) {
+ this.setState({ second });
+ },
+
+ /**
+ * Set millisecond (0 ~ 999)
+ * @param {Number} millisecond
+ */
+ setMillisecond(millisecond) {
+ this.setState({ millisecond });
+ },
+
+ /**
+ * Calculate the range of possible choices for each time unit.
+ * Reuse the old result if the input has not changed.
+ *
+ * @param {Number} dayPeriod
+ * @param {Number} hour
+ * @param {Number} minute
+ * @param {Number} second
+ */
+ _setRanges(dayPeriod, hour, minute, second) {
+ this.state.ranges.dayPeriod =
+ this.state.ranges.dayPeriod || this._getDayPeriodRange();
+
+ if (this.state.dayPeriod != dayPeriod) {
+ this.state.ranges.hours = this._getHoursRange(dayPeriod);
+ }
+
+ if (this.state.hour != hour) {
+ this.state.ranges.minutes = this._getMinutesRange(hour);
+ }
+
+ if (this.state.hour != hour || this.state.minute != minute) {
+ this.state.ranges.seconds = this._getSecondsRange(hour, minute);
+ }
+
+ if (this.state.hour != hour || this.state.minute != minute || this.state.second != second) {
+ this.state.ranges.milliseconds = this._getMillisecondsRange(hour, minute, second);
+ }
+
+ // Save the time states for comparison.
+ this.state.dayPeriod = dayPeriod;
+ this.state.hour = hour;
+ this.state.minute = minute;
+ this.state.second = second;
+ },
+
+ /**
+ * Get the AM/PM range. Return an empty array if in 24hr mode.
+ *
+ * @return {Array<Number>}
+ */
+ _getDayPeriodRange() {
+ if (this.props.format == TIME_FORMAT_24) {
+ return [];
+ }
+
+ const start = 0;
+ const end = DAY_IN_MS - 1;
+ const minStep = DAY_PERIOD_IN_MS;
+ const formatter = (time) =>
+ new Date(time).getUTCHours() < DAY_PERIOD_IN_HOURS ? 0 : DAY_PERIOD_IN_HOURS;
+
+ return this._getSteps(start, end, minStep, formatter);
+ },
+
+ /**
+ * Get the hours range.
+ *
+ * @param {Number} dayPeriod
+ * @return {Array<Number>}
+ */
+ _getHoursRange(dayPeriod) {
+ const { format } = this.props;
+ const start = format == "24" ? 0 : dayPeriod * HOUR_IN_MS;
+ const end = format == "24" ? DAY_IN_MS - 1 : start + DAY_PERIOD_IN_MS - 1;
+ const minStep = HOUR_IN_MS;
+ const formatter = (time) => new Date(time).getUTCHours();
+
+ return this._getSteps(start, end, minStep, formatter);
+ },
+
+ /**
+ * Get the minutes range
+ *
+ * @param {Number} hour
+ * @return {Array<Number>}
+ */
+ _getMinutesRange(hour) {
+ const start = hour * HOUR_IN_MS;
+ const end = start + HOUR_IN_MS - 1;
+ const minStep = MINUTE_IN_MS;
+ const formatter = (time) => new Date(time).getUTCMinutes();
+
+ return this._getSteps(start, end, minStep, formatter);
+ },
+
+ /**
+ * Get the seconds range
+ *
+ * @param {Number} hour
+ * @param {Number} minute
+ * @return {Array<Number>}
+ */
+ _getSecondsRange(hour, minute) {
+ const start = hour * HOUR_IN_MS + minute * MINUTE_IN_MS;
+ const end = start + MINUTE_IN_MS - 1;
+ const minStep = SECOND_IN_MS;
+ const formatter = (time) => new Date(time).getUTCSeconds();
+
+ return this._getSteps(start, end, minStep, formatter);
+ },
+
+ /**
+ * Get the milliseconds range
+ * @param {Number} hour
+ * @param {Number} minute
+ * @param {Number} second
+ * @return {Array<Number>}
+ */
+ _getMillisecondsRange(hour, minute, second) {
+ const start = hour * HOUR_IN_MS + minute * MINUTE_IN_MS + second * SECOND_IN_MS;
+ const end = start + SECOND_IN_MS - 1;
+ const minStep = 1;
+ const formatter = (time) => new Date(time).getUTCMilliseconds();
+
+ return this._getSteps(start, end, minStep, formatter);
+ },
+
+ /**
+ * Calculate the range of possible steps.
+ *
+ * @param {Number} startValue: Start time in ms
+ * @param {Number} endValue: End time in ms
+ * @param {Number} minStep: Smallest step in ms for the time unit
+ * @param {Function} formatter: Outputs time in a particular format
+ * @return {Array<Object>}
+ * {
+ * {Number} value
+ * {Boolean} enabled
+ * }
+ */
+ _getSteps(startValue, endValue, minStep, formatter) {
+ const { min, max, stepInMs } = this.props;
+ // The timeStep should be big enough so that there won't be
+ // duplications. Ex: minimum step for minute should be 60000ms,
+ // if smaller than that, next step might return the same minute.
+ const timeStep = Math.max(minStep, stepInMs);
+
+ // Make sure the starting point and end point is not off step
+ let time = min.valueOf() + Math.ceil((startValue - min.valueOf()) / timeStep) * timeStep;
+ let maxValue = min.valueOf() + Math.floor((max.valueOf() - min.valueOf()) / stepInMs) * stepInMs;
+ let steps = [];
+
+ // Increment by timeStep until reaching the end of the range.
+ while (time <= endValue) {
+ steps.push({
+ value: formatter(time),
+ // Check if the value is within the min and max. If it's out of range,
+ // also check for the case when minStep is too large, and has stepped out
+ // of range when it should be enabled.
+ enabled: (time >= min.valueOf() && time <= max.valueOf()) ||
+ (time > maxValue && startValue <= maxValue &&
+ endValue >= maxValue && formatter(time) == formatter(maxValue))
+ });
+ time += timeStep;
+ }
+
+ return steps;
+ },
+
+ /**
+ * A generic function for stepping up or down from a value of a range.
+ * It stops at the upper and lower limits.
+ *
+ * @param {Number} current: The current value
+ * @param {Number} offset: The offset relative to current value
+ * @param {Array<Object>} range: List of possible steps
+ * @return {Number} The new value
+ */
+ _step(current, offset, range) {
+ const index = range.findIndex(step => step.value == current);
+ const newIndex = offset > 0 ?
+ Math.min(index + offset, range.length - 1) :
+ Math.max(index + offset, 0);
+ return range[newIndex].value;
+ },
+
+ /**
+ * Step up or down AM/PM
+ *
+ * @param {Number} offset
+ */
+ stepDayPeriodBy(offset) {
+ const current = this.dayPeriod;
+ const dayPeriod = this._step(current, offset, this.state.ranges.dayPeriod);
+
+ if (current != dayPeriod) {
+ this.hour < DAY_PERIOD_IN_HOURS ?
+ this.setState({ hour: this.hour + DAY_PERIOD_IN_HOURS }) :
+ this.setState({ hour: this.hour - DAY_PERIOD_IN_HOURS });
+ }
+ },
+
+ /**
+ * Step up or down hours
+ *
+ * @param {Number} offset
+ */
+ stepHourBy(offset) {
+ const current = this.hour;
+ const hour = this._step(current, offset, this.state.ranges.hours);
+
+ if (current != hour) {
+ this.setState({ hour });
+ }
+ },
+
+ /**
+ * Step up or down minutes
+ *
+ * @param {Number} offset
+ */
+ stepMinuteBy(offset) {
+ const current = this.minute;
+ const minute = this._step(current, offset, this.state.ranges.minutes);
+
+ if (current != minute) {
+ this.setState({ minute });
+ }
+ },
+
+ /**
+ * Step up or down seconds
+ *
+ * @param {Number} offset
+ */
+ stepSecondBy(offset) {
+ const current = this.second;
+ const second = this._step(current, offset, this.state.ranges.seconds);
+
+ if (current != second) {
+ this.setState({ second });
+ }
+ },
+
+ /**
+ * Step up or down milliseconds
+ *
+ * @param {Number} offset
+ */
+ stepMillisecondBy(offset) {
+ const current = this.milliseconds;
+ const millisecond = this._step(current, offset, this.state.ranges.millisecond);
+
+ if (current != millisecond) {
+ this.setState({ millisecond });
+ }
+ },
+
+ /**
+ * Checks if the time state is off step.
+ *
+ * @param {Date} time
+ * @return {Boolean}
+ */
+ _isOffStep(time) {
+ const { min, stepInMs } = this.props;
+
+ return (time.valueOf() - min.valueOf()) % stepInMs != 0;
+ }
+ };
+}
diff --git a/toolkit/content/widgets/timepicker.js b/toolkit/content/widgets/timepicker.js
new file mode 100644
index 000000000..f438e9ec6
--- /dev/null
+++ b/toolkit/content/widgets/timepicker.js
@@ -0,0 +1,277 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+function TimePicker(context) {
+ this.context = context;
+ this._attachEventListeners();
+}
+
+{
+ const debug = 0 ? console.log.bind(console, "[timepicker]") : function() {};
+
+ const DAY_PERIOD_IN_HOURS = 12,
+ SECOND_IN_MS = 1000,
+ MINUTE_IN_MS = 60000,
+ DAY_IN_MS = 86400000;
+
+ TimePicker.prototype = {
+ /**
+ * Initializes the time picker. Set the default states and properties.
+ * @param {Object} props
+ * {
+ * {Number} hour [optional]: Hour in 24 hours format (0~23), default is current hour
+ * {Number} minute [optional]: Minute (0~59), default is current minute
+ * {String} min [optional]: Minimum time, in 24 hours format. ex: "05:45"
+ * {String} max [optional]: Maximum time, in 24 hours format. ex: "23:00"
+ * {Number} step [optional]: Step size in minutes. Default is 60.
+ * {String} format [optional]: "12" for 12 hours, "24" for 24 hours format
+ * {String} locale [optional]: User preferred locale
+ * }
+ */
+ init(props) {
+ this.props = props || {};
+ this._setDefaultState();
+ this._createComponents();
+ this._setComponentStates();
+ },
+
+ /*
+ * Set initial time states. If there's no hour & minute, it will
+ * use the current time. The Time module keeps track of the time states,
+ * and calculates the valid options given the time, min, max, step,
+ * and format (12 or 24).
+ */
+ _setDefaultState() {
+ const { hour, minute, min, max, step, format } = this.props;
+ const now = new Date();
+
+ let timerHour = hour == undefined ? now.getHours() : hour;
+ let timerMinute = minute == undefined ? now.getMinutes() : minute;
+
+ // The spec defines 1 step == 1 second, need to convert to ms for timekeeper
+ let timeKeeper = new TimeKeeper({
+ min: this._parseTimeString(min) || new Date(0),
+ max: this._parseTimeString(max) || new Date(DAY_IN_MS - 1),
+ stepInMs: step ? step * SECOND_IN_MS : MINUTE_IN_MS,
+ format: format || "12"
+ });
+ timeKeeper.setState({ hour: timerHour, minute: timerMinute });
+
+ this.state = { timeKeeper };
+ },
+
+ /**
+ * Convert a time string from DOM attribute to a date object.
+ *
+ * @param {String} timeString: (ex. "10:30", "23:55", "12:34:56.789")
+ * @return {Date/Boolean} Date object or false if date is invalid.
+ */
+ _parseTimeString(timeString) {
+ let time = new Date("1970-01-01T" + timeString + "Z");
+ return time.toString() == "Invalid Date" ? false : time;
+ },
+
+ /**
+ * Initalize the spinner components.
+ */
+ _createComponents() {
+ const { locale, step, format } = this.props;
+ const { timeKeeper } = this.state;
+
+ const wrapSetValueFn = (setTimeFunction) => {
+ return (value) => {
+ setTimeFunction(value);
+ this._setComponentStates();
+ this._dispatchState();
+ };
+ };
+ const numberFormat = new Intl.NumberFormat(locale).format;
+
+ this.components = {
+ hour: new Spinner({
+ setValue: wrapSetValueFn(value => {
+ timeKeeper.setHour(value);
+ this.state.isHourSet = true;
+ }),
+ getDisplayString: hour => {
+ if (format == "24") {
+ return numberFormat(hour);
+ }
+ // Hour 0 in 12 hour format is displayed as 12.
+ const hourIn12 = hour % DAY_PERIOD_IN_HOURS;
+ return hourIn12 == 0 ? numberFormat(12)
+ : numberFormat(hourIn12);
+ }
+ }, this.context),
+ minute: new Spinner({
+ setValue: wrapSetValueFn(value => {
+ timeKeeper.setMinute(value);
+ this.state.isMinuteSet = true;
+ }),
+ getDisplayString: minute => numberFormat(minute)
+ }, this.context)
+ };
+
+ this._insertLayoutElement({
+ tag: "div",
+ textContent: ":",
+ className: "colon",
+ insertBefore: this.components.minute.elements.container
+ });
+
+ // The AM/PM spinner is only available in 12hr mode
+ // TODO: Replace AM & PM string with localized string
+ if (format == "12") {
+ this.components.dayPeriod = new Spinner({
+ setValue: wrapSetValueFn(value => {
+ timeKeeper.setDayPeriod(value);
+ this.state.isDayPeriodSet = true;
+ }),
+ getDisplayString: dayPeriod => dayPeriod == 0 ? "AM" : "PM",
+ hideButtons: true
+ }, this.context);
+
+ this._insertLayoutElement({
+ tag: "div",
+ className: "spacer",
+ insertBefore: this.components.dayPeriod.elements.container
+ });
+ }
+ },
+
+ /**
+ * Insert element for layout purposes.
+ *
+ * @param {Object}
+ * {
+ * {String} tag: The tag to create
+ * {DOMElement} insertBefore: The DOM node to insert before
+ * {String} className [optional]: Class name
+ * {String} textContent [optional]: Text content
+ * }
+ */
+ _insertLayoutElement({ tag, insertBefore, className, textContent }) {
+ let el = document.createElement(tag);
+ el.textContent = textContent;
+ el.className = className;
+ this.context.insertBefore(el, insertBefore);
+ },
+
+ /**
+ * Set component states.
+ */
+ _setComponentStates() {
+ const { timeKeeper, isHourSet, isMinuteSet, isDayPeriodSet } = this.state;
+ const isInvalid = timeKeeper.state.isInvalid;
+ // Value is set to min if it's first opened and time state is invalid
+ const setToMinValue = !isHourSet && !isMinuteSet && !isDayPeriodSet && isInvalid;
+
+ this.components.hour.setState({
+ value: setToMinValue ? timeKeeper.ranges.hours[0].value : timeKeeper.hour,
+ items: timeKeeper.ranges.hours,
+ isInfiniteScroll: true,
+ isValueSet: isHourSet,
+ isInvalid
+ });
+
+ this.components.minute.setState({
+ value: setToMinValue ? timeKeeper.ranges.minutes[0].value : timeKeeper.minute,
+ items: timeKeeper.ranges.minutes,
+ isInfiniteScroll: true,
+ isValueSet: isMinuteSet,
+ isInvalid
+ });
+
+ // The AM/PM spinner is only available in 12hr mode
+ if (this.props.format == "12") {
+ this.components.dayPeriod.setState({
+ value: setToMinValue ? timeKeeper.ranges.dayPeriod[0].value : timeKeeper.dayPeriod,
+ items: timeKeeper.ranges.dayPeriod,
+ isInfiniteScroll: false,
+ isValueSet: isDayPeriodSet,
+ isInvalid
+ });
+ }
+ },
+
+ /**
+ * Dispatch CustomEvent to pass the state of picker to the panel.
+ */
+ _dispatchState() {
+ const { hour, minute } = this.state.timeKeeper;
+ const { isHourSet, isMinuteSet, isDayPeriodSet } = this.state;
+ // The panel is listening to window for postMessage event, so we
+ // do postMessage to itself to send data to input boxes.
+ window.postMessage({
+ name: "TimePickerPopupChanged",
+ detail: {
+ hour,
+ minute,
+ isHourSet,
+ isMinuteSet,
+ isDayPeriodSet
+ }
+ }, "*");
+ },
+ _attachEventListeners() {
+ window.addEventListener("message", this);
+ },
+
+ /**
+ * Handle events.
+ *
+ * @param {Event} event
+ */
+ handleEvent(event) {
+ switch (event.type) {
+ case "message": {
+ this.handleMessage(event);
+ break;
+ }
+ }
+ },
+
+ /**
+ * Handle postMessage events.
+ *
+ * @param {Event} event
+ */
+ handleMessage(event) {
+ switch (event.data.name) {
+ case "TimePickerSetValue": {
+ this.set(event.data.detail);
+ break;
+ }
+ case "TimePickerInit": {
+ this.init(event.data.detail);
+ break;
+ }
+ }
+ },
+
+ /**
+ * Set the time state and update the components with the new state.
+ *
+ * @param {Object} timeState
+ * {
+ * {Number} hour [optional]
+ * {Number} minute [optional]
+ * {Number} second [optional]
+ * {Number} millisecond [optional]
+ * }
+ */
+ set(timeState) {
+ if (timeState.hour != undefined) {
+ this.state.isHourSet = true;
+ }
+ if (timeState.minute != undefined) {
+ this.state.isMinuteSet = true;
+ }
+ this.state.timeKeeper.setState(timeState);
+ this._setComponentStates();
+ }
+ };
+}
diff --git a/toolkit/content/widgets/toolbar.xml b/toolkit/content/widgets/toolbar.xml
new file mode 100644
index 000000000..548504e24
--- /dev/null
+++ b/toolkit/content/widgets/toolbar.xml
@@ -0,0 +1,590 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<bindings id="toolbarBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="toolbar-base">
+ <resources>
+ <stylesheet src="chrome://global/skin/toolbar.css"/>
+ </resources>
+ </binding>
+
+ <binding id="toolbox" extends="chrome://global/content/bindings/toolbar.xml#toolbar-base">
+ <implementation>
+ <field name="palette">
+ null
+ </field>
+
+ <field name="toolbarset">
+ null
+ </field>
+
+ <field name="customToolbarCount">
+ 0
+ </field>
+
+ <field name="externalToolbars">
+ []
+ </field>
+
+ <!-- Set by customizeToolbar.js -->
+ <property name="customizing">
+ <getter><![CDATA[
+ return this.getAttribute("customizing") == "true";
+ ]]></getter>
+ <setter><![CDATA[
+ if (val)
+ this.setAttribute("customizing", "true");
+ else
+ this.removeAttribute("customizing");
+ return val;
+ ]]></setter>
+ </property>
+
+ <constructor>
+ <![CDATA[
+ // Look to see if there is a toolbarset.
+ this.toolbarset = this.firstChild;
+ while (this.toolbarset && this.toolbarset.localName != "toolbarset")
+ this.toolbarset = toolbarset.nextSibling;
+
+ if (this.toolbarset) {
+ // Create each toolbar described by the toolbarset.
+ var index = 0;
+ while (toolbarset.hasAttribute("toolbar"+(++index))) {
+ var toolbarInfo = toolbarset.getAttribute("toolbar"+index);
+ var infoSplit = toolbarInfo.split(":");
+ this.appendCustomToolbar(infoSplit[0], infoSplit[1]);
+ }
+ }
+ ]]>
+ </constructor>
+
+ <method name="appendCustomToolbar">
+ <parameter name="aName"/>
+ <parameter name="aCurrentSet"/>
+ <body>
+ <![CDATA[
+ if (!this.toolbarset)
+ return null;
+ var toolbar = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+ "toolbar");
+ toolbar.id = "__customToolbar_" + aName.replace(" ", "_");
+ toolbar.setAttribute("customizable", "true");
+ toolbar.setAttribute("customindex", ++this.customToolbarCount);
+ toolbar.setAttribute("toolbarname", aName);
+ toolbar.setAttribute("currentset", aCurrentSet);
+ toolbar.setAttribute("mode", this.getAttribute("mode"));
+ toolbar.setAttribute("iconsize", this.getAttribute("iconsize"));
+ toolbar.setAttribute("context", this.toolbarset.getAttribute("context"));
+ toolbar.setAttribute("class", "chromeclass-toolbar");
+
+ this.insertBefore(toolbar, this.toolbarset);
+ return toolbar;
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="toolbar" role="xul:toolbar"
+ extends="chrome://global/content/bindings/toolbar.xml#toolbar-base">
+ <implementation>
+ <property name="toolbarName"
+ onget="return this.getAttribute('toolbarname');"
+ onset="this.setAttribute('toolbarname', val); return val;"/>
+
+ <field name="_toolbox">null</field>
+ <property name="toolbox" readonly="true">
+ <getter><![CDATA[
+ if (this._toolbox)
+ return this._toolbox;
+
+ let toolboxId = this.getAttribute("toolboxid");
+ if (toolboxId) {
+ let toolbox = document.getElementById(toolboxId);
+ if (!toolbox) {
+ let tbName = this.toolbarName;
+ if (tbName)
+ tbName = " (" + tbName + ")";
+ else
+ tbName = "";
+ throw new Error(`toolbar ID ${this.id}${tbName}: toolboxid attribute '${toolboxId}' points to a toolbox that doesn't exist`);
+ }
+
+ if (toolbox.externalToolbars.indexOf(this) == -1)
+ toolbox.externalToolbars.push(this);
+
+ return this._toolbox = toolbox;
+ }
+
+ return this._toolbox = (this.parentNode &&
+ this.parentNode.localName == "toolbox") ?
+ this.parentNode : null;
+ ]]></getter>
+ </property>
+
+ <constructor>
+ <![CDATA[
+ if (document.readyState == "complete") {
+ this._init();
+ } else {
+ // Need to wait until XUL overlays are loaded. See bug 554279.
+ let self = this;
+ document.addEventListener("readystatechange", function (event) {
+ if (document.readyState != "complete")
+ return;
+ document.removeEventListener("readystatechange", arguments.callee, false);
+ self._init();
+ }, false);
+ }
+ ]]>
+ </constructor>
+
+ <method name="_init">
+ <body>
+ <![CDATA[
+ // Searching for the toolbox palette in the toolbar binding because
+ // toolbars are constructed first.
+ var toolbox = this.toolbox;
+ if (!toolbox)
+ return;
+
+ if (!toolbox.palette) {
+ // Look to see if there is a toolbarpalette.
+ var node = toolbox.firstChild;
+ while (node) {
+ if (node.localName == "toolbarpalette")
+ break;
+ node = node.nextSibling;
+ }
+
+ if (!node)
+ return;
+
+ // Hold on to the palette but remove it from the document.
+ toolbox.palette = node;
+ toolbox.removeChild(node);
+ }
+
+ // Build up our contents from the palette.
+ var currentSet = this.getAttribute("currentset");
+ if (!currentSet)
+ currentSet = this.getAttribute("defaultset");
+ if (currentSet)
+ this.currentSet = currentSet;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_idFromNode">
+ <parameter name="aNode"/>
+ <body>
+ <![CDATA[
+ if (aNode.getAttribute("skipintoolbarset") == "true")
+ return "";
+
+ switch (aNode.localName) {
+ case "toolbarseparator":
+ return "separator";
+ case "toolbarspring":
+ return "spring";
+ case "toolbarspacer":
+ return "spacer";
+ default:
+ return aNode.id;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <property name="currentSet">
+ <getter>
+ <![CDATA[
+ var node = this.firstChild;
+ var currentSet = [];
+ while (node) {
+ var id = this._idFromNode(node);
+ if (id) {
+ currentSet.push(id);
+ }
+ node = node.nextSibling;
+ }
+
+ return currentSet.join(",") || "__empty";
+ ]]>
+ </getter>
+
+ <setter>
+ <![CDATA[
+ if (val == this.currentSet)
+ return val;
+
+ var ids = (val == "__empty") ? [] : val.split(",");
+
+ var nodeidx = 0;
+ var paletteItems = { }, added = { };
+
+ var palette = this.toolbox ? this.toolbox.palette : null;
+
+ // build a cache of items in the toolbarpalette
+ var paletteChildren = palette ? palette.childNodes : [];
+ for (let c = 0; c < paletteChildren.length; c++) {
+ let curNode = paletteChildren[c];
+ paletteItems[curNode.id] = curNode;
+ }
+
+ var children = this.childNodes;
+
+ // iterate over the ids to use on the toolbar
+ for (let i = 0; i < ids.length; i++) {
+ let id = ids[i];
+ // iterate over the existing nodes on the toolbar. nodeidx is the
+ // spot where we want to insert items.
+ let found = false;
+ for (let c = nodeidx; c < children.length; c++) {
+ let curNode = children[c];
+ if (this._idFromNode(curNode) == id) {
+ // the node already exists. If c equals nodeidx, we haven't
+ // iterated yet, so the item is already in the right position.
+ // Otherwise, insert it here.
+ if (c != nodeidx) {
+ this.insertBefore(curNode, children[nodeidx]);
+ }
+
+ added[curNode.id] = true;
+ nodeidx++;
+ found = true;
+ break;
+ }
+ }
+ if (found) {
+ // move on to the next id
+ continue;
+ }
+
+ // the node isn't already on the toolbar, so add a new one.
+ var nodeToAdd = paletteItems[id] || this._getToolbarItem(id);
+ if (nodeToAdd && !(nodeToAdd.id in added)) {
+ added[nodeToAdd.id] = true;
+ this.insertBefore(nodeToAdd, children[nodeidx] || null);
+ nodeToAdd.setAttribute("removable", "true");
+ nodeidx++;
+ }
+ }
+
+ // remove any leftover removable nodes
+ for (let i = children.length - 1; i >= nodeidx; i--) {
+ let curNode = children[i];
+
+ let curNodeId = this._idFromNode(curNode);
+ // skip over fixed items
+ if (curNodeId && curNode.getAttribute("removable") == "true") {
+ if (palette)
+ palette.appendChild(curNode);
+ else
+ this.removeChild(curNode);
+ }
+ }
+
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <field name="_newElementCount">0</field>
+ <method name="_getToolbarItem">
+ <parameter name="aId"/>
+ <body>
+ <![CDATA[
+ const XUL_NS = "http://www.mozilla.org/keymaster/" +
+ "gatekeeper/there.is.only.xul";
+
+ var newItem = null;
+ switch (aId) {
+ // Handle special cases
+ case "separator":
+ case "spring":
+ case "spacer":
+ newItem = document.createElementNS(XUL_NS, "toolbar" + aId);
+ // Due to timers resolution Date.now() can be the same for
+ // elements created in small timeframes. So ids are
+ // differentiated through a unique count suffix.
+ newItem.id = aId + Date.now() + (++this._newElementCount);
+ if (aId == "spring")
+ newItem.flex = 1;
+ break;
+ default:
+ var toolbox = this.toolbox;
+ if (!toolbox)
+ break;
+
+ // look for an item with the same id, as the item may be
+ // in a different toolbar.
+ var item = document.getElementById(aId);
+ if (item && item.parentNode &&
+ item.parentNode.localName == "toolbar" &&
+ item.parentNode.toolbox == toolbox) {
+ newItem = item;
+ break;
+ }
+
+ if (toolbox.palette) {
+ // Attempt to locate an item with a matching ID within
+ // the palette.
+ let paletteItem = this.toolbox.palette.firstChild;
+ while (paletteItem) {
+ if (paletteItem.id == aId) {
+ newItem = paletteItem;
+ break;
+ }
+ paletteItem = paletteItem.nextSibling;
+ }
+ }
+ break;
+ }
+
+ return newItem;
+ ]]>
+ </body>
+ </method>
+
+ <method name="insertItem">
+ <parameter name="aId"/>
+ <parameter name="aBeforeElt"/>
+ <parameter name="aWrapper"/>
+ <body>
+ <![CDATA[
+ var newItem = this._getToolbarItem(aId);
+ if (!newItem)
+ return null;
+
+ var insertItem = newItem;
+ // make sure added items are removable
+ newItem.setAttribute("removable", "true");
+
+ // Wrap the item in another node if so inclined.
+ if (aWrapper) {
+ aWrapper.appendChild(newItem);
+ insertItem = aWrapper;
+ }
+
+ // Insert the palette item into the toolbar.
+ if (aBeforeElt)
+ this.insertBefore(insertItem, aBeforeElt);
+ else
+ this.appendChild(insertItem);
+
+ return newItem;
+ ]]>
+ </body>
+ </method>
+
+ <method name="hasCustomInteractiveItems">
+ <parameter name="aCurrentSet"/>
+ <body><![CDATA[
+ if (aCurrentSet == "__empty")
+ return false;
+
+ var defaultOrNoninteractive = (this.getAttribute("defaultset") || "")
+ .split(",")
+ .concat(["separator", "spacer", "spring"]);
+ return aCurrentSet.split(",").some(function (item) {
+ return defaultOrNoninteractive.indexOf(item) == -1;
+ });
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="toolbar-menubar-autohide"
+ extends="chrome://global/content/bindings/toolbar.xml#toolbar">
+ <implementation>
+ <constructor>
+ this._setInactive();
+ </constructor>
+ <destructor>
+ this._setActive();
+ </destructor>
+
+ <field name="_inactiveTimeout">null</field>
+
+ <field name="_contextMenuListener"><![CDATA[({
+ toolbar: this,
+ contextMenu: null,
+
+ get active () {
+ return !!this.contextMenu;
+ },
+
+ init: function (event) {
+ var node = event.target;
+ while (node != this.toolbar) {
+ if (node.localName == "menupopup")
+ return;
+ node = node.parentNode;
+ }
+
+ var contextMenuId = this.toolbar.getAttribute("context");
+ if (!contextMenuId)
+ return;
+
+ this.contextMenu = document.getElementById(contextMenuId);
+ if (!this.contextMenu)
+ return;
+
+ this.contextMenu.addEventListener("popupshown", this, false);
+ this.contextMenu.addEventListener("popuphiding", this, false);
+ this.toolbar.addEventListener("mousemove", this, false);
+ },
+ handleEvent: function (event) {
+ switch (event.type) {
+ case "popupshown":
+ this.toolbar.removeEventListener("mousemove", this, false);
+ break;
+ case "popuphiding":
+ case "mousemove":
+ this.toolbar._setInactiveAsync();
+ this.toolbar.removeEventListener("mousemove", this, false);
+ this.contextMenu.removeEventListener("popuphiding", this, false);
+ this.contextMenu.removeEventListener("popupshown", this, false);
+ this.contextMenu = null;
+ break;
+ }
+ }
+ })]]></field>
+
+ <method name="_setInactive">
+ <body><![CDATA[
+ this.setAttribute("inactive", "true");
+ ]]></body>
+ </method>
+
+ <method name="_setInactiveAsync">
+ <body><![CDATA[
+ this._inactiveTimeout = setTimeout(function (self) {
+ if (self.getAttribute("autohide") == "true") {
+ self._inactiveTimeout = null;
+ self._setInactive();
+ }
+ }, 0, this);
+ ]]></body>
+ </method>
+
+ <method name="_setActive">
+ <body><![CDATA[
+ if (this._inactiveTimeout) {
+ clearTimeout(this._inactiveTimeout);
+ this._inactiveTimeout = null;
+ }
+ this.removeAttribute("inactive");
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="DOMMenuBarActive" action="this._setActive();"/>
+ <handler event="popupshowing" action="this._setActive();"/>
+ <handler event="mousedown" button="2" action="this._contextMenuListener.init(event);"/>
+ <handler event="DOMMenuBarInactive"><![CDATA[
+ if (!this._contextMenuListener.active)
+ this._setInactiveAsync();
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="toolbar-drag"
+ extends="chrome://global/content/bindings/toolbar.xml#toolbar">
+ <implementation>
+ <field name="_dragBindingAlive">true</field>
+ <constructor><![CDATA[
+ if (!this._draggableStarted) {
+ this._draggableStarted = true;
+ try {
+ let tmp = {};
+ Components.utils.import("resource://gre/modules/WindowDraggingUtils.jsm", tmp);
+ let draggableThis = new tmp.WindowDraggingElement(this);
+ draggableThis.mouseDownCheck = function(e) {
+ // Don't move while customizing.
+ return this._dragBindingAlive &&
+ this.getAttribute("customizing") != "true";
+ };
+ } catch (e) {}
+ }
+ ]]></constructor>
+ </implementation>
+ </binding>
+
+ <binding id="menubar" role="xul:menubar"
+ extends="chrome://global/content/bindings/toolbar.xml#toolbar-base" display="xul:menubar">
+ <implementation>
+ <field name="_active">false</field>
+ <field name="_statusbar">null</field>
+ <field name="_originalStatusText">null</field>
+ <property name="statusbar" onget="return this.getAttribute('statusbar');"
+ onset="this.setAttribute('statusbar', val); return val;"/>
+ <method name="_updateStatusText">
+ <parameter name="itemText"/>
+ <body>
+ <![CDATA[
+ if (!this._active)
+ return;
+ var newText = itemText ? itemText : this._originalStatusText;
+ if (newText != this._statusbar.label)
+ this._statusbar.label = newText;
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ <handlers>
+ <handler event="DOMMenuBarActive">
+ <![CDATA[
+ if (!this.statusbar) return;
+ this._statusbar = document.getElementById(this.statusbar);
+ if (!this._statusbar)
+ return;
+ this._active = true;
+ this._originalStatusText = this._statusbar.label;
+ ]]>
+ </handler>
+ <handler event="DOMMenuBarInactive">
+ <![CDATA[
+ if (!this._active)
+ return;
+ this._active = false;
+ this._statusbar.label = this._originalStatusText;
+ ]]>
+ </handler>
+ <handler event="DOMMenuItemActive">this._updateStatusText(event.target.statusText);</handler>
+ <handler event="DOMMenuItemInactive">this._updateStatusText("");</handler>
+ </handlers>
+ </binding>
+
+ <binding id="toolbardecoration" role="xul:toolbarseparator" extends="chrome://global/content/bindings/toolbar.xml#toolbar-base">
+ </binding>
+
+ <binding id="toolbarpaletteitem" extends="chrome://global/content/bindings/toolbar.xml#toolbar-base" display="xul:button">
+ <content>
+ <xul:hbox class="toolbarpaletteitem-box" flex="1" xbl:inherits="type,place">
+ <children/>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="toolbarpaletteitem-palette" extends="chrome://global/content/bindings/toolbar.xml#toolbarpaletteitem">
+ <content>
+ <xul:hbox class="toolbarpaletteitem-box" xbl:inherits="type,place">
+ <children/>
+ </xul:hbox>
+ <xul:label xbl:inherits="value=title"/>
+ </content>
+ </binding>
+
+</bindings>
+
diff --git a/toolkit/content/widgets/toolbarbutton.xml b/toolkit/content/widgets/toolbarbutton.xml
new file mode 100644
index 000000000..5de3f040d
--- /dev/null
+++ b/toolkit/content/widgets/toolbarbutton.xml
@@ -0,0 +1,115 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<bindings id="toolbarbuttonBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="toolbarbutton" display="xul:button" role="xul:toolbarbutton"
+ extends="chrome://global/content/bindings/button.xml#button-base">
+ <resources>
+ <stylesheet src="chrome://global/skin/toolbarbutton.css"/>
+ </resources>
+
+ <content>
+ <children includes="observes|template|menupopup|panel|tooltip"/>
+ <xul:image class="toolbarbutton-icon" xbl:inherits="validate,src=image,label,consumeanchor"/>
+ <xul:label class="toolbarbutton-text" crop="right" flex="1"
+ xbl:inherits="value=label,accesskey,crop,wrap"/>
+ <xul:label class="toolbarbutton-multiline-text" flex="1"
+ xbl:inherits="xbl:text=label,accesskey,wrap"/>
+ </content>
+ </binding>
+
+ <binding id="menu" display="xul:menu"
+ extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton">
+ <content>
+ <children includes="observes|template|menupopup|panel|tooltip"/>
+ <xul:image class="toolbarbutton-icon" xbl:inherits="validate,src=image,label,type,consumeanchor"/>
+ <xul:label class="toolbarbutton-text" crop="right" flex="1"
+ xbl:inherits="value=label,accesskey,crop,dragover-top,wrap"/>
+ <xul:label class="toolbarbutton-multiline-text" flex="1"
+ xbl:inherits="xbl:text=label,accesskey,wrap"/>
+ <xul:dropmarker anonid="dropmarker" type="menu"
+ class="toolbarbutton-menu-dropmarker" xbl:inherits="disabled,label"/>
+ </content>
+ </binding>
+
+ <binding id="menu-vertical" display="xul:menu"
+ extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton">
+ <content>
+ <children includes="observes|template|menupopup|panel|tooltip"/>
+ <xul:hbox flex="1" align="center">
+ <xul:vbox flex="1" align="center">
+ <xul:image class="toolbarbutton-icon" xbl:inherits="validate,src=image,label,consumeanchor"/>
+ <xul:label class="toolbarbutton-text" crop="right" flex="1"
+ xbl:inherits="value=label,accesskey,crop,dragover-top,wrap"/>
+ <xul:label class="toolbarbutton-multiline-text" flex="1"
+ xbl:inherits="xbl:text=label,accesskey,wrap"/>
+ </xul:vbox>
+ <xul:dropmarker anonid="dropmarker" type="menu"
+ class="toolbarbutton-menu-dropmarker" xbl:inherits="disabled,label"/>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="menu-button" display="xul:menu"
+ extends="chrome://global/content/bindings/button.xml#menu-button-base">
+ <resources>
+ <stylesheet src="chrome://global/skin/toolbarbutton.css"/>
+ </resources>
+
+ <content>
+ <children includes="observes|template|menupopup|panel|tooltip"/>
+ <xul:toolbarbutton class="box-inherit toolbarbutton-menubutton-button"
+ anonid="button" flex="1" allowevents="true"
+ xbl:inherits="disabled,crop,image,label,accesskey,command,wrap,badge,
+ align,dir,pack,orient,tooltiptext=buttontooltiptext"/>
+ <xul:dropmarker type="menu-button" class="toolbarbutton-menubutton-dropmarker"
+ anonid="dropmarker" xbl:inherits="align,dir,pack,orient,disabled,label,open,consumeanchor"/>
+ </content>
+ </binding>
+
+ <binding id="toolbarbutton-image"
+ extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton">
+ <content>
+ <xul:image class="toolbarbutton-icon" xbl:inherits="src=image"/>
+ </content>
+ </binding>
+
+ <binding id="toolbarbutton-badged"
+ extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton">
+ <content>
+ <children includes="observes|template|menupopup|panel|tooltip"/>
+ <xul:stack class="toolbarbutton-badge-stack">
+ <xul:image class="toolbarbutton-icon" xbl:inherits="validate,src=image,label,consumeanchor"/>
+ <xul:label class="toolbarbutton-badge" xbl:inherits="value=badge" top="0" end="0" crop="none"/>
+ </xul:stack>
+ <xul:label class="toolbarbutton-text" crop="right" flex="1"
+ xbl:inherits="value=label,accesskey,crop,wrap"/>
+ <xul:label class="toolbarbutton-multiline-text" flex="1"
+ xbl:inherits="xbl:text=label,accesskey,wrap"/>
+ </content>
+ </binding>
+
+ <binding id="toolbarbutton-badged-menu" display="xul:menu"
+ extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton">
+ <content>
+ <children includes="observes|template|menupopup|panel|tooltip"/>
+ <xul:stack class="toolbarbutton-badge-stack">
+ <xul:image class="toolbarbutton-icon" xbl:inherits="validate,src=image,label,consumeanchor"/>
+ <xul:label class="toolbarbutton-badge" xbl:inherits="value=badge" top="0" end="0" crop="none"/>
+ </xul:stack>
+ <xul:label class="toolbarbutton-text" crop="right" flex="1"
+ xbl:inherits="value=label,accesskey,crop,dragover-top,wrap"/>
+ <xul:label class="toolbarbutton-multiline-text" flex="1"
+ xbl:inherits="xbl:text=label,accesskey,wrap"/>
+ <xul:dropmarker anonid="dropmarker" type="menu"
+ class="toolbarbutton-menu-dropmarker" xbl:inherits="disabled,label"/>
+ </content>
+ </binding>
+</bindings>
diff --git a/toolkit/content/widgets/tree.xml b/toolkit/content/widgets/tree.xml
new file mode 100644
index 000000000..aa1717257
--- /dev/null
+++ b/toolkit/content/widgets/tree.xml
@@ -0,0 +1,1561 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<!DOCTYPE bindings [
+<!ENTITY % treeDTD SYSTEM "chrome://global/locale/tree.dtd">
+%treeDTD;
+]>
+
+<bindings id="treeBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="tree-base" extends="chrome://global/content/bindings/general.xml#basecontrol">
+ <resources>
+ <stylesheet src="chrome://global/skin/tree.css"/>
+ </resources>
+ <implementation>
+ <method name="_isAccelPressed">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ return aEvent.getModifierState("Accel");
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="tree" extends="chrome://global/content/bindings/tree.xml#tree-base" role="xul:tree">
+ <content hidevscroll="true" hidehscroll="true" clickthrough="never">
+ <children includes="treecols"/>
+ <xul:stack class="tree-stack" flex="1">
+ <xul:treerows class="tree-rows" flex="1" xbl:inherits="hidevscroll">
+ <children/>
+ </xul:treerows>
+ <xul:textbox anonid="input" class="tree-input" left="0" top="0" hidden="true"/>
+ </xul:stack>
+ <xul:hbox xbl:inherits="collapsed=hidehscroll">
+ <xul:scrollbar orient="horizontal" flex="1" increment="16" style="position:relative; z-index:2147483647;"/>
+ <xul:scrollcorner xbl:inherits="collapsed=hidevscroll"/>
+ </xul:hbox>
+ </content>
+
+ <implementation implements="nsIDOMXULTreeElement, nsIDOMXULMultiSelectControlElement">
+
+ <!-- ///////////////// nsIDOMXULTreeElement ///////////////// -->
+
+ <property name="columns"
+ onget="return this.treeBoxObject.columns;"/>
+
+ <property name="view"
+ onget="return this.treeBoxObject.view ?
+ this.treeBoxObject.view.QueryInterface(Components.interfaces.nsITreeView) :
+ null;"
+ onset="return this.treeBoxObject.view = val;"/>
+
+ <property name="body"
+ onget="return this.treeBoxObject.treeBody;"/>
+
+ <property name="editable"
+ onget="return this.getAttribute('editable') == 'true';"
+ onset="if (val) this.setAttribute('editable', 'true');
+ else this.removeAttribute('editable'); return val;"/>
+
+ <!-- ///////////////// nsIDOMXULSelectControlElement ///////////////// -->
+
+ <!-- ///////////////// nsIDOMXULMultiSelectControlElement ///////////////// -->
+
+ <property name="selType"
+ onget="return this.getAttribute('seltype')"
+ onset="this.setAttribute('seltype', val); return val;"/>
+
+ <property name="currentIndex"
+ onget="return this.view ? this.view.selection.currentIndex: - 1;"
+ onset="if (this.view) return this.view.selection.currentIndex = val; return val;"/>
+
+ <property name="treeBoxObject"
+ onget="return this.boxObject;"
+ readonly="true"/>
+# contentView is obsolete (see bug 202391)
+ <property name="contentView"
+ onget="return this.view; /*.QueryInterface(Components.interfaces.nsITreeContentView)*/"
+ readonly="true"/>
+# builderView is obsolete (see bug 202393)
+ <property name="builderView"
+ onget="return this.view; /*.QueryInterface(Components.interfaces.nsIXULTreeBuilder)*/"
+ readonly="true"/>
+ <field name="pageUpOrDownMovesSelection">
+ !/Mac/.test(navigator.platform)
+ </field>
+ <property name="keepCurrentInView"
+ onget="return (this.getAttribute('keepcurrentinview') == 'true');"
+ onset="if (val) this.setAttribute('keepcurrentinview', 'true');
+ else this.removeAttribute('keepcurrentinview'); return val;"/>
+
+ <property name="enableColumnDrag"
+ onget="return this.hasAttribute('enableColumnDrag');"
+ onset="if (val) this.setAttribute('enableColumnDrag', 'true');
+ else this.removeAttribute('enableColumnDrag'); return val;"/>
+
+ <field name="_inputField">null</field>
+
+ <property name="inputField" readonly="true">
+ <getter><![CDATA[
+ if (!this._inputField)
+ this._inputField = document.getAnonymousElementByAttribute(this, "anonid", "input");
+ return this._inputField;
+ ]]></getter>
+ </property>
+
+ <property name="disableKeyNavigation"
+ onget="return this.hasAttribute('disableKeyNavigation');"
+ onset="if (val) this.setAttribute('disableKeyNavigation', 'true');
+ else this.removeAttribute('disableKeyNavigation'); return val;"/>
+
+ <field name="_editingRow">-1</field>
+ <field name="_editingColumn">null</field>
+
+ <property name="editingRow" readonly="true"
+ onget="return this._editingRow;"/>
+ <property name="editingColumn" readonly="true"
+ onget="return this._editingColumn;"/>
+
+ <property name="_selectDelay"
+ onset="this.setAttribute('_selectDelay', val);"
+ onget="return this.getAttribute('_selectDelay') || 50;"/>
+ <field name="_columnsDirty">true</field>
+ <field name="_lastKeyTime">0</field>
+ <field name="_incrementalString">""</field>
+
+ <field name="_touchY">-1</field>
+
+ <method name="_ensureColumnOrder">
+ <body><![CDATA[
+ if (!this._columnsDirty)
+ return;
+
+ if (this.columns) {
+ // update the ordinal position of each column to assure that it is
+ // an odd number and 2 positions above its next sibling
+ var cols = [];
+ var i;
+ for (var col = this.columns.getFirstColumn(); col; col = col.getNext())
+ cols.push(col.element);
+ for (i = 0; i < cols.length; ++i)
+ cols[i].setAttribute("ordinal", (i*2)+1);
+
+ // update the ordinal positions of splitters to even numbers, so that
+ // they are in between columns
+ var splitters = this.getElementsByTagName("splitter");
+ for (i = 0; i < splitters.length; ++i)
+ splitters[i].setAttribute("ordinal", (i+1)*2);
+ }
+ this._columnsDirty = false;
+ ]]></body>
+ </method>
+
+ <method name="_reorderColumn">
+ <parameter name="aColMove"/>
+ <parameter name="aColBefore"/>
+ <parameter name="aBefore"/>
+ <body><![CDATA[
+ this._ensureColumnOrder();
+
+ var i;
+ var cols = [];
+ var col = this.columns.getColumnFor(aColBefore);
+ if (parseInt(aColBefore.ordinal) < parseInt(aColMove.ordinal)) {
+ if (aBefore)
+ cols.push(aColBefore);
+ for (col = col.getNext(); col.element != aColMove;
+ col = col.getNext())
+ cols.push(col.element);
+
+ aColMove.ordinal = cols[0].ordinal;
+ for (i = 0; i < cols.length; ++i)
+ cols[i].ordinal = parseInt(cols[i].ordinal) + 2;
+ } else if (aColBefore.ordinal != aColMove.ordinal) {
+ if (!aBefore)
+ cols.push(aColBefore);
+ for (col = col.getPrevious(); col.element != aColMove;
+ col = col.getPrevious())
+ cols.push(col.element);
+
+ aColMove.ordinal = cols[0].ordinal;
+ for (i = 0; i < cols.length; ++i)
+ cols[i].ordinal = parseInt(cols[i].ordinal) - 2;
+ }
+ ]]></body>
+ </method>
+
+ <method name="_getColumnAtX">
+ <parameter name="aX"/>
+ <parameter name="aThresh"/>
+ <parameter name="aPos"/>
+ <body><![CDATA[
+ var isRTL = document.defaultView.getComputedStyle(this, "")
+ .direction == "rtl";
+
+ if (aPos)
+ aPos.value = isRTL ? "after" : "before";
+
+ var columns = [];
+ var col = this.columns.getFirstColumn();
+ while (col) {
+ columns.push(col);
+ col = col.getNext();
+ }
+ if (isRTL)
+ columns.reverse();
+ var currentX = this.boxObject.x;
+ var adjustedX = aX + this.treeBoxObject.horizontalPosition;
+ for (var i = 0; i < columns.length; ++i) {
+ col = columns[i];
+ var cw = col.element.boxObject.width;
+ if (cw > 0) {
+ currentX += cw;
+ if (currentX - (cw * aThresh) > adjustedX)
+ return col.element;
+ }
+ }
+
+ if (aPos)
+ aPos.value = isRTL ? "before" : "after";
+ return columns.pop().element;
+ ]]></body>
+ </method>
+
+ <method name="changeOpenState">
+ <parameter name="row"/>
+ <!-- Optional parameter openState == true or false to set.
+ No openState param == toggle -->
+ <parameter name="openState"/>
+ <body><![CDATA[
+ if (row < 0 || !this.view.isContainer(row)) {
+ return false;
+ }
+
+ if (this.view.isContainerOpen(row) != openState) {
+ this.view.toggleOpenState(row);
+ if (row == this.currentIndex) {
+ // Only fire event when current row is expanded or collapsed
+ // because that's all the assistive technology really cares about.
+ var event = document.createEvent('Events');
+ event.initEvent('OpenStateChange', true, true);
+ this.dispatchEvent(event);
+ }
+ return true;
+ }
+ return false;
+ ]]></body>
+ </method>
+
+ <property name="_cellSelType">
+ <getter>
+ <![CDATA[
+ var seltype = this.selType;
+ if (seltype == "cell" || seltype == "text")
+ return seltype;
+ return null;
+ ]]>
+ </getter>
+ </property>
+
+ <method name="_getNextColumn">
+ <parameter name="row"/>
+ <parameter name="left"/>
+ <body><![CDATA[
+ var col = this.view.selection.currentColumn;
+ if (col) {
+ col = left ? col.getPrevious() : col.getNext();
+ }
+ else {
+ col = this.columns.getKeyColumn();
+ }
+ while (col && (col.width == 0 || !col.selectable ||
+ !this.view.isSelectable(row, col)))
+ col = left ? col.getPrevious() : col.getNext();
+ return col;
+ ]]></body>
+ </method>
+
+ <method name="_keyNavigate">
+ <parameter name="event"/>
+ <body><![CDATA[
+ var key = String.fromCharCode(event.charCode).toLowerCase();
+ if (event.timeStamp - this._lastKeyTime > 1000)
+ this._incrementalString = key;
+ else
+ this._incrementalString += key;
+ this._lastKeyTime = event.timeStamp;
+
+ var length = this._incrementalString.length;
+ var incrementalString = this._incrementalString;
+ var charIndex = 1;
+ while (charIndex < length && incrementalString[charIndex] == incrementalString[charIndex - 1])
+ charIndex++;
+ // If all letters in incremental string are same, just try to match the first one
+ if (charIndex == length) {
+ length = 1;
+ incrementalString = incrementalString.substring(0, length);
+ }
+
+ var keyCol = this.columns.getKeyColumn();
+ var rowCount = this.view.rowCount;
+ var start = 1;
+
+ var c = this.currentIndex;
+ if (length > 1) {
+ start = 0;
+ if (c < 0)
+ c = 0;
+ }
+
+ for (var i = 0; i < rowCount; i++) {
+ var l = (i + start + c) % rowCount;
+ var cellText = this.view.getCellText(l, keyCol);
+ cellText = cellText.substring(0, length).toLowerCase();
+ if (cellText == incrementalString)
+ return l;
+ }
+ return -1;
+ ]]></body>
+ </method>
+
+ <method name="startEditing">
+ <parameter name="row"/>
+ <parameter name="column"/>
+ <body>
+ <![CDATA[
+ if (!this.editable)
+ return false;
+ if (row < 0 || row >= this.view.rowCount || !column)
+ return false;
+ if (column.type != Components.interfaces.nsITreeColumn.TYPE_TEXT &&
+ column.type != Components.interfaces.nsITreeColumn.TYPE_PASSWORD)
+ return false;
+ if (column.cycler || !this.view.isEditable(row, column))
+ return false;
+
+ // Beyond this point, we are going to edit the cell.
+ if (this._editingColumn)
+ this.stopEditing();
+
+ var input = this.inputField;
+
+ var box = this.treeBoxObject;
+ box.ensureCellIsVisible(row, column);
+
+ // Get the coordinates of the text inside the cell.
+ var textRect = box.getCoordsForCellItem(row, column, "text");
+
+ // Get the coordinates of the cell itself.
+ var cellRect = box.getCoordsForCellItem(row, column, "cell");
+
+ // Calculate the top offset of the textbox.
+ var style = window.getComputedStyle(input, "");
+ var topadj = parseInt(style.borderTopWidth) + parseInt(style.paddingTop);
+ input.top = textRect.y - topadj;
+
+ // The leftside of the textbox is aligned to the left side of the text
+ // in LTR mode, and left side of the cell in RTL mode.
+ var left, widthdiff;
+ if (style.direction == "rtl") {
+ left = cellRect.x;
+ widthdiff = cellRect.x - textRect.x;
+ } else {
+ left = textRect.x;
+ widthdiff = textRect.x - cellRect.x;
+ }
+
+ input.left = left;
+ input.height = textRect.height + topadj +
+ parseInt(style.borderBottomWidth) +
+ parseInt(style.paddingBottom);
+ input.width = cellRect.width - widthdiff;
+ input.hidden = false;
+
+ input.value = this.view.getCellText(row, column);
+ var selectText = function selectText() {
+ input.select();
+ input.inputField.focus();
+ }
+ setTimeout(selectText, 0);
+
+ this._editingRow = row;
+ this._editingColumn = column;
+ this.setAttribute("editing", "true");
+
+ box.invalidateCell(row, column);
+ return true;
+ ]]>
+ </body>
+ </method>
+
+ <method name="stopEditing">
+ <parameter name="accept"/>
+ <body>
+ <![CDATA[
+ if (!this._editingColumn)
+ return;
+
+ var input = this.inputField;
+ var editingRow = this._editingRow;
+ var editingColumn = this._editingColumn;
+ this._editingRow = -1;
+ this._editingColumn = null;
+
+ if (accept) {
+ var value = input.value;
+ this.view.setCellText(editingRow, editingColumn, value);
+ }
+ input.hidden = true;
+ input.value = "";
+ this.removeAttribute("editing");
+ ]]>
+ </body>
+ </method>
+
+ <method name="_moveByOffset">
+ <parameter name="offset"/>
+ <parameter name="edge"/>
+ <parameter name="event"/>
+ <body>
+ <![CDATA[
+ event.preventDefault();
+
+ if (this.view.rowCount == 0)
+ return;
+
+ if (this._isAccelPressed(event) && this.view.selection.single) {
+ this.treeBoxObject.scrollByLines(offset);
+ return;
+ }
+
+ var c = this.currentIndex + offset;
+ if (offset > 0 ? c > edge : c < edge) {
+ if (this.view.selection.isSelected(edge) && this.view.selection.count <= 1)
+ return;
+ c = edge;
+ }
+
+ var cellSelType = this._cellSelType;
+ if (cellSelType) {
+ var column = this.view.selection.currentColumn;
+ if (!column)
+ return;
+
+ while ((offset > 0 ? c <= edge : c >= edge) && !this.view.isSelectable(c, column))
+ c += offset;
+ if (offset > 0 ? c > edge : c < edge)
+ return;
+ }
+
+ if (!this._isAccelPressed(event))
+ this.view.selection.timedSelect(c, this._selectDelay);
+ else // Ctrl+Up/Down moves the anchor without selecting
+ this.currentIndex = c;
+ this.treeBoxObject.ensureRowIsVisible(c);
+ ]]>
+ </body>
+ </method>
+
+ <method name="_moveByOffsetShift">
+ <parameter name="offset"/>
+ <parameter name="edge"/>
+ <parameter name="event"/>
+ <body>
+ <![CDATA[
+ event.preventDefault();
+
+ if (this.view.rowCount == 0)
+ return;
+
+ if (this.view.selection.single) {
+ this.treeBoxObject.scrollByLines(offset);
+ return;
+ }
+
+ if (this.view.rowCount == 1 && !this.view.selection.isSelected(0)) {
+ this.view.selection.timedSelect(0, this._selectDelay);
+ return;
+ }
+
+ var c = this.currentIndex;
+ if (c == -1)
+ c = 0;
+
+ if (c == edge) {
+ if (this.view.selection.isSelected(c))
+ return;
+ }
+
+ // Extend the selection from the existing pivot, if any
+ this.view.selection.rangedSelect(-1, c + offset,
+ this._isAccelPressed(event));
+ this.treeBoxObject.ensureRowIsVisible(c + offset);
+
+ ]]>
+ </body>
+ </method>
+
+ <method name="_moveByPage">
+ <parameter name="offset"/>
+ <parameter name="edge"/>
+ <parameter name="event"/>
+ <body>
+ <![CDATA[
+ event.preventDefault();
+
+ if (this.view.rowCount == 0)
+ return;
+
+ if (this.pageUpOrDownMovesSelection == this._isAccelPressed(event)) {
+ this.treeBoxObject.scrollByPages(offset);
+ return;
+ }
+
+ if (this.view.rowCount == 1 && !this.view.selection.isSelected(0)) {
+ this.view.selection.timedSelect(0, this._selectDelay);
+ return;
+ }
+
+ var c = this.currentIndex;
+ if (c == -1)
+ return;
+
+ if (c == edge && this.view.selection.isSelected(c)) {
+ this.treeBoxObject.ensureRowIsVisible(c);
+ return;
+ }
+ var i = this.treeBoxObject.getFirstVisibleRow();
+ var p = this.treeBoxObject.getPageLength();
+
+ if (offset > 0) {
+ i += p - 1;
+ if (c >= i) {
+ i = c + p;
+ this.treeBoxObject.ensureRowIsVisible(i > edge ? edge : i);
+ }
+ i = i > edge ? edge : i;
+
+ } else if (c <= i) {
+ i = c <= p ? 0 : c - p;
+ this.treeBoxObject.ensureRowIsVisible(i);
+ }
+ this.view.selection.timedSelect(i, this._selectDelay);
+ ]]>
+ </body>
+ </method>
+
+ <method name="_moveByPageShift">
+ <parameter name="offset"/>
+ <parameter name="edge"/>
+ <parameter name="event"/>
+ <body>
+ <![CDATA[
+ event.preventDefault();
+
+ if (this.view.rowCount == 0)
+ return;
+
+ if (this.view.rowCount == 1 && !this.view.selection.isSelected(0) &&
+ !(this.pageUpOrDownMovesSelection == this._isAccelPressed(event))) {
+ this.view.selection.timedSelect(0, this._selectDelay);
+ return;
+ }
+
+ if (this.view.selection.single)
+ return;
+
+ var c = this.currentIndex;
+ if (c == -1)
+ return;
+ if (c == edge && this.view.selection.isSelected(c)) {
+ this.treeBoxObject.ensureRowIsVisible(edge);
+ return;
+ }
+ var i = this.treeBoxObject.getFirstVisibleRow();
+ var p = this.treeBoxObject.getPageLength();
+
+ if (offset > 0) {
+ i += p - 1;
+ if (c >= i) {
+ i = c + p;
+ this.treeBoxObject.ensureRowIsVisible(i > edge ? edge : i);
+ }
+ // Extend the selection from the existing pivot, if any
+ this.view.selection.rangedSelect(-1, i > edge ? edge : i, this._isAccelPressed(event));
+
+ } else {
+
+ if (c <= i) {
+ i = c <= p ? 0 : c - p;
+ this.treeBoxObject.ensureRowIsVisible(i);
+ }
+ // Extend the selection from the existing pivot, if any
+ this.view.selection.rangedSelect(-1, i, this._isAccelPressed(event));
+ }
+
+ ]]>
+ </body>
+ </method>
+
+ <method name="_moveToEdge">
+ <parameter name="edge"/>
+ <parameter name="event"/>
+ <body>
+ <![CDATA[
+ event.preventDefault();
+
+ if (this.view.rowCount == 0)
+ return;
+
+ if (this.view.selection.isSelected(edge) && this.view.selection.count == 1) {
+ this.currentIndex = edge;
+ return;
+ }
+
+ // Normal behaviour is to select the first/last row
+ if (!this._isAccelPressed(event))
+ this.view.selection.timedSelect(edge, this._selectDelay);
+
+ // In a multiselect tree Ctrl+Home/End moves the anchor
+ else if (!this.view.selection.single)
+ this.currentIndex = edge;
+
+ this.treeBoxObject.ensureRowIsVisible(edge);
+ ]]>
+ </body>
+ </method>
+
+ <method name="_moveToEdgeShift">
+ <parameter name="edge"/>
+ <parameter name="event"/>
+ <body>
+ <![CDATA[
+ event.preventDefault();
+
+ if (this.view.rowCount == 0)
+ return;
+
+ if (this.view.rowCount == 1 && !this.view.selection.isSelected(0)) {
+ this.view.selection.timedSelect(0, this._selectDelay);
+ return;
+ }
+
+ if (this.view.selection.single ||
+ (this.view.selection.isSelected(edge)) && this.view.selection.isSelected(this.currentIndex))
+ return;
+
+ // Extend the selection from the existing pivot, if any.
+ // -1 doesn't work here, so using currentIndex instead
+ this.view.selection.rangedSelect(this.currentIndex, edge, this._isAccelPressed(event));
+
+ this.treeBoxObject.ensureRowIsVisible(edge);
+ ]]>
+ </body>
+ </method>
+ <method name="_handleEnter">
+ <parameter name="event"/>
+ <body><![CDATA[
+ if (this._editingColumn) {
+ this.stopEditing(true);
+ this.focus();
+ return true;
+ }
+
+ if (/Mac/.test(navigator.platform)) {
+ // See if we can edit the cell.
+ var row = this.currentIndex;
+ if (this._cellSelType) {
+ var column = this.view.selection.currentColumn;
+ var startedEditing = this.startEditing(row, column);
+ if (startedEditing)
+ return true;
+ }
+ }
+ return this.changeOpenState(this.currentIndex);
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="touchstart">
+ <![CDATA[
+ if (event.touches.length > 1) {
+ // Multiple touch points detected, abort. In particular this aborts
+ // the panning gesture when the user puts a second finger down after
+ // already panning with one finger. Aborting at this point prevents
+ // the pan gesture from being resumed until all fingers are lifted
+ // (as opposed to when the user is back down to one finger).
+ this._touchY = -1;
+ } else {
+ this._touchY = event.touches[0].screenY;
+ }
+ ]]>
+ </handler>
+ <handler event="touchmove">
+ <![CDATA[
+ if (event.touches.length == 1 &&
+ this._touchY >= 0) {
+ var deltaY = this._touchY - event.touches[0].screenY;
+ var lines = Math.trunc(deltaY / this.treeBoxObject.rowHeight);
+ if (Math.abs(lines) > 0) {
+ this.treeBoxObject.scrollByLines(lines);
+ deltaY -= lines * this.treeBoxObject.rowHeight;
+ this._touchY = event.touches[0].screenY + deltaY;
+ }
+ event.preventDefault();
+ }
+ ]]>
+ </handler>
+ <handler event="touchend">
+ <![CDATA[
+ this._touchY = -1;
+ ]]>
+ </handler>
+ <handler event="MozMousePixelScroll" preventdefault="true"/>
+ <handler event="DOMMouseScroll" preventdefault="true">
+ <![CDATA[
+ if (this._editingColumn)
+ return;
+ if (event.axis == event.HORIZONTAL_AXIS)
+ return;
+
+ var rows = event.detail;
+ if (rows == UIEvent.SCROLL_PAGE_UP)
+ this.treeBoxObject.scrollByPages(-1);
+ else if (rows == UIEvent.SCROLL_PAGE_DOWN)
+ this.treeBoxObject.scrollByPages(1);
+ else
+ this.treeBoxObject.scrollByLines(rows);
+ ]]>
+ </handler>
+ <handler event="MozSwipeGesture" preventdefault="true">
+ <![CDATA[
+ // Figure out which row to show
+ let targetRow = 0;
+
+ // Only handle swipe gestures up and down
+ switch (event.direction) {
+ case event.DIRECTION_DOWN:
+ targetRow = this.view.rowCount - 1;
+ // Fall through for actual action
+ case event.DIRECTION_UP:
+ this.treeBoxObject.ensureRowIsVisible(targetRow);
+ break;
+ }
+ ]]>
+ </handler>
+ <handler event="select" phase="target"
+ action="if (event.originalTarget == this) this.stopEditing(true);"/>
+ <handler event="focus">
+ <![CDATA[
+ this.treeBoxObject.focused = true;
+ if (this.currentIndex == -1 && this.view.rowCount > 0) {
+ this.currentIndex = this.treeBoxObject.getFirstVisibleRow();
+ }
+ if (this._cellSelType && !this.view.selection.currentColumn) {
+ var col = this._getNextColumn(this.currentIndex, false);
+ this.view.selection.currentColumn = col;
+ }
+ ]]>
+ </handler>
+ <handler event="blur" action="this.treeBoxObject.focused = false;"/>
+ <handler event="blur" phase="capturing"
+ action="if (event.originalTarget == this.inputField.inputField) this.stopEditing(true);"/>
+ <handler event="keydown" keycode="VK_RETURN">
+ if (this._handleEnter(event)) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ </handler>
+#ifndef XP_MACOSX
+ <!-- Use F2 key to enter text editing. -->
+ <handler event="keydown" keycode="VK_F2">
+ <![CDATA[
+ if (!this._cellSelType)
+ return;
+ var row = this.currentIndex;
+ var column = this.view.selection.currentColumn;
+ if (this.startEditing(row, column))
+ event.preventDefault();
+ ]]>
+ </handler>
+#endif // XP_MACOSX
+
+ <handler event="keydown" keycode="VK_ESCAPE">
+ <![CDATA[
+ if (this._editingColumn) {
+ this.stopEditing(false);
+ this.focus();
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ ]]>
+ </handler>
+ <handler event="keydown" keycode="VK_LEFT">
+ <![CDATA[
+ if (this._editingColumn)
+ return;
+
+ var row = this.currentIndex;
+ if (row < 0)
+ return;
+
+ var cellSelType = this._cellSelType;
+ var checkContainers = true;
+
+ var currentColumn;
+ if (cellSelType) {
+ currentColumn = this.view.selection.currentColumn;
+ if (currentColumn && !currentColumn.primary)
+ checkContainers = false;
+ }
+
+ if (checkContainers) {
+ if (this.changeOpenState(this.currentIndex, false)) {
+ event.preventDefault();
+ return;
+ }
+ var parentIndex = this.view.getParentIndex(this.currentIndex);
+ if (parentIndex >= 0) {
+ if (cellSelType && !this.view.isSelectable(parentIndex, currentColumn)) {
+ return;
+ }
+ this.view.selection.select(parentIndex);
+ this.treeBoxObject.ensureRowIsVisible(parentIndex);
+ event.preventDefault();
+ return;
+ }
+ }
+
+ if (cellSelType) {
+ var col = this._getNextColumn(row, true);
+ if (col) {
+ this.view.selection.currentColumn = col;
+ this.treeBoxObject.ensureCellIsVisible(row, col);
+ event.preventDefault();
+ }
+ }
+ ]]>
+ </handler>
+ <handler event="keydown" keycode="VK_RIGHT">
+ <![CDATA[
+ if (this._editingColumn)
+ return;
+
+ var row = this.currentIndex;
+ if (row < 0)
+ return;
+
+ var cellSelType = this._cellSelType;
+ var checkContainers = true;
+
+ var currentColumn;
+ if (cellSelType) {
+ currentColumn = this.view.selection.currentColumn;
+ if (currentColumn && !currentColumn.primary)
+ checkContainers = false;
+ }
+
+ if (checkContainers) {
+ if (this.changeOpenState(row, true)) {
+ event.preventDefault();
+ return;
+ }
+ var c = row + 1;
+ var view = this.view;
+ if (c < view.rowCount &&
+ view.getParentIndex(c) == row) {
+ // If already opened, select the first child.
+ // The getParentIndex test above ensures that the children
+ // are already populated and ready.
+ if (cellSelType && !this.view.isSelectable(c, currentColumn)) {
+ let col = this._getNextColumn(c, false);
+ if (col) {
+ this.view.selection.currentColumn = col;
+ }
+ }
+ this.view.selection.timedSelect(c, this._selectDelay);
+ this.treeBoxObject.ensureRowIsVisible(c);
+ event.preventDefault();
+ return;
+ }
+ }
+
+ if (cellSelType) {
+ let col = this._getNextColumn(row, false);
+ if (col) {
+ this.view.selection.currentColumn = col;
+ this.treeBoxObject.ensureCellIsVisible(row, col);
+ event.preventDefault();
+ }
+ }
+ ]]>
+ </handler>
+ <handler event="keydown" keycode="VK_UP" modifiers="accel any">
+ <![CDATA[
+ if (this._editingColumn)
+ return;
+ _moveByOffset(-1, 0, event);
+ ]]>
+ </handler>
+ <handler event="keydown" keycode="VK_DOWN" modifiers="accel any">
+ <![CDATA[
+ if (this._editingColumn)
+ return;
+ _moveByOffset(1, this.view.rowCount - 1, event);
+ ]]>
+ </handler>
+ <handler event="keydown" keycode="VK_UP" modifiers="accel any, shift">
+ <![CDATA[
+ if (this._editingColumn)
+ return;
+ _moveByOffsetShift(-1, 0, event);
+ ]]>
+ </handler>
+ <handler event="keydown" keycode="VK_DOWN" modifiers="accel any, shift">
+ <![CDATA[
+ if (this._editingColumn)
+ return;
+ _moveByOffsetShift(1, this.view.rowCount - 1, event);
+ ]]>
+ </handler>
+ <handler event="keydown" keycode="VK_PAGE_UP" modifiers="accel any">
+ <![CDATA[
+ if (this._editingColumn)
+ return;
+ _moveByPage(-1, 0, event);
+ ]]>
+ </handler>
+ <handler event="keydown" keycode="VK_PAGE_DOWN" modifiers="accel any">
+ <![CDATA[
+ if (this._editingColumn)
+ return;
+ _moveByPage(1, this.view.rowCount - 1, event);
+ ]]>
+ </handler>
+ <handler event="keydown" keycode="VK_PAGE_UP" modifiers="accel any, shift">
+ <![CDATA[
+ if (this._editingColumn)
+ return;
+ _moveByPageShift(-1, 0, event);
+ ]]>
+ </handler>
+ <handler event="keydown" keycode="VK_PAGE_DOWN" modifiers="accel any, shift">
+ <![CDATA[
+ if (this._editingColumn)
+ return;
+ _moveByPageShift(1, this.view.rowCount - 1, event);
+ ]]>
+ </handler>
+ <handler event="keydown" keycode="VK_HOME" modifiers="accel any">
+ <![CDATA[
+ if (this._editingColumn)
+ return;
+ _moveToEdge(0, event);
+ ]]>
+ </handler>
+ <handler event="keydown" keycode="VK_END" modifiers="accel any">
+ <![CDATA[
+ if (this._editingColumn)
+ return;
+ _moveToEdge(this.view.rowCount - 1, event);
+ ]]>
+ </handler>
+ <handler event="keydown" keycode="VK_HOME" modifiers="accel any, shift">
+ <![CDATA[
+ if (this._editingColumn)
+ return;
+ _moveToEdgeShift(0, event);
+ ]]>
+ </handler>
+ <handler event="keydown" keycode="VK_END" modifiers="accel any, shift">
+ <![CDATA[
+ if (this._editingColumn)
+ return;
+ _moveToEdgeShift(this.view.rowCount - 1, event);
+ ]]>
+ </handler>
+ <handler event="keypress">
+ <![CDATA[
+ if (this._editingColumn)
+ return;
+
+ if (event.charCode == ' '.charCodeAt(0)) {
+ var c = this.currentIndex;
+ if (!this.view.selection.isSelected(c) ||
+ (!this.view.selection.single && this._isAccelPressed(event))) {
+ this.view.selection.toggleSelect(c);
+ event.preventDefault();
+ }
+ }
+ else if (!this.disableKeyNavigation && event.charCode > 0 &&
+ !event.altKey && !this._isAccelPressed(event) &&
+ !event.metaKey && !event.ctrlKey) {
+ var l = this._keyNavigate(event);
+ if (l >= 0) {
+ this.view.selection.timedSelect(l, this._selectDelay);
+ this.treeBoxObject.ensureRowIsVisible(l);
+ }
+ event.preventDefault();
+ }
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="treecols" role="xul:treecolumns">
+ <resources>
+ <stylesheet src="chrome://global/skin/tree.css"/>
+ </resources>
+ <content orient="horizontal">
+ <xul:hbox class="tree-scrollable-columns" flex="1">
+ <children includes="treecol|splitter"/>
+ </xul:hbox>
+ <xul:treecolpicker class="treecol-image" fixed="true" xbl:inherits="tooltiptext=pickertooltiptext"/>
+ </content>
+ <implementation>
+ <constructor><![CDATA[
+ // Set resizeafter="farthest" on the splitters if nothing else has been
+ // specified.
+ Array.forEach(this.getElementsByTagName("splitter"), function (splitter) {
+ if (!splitter.hasAttribute("resizeafter"))
+ splitter.setAttribute("resizeafter", "farthest");
+ });
+ ]]></constructor>
+ </implementation>
+ </binding>
+
+ <binding id="treerows" extends="chrome://global/content/bindings/tree.xml#tree-base">
+ <content>
+ <xul:hbox flex="1" class="tree-bodybox">
+ <children/>
+ </xul:hbox>
+ <xul:scrollbar height="0" minwidth="0" minheight="0" orient="vertical" xbl:inherits="collapsed=hidevscroll" style="position:relative; z-index:2147483647;"/>
+ </content>
+ <handlers>
+ <handler event="underflow">
+ <![CDATA[
+ // Scrollport event orientation
+ // 0: vertical
+ // 1: horizontal
+ // 2: both (not used)
+ var tree = document.getBindingParent(this);
+ if (event.detail == 1)
+ tree.setAttribute("hidehscroll", "true");
+ else if (event.detail == 0)
+ tree.setAttribute("hidevscroll", "true");
+ event.stopPropagation();
+ ]]>
+ </handler>
+ <handler event="overflow">
+ <![CDATA[
+ var tree = document.getBindingParent(this);
+ if (event.detail == 1)
+ tree.removeAttribute("hidehscroll");
+ else if (event.detail == 0)
+ tree.removeAttribute("hidevscroll");
+ event.stopPropagation();
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="treebody" extends="chrome://global/content/bindings/tree.xml#tree-base">
+ <implementation>
+ <constructor>
+ if ("_ensureColumnOrder" in this.parentNode)
+ this.parentNode._ensureColumnOrder();
+ </constructor>
+
+ <field name="_lastSelectedRow">
+ -1
+ </field>
+ </implementation>
+ <handlers>
+ <!-- If there is no modifier key, we select on mousedown, not
+ click, so that drags work correctly. -->
+ <handler event="mousedown" clickcount="1">
+ <![CDATA[
+ if (this.parentNode.disabled)
+ return;
+ if (((!this._isAccelPressed(event) ||
+ !this.parentNode.pageUpOrDownMovesSelection) &&
+ !event.shiftKey && !event.metaKey) ||
+ this.parentNode.view.selection.single) {
+ var b = this.parentNode.treeBoxObject;
+ var cell = b.getCellAt(event.clientX, event.clientY);
+ var view = this.parentNode.view;
+
+ // save off the last selected row
+ this._lastSelectedRow = cell.row;
+
+ if (cell.row == -1)
+ return;
+
+ if (cell.childElt == "twisty")
+ return;
+
+ if (cell.col && event.button == 0) {
+ if (cell.col.cycler) {
+ view.cycleCell(cell.row, cell.col);
+ return;
+ } else if (cell.col.type == Components.interfaces.nsITreeColumn.TYPE_CHECKBOX) {
+ if (this.parentNode.editable && cell.col.editable &&
+ view.isEditable(cell.row, cell.col)) {
+ var value = view.getCellValue(cell.row, cell.col);
+ value = value == "true" ? "false" : "true";
+ view.setCellValue(cell.row, cell.col, value);
+ return;
+ }
+ }
+ }
+
+ var cellSelType = this.parentNode._cellSelType;
+ if (cellSelType == "text" && cell.childElt != "text" && cell.childElt != "image")
+ return;
+
+ if (cellSelType) {
+ if (!cell.col.selectable ||
+ !view.isSelectable(cell.row, cell.col)) {
+ return;
+ }
+ }
+
+ if (!view.selection.isSelected(cell.row)) {
+ view.selection.select(cell.row);
+ b.ensureRowIsVisible(cell.row);
+ }
+
+ if (cellSelType) {
+ view.selection.currentColumn = cell.col;
+ }
+ }
+ ]]>
+ </handler>
+
+ <!-- On a click (up+down on the same item), deselect everything
+ except this item. -->
+ <handler event="click" button="0" clickcount="1">
+ <![CDATA[
+ if (this.parentNode.disabled)
+ return;
+ var b = this.parentNode.treeBoxObject;
+ var cell = b.getCellAt(event.clientX, event.clientY);
+ var view = this.parentNode.view;
+
+ if (cell.row == -1)
+ return;
+
+ if (cell.childElt == "twisty") {
+ if (view.selection.currentIndex >= 0 &&
+ view.isContainerOpen(cell.row)) {
+ var parentIndex = view.getParentIndex(view.selection.currentIndex);
+ while (parentIndex >= 0 && parentIndex != cell.row)
+ parentIndex = view.getParentIndex(parentIndex);
+ if (parentIndex == cell.row) {
+ var parentSelectable = true;
+ if (this.parentNode._cellSelType) {
+ var currentColumn = view.selection.currentColumn;
+ if (!view.isSelectable(parentIndex, currentColumn))
+ parentSelectable = false;
+ }
+ if (parentSelectable)
+ view.selection.select(parentIndex);
+ }
+ }
+ this.parentNode.changeOpenState(cell.row);
+ return;
+ }
+
+ if (! view.selection.single) {
+ var augment = this._isAccelPressed(event);
+ if (event.shiftKey) {
+ view.selection.rangedSelect(-1, cell.row, augment);
+ b.ensureRowIsVisible(cell.row);
+ return;
+ }
+ if (augment) {
+ view.selection.toggleSelect(cell.row);
+ b.ensureRowIsVisible(cell.row);
+ view.selection.currentIndex = cell.row;
+ return;
+ }
+ }
+
+ /* We want to deselect all the selected items except what was
+ clicked, UNLESS it was a right-click. We have to do this
+ in click rather than mousedown so that you can drag a
+ selected group of items */
+
+ if (!cell.col) return;
+
+ // if the last row has changed in between the time we
+ // mousedown and the time we click, don't fire the select handler.
+ // see bug #92366
+ if (!cell.col.cycler && this._lastSelectedRow == cell.row &&
+ cell.col.type != Components.interfaces.nsITreeColumn.TYPE_CHECKBOX) {
+
+ var cellSelType = this.parentNode._cellSelType;
+ if (cellSelType == "text" && cell.childElt != "text" && cell.childElt != "image")
+ return;
+
+ if (cellSelType) {
+ if (!cell.col.selectable ||
+ !view.isSelectable(cell.row, cell.col)) {
+ return;
+ }
+ }
+
+ view.selection.select(cell.row);
+ b.ensureRowIsVisible(cell.row);
+
+ if (cellSelType) {
+ view.selection.currentColumn = cell.col;
+ }
+ }
+ ]]>
+ </handler>
+
+ <!-- double-click -->
+ <handler event="click" clickcount="2">
+ <![CDATA[
+ if (this.parentNode.disabled)
+ return;
+ var tbo = this.parentNode.treeBoxObject;
+ var view = this.parentNode.view;
+ var row = view.selection.currentIndex;
+
+ if (row == -1)
+ return;
+
+ var cell = tbo.getCellAt(event.clientX, event.clientY);
+
+ if (cell.childElt != "twisty") {
+ view.selection.currentColumn = cell.col;
+ this.parentNode.startEditing(row, cell.col);
+ }
+
+ if (this.parentNode._editingColumn || !view.isContainer(row))
+ return;
+
+ // Cyclers and twisties respond to single clicks, not double clicks
+ if (cell.col && !cell.col.cycler && cell.childElt != "twisty")
+ this.parentNode.changeOpenState(row);
+ ]]>
+ </handler>
+
+ </handlers>
+ </binding>
+
+ <binding id="treecol-base" role="xul:treecolumnitem"
+ extends="chrome://global/content/bindings/tree.xml#tree-base">
+ <implementation>
+ <constructor>
+ this.parentNode.parentNode._columnsDirty = true;
+ </constructor>
+
+ <property name="ordinal">
+ <getter><![CDATA[
+ var val = this.getAttribute("ordinal");
+ if (val == "")
+ return "1";
+
+ return "" + (val == "0" ? 0 : parseInt(val));
+ ]]></getter>
+ <setter><![CDATA[
+ this.setAttribute("ordinal", val);
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="_previousVisibleColumn">
+ <getter><![CDATA[
+ var sib = this.boxObject.previousSibling;
+ while (sib) {
+ if (sib.localName == "treecol" && sib.boxObject.width > 0 && sib.parentNode == this.parentNode)
+ return sib;
+ sib = sib.boxObject.previousSibling;
+ }
+ return null;
+ ]]></getter>
+ </property>
+
+ <method name="_onDragMouseMove">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ var col = document.treecolDragging;
+ if (!col) return;
+
+ // determine if we have moved the mouse far enough
+ // to initiate a drag
+ if (col.mDragGesturing) {
+ if (Math.abs(aEvent.clientX - col.mStartDragX) < 5 &&
+ Math.abs(aEvent.clientY - col.mStartDragY) < 5) {
+ return;
+ }
+ col.mDragGesturing = false;
+ col.setAttribute("dragging", "true");
+ window.addEventListener("click", col._onDragMouseClick, true);
+ }
+
+ var pos = {};
+ var targetCol = col.parentNode.parentNode._getColumnAtX(aEvent.clientX, 0.5, pos);
+
+ // bail if we haven't mousemoved to a different column
+ if (col.mTargetCol == targetCol && col.mTargetDir == pos.value)
+ return;
+
+ var tree = col.parentNode.parentNode;
+ var sib;
+ var column;
+ if (col.mTargetCol) {
+ // remove previous insertbefore/after attributes
+ col.mTargetCol.removeAttribute("insertbefore");
+ col.mTargetCol.removeAttribute("insertafter");
+ column = tree.columns.getColumnFor(col.mTargetCol);
+ tree.treeBoxObject.invalidateColumn(column);
+ sib = col.mTargetCol._previousVisibleColumn;
+ if (sib) {
+ sib.removeAttribute("insertafter");
+ column = tree.columns.getColumnFor(sib);
+ tree.treeBoxObject.invalidateColumn(column);
+ }
+ col.mTargetCol = null;
+ col.mTargetDir = null;
+ }
+
+ if (targetCol) {
+ // set insertbefore/after attributes
+ if (pos.value == "after") {
+ targetCol.setAttribute("insertafter", "true");
+ } else {
+ targetCol.setAttribute("insertbefore", "true");
+ sib = targetCol._previousVisibleColumn;
+ if (sib) {
+ sib.setAttribute("insertafter", "true");
+ column = tree.columns.getColumnFor(sib);
+ tree.treeBoxObject.invalidateColumn(column);
+ }
+ }
+ column = tree.columns.getColumnFor(targetCol);
+ tree.treeBoxObject.invalidateColumn(column);
+ col.mTargetCol = targetCol;
+ col.mTargetDir = pos.value;
+ }
+ ]]></body>
+ </method>
+
+ <method name="_onDragMouseUp">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ var col = document.treecolDragging;
+ if (!col) return;
+
+ if (!col.mDragGesturing) {
+ if (col.mTargetCol) {
+ // remove insertbefore/after attributes
+ var before = col.mTargetCol.hasAttribute("insertbefore");
+ col.mTargetCol.removeAttribute(before ? "insertbefore" : "insertafter");
+
+ var sib = col.mTargetCol._previousVisibleColumn;
+ if (before && sib) {
+ sib.removeAttribute("insertafter");
+ }
+
+ // Move the column only if it will result in a different column
+ // ordering
+ var move = true;
+
+ // If this is a before move and the previous visible column is
+ // the same as the column we're moving, don't move
+ if (before && col == sib) {
+ move = false;
+ }
+ else if (!before && col == col.mTargetCol) {
+ // If this is an after move and the column we're moving is
+ // the same as the target column, don't move.
+ move = false;
+ }
+
+ if (move) {
+ col.parentNode.parentNode._reorderColumn(col, col.mTargetCol, before);
+ }
+
+ // repaint to remove lines
+ col.parentNode.parentNode.treeBoxObject.invalidate();
+
+ col.mTargetCol = null;
+ }
+ } else
+ col.mDragGesturing = false;
+
+ document.treecolDragging = null;
+ col.removeAttribute("dragging");
+
+ window.removeEventListener("mousemove", col._onDragMouseMove, true);
+ window.removeEventListener("mouseup", col._onDragMouseUp, true);
+ // we have to wait for the click event to fire before removing
+ // cancelling handler
+ var clickHandler = function(handler) {
+ window.removeEventListener("click", handler, true);
+ };
+ window.setTimeout(clickHandler, 0, col._onDragMouseClick);
+ ]]></body>
+ </method>
+
+ <method name="_onDragMouseClick">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ // prevent click event from firing after column drag and drop
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="mousedown" button="0"><![CDATA[
+ if (this.parentNode.parentNode.enableColumnDrag) {
+ var xulns = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ var cols = this.parentNode.getElementsByTagNameNS(xulns, "treecol");
+
+ // only start column drag operation if there are at least 2 visible columns
+ var visible = 0;
+ for (var i = 0; i < cols.length; ++i)
+ if (cols[i].boxObject.width > 0) ++visible;
+
+ if (visible > 1) {
+ window.addEventListener("mousemove", this._onDragMouseMove, true);
+ window.addEventListener("mouseup", this._onDragMouseUp, true);
+ document.treecolDragging = this;
+ this.mDragGesturing = true;
+ this.mStartDragX = event.clientX;
+ this.mStartDragY = event.clientY;
+ }
+ }
+ ]]></handler>
+ <handler event="click" button="0" phase="target">
+ <![CDATA[
+ if (event.target != event.originalTarget)
+ return;
+
+ // On Windows multiple clicking on tree columns only cycles one time
+ // every 2 clicks.
+ if (/Win/.test(navigator.platform) && event.detail % 2 == 0)
+ return;
+
+ var tree = this.parentNode.parentNode;
+ if (tree.columns) {
+ tree.view.cycleHeader(tree.columns.getColumnFor(this));
+ }
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="treecol" extends="chrome://global/content/bindings/tree.xml#treecol-base">
+ <content>
+ <xul:label class="treecol-text" xbl:inherits="crop,value=label" flex="1" crop="right"/>
+ <xul:image class="treecol-sortdirection" xbl:inherits="sortDirection,hidden=hideheader"/>
+ </content>
+ </binding>
+
+ <binding id="treecol-image" extends="chrome://global/content/bindings/tree.xml#treecol-base">
+ <content>
+ <xul:image class="treecol-icon" xbl:inherits="src"/>
+ </content>
+ </binding>
+
+ <binding id="columnpicker" display="xul:button" role="xul:button"
+ extends="chrome://global/content/bindings/tree.xml#tree-base">
+ <content>
+ <xul:image class="tree-columnpicker-icon"/>
+ <xul:menupopup anonid="popup">
+ <xul:menuseparator anonid="menuseparator"/>
+ <xul:menuitem anonid="menuitem" label="&restoreColumnOrder.label;"/>
+ </xul:menupopup>
+ </content>
+
+ <implementation>
+ <method name="buildPopup">
+ <parameter name="aPopup"/>
+ <body>
+ <![CDATA[
+ // We no longer cache the picker content, remove the old content.
+ while (aPopup.childNodes.length > 2)
+ aPopup.removeChild(aPopup.firstChild);
+
+ var refChild = aPopup.firstChild;
+
+ var tree = this.parentNode.parentNode;
+ for (var currCol = tree.columns.getFirstColumn(); currCol;
+ currCol = currCol.getNext()) {
+ // Construct an entry for each column in the row, unless
+ // it is not being shown.
+ var currElement = currCol.element;
+ if (!currElement.hasAttribute("ignoreincolumnpicker")) {
+ var popupChild = document.createElement("menuitem");
+ popupChild.setAttribute("type", "checkbox");
+ var columnName = currElement.getAttribute("display") ||
+ currElement.getAttribute("label");
+ popupChild.setAttribute("label", columnName);
+ popupChild.setAttribute("colindex", currCol.index);
+ if (currElement.getAttribute("hidden") != "true")
+ popupChild.setAttribute("checked", "true");
+ if (currCol.primary)
+ popupChild.setAttribute("disabled", "true");
+ aPopup.insertBefore(popupChild, refChild);
+ }
+ }
+
+ var hidden = !tree.enableColumnDrag;
+ const anonids = ["menuseparator", "menuitem"];
+ for (var i = 0; i < anonids.length; i++) {
+ var element = document.getAnonymousElementByAttribute(this, "anonid", anonids[i]);
+ element.hidden = hidden;
+ }
+ ]]>
+ </body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="command">
+ <![CDATA[
+ if (event.originalTarget == this) {
+ var popup = document.getAnonymousElementByAttribute(this, "anonid", "popup");
+ this.buildPopup(popup);
+ popup.showPopup(this, -1, -1, "popup", "bottomright", "topright");
+ }
+ else {
+ var tree = this.parentNode.parentNode;
+ tree.stopEditing(true);
+ var menuitem = document.getAnonymousElementByAttribute(this, "anonid", "menuitem");
+ if (event.originalTarget == menuitem) {
+ tree.columns.restoreNaturalOrder();
+ tree._ensureColumnOrder();
+ }
+ else {
+ var colindex = event.originalTarget.getAttribute("colindex");
+ var column = tree.columns[colindex];
+ if (column) {
+ var element = column.element;
+ if (element.getAttribute("hidden") == "true")
+ element.setAttribute("hidden", "false");
+ else
+ element.setAttribute("hidden", "true");
+ }
+ }
+ }
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+</bindings>
diff --git a/toolkit/content/widgets/videocontrols.css b/toolkit/content/widgets/videocontrols.css
new file mode 100644
index 000000000..99dbf5a2f
--- /dev/null
+++ b/toolkit/content/widgets/videocontrols.css
@@ -0,0 +1,128 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+@namespace html url("http://www.w3.org/1999/xhtml");
+
+.scrubber,
+.volumeControl {
+ -moz-binding: url("chrome://global/content/bindings/videocontrols.xml#suppressChangeEvent");
+}
+
+.scrubber .scale-thumb {
+ -moz-binding: url("chrome://global/content/bindings/videocontrols.xml#timeThumb");
+}
+
+.playButton,
+.muteButton,
+.scrubber .scale-slider,
+.volumeControl .scale-slider {
+ -moz-user-focus: none;
+}
+
+.controlBar[fullscreen-unavailable] > .fullscreenButton {
+ display: none;
+}
+
+.mediaControlsFrame {
+ direction: ltr;
+ /* Prevent unwanted style inheritance. See bug 554717. */
+ text-align: left;
+ list-style-image: none !important;
+ font: normal normal normal 100%/normal sans-serif !important;
+ text-decoration: none !important;
+}
+
+.controlsSpacer[hideCursor] {
+ cursor: none;
+}
+
+.controlsOverlay[scaled] {
+ -moz-box-align: center;
+}
+
+/* CSS Transitions
+ *
+ * These are overriden by the default theme; the rules here just
+ * provide a fallback to drive the required transitionend event
+ * (in case a 3rd party theme does not provide transitions).
+ */
+.controlBar:not([immediate]) {
+ transition-property: opacity;
+ transition-duration: 1ms;
+}
+.controlBar[fadeout] {
+ opacity: 0;
+}
+.volumeStack:not([immediate]) {
+ transition-property: opacity, margin-top;
+ transition-duration: 1ms, 1ms;
+}
+.volumeStack[fadeout] {
+ opacity: 0;
+ margin-top: 0;
+}
+.statusOverlay:not([immediate]) {
+ transition-property: opacity;
+ transition-duration: 1ms;
+ transition-delay: 750ms;
+}
+.statusOverlay[fadeout] {
+ opacity: 0;
+}
+
+/* Statistics formatting */
+html|td.statLabel {
+ font-weight: bold;
+ max-width: 20%;
+ white-space: nowrap;
+}
+html|td.statValue {
+ max-width: 30%;
+}
+html|td.filename {
+ max-width: 80%;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+html|span.statActivity > html|span {
+ display: none;
+}
+html|span.statActivity[activity="paused"] > html|span.statActivityPaused,
+html|span.statActivity[activity="playing"] > html|span.statActivityPlaying,
+html|span.statActivity[activity="ended"] > html|span.statActivityEnded,
+html|span.statActivity[seeking] > html|span.statActivitySeeking {
+ display: inline;
+}
+
+.controlBar[size="hidden"],
+.controlBar[size="small"] .durationBox,
+.controlBar[size="small"] .durationLabel,
+.controlBar[size="small"] .positionLabel,
+.controlBar[size="small"] .volumeStack {
+ visibility: collapse;
+}
+
+.controlBar[size="small"] .scrubberStack,
+.controlBar[size="small"] .backgroundBar,
+.controlBar[size="small"] .bufferBar,
+.controlBar[size="small"] .progressBar,
+.controlBar[size="small"] .scrubber {
+ visibility: hidden;
+}
+
+/* Error description formatting */
+.errorLabel {
+ display: none;
+}
+
+[error="errorAborted"] > [anonid="errorAborted"],
+[error="errorNetwork"] > [anonid="errorNetwork"],
+[error="errorDecode"] > [anonid="errorDecode"],
+[error="errorSrcNotSupported"] > [anonid="errorSrcNotSupported"],
+[error="errorNoSource"] > [anonid="errorNoSource"],
+[error="errorGeneric"] > [anonid="errorGeneric"] {
+ display: inline;
+}
diff --git a/toolkit/content/widgets/videocontrols.xml b/toolkit/content/widgets/videocontrols.xml
new file mode 100644
index 000000000..630f5a022
--- /dev/null
+++ b/toolkit/content/widgets/videocontrols.xml
@@ -0,0 +1,2027 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<!DOCTYPE bindings [
+ <!ENTITY % videocontrolsDTD SYSTEM "chrome://global/locale/videocontrols.dtd">
+ %videocontrolsDTD;
+]>
+
+<bindings id="videoControlBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+
+ <binding id="timeThumb"
+ extends="chrome://global/content/bindings/scale.xml#scalethumb">
+ <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <xbl:children/>
+ <hbox class="timeThumb" xbl:inherits="showhours">
+ <label class="timeLabel"/>
+ </hbox>
+ </xbl:content>
+ <implementation>
+
+ <constructor>
+ <![CDATA[
+ this.timeLabel = document.getAnonymousElementByAttribute(this, "class", "timeLabel");
+ this.timeLabel.setAttribute("value", "0:00");
+ ]]>
+ </constructor>
+
+ <property name="showHours">
+ <getter>
+ <![CDATA[
+ return this.getAttribute("showhours") == "true";
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ this.setAttribute("showhours", val);
+ // If the duration becomes known while we're still showing the value
+ // for time=0, immediately update the value to show or hide the hours.
+ // It's less intrusive to do it now than when the user clicks play and
+ // is looking right next to the thumb.
+ var displayedTime = this.timeLabel.getAttribute("value");
+ if (val && displayedTime == "0:00")
+ this.timeLabel.setAttribute("value", "0:00:00");
+ else if (!val && displayedTime == "0:00:00")
+ this.timeLabel.setAttribute("value", "0:00");
+ ]]>
+ </setter>
+ </property>
+
+ <method name="setTime">
+ <parameter name="time"/>
+ <body>
+ <![CDATA[
+ var timeString;
+ time = Math.round(time / 1000);
+ var hours = Math.floor(time / 3600);
+ var mins = Math.floor((time % 3600) / 60);
+ var secs = Math.floor(time % 60);
+ if (secs < 10)
+ secs = "0" + secs;
+ if (hours || this.showHours) {
+ if (mins < 10)
+ mins = "0" + mins;
+ timeString = hours + ":" + mins + ":" + secs;
+ } else {
+ timeString = mins + ":" + secs;
+ }
+
+ this.timeLabel.setAttribute("value", timeString);
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="suppressChangeEvent"
+ extends="chrome://global/content/bindings/scale.xml#scale">
+ <implementation implements="nsIXBLAccessible">
+ <!-- nsIXBLAccessible -->
+ <property name="accessibleName" readonly="true">
+ <getter>
+ if (this.type != "scrubber")
+ return "";
+
+ var currTime = this.thumb.timeLabel.getAttribute("value");
+ var totalTime = this.durationValue;
+
+ return this.scrubberNameFormat.replace(/#1/, currTime).
+ replace(/#2/, totalTime);
+ </getter>
+ </property>
+
+ <constructor>
+ <![CDATA[
+ this.scrubberNameFormat = ]]>"&scrubberScale.nameFormat;"<![CDATA[;
+ this.durationValue = "";
+ this.valueBar = null;
+ this.isDragging = false;
+ this.isPausedByDragging = false;
+
+ this.thumb = document.getAnonymousElementByAttribute(this, "class", "scale-thumb");
+ this.type = this.getAttribute("class");
+ this.Utils = document.getBindingParent(this.parentNode).Utils;
+ if (this.type == "scrubber")
+ this.valueBar = this.Utils.progressBar;
+ ]]>
+ </constructor>
+
+ <method name="valueChanged">
+ <parameter name="which"/>
+ <parameter name="newValue"/>
+ <parameter name="userChanged"/>
+ <body>
+ <![CDATA[
+ // This method is a copy of the base binding's valueChanged(), except that it does
+ // not dispatch a |change| event (to avoid exposing the event to web content), and
+ // just calls the videocontrol's seekToPosition() method directly.
+ switch (which) {
+ case "curpos":
+ if (this.type == "scrubber") {
+ // Update the time shown in the thumb.
+ this.thumb.setTime(newValue);
+ this.Utils.positionLabel.setAttribute("value", this.thumb.timeLabel.value);
+ // Update the value bar to match the thumb position.
+ let percent = newValue / this.max;
+ this.valueBar.value = Math.round(percent * 10000); // has max=10000
+ }
+
+ // The value of userChanged is true when changing the position with the mouse,
+ // but not when pressing an arrow key. However, the base binding sets
+ // ._userChanged in its keypress handlers, so we just need to check both.
+ if (!userChanged && !this._userChanged)
+ return;
+ this.setAttribute("value", newValue);
+
+ if (this.type == "scrubber")
+ this.Utils.seekToPosition(newValue);
+ else if (this.type == "volumeControl")
+ this.Utils.setVolume(newValue / 100);
+ break;
+
+ case "minpos":
+ this.setAttribute("min", newValue);
+ break;
+
+ case "maxpos":
+ if (this.type == "scrubber") {
+ // Update the value bar to match the thumb position.
+ let percent = this.value / newValue;
+ this.valueBar.value = Math.round(percent * 10000); // has max=10000
+ }
+ this.setAttribute("max", newValue);
+ break;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="dragStateChanged">
+ <parameter name="isDragging"/>
+ <body>
+ <![CDATA[
+ if (this.type == "scrubber") {
+ this.Utils.log("--- dragStateChanged: " + isDragging + " ---");
+ this.isDragging = isDragging;
+ if (this.isPausedByDragging && !isDragging) {
+ // After the drag ends, resume playing.
+ this.Utils.video.play();
+ this.isPausedByDragging = false;
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="pauseVideoDuringDragging">
+ <body>
+ <![CDATA[
+ if (this.isDragging &&
+ !this.Utils.video.paused && !this.isPausedByDragging) {
+ this.isPausedByDragging = true;
+ this.Utils.video.pause();
+ }
+ ]]>
+ </body>
+ </method>
+
+ </implementation>
+ </binding>
+
+ <binding id="videoControls">
+
+ <resources>
+ <stylesheet src="chrome://global/content/bindings/videocontrols.css"/>
+ <stylesheet src="chrome://global/skin/media/videocontrols.css"/>
+ </resources>
+
+ <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ class="mediaControlsFrame">
+ <stack flex="1">
+ <vbox flex="1" class="statusOverlay" hidden="true">
+ <box class="statusIcon"/>
+ <label class="errorLabel" anonid="errorAborted">&error.aborted;</label>
+ <label class="errorLabel" anonid="errorNetwork">&error.network;</label>
+ <label class="errorLabel" anonid="errorDecode">&error.decode;</label>
+ <label class="errorLabel" anonid="errorSrcNotSupported">&error.srcNotSupported;</label>
+ <label class="errorLabel" anonid="errorNoSource">&error.noSource2;</label>
+ <label class="errorLabel" anonid="errorGeneric">&error.generic;</label>
+ </vbox>
+
+ <vbox class="controlsOverlay">
+ <stack flex="1">
+ <spacer class="controlsSpacer" flex="1"/>
+ <box class="clickToPlay" hidden="true" flex="1"/>
+ <vbox class="textTrackList" hidden="true" offlabel="&closedCaption.off;"></vbox>
+ </stack>
+ <hbox class="controlBar" hidden="true">
+ <button class="playButton"
+ playlabel="&playButton.playLabel;"
+ pauselabel="&playButton.pauseLabel;"/>
+ <stack class="scrubberStack" flex="1">
+ <box class="backgroundBar"/>
+ <progressmeter class="bufferBar"/>
+ <progressmeter class="progressBar" max="10000"/>
+ <scale class="scrubber" movetoclick="true"/>
+ </stack>
+ <vbox class="durationBox">
+ <label class="positionLabel" role="presentation"/>
+ <label class="durationLabel" role="presentation"/>
+ </vbox>
+ <button class="muteButton"
+ mutelabel="&muteButton.muteLabel;"
+ unmutelabel="&muteButton.unmuteLabel;"/>
+ <stack class="volumeStack">
+ <box class="volumeBackground"/>
+ <box class="volumeForeground" anonid="volumeForeground"/>
+ <scale class="volumeControl" movetoclick="true"/>
+ </stack>
+ <button class="closedCaptionButton"/>
+ <button class="fullscreenButton"
+ enterfullscreenlabel="&fullscreenButton.enterfullscreenlabel;"
+ exitfullscreenlabel="&fullscreenButton.exitfullscreenlabel;"/>
+ </hbox>
+ </vbox>
+ </stack>
+ </xbl:content>
+
+ <implementation>
+
+ <constructor>
+ <![CDATA[
+ this.isTouchControl = false;
+ this.randomID = 0;
+
+ this.Utils = {
+ debug : false,
+ video : null,
+ videocontrols : null,
+ controlBar : null,
+ playButton : null,
+ muteButton : null,
+ volumeControl : null,
+ durationLabel : null,
+ positionLabel : null,
+ scrubberThumb : null,
+ scrubber : null,
+ progressBar : null,
+ bufferBar : null,
+ statusOverlay : null,
+ controlsSpacer : null,
+ clickToPlay : null,
+ controlsOverlay : null,
+ fullscreenButton : null,
+ currentTextTrackIndex: 0,
+
+ textTracksCount: 0,
+ randomID : 0,
+ videoEvents : ["play", "pause", "ended", "volumechange", "loadeddata",
+ "loadstart", "timeupdate", "progress",
+ "playing", "waiting", "canplay", "canplaythrough",
+ "seeking", "seeked", "emptied", "loadedmetadata",
+ "error", "suspend", "stalled",
+ "mozinterruptbegin", "mozinterruptend" ],
+
+ firstFrameShown : false,
+ timeUpdateCount : 0,
+ maxCurrentTimeSeen : 0,
+ _isAudioOnly : false,
+ get isAudioOnly() { return this._isAudioOnly; },
+ set isAudioOnly(val) {
+ this._isAudioOnly = val;
+ this.setFullscreenButtonState();
+
+ if (!this.isTopLevelSyntheticDocument)
+ return;
+ if (this._isAudioOnly) {
+ this.video.style.height = this._controlBarHeight + "px";
+ this.video.style.width = "66%";
+ } else {
+ this.video.style.removeProperty("height");
+ this.video.style.removeProperty("width");
+ }
+ },
+ suppressError : false,
+
+ setupStatusFader : function(immediate) {
+ // Since the play button will be showing, we don't want to
+ // show the throbber behind it. The throbber here will
+ // only show if needed after the play button has been pressed.
+ if (!this.clickToPlay.hidden) {
+ this.startFadeOut(this.statusOverlay, true);
+ return;
+ }
+
+ var show = false;
+ if (this.video.seeking ||
+ (this.video.error && !this.suppressError) ||
+ this.video.networkState == this.video.NETWORK_NO_SOURCE ||
+ (this.video.networkState == this.video.NETWORK_LOADING &&
+ (this.video.paused || this.video.ended
+ ? this.video.readyState < this.video.HAVE_CURRENT_DATA
+ : this.video.readyState < this.video.HAVE_FUTURE_DATA)) ||
+ (this.timeUpdateCount <= 1 && !this.video.ended &&
+ this.video.readyState < this.video.HAVE_FUTURE_DATA &&
+ this.video.networkState == this.video.NETWORK_LOADING))
+ show = true;
+
+ // Explicitly hide the status fader if this
+ // is audio only until bug 619421 is fixed.
+ if (this.isAudioOnly)
+ show = false;
+
+ this.log("Status overlay: seeking=" + this.video.seeking +
+ " error=" + this.video.error + " readyState=" + this.video.readyState +
+ " paused=" + this.video.paused + " ended=" + this.video.ended +
+ " networkState=" + this.video.networkState +
+ " timeUpdateCount=" + this.timeUpdateCount +
+ " --> " + (show ? "SHOW" : "HIDE"));
+ this.startFade(this.statusOverlay, show, immediate);
+ },
+
+ /*
+ * Set the initial state of the controls. The binding is normally created along
+ * with video element, but could be attached at any point (eg, if the video is
+ * removed from the document and then reinserted). Thus, some one-time events may
+ * have already fired, and so we'll need to explicitly check the initial state.
+ */
+ setupInitialState : function() {
+ this.randomID = Math.random();
+ this.videocontrols.randomID = this.randomID;
+
+ this.setPlayButtonState(this.video.paused);
+
+ this.setFullscreenButtonState();
+
+ var duration = Math.round(this.video.duration * 1000); // in ms
+ var currentTime = Math.round(this.video.currentTime * 1000); // in ms
+ this.log("Initial playback position is at " + currentTime + " of " + duration);
+ // It would be nice to retain maxCurrentTimeSeen, but it would be difficult
+ // to determine if the media source changed while we were detached.
+ this.maxCurrentTimeSeen = currentTime;
+ this.showPosition(currentTime, duration);
+
+ // If we have metadata, check if this is a <video> without
+ // video data, or a video with no audio track.
+ if (this.video.readyState >= this.video.HAVE_METADATA) {
+ if (this.video instanceof HTMLVideoElement &&
+ (this.video.videoWidth == 0 || this.video.videoHeight == 0))
+ this.isAudioOnly = true;
+
+ // We have to check again if the media has audio here,
+ // because of bug 718107: switching to fullscreen may
+ // cause the bindings to detach and reattach, hence
+ // unsetting the attribute.
+ if (!this.isAudioOnly && !this.video.mozHasAudio) {
+ this.muteButton.setAttribute("noAudio", "true");
+ this.muteButton.setAttribute("disabled", "true");
+ }
+ }
+
+ if (this.isAudioOnly)
+ this.clickToPlay.hidden = true;
+
+ // If the first frame hasn't loaded, kick off a throbber fade-in.
+ if (this.video.readyState >= this.video.HAVE_CURRENT_DATA)
+ this.firstFrameShown = true;
+
+ // We can't determine the exact buffering status, but do know if it's
+ // fully loaded. (If it's still loading, it will fire a progress event
+ // and we'll figure out the exact state then.)
+ this.bufferBar.setAttribute("max", 100);
+ if (this.video.readyState >= this.video.HAVE_METADATA)
+ this.showBuffered();
+ else
+ this.bufferBar.setAttribute("value", 0);
+
+ // Set the current status icon.
+ if (this.hasError()) {
+ this.clickToPlay.hidden = true;
+ this.statusIcon.setAttribute("type", "error");
+ this.updateErrorText();
+ this.setupStatusFader(true);
+ }
+
+ // An event handler for |onresize| should be added when bug 227495 is fixed.
+ this.controlBar.hidden = false;
+ this._playButtonWidth = this.playButton.clientWidth;
+ this._durationLabelWidth = this.durationLabel.clientWidth;
+ this._muteButtonWidth = this.muteButton.clientWidth;
+ this._volumeControlWidth = this.volumeControl.clientWidth;
+ this._closedCaptionButtonWidth = this.closedCaptionButton.clientWidth;
+ this._fullscreenButtonWidth = this.fullscreenButton.clientWidth;
+ this._controlBarHeight = this.controlBar.clientHeight;
+ this.controlBar.hidden = true;
+ this.adjustControlSize();
+
+ // Can only update the volume controls once we've computed
+ // _volumeControlWidth, since the volume slider implementation
+ // depends on it.
+ this.updateVolumeControls();
+ },
+
+ setupNewLoadState : function() {
+ // videocontrols.css hides the control bar by default, because if script
+ // is disabled our binding's script is disabled too (bug 449358). Thus,
+ // the controls are broken and we don't want them shown. But if script is
+ // enabled, the code here will run and can explicitly unhide the controls.
+ //
+ // For videos with |autoplay| set, we'll leave the controls initially hidden,
+ // so that they don't get in the way of the playing video. Otherwise we'll
+ // go ahead and reveal the controls now, so they're an obvious user cue.
+ //
+ // (Note: the |controls| attribute is already handled via layout/style/html.css)
+ var shouldShow = !this.dynamicControls ||
+ (this.video.paused &&
+ !(this.video.autoplay && this.video.mozAutoplayEnabled));
+ // Hide the overlay if the video time is non-zero or if an error occurred to workaround bug 718107.
+ this.startFade(this.clickToPlay, shouldShow && !this.isAudioOnly &&
+ this.video.currentTime == 0 && !this.hasError(), true);
+ this.startFade(this.controlBar, shouldShow, true);
+ },
+
+ get dynamicControls() {
+ // Don't fade controls for <audio> elements.
+ var enabled = !this.isAudioOnly;
+
+ // Allow tests to explicitly suppress the fading of controls.
+ if (this.video.hasAttribute("mozNoDynamicControls"))
+ enabled = false;
+
+ // If the video hits an error, suppress controls if it
+ // hasn't managed to do anything else yet.
+ if (!this.firstFrameShown && this.hasError())
+ enabled = false;
+
+ return enabled;
+ },
+
+ updateVolumeControls() {
+ var volume = this.video.muted ? 0 : this.video.volume;
+ var volumePercentage = Math.round(volume * 100);
+ this.updateMuteButtonState();
+ this.volumeControl.value = volumePercentage;
+ this.volumeForeground.style.paddingRight = (1 - volume) * this._volumeControlWidth + "px";
+ },
+
+ handleEvent : function (aEvent) {
+ this.log("Got media event ----> " + aEvent.type);
+
+ // If the binding is detached (or has been replaced by a
+ // newer instance of the binding), nuke our event-listeners.
+ if (this.videocontrols.randomID != this.randomID) {
+ this.terminateEventListeners();
+ return;
+ }
+
+ switch (aEvent.type) {
+ case "play":
+ this.setPlayButtonState(false);
+ this.setupStatusFader();
+ if (!this._triggeredByControls && this.dynamicControls && this.videocontrols.isTouchControl)
+ this.startFadeOut(this.controlBar);
+ if (!this._triggeredByControls)
+ this.clickToPlay.hidden = true;
+ this._triggeredByControls = false;
+ break;
+ case "pause":
+ // Little white lie: if we've internally paused the video
+ // while dragging the scrubber, don't change the button state.
+ if (!this.scrubber.isDragging)
+ this.setPlayButtonState(true);
+ this.setupStatusFader();
+ break;
+ case "ended":
+ this.setPlayButtonState(true);
+ // We throttle timechange events, so the thumb might not be
+ // exactly at the end when the video finishes.
+ this.showPosition(Math.round(this.video.currentTime * 1000),
+ Math.round(this.video.duration * 1000));
+ this.startFadeIn(this.controlBar);
+ this.setupStatusFader();
+ break;
+ case "volumechange":
+ this.updateVolumeControls();
+ // Show the controls to highlight the changing volume,
+ // but only if the click-to-play overlay has already
+ // been hidden (we don't hide controls when the overlay is visible).
+ if (this.clickToPlay.hidden && !this.isAudioOnly) {
+ this.startFadeIn(this.controlBar);
+ clearTimeout(this._hideControlsTimeout);
+ this._hideControlsTimeout = setTimeout(this._hideControlsFn, this.HIDE_CONTROLS_TIMEOUT_MS);
+ }
+ break;
+ case "loadedmetadata":
+ this.adjustControlSize();
+ // If a <video> doesn't have any video data, treat it as <audio>
+ // and show the controls (they won't fade back out)
+ if (this.video instanceof HTMLVideoElement &&
+ (this.video.videoWidth == 0 || this.video.videoHeight == 0)) {
+ this.isAudioOnly = true;
+ this.clickToPlay.hidden = true;
+ this.startFadeIn(this.controlBar);
+ this.setFullscreenButtonState();
+ }
+ this.showDuration(Math.round(this.video.duration * 1000));
+ if (!this.isAudioOnly && !this.video.mozHasAudio) {
+ this.muteButton.setAttribute("noAudio", "true");
+ this.muteButton.setAttribute("disabled", "true");
+ }
+ break;
+ case "loadeddata":
+ this.firstFrameShown = true;
+ this.setupStatusFader();
+ break;
+ case "loadstart":
+ this.maxCurrentTimeSeen = 0;
+ this.controlsSpacer.removeAttribute("aria-label");
+ this.statusOverlay.removeAttribute("error");
+ this.statusIcon.setAttribute("type", "throbber");
+ this.isAudioOnly = (this.video instanceof HTMLAudioElement);
+ this.setPlayButtonState(true);
+ this.setupNewLoadState();
+ this.setupStatusFader();
+ break;
+ case "progress":
+ this.statusIcon.removeAttribute("stalled");
+ this.showBuffered();
+ this.setupStatusFader();
+ break;
+ case "stalled":
+ this.statusIcon.setAttribute("stalled", "true");
+ this.statusIcon.setAttribute("type", "throbber");
+ this.setupStatusFader();
+ break;
+ case "suspend":
+ this.setupStatusFader();
+ break;
+ case "timeupdate":
+ var currentTime = Math.round(this.video.currentTime * 1000); // in ms
+ var duration = Math.round(this.video.duration * 1000); // in ms
+
+ // If playing/seeking after the video ended, we won't get a "play"
+ // event, so update the button state here.
+ if (!this.video.paused)
+ this.setPlayButtonState(false);
+
+ this.timeUpdateCount++;
+ // Whether we show the statusOverlay sometimes depends
+ // on whether we've seen more than one timeupdate
+ // event (if we haven't, there hasn't been any
+ // "playback activity" and we may wish to show the
+ // statusOverlay while we wait for HAVE_ENOUGH_DATA).
+ // If we've seen more than 2 timeupdate events,
+ // the count is no longer relevant to setupStatusFader.
+ if (this.timeUpdateCount <= 2)
+ this.setupStatusFader();
+
+ // If the user is dragging the scrubber ignore the delayed seek
+ // responses (don't yank the thumb away from the user)
+ if (this.scrubber.isDragging)
+ return;
+
+ this.showPosition(currentTime, duration);
+ break;
+ case "emptied":
+ this.bufferBar.value = 0;
+ this.showPosition(0, 0);
+ break;
+ case "seeking":
+ this.showBuffered();
+ this.statusIcon.setAttribute("type", "throbber");
+ this.setupStatusFader();
+ break;
+ case "waiting":
+ this.statusIcon.setAttribute("type", "throbber");
+ this.setupStatusFader();
+ break;
+ case "seeked":
+ case "playing":
+ case "canplay":
+ case "canplaythrough":
+ this.setupStatusFader();
+ break;
+ case "error":
+ // We'll show the error status icon when we receive an error event
+ // under either of the following conditions:
+ // 1. The video has its error attribute set; this means we're loading
+ // from our src attribute, and the load failed, or we we're loading
+ // from source children and the decode or playback failed after we
+ // determined our selected resource was playable.
+ // 2. The video's networkState is NETWORK_NO_SOURCE. This means we we're
+ // loading from child source elements, but we were unable to select
+ // any of the child elements for playback during resource selection.
+ if (this.hasError()) {
+ this.suppressError = false;
+ this.clickToPlay.hidden = true;
+ this.statusIcon.setAttribute("type", "error");
+ this.updateErrorText();
+ this.setupStatusFader(true);
+ // If video hasn't shown anything yet, disable the controls.
+ if (!this.firstFrameShown)
+ this.startFadeOut(this.controlBar);
+ this.controlsSpacer.removeAttribute("hideCursor");
+ }
+ break;
+ case "mozinterruptbegin":
+ case "mozinterruptend":
+ // Nothing to do...
+ break;
+ default:
+ this.log("!!! event " + aEvent.type + " not handled!");
+ }
+ },
+
+ terminateEventListeners : function () {
+ if (this.videoEvents) {
+ for (let event of this.videoEvents) {
+ this.video.removeEventListener(event, this, {
+ capture: true,
+ mozSystemGroup: true
+ });
+ }
+ }
+
+ if (this.controlListeners) {
+ for (let element of this.controlListeners) {
+ element.item.removeEventListener(element.event, element.func,
+ { mozSystemGroup: true });
+ }
+
+ delete this.controlListeners;
+ }
+
+ this.log("--- videocontrols terminated ---");
+ },
+
+ hasError : function () {
+ // We either have an explicit error, or the resource selection
+ // algorithm is running and we've tried to load something and failed.
+ // Note: we don't consider the case where we've tried to load but
+ // there's no sources to load as an error condition, as sites may
+ // do this intentionally to work around requires-user-interaction to
+ // play restrictions, and we don't want to display a debug message
+ // if that's the case.
+ return this.video.error != null ||
+ (this.video.networkState == this.video.NETWORK_NO_SOURCE &&
+ this.hasSources());
+ },
+
+ hasSources : function() {
+ if (this.video.hasAttribute('src') &&
+ this.video.getAttribute('src') !== "") {
+ return true;
+ }
+ for (var child = this.video.firstChild;
+ child !== null;
+ child = child.nextElementSibling) {
+ if (child instanceof HTMLSourceElement) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ updateErrorText : function () {
+ let error;
+ let v = this.video;
+ // It is possible to have both v.networkState == NETWORK_NO_SOURCE
+ // as well as v.error being non-null. In this case, we will show
+ // the v.error.code instead of the v.networkState error.
+ if (v.error) {
+ switch (v.error.code) {
+ case v.error.MEDIA_ERR_ABORTED:
+ error = "errorAborted";
+ break;
+ case v.error.MEDIA_ERR_NETWORK:
+ error = "errorNetwork";
+ break;
+ case v.error.MEDIA_ERR_DECODE:
+ error = "errorDecode";
+ break;
+ case v.error.MEDIA_ERR_SRC_NOT_SUPPORTED:
+ error = "errorSrcNotSupported";
+ break;
+ default:
+ error = "errorGeneric";
+ break;
+ }
+ } else if (v.networkState == v.NETWORK_NO_SOURCE) {
+ error = "errorNoSource";
+ } else {
+ return; // No error found.
+ }
+
+ let label = document.getAnonymousElementByAttribute(this.videocontrols, "anonid", error);
+ this.controlsSpacer.setAttribute("aria-label", label.textContent);
+ this.statusOverlay.setAttribute("error", error);
+ },
+
+ formatTime : function(aTime) {
+ // Format the duration as "h:mm:ss" or "m:ss"
+ aTime = Math.round(aTime / 1000);
+ let hours = Math.floor(aTime / 3600);
+ let mins = Math.floor((aTime % 3600) / 60);
+ let secs = Math.floor(aTime % 60);
+ let timeString;
+ if (secs < 10)
+ secs = "0" + secs;
+ if (hours) {
+ if (mins < 10)
+ mins = "0" + mins;
+ timeString = hours + ":" + mins + ":" + secs;
+ } else {
+ timeString = mins + ":" + secs;
+ }
+ return timeString;
+ },
+
+ showDuration : function (duration) {
+ let isInfinite = (duration == Infinity);
+ this.log("Duration is " + duration + "ms.\n");
+
+ if (isNaN(duration) || isInfinite)
+ duration = this.maxCurrentTimeSeen;
+
+ // Format the duration as "h:mm:ss" or "m:ss"
+ let timeString = isInfinite ? "" : this.formatTime(duration);
+ this.durationLabel.setAttribute("value", timeString);
+
+ // "durationValue" property is used by scale binding to
+ // generate accessible name.
+ this.scrubber.durationValue = timeString;
+
+ // If the duration is over an hour, thumb should show h:mm:ss instead of mm:ss
+ this.scrubberThumb.showHours = (duration >= 3600000);
+
+ this.scrubber.max = duration;
+ // XXX Can't set increment here, due to bug 473103. Also, doing so causes
+ // snapping when dragging with the mouse, so we can't just set a value for
+ // the arrow-keys.
+ this.scrubber.pageIncrement = Math.round(duration / 10);
+ },
+
+ seekToPosition : function(newPosition) {
+ newPosition /= 1000; // convert from ms
+ this.log("+++ seeking to " + newPosition);
+ if (this.videocontrols.isGonk) {
+ // We use fastSeek() on B2G, and an accurate (but slower)
+ // seek on other platforms (that are likely to be higher
+ // perf).
+ this.video.fastSeek(newPosition);
+ } else {
+ this.video.currentTime = newPosition;
+ }
+ },
+
+ setVolume : function(newVolume) {
+ this.log("*** setting volume to " + newVolume);
+ this.video.volume = newVolume;
+ this.video.muted = false;
+ },
+
+ showPosition : function(currentTime, duration) {
+ // If the duration is unknown (because the server didn't provide
+ // it, or the video is a stream), then we want to fudge the duration
+ // by using the maximum playback position that's been seen.
+ if (currentTime > this.maxCurrentTimeSeen)
+ this.maxCurrentTimeSeen = currentTime;
+ this.showDuration(duration);
+
+ this.log("time update @ " + currentTime + "ms of " + duration + "ms");
+
+ this.positionLabel.setAttribute("value", this.formatTime(currentTime));
+ this.scrubber.value = currentTime;
+ },
+
+ showBuffered : function() {
+ function bsearch(haystack, needle, cmp) {
+ var length = haystack.length;
+ var low = 0;
+ var high = length;
+ while (low < high) {
+ var probe = low + ((high - low) >> 1);
+ var r = cmp(haystack, probe, needle);
+ if (r == 0) {
+ return probe;
+ } else if (r > 0) {
+ low = probe + 1;
+ } else {
+ high = probe;
+ }
+ }
+ return -1;
+ }
+
+ function bufferedCompare(buffered, i, time) {
+ if (time > buffered.end(i)) {
+ return 1;
+ } else if (time >= buffered.start(i)) {
+ return 0;
+ }
+ return -1;
+ }
+
+ var duration = Math.round(this.video.duration * 1000);
+ if (isNaN(duration))
+ duration = this.maxCurrentTimeSeen;
+
+ // Find the range that the current play position is in and use that
+ // range for bufferBar. At some point we may support multiple ranges
+ // displayed in the bar.
+ var currentTime = this.video.currentTime;
+ var buffered = this.video.buffered;
+ var index = bsearch(buffered, currentTime, bufferedCompare);
+ var endTime = 0;
+ if (index >= 0) {
+ endTime = Math.round(buffered.end(index) * 1000);
+ }
+ this.bufferBar.max = duration;
+ this.bufferBar.value = endTime;
+ },
+
+ _controlsHiddenByTimeout : false,
+ _showControlsTimeout : 0,
+ SHOW_CONTROLS_TIMEOUT_MS: 500,
+ _showControlsFn : function () {
+ if (Utils.video.matches("video:hover")) {
+ Utils.startFadeIn(Utils.controlBar, false);
+ Utils._showControlsTimeout = 0;
+ Utils._controlsHiddenByTimeout = false;
+ }
+ },
+
+ _hideControlsTimeout : 0,
+ _hideControlsFn : function () {
+ if (!Utils.scrubber.isDragging) {
+ Utils.startFade(Utils.controlBar, false);
+ Utils._hideControlsTimeout = 0;
+ Utils._controlsHiddenByTimeout = true;
+ }
+ },
+ HIDE_CONTROLS_TIMEOUT_MS : 2000,
+ onMouseMove : function (event) {
+ // Pause playing video when the mouse is dragging over the control bar.
+ if (this.scrubber.isDragging) {
+ this.scrubber.pauseVideoDuringDragging();
+ }
+
+ // If the controls are static, don't change anything.
+ if (!this.dynamicControls)
+ return;
+
+ clearTimeout(this._hideControlsTimeout);
+
+ // Suppress fading out the controls until the video has rendered
+ // its first frame. But since autoplay videos start off with no
+ // controls, let them fade-out so the controls don't get stuck on.
+ if (!this.firstFrameShown &&
+ !(this.video.autoplay && this.video.mozAutoplayEnabled))
+ return;
+
+ if (this._controlsHiddenByTimeout)
+ this._showControlsTimeout = setTimeout(this._showControlsFn, this.SHOW_CONTROLS_TIMEOUT_MS);
+ else
+ this.startFade(this.controlBar, true);
+
+ // Hide the controls if the mouse cursor is left on top of the video
+ // but above the control bar and if the click-to-play overlay is hidden.
+ if ((this._controlsHiddenByTimeout ||
+ event.clientY < this.controlBar.getBoundingClientRect().top) &&
+ this.clickToPlay.hidden) {
+ this._hideControlsTimeout = setTimeout(this._hideControlsFn, this.HIDE_CONTROLS_TIMEOUT_MS);
+ }
+ },
+
+ onMouseInOut : function (event) {
+ // If the controls are static, don't change anything.
+ if (!this.dynamicControls)
+ return;
+
+ clearTimeout(this._hideControlsTimeout);
+
+ // Ignore events caused by transitions between child nodes.
+ // Note that the videocontrols element is the same
+ // size as the *content area* of the video element,
+ // but this is not the same as the video element's
+ // border area if the video has border or padding.
+ if (this.isEventWithin(event, this.videocontrols))
+ return;
+
+ var isMouseOver = (event.type == "mouseover");
+
+ var controlRect = this.controlBar.getBoundingClientRect();
+ var isMouseInControls = event.clientY > controlRect.top &&
+ event.clientY < controlRect.bottom &&
+ event.clientX > controlRect.left &&
+ event.clientX < controlRect.right;
+
+ // Suppress fading out the controls until the video has rendered
+ // its first frame. But since autoplay videos start off with no
+ // controls, let them fade-out so the controls don't get stuck on.
+ if (!this.firstFrameShown && !isMouseOver &&
+ !(this.video.autoplay && this.video.mozAutoplayEnabled))
+ return;
+
+ if (!isMouseOver && !isMouseInControls) {
+ this.adjustControlSize();
+
+ // Keep the controls visible if the click-to-play is visible.
+ if (!this.clickToPlay.hidden)
+ return;
+
+ this.startFadeOut(this.controlBar, false);
+ this.textTrackList.setAttribute("hidden", "true");
+ clearTimeout(this._showControlsTimeout);
+ Utils._controlsHiddenByTimeout = false;
+ }
+ },
+
+ startFadeIn : function (element, immediate) {
+ this.startFade(element, true, immediate);
+ },
+
+ startFadeOut : function (element, immediate) {
+ this.startFade(element, false, immediate);
+ },
+
+ startFade : function (element, fadeIn, immediate) {
+ if (element.classList.contains("controlBar") && fadeIn) {
+ // Bug 493523, the scrubber doesn't call valueChanged while hidden,
+ // so our dependent state (eg, timestamp in the thumb) will be stale.
+ // As a workaround, update it manually when it first becomes unhidden.
+ if (element.hidden)
+ this.scrubber.valueChanged("curpos", this.video.currentTime * 1000, false);
+ }
+
+ if (immediate)
+ element.setAttribute("immediate", true);
+ else
+ element.removeAttribute("immediate");
+
+ if (fadeIn) {
+ element.hidden = false;
+ // force style resolution, so that transition begins
+ // when we remove the attribute.
+ element.clientTop;
+ element.removeAttribute("fadeout");
+ if (element.classList.contains("controlBar"))
+ this.controlsSpacer.removeAttribute("hideCursor");
+ } else {
+ element.setAttribute("fadeout", true);
+ if (element.classList.contains("controlBar") && !this.hasError() &&
+ document.mozFullScreenElement == this.video)
+ this.controlsSpacer.setAttribute("hideCursor", true);
+
+ }
+ },
+
+ onTransitionEnd : function (event) {
+ // Ignore events for things other than opacity changes.
+ if (event.propertyName != "opacity")
+ return;
+
+ var element = event.originalTarget;
+
+ // Nothing to do when a fade *in* finishes.
+ if (!element.hasAttribute("fadeout"))
+ return;
+
+ this.scrubber.dragStateChanged(false);
+ element.hidden = true;
+ },
+
+ _triggeredByControls: false,
+
+ startPlay : function () {
+ this._triggeredByControls = true;
+ this.hideClickToPlay();
+ this.video.play();
+ },
+
+ togglePause : function () {
+ if (this.video.paused || this.video.ended) {
+ this.startPlay();
+ } else {
+ this.video.pause();
+ }
+
+ // We'll handle style changes in the event listener for
+ // the "play" and "pause" events, same as if content
+ // script was controlling video playback.
+ },
+
+ isVideoWithoutAudioTrack : function() {
+ return this.video.readyState >= this.video.HAVE_METADATA &&
+ !this.isAudioOnly &&
+ !this.video.mozHasAudio;
+ },
+
+ toggleMute : function () {
+ if (this.isVideoWithoutAudioTrack()) {
+ return;
+ }
+ this.video.muted = !this.isEffectivelyMuted();
+ if (this.video.volume === 0) {
+ this.video.volume = 0.5;
+ }
+
+ // We'll handle style changes in the event listener for
+ // the "volumechange" event, same as if content script was
+ // controlling volume.
+ },
+
+ isVideoInFullScreen : function () {
+ return document.mozFullScreenElement == this.video;
+ },
+
+ toggleFullscreen : function () {
+ this.isVideoInFullScreen() ?
+ document.mozCancelFullScreen() :
+ this.video.mozRequestFullScreen();
+ },
+
+ setFullscreenButtonState : function () {
+ if (this.isAudioOnly || !document.mozFullScreenEnabled) {
+ this.controlBar.setAttribute("fullscreen-unavailable", true);
+ this.adjustControlSize();
+ return;
+ }
+ this.controlBar.removeAttribute("fullscreen-unavailable");
+ this.adjustControlSize();
+
+ var attrName = this.isVideoInFullScreen() ? "exitfullscreenlabel" : "enterfullscreenlabel";
+ var value = this.fullscreenButton.getAttribute(attrName);
+ this.fullscreenButton.setAttribute("aria-label", value);
+
+ if (this.isVideoInFullScreen())
+ this.fullscreenButton.setAttribute("fullscreened", "true");
+ else
+ this.fullscreenButton.removeAttribute("fullscreened");
+ },
+
+ onFullscreenChange: function () {
+ if (this.isVideoInFullScreen()) {
+ Utils._hideControlsTimeout = setTimeout(this._hideControlsFn, this.HIDE_CONTROLS_TIMEOUT_MS);
+ }
+ this.setFullscreenButtonState();
+ },
+
+ clickToPlayClickHandler : function(e) {
+ if (e.button != 0)
+ return;
+ if (this.hasError() && !this.suppressError) {
+ // Errors that can be dismissed should be placed here as we discover them.
+ if (this.video.error.code != this.video.error.MEDIA_ERR_ABORTED)
+ return;
+ this.statusOverlay.hidden = true;
+ this.suppressError = true;
+ return;
+ }
+ if (e.defaultPrevented)
+ return;
+ if (this.playButton.hasAttribute("paused")) {
+ this.startPlay();
+ } else {
+ this.video.pause();
+ }
+ },
+ hideClickToPlay : function () {
+ let videoHeight = this.video.clientHeight;
+ let videoWidth = this.video.clientWidth;
+
+ // The play button will animate to 3x its size. This
+ // shows the animation unless the video is too small
+ // to show 2/3 of the animation.
+ let animationScale = 2;
+ if (this._overlayPlayButtonHeight * animationScale > (videoHeight - this._controlBarHeight)||
+ this._overlayPlayButtonWidth * animationScale > videoWidth) {
+ this.clickToPlay.setAttribute("immediate", "true");
+ this.clickToPlay.hidden = true;
+ } else {
+ this.clickToPlay.removeAttribute("immediate");
+ }
+ this.clickToPlay.setAttribute("fadeout", "true");
+ },
+
+ setPlayButtonState : function(aPaused) {
+ if (aPaused)
+ this.playButton.setAttribute("paused", "true");
+ else
+ this.playButton.removeAttribute("paused");
+
+ var attrName = aPaused ? "playlabel" : "pauselabel";
+ var value = this.playButton.getAttribute(attrName);
+ this.playButton.setAttribute("aria-label", value);
+ },
+
+ isEffectivelyMuted : function() {
+ return this.video.muted || !this.video.volume;
+ },
+
+ updateMuteButtonState : function() {
+ var muted = this.isEffectivelyMuted();
+
+ if (muted)
+ this.muteButton.setAttribute("muted", "true");
+ else
+ this.muteButton.removeAttribute("muted");
+
+ var attrName = muted ? "unmutelabel" : "mutelabel";
+ var value = this.muteButton.getAttribute(attrName);
+ this.muteButton.setAttribute("aria-label", value);
+ },
+
+ _getComputedPropertyValueAsInt : function(element, property) {
+ let value = getComputedStyle(element, null).getPropertyValue(property);
+ return parseInt(value, 10);
+ },
+
+ keyHandler : function(event) {
+ // Ignore keys when content might be providing its own.
+ if (!this.video.hasAttribute("controls"))
+ return;
+
+ var keystroke = "";
+ if (event.altKey)
+ keystroke += "alt-";
+ if (event.shiftKey)
+ keystroke += "shift-";
+ if (navigator.platform.startsWith("Mac")) {
+ if (event.metaKey)
+ keystroke += "accel-";
+ if (event.ctrlKey)
+ keystroke += "control-";
+ } else {
+ if (event.metaKey)
+ keystroke += "meta-";
+ if (event.ctrlKey)
+ keystroke += "accel-";
+ }
+ switch (event.keyCode) {
+ case KeyEvent.DOM_VK_UP:
+ keystroke += "upArrow";
+ break;
+ case KeyEvent.DOM_VK_DOWN:
+ keystroke += "downArrow";
+ break;
+ case KeyEvent.DOM_VK_LEFT:
+ keystroke += "leftArrow";
+ break;
+ case KeyEvent.DOM_VK_RIGHT:
+ keystroke += "rightArrow";
+ break;
+ case KeyEvent.DOM_VK_HOME:
+ keystroke += "home";
+ break;
+ case KeyEvent.DOM_VK_END:
+ keystroke += "end";
+ break;
+ }
+
+ if (String.fromCharCode(event.charCode) == ' ')
+ keystroke += "space";
+
+ this.log("Got keystroke: " + keystroke);
+ var oldval, newval;
+
+ try {
+ switch (keystroke) {
+ case "space": /* Play */
+ this.togglePause();
+ break;
+ case "downArrow": /* Volume decrease */
+ oldval = this.video.volume;
+ this.video.volume = (oldval < 0.1 ? 0 : oldval - 0.1);
+ this.video.muted = false;
+ break;
+ case "upArrow": /* Volume increase */
+ oldval = this.video.volume;
+ this.video.volume = (oldval > 0.9 ? 1 : oldval + 0.1);
+ this.video.muted = false;
+ break;
+ case "accel-downArrow": /* Mute */
+ this.video.muted = true;
+ break;
+ case "accel-upArrow": /* Unmute */
+ this.video.muted = false;
+ break;
+ case "leftArrow": /* Seek back 15 seconds */
+ case "accel-leftArrow": /* Seek back 10% */
+ oldval = this.video.currentTime;
+ if (keystroke == "leftArrow")
+ newval = oldval - 15;
+ else
+ newval = oldval - (this.video.duration || this.maxCurrentTimeSeen / 1000) / 10;
+ this.video.currentTime = (newval >= 0 ? newval : 0);
+ break;
+ case "rightArrow": /* Seek forward 15 seconds */
+ case "accel-rightArrow": /* Seek forward 10% */
+ oldval = this.video.currentTime;
+ var maxtime = (this.video.duration || this.maxCurrentTimeSeen / 1000);
+ if (keystroke == "rightArrow")
+ newval = oldval + 15;
+ else
+ newval = oldval + maxtime / 10;
+ this.video.currentTime = (newval <= maxtime ? newval : maxtime);
+ break;
+ case "home": /* Seek to beginning */
+ this.video.currentTime = 0;
+ break;
+ case "end": /* Seek to end */
+ if (this.video.currentTime != this.video.duration)
+ this.video.currentTime = (this.video.duration || this.maxCurrentTimeSeen / 1000);
+ break;
+ default:
+ return;
+ }
+ } catch (e) { /* ignore any exception from setting .currentTime */ }
+
+ event.preventDefault(); // Prevent page scrolling
+ },
+
+ isSupportedTextTrack : function(textTrack) {
+ return textTrack.kind == "subtitles" ||
+ textTrack.kind == "captions";
+ },
+
+ get overlayableTextTracks() {
+ return Array.prototype.filter.call(this.video.textTracks, this.isSupportedTextTrack);
+ },
+
+ isClosedCaptionOn : function () {
+ for (let tt of this.overlayableTextTracks) {
+ if (tt.mode === "showing") {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ setClosedCaptionButtonState : function () {
+ if (!this.overlayableTextTracks.length || this.videocontrols.isTouchControl) {
+ this.closedCaptionButton.setAttribute("hidden", "true");
+ return;
+ }
+
+ this.closedCaptionButton.removeAttribute("hidden");
+
+ if (this.isClosedCaptionOn()) {
+ this.closedCaptionButton.setAttribute("enabled", "true");
+ } else {
+ this.closedCaptionButton.removeAttribute("enabled");
+ }
+
+ let ttItems = this.textTrackList.childNodes;
+
+ for (let tti of ttItems) {
+ const idx = +tti.getAttribute("index");
+
+ if (idx == this.currentTextTrackIndex) {
+ tti.setAttribute("on", "true");
+ } else {
+ tti.removeAttribute("on");
+ }
+ }
+ },
+
+ addNewTextTrack : function (tt) {
+ if (!this.isSupportedTextTrack(tt)) {
+ return;
+ }
+
+ if (tt.index && tt.index < this.textTracksCount) {
+ // Don't create items for initialized tracks. However, we
+ // still need to care about mode since TextTrackManager would
+ // turn on the first available track automatically.
+ if (tt.mode === "showing") {
+ this.changeTextTrack(tt.index);
+ }
+ return;
+ }
+
+ tt.index = this.textTracksCount++;
+
+ const label = tt.label || "";
+ const ttText = document.createTextNode(label);
+ const ttBtn = document.createElement("button");
+
+ ttBtn.classList.add("textTrackItem");
+ ttBtn.setAttribute("index", tt.index);
+
+ ttBtn.addEventListener("click", function (event) {
+ event.stopPropagation();
+
+ this.changeTextTrack(tt.index);
+ }.bind(this));
+
+ ttBtn.appendChild(ttText);
+
+ this.textTrackList.appendChild(ttBtn);
+
+ if (tt.mode === "showing" && tt.index) {
+ this.changeTextTrack(tt.index);
+ }
+ },
+
+ changeTextTrack : function (index) {
+ for (let tt of this.overlayableTextTracks) {
+ if (tt.index === index) {
+ tt.mode = "showing";
+
+ this.currentTextTrackIndex = tt.index;
+ } else {
+ tt.mode = "disabled";
+ }
+ }
+
+ // should fallback to off
+ if (this.currentTextTrackIndex !== index) {
+ this.currentTextTrackIndex = 0;
+ }
+
+ this.textTrackList.setAttribute("hidden", "true");
+ this.setClosedCaptionButtonState();
+ },
+
+ onControlBarTransitioned : function () {
+ this.textTrackList.setAttribute("hidden", "true");
+ this.video.dispatchEvent(new CustomEvent("controlbarchange"));
+ },
+
+ toggleClosedCaption : function () {
+ if (this.overlayableTextTracks.length === 1) {
+ const lastTTIdx = this.overlayableTextTracks[0].index;
+ this.changeTextTrack(this.isClosedCaptionOn() ? 0 : lastTTIdx);
+ return;
+ }
+
+ if (this.textTrackList.hasAttribute("hidden")) {
+ this.textTrackList.removeAttribute("hidden");
+ } else {
+ this.textTrackList.setAttribute("hidden", "true");
+ }
+
+ let maxButtonWidth = 0;
+
+ for (let tti of this.textTrackList.childNodes) {
+ if (tti.clientWidth > maxButtonWidth) {
+ maxButtonWidth = tti.clientWidth;
+ }
+ }
+
+ if (maxButtonWidth > this.video.clientWidth) {
+ maxButtonWidth = this.video.clientWidth;
+ }
+
+ for (let tti of this.textTrackList.childNodes) {
+ tti.style.width = maxButtonWidth + "px";
+ }
+ },
+
+ onTextTrackAdd : function (trackEvent) {
+ this.addNewTextTrack(trackEvent.track);
+ this.setClosedCaptionButtonState();
+ },
+
+ onTextTrackRemove : function (trackEvent) {
+ const toRemoveIndex = trackEvent.track.index;
+ const ttItems = this.textTrackList.childNodes;
+
+ if (!ttItems) {
+ return;
+ }
+
+ for (let tti of ttItems) {
+ const idx = +tti.getAttribute("index");
+
+ if (idx === toRemoveIndex) {
+ tti.remove();
+ this.textTracksCount--;
+ }
+
+ if (idx === this.currentTextTrackIndex) {
+ this.currentTextTrackIndex = 0;
+
+ this.video.dispatchEvent(new CustomEvent("texttrackchange"));
+ }
+ }
+
+ this.setClosedCaptionButtonState();
+ },
+
+ initTextTracks : function () {
+ // add 'off' button anyway as new text track might be
+ // dynamically added after initialization.
+ const offLabel = this.textTrackList.getAttribute("offlabel");
+
+ this.addNewTextTrack({
+ label: offLabel,
+ kind: "subtitles"
+ });
+
+ for (let tt of this.overlayableTextTracks) {
+ this.addNewTextTrack(tt);
+ }
+
+ this.setClosedCaptionButtonState();
+ },
+
+ isEventWithin : function (event, parent1, parent2) {
+ function isDescendant (node) {
+ while (node) {
+ if (node == parent1 || node == parent2)
+ return true;
+ node = node.parentNode;
+ }
+ return false;
+ }
+ return isDescendant(event.target) && isDescendant(event.relatedTarget);
+ },
+
+ log : function (msg) {
+ if (this.debug)
+ console.log("videoctl: " + msg + "\n");
+ },
+
+ get isTopLevelSyntheticDocument() {
+ let doc = this.video.ownerDocument;
+ let win = doc.defaultView;
+ return doc.mozSyntheticDocument && win === win.top;
+ },
+
+ _playButtonWidth : 0,
+ _durationLabelWidth : 0,
+ _muteButtonWidth : 0,
+ _volumeControlWidth : 0,
+ _closedCaptionButtonWidth : 0,
+ _fullscreenButtonWidth : 0,
+ _controlBarHeight : 0,
+ _overlayPlayButtonHeight : 64,
+ _overlayPlayButtonWidth : 64,
+ _controlBarPaddingEnd: 8,
+ adjustControlSize : function adjustControlSize() {
+ let doc = this.video.ownerDocument;
+
+ // The scrubber has |flex=1|, therefore |minScrubberWidth|
+ // was generated by empirical testing.
+ let minScrubberWidth = 25;
+ let minWidthAllControls = this._playButtonWidth +
+ minScrubberWidth +
+ this._durationLabelWidth +
+ this._muteButtonWidth +
+ this._volumeControlWidth +
+ this._closedCaptionButtonWidth +
+ this._fullscreenButtonWidth;
+
+ let isFullscreenUnavailable = this.controlBar.hasAttribute("fullscreen-unavailable");
+ if (isFullscreenUnavailable) {
+ // When the fullscreen button is hidden we add margin-end to the volume stack.
+ minWidthAllControls -= this._fullscreenButtonWidth - this._controlBarPaddingEnd;
+ }
+
+ let minHeightForControlBar = this._controlBarHeight;
+ let minWidthOnlyPlayPause = this._playButtonWidth + this._muteButtonWidth;
+
+ let isAudioOnly = this.isAudioOnly;
+ let videoHeight = isAudioOnly ? minHeightForControlBar : this.video.clientHeight;
+ let videoWidth = isAudioOnly ? minWidthAllControls : this.video.clientWidth;
+
+ // Adapt the size of the controls to the size of the video
+ if (this.video.readyState >= this.video.HAVE_METADATA) {
+ if (!this.isAudioOnly && this.video.videoWidth && this.video.videoHeight) {
+ var rect = this.video.getBoundingClientRect();
+ var widthRatio = rect.width / this.video.videoWidth;
+ var heightRatio = rect.height / this.video.videoHeight;
+ var width = this.video.videoWidth * Math.min(widthRatio, heightRatio);
+
+ this.controlsOverlay.setAttribute("scaled", true);
+ this.controlsOverlay.style.width = width + "px";
+ this.controlsSpacer.style.width = width + "px";
+ this.controlBar.style.width = width + "px";
+ } else {
+ this.controlsOverlay.removeAttribute("scaled");
+ this.controlsOverlay.style.width = "";
+ this.controlsSpacer.style.width = "";
+ this.controlBar.style.width = "";
+ }
+ }
+
+ if ((this._overlayPlayButtonHeight + this._controlBarHeight) > videoHeight ||
+ this._overlayPlayButtonWidth > videoWidth) {
+ this.clickToPlay.hidden = true;
+ } else if (this.clickToPlay.hidden &&
+ !this.video.played.length &&
+ this.video.paused) {
+ // Check this.video.paused to handle when a video is
+ // playing but hasn't processed any frames yet
+ this.clickToPlay.hidden = false;
+ }
+
+ let size = "normal";
+ if (videoHeight < minHeightForControlBar)
+ size = "hidden";
+ else if (videoWidth < minWidthOnlyPlayPause)
+ size = "hidden";
+ else if (videoWidth < minWidthAllControls)
+ size = "small";
+ this.controlBar.setAttribute("size", size);
+ },
+
+ init : function (binding) {
+ this.video = binding.parentNode;
+ this.videocontrols = binding;
+
+ this.statusIcon = document.getAnonymousElementByAttribute(binding, "class", "statusIcon");
+ this.controlBar = document.getAnonymousElementByAttribute(binding, "class", "controlBar");
+ this.playButton = document.getAnonymousElementByAttribute(binding, "class", "playButton");
+ this.muteButton = document.getAnonymousElementByAttribute(binding, "class", "muteButton");
+ this.volumeControl = document.getAnonymousElementByAttribute(binding, "class", "volumeControl");
+ this.progressBar = document.getAnonymousElementByAttribute(binding, "class", "progressBar");
+ this.bufferBar = document.getAnonymousElementByAttribute(binding, "class", "bufferBar");
+ this.scrubber = document.getAnonymousElementByAttribute(binding, "class", "scrubber");
+ this.scrubberThumb = document.getAnonymousElementByAttribute(this.scrubber, "class", "scale-thumb");
+ this.durationLabel = document.getAnonymousElementByAttribute(binding, "class", "durationLabel");
+ this.positionLabel = document.getAnonymousElementByAttribute(binding, "class", "positionLabel");
+ this.statusOverlay = document.getAnonymousElementByAttribute(binding, "class", "statusOverlay");
+ this.controlsOverlay = document.getAnonymousElementByAttribute(binding, "class", "controlsOverlay");
+ this.controlsSpacer = document.getAnonymousElementByAttribute(binding, "class", "controlsSpacer");
+ this.clickToPlay = document.getAnonymousElementByAttribute(binding, "class", "clickToPlay");
+ this.fullscreenButton = document.getAnonymousElementByAttribute(binding, "class", "fullscreenButton");
+ this.volumeForeground = document.getAnonymousElementByAttribute(binding, "anonid", "volumeForeground");
+ this.closedCaptionButton = document.getAnonymousElementByAttribute(binding, "class", "closedCaptionButton");
+ this.textTrackList = document.getAnonymousElementByAttribute(binding, "class", "textTrackList");
+
+ this.isAudioOnly = (this.video instanceof HTMLAudioElement);
+ this.setupInitialState();
+ this.setupNewLoadState();
+ this.initTextTracks();
+
+ // Use the handleEvent() callback for all media events.
+ // Only the "error" event listener must capture, so that it can trap error
+ // events from <source> children, which don't bubble. But we use capture
+ // for all events in order to simplify the event listener add/remove.
+ for (let event of this.videoEvents) {
+ this.video.addEventListener(event, this, {
+ capture: true,
+ mozSystemGroup: true
+ });
+ }
+
+ var self = this;
+ this.controlListeners = [];
+
+ // Helper function to add an event listener to the given element
+ function addListener(elem, eventName, func) {
+ let boundFunc = func.bind(self);
+ self.controlListeners.push({ item: elem, event: eventName, func: boundFunc });
+ elem.addEventListener(eventName, boundFunc, { mozSystemGroup: true });
+ }
+
+ addListener(this.muteButton, "command", this.toggleMute);
+ addListener(this.closedCaptionButton, "command", this.toggleClosedCaption);
+ addListener(this.playButton, "click", this.clickToPlayClickHandler);
+ addListener(this.fullscreenButton, "command", this.toggleFullscreen);
+ addListener(this.clickToPlay, "click", this.clickToPlayClickHandler);
+ addListener(this.controlsSpacer, "click", this.clickToPlayClickHandler);
+ addListener(this.controlsSpacer, "dblclick", this.toggleFullscreen);
+
+ addListener(this.videocontrols, "resizevideocontrols", this.adjustControlSize);
+ addListener(this.videocontrols, "transitionend", this.onTransitionEnd);
+ addListener(this.video.ownerDocument, "mozfullscreenchange", this.onFullscreenChange);
+ addListener(this.videocontrols, "transitionend", this.onControlBarTransitioned);
+ addListener(this.video.ownerDocument, "fullscreenchange", this.onFullscreenChange);
+ addListener(this.video, "keypress", this.keyHandler);
+ addListener(this.video.textTracks, "addtrack", this.onTextTrackAdd);
+ addListener(this.video.textTracks, "removetrack", this.onTextTrackRemove);
+
+ addListener(this.videocontrols, "dragstart", function(event) {
+ event.preventDefault(); // prevent dragging of controls image (bug 517114)
+ });
+
+ this.log("--- videocontrols initialized ---");
+ }
+ };
+ this.Utils.init(this);
+ ]]>
+ </constructor>
+ <destructor>
+ <![CDATA[
+ this.Utils.terminateEventListeners();
+ // randomID used to be a <field>, which meant that the XBL machinery
+ // undefined the property when the element was unbound. The code in
+ // this file actually depends on this, so now that randomID is an
+ // expando, we need to make sure to explicitly delete it.
+ delete this.randomID;
+ ]]>
+ </destructor>
+
+ </implementation>
+
+ <handlers>
+ <handler event="mouseover">
+ if (!this.isTouchControl)
+ this.Utils.onMouseInOut(event);
+ </handler>
+ <handler event="mouseout">
+ if (!this.isTouchControl)
+ this.Utils.onMouseInOut(event);
+ </handler>
+ <handler event="mousemove">
+ if (!this.isTouchControl)
+ this.Utils.onMouseMove(event);
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="touchControls" extends="chrome://global/content/bindings/videocontrols.xml#videoControls">
+
+ <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="mediaControlsFrame">
+ <stack flex="1">
+ <vbox flex="1" class="statusOverlay" hidden="true">
+ <box class="statusIcon"/>
+ <label class="errorLabel" anonid="errorAborted">&error.aborted;</label>
+ <label class="errorLabel" anonid="errorNetwork">&error.network;</label>
+ <label class="errorLabel" anonid="errorDecode">&error.decode;</label>
+ <label class="errorLabel" anonid="errorSrcNotSupported">&error.srcNotSupported;</label>
+ <label class="errorLabel" anonid="errorNoSource">&error.noSource2;</label>
+ <label class="errorLabel" anonid="errorGeneric">&error.generic;</label>
+ </vbox>
+
+ <vbox class="controlsOverlay">
+ <spacer class="controlsSpacer" flex="1"/>
+ <box flex="1" hidden="true">
+ <box class="clickToPlay" hidden="true" flex="1"/>
+ <vbox class="textTrackList" hidden="true" offlabel="&closedCaption.off;"></vbox>
+ </box>
+ <vbox class="controlBar" hidden="true">
+ <hbox class="buttonsBar">
+ <button class="playButton"
+ playlabel="&playButton.playLabel;"
+ pauselabel="&playButton.pauseLabel;"/>
+ <label class="positionLabel" role="presentation"/>
+ <stack class="scrubberStack">
+ <box class="backgroundBar"/>
+ <progressmeter class="flexibleBar" value="100"/>
+ <progressmeter class="bufferBar"/>
+ <progressmeter class="progressBar" max="10000"/>
+ <scale class="scrubber" movetoclick="true"/>
+ </stack>
+ <label class="durationLabel" role="presentation"/>
+ <button class="muteButton"
+ mutelabel="&muteButton.muteLabel;"
+ unmutelabel="&muteButton.unmuteLabel;"/>
+ <stack class="volumeStack">
+ <box class="volumeBackground"/>
+ <box class="volumeForeground" anonid="volumeForeground"/>
+ <scale class="volumeControl" movetoclick="true"/>
+ </stack>
+ <button class="castingButton" hidden="true"
+ aria-label="&castingButton.castingLabel;"/>
+ <button class="closedCaptionButton" hidden="true"/>
+ <button class="fullscreenButton"
+ enterfullscreenlabel="&fullscreenButton.enterfullscreenlabel;"
+ exitfullscreenlabel="&fullscreenButton.exitfullscreenlabel;"/>
+ </hbox>
+ </vbox>
+ </vbox>
+ </stack>
+ </xbl:content>
+
+ <implementation>
+
+ <constructor>
+ <![CDATA[
+ this.isTouchControl = true;
+ this.TouchUtils = {
+ videocontrols: null,
+ video: null,
+ controlsTimer: null,
+ controlsTimeout: 5000,
+ positionLabel: null,
+ castingButton: null,
+
+ get Utils() {
+ return this.videocontrols.Utils;
+ },
+
+ get visible() {
+ return !this.Utils.controlBar.hasAttribute("fadeout") &&
+ !(this.Utils.controlBar.getAttribute("hidden") == "true");
+ },
+
+ _firstShow: false,
+ get firstShow() { return this._firstShow; },
+ set firstShow(val) {
+ this._firstShow = val;
+ this.Utils.controlBar.setAttribute("firstshow", val);
+ },
+
+ toggleControls: function() {
+ if (!this.Utils.dynamicControls || !this.visible)
+ this.showControls();
+ else
+ this.delayHideControls(0);
+ },
+
+ showControls : function() {
+ if (this.Utils.dynamicControls) {
+ this.Utils.startFadeIn(this.Utils.controlBar);
+ this.delayHideControls(this.controlsTimeout);
+ }
+ },
+
+ clearTimer: function() {
+ if (this.controlsTimer) {
+ clearTimeout(this.controlsTimer);
+ this.controlsTimer = null;
+ }
+ },
+
+ delayHideControls : function(aTimeout) {
+ this.clearTimer();
+ let self = this;
+ this.controlsTimer = setTimeout(function() {
+ self.hideControls();
+ }, aTimeout);
+ },
+
+ hideControls : function() {
+ if (!this.Utils.dynamicControls)
+ return;
+ this.Utils.startFadeOut(this.Utils.controlBar);
+ if (this.firstShow)
+ this.videocontrols.addEventListener("transitionend", this, false);
+ },
+
+ handleEvent : function (aEvent) {
+ if (aEvent.type == "transitionend") {
+ this.firstShow = false;
+ this.videocontrols.removeEventListener("transitionend", this, false);
+ return;
+ }
+
+ if (this.videocontrols.randomID != this.Utils.randomID)
+ this.terminateEventListeners();
+
+ },
+
+ terminateEventListeners : function () {
+ for (var event of this.videoEvents)
+ this.Utils.video.removeEventListener(event, this, false);
+ },
+
+ isVideoCasting : function () {
+ if (this.video.mozIsCasting)
+ return true;
+ return false;
+ },
+
+ updateCasting : function (eventDetail) {
+ let castingData = JSON.parse(eventDetail);
+ if ("allow" in castingData) {
+ this.video.mozAllowCasting = !!castingData.allow;
+ }
+
+ if ("active" in castingData) {
+ this.video.mozIsCasting = !!castingData.active;
+ }
+ this.setCastButtonState();
+ },
+
+ startCasting : function () {
+ this.videocontrols.dispatchEvent(new CustomEvent("VideoBindingCast"));
+ },
+
+ setCastButtonState : function () {
+ if (this.isAudioOnly || !this.video.mozAllowCasting) {
+ this.castingButton.hidden = true;
+ return;
+ }
+
+ if (this.video.mozIsCasting) {
+ this.castingButton.setAttribute("active", "true");
+ } else {
+ this.castingButton.removeAttribute("active");
+ }
+
+ this.castingButton.hidden = false;
+ },
+
+ init : function (binding) {
+ this.videocontrols = binding;
+ this.video = binding.parentNode;
+
+ let self = this;
+ this.Utils.playButton.addEventListener("command", function() {
+ if (!self.video.paused)
+ self.delayHideControls(0);
+ else
+ self.showControls();
+ }, false);
+ this.Utils.scrubber.addEventListener("touchstart", function() {
+ self.clearTimer();
+ }, false);
+ this.Utils.scrubber.addEventListener("touchend", function() {
+ self.delayHideControls(self.controlsTimeout);
+ }, false);
+ this.Utils.muteButton.addEventListener("click", function() { self.delayHideControls(self.controlsTimeout); }, false);
+
+ this.castingButton = document.getAnonymousElementByAttribute(binding, "class", "castingButton");
+ this.castingButton.addEventListener("command", function() {
+ self.startCasting();
+ }, false);
+
+ this.video.addEventListener("media-videoCasting", function (e) {
+ if (!e.isTrusted)
+ return;
+ self.updateCasting(e.detail);
+ }, false, true);
+
+ // The first time the controls appear we want to just display
+ // a play button that does not fade away. The firstShow property
+ // makes that happen. But because of bug 718107 this init() method
+ // may be called again when we switch in or out of fullscreen
+ // mode. So we only set firstShow if we're not autoplaying and
+ // if we are at the beginning of the video and not already playing
+ if (!this.video.autoplay && this.Utils.dynamicControls && this.video.paused &&
+ this.video.currentTime === 0)
+ this.firstShow = true;
+
+ // If the video is not at the start, then we probably just
+ // transitioned into or out of fullscreen mode, and we don't want
+ // the controls to remain visible. this.controlsTimeout is a full
+ // 5s, which feels too long after the transition.
+ if (this.video.currentTime !== 0) {
+ this.delayHideControls(this.Utils.HIDE_CONTROLS_TIMEOUT_MS);
+ }
+ }
+ };
+ this.TouchUtils.init(this);
+ this.dispatchEvent(new CustomEvent("VideoBindingAttached"));
+ ]]>
+ </constructor>
+ <destructor>
+ <![CDATA[
+ // XBL destructors don't appear to be inherited properly, so we need
+ // to do this here in addition to the videoControls destructor. :-(
+ delete this.randomID;
+ ]]>
+ </destructor>
+
+ </implementation>
+
+ <handlers>
+ <handler event="mouseup">
+ if (event.originalTarget.nodeName == "vbox") {
+ if (this.TouchUtils.firstShow)
+ this.Utils.video.play();
+ this.TouchUtils.toggleControls();
+ }
+ </handler>
+ </handlers>
+
+ </binding>
+
+ <binding id="touchControlsGonk" extends="chrome://global/content/bindings/videoControls.xml#touchControls">
+ <implementation>
+ <constructor>
+ this.isGonk = true;
+ </constructor>
+ </implementation>
+ </binding>
+
+ <binding id="noControls">
+
+ <resources>
+ <stylesheet src="chrome://global/content/bindings/videocontrols.css"/>
+ <stylesheet src="chrome://global/skin/media/videocontrols.css"/>
+ </resources>
+
+ <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="mediaControlsFrame">
+ <vbox flex="1" class="statusOverlay" hidden="true">
+ <box flex="1">
+ <box class="clickToPlay" flex="1"/>
+ </box>
+ </vbox>
+ </xbl:content>
+
+ <implementation>
+ <constructor>
+ <![CDATA[
+ this.randomID = 0;
+ this.Utils = {
+ randomID : 0,
+ videoEvents : ["play",
+ "playing"],
+ controlListeners: [],
+ terminateEventListeners : function () {
+ for (let event of this.videoEvents)
+ this.video.removeEventListener(event, this, { mozSystemGroup: true });
+
+ for (let element of this.controlListeners) {
+ element.item.removeEventListener(element.event, element.func,
+ { mozSystemGroup: true });
+ }
+
+ delete this.controlListeners;
+ },
+
+ hasError : function () {
+ return (this.video.error != null || this.video.networkState == this.video.NETWORK_NO_SOURCE);
+ },
+
+ handleEvent : function (aEvent) {
+ // If the binding is detached (or has been replaced by a
+ // newer instance of the binding), nuke our event-listeners.
+ if (this.binding.randomID != this.randomID) {
+ this.terminateEventListeners();
+ return;
+ }
+
+ switch (aEvent.type) {
+ case "play":
+ this.noControlsOverlay.hidden = true;
+ break;
+ case "playing":
+ this.noControlsOverlay.hidden = true;
+ break;
+ }
+ },
+
+ blockedVideoHandler : function () {
+ if (this.binding.randomID != this.randomID) {
+ this.terminateEventListeners();
+ return;
+ } else if (this.hasError()) {
+ this.noControlsOverlay.hidden = true;
+ return;
+ }
+ this.noControlsOverlay.hidden = false;
+ },
+
+ clickToPlayClickHandler : function (e) {
+ if (this.binding.randomID != this.randomID) {
+ this.terminateEventListeners();
+ return;
+ } else if (e.button != 0) {
+ return;
+ }
+
+ this.noControlsOverlay.hidden = true;
+ this.video.play();
+ },
+
+ init : function (binding) {
+ this.binding = binding;
+ this.randomID = Math.random();
+ this.binding.randomID = this.randomID;
+ this.video = binding.parentNode;
+ this.clickToPlay = document.getAnonymousElementByAttribute(binding, "class", "clickToPlay");
+ this.noControlsOverlay = document.getAnonymousElementByAttribute(binding, "class", "statusOverlay");
+
+ let self = this;
+ function addListener(elem, eventName, func) {
+ let boundFunc = func.bind(self);
+ self.controlListeners.push({ item: elem, event: eventName, func: boundFunc });
+ elem.addEventListener(eventName, boundFunc, { mozSystemGroup: true });
+ }
+ addListener(this.clickToPlay, "click", this.clickToPlayClickHandler);
+ addListener(this.video, "MozNoControlsBlockedVideo", this.blockedVideoHandler);
+
+ for (let event of this.videoEvents) {
+ this.video.addEventListener(event, this, { mozSystemGroup: true });
+ }
+
+ if (this.video.autoplay && !this.video.mozAutoplayEnabled) {
+ this.blockedVideoHandler();
+ }
+ }
+ };
+ this.Utils.init(this);
+ this.Utils.video.dispatchEvent(new CustomEvent("MozNoControlsVideoBindingAttached"));
+ ]]>
+ </constructor>
+ <destructor>
+ <![CDATA[
+ this.Utils.terminateEventListeners();
+ // randomID used to be a <field>, which meant that the XBL machinery
+ // undefined the property when the element was unbound. The code in
+ // this file actually depends on this, so now that randomID is an
+ // expando, we need to make sure to explicitly delete it.
+ delete this.randomID;
+ ]]>
+ </destructor>
+ </implementation>
+ </binding>
+
+</bindings>
diff --git a/toolkit/content/widgets/wizard.xml b/toolkit/content/widgets/wizard.xml
new file mode 100644
index 000000000..3a8ec2cfe
--- /dev/null
+++ b/toolkit/content/widgets/wizard.xml
@@ -0,0 +1,607 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<!DOCTYPE bindings [
+ <!ENTITY % wizardDTD SYSTEM "chrome://global/locale/wizard.dtd">
+ %wizardDTD;
+]>
+
+<bindings id="wizardBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="wizard-base">
+ <resources>
+ <stylesheet src="chrome://global/skin/wizard.css"/>
+ </resources>
+ </binding>
+
+ <binding id="wizard" extends="chrome://global/content/bindings/general.xml#root-element">
+ <resources>
+ <stylesheet src="chrome://global/skin/wizard.css"/>
+ </resources>
+ <content>
+ <xul:hbox class="wizard-header" anonid="Header"/>
+
+ <xul:deck class="wizard-page-box" flex="1" anonid="Deck">
+ <children includes="wizardpage"/>
+ </xul:deck>
+ <children/>
+
+ <xul:hbox class="wizard-buttons" anonid="Buttons" xbl:inherits="pagestep,firstpage,lastpage"/>
+ </content>
+
+ <implementation>
+ <property name="title" onget="return document.title;"
+ onset="return document.title = val;"/>
+
+ <property name="canAdvance" onget="return this._canAdvance;"
+ onset="this._nextButton.disabled = !val; return this._canAdvance = val;"/>
+ <property name="canRewind" onget="return this._canRewind;"
+ onset="this._backButton.disabled = !val; return this._canRewind = val;"/>
+
+ <property name="pageStep" readonly="true" onget="return this._pageStack.length"/>
+
+ <field name="pageCount">0</field>
+
+ <field name="_accessMethod">null</field>
+ <field name="_pageStack">null</field>
+ <field name="_currentPage">null</field>
+
+ <property name="wizardPages">
+ <getter>
+ <![CDATA[
+ var xulns = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ return this.getElementsByTagNameNS(xulns, "wizardpage");
+ ]]>
+ </getter>
+ </property>
+
+ <property name="currentPage" onget="return this._currentPage">
+ <setter>
+ <![CDATA[
+ if (!val)
+ return val;
+
+ this._currentPage = val;
+
+ // Setting this attribute allows wizard's clients to dynamically
+ // change the styles of each page based on purpose of the page.
+ this.setAttribute("currentpageid", val.pageid);
+ if (this.onFirstPage) {
+ this.canRewind = false;
+ this.setAttribute("firstpage", "true");
+ if (/Linux/.test(navigator.platform)) {
+ this._backButton.setAttribute('hidden', 'true');
+ }
+ } else {
+ this.canRewind = true;
+ this.setAttribute("firstpage", "false");
+ if (/Linux/.test(navigator.platform)) {
+ this._backButton.setAttribute('hidden', 'false');
+ }
+ }
+
+ if (this.onLastPage) {
+ this.canAdvance = true;
+ this.setAttribute("lastpage", "true");
+ } else {
+ this.setAttribute("lastpage", "false");
+ }
+
+ this._deck.setAttribute("selectedIndex", val.pageIndex);
+ this._advanceFocusToPage(val);
+
+ this._adjustWizardHeader();
+ this._wizardButtons.onPageChange();
+
+ this._fireEvent(val, "pageshow");
+
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="pageIndex"
+ onget="return this._currentPage ? this._currentPage.pageIndex : -1;">
+ <setter>
+ <![CDATA[
+ if (val < 0 || val >= this.pageCount)
+ return val;
+
+ var page = this.wizardPages[val];
+ this._pageStack[this._pageStack.length-1] = page;
+ this.currentPage = page;
+
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="onFirstPage" readonly="true"
+ onget="return this._pageStack.length == 1;"/>
+
+ <property name="onLastPage" readonly="true">
+ <getter><![CDATA[
+ var cp = this.currentPage;
+ return cp && ((this._accessMethod == "sequential" && cp.pageIndex == this.pageCount-1) ||
+ (this._accessMethod == "random" && cp.next == ""));
+ ]]></getter>
+ </property>
+
+ <method name="getButton">
+ <parameter name="aDlgType"/>
+ <body>
+ <![CDATA[
+ var btns = this.getElementsByAttribute("dlgtype", aDlgType);
+ return btns.item(0) ? btns[0] : document.getAnonymousElementByAttribute(this._wizardButtons, "dlgtype", aDlgType);
+ ]]>
+ </body>
+ </method>
+
+ <field name="_canAdvance"/>
+ <field name="_canRewind"/>
+ <field name="_wizardHeader"/>
+ <field name="_wizardButtons"/>
+ <field name="_deck"/>
+ <field name="_backButton"/>
+ <field name="_nextButton"/>
+ <field name="_cancelButton"/>
+
+ <!-- functions to be added as oncommand listeners to the wizard buttons -->
+ <field name="_backFunc">(function() { document.documentElement.rewind(); })</field>
+ <field name="_nextFunc">(function() { document.documentElement.advance(); })</field>
+ <field name="_finishFunc">(function() { document.documentElement.advance(); })</field>
+ <field name="_cancelFunc">(function() { document.documentElement.cancel(); })</field>
+ <field name="_extra1Func">(function() { document.documentElement.extra1(); })</field>
+ <field name="_extra2Func">(function() { document.documentElement.extra2(); })</field>
+
+ <field name="_closeHandler">(function(event) {
+ if (document.documentElement.cancel())
+ event.preventDefault();
+ })</field>
+
+ <constructor><![CDATA[
+ this._canAdvance = true;
+ this._canRewind = false;
+ this._hasLoaded = false;
+
+ this._pageStack = [];
+
+ try {
+ // need to create string bundle manually instead of using <xul:stringbundle/>
+ // see bug 63370 for details
+ this._bundle = Components.classes["@mozilla.org/intl/stringbundle;1"]
+ .getService(Components.interfaces.nsIStringBundleService)
+ .createBundle("chrome://global/locale/wizard.properties");
+ } catch (e) {
+ // This fails in remote XUL, which has to provide titles for all pages
+ // see bug 142502
+ }
+
+ // get anonymous content references
+ this._wizardHeader = document.getAnonymousElementByAttribute(this, "anonid", "Header");
+ this._wizardButtons = document.getAnonymousElementByAttribute(this, "anonid", "Buttons");
+ this._deck = document.getAnonymousElementByAttribute(this, "anonid", "Deck");
+
+ this._initWizardButton("back");
+ this._initWizardButton("next");
+ this._initWizardButton("finish");
+ this._initWizardButton("cancel");
+ this._initWizardButton("extra1");
+ this._initWizardButton("extra2");
+
+ this._initPages();
+
+ window.addEventListener("close", this._closeHandler, false);
+
+ // start off on the first page
+ this.pageCount = this.wizardPages.length;
+ this.advance();
+
+ // give focus to the first focusable element in the dialog
+ window.addEventListener("load", this._setInitialFocus, false);
+ ]]></constructor>
+
+ <method name="getPageById">
+ <parameter name="aPageId"/>
+ <body><![CDATA[
+ var els = this.getElementsByAttribute("pageid", aPageId);
+ return els.item(0);
+ ]]></body>
+ </method>
+
+ <method name="extra1">
+ <body><![CDATA[
+ if (this.currentPage)
+ this._fireEvent(this.currentPage, "extra1");
+ ]]></body>
+ </method>
+
+ <method name="extra2">
+ <body><![CDATA[
+ if (this.currentPage)
+ this._fireEvent(this.currentPage, "extra2");
+ ]]></body>
+ </method>
+
+ <method name="rewind">
+ <body><![CDATA[
+ if (!this.canRewind)
+ return;
+
+ if (this.currentPage && !this._fireEvent(this.currentPage, "pagehide"))
+ return;
+
+ if (this.currentPage && !this._fireEvent(this.currentPage, "pagerewound"))
+ return;
+
+ if (!this._fireEvent(this, "wizardback"))
+ return;
+
+
+ this._pageStack.pop();
+ this.currentPage = this._pageStack[this._pageStack.length-1];
+ this.setAttribute("pagestep", this._pageStack.length);
+ ]]></body>
+ </method>
+
+ <method name="advance">
+ <parameter name="aPageId"/>
+ <body><![CDATA[
+ if (!this.canAdvance)
+ return;
+
+ if (this.currentPage && !this._fireEvent(this.currentPage, "pagehide"))
+ return;
+
+ if (this.currentPage && !this._fireEvent(this.currentPage, "pageadvanced"))
+ return;
+
+ if (this.onLastPage && !aPageId) {
+ if (this._fireEvent(this, "wizardfinish"))
+ window.setTimeout(function() {window.close();}, 1);
+ } else {
+ if (!this._fireEvent(this, "wizardnext"))
+ return;
+
+ var page;
+ if (aPageId)
+ page = this.getPageById(aPageId);
+ else {
+ if (this.currentPage) {
+ if (this._accessMethod == "random")
+ page = this.getPageById(this.currentPage.next);
+ else
+ page = this.wizardPages[this.currentPage.pageIndex+1];
+ } else
+ page = this.wizardPages[0];
+ }
+
+ if (page) {
+ this._pageStack.push(page);
+ this.setAttribute("pagestep", this._pageStack.length);
+
+ this.currentPage = page;
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="goTo">
+ <parameter name="aPageId"/>
+ <body><![CDATA[
+ var page = this.getPageById(aPageId);
+ if (page) {
+ this._pageStack[this._pageStack.length-1] = page;
+ this.currentPage = page;
+ }
+ ]]></body>
+ </method>
+
+ <method name="cancel">
+ <body><![CDATA[
+ if (!this._fireEvent(this, "wizardcancel"))
+ return true;
+
+ window.close();
+ window.setTimeout(function() {window.close();}, 1);
+ return false;
+ ]]></body>
+ </method>
+
+ <method name="_setInitialFocus">
+ <parameter name="aEvent"/>
+ <body>
+ <![CDATA[
+ document.documentElement._hasLoaded = true;
+ var focusInit =
+ function() {
+ // give focus to the first focusable element in the dialog
+ if (!document.commandDispatcher.focusedElement)
+ document.commandDispatcher.advanceFocusIntoSubtree(document.documentElement);
+
+ try {
+ var button =
+ document.documentElement._wizardButtons.defaultButton;
+ if (button)
+ window.notifyDefaultButtonLoaded(button);
+ } catch (e) { }
+ };
+
+ // Give focus after onload completes, see bug 103197.
+ setTimeout(focusInit, 0);
+ ]]>
+ </body>
+ </method>
+
+ <method name="_advanceFocusToPage">
+ <parameter name="aPage"/>
+ <body>
+ <![CDATA[
+ if (!this._hasLoaded)
+ return;
+
+ document.commandDispatcher.advanceFocusIntoSubtree(aPage);
+
+ // if advanceFocusIntoSubtree tries to focus one of our
+ // dialog buttons, then remove it and put it on the root
+ var focused = document.commandDispatcher.focusedElement;
+ if (focused && focused.hasAttribute("dlgtype"))
+ this.focus();
+ ]]>
+ </body>
+ </method>
+
+ <method name="_initPages">
+ <body><![CDATA[
+ var meth = "sequential";
+ var pages = this.wizardPages;
+ for (var i = 0; i < pages.length; ++i) {
+ var page = pages[i];
+ page.pageIndex = i;
+ if (page.next != "")
+ meth = "random";
+ }
+ this._accessMethod = meth;
+ ]]></body>
+ </method>
+
+ <method name="_initWizardButton">
+ <parameter name="aName"/>
+ <body><![CDATA[
+ var btn = document.getAnonymousElementByAttribute(this._wizardButtons, "dlgtype", aName);
+ if (btn) {
+ btn.addEventListener("command", this["_"+aName+"Func"], false);
+ this["_"+aName+"Button"] = btn;
+ }
+ return btn;
+ ]]></body>
+ </method>
+
+ <method name="_adjustWizardHeader">
+ <body><![CDATA[
+ var label = this.currentPage.getAttribute("label");
+ if (!label && this.onFirstPage && this._bundle) {
+ if (/Mac/.test(navigator.platform)) {
+ label = this._bundle.GetStringFromName("default-first-title-mac");
+ } else {
+ label = this._bundle.formatStringFromName("default-first-title", [this.title], 1);
+ }
+ } else if (!label && this.onLastPage && this._bundle) {
+ if (/Mac/.test(navigator.platform)) {
+ label = this._bundle.GetStringFromName("default-last-title-mac");
+ } else {
+ label = this._bundle.formatStringFromName("default-last-title", [this.title], 1);
+ }
+ }
+ this._wizardHeader.setAttribute("label", label);
+ this._wizardHeader.setAttribute("description", this.currentPage.getAttribute("description"));
+ ]]></body>
+ </method>
+
+ <method name="_hitEnter">
+ <parameter name="evt"/>
+ <body>
+ <![CDATA[
+ if (!evt.defaultPrevented)
+ this.advance();
+ ]]>
+ </body>
+ </method>
+
+ <method name="_fireEvent">
+ <parameter name="aTarget"/>
+ <parameter name="aType"/>
+ <body>
+ <![CDATA[
+ var event = document.createEvent("Events");
+ event.initEvent(aType, true, true);
+
+ // handle dom event handlers
+ var noCancel = aTarget.dispatchEvent(event);
+
+ // handle any xml attribute event handlers
+ var handler = aTarget.getAttribute("on"+aType);
+ if (handler != "") {
+ var fn = new Function("event", handler);
+ var returned = fn.apply(aTarget, [event]);
+ if (returned == false)
+ noCancel = false;
+ }
+
+ return noCancel;
+ ]]>
+ </body>
+ </method>
+
+ </implementation>
+
+ <handlers>
+ <handler event="keypress" keycode="VK_RETURN"
+ group="system" action="this._hitEnter(event)"/>
+ <handler event="keypress" keycode="VK_ESCAPE" group="system">
+ if (!event.defaultPrevented)
+ this.cancel();
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="wizardpage" extends="chrome://global/content/bindings/wizard.xml#wizard-base">
+ <implementation>
+ <field name="pageIndex">-1</field>
+
+ <property name="pageid" onget="return this.getAttribute('pageid');"
+ onset="this.setAttribute('pageid', val);"/>
+
+ <property name="next" onget="return this.getAttribute('next');"
+ onset="this.setAttribute('next', val);
+ this.parentNode._accessMethod = 'random';
+ return val;"/>
+ </implementation>
+ </binding>
+
+#ifdef XP_MACOSX
+ <binding id="wizard-header" extends="chrome://global/content/bindings/wizard.xml#wizard-base">
+ <content>
+ <xul:stack class="wizard-header-stack" flex="1">
+ <xul:vbox class="wizard-header-box-1">
+ <xul:vbox class="wizard-header-box-text">
+ <xul:label class="wizard-header-label" xbl:inherits="xbl:text=label"/>
+ </xul:vbox>
+ </xul:vbox>
+ <xul:hbox class="wizard-header-box-icon">
+ <xul:spacer flex="1"/>
+ <xul:image class="wizard-header-icon" xbl:inherits="src=iconsrc"/>
+ </xul:hbox>
+ </xul:stack>
+ </content>
+ </binding>
+
+ <binding id="wizard-buttons" extends="chrome://global/content/bindings/wizard.xml#wizard-base">
+ <content>
+ <xul:vbox flex="1">
+ <xul:hbox class="wizard-buttons-btm">
+ <xul:button class="wizard-button" dlgtype="extra1" hidden="true"/>
+ <xul:button class="wizard-button" dlgtype="extra2" hidden="true"/>
+ <xul:button label="&button-cancel-mac.label;" class="wizard-button" dlgtype="cancel"/>
+ <xul:spacer flex="1"/>
+ <xul:button label="&button-back-mac.label;" accesskey="&button-back-mac.accesskey;"
+ class="wizard-button wizard-nav-button" dlgtype="back"/>
+ <xul:button label="&button-next-mac.label;" accesskey="&button-next-mac.accesskey;"
+ class="wizard-button wizard-nav-button" dlgtype="next"
+ default="true" xbl:inherits="hidden=lastpage" />
+ <xul:button label="&button-finish-mac.label;" class="wizard-button"
+ dlgtype="finish" default="true" xbl:inherits="hidden=hidefinishbutton" />
+ </xul:hbox>
+ </xul:vbox>
+ </content>
+
+ <implementation>
+ <method name="onPageChange">
+ <body><![CDATA[
+ this.setAttribute("hidefinishbutton", !(this.getAttribute("lastpage") == "true"));
+ ]]></body>
+ </method>
+ </implementation>
+
+ </binding>
+
+#else
+
+ <binding id="wizard-header" extends="chrome://global/content/bindings/wizard.xml#wizard-base">
+ <content>
+ <xul:hbox class="wizard-header-box-1" flex="1">
+ <xul:vbox class="wizard-header-box-text" flex="1">
+ <xul:label class="wizard-header-label" xbl:inherits="xbl:text=label"/>
+ <xul:label class="wizard-header-description" xbl:inherits="xbl:text=description"/>
+ </xul:vbox>
+ <xul:image class="wizard-header-icon" xbl:inherits="src=iconsrc"/>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="wizard-buttons" extends="chrome://global/content/bindings/wizard.xml#wizard-base">
+ <content>
+ <xul:vbox class="wizard-buttons-box-1" flex="1">
+ <xul:separator class="wizard-buttons-separator groove"/>
+ <xul:hbox class="wizard-buttons-box-2">
+ <xul:button class="wizard-button" dlgtype="extra1" hidden="true"/>
+ <xul:button class="wizard-button" dlgtype="extra2" hidden="true"/>
+ <xul:spacer flex="1" anonid="spacer"/>
+#ifdef XP_UNIX
+ <xul:button label="&button-cancel-unix.label;" class="wizard-button"
+ dlgtype="cancel" icon="cancel"/>
+ <xul:spacer style="width: 24px"/>
+ <xul:button label="&button-back-unix.label;" accesskey="&button-back-unix.accesskey;"
+ class="wizard-button" dlgtype="back" icon="go-back"/>
+ <xul:deck class="wizard-next-deck" anonid="WizardButtonDeck">
+ <xul:hbox>
+ <xul:button label="&button-finish-unix.label;" class="wizard-button"
+ dlgtype="finish" default="true" flex="1"/>
+ </xul:hbox>
+ <xul:hbox>
+ <xul:button label="&button-next-unix.label;" accesskey="&button-next-unix.accesskey;"
+ class="wizard-button" dlgtype="next" icon="go-forward"
+ default="true" flex="1"/>
+ </xul:hbox>
+ </xul:deck>
+#else
+ <xul:button label="&button-back-win.label;" accesskey="&button-back-win.accesskey;"
+ class="wizard-button" dlgtype="back" icon="go-back"/>
+ <xul:deck class="wizard-next-deck" anonid="WizardButtonDeck">
+ <xul:hbox>
+ <xul:button label="&button-finish-win.label;" class="wizard-button"
+ dlgtype="finish" default="true" flex="1"/>
+ </xul:hbox>
+ <xul:hbox>
+ <xul:button label="&button-next-win.label;" accesskey="&button-next-win.accesskey;"
+ class="wizard-button" dlgtype="next" icon="go-forward"
+ default="true" flex="1"/>
+ </xul:hbox>
+ </xul:deck>
+ <xul:button label="&button-cancel-win.label;" class="wizard-button"
+ dlgtype="cancel" icon="cancel"/>
+#endif
+ </xul:hbox>
+ </xul:vbox>
+ </content>
+
+ <implementation>
+ <field name="_wizardButtonDeck" readonly="true">
+ document.getAnonymousElementByAttribute(this, "anonid", "WizardButtonDeck");
+ </field>
+
+ <method name="onPageChange">
+ <body><![CDATA[
+ if (this.getAttribute("lastpage") == "true") {
+ this._wizardButtonDeck.setAttribute("selectedIndex", 0);
+ } else {
+ this._wizardButtonDeck.setAttribute("selectedIndex", 1);
+ }
+ ]]></body>
+ </method>
+
+ <property name="defaultButton" readonly="true">
+ <getter><![CDATA[
+ const kXULNS =
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ var buttons = this._wizardButtonDeck.selectedPanel
+ .getElementsByTagNameNS(kXULNS, "button");
+ for (var i = 0; i < buttons.length; i++) {
+ if (buttons[i].getAttribute("default") == "true" &&
+ !buttons[i].hidden && !buttons[i].disabled)
+ return buttons[i];
+ }
+ return null;
+ ]]></getter>
+ </property>
+ </implementation>
+ </binding>
+#endif
+
+</bindings>