diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /toolkit/content/widgets | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-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')
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="©Cmd.label;" accesskey="©Cmd.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="©Cmd.label;" accesskey="©Cmd.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> |