diff options
269 files changed, 11250 insertions, 2660 deletions
diff --git a/accessible/tests/mochitest/jsat/test_output.html b/accessible/tests/mochitest/jsat/test_output.html index ec2b289be..525642607 100644 --- a/accessible/tests/mochitest/jsat/test_output.html +++ b/accessible/tests/mochitest/jsat/test_output.html @@ -125,14 +125,6 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=753984 {"string": "listAbbr"}, {"string": "cellInfoAbbr", "args": [ 1, 1]}]] }, { - accOrElmOrID: "date", - expectedUtterance: [[{"string": "textInputType_date"}, - {"string": "entry"}, "2011-09-29"], ["2011-09-29", - {"string": "textInputType_date"}, {"string": "entry"}]], - expectedBraille: [[{"string": "textInputType_date"}, - {"string": "entryAbbr"}, "2011-09-29"], ["2011-09-29", - {"string": "textInputType_date"}, {"string": "entryAbbr"}]] - }, { accOrElmOrID: "email", expectedUtterance: [[{"string": "textInputType_email"}, {"string": "entry"}, "test@example.com"], ["test@example.com", @@ -619,7 +611,6 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=753984 <label for="password">Secret Password</label><input id="password" type="password"></input> <label for="radio_unselected">any old radio button</label><input id="radio_unselected" type="radio"></input> <label for="radio_selected">a unique radio button</label><input id="radio_selected" type="radio" checked></input> - <input id="date" type="date" value="2011-09-29" /> <input id="email" type="email" value="test@example.com" /> <input id="search" type="search" value="This is a search" /> <input id="tel" type="tel" value="555-5555" /> diff --git a/application/palemoon/app/Makefile.in b/application/palemoon/app/Makefile.in index 580fcb164..6f9bbfaf0 100644 --- a/application/palemoon/app/Makefile.in +++ b/application/palemoon/app/Makefile.in @@ -54,14 +54,6 @@ GARBAGE += $(addprefix $(FINAL_TARGET)/defaults/pref/, palemoon.js) endif -ifdef MOZ_WIDGET_GTK -libs:: - $(INSTALL) $(IFLAGS1) $(DIST)/branding/mozicon128.png $(FINAL_TARGET)/icons - $(INSTALL) $(IFLAGS1) $(DIST)/branding/default16.png $(FINAL_TARGET)/chrome/icons/default - $(INSTALL) $(IFLAGS1) $(DIST)/branding/default32.png $(FINAL_TARGET)/chrome/icons/default - $(INSTALL) $(IFLAGS1) $(DIST)/branding/default48.png $(FINAL_TARGET)/chrome/icons/default -endif - ifndef LIBXUL_SDK # channel-prefs.js is handled separate from other prefs due to bug 756325 libs:: $(srcdir)/profile/channel-prefs.js diff --git a/application/palemoon/base/content/abouthome/aboutHome.js b/application/palemoon/base/content/abouthome/aboutHome.js index 9e826b69e..6a970acd6 100644 --- a/application/palemoon/base/content/abouthome/aboutHome.js +++ b/application/palemoon/base/content/abouthome/aboutHome.js @@ -234,8 +234,8 @@ const SEARCH_ENGINES = { }; // This global tracks if the page has been set up before, to prevent double inits -let gInitialized = false; -let gObserver = new MutationObserver(function (mutations) { +var gInitialized = false; +var gObserver = new MutationObserver(function (mutations) { for (let mutation of mutations) { if (mutation.attributeName == "searchEngineURL") { setupSearchEngine(); diff --git a/application/palemoon/base/content/autocomplete.css b/application/palemoon/base/content/autocomplete.css new file mode 100644 index 000000000..960bdc456 --- /dev/null +++ b/application/palemoon/base/content/autocomplete.css @@ -0,0 +1,17 @@ +/* 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"); + +/* Apply crisp rendering for favicons at exactly 2dppx resolution */ +@media (resolution: 2dppx) { + .ac-site-icon { + image-rendering: -moz-crisp-edges; + } +} + +richlistitem > .ac-title-box > .ac-title > .ac-comment:not([selected]) > html|span.ac-selected-text { + display: none; +} diff --git a/application/palemoon/base/content/autocomplete.xml b/application/palemoon/base/content/autocomplete.xml new file mode 100644 index 000000000..3cb80b19d --- /dev/null +++ b/application/palemoon/base/content/autocomplete.xml @@ -0,0 +1,2122 @@ +<?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://browser/content/autocomplete.css"/> + <stylesheet src="chrome://browser/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="mEnterEvent">null</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"><![CDATA[ + // Wrap in a block so that the let statements don't + // create properties on 'this' (bug 635252). + { + 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; + popup; + } + ]]></field> + + <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> + + <field name="PrivateBrowsingUtils" readonly="true"> + { + let utils = {}; + Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm", utils); + utils.PrivateBrowsingUtils + } + </field> + + <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> + + <property name="textValue" + onget="return this.value;"> + <setter><![CDATA[ + // Completing a result should simulate the user typing the result, + // so fire an input event. + // Trim popup selected values, but never trim results coming from + // autofill. + if (this.popup.selectedIndex == -1) + this._disableTrim = true; + this.value = val; + this._disableTrim = false; + + var 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"> + <body><![CDATA[ + let rv = false; + if (this._textEnteredHandler) + rv = this._textEnteredHandler(this.mEnterEvent); + this.mEnterEvent = null; + 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.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; + + let { AppConstants } = + Components.utils.import("resource://gre/modules/AppConstants.jsm", {}); + // 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 = AppConstants.platform == "macosx" ? 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 (AppConstants.platform == "macosx") { + // Prevent the default action, since it will beep on Mac + if (aEvent.metaKey) + aEvent.preventDefault(); + } + this.mEnterEvent = aEvent; + if (this.mController.selection) { + this._selectionDetails = { + index: this.mController.selection.currentIndex, + kind: "key" + }; + } + cancel = this.handleEnter(); + break; + case KeyEvent.DOM_VK_DELETE: + if (AppConstants.platform == "macosx" && !aEvent.shiftKey) { + break; + } + cancel = this.handleDelete(); + break; + case KeyEvent.DOM_VK_BACK_SPACE: + if (AppConstants.platform == "macosx" && 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 (AppConstants.platform != "macosx") { + this.toggleHistoryPopup(); + } + break; + } + + if (cancel) { + aEvent.stopPropagation(); + aEvent.preventDefault(); + } + + return true; + ]]></body> + </method> + + <method name="handleEnter"> + <body><![CDATA[ + return this.mController.handleEnter(false); + ]]></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" + action="this.attachController();"/> + + <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://browser/content/autocomplete.xml#autocomplete-base-popup"> + <resources> + <stylesheet src="chrome://browser/content/autocomplete.css"/> + <stylesheet src="chrome://global/skin/tree.css"/> + <stylesheet src="chrome://browser/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> + + <!-- =================== 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;"/> + + <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); + ]]></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; + + // 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://browser/content/autocomplete.xml#autocomplete-base-popup"> + <resources> + <stylesheet src="chrome://browser/content/autocomplete.css"/> + <stylesheet src="chrome://browser/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="_rowHeight">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; + ]]></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; + // 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); + } + this._adjustHeightTimeout = setTimeout(() => this.adjustHeight(), 0); + + 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) { + if (!this._rowHeight) { + let firstRowRect = rows[0].getBoundingClientRect(); + this._rowHeight = firstRowRect.height; + + let transition = + window.getComputedStyle(this.richlistbox).transitionProperty; + this._rlbAnimated = transition && transition != "none"; + + // Set a fixed max-height to avoid flicker when growing the panel. + this.richlistbox.style.maxHeight = (this._rowHeight * this.maxRows) + "px"; + } + + // Calculate the height to have the first row to last row shown + height = this._rowHeight * numRows; + } + + 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]; + + // 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)) { + item.collapsed = false; + this._currentIndex++; + continue; + } + } + else { + // need to create a new item + item = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "richlistitem"); + } + + // 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("type", 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); + } + + 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> + + <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> + </binding> + + <binding id="autocomplete-richlistitem" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem"> + <content> + <xul:hbox align="center" class="ac-title-box"> + <xul:image xbl:inherits="src=image" class="ac-site-icon"/> + <xul:hbox anonid="title-box" class="ac-title" flex="1" + onunderflow="_doUnderflow('_title');" + onoverflow="_doOverflow('_title');"> + <xul:description anonid="title" class="ac-normal-text ac-comment" xbl:inherits="selected"/> + </xul:hbox> + <xul:label anonid="title-overflow-ellipsis" xbl:inherits="selected" + class="ac-ellipsis-after ac-comment"/> + <xul:hbox anonid="extra-box" class="ac-extra" align="center" hidden="true"> + <xul:image class="ac-result-type-tag"/> + <xul:label class="ac-normal-text ac-comment" xbl:inherits="selected" value=":"/> + <xul:description anonid="extra" class="ac-normal-text ac-comment" xbl:inherits="selected"/> + </xul:hbox> + <xul:image anonid="type-image" class="ac-type-icon" xbl:inherits="selected"/> + </xul:hbox> + <xul:hbox align="center" class="ac-url-box"> + <xul:spacer class="ac-site-icon"/> + <xul:image class="ac-action-icon"/> + <xul:hbox anonid="url-box" class="ac-url" flex="1" + onunderflow="_doUnderflow('_url');" + onoverflow="_doOverflow('_url');"> + <xul:description anonid="url" class="ac-normal-text ac-url-text" + xbl:inherits="selected type"/> + <xul:description anonid="action" class="ac-normal-text ac-action-text" + xbl:inherits="selected type"/> + </xul:hbox> + <xul:label anonid="url-overflow-ellipsis" xbl:inherits="selected" + class="ac-ellipsis-after ac-url-text"/> + <xul:spacer class="ac-type-icon"/> + </xul:hbox> + </content> + <implementation implements="nsIDOMXULSelectControlItemElement"> + <constructor> + <![CDATA[ + let ellipsis = "\u2026"; + try { + ellipsis = Components.classes["@mozilla.org/preferences-service;1"]. + getService(Components.interfaces.nsIPrefBranch). + getComplexValue("intl.ellipsis", + Components.interfaces.nsIPrefLocalizedString).data; + } catch (ex) { + // Do nothing.. we already have a default + } + + this._urlOverflowEllipsis = document.getAnonymousElementByAttribute(this, "anonid", "url-overflow-ellipsis"); + this._titleOverflowEllipsis = document.getAnonymousElementByAttribute(this, "anonid", "title-overflow-ellipsis"); + + this._urlOverflowEllipsis.value = ellipsis; + this._titleOverflowEllipsis.value = ellipsis; + + this._typeImage = document.getAnonymousElementByAttribute(this, "anonid", "type-image"); + + this._urlBox = document.getAnonymousElementByAttribute(this, "anonid", "url-box"); + this._url = document.getAnonymousElementByAttribute(this, "anonid", "url"); + this._action = document.getAnonymousElementByAttribute(this, "anonid", "action"); + + this._titleBox = document.getAnonymousElementByAttribute(this, "anonid", "title-box"); + this._title = document.getAnonymousElementByAttribute(this, "anonid", "title"); + + this._extraBox = document.getAnonymousElementByAttribute(this, "anonid", "extra-box"); + this._extra = document.getAnonymousElementByAttribute(this, "anonid", "extra"); + + 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> + + <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 + 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); + + 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 text = aText.substr(start, next - start); + start = next; + + if (i % 2 == 0) { + // Emphasize the text for even indices + let span = aDescriptionElement.appendChild( + document.createElementNS("http://www.w3.org/1999/xhtml", "span")); + span.className = "ac-emphasize-text"; + span.textContent = text; + } else { + // Otherwise, it's plain text + aDescriptionElement.appendChild(document.createTextNode(text)); + } + } + ]]> + </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": + span.className = "ac-emphasize-text"; + break; + case "selected": + span.className = "ac-selected-text"; + 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 originalUrl = this.getAttribute("url"); + let title = this.getAttribute("title"); + let type = this.getAttribute("type"); + + let displayUrl; + let emphasiseTitle = true; + let emphasiseUrl = true; + + // Hide the title's extra box by default, until we find out later if + // we need extra stuff. + this._extraBox.hidden = true; + this._titleBox.flex = 1; + this._typeImage.hidden = false; + + this.removeAttribute("actiontype"); + this.classList.remove("overridable-action"); + + // The ellipses are hidden via their visibility so that they always + // take up space and don't pop in on top of text when shown. For + // keyword searches, however, the title ellipsis should not take up + // space when hidden. Setting the hidden property accomplishes that. + this._titleOverflowEllipsis.hidden = false; + + let types = new Set(type.split(/\s+/)); + + // Remove types that should ultimately not be in the `type` string. + let initialTypes = new Set(types); + types.delete("action"); + types.delete("autofill"); + types.delete("heuristic"); + types.delete("search"); + + type = [...types][0] || ""; + + // If the type includes an action, set up the item appropriately. + if (initialTypes.has("action")) { + let 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("switchToTab"); + this._setUpDescription(this._action, desc, true); + } else if (action.type == "remotetab") { + displayUrl = this._unescapeUrl(action.params.url); + let desc = action.params.deviceName; + this._setUpDescription(this._action, 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; + let engineStr = " - " + + this._stringBundle.formatStringFromName("searchWithEngine", + [engineName], 1); + + // 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, ""], + ]; + } + pairs.push([engineStr, "selected"]); + 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) { + this.removeAttribute("image"); + } + } else if (action.type == "visiturl") { + emphasiseUrl = false; + displayUrl = this._unescapeUrl(action.params.url); + let sourceStr = this._stringBundle.GetStringFromName("visitURL"); + title = this._generateEmphasisPairs(sourceStr, [ + [displayUrl, "match"], + ]); + } + } + + // Check if we have a search engine name + if (initialTypes.has("search")) { + emphasiseUrl = false; + + const TITLE_SEARCH_ENGINE_SEPARATOR = " \u00B7\u2013\u00B7 "; + + let searchEngine = ""; + [title, searchEngine] = title.split(TITLE_SEARCH_ENGINE_SEPARATOR); + displayUrl = this._stringBundle.formatStringFromName("searchWithEngine", [searchEngine], 1); + } + + if (!displayUrl) { + let input = this.parentNode.parentNode.input; + let url = typeof(input.trimValue) == "function" ? + input.trimValue(originalUrl) : + originalUrl; + displayUrl = this._unescapeUrl(url); + } + this.setAttribute("displayurl", displayUrl); + + // Check if we have an auto-fill URL + if (initialTypes.has("autofill")) { + emphasiseUrl = false; + + let sourceStr = this._stringBundle.GetStringFromName("visitURL"); + title = this._generateEmphasisPairs(sourceStr, [ + [displayUrl, "match"], + ]); + } + + // If we have a tag match, show the tags and icon + if (type == "tag" || type == "bookmark-tag") { + // Configure the extra box for tags display + this._extraBox.hidden = false; + this._extraBox.childNodes[0].hidden = false; + this._extraBox.childNodes[1].hidden = true; + this._extraBox.pack = "end"; + this._titleBox.flex = 1; + + // 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(",").sort().join(", "); + + // Emphasize the matching text in the tags + this._setUpDescription(this._extra, sortedTags); + + // If we're suggesting bookmarks, then treat tagged matches as + // bookmarks for the star. + if (type == "bookmark-tag") { + type = "bookmark"; + } else { + this._typeImage.hidden = true; + } + // keyword and favicon type results for search engines + // have an extra magnifying glass icon after them + } else if (type == "keyword" || (initialTypes.has("search") && + initialTypes.has("favicon"))) { + // Configure the extra box for keyword display + this._extraBox.hidden = false; + this._extraBox.childNodes[0].hidden = true; + // The second child node is ":" and it should be hidden for non keyword types + this._extraBox.childNodes[1].hidden = type == "keyword" ? false : true; + this._extraBox.pack = "start"; + this._titleBox.flex = 0; + + // Hide the ellipsis so it doesn't take up space. + this._titleOverflowEllipsis.hidden = true; + + if (type == "keyword") { + // Put the parameters next to the title if we have any + let search = this.getAttribute("text"); + let params = ""; + let paramsIndex = search.indexOf(" "); + if (paramsIndex != -1) + params = search.substr(paramsIndex + 1); + + // Emphasize the keyword parameters + this._setUpDescription(this._extra, params); + + // Don't emphasize keyword searches in the title or url + emphasiseUrl = false; + emphasiseTitle = false; + } else { + // Don't show any description for non keyword types. + this._setUpDescription(this._extra, "", true); + } + // If the result has the type favicon and a known search provider, + // customize it the same way as a keyword result. + type = "keyword"; + } + + // Give the image the icon style and a special one for the type + this._typeImage.className = "ac-type-icon" + + (type ? " ac-result-type-" + type : ""); + + // Show the domain as the title if we don't have a title. + if (title == "") { + title = displayUrl; + try { + let uri = Services.io.newURI(originalUrl, null, null); + // Not all valid URLs have a domain. + if (uri.host) + title = uri.host; + } catch (e) {} + } + + // Emphasize the matching search terms for the description + if (Array.isArray(title)) + this._setUpEmphasisedSections(this._title, title); + else + this._setUpDescription(this._title, title, !emphasiseTitle); + + this._setUpDescription(this._url, displayUrl, !emphasiseUrl); + + // Set up overflow on a timeout because the contents of the box + // might not have a width yet even though we just changed them + setTimeout(this._setUpOverflow, 0, this._titleBox, this._titleOverflowEllipsis); + setTimeout(this._setUpOverflow, 0, this._urlBox, this._urlOverflowEllipsis); + ]]> + </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 will happen when + // UnifiedComplete is disabled - in which case, the param is always + // a URL. + action.params = { + url: params, + } + } + + return action; + ]]></body> + </method> + + <method name="_setUpOverflow"> + <parameter name="aParentBox"/> + <parameter name="aEllipsis"/> + <body> + <![CDATA[ + // Hide the ellipsis incase there's just enough to not underflow + aEllipsis.style.visibility = "hidden"; + + // Start with the parent's width and subtract off its children + let tooltip = []; + let children = aParentBox.childNodes; + let widthDiff = aParentBox.boxObject.width; + + for (let i = 0; i < children.length; i++) { + // Only consider a child if it actually takes up space + let childWidth = children[i].boxObject.width; + if (childWidth > 0) { + // Subtract a little less to account for subpixel rounding + widthDiff -= childWidth - .5; + + // Add to the tooltip if it's not hidden and has text + let childText = children[i].textContent; + if (childText) + tooltip.push(childText); + } + } + + // If the children take up more space than the parent.. overflow! + if (widthDiff < 0) { + // Re-show the ellipsis now that we know it's needed + aEllipsis.style.visibility = "visible"; + + // Separate text components with a ndash -- + aParentBox.tooltipText = tooltip.join(" \u2013 "); + } + ]]> + </body> + </method> + + <method name="_doUnderflow"> + <parameter name="aName"/> + <body> + <![CDATA[ + // Hide the ellipsis right when we know we're underflowing instead of + // waiting for the timeout to trigger the _setUpOverflow calculations + this[aName + "Box"].tooltipText = ""; + this[aName + "OverflowEllipsis"].style.visibility = "hidden"; + ]]> + </body> + </method> + + <method name="_doOverflow"> + <parameter name="aName"/> + <body> + <![CDATA[ + this._setUpOverflow(this[aName + "Box"], + this[aName + "OverflowEllipsis"]); + ]]> + </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"> + <implementation> + <method name="showPopup"> + <body><![CDATA[ + var textbox = document.getBindingParent(this); + textbox.showHistoryPopup(); + ]]></body> + </method> + </implementation> + + <handlers> + <handler event="mousedown" button="0"><![CDATA[ + this.showPopup(); + ]]></handler> + </handlers> + </binding> + +</bindings> diff --git a/application/palemoon/base/content/browser-addons.js b/application/palemoon/base/content/browser-addons.js index 7993a0c9c..f5c398f33 100644 --- a/application/palemoon/base/content/browser-addons.js +++ b/application/palemoon/base/content/browser-addons.js @@ -226,7 +226,7 @@ const gXPInstallObserver = { * - If an add-on was installed, incrementing the count, show the bar. * - If an add-on was uninstalled, and no more items are left, hide the bar. */ -let AddonsMgrListener = { +var AddonsMgrListener = { get addonBar() document.getElementById("addon-bar"), get statusBar() document.getElementById("status-bar"), getAddonBarItemCount: function() { @@ -461,7 +461,7 @@ var LightWeightThemeWebInstaller = { /* * Listen for Lightweight Theme styling changes and update the browser's theme accordingly. */ -let LightweightThemeListener = { +var LightweightThemeListener = { _modifiedStyles: [], init: function () { diff --git a/application/palemoon/base/content/browser-gestureSupport.js b/application/palemoon/base/content/browser-gestureSupport.js index d88f47c79..13eb71b99 100644 --- a/application/palemoon/base/content/browser-gestureSupport.js +++ b/application/palemoon/base/content/browser-gestureSupport.js @@ -13,7 +13,7 @@ // chrome-only, we must listen for the simple gesture events during // the capturing phase and call stopPropagation on every event. -let gGestureSupport = { +var gGestureSupport = { _currentRotation: 0, _lastRotateDelta: 0, _rotateMomentumThreshold: .75, @@ -532,7 +532,7 @@ let gGestureSupport = { }; // History Swipe Animation Support (bug 678392) -let gHistorySwipeAnimation = { +var gHistorySwipeAnimation = { active: false, isLTR: false, diff --git a/application/palemoon/base/content/browser-places.js b/application/palemoon/base/content/browser-places.js index cf9c28597..5c13a43f9 100644 --- a/application/palemoon/base/content/browser-places.js +++ b/application/palemoon/base/content/browser-places.js @@ -959,7 +959,7 @@ var PlacesMenuDNDHandler = { * This object handles the initialization and uninitialization of the bookmarks * toolbar. */ -let PlacesToolbarHelper = { +var PlacesToolbarHelper = { _place: "place:folder=TOOLBAR", get _viewElt() { @@ -1006,7 +1006,7 @@ let PlacesToolbarHelper = { * menu button. */ -let BookmarkingUI = { +var BookmarkingUI = { get button() { if (!this._button) { this._button = document.getElementById("bookmarks-menu-button"); diff --git a/application/palemoon/base/content/browser-syncui.js b/application/palemoon/base/content/browser-syncui.js index fc8c7f016..67056e221 100644 --- a/application/palemoon/base/content/browser-syncui.js +++ b/application/palemoon/base/content/browser-syncui.js @@ -3,7 +3,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. // gSyncUI handles updating the tools menu -let gSyncUI = { +var gSyncUI = { _obs: ["weave:service:sync:start", "weave:service:sync:delayed", "weave:service:quota:remaining", diff --git a/application/palemoon/base/content/browser-thumbnails.js b/application/palemoon/base/content/browser-thumbnails.js index b06dfd503..079b0ac1b 100644 --- a/application/palemoon/base/content/browser-thumbnails.js +++ b/application/palemoon/base/content/browser-thumbnails.js @@ -7,7 +7,7 @@ /** * Keeps thumbnails of open web pages up-to-date. */ -let gBrowserThumbnails = { +var gBrowserThumbnails = { /** * Pref that controls whether we can store SSL content on disk */ diff --git a/application/palemoon/base/content/browser-uacompat.js b/application/palemoon/base/content/browser-uacompat.js new file mode 100644 index 000000000..933aa55d1 --- /dev/null +++ b/application/palemoon/base/content/browser-uacompat.js @@ -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/. */ + +var UserAgentCompatibility = { + PREF_UA_COMPAT: "general.useragent.compatMode", + PREF_UA_COMPAT_GECKO: "general.useragent.compatMode.gecko", + PREF_UA_COMPAT_FIREFOX: "general.useragent.compatMode.firefox", + + init: function() { + this.checkPreferences(); + Services.prefs.addObserver(this.PREF_UA_COMPAT, this, false); + Services.prefs.addObserver(this.PREF_UA_COMPAT_GECKO, this, false); + Services.prefs.addObserver(this.PREF_UA_COMPAT_FIREFOX, this, false); + }, + + uninit: function() { + Services.prefs.removeObserver(this.PREF_UA_COMPAT, this, false); + Services.prefs.removeObserver(this.PREF_UA_COMPAT_GECKO, this, false); + Services.prefs.removeObserver(this.PREF_UA_COMPAT_FIREFOX, this, false); + }, + + observe: function() { + this.checkPreferences(); + }, + + checkPreferences: function() { + var compatMode = Services.prefs.getIntPref(this.PREF_UA_COMPAT); + + switch(compatMode) { + case 0: + Services.prefs.setBoolPref(this.PREF_UA_COMPAT_GECKO, false); + Services.prefs.setBoolPref(this.PREF_UA_COMPAT_FIREFOX, false); + break; + case 1: + Services.prefs.setBoolPref(this.PREF_UA_COMPAT_GECKO, true); + Services.prefs.setBoolPref(this.PREF_UA_COMPAT_FIREFOX, false); + break; + case 2: + Services.prefs.setBoolPref(this.PREF_UA_COMPAT_GECKO, true); + Services.prefs.setBoolPref(this.PREF_UA_COMPAT_FIREFOX, true); + break; + } + } +}; diff --git a/application/palemoon/base/content/browser-webrtcUI.js b/application/palemoon/base/content/browser-webrtcUI.js index a6c9008ca..d59134ce5 100644 --- a/application/palemoon/base/content/browser-webrtcUI.js +++ b/application/palemoon/base/content/browser-webrtcUI.js @@ -3,7 +3,7 @@ # 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/. -let WebrtcIndicator = { +var WebrtcIndicator = { init: function () { let temp = {}; Cu.import("resource:///modules/webrtcUI.jsm", temp); diff --git a/application/palemoon/base/content/browser.css b/application/palemoon/base/content/browser.css index e6a84421b..a72d77488 100644 --- a/application/palemoon/base/content/browser.css +++ b/application/palemoon/base/content/browser.css @@ -280,6 +280,10 @@ panel[noactions] > richlistbox > richlistitem[type~="action"] > .ac-url-box > .a -moz-binding: url("chrome://browser/content/urlbarBindings.xml#urlbar-rich-result-popup"); } +#DateTimePickerPanel[active="true"] { + -moz-binding: url("chrome://global/content/bindings/datetimepopup.xml#datetime-popup"); +} + /* Pale Moon: Address bar: Feeds */ #ub-feed-button > .button-box > .box-inherit > .button-text, #ub-feed-button > .button-box > .button-menu-dropmarker { @@ -324,6 +328,68 @@ panel[noactions] > richlistbox > richlistitem[type~="action"] > .ac-url-box > .a visibility: hidden; } +/* Override xul.css for autocomplete bindings and css */ +textbox[type="autocomplete"] { + -moz-binding: url("chrome://browser/content/autocomplete.xml#autocomplete"); +} + +panel[type="autocomplete"] { + -moz-binding: url("chrome://browser/content/autocomplete.xml#autocomplete-result-popup"); +} + +panel[type="autocomplete-richlistbox"] { + -moz-binding: url("chrome://browser/content/autocomplete.xml#autocomplete-rich-result-popup"); +} + +/* FIXME: bug 616258 */ + +.autocomplete-tree { + -moz-binding: url("chrome://browser/content/autocomplete.xml#autocomplete-tree"); + -moz-user-focus: ignore; +} + +.autocomplete-treebody { + -moz-binding: url("chrome://browser/content/autocomplete.xml#autocomplete-treebody"); +} + +.autocomplete-richlistbox { + -moz-binding: url("chrome://browser/content/autocomplete.xml#autocomplete-richlistbox"); + -moz-user-focus: ignore; +} + +.autocomplete-richlistbox > scrollbox { + overflow-x: hidden !important; +} + +.autocomplete-richlistitem { + -moz-binding: url("chrome://browser/content/autocomplete.xml#autocomplete-richlistitem"); + -moz-box-orient: vertical; + overflow: -moz-hidden-unscrollable; +} + +.autocomplete-treerows { + -moz-binding: url("chrome://browser/content/autocomplete.xml#autocomplete-treerows"); +} + +.autocomplete-history-dropmarker { + display: none; +} + +.autocomplete-history-dropmarker[enablehistory="true"] { + display: -moz-box; + -moz-binding: url("chrome://browser/content/autocomplete.xml#history-dropmarker"); +} + +.ac-ellipsis-after { + visibility: hidden; +} + +.ac-url-text[type~="action"], +.ac-action-text:not([type~="action"]) { + visibility: collapse; +} + + /* ::::: Unified Back-/Forward Button ::::: */ #back-button > .toolbarbutton-menu-dropmarker, #forward-button > .toolbarbutton-menu-dropmarker { @@ -542,11 +608,11 @@ window[chromehidden~="toolbar"] toolbar:not(.toolbar-primary):not(#nav-bar):not( max-width: 280px; } -.form-validation-anchor { +.popup-anchor { /* should occupy space but not be visible */ opacity: 0; - visibility: hidden; pointer-events: none; + -moz-stack-sizing: ignore; } #addon-progress-notification { @@ -627,10 +693,6 @@ statuspanel[inactive][previoustype=overLink] { -moz-box-align: end; } -.panel-inner-arrowcontentfooter[footertype="promobox"] { - -moz-binding: url("chrome://browser/content/urlbarBindings.xml#promobox"); -} - /* highlighter */ %include highlighter.css diff --git a/application/palemoon/base/content/browser.js b/application/palemoon/base/content/browser.js index b2d260101..c9f0aa4aa 100644 --- a/application/palemoon/base/content/browser.js +++ b/application/palemoon/base/content/browser.js @@ -7,6 +7,7 @@ var Ci = Components.interfaces; var Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource:///modules/RecentWindow.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu", @@ -141,6 +142,7 @@ let gInitialPages = [ #include browser-plugins.js #include browser-tabPreviews.js #include browser-thumbnails.js +#include browser-uacompat.js #ifdef MOZ_WEBRTC #include browser-webrtcUI.js @@ -917,6 +919,7 @@ var gBrowserInit = { TabsInTitlebar.init(); retrieveToolbarIconsizesFromTheme(); ToolbarIconColor.init(); + UserAgentCompatibility.init(); #ifdef XP_WIN if (window.matchMedia("(-moz-os-version: windows-win8)").matches && @@ -1293,6 +1296,8 @@ var gBrowserInit = { DevToolsTheme.uninit(); #endif + UserAgentCompatibility.uninit(); + var enumerator = Services.wm.getEnumerator(null); enumerator.getNext(); if (!enumerator.hasMoreElements()) { @@ -2329,7 +2334,7 @@ function BrowserOnAboutPageLoad(doc) { /** * Handle command events bubbling up from error page content */ -let BrowserOnClick = { +var BrowserOnClick = { handleEvent: function BrowserOnClick_handleEvent(aEvent) { if (!aEvent.isTrusted || // Don't trust synthetic events aEvent.button == 2 || aEvent.target.localName != "button") { @@ -6998,7 +7003,8 @@ function focusNextFrame(event) { if (element.ownerDocument == document) focusAndSelectUrlBar(); } -let BrowserChromeTest = { + +var BrowserChromeTest = { _cb: null, _ready: false, markAsReady: function () { @@ -7016,7 +7022,7 @@ let BrowserChromeTest = { } }; -let ToolbarIconColor = { +var ToolbarIconColor = { init: function () { this._initialized = true; diff --git a/application/palemoon/base/content/browser.xul b/application/palemoon/base/content/browser.xul index df152bbaa..ea9c3f969 100644 --- a/application/palemoon/base/content/browser.xul +++ b/application/palemoon/base/content/browser.xul @@ -132,6 +132,20 @@ <!-- for url bar autocomplete --> <panel type="autocomplete-richlistbox" id="PopupAutoCompleteRichResult" noautofocus="true" hidden="true"/> + <!-- for date/time picker. consumeoutsideclicks is set to never, so that + clicks on the anchored input box are never consumed. --> + <panel id="DateTimePickerPanel" + type="arrow" + hidden="true" + orient="vertical" + noautofocus="true" + norolluponanchor="true" + consumeoutsideclicks="never" + level="parent" + tabspecific="true"> + <iframe id="dateTimePopupFrame"/> + </panel> + <!-- for invalid form error message --> <panel id="invalid-form-popup" type="arrow" orient="vertical" noautofocus="true" hidden="true" level="parent"> <description/> @@ -139,7 +153,6 @@ <panel id="editBookmarkPanel" type="arrow" - footertype="promobox" orient="vertical" ignorekeys="true" consumeoutsideclicks="true" @@ -946,7 +959,8 @@ flex="1" contenttooltip="aHTMLTooltip" tabcontainer="tabbrowser-tabs" contentcontextmenu="contentAreaContextMenu" - autocompletepopup="PopupAutoComplete"/> + autocompletepopup="PopupAutoComplete" + datetimepicker="DateTimePickerPanel"/> <chatbar id="pinnedchats" layer="true" mousethrough="always" hidden="true"/> <statuspanel id="statusbar-display" inactive="true"/> </vbox> diff --git a/application/palemoon/base/content/newtab/drag.js b/application/palemoon/base/content/newtab/drag.js index 8f0bf674e..fbd688faa 100644 --- a/application/palemoon/base/content/newtab/drag.js +++ b/application/palemoon/base/content/newtab/drag.js @@ -7,7 +7,7 @@ /** * This singleton implements site dragging functionality. */ -let gDrag = { +var gDrag = { /** * The site offset to the drag start point. */ diff --git a/application/palemoon/base/content/newtab/dragDataHelper.js b/application/palemoon/base/content/newtab/dragDataHelper.js index a66e4e87e..54348ab14 100644 --- a/application/palemoon/base/content/newtab/dragDataHelper.js +++ b/application/palemoon/base/content/newtab/dragDataHelper.js @@ -4,7 +4,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ #endif -let gDragDataHelper = { +var gDragDataHelper = { get mimeType() { return "text/x-moz-url"; }, diff --git a/application/palemoon/base/content/newtab/drop.js b/application/palemoon/base/content/newtab/drop.js index d7bf30506..748652455 100644 --- a/application/palemoon/base/content/newtab/drop.js +++ b/application/palemoon/base/content/newtab/drop.js @@ -11,7 +11,7 @@ const DELAY_REARRANGE_MS = 100; /** * This singleton implements site dropping functionality. */ -let gDrop = { +var gDrop = { /** * The last drop target. */ diff --git a/application/palemoon/base/content/newtab/dropPreview.js b/application/palemoon/base/content/newtab/dropPreview.js index 903762345..fd7587a35 100644 --- a/application/palemoon/base/content/newtab/dropPreview.js +++ b/application/palemoon/base/content/newtab/dropPreview.js @@ -9,7 +9,7 @@ * indicate the transformation that results from dropping a cell at a certain * position. */ -let gDropPreview = { +var gDropPreview = { /** * Rearranges the sites currently contained in the grid when a site would be * dropped onto the given cell. diff --git a/application/palemoon/base/content/newtab/dropTargetShim.js b/application/palemoon/base/content/newtab/dropTargetShim.js index a85a6ccd6..046dbea1e 100644 --- a/application/palemoon/base/content/newtab/dropTargetShim.js +++ b/application/palemoon/base/content/newtab/dropTargetShim.js @@ -9,7 +9,7 @@ * the default DnD target detection relies on the cursor's position. We want * to pick a drop target based on the dragged site's position. */ -let gDropTargetShim = { +var gDropTargetShim = { /** * Cache for the position of all cells, cleaned after drag finished. */ diff --git a/application/palemoon/base/content/newtab/grid.js b/application/palemoon/base/content/newtab/grid.js index 3b7dfc35f..46e0b804b 100644 --- a/application/palemoon/base/content/newtab/grid.js +++ b/application/palemoon/base/content/newtab/grid.js @@ -7,7 +7,7 @@ /** * This singleton represents the grid that contains all sites. */ -let gGrid = { +var gGrid = { /** * The DOM node of the grid. */ diff --git a/application/palemoon/base/content/newtab/newTab.js b/application/palemoon/base/content/newtab/newTab.js index 6d8647ab6..bba73fd7a 100644 --- a/application/palemoon/base/content/newtab/newTab.js +++ b/application/palemoon/base/content/newtab/newTab.js @@ -18,7 +18,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "Rect", XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); -let { +var { links: gLinks, allPages: gAllPages, linkChecker: gLinkChecker, diff --git a/application/palemoon/base/content/newtab/page.js b/application/palemoon/base/content/newtab/page.js index afe5bfba8..fc836a55e 100644 --- a/application/palemoon/base/content/newtab/page.js +++ b/application/palemoon/base/content/newtab/page.js @@ -8,7 +8,7 @@ * This singleton represents the whole 'New Tab Page' and takes care of * initializing all its components. */ -let gPage = { +var gPage = { /** * Initializes the page. */ diff --git a/application/palemoon/base/content/newtab/transformations.js b/application/palemoon/base/content/newtab/transformations.js index 6d1554f5f..0711d7d0a 100644 --- a/application/palemoon/base/content/newtab/transformations.js +++ b/application/palemoon/base/content/newtab/transformations.js @@ -9,7 +9,7 @@ * in the DOM and by showing or hiding the node. It additionally provides * convenience methods to work with a site's DOM node. */ -let gTransformation = { +var gTransformation = { /** * Returns the width of the left and top border of a cell. We need to take it * into account when measuring and comparing site and cell positions. diff --git a/application/palemoon/base/content/newtab/undo.js b/application/palemoon/base/content/newtab/undo.js index 5f619e980..b856914d2 100644 --- a/application/palemoon/base/content/newtab/undo.js +++ b/application/palemoon/base/content/newtab/undo.js @@ -8,7 +8,7 @@ * Dialog allowing to undo the removal of single site or to completely restore * the grid's original state. */ -let gUndoDialog = { +var gUndoDialog = { /** * The undo dialog's timeout in miliseconds. */ diff --git a/application/palemoon/base/content/newtab/updater.js b/application/palemoon/base/content/newtab/updater.js index 7b483e037..e6da37f87 100644 --- a/application/palemoon/base/content/newtab/updater.js +++ b/application/palemoon/base/content/newtab/updater.js @@ -8,7 +8,7 @@ * This singleton provides functionality to update the current grid to a new * set of pinned and blocked sites. It adds, moves and removes sites. */ -let gUpdater = { +var gUpdater = { /** * Updates the current grid according to its pinned and blocked sites. * This removes old, moves existing and creates new sites to fill gaps. diff --git a/application/palemoon/base/content/nsContextMenu.js b/application/palemoon/base/content/nsContextMenu.js index 017ab87cc..1fe592b52 100644 --- a/application/palemoon/base/content/nsContextMenu.js +++ b/application/palemoon/base/content/nsContextMenu.js @@ -1394,9 +1394,8 @@ nsContextMenu.prototype = { isDisabledForEvents: function(aNode) { let ownerDoc = aNode.ownerDocument; - return - ownerDoc.defaultView && - ownerDoc.defaultView + return ownerDoc.defaultView && + ownerDoc.defaultView .QueryInterface(Components.interfaces.nsIInterfaceRequestor) .getInterface(Components.interfaces.nsIDOMWindowUtils) .isNodeDisabledForEvents(aNode); diff --git a/application/palemoon/base/content/openLocation.js b/application/palemoon/base/content/openLocation.js index 5b731c7e8..316dfac70 100644 --- a/application/palemoon/base/content/openLocation.js +++ b/application/palemoon/base/content/openLocation.js @@ -7,7 +7,7 @@ var browser; var dialog = {}; var pref = null; -let openLocationModule = {}; +var openLocationModule = {}; try { pref = Components.classes["@mozilla.org/preferences-service;1"] .getService(Components.interfaces.nsIPrefBranch); @@ -16,7 +16,7 @@ try { } Components.utils.import("resource:///modules/openLocationLastURL.jsm", openLocationModule); -let gOpenLocationLastURL = new openLocationModule.OpenLocationLastURL(window.opener); +var gOpenLocationLastURL = new openLocationModule.OpenLocationLastURL(window.opener); function onLoad() { diff --git a/application/palemoon/base/content/popup-notifications.inc b/application/palemoon/base/content/popup-notifications.inc index 7be975b7f..31a72b489 100644 --- a/application/palemoon/base/content/popup-notifications.inc +++ b/application/palemoon/base/content/popup-notifications.inc @@ -2,7 +2,6 @@ <panel id="notification-popup" type="arrow" - footertype="promobox" position="after_start" hidden="true" orient="vertical" diff --git a/application/palemoon/base/content/sync/aboutSyncTabs.js b/application/palemoon/base/content/sync/aboutSyncTabs.js index 535c43e56..bc624a459 100644 --- a/application/palemoon/base/content/sync/aboutSyncTabs.js +++ b/application/palemoon/base/content/sync/aboutSyncTabs.js @@ -11,7 +11,7 @@ Cu.import("resource://gre/modules/PlacesUtils.jsm", this); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -let RemoteTabViewer = { +var RemoteTabViewer = { _tabsList: null, init: function () { diff --git a/application/palemoon/base/content/sync/addDevice.js b/application/palemoon/base/content/sync/addDevice.js index 556e75768..40862a791 100644 --- a/application/palemoon/base/content/sync/addDevice.js +++ b/application/palemoon/base/content/sync/addDevice.js @@ -15,7 +15,7 @@ const ADD_DEVICE_PAGE = 0; const SYNC_KEY_PAGE = 1; const DEVICE_CONNECTED_PAGE = 2; -let gSyncAddDevice = { +var gSyncAddDevice = { init: function init() { this.pin1.setAttribute("maxlength", PIN_PART_LENGTH); diff --git a/application/palemoon/base/content/sync/genericChange.js b/application/palemoon/base/content/sync/genericChange.js index 6d1ce9485..0c6dc145e 100644 --- a/application/palemoon/base/content/sync/genericChange.js +++ b/application/palemoon/base/content/sync/genericChange.js @@ -8,7 +8,7 @@ const Cc = Components.classes; Components.utils.import("resource://services-sync/main.js"); Components.utils.import("resource://gre/modules/Services.jsm"); -let Change = { +var Change = { _dialog: null, _dialogType: null, _status: null, diff --git a/application/palemoon/base/content/sync/progress.js b/application/palemoon/base/content/sync/progress.js index 2063f612a..101160fa8 100644 --- a/application/palemoon/base/content/sync/progress.js +++ b/application/palemoon/base/content/sync/progress.js @@ -6,8 +6,8 @@ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://services-sync/main.js"); -let gProgressBar; -let gCounter = 0; +var gProgressBar; +var gCounter = 0; function onLoad(event) { Services.obs.addObserver(onEngineSync, "weave:engine:sync:finish", false); diff --git a/application/palemoon/base/content/sync/quota.js b/application/palemoon/base/content/sync/quota.js index 7117a2ddf..454052754 100644 --- a/application/palemoon/base/content/sync/quota.js +++ b/application/palemoon/base/content/sync/quota.js @@ -10,7 +10,7 @@ const Cu = Components.utils; Cu.import("resource://services-sync/main.js"); Cu.import("resource://gre/modules/DownloadUtils.jsm"); -let gSyncQuota = { +var gSyncQuota = { init: function init() { this.bundle = document.getElementById("quotaStrings"); @@ -86,7 +86,7 @@ let gSyncQuota = { }; -let gUsageTreeView = { +var gUsageTreeView = { _ignored: {keys: true, meta: true, diff --git a/application/palemoon/base/content/sync/utils.js b/application/palemoon/base/content/sync/utils.js index 0c02b5bc0..d41ecf18a 100644 --- a/application/palemoon/base/content/sync/utils.js +++ b/application/palemoon/base/content/sync/utils.js @@ -8,7 +8,7 @@ const PERMISSIONS_RWUSR = 0x180; // Weave should always exist before before this file gets included. -let gSyncUtils = { +var gSyncUtils = { get bundle() { delete this.bundle; return this.bundle = Services.strings.createBundle("chrome://browser/locale/syncSetup.properties"); diff --git a/application/palemoon/base/content/tabbrowser.xml b/application/palemoon/base/content/tabbrowser.xml index 4b10855a7..a7cc6deea 100644 --- a/application/palemoon/base/content/tabbrowser.xml +++ b/application/palemoon/base/content/tabbrowser.xml @@ -30,7 +30,7 @@ <xul:vbox flex="1" class="browserContainer"> <xul:stack flex="1" class="browserStack" anonid="browserStack"> <xul:browser anonid="initialBrowser" type="content-primary" message="true" disablehistory="true" - xbl:inherits="tooltip=contenttooltip,contextmenu=contentcontextmenu,autocompletepopup"/> + xbl:inherits="tooltip=contenttooltip,contextmenu=contentcontextmenu,autocompletepopup,datetimepicker"/> </xul:stack> </xul:vbox> </xul:hbox> @@ -73,7 +73,7 @@ .getService(Components.interfaces.nsIFaviconService); </field> <field name="_placesAutocomplete" readonly="true"> - Components.classes["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"] + Components.classes["@mozilla.org/autocomplete/search;1?name=history"] .getService(Components.interfaces.mozIPlacesAutoComplete); </field> <field name="mTabBox" readonly="true"> @@ -138,19 +138,19 @@ ]]></getter> </property> - <property name="formValidationAnchor" readonly="true"> + <property name="popupAnchor" readonly="true"> <getter><![CDATA[ - if (this.mCurrentTab._formValidationAnchor) { - return this.mCurrentTab._formValidationAnchor; + if (this.mCurrentTab._popupAnchor) { + return this.mCurrentTab._popupAnchor; } let stack = this.mCurrentBrowser.parentNode; - // Create an anchor for the form validation popup + // Create an anchor for the popup const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; - let formValidationAnchor = document.createElementNS(NS_XUL, "hbox"); - formValidationAnchor.className = "form-validation-anchor"; - formValidationAnchor.hidden = true; - stack.appendChild(formValidationAnchor); - return this.mCurrentTab._formValidationAnchor = formValidationAnchor; + let popupAnchor = document.createElementNS(NS_XUL, "hbox"); + popupAnchor.className = "popup-anchor"; + popupAnchor.hidden = true; + stack.appendChild(popupAnchor); + return this.mCurrentTab._popupAnchor = popupAnchor; ]]></getter> </property> @@ -1451,6 +1451,10 @@ b.setAttribute("autocompletepopup", this.getAttribute("autocompletepopup")); b.setAttribute("autoscrollpopup", this._autoScrollPopup.id); + if (this.hasAttribute("datetimepicker")) { + b.setAttribute("datetimepicker", this.getAttribute("datetimepicker")); + } + // Create the browserStack container var stack = document.createElementNS(NS_XUL, "stack"); stack.className = "browserStack"; diff --git a/application/palemoon/base/content/urlbarBindings.xml b/application/palemoon/base/content/urlbarBindings.xml index c99819f0d..6b4350f99 100644 --- a/application/palemoon/base/content/urlbarBindings.xml +++ b/application/palemoon/base/content/urlbarBindings.xml @@ -17,7 +17,7 @@ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:xbl="http://www.mozilla.org/xbl"> - <binding id="urlbar" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete"> + <binding id="urlbar" extends="chrome://browser/content/autocomplete.xml#autocomplete"> <content sizetopopup="pref"> <xul:hbox anonid="textbox-container" @@ -769,7 +769,7 @@ </binding> <!-- Note: this binding is applied to the autocomplete popup used in the Search bar and in web page content --> - <binding id="browser-autocomplete-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-result-popup"> + <binding id="browser-autocomplete-result-popup" extends="chrome://browser/content/autocomplete.xml#autocomplete-result-popup"> <implementation> <method name="openAutocompletePopup"> <parameter name="aInput"/> @@ -823,7 +823,7 @@ </implementation> </binding> - <binding id="urlbar-rich-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-rich-result-popup"> + <binding id="urlbar-rich-result-popup" extends="chrome://browser/content/autocomplete.xml#autocomplete-rich-result-popup"> <implementation> <field name="_maxResults">0</field> @@ -1764,195 +1764,6 @@ </implementation> </binding> - <binding id="promobox"> - <content> - <xul:hbox class="panel-promo-box" align="start" flex="1"> - <xul:hbox align="center" flex="1"> - <xul:image class="panel-promo-icon"/> - <xul:description anonid="promo-message" class="panel-promo-message" flex="1"> - <xul:description anonid="promo-link" - class="plain text-link inline-link" - onclick="document.getBindingParent(this).onLinkClick();"/> - </xul:description> - </xul:hbox> - <xul:toolbarbutton class="panel-promo-closebutton close-icon" - oncommand="document.getBindingParent(this).onCloseButtonCommand();" - tooltiptext="&closeNotification.tooltip;"/> - </xul:hbox> - </content> - - <implementation implements="nsIDOMEventListener"> - <constructor><![CDATA[ - this._panel.addEventListener("popupshowing", this, false); - ]]></constructor> - - <destructor><![CDATA[ - this._panel.removeEventListener("popupshowing", this, false); - ]]></destructor> - - <field name="_panel" readonly="true"><![CDATA[ - let node = this.parentNode; - while(node && node.localName != "panel") { - node = node.parentNode; - } - node; - ]]></field> - <field name="_promomessage" readonly="true"> - document.getAnonymousElementByAttribute(this, "anonid", "promo-message"); - </field> - <field name="_promolink" readonly="true"> - document.getAnonymousElementByAttribute(this, "anonid", "promo-link"); - </field> - <field name="_brandBundle" readonly="true"> - Services.strings.createBundle("chrome://branding/locale/brand.properties"); - </field> - <property name="_viewsLeftMap"> - <getter><![CDATA[ - let viewsLeftMap = {}; - try { - viewsLeftMap = JSON.parse(Services.prefs.getCharPref("browser.syncPromoViewsLeftMap")); - } catch (ex) { - // If the old preference exists, migrate it to the new one. - try { - let oldPref = Services.prefs.getIntPref("browser.syncPromoViewsLeft"); - Services.prefs.clearUserPref("browser.syncPromoViewsLeft"); - viewsLeftMap.bookmarks = oldPref; - viewsLeftMap.passwords = oldPref; - Services.prefs.setCharPref("browser.syncPromoViewsLeftMap", - JSON.stringify(viewsLeftMap)); - } catch (ex2) {} - } - return viewsLeftMap; - ]]></getter> - </property> - <property name="_viewsLeft"> - <getter><![CDATA[ - let views = 5; - let map = this._viewsLeftMap; - if (this._notificationType in map) { - views = map[this._notificationType]; - } - return views; - ]]></getter> - <setter><![CDATA[ - let map = this._viewsLeftMap; - map[this._notificationType] = val; - Services.prefs.setCharPref("browser.syncPromoViewsLeftMap", - JSON.stringify(map)); - return val; - ]]></setter> - </property> - <property name="_notificationType"> - <getter><![CDATA[ - // Use the popupid attribute to identify the notification type, - // otherwise just rely on the panel id for common arrowpanels. - let type = this._panel.firstChild.getAttribute("popupid") || - this._panel.id; - if (type.startsWith("password-")) - return "passwords"; - if (type == "editBookmarkPanel") - return "bookmarks"; - if (type == "addon-install-complete") { - if (!Services.prefs.prefHasUserValue("services.sync.username")) - return "addons"; - if (!Services.prefs.getBoolPref("services.sync.engine.addons")) - return "addons-sync-disabled"; - } - return null; - ]]></getter> - </property> - <property name="_notificationMessage"> - <getter><![CDATA[ - return gNavigatorBundle.getFormattedString( - "syncPromoNotification." + this._notificationType + ".description", - [this._brandBundle.GetStringFromName("syncBrandShortName")] - ); - ]]></getter> - </property> - <property name="_notificationLink"> - <getter><![CDATA[ - if (this._notificationType == "addons-sync-disabled") { - return "https://forum.palemoon.org/viewforum.php?f=52"; - } - return "http://www.palemoon.org/sync/"; - ]]></getter> - </property> - <method name="onCloseButtonCommand"> - <body><![CDATA[ - this._viewsLeft = 0; - this.hidden = true; - ]]></body> - </method> - <method name="onLinkClick"> - <body><![CDATA[ - // Open a new selected tab and close the current panel. - gBrowser.loadOneTab(this._promolink.getAttribute("href"), - { inBackground: false }); - this._panel.hidePopup(); - ]]></body> - </method> - <method name="handleEvent"> - <parameter name="event"/> - <body><![CDATA[ - if (event.type != "popupshowing" || event.target != this._panel) - return; - - // A previous notification may have unhidden this. - this.hidden = true; - - // Only handle supported notification panels. - if (!this._notificationType) { - return; - } - - let viewsLeft = this._viewsLeft; - if (viewsLeft) { - // XXX: Short-circuit this for now. - // TO-DO: Clean up this code for sync promotion - this._viewsLeft = 0; - viewsLeft = 0; - return; - // XXX - - if (Services.prefs.prefHasUserValue("services.sync.username") && - this._notificationType != "addons-sync-disabled") { - // If the user has already setup Sync, don't show the notification. - this._viewsLeft = 0; - // Be sure to hide the panel, in case it was visible and the user - // decided to setup Sync after noticing it. - viewsLeft = 0; - // The panel is still hidden, just bail out. - return; - } - else { - this._viewsLeft = viewsLeft - 1; - } - - this._promolink.setAttribute("href", this._notificationLink); - this._promolink.value = gNavigatorBundle.getString("syncPromoNotification.learnMoreLinkText"); - - this.hidden = false; - - // HACK: The description element doesn't wrap correctly in panels, - // thus set a width on it, based on the available space, before - // setting its textContent. Then set its height as well, to - // fix wrong height calculation on Linux (bug 659578). - this._panel.addEventListener("popupshown", function panelShown() { - this._panel.removeEventListener("popupshown", panelShown, true); - // Previous popupShown events may close the panel or change - // its contents, so ensure this is still valid. - if (this._panel.state != "open" || !this._notificationType) - return; - this._promomessage.width = this._promomessage.getBoundingClientRect().width; - this._promomessage.firstChild.textContent = this._notificationMessage; - this._promomessage.height = this._promomessage.getBoundingClientRect().height; - }.bind(this), true); - } - ]]></body> - </method> - </implementation> - </binding> - <binding id="toolbarbutton-badged" display="xul:button" extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton"> <content> diff --git a/application/palemoon/base/jar.mn b/application/palemoon/base/jar.mn index 29896341e..fd2df79e1 100644 --- a/application/palemoon/base/jar.mn +++ b/application/palemoon/base/jar.mn @@ -108,6 +108,8 @@ browser.jar: * content/browser/sanitize.xul (content/sanitize.xul) * content/browser/sanitizeDialog.js (content/sanitizeDialog.js) content/browser/sanitizeDialog.css (content/sanitizeDialog.css) +* content/browser/autocomplete.css (content/autocomplete.css) +* content/browser/autocomplete.xml (content/autocomplete.xml) content/browser/tabbrowser.css (content/tabbrowser.css) * content/browser/tabbrowser.xml (content/tabbrowser.xml) * content/browser/urlbarBindings.xml (content/urlbarBindings.xml) diff --git a/application/palemoon/branding/official/pref/palemoon-branding.js b/application/palemoon/branding/official/pref/palemoon-branding.js index 4f5d196d7..8e8703fb7 100644 --- a/application/palemoon/branding/official/pref/palemoon-branding.js +++ b/application/palemoon/branding/official/pref/palemoon-branding.js @@ -8,6 +8,8 @@ pref("app.releaseNotesURL", "http://www.palemoon.org/releasenotes.shtml"); // Enable Firefox compatmode by default.
pref("general.useragent.compatMode", 2);
+pref("general.useragent.compatMode.gecko", true);
+pref("general.useragent.compatMode.firefox", true);
// ========================= updates ========================
#if defined(XP_WIN)
diff --git a/application/palemoon/branding/shared/branding.mozbuild b/application/palemoon/branding/shared/branding.mozbuild index fc832dbe7..284520add 100644 --- a/application/palemoon/branding/shared/branding.mozbuild +++ b/application/palemoon/branding/shared/branding.mozbuild @@ -38,13 +38,19 @@ def ApplicationBranding(): 'dsstore', 'firefox.icns', ] - elif CONFIG['MOZ_WIDGET_GTK']: + elif 'gtk' in CONFIG['MOZ_WIDGET_TOOLKIT']: BRANDING_FILES += [ 'default16.png', 'default32.png', 'default48.png', 'mozicon128.png', ] + FINAL_TARGET_FILES.icons += ['mozicon128.png'] + FINAL_TARGET_FILES.chrome.icons.default += [ + 'default16.png', + 'default32.png', + 'default48.png', + ] DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION'] DEFINES['MOZ_BRANDING_DIRECTORY'] = CONFIG['MOZ_BRANDING_DIRECTORY'] diff --git a/application/palemoon/branding/unstable/pref/palemoon-branding.js b/application/palemoon/branding/unstable/pref/palemoon-branding.js index 4c088f76d..dda6f86a5 100644 --- a/application/palemoon/branding/unstable/pref/palemoon-branding.js +++ b/application/palemoon/branding/unstable/pref/palemoon-branding.js @@ -8,6 +8,8 @@ pref("app.releaseNotesURL", "http://www.palemoon.org/unstable/releasenotes.shtml // Enable Firefox compatmode by default.
pref("general.useragent.compatMode", 2);
+pref("general.useragent.compatMode.gecko", true);
+pref("general.useragent.compatMode.firefox", true);
// ========================= updates ========================
#if defined(XP_WIN)
diff --git a/application/palemoon/components/downloads/DownloadsCommon.jsm b/application/palemoon/components/downloads/DownloadsCommon.jsm index 0921f8400..c6614e780 100644 --- a/application/palemoon/components/downloads/DownloadsCommon.jsm +++ b/application/palemoon/components/downloads/DownloadsCommon.jsm @@ -104,7 +104,7 @@ const kPartialDownloadSuffix = ".part"; const kPrefBranch = Services.prefs.getBranch("browser.download."); -let PrefObserver = { +var PrefObserver = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]), getPref: function PO_getPref(name) { diff --git a/application/palemoon/components/downloads/content/contentAreaDownloadsView.js b/application/palemoon/components/downloads/content/contentAreaDownloadsView.js index 29e2e368c..fbb18ab04 100644 --- a/application/palemoon/components/downloads/content/contentAreaDownloadsView.js +++ b/application/palemoon/components/downloads/content/contentAreaDownloadsView.js @@ -4,7 +4,7 @@ Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); -let ContentAreaDownloadsView = { +var ContentAreaDownloadsView = { init: function CADV_init() { let view = new DownloadsPlacesView(document.getElementById("downloadsRichListBox")); // Do not display the Places downloads in private windows diff --git a/application/palemoon/components/nsBrowserGlue.js b/application/palemoon/components/nsBrowserGlue.js index cdb0b7522..225cddd52 100644 --- a/application/palemoon/components/nsBrowserGlue.js +++ b/application/palemoon/components/nsBrowserGlue.js @@ -35,6 +35,8 @@ Cu.import("resource://gre/modules/Services.jsm"); ["OS", "resource://gre/modules/osfile.jsm"], ["LoginManagerParent", "resource://gre/modules/LoginManagerParent.jsm"], ["FormValidationHandler", "resource:///modules/FormValidationHandler.jsm"], + ["AutoCompletePopup", "resource:///modules/AutoCompletePopup.jsm"], + ["DateTimePickerHelper", "resource://gre/modules/DateTimePickerHelper.jsm"], ].forEach(([name, resource]) => XPCOMUtils.defineLazyModuleGetter(this, name, resource)); const PREF_PLUGINS_NOTIFYUSER = "plugins.update.notifyUser"; @@ -167,6 +169,7 @@ BrowserGlue.prototype = { } catch (e) { Cu.reportError("Could not end startup crash tracking in quit-application-granted: " + e); } + DateTimePickerHelper.uninit(); break; #ifdef OBSERVE_LASTWINDOW_CLOSE_TOPICS case "browser-lastwindow-close-requested": @@ -403,6 +406,8 @@ BrowserGlue.prototype = { #endif FormValidationHandler.init(); + AutoCompletePopup.init(); + LoginManagerParent.init(); Services.obs.notifyObservers(null, "browser-ui-startup-complete", ""); @@ -496,6 +501,8 @@ BrowserGlue.prototype = { } #endif + DateTimePickerHelper.init(); + this._trackSlowStartup(); }, @@ -511,6 +518,7 @@ BrowserGlue.prototype = { webrtcUI.uninit(); #endif FormValidationHandler.uninit(); + AutoCompletePopup.uninit(); this._dispose(); }, diff --git a/application/palemoon/components/places/BrowserPlaces.manifest b/application/palemoon/components/places/BrowserPlaces.manifest deleted file mode 100644 index 3b25f50f4..000000000 --- a/application/palemoon/components/places/BrowserPlaces.manifest +++ /dev/null @@ -1,2 +0,0 @@ -component {6bcb9bde-9018-4443-a071-c32653469597} PlacesProtocolHandler.js -contract @mozilla.org/network/protocol;1?name=place {6bcb9bde-9018-4443-a071-c32653469597} diff --git a/application/palemoon/components/places/PlacesProtocolHandler.js b/application/palemoon/components/places/PlacesProtocolHandler.js deleted file mode 100644 index ebffd2e28..000000000 --- a/application/palemoon/components/places/PlacesProtocolHandler.js +++ /dev/null @@ -1,49 +0,0 @@ -/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- - * vim: sw=2 ts=2 sts=2 et - * 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/. */ - -const Cc = Components.classes; -const Ci = Components.interfaces; - -Components.utils.import("resource://gre/modules/NetUtil.jsm"); -Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); - -const SCHEME = "place"; -const URL = "chrome://browser/content/places/content-ui/controller.xhtml"; - -function PlacesProtocolHandler() {} - -PlacesProtocolHandler.prototype = { - scheme: SCHEME, - defaultPort: -1, - protocolFlags: Ci.nsIProtocolHandler.URI_DANGEROUS_TO_LOAD | - Ci.nsIProtocolHandler.URI_IS_LOCAL_RESOURCE | - Ci.nsIProtocolHandler.URI_NORELATIVE | - Ci.nsIProtocolHandler.URI_NOAUTH, - - newURI: function PPH_newURI(aSpec, aOriginCharset, aBaseUri) { - let uri = Cc["@mozilla.org/network/simple-uri;1"].createInstance(Ci.nsIURI); - uri.spec = aSpec; - return uri; - }, - - newChannel: function PPH_newChannel(aUri) { - let chan = NetUtil.newChannel(URL); - chan.originalURI = aUri; - return chan; - }, - - allowPort: function PPH_allowPort(aPort, aScheme) { - return false; - }, - - QueryInterface: XPCOMUtils.generateQI([ - Ci.nsIProtocolHandler - ]), - - classID: Components.ID("{6bcb9bde-9018-4443-a071-c32653469597}") -}; - -this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PlacesProtocolHandler]); diff --git a/application/palemoon/components/places/content/controller.js b/application/palemoon/components/places/content/controller.js index 4d3773905..e2ae2afb0 100644 --- a/application/palemoon/components/places/content/controller.js +++ b/application/palemoon/components/places/content/controller.js @@ -1603,7 +1603,7 @@ PlacesController.prototype = { * the view that the item(s) have been dropped on was not necessarily active. * Drop functions are passed the view that is being dropped on. */ -let PlacesControllerDragHelper = { +var PlacesControllerDragHelper = { /** * DOM Element currently being dragged over */ diff --git a/application/palemoon/components/places/content/places.js b/application/palemoon/components/places/content/places.js index 9ecdfebe0..a94193823 100644 --- a/application/palemoon/components/places/content/places.js +++ b/application/palemoon/components/places/content/places.js @@ -1377,7 +1377,7 @@ var ViewMenu = { } } -let ContentArea = { +var ContentArea = { _specialViews: new Map(), init: function CA_init() { @@ -1511,7 +1511,7 @@ let ContentArea = { } }; -let ContentTree = { +var ContentTree = { init: function CT_init() { this._view = document.getElementById("placeContent"); }, diff --git a/application/palemoon/components/places/moz.build b/application/palemoon/components/places/moz.build index fc69beed9..2e35e1951 100644 --- a/application/palemoon/components/places/moz.build +++ b/application/palemoon/components/places/moz.build @@ -6,9 +6,5 @@ JAR_MANIFESTS += ['jar.mn'] -EXTRA_COMPONENTS += [ - 'BrowserPlaces.manifest', - 'PlacesProtocolHandler.js', -] EXTRA_JS_MODULES += [ 'PlacesUIUtils.jsm' ] diff --git a/application/palemoon/components/preferences/aboutPermissions.js b/application/palemoon/components/preferences/aboutPermissions.js index 106d45f89..31b48f88e 100644 --- a/application/palemoon/components/preferences/aboutPermissions.js +++ b/application/palemoon/components/preferences/aboutPermissions.js @@ -13,15 +13,15 @@ Cu.import("resource://gre/modules/AddonManager.jsm"); Cu.import("resource://gre/modules/NetUtil.jsm"); Cu.import("resource://gre/modules/ForgetAboutSite.jsm"); -let gFaviconService = Cc["@mozilla.org/browser/favicon-service;1"]. +var gFaviconService = Cc["@mozilla.org/browser/favicon-service;1"]. getService(Ci.nsIFaviconService); -let gPlacesDatabase = Cc["@mozilla.org/browser/nav-history-service;1"]. +var gPlacesDatabase = Cc["@mozilla.org/browser/nav-history-service;1"]. getService(Ci.nsPIPlacesDatabase). DBConnection. clone(true); -let gSitesStmt = gPlacesDatabase.createAsyncStatement( +var gSitesStmt = gPlacesDatabase.createAsyncStatement( "SELECT get_unreversed_host(rev_host) AS host " + "FROM moz_places " + "WHERE rev_host > '.' " + @@ -30,12 +30,12 @@ let gSitesStmt = gPlacesDatabase.createAsyncStatement( "ORDER BY MAX(frecency) DESC " + "LIMIT :limit"); -let gVisitStmt = gPlacesDatabase.createAsyncStatement( +var gVisitStmt = gPlacesDatabase.createAsyncStatement( "SELECT SUM(visit_count) AS count " + "FROM moz_places " + "WHERE rev_host = :rev_host"); -let gFlash = { +var gFlash = { name: "Shockwave Flash", betterName: "Adobe Flash", type: "application/x-shockwave-flash", @@ -290,7 +290,7 @@ Site.prototype = { * * Inspired by pageinfo/permissions.js */ -let PermissionDefaults = { +var PermissionDefaults = { UNKNOWN: Ci.nsIPermissionManager.UNKNOWN_ACTION, // 0 ALLOW: Ci.nsIPermissionManager.ALLOW_ACTION, // 1 DENY: Ci.nsIPermissionManager.DENY_ACTION, // 2 @@ -449,7 +449,7 @@ let PermissionDefaults = { /** * AboutPermissions manages the about:permissions page. */ -let AboutPermissions = { +var AboutPermissions = { /** * Maximum number of sites to return from the places database. */ diff --git a/application/palemoon/components/preferences/sanitize.js b/application/palemoon/components/preferences/sanitize.js index 893155e62..15e6f58f4 100644 --- a/application/palemoon/components/preferences/sanitize.js +++ b/application/palemoon/components/preferences/sanitize.js @@ -3,7 +3,7 @@ * 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/. */ -let gSanitizeDialog = Object.freeze({ +var gSanitizeDialog = Object.freeze({ onClearHistoryChanged: function () { let downloadsPref = document.getElementById("privacy.clearOnShutdown.downloads"); let historyPref = document.getElementById("privacy.clearOnShutdown.history"); diff --git a/application/palemoon/components/preferences/sync.js b/application/palemoon/components/preferences/sync.js index 9c7f1aa1f..f29728dbf 100644 --- a/application/palemoon/components/preferences/sync.js +++ b/application/palemoon/components/preferences/sync.js @@ -9,7 +9,7 @@ const PAGE_NO_ACCOUNT = 0; const PAGE_HAS_ACCOUNT = 1; const PAGE_NEEDS_UPDATE = 2; -let gSyncPane = { +var gSyncPane = { _stringBundle: null, prefArray: ["engine.bookmarks", "engine.passwords", "engine.prefs", "engine.tabs", "engine.history"], diff --git a/application/palemoon/components/sessionstore/SessionStorage.jsm b/application/palemoon/components/sessionstore/SessionStorage.jsm index 192352c49..64aef35a5 100644 --- a/application/palemoon/components/sessionstore/SessionStorage.jsm +++ b/application/palemoon/components/sessionstore/SessionStorage.jsm @@ -38,7 +38,7 @@ this.SessionStorage = { Object.freeze(SessionStorage); -let DomStorage = { +var DomStorage = { /** * Reads all session storage data from the given docShell. * @param aDocShell @@ -142,7 +142,7 @@ let DomStorage = { } }; -let History = { +var History = { /** * Returns a given history entry's URI. * @param aHistory diff --git a/application/palemoon/components/sessionstore/SessionStore.jsm b/application/palemoon/components/sessionstore/SessionStore.jsm index 5e09ff5c8..f7c495be8 100644 --- a/application/palemoon/components/sessionstore/SessionStore.jsm +++ b/application/palemoon/components/sessionstore/SessionStore.jsm @@ -88,7 +88,7 @@ XPCOMUtils.defineLazyServiceGetter(this, "gScreenManager", // retrieved from a given docShell if not already collected before. // This is made so they're automatically in sync with all nsIDocShell.allow* // properties. -let gDocShellCapabilities = (function () { +var gDocShellCapabilities = (function () { let caps; return docShell => { @@ -247,7 +247,7 @@ this.SessionStore = { // Freeze the SessionStore object. We don't want anyone to modify it. Object.freeze(SessionStore); -let SessionStoreInternal = { +var SessionStoreInternal = { QueryInterface: XPCOMUtils.generateQI([ Ci.nsIDOMEventListener, Ci.nsIObserver, @@ -2404,7 +2404,7 @@ let SessionStoreInternal = { for (var [host, isPinned] in Iterator(internalWindow.hosts)) { let list; try { - list = Services.cookies.getCookiesFromHost(host); + list = Services.cookies.getCookiesFromHost(host, {}); } catch (ex) { debug("getCookiesFromHost failed. Host: " + host); @@ -3655,7 +3655,7 @@ let SessionStoreInternal = { try { Services.cookies.add(cookie.host, cookie.path || "", cookie.name || "", cookie.value, !!cookie.secure, !!cookie.httponly, true, - "expiry" in cookie ? cookie.expiry : MAX_EXPIRY); + "expiry" in cookie ? cookie.expiry : MAX_EXPIRY, {}); } catch (ex) { Cu.reportError(ex); } // don't let a single cookie stop recovering } @@ -4471,7 +4471,7 @@ let SessionStoreInternal = { * pinned, visible and hidden tabs in that and FIFO order. Hidden tabs are only * restored with restore_hidden_tabs=true. */ -let TabRestoreQueue = { +var TabRestoreQueue = { // The separate buckets used to store tabs. tabs: {priority: [], visible: [], hidden: []}, @@ -4608,7 +4608,7 @@ let TabRestoreQueue = { // A map storing a closed window's state data until it goes aways (is GC'ed). // This ensures that API clients can still read (but not write) states of // windows they still hold a reference to but we don't. -let DyingWindowCache = { +var DyingWindowCache = { _data: new WeakMap(), has: function (window) { @@ -4631,7 +4631,7 @@ let DyingWindowCache = { // A set of tab attributes to persist. We will read a given list of tab // attributes when collecting tab data and will re-set those attributes when // the given tab data is restored to a new tab. -let TabAttributes = { +var TabAttributes = { _attrs: new Set(), // We never want to directly read or write those attributes. @@ -4677,7 +4677,7 @@ let TabAttributes = { // This is used to help meter the number of restoring tabs. This is the control // point for telling the next tab to restore. It gets attached to each gBrowser // via gBrowser.addTabsProgressListener -let gRestoreTabsProgressListener = { +var gRestoreTabsProgressListener = { onStateChange: function(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) { // Ignore state changes on browsers that we've already restored and state // changes that aren't applicable. diff --git a/application/palemoon/components/sessionstore/_SessionFile.jsm b/application/palemoon/components/sessionstore/_SessionFile.jsm index 9d40b9cca..62b4d1687 100644 --- a/application/palemoon/components/sessionstore/_SessionFile.jsm +++ b/application/palemoon/components/sessionstore/_SessionFile.jsm @@ -130,7 +130,7 @@ const TaskUtils = { } }; -let SessionFileInternal = { +var SessionFileInternal = { /** * A promise fulfilled once initialization is complete */ diff --git a/application/palemoon/components/sessionstore/nsSessionStartup.js b/application/palemoon/components/sessionstore/nsSessionStartup.js index 7f07e9050..04037c10e 100644 --- a/application/palemoon/components/sessionstore/nsSessionStartup.js +++ b/application/palemoon/components/sessionstore/nsSessionStartup.js @@ -50,7 +50,7 @@ function debug(aMsg) { Services.console.logStringMessage(aMsg); } -let gOnceInitializedDeferred = Promise.defer(); +var gOnceInitializedDeferred = Promise.defer(); /* :::::::: The Service ::::::::::::::: */ diff --git a/application/palemoon/components/shell/ShellService.jsm b/application/palemoon/components/shell/ShellService.jsm index 12e275bdb..74632b692 100644 --- a/application/palemoon/components/shell/ShellService.jsm +++ b/application/palemoon/components/shell/ShellService.jsm @@ -17,7 +17,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry", /** * Internal functionality to save and restore the docShell.allow* properties. */ -let ShellServiceInternal = { +var ShellServiceInternal = { /** * Used to determine whether or not to offer "Set as desktop background" * functionality. Even if shell service is available it is not diff --git a/application/palemoon/configure.in b/application/palemoon/configure.in index df5fb98a1..0dbb0d8bc 100644 --- a/application/palemoon/configure.in +++ b/application/palemoon/configure.in @@ -7,10 +7,10 @@ dnl file, You can obtain one at http://mozilla.org/MPL/2.0/. dnl Things we need to carry from confvars.sh AC_DEFINE(MOZ_PHOENIX) AC_SUBST(MOZ_PHOENIX) - AC_DEFINE(MC_PALEMOON) AC_SUBST(MC_PALEMOON) - +AC_DEFINE(MOZ_PHOENIX_EXTENSIONS) +AC_SUBST(MOZ_PHOENIX_EXTENSIONS) dnl Optional parts of the build. diff --git a/application/palemoon/fonts/moz.build b/application/palemoon/fonts/moz.build index 314d41bd0..02c027c46 100644 --- a/application/palemoon/fonts/moz.build +++ b/application/palemoon/fonts/moz.build @@ -4,7 +4,7 @@ # 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/. -if CONFIG['OS_ARCH'] in ('WINNT'): +if CONFIG['OS_ARCH'] in ('WINNT', 'Linux'): DIST_SUBDIR = '' FINAL_TARGET_FILES.fonts += [ 'TwemojiMozilla.ttf' diff --git a/application/palemoon/installer/Makefile.in b/application/palemoon/installer/Makefile.in index 7ba8ae9e8..368b16efc 100644 --- a/application/palemoon/installer/Makefile.in +++ b/application/palemoon/installer/Makefile.in @@ -34,9 +34,9 @@ ifdef MOZ_ENABLE_GNOME_COMPONENT DEFINES += -DMOZ_ENABLE_GNOME_COMPONENT=1 endif -ifdef MOZ_WIDGET_GTK +ifneq (,$(filter gtk%,$(MOZ_WIDGET_TOOLKIT))) DEFINES += -DMOZ_GTK=1 -ifdef MOZ_ENABLE_GTK3 +ifeq ($(MOZ_WIDGET_TOOLKIT),gtk3) DEFINES += -DMOZ_GTK3=1 endif endif @@ -65,18 +65,14 @@ endif DEFINES += -DMOZ_CHILD_PROCESS_NAME=$(MOZ_CHILD_PROCESS_NAME) # Set MSVC dlls version to package, if any. -ifdef WIN32_REDIST_DIR ifdef MOZ_NO_DEBUG_RTL +ifdef WIN32_REDIST_DIR DEFINES += -DMOZ_PACKAGE_MSVC_DLLS=1 DEFINES += -DMSVC_C_RUNTIME_DLL=$(MSVC_C_RUNTIME_DLL) DEFINES += -DMSVC_CXX_RUNTIME_DLL=$(MSVC_CXX_RUNTIME_DLL) -DEFINES += -DMSVC_OPENMP_DLL=$(MSVC_OPENMP_DLL) -ifdef MSVC_APPCRT_DLL -DEFINES += -DMSVC_APPCRT_DLL=$(MSVC_APPCRT_DLL) -endif -ifdef MSVC_DESKTOPCRT_DLL -DEFINES += -DMSVC_DESKTOPCRT_DLL=$(MSVC_DESKTOPCRT_DLL) endif +ifdef WIN_UCRT_REDIST_DIR +DEFINES += -DMOZ_PACKAGE_WIN_UCRT_DLLS=1 endif endif @@ -135,16 +131,17 @@ AB = $(firstword $(subst -, ,$(AB_CD))) DEFINES += -DAB=$(AB) DEFINES += -DMOZ_ICU_VERSION=$(MOZ_ICU_VERSION) -ifdef MOZ_NATIVE_ICU -DEFINES += -DMOZ_NATIVE_ICU +ifdef MOZ_SYSTEM_ICU +DEFINES += -DMOZ_SYSTEM_ICU endif -ifdef MOZ_SHARED_ICU -DEFINES += -DMOZ_SHARED_ICU +ifdef MOZ_ICU_DATA_ARCHIVE +DEFINES += -DMOZ_ICU_DATA_ARCHIVE endif ifdef MOZ_JEMALLOC3 DEFINES += -DMOZ_JEMALLOC3 endif DEFINES += -DMOZ_ICU_DBG_SUFFIX=$(MOZ_ICU_DBG_SUFFIX) +DEFINES += -DICU_DATA_FILE=$(ICU_DATA_FILE) ifdef CLANG_CXX DEFINES += -DCLANG_CXX endif @@ -161,7 +158,7 @@ endif libs:: - $(MAKE) -C $(DEPTH)/browser/locales langpack + $(MAKE) -C $(DEPTH)/application/palemoon/locales langpack ifeq (WINNT,$(OS_ARCH)) PKGCOMP_FIND_OPTS = diff --git a/application/palemoon/installer/package-manifest.in b/application/palemoon/installer/package-manifest.in index ffe033596..f7b12838b 100644 --- a/application/palemoon/installer/package-manifest.in +++ b/application/palemoon/installer/package-manifest.in @@ -35,10 +35,11 @@ #ifdef XP_MACOSX ; Mac bundle stuff @APPNAME@/Contents/Info.plist +@APPNAME@/Contents/Library/LaunchServices @APPNAME@/Contents/PkgInfo @RESPATH@/firefox.icns @RESPATH@/document.icns -@RESPATH@/@AB@.lproj/* +@RESPATH@/@LPROJ_ROOT@.lproj/* #endif [@AB_CD@] @@ -51,14 +52,14 @@ @RESPATH@/browser/defaults/profile/localstore.rdf @RESPATH@/browser/defaults/profile/mimeTypes.rdf @RESPATH@/dictionaries/* -#if defined(XP_WIN) +#if defined(XP_WIN) || defined(XP_LINUX) @RESPATH@/fonts/* #endif @RESPATH@/hyphenation/* @RESPATH@/browser/@PREF_DIR@/palemoon-l10n.js @RESPATH@/browser/searchplugins/* -#ifdef XP_WIN32 -@RESPATH@/uninstall/helper.exe +#ifdef HAVE_MAKENSISU +@BINPATH@/uninstall/helper.exe #endif #ifdef MOZ_UPDATER @RESPATH@/update.locale @@ -67,10 +68,6 @@ [xpcom] @RESPATH@/dependentlibs.list -#ifdef GKMEDIAS_SHARED_LIBRARY -@BINPATH@/@DLL_PREFIX@gkmedias@DLL_SUFFIX@ -#endif -@BINPATH@/@DLL_PREFIX@mozalloc@DLL_SUFFIX@ #ifdef MOZ_SHARED_MOZGLUE @BINPATH@/@DLL_PREFIX@mozglue@DLL_SUFFIX@ #endif @@ -80,7 +77,7 @@ #ifdef MOZ_DMD @BINPATH@/@DLL_PREFIX@dmd@DLL_SUFFIX@ #endif -#ifndef MOZ_NATIVE_NSPR +#ifndef MOZ_SYSTEM_NSPR #ifndef MOZ_FOLD_LIBS @BINPATH@/@DLL_PREFIX@nspr4@DLL_SUFFIX@ @BINPATH@/@DLL_PREFIX@plc4@DLL_SUFFIX@ @@ -100,60 +97,32 @@ #endif #ifdef XP_WIN32 @BINPATH@/plugin-hang-ui@BIN_SUFFIX@ -#ifdef MOZ_PACKAGE_MSVC_DLLS +#if MOZ_PACKAGE_MSVC_DLLS @BINPATH@/@MSVC_C_RUNTIME_DLL@ @BINPATH@/@MSVC_CXX_RUNTIME_DLL@ -@BINPATH@/@MSVC_OPENMP_DLL@ -#ifdef MSVC_APPCRT_DLL -@BINPATH@/@MSVC_APPCRT_DLL@ -#endif -#ifdef MSVC_DESKTOPCRT_DLL -@BINPATH@/@MSVC_DESKTOPCRT_DLL@ -#endif -#endif -#endif -#ifndef MOZ_NATIVE_ICU -#ifdef MOZ_SHARED_ICU -#ifdef XP_WIN -#ifdef MOZ_DEBUG -@BINPATH@/icudt@MOZ_ICU_DBG_SUFFIX@@MOZ_ICU_VERSION@.dll -@BINPATH@/icuin@MOZ_ICU_DBG_SUFFIX@@MOZ_ICU_VERSION@.dll -@BINPATH@/icuuc@MOZ_ICU_DBG_SUFFIX@@MOZ_ICU_VERSION@.dll -#else -@BINPATH@/icudt@MOZ_ICU_VERSION@.dll -@BINPATH@/icuin@MOZ_ICU_VERSION@.dll -@BINPATH@/icuuc@MOZ_ICU_VERSION@.dll #endif -#elif defined(XP_MACOSX) -@BINPATH@/libicudata.@MOZ_ICU_VERSION@.dylib -@BINPATH@/libicui18n.@MOZ_ICU_VERSION@.dylib -@BINPATH@/libicuuc.@MOZ_ICU_VERSION@.dylib -#elif defined(XP_UNIX) -@BINPATH@/libicudata.so.@MOZ_ICU_VERSION@ -@BINPATH@/libicui18n.so.@MOZ_ICU_VERSION@ -@BINPATH@/libicuuc.so.@MOZ_ICU_VERSION@ +#if MOZ_PACKAGE_WIN_UCRT_DLLS +@BINPATH@/api-ms-win-*.dll +@BINPATH@/ucrtbase.dll #endif #endif -#endif -#ifdef MOZ_REPLACE_MALLOC -#ifndef MOZ_JEMALLOC3 -@BINPATH@/@DLL_PREFIX@replace_jemalloc@DLL_SUFFIX@ -#endif +#ifdef MOZ_ICU_DATA_ARCHIVE +@RESPATH@/@ICU_DATA_FILE@ #endif #ifdef MOZ_GTK3 @BINPATH@/@DLL_PREFIX@mozgtk@DLL_SUFFIX@ -@BINPATH@/@DLL_PREFIX@mozgtk2@DLL_SUFFIX@ +@BINPATH@/gtk2/@DLL_PREFIX@mozgtk@DLL_SUFFIX@ #endif [browser] ; [Base Browser Files] #ifndef XP_UNIX @BINPATH@/@MOZ_APP_NAME@.exe -@BINPATH@/palemoon.VisualElementsManifest.xml +@BINPATH@/@MOZ_APP_NAME@.VisualElementsManifest.xml @BINPATH@/browser/VisualElements/VisualElements_150.png @BINPATH@/browser/VisualElements/VisualElements_70.png #else -@RESPATH@/@MOZ_APP_NAME@-bin +@BINPATH@/@MOZ_APP_NAME@-bin @BINPATH@/@MOZ_APP_NAME@ #endif @RESPATH@/application.ini @@ -161,490 +130,67 @@ @RESPATH@/update-settings.ini #endif @RESPATH@/platform.ini -#ifndef MOZ_NATIVE_SQLITE +#ifndef MOZ_SYSTEM_SQLITE #ifndef MOZ_FOLD_LIBS @BINPATH@/@DLL_PREFIX@mozsqlite3@DLL_SUFFIX@ #endif #endif +@BINPATH@/@DLL_PREFIX@lgpllibs@DLL_SUFFIX@ +#ifdef MOZ_FFVPX +@BINPATH@/@DLL_PREFIX@mozavutil@DLL_SUFFIX@ +@BINPATH@/@DLL_PREFIX@mozavcodec@DLL_SUFFIX@ +#endif @RESPATH@/browser/blocklist.xml #ifdef XP_UNIX #ifndef XP_MACOSX @RESPATH@/run-mozilla.sh #endif #endif +#ifdef XP_WIN +#ifdef _AMD64_ +@BINPATH@/@DLL_PREFIX@qipcap64@DLL_SUFFIX@ +#else +@BINPATH@/@DLL_PREFIX@qipcap@DLL_SUFFIX@ +#endif +#endif ; [Components] -@RESPATH@/browser/components/components.manifest -@RESPATH@/components/alerts.xpt +@RESPATH@/components/* +@RESPATH@/browser/components/* +#ifdef MOZ_ARTIFACT_BUILDS +#endif #ifdef ACCESSIBILITY #ifdef XP_WIN32 +@BINPATH@/Accessible.tlb @BINPATH@/AccessibleMarshal.dll +@BINPATH@/IA2Marshal.dll +#endif #endif -@RESPATH@/components/accessibility.xpt -#endif -@RESPATH@/components/appshell.xpt -@RESPATH@/components/appstartup.xpt -@RESPATH@/components/autocomplete.xpt -@RESPATH@/components/autoconfig.xpt -@RESPATH@/components/browser-element.xpt -@RESPATH@/browser/components/browsercompsbase.xpt -@RESPATH@/browser/components/browser-feeds.xpt -@RESPATH@/components/caps.xpt -@RESPATH@/components/chrome.xpt -@RESPATH@/components/commandhandler.xpt -@RESPATH@/components/commandlines.xpt -@RESPATH@/components/compartments.xpt -@RESPATH@/components/composer.xpt -@RESPATH@/components/content_events.xpt -@RESPATH@/components/content_html.xpt -@RESPATH@/components/content_goannamediaplugins.xpt #ifdef MOZ_WEBRTC -@RESPATH@/components/content_webrtc.xpt -#endif -@RESPATH@/components/content_xslt.xpt -@RESPATH@/components/cookie.xpt -@RESPATH@/components/devtools_security.xpt -@RESPATH@/components/directory.xpt -@RESPATH@/components/docshell.xpt -@RESPATH@/components/dom.xpt -#ifdef MOZ_ACTIVITIES -@RESPATH@/components/dom_activities.xpt -@RESPATH@/components/dom_messages.xpt -#endif -@RESPATH@/components/dom_apps.xpt -@RESPATH@/components/dom_base.xpt -@RESPATH@/components/dom_system.xpt -#ifdef MOZ_B2G_BT -@RESPATH@/components/dom_bluetooth.xpt -#endif -@RESPATH@/components/dom_canvas.xpt -@RESPATH@/components/dom_alarm.xpt -@RESPATH@/components/dom_core.xpt -@RESPATH@/components/dom_css.xpt -@RESPATH@/components/dom_devicestorage.xpt -@RESPATH@/components/dom_events.xpt -@RESPATH@/components/dom_geolocation.xpt -@RESPATH@/components/dom_media.xpt -@RESPATH@/components/dom_network.xpt -@RESPATH@/components/dom_notification.xpt -@RESPATH@/components/dom_html.xpt -@RESPATH@/components/dom_offline.xpt -@RESPATH@/components/dom_json.xpt -@RESPATH@/components/dom_power.xpt -@RESPATH@/components/dom_quota.xpt -@RESPATH@/components/dom_range.xpt -@RESPATH@/components/dom_security.xpt -@RESPATH@/components/dom_settings.xpt -@RESPATH@/components/dom_permissionsettings.xpt -@RESPATH@/components/dom_sidebar.xpt -@RESPATH@/components/dom_cellbroadcast.xpt -@RESPATH@/components/dom_mobilemessage.xpt -@RESPATH@/components/dom_storage.xpt -@RESPATH@/components/dom_stylesheets.xpt -@RESPATH@/components/dom_telephony.xpt -@RESPATH@/components/dom_traversal.xpt -@RESPATH@/components/dom_tv.xpt -@RESPATH@/components/dom_voicemail.xpt +#endif #ifdef MOZ_WEBSPEECH -@RESPATH@/components/dom_webspeechrecognition.xpt -#endif -@RESPATH@/components/dom_workers.xpt -@RESPATH@/components/dom_xbl.xpt -@RESPATH@/components/dom_xpath.xpt -@RESPATH@/components/dom_xul.xpt -#ifdef MOZ_GAMEPAD -@RESPATH@/components/dom_gamepad.xpt -#endif -#ifdef MOZ_PAY -@RESPATH@/components/dom_payment.xpt -#endif -@RESPATH@/components/dom_presentation.xpt -@RESPATH@/components/downloads.xpt -@RESPATH@/components/editor.xpt -@RESPATH@/components/embed_base.xpt -@RESPATH@/components/extensions.xpt -@RESPATH@/components/exthandler.xpt -@RESPATH@/components/exthelper.xpt -@RESPATH@/components/fastfind.xpt -@RESPATH@/components/feeds.xpt -#ifdef MOZ_GTK -@RESPATH@/components/filepicker.xpt -#endif -@RESPATH@/components/find.xpt -@RESPATH@/browser/components/fuel.xpt -@RESPATH@/components/gfx.xpt -@RESPATH@/components/html5.xpt -@RESPATH@/components/htmlparser.xpt -@RESPATH@/components/identity.xpt -@RESPATH@/components/imglib2.xpt -@RESPATH@/components/inspector.xpt -@RESPATH@/components/intl.xpt -@RESPATH@/components/jar.xpt -@RESPATH@/components/jsdebugger.xpt -@RESPATH@/components/jsdownloads.xpt -@RESPATH@/browser/components/jsinspector.xpt -@RESPATH@/components/layout_base.xpt - -#ifdef NS_PRINTING -@RESPATH@/components/layout_printing.xpt -#endif -@RESPATH@/components/layout_xul_tree.xpt -@RESPATH@/components/layout_xul.xpt -@RESPATH@/components/locale.xpt -@RESPATH@/components/lwbrk.xpt -@RESPATH@/browser/components/migration.xpt -@RESPATH@/components/mimetype.xpt -@RESPATH@/components/mozfind.xpt -@RESPATH@/components/necko_about.xpt -@RESPATH@/components/necko_cache.xpt -@RESPATH@/components/necko_cache2.xpt -@RESPATH@/components/necko_cookie.xpt -@RESPATH@/components/necko_dns.xpt -@RESPATH@/components/necko_file.xpt -@RESPATH@/components/necko_ftp.xpt -@RESPATH@/components/necko_http.xpt -@RESPATH@/components/necko_res.xpt -@RESPATH@/components/necko_socket.xpt -@RESPATH@/components/necko_strconv.xpt -@RESPATH@/components/necko_viewsource.xpt -@RESPATH@/components/necko_websocket.xpt -#ifdef NECKO_WIFI -@RESPATH@/components/necko_wifi.xpt #endif -@RESPATH@/components/necko_wyciwyg.xpt -@RESPATH@/components/necko.xpt -@RESPATH@/components/loginmgr.xpt -@RESPATH@/components/parentalcontrols.xpt -#ifdef MOZ_WEBRTC -@RESPATH@/components/peerconnection.xpt -#endif -@RESPATH@/components/places.xpt -@RESPATH@/components/plugin.xpt -@RESPATH@/components/pref.xpt -@RESPATH@/components/prefetch.xpt -@RESPATH@/components/profiler.xpt -@RESPATH@/components/rdf.xpt -@RESPATH@/components/satchel.xpt -@RESPATH@/components/saxparser.xpt -@RESPATH@/browser/components/sessionstore.xpt -@RESPATH@/components/services-crypto-component.xpt -#ifdef MOZ_CAPTIVEDETECT -@RESPATH@/components/captivedetect.xpt -#endif -@RESPATH@/browser/components/shellservice.xpt -#ifdef MOZ_BROWSER_STATUSBAR -@RESPATH@/browser/components/status4evar.xpt -#endif -@RESPATH@/components/shistory.xpt -@RESPATH@/components/spellchecker.xpt -@RESPATH@/components/storage.xpt -@RESPATH@/components/toolkit_asyncshutdown.xpt -@RESPATH@/components/toolkit_filewatcher.xpt -@RESPATH@/components/toolkit_finalizationwitness.xpt -@RESPATH@/components/toolkit_formautofill.xpt -@RESPATH@/components/toolkit_osfile.xpt -@RESPATH@/components/toolkit_xulstore.xpt -@RESPATH@/components/toolkitprofile.xpt -#ifdef MOZ_ENABLE_XREMOTE -@RESPATH@/components/toolkitremote.xpt -#endif -@RESPATH@/components/txtsvc.xpt -@RESPATH@/components/txmgr.xpt -@RESPATH@/components/uconv.xpt -@RESPATH@/components/unicharutil.xpt -@RESPATH@/components/update.xpt -@RESPATH@/components/uriloader.xpt -@RESPATH@/components/urlformatter.xpt -@RESPATH@/components/webBrowser_core.xpt -@RESPATH@/components/webbrowserpersist.xpt -@RESPATH@/components/widget.xpt -#ifdef XP_MACOSX -@RESPATH@/components/widget_cocoa.xpt -#endif -@RESPATH@/components/windowds.xpt -@RESPATH@/components/windowwatcher.xpt -@RESPATH@/components/xpcom_base.xpt -@RESPATH@/components/xpcom_system.xpt -@RESPATH@/components/xpcom_components.xpt -@RESPATH@/components/xpcom_ds.xpt -@RESPATH@/components/xpcom_io.xpt -@RESPATH@/components/xpcom_threads.xpt -@RESPATH@/components/xpcom_xpti.xpt -@RESPATH@/components/xpconnect.xpt -@RESPATH@/components/xulapp.xpt -@RESPATH@/components/xul.xpt -@RESPATH@/components/xultmpl.xpt -@RESPATH@/components/zipwriter.xpt -@RESPATH@/components/telemetry.xpt - -; JavaScript components -@RESPATH@/components/ChromeNotifications.js -@RESPATH@/components/ChromeNotifications.manifest -@RESPATH@/components/ConsoleAPI.manifest -@RESPATH@/components/ConsoleAPIStorage.js -@RESPATH@/components/BrowserElementParent.manifest -@RESPATH@/components/BrowserElementParent.js -@RESPATH@/components/FeedProcessor.manifest -@RESPATH@/components/FeedProcessor.js -@RESPATH@/browser/components/BrowserFeeds.manifest -@RESPATH@/browser/components/FeedConverter.js -@RESPATH@/browser/components/FeedWriter.js -@RESPATH@/browser/components/fuelApplication.manifest -@RESPATH@/browser/components/fuelApplication.js -@RESPATH@/browser/components/WebContentConverter.js -@RESPATH@/browser/components/BrowserComponents.manifest -@RESPATH@/browser/components/nsBrowserContentHandler.js -@RESPATH@/browser/components/nsBrowserGlue.js -@RESPATH@/browser/components/nsSetDefaultBrowser.manifest -@RESPATH@/browser/components/nsSetDefaultBrowser.js -@RESPATH@/browser/components/BrowserDownloads.manifest -@RESPATH@/browser/components/DownloadsStartup.js -@RESPATH@/browser/components/DownloadsUI.js -@RESPATH@/browser/components/BrowserPlaces.manifest -@RESPATH@/components/Downloads.manifest -@RESPATH@/components/DownloadLegacy.js -@RESPATH@/components/BrowserPageThumbs.manifest -@RESPATH@/components/crashmonitor.manifest -@RESPATH@/components/nsCrashMonitor.js -@RESPATH@/components/SiteSpecificUserAgent.js -@RESPATH@/components/SiteSpecificUserAgent.manifest -@RESPATH@/components/toolkitsearch.manifest -@RESPATH@/components/nsSearchService.js -@RESPATH@/components/nsSearchSuggestions.js -@RESPATH@/components/nsSidebar.js -@RESPATH@/components/passwordmgr.manifest -@RESPATH@/components/nsLoginInfo.js -@RESPATH@/components/nsLoginManager.js -@RESPATH@/components/nsLoginManagerPrompter.js -@RESPATH@/components/storage-json.js -@RESPATH@/components/crypto-SDR.js -@RESPATH@/components/jsconsole-clhandler.manifest -@RESPATH@/components/jsconsole-clhandler.js -#ifdef MOZ_DEVTOOLS -@RESPATH@/browser/components/devtools-startup.manifest -@RESPATH@/browser/components/devtools-startup.js -#endif -@RESPATH@/components/webvtt.xpt -@RESPATH@/components/WebVTT.manifest -@RESPATH@/components/WebVTTParserWrapper.js #ifdef MOZ_GTK -@RESPATH@/components/nsFilePicker.manifest -@RESPATH@/components/nsFilePicker.js -#endif -@RESPATH@/components/nsHelperAppDlg.manifest -@RESPATH@/components/nsHelperAppDlg.js -@RESPATH@/components/NetworkGeolocationProvider.manifest -@RESPATH@/components/NetworkGeolocationProvider.js -@RESPATH@/components/extensions.manifest -@RESPATH@/components/addonManager.js -@RESPATH@/components/amContentHandler.js -@RESPATH@/components/amInstallTrigger.js -@RESPATH@/components/amWebInstallListener.js -@RESPATH@/components/nsBlocklistService.js -#ifdef MOZ_UPDATER -@RESPATH@/components/nsUpdateService.manifest -@RESPATH@/components/nsUpdateService.js -@RESPATH@/components/nsUpdateServiceStub.js -#endif -@RESPATH@/components/nsUpdateTimerManager.manifest -@RESPATH@/components/nsUpdateTimerManager.js -@RESPATH@/components/addoncompat.manifest -@RESPATH@/components/multiprocessShims.js -@RESPATH@/components/remoteTagService.js -@RESPATH@/components/pluginGlue.manifest -@RESPATH@/components/ProcessSingleton.manifest -@RESPATH@/components/MainProcessSingleton.js -@RESPATH@/components/ContentProcessSingleton.js -@RESPATH@/browser/components/nsSessionStore.manifest -@RESPATH@/browser/components/nsSessionStartup.js -@RESPATH@/browser/components/nsSessionStore.js -@RESPATH@/components/nsURLFormatter.manifest -@RESPATH@/components/nsURLFormatter.js -@RESPATH@/browser/components/@DLL_PREFIX@browsercomps@DLL_SUFFIX@ -@RESPATH@/components/txEXSLTRegExFunctions.manifest -@RESPATH@/components/txEXSLTRegExFunctions.js -@RESPATH@/components/toolkitplaces.manifest -@RESPATH@/components/nsLivemarkService.js -@RESPATH@/components/nsTaggingService.js -@RESPATH@/components/nsPlacesAutoComplete.manifest -@RESPATH@/components/nsPlacesAutoComplete.js -@RESPATH@/components/UnifiedComplete.manifest -@RESPATH@/components/UnifiedComplete.js -@RESPATH@/components/nsPlacesExpiration.js -@RESPATH@/browser/components/PlacesProtocolHandler.js -@RESPATH@/components/PlacesCategoriesStarter.js -@RESPATH@/components/ColorAnalyzer.js -@RESPATH@/components/PageThumbsProtocol.js -@RESPATH@/components/nsDefaultCLH.manifest -@RESPATH@/components/nsDefaultCLH.js -@RESPATH@/components/nsContentPrefService.manifest -@RESPATH@/components/nsContentPrefService.js -@RESPATH@/components/nsContentDispatchChooser.manifest -@RESPATH@/components/nsContentDispatchChooser.js -@RESPATH@/components/nsHandlerService.manifest -@RESPATH@/components/nsHandlerService.js -@RESPATH@/components/nsWebHandlerApp.manifest -@RESPATH@/components/nsWebHandlerApp.js -@RESPATH@/components/satchel.manifest -@RESPATH@/components/nsFormAutoComplete.js -@RESPATH@/components/nsFormHistory.js -@RESPATH@/components/FormHistoryStartup.js -@RESPATH@/components/nsInputListAutoComplete.js -@RESPATH@/components/formautofill.manifest -@RESPATH@/components/FormAutofillContentService.js -@RESPATH@/components/FormAutofillStartup.js -@RESPATH@/components/contentAreaDropListener.manifest -@RESPATH@/components/contentAreaDropListener.js -@RESPATH@/browser/components/BrowserProfileMigrators.manifest -@RESPATH@/browser/components/ProfileMigrator.js -@RESPATH@/browser/components/ChromeProfileMigrator.js -@RESPATH@/browser/components/FirefoxProfileMigrator.js -#ifdef MOZ_BROWSER_STATUSBAR -@RESPATH@/browser/components/status4evar.js -@RESPATH@/browser/components/status4evar.manifest #endif -#ifdef XP_WIN -@RESPATH@/browser/components/IEProfileMigrator.js -@RESPATH@/browser/components/SafariProfileMigrator.js +#ifdef NS_PRINTING #endif -#ifdef XP_MACOSX -@RESPATH@/browser/components/SafariProfileMigrator.js -#endif -#ifdef MOZ_ENABLE_DBUS -@RESPATH@/components/@DLL_PREFIX@dbusservice@DLL_SUFFIX@ -#endif -#ifdef MOZ_ENABLE_GNOME_COMPONENT -@RESPATH@/components/@DLL_PREFIX@mozgnome@DLL_SUFFIX@ -#endif -#ifdef MOZ_ENABLE_GNOMEVFS -@RESPATH@/components/@DLL_PREFIX@nkgnomevfs@DLL_SUFFIX@ -#endif -#if defined(MOZ_ENABLE_DBUS) || defined(MOZ_ENABLE_GNOME_COMPONENT) || defined(MOZ_ENABLE_GNOMEVFS) -@RESPATH@/components/components.manifest -#endif -@RESPATH@/components/nsINIProcessor.manifest -@RESPATH@/components/nsINIProcessor.js -@RESPATH@/components/nsPrompter.manifest -@RESPATH@/components/nsPrompter.js -#ifdef MOZ_DATA_REPORTING -@RESPATH@/components/DataReporting.manifest -@RESPATH@/components/DataReportingService.js -#endif -#ifdef MOZ_SERVICES_HEALTHREPORT -@RESPATH@/components/HealthReportComponents.manifest -@RESPATH@/browser/components/SelfSupportService.manifest -@RESPATH@/browser/components/SelfSupportService.js -#endif -#ifdef MOZ_SERVICES_SYNC -@RESPATH@/components/SyncComponents.manifest -@RESPATH@/components/Weave.js -#endif -#ifdef MOZ_CAPTIVEDETECT -@RESPATH@/components/CaptivePortalDetectComponents.manifest -@RESPATH@/components/captivedetect.js -#endif -@RESPATH@/components/servicesComponents.manifest -@RESPATH@/components/cryptoComponents.manifest -@RESPATH@/components/TelemetryStartup.js -@RESPATH@/components/TelemetryStartup.manifest -@RESPATH@/components/XULStore.js -@RESPATH@/components/XULStore.manifest -@RESPATH@/components/messageWakeupService.js -@RESPATH@/components/messageWakeupService.manifest -@RESPATH@/components/SettingsManager.js -@RESPATH@/components/SettingsManager.manifest -@RESPATH@/components/Webapps.js -@RESPATH@/components/Webapps.manifest -@RESPATH@/components/AppsService.js -@RESPATH@/components/AppsService.manifest -@RESPATH@/components/nsDOMIdentity.js -@RESPATH@/components/nsIDService.js -@RESPATH@/components/Identity.manifest -@RESPATH@/components/recording-cmdline.js -@RESPATH@/components/recording-cmdline.manifest -@RESPATH@/components/htmlMenuBuilder.js -@RESPATH@/components/htmlMenuBuilder.manifest - -@RESPATH@/components/RequestSync.manifest -@RESPATH@/components/RequestSyncManager.js -@RESPATH@/components/RequestSyncScheduler.js - -@RESPATH@/components/PermissionSettings.js -@RESPATH@/components/PermissionSettings.manifest -@RESPATH@/components/ContactManager.js -@RESPATH@/components/ContactManager.manifest -@RESPATH@/components/PhoneNumberService.js -@RESPATH@/components/PhoneNumberService.manifest -@RESPATH@/components/NotificationStorage.js -@RESPATH@/components/NotificationStorage.manifest -@RESPATH@/components/AlarmsManager.js -@RESPATH@/components/AlarmsManager.manifest -@RESPATH@/components/Push.js -@RESPATH@/components/Push.manifest -@RESPATH@/components/PushServiceLauncher.js - -@RESPATH@/components/SlowScriptDebug.manifest -@RESPATH@/components/SlowScriptDebug.js - -#ifndef RELEASE_BUILD -@RESPATH@/components/InterAppComm.manifest -@RESPATH@/components/InterAppCommService.js -@RESPATH@/components/InterAppConnection.js -@RESPATH@/components/InterAppMessagePort.js +#ifdef MOZ_ENABLE_PROFILER_SPS #endif - -@RESPATH@/components/TCPSocket.js -@RESPATH@/components/TCPServerSocket.js -@RESPATH@/components/TCPSocketParentIntermediary.js -@RESPATH@/components/TCPSocket.manifest - -#ifdef MOZ_ACTIVITIES -@RESPATH@/components/SystemMessageCache.js -@RESPATH@/components/SystemMessageInternal.js -@RESPATH@/components/SystemMessageManager.js -@RESPATH@/components/SystemMessageManager.manifest - -@RESPATH@/components/Activities.manifest -@RESPATH@/components/ActivityProxy.js -@RESPATH@/components/ActivityRequestHandler.js -@RESPATH@/components/ActivityWrapper.js -@RESPATH@/components/ActivityMessageConfigurator.js +#ifdef ENABLE_INTL_API #endif - -#ifdef MOZ_PAY -@RESPATH@/components/Payment.js -@RESPATH@/components/PaymentFlowInfo.js -@RESPATH@/components/Payment.manifest +#ifdef NECKO_WIFI #endif - #ifdef MOZ_WEBRTC -@RESPATH@/components/PeerConnection.js -@RESPATH@/components/PeerConnection.manifest +#endif +#ifdef MOZ_ENABLE_PROFILER_SPS +#endif +#ifdef MOZ_ENABLE_XREMOTE +#endif +#ifdef XP_MACOSX #endif @RESPATH@/chrome/marionette@JAREXT@ @RESPATH@/chrome/marionette.manifest -@RESPATH@/components/MarionetteComponents.manifest -@RESPATH@/components/marionettecomponent.js - -#ifdef MOZ_WEBSPEECH -@RESPATH@/components/dom_webspeechsynth.xpt -#endif - -@RESPATH@/components/nsAsyncShutdown.manifest -@RESPATH@/components/nsAsyncShutdown.js - -@RESPATH@/components/PresentationDeviceInfoManager.manifest -@RESPATH@/components/PresentationDeviceInfoManager.js - -; InputMethod API -@RESPATH@/components/MozKeyboard.js -@RESPATH@/components/InputMethod.manifest - -#if defined(ENABLE_TESTS) && defined(MOZ_DEBUG) -@RESPATH@/components/TestInterfaceJS.js -@RESPATH@/components/TestInterfaceJS.manifest -#endif ; Modules @RESPATH@/browser/modules/* @@ -658,15 +204,17 @@ #ifdef MOZ_D3DCOMPILER_VISTA_DLL @BINPATH@/@MOZ_D3DCOMPILER_VISTA_DLL@ #endif - #endif # MOZ_ANGLE_RENDERER ; [Browser Chrome Files] @RESPATH@/browser/chrome.manifest @RESPATH@/browser/chrome/browser@JAREXT@ @RESPATH@/browser/chrome/browser.manifest -@RESPATH@/browser/extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}/install.rdf +#ifdef XP_WIN +@RESPATH@/browser/extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}/chrome.manifest +#endif @RESPATH@/browser/extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}/icon.png +@RESPATH@/browser/extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}/install.rdf @RESPATH@/chrome/toolkit@JAREXT@ @RESPATH@/chrome/toolkit.manifest @RESPATH@/chrome/recording.manifest @@ -686,7 +234,7 @@ @RESPATH@/browser/chrome/devtools@JAREXT@ @RESPATH@/browser/chrome/devtools.manifest @RESPATH@/browser/@PREF_DIR@/devtools.js - + ; shell icons #ifdef XP_UNIX #ifndef XP_MACOSX @@ -703,28 +251,16 @@ ; All the pref files must be part of base to prevent migration bugs @RESPATH@/browser/@PREF_DIR@/palemoon.js @RESPATH@/browser/@PREF_DIR@/palemoon-branding.js -@RESPATH@/goanna.js -@RESPATH@/defaults/autoconfig/platform.js +@RESPATH@/greprefs.js @RESPATH@/defaults/autoconfig/prefcalls.js -@RESPATH@/browser/defaults/profile/prefs.js -#ifndef LIBXUL_SDK ; Warning: changing the path to channel-prefs.js can cause bugs (Bug 756325) ; Technically this is an app pref file, but we are keeping it in the original ; gre location for now. @RESPATH@/defaults/pref/channel-prefs.js -#else -; For Fx-on-xr, channel-prefs lives with the app preferences. (Bug 762588) -@RESPATH@/@PREF_DIR@/channel-prefs.js -#endif ; Services (gre) prefs -#ifdef MOZ_SERVICES_NOTIFICATIONS -@RESPATH@/defaults/pref/services-notifications.js -#endif -#ifdef MOZ_SERVICES_SYNC @RESPATH@/defaults/pref/services-sync.js -#endif ; [Layout Engine Resources] ; Style Sheets, Graphics and other Resources used by the layout engine. @@ -759,7 +295,7 @@ @RESPATH@/res/fonts/* @RESPATH@/res/dtd/* @RESPATH@/res/html/* -#if defined(XP_MACOSX) || defined(XP_WIN) +#if defined(XP_MACOSX) ; For SafariProfileMigrator.js. @RESPATH@/res/langGroups.properties #endif @@ -771,15 +307,13 @@ ; svg @RESPATH@/res/svg.css -@RESPATH@/components/dom_svg.xpt -@RESPATH@/components/dom_smil.xpt ; [Personal Security Manager] ; ; NSS libraries are signed in the staging directory, ; meaning their .chk files are created there directly. ; -#ifndef MOZ_NATIVE_NSS +#ifndef MOZ_SYSTEM_NSS #if defined(XP_LINUX) && !defined(ANDROID) @BINPATH@/@DLL_PREFIX@freeblpriv3@DLL_SUFFIX@ #else @@ -799,20 +333,22 @@ #endif @RESPATH@/chrome/pippki@JAREXT@ @RESPATH@/chrome/pippki.manifest -@RESPATH@/components/pipboot.xpt -@RESPATH@/components/pipnss.xpt -@RESPATH@/components/pippki.xpt ; For process sandboxing #if defined(MOZ_SANDBOX) #if defined(XP_WIN) -@BINPATH@/@DLL_PREFIX@sandboxbroker@DLL_SUFFIX@ #if defined(WOW_HELPER) @BINPATH@/wow_helper.exe #endif #endif #endif +#if defined(MOZ_SANDBOX) +#if defined(XP_LINUX) +@BINPATH@/@DLL_PREFIX@mozsandbox@DLL_SUFFIX@ +#endif +#endif + ; for Solaris SPARC #ifdef SOLARIS bin/libfreebl_32fpu_3.so @@ -830,13 +366,7 @@ bin/libfreebl_32int64_3.so #endif #endif -@RESPATH@/components/DataStore.manifest -@RESPATH@/components/DataStoreImpl.js -@RESPATH@/components/dom_datastore.xpt - ; Shutdown Terminator -@RESPATH@/components/nsTerminatorTelemetry.js -@RESPATH@/components/terminator.manifest #if defined(CLANG_CXX) #if defined(MOZ_ASAN) || defined(MOZ_TSAN) @@ -845,5 +375,5 @@ bin/libfreebl_32int64_3.so #endif #if defined(MOZ_ASAN) && defined(CLANG_CL) -@BINPATH@/clang_rt.asan_dynamic-i386.dll +@BINPATH@/clang_rt.asan_dynamic-*.dll #endif diff --git a/application/palemoon/installer/removed-files.in b/application/palemoon/installer/removed-files.in index 8bfd72e4e..8801d1cb0 100644 --- a/application/palemoon/installer/removed-files.in +++ b/application/palemoon/installer/removed-files.in @@ -65,29 +65,27 @@ # platforms. # Common File Removals +# This is located under the "distribution/" directory and it was added before +# Firefox 27 +@DIR_MACOS@distribution/extensions/testpilot@labs.mozilla.com.xpi -# Some users are ending up with unpacked chrome instead of omni.ja. This -# causes updates to break badly, see bug 1063052. Removing the toplevel -# chrome.manifest causes us to use the updated omni.ja. -#ifndef MOZ_GTK - @DIR_MACOS@chrome.manifest - #ifdef XP_MACOSX - @DIR_RESOURCES@chrome.manifest - #endif +# Mac OS X v2 signing removals +#ifdef XP_MACOSX + @DIR_MACOS@active-update.xml + @DIR_MACOS@update-settings.ini + @DIR_MACOS@updates.xml + @DIR_MACOS@defaults/* + @DIR_MACOS@updates/* #endif # Common Directory removals +@DIR_MACOS@chrome/ #ifdef XP_UNIX #ifndef XP_MACOSX chrome/icons/ chrome/icons/default/ #endif #endif - -# Remove previously-bundled ruby/s4e extensions -@DIR_MACOS@distribution/bundles/statusbar@palemoon.org/* -@DIR_MACOS@distribution/bundles/{3ff46564-d77c-491c-bfc5-fc555c87dbc4}/* - @DIR_MACOS@chrome/overlayinfo/ @DIR_MACOS@components/ @DIR_MACOS@defaults/autoconfig/ @@ -114,4 +112,5 @@ @DIR_MACOS@plugins/MRJPlugin.plugin/* Contents/Plug-Ins/PrintPDE.plugin/* #endif +@DIR_MACOS@searchplugins/* @DIR_MACOS@webapprt/components/ diff --git a/application/palemoon/installer/windows/Makefile.in b/application/palemoon/installer/windows/Makefile.in index 600bdfeb6..8c434c54f 100644 --- a/application/palemoon/installer/windows/Makefile.in +++ b/application/palemoon/installer/windows/Makefile.in @@ -5,7 +5,7 @@ include $(topsrcdir)/toolkit/mozapps/installer/package-name.mk CONFIG_DIR = instgen -SFX_MODULE = $(topsrcdir)/other-licenses/7zstub/uxp/7zSD.sfx +SFX_MODULE = $(topsrcdir)/other-licenses/7zstub/palemoon/7zSD.sfx INSTALLER_FILES = \ app.tag \ diff --git a/application/palemoon/locales/en-US/chrome/browser/browser.properties b/application/palemoon/locales/en-US/chrome/browser/browser.properties index 8b3fea4d5..bf363d103 100644 --- a/application/palemoon/locales/en-US/chrome/browser/browser.properties +++ b/application/palemoon/locales/en-US/chrome/browser/browser.properties @@ -363,26 +363,6 @@ restartButton=Restart # menu, set this to "true". Otherwise, you can leave it as "false". browser.menu.showCharacterEncoding=false -# LOCALIZATION NOTE (syncPromoNotification.bookmarks.label): This appears in -# the add bookmark star panel. %S will be replaced by syncBrandShortName. -# The final space separates this text from the Learn More link. -syncPromoNotification.bookmarks.description=You can access your bookmarks on all your devices with %S.\u0020 -# LOCALIZATION NOTE (syncPromoNotification.passwords.label): This appears in -# the remember password panel. %S will be replaced by syncBrandShortName. -# The final space separates this text from the Learn More link. -syncPromoNotification.passwords.description=You can access your passwords on all your devices with %S.\u0020 -syncPromoNotification.learnMoreLinkText=Learn More -# LOCALIZATION NOTE (syncPromoNotification.addons.label): This appears in -# the add-on install complete panel when Sync isn't set. -# %S will be replaced by syncBrandShortName. -# The final space separates this text from the Learn More link. -syncPromoNotification.addons.description=You can access your add-ons on all your devices with %S.\u0020 -# LOCALIZATION NOTE (syncPromoNotification.addons-sync-disabled.label): -# This appears in the add-on install complete panel when Sync is set -# but addons sync is not. %S will be replaced by syncBrandShortName. -# The final space separates this text from the Learn More link. -syncPromoNotification.addons-sync-disabled.description=You can use your %S account to synchronize add-ons across multiple devices.\u0020 - # Mozilla data reporting notification (Telemetry, Firefox Health Report, etc) dataReportingNotification.message = %1$S automatically sends some data to %2$S so that we can improve your experience. dataReportingNotification.button.label = Choose What I Share diff --git a/application/palemoon/locales/en-US/chrome/overrides/netError.dtd b/application/palemoon/locales/en-US/chrome/overrides/netError.dtd index 51aedb275..c97bd1b59 100644 --- a/application/palemoon/locales/en-US/chrome/overrides/netError.dtd +++ b/application/palemoon/locales/en-US/chrome/overrides/netError.dtd @@ -51,7 +51,6 @@ <p>&brandShortName; can’t load this page for some reason.</p> "> -<!ENTITY malformedURI.title "The address isn't valid"> <!ENTITY captivePortal.title "Login to network"> <!ENTITY captivePortal.longDesc " <p>This network may require you to login to access the internet.</p> @@ -59,7 +58,7 @@ <!ENTITY openPortalLoginPage.label "Open Login Page"> -<!ENTITY malformedURI.title "The address isn’t valid"> +<!ENTITY malformedURI.title "The address isn't valid"> <!ENTITY malformedURI.longDesc " <ul> <li>Web addresses are usually written like diff --git a/application/palemoon/locales/generic/install.rdf b/application/palemoon/locales/generic/install.rdf deleted file mode 100644 index d04c67a61..000000000 --- a/application/palemoon/locales/generic/install.rdf +++ /dev/null @@ -1,30 +0,0 @@ -<?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/. - -#filter substitution ---> - -<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:em="http://www.mozilla.org/2004/em-rdf#"> - <Description about="urn:mozilla:install-manifest" - em:id="@MOZ_LANGPACK_EID@" - em:name="@MOZ_LANG_TITLE@ Language Pack" - em:version="@MOZ_APP_VERSION@" - em:type="8" - em:creator="@MOZ_LANGPACK_CREATOR@"> -#ifdef MOZ_LANGPACK_CONTRIBUTORS - @MOZ_LANGPACK_CONTRIBUTORS@ -#endif - - <em:targetApplication> - <Description> - <em:id>{8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4}</em:id> - <em:minVersion>@MOZ_APP_VERSION@</em:minVersion> - <em:maxVersion>@MOZ_APP_MAXVERSION@</em:maxVersion> - </Description> - </em:targetApplication> - </Description> -</RDF> diff --git a/application/palemoon/locales/l10n.ini b/application/palemoon/locales/l10n.ini index df5eb7ac0..9a466b65e 100644 --- a/application/palemoon/locales/l10n.ini +++ b/application/palemoon/locales/l10n.ini @@ -3,14 +3,14 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. [general] -depth = ../.. -all = browser/locales/all-locales +depth = ../../.. +all = application/palemoon/locales/all-locales [compare] -dirs = browser +dirs = application/palemoon extensions/reporter other-licenses/branding/firefox - browser/branding/official + application/palemoon/branding/official [includes] # non-central apps might want to use %(topsrcdir)s here, or other vars diff --git a/application/palemoon/modules/AutoCompletePopup.jsm b/application/palemoon/modules/AutoCompletePopup.jsm new file mode 100644 index 000000000..c3698f905 --- /dev/null +++ b/application/palemoon/modules/AutoCompletePopup.jsm @@ -0,0 +1,293 @@ +/* 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"; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +this.EXPORTED_SYMBOLS = [ "AutoCompletePopup" ]; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +// nsITreeView implementation that feeds the autocomplete popup +// with the search data. +var AutoCompleteTreeView = { + // nsISupports + QueryInterface: XPCOMUtils.generateQI([Ci.nsITreeView, + Ci.nsIAutoCompleteController]), + + // Private variables + treeBox: null, + results: [], + + // nsITreeView + selection: null, + + get rowCount() { return this.results.length; }, + setTree: function(treeBox) { this.treeBox = treeBox; }, + getCellText: function(idx, column) { return this.results[idx].value }, + isContainer: function(idx) { return false; }, + getCellValue: function(idx, column) { return false }, + isContainerOpen: function(idx) { return false; }, + isContainerEmpty: function(idx) { return false; }, + isSeparator: function(idx) { return false; }, + isSorted: function() { return false; }, + isEditable: function(idx, column) { return false; }, + canDrop: function(idx, orientation, dt) { return false; }, + getLevel: function(idx) { return 0; }, + getParentIndex: function(idx) { return -1; }, + hasNextSibling: function(idx, after) { return idx < this.results.length - 1 }, + toggleOpenState: function(idx) { }, + getCellProperties: function(idx, column) { return this.results[idx].style || ""; }, + getRowProperties: function(idx) { return ""; }, + getImageSrc: function(idx, column) { return null; }, + getProgressMode : function(idx, column) { }, + cycleHeader: function(column) { }, + cycleCell: function(idx, column) { }, + selectionChanged: function() { }, + performAction: function(action) { }, + performActionOnCell: function(action, index, column) { }, + getColumnProperties: function(column) { return ""; }, + + // nsIAutoCompleteController + get matchCount() { + return this.rowCount; + }, + + handleEnter: function(aIsPopupSelection) { + AutoCompletePopup.handleEnter(aIsPopupSelection); + }, + + stopSearch: function() {}, + + // Internal JS-only API + clearResults: function() { + this.results = []; + }, + + setResults: function(results) { + this.results = results; + }, +}; + +this.AutoCompletePopup = { + MESSAGES: [ + "FormAutoComplete:SelectBy", + "FormAutoComplete:GetSelectedIndex", + "FormAutoComplete:SetSelectedIndex", + "FormAutoComplete:MaybeOpenPopup", + "FormAutoComplete:ClosePopup", + "FormAutoComplete:Disconnect", + "FormAutoComplete:RemoveEntry", + "FormAutoComplete:Invalidate", + ], + + init: function() { + for (let msg of this.MESSAGES) { + Services.mm.addMessageListener(msg, this); + } + }, + + uninit: function() { + for (let msg of this.MESSAGES) { + Services.mm.removeMessageListener(msg, this); + } + }, + + handleEvent: function(evt) { + switch (evt.type) { + case "popupshowing": { + this.sendMessageToBrowser("FormAutoComplete:PopupOpened"); + break; + } + + case "popuphidden": { + this.sendMessageToBrowser("FormAutoComplete:PopupClosed"); + this.openedPopup = null; + this.weakBrowser = null; + evt.target.removeEventListener("popuphidden", this); + evt.target.removeEventListener("popupshowing", this); + break; + } + } + }, + + // Along with being called internally by the receiveMessage handler, + // this function is also called directly by the login manager, which + // uses a single message to fill in the autocomplete results. See + // "RemoteLogins:autoCompleteLogins". + showPopupWithResults: function({ browser, rect, dir, results }) { + if (!results.length || this.openedPopup) { + // We shouldn't ever be showing an empty popup, and if we + // already have a popup open, the old one needs to close before + // we consider opening a new one. + return; + } + + let window = browser.ownerDocument.defaultView; + let tabbrowser = window.gBrowser; + if (Services.focus.activeWindow != window || + tabbrowser.selectedBrowser != browser) { + // We were sent a message from a window or tab that went into the + // background, so we'll ignore it for now. + return; + } + + this.weakBrowser = Cu.getWeakReference(browser); + this.openedPopup = browser.autoCompletePopup; + this.openedPopup.hidden = false; + // don't allow the popup to become overly narrow + this.openedPopup.setAttribute("width", Math.max(100, rect.width)); + this.openedPopup.style.direction = dir; + + AutoCompleteTreeView.setResults(results); + this.openedPopup.view = AutoCompleteTreeView; + this.openedPopup.selectedIndex = -1; + this.openedPopup.invalidate(); + + if (results.length) { + // Reset fields that were set from the last time the search popup was open + this.openedPopup.mInput = null; + this.openedPopup.showCommentColumn = false; + this.openedPopup.showImageColumn = false; + this.openedPopup.addEventListener("popuphidden", this); + this.openedPopup.addEventListener("popupshowing", this); + this.openedPopup.openPopupAtScreenRect("after_start", rect.left, rect.top, + rect.width, rect.height, false, + false); + } else { + this.closePopup(); + } + }, + + invalidate(results) { + if (!this.openedPopup) { + return; + } + + if (!results.length) { + this.closePopup(); + } else { + AutoCompleteTreeView.setResults(results); + // We need to re-set the view in order for the + // tree to know the view has changed. + this.openedPopup.view = AutoCompleteTreeView; + this.openedPopup.invalidate(); + } + }, + + closePopup() { + if (this.openedPopup) { + // Note that hidePopup() closes the popup immediately, + // so popuphiding or popuphidden events will be fired + // and handled during this call. + this.openedPopup.hidePopup(); + } + AutoCompleteTreeView.clearResults(); + }, + + removeLogin(login) { + Services.logins.removeLogin(login); + }, + + receiveMessage: function(message) { + if (!message.target.autoCompletePopup) { + // Returning false to pacify ESLint, but this return value is + // ignored by the messaging infrastructure. + return false; + } + + switch (message.name) { + case "FormAutoComplete:SelectBy": { + this.openedPopup.selectBy(message.data.reverse, message.data.page); + break; + } + + case "FormAutoComplete:GetSelectedIndex": { + if (this.openedPopup) { + return this.openedPopup.selectedIndex; + } + // If the popup was closed, then the selection + // has not changed. + return -1; + } + + case "FormAutoComplete:SetSelectedIndex": { + let { index } = message.data; + if (this.openedPopup) { + this.openedPopup.selectedIndex = index; + } + break; + } + + case "FormAutoComplete:MaybeOpenPopup": { + let { results, rect, dir } = message.data; + this.showPopupWithResults({ browser: message.target, rect, dir, + results }); + break; + } + + case "FormAutoComplete:Invalidate": { + let { results } = message.data; + this.invalidate(results); + break; + } + + case "FormAutoComplete:ClosePopup": { + this.closePopup(); + break; + } + + case "FormAutoComplete:Disconnect": { + // The controller stopped controlling the current input, so clear + // any cached data. This is necessary cause otherwise we'd clear data + // only when starting a new search, but the next input could not support + // autocomplete and it would end up inheriting the existing data. + AutoCompleteTreeView.clearResults(); + break; + } + } + // Returning false to pacify ESLint, but this return value is + // ignored by the messaging infrastructure. + return false; + }, + + /** + * Despite its name, this handleEnter is only called when the user clicks on + * one of the items in the popup since the popup is rendered in the parent process. + * The real controller's handleEnter is called directly in the content process + * for other methods of completing a selection (e.g. using the tab or enter + * keys) since the field with focus is in that process. + */ + handleEnter(aIsPopupSelection) { + if (this.openedPopup) { + this.sendMessageToBrowser("FormAutoComplete:HandleEnter", { + selectedIndex: this.openedPopup.selectedIndex, + isPopupSelection: aIsPopupSelection, + }); + } + }, + + /** + * If a browser exists that AutoCompletePopup knows about, + * sends it a message. Otherwise, this is a no-op. + * + * @param {string} msgName + * The name of the message to send. + * @param {object} data + * The optional data to send with the message. + */ + sendMessageToBrowser(msgName, data) { + let browser = this.weakBrowser ? this.weakBrowser.get() + : null; + if (browser) { + browser.messageManager.sendAsyncMessage(msgName, data); + } + }, + + stopSearch: function() {} +} diff --git a/application/palemoon/modules/BrowserNewTabPreloader.jsm b/application/palemoon/modules/BrowserNewTabPreloader.jsm index 719a9e05e..778698fba 100644 --- a/application/palemoon/modules/BrowserNewTabPreloader.jsm +++ b/application/palemoon/modules/BrowserNewTabPreloader.jsm @@ -79,7 +79,7 @@ this.BrowserNewTabPreloader = { Object.freeze(BrowserNewTabPreloader); -let Initializer = { +var Initializer = { _timer: null, _observing: false, @@ -120,7 +120,7 @@ let Initializer = { } }; -let Preferences = { +var Preferences = { _enabled: null, _branch: null, @@ -157,7 +157,7 @@ let Preferences = { }, }; -let HiddenBrowsers = { +var HiddenBrowsers = { _browsers: null, _updateTimer: null, @@ -379,7 +379,7 @@ HiddenBrowser.prototype = { } }; -let HostFrame = { +var HostFrame = { _frame: null, _deferred: null, diff --git a/application/palemoon/modules/FormSubmitObserver.jsm b/application/palemoon/modules/FormSubmitObserver.jsm index 72c5ca601..e4d3e765e 100644 --- a/application/palemoon/modules/FormSubmitObserver.jsm +++ b/application/palemoon/modules/FormSubmitObserver.jsm @@ -9,14 +9,14 @@ "use strict"; -let Cc = Components.classes; -let Ci = Components.interfaces; -let Cu = Components.utils; - -let HTMLInputElement = Ci.nsIDOMHTMLInputElement; -let HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement; -let HTMLSelectElement = Ci.nsIDOMHTMLSelectElement; -let HTMLButtonElement = Ci.nsIDOMHTMLButtonElement; +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; + +var HTMLInputElement = Ci.nsIDOMHTMLInputElement; +var HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement; +var HTMLSelectElement = Ci.nsIDOMHTMLSelectElement; +var HTMLButtonElement = Ci.nsIDOMHTMLButtonElement; this.EXPORTED_SYMBOLS = [ "FormSubmitObserver" ]; diff --git a/application/palemoon/modules/FormValidationHandler.jsm b/application/palemoon/modules/FormValidationHandler.jsm index 05be510e1..387c221d7 100644 --- a/application/palemoon/modules/FormValidationHandler.jsm +++ b/application/palemoon/modules/FormValidationHandler.jsm @@ -8,15 +8,15 @@ "use strict"; -let Cc = Components.classes; -let Ci = Components.interfaces; -let Cu = Components.utils; +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; this.EXPORTED_SYMBOLS = [ "FormValidationHandler" ]; Cu.import("resource://gre/modules/Services.jsm"); -let FormValidationHandler = +var FormValidationHandler = { _panel: null, _anchor: null, @@ -122,7 +122,7 @@ let FormValidationHandler = this._panel.hidden = false; let tabBrowser = aWindow.gBrowser; - this._anchor = tabBrowser.formValidationAnchor; + this._anchor = tabBrowser.popupAnchor; this._anchor.left = aPanelData.contentRect.left; this._anchor.top = aPanelData.contentRect.top; this._anchor.width = aPanelData.contentRect.width; diff --git a/application/palemoon/modules/NetworkPrioritizer.jsm b/application/palemoon/modules/NetworkPrioritizer.jsm index ea4a87790..23d688a30 100644 --- a/application/palemoon/modules/NetworkPrioritizer.jsm +++ b/application/palemoon/modules/NetworkPrioritizer.jsm @@ -34,8 +34,8 @@ const PRIORITY_DELTA = -10; // Variables -let _lastFocusedWindow = null; -let _windows = []; +var _lastFocusedWindow = null; +var _windows = []; // Exported symbol @@ -64,7 +64,7 @@ function _handleEvent(aEvent) { // Methods that impact a browser. Put into single object for organization. -let BrowserHelper = { +var BrowserHelper = { onOpen: function NP_BH_onOpen(aBrowser) { // If the tab is in the focused window, leave priority as it is if (aBrowser.ownerDocument.defaultView != _lastFocusedWindow) @@ -91,7 +91,7 @@ let BrowserHelper = { // Methods that impact a window. Put into single object for organization. -let WindowHelper = { +var WindowHelper = { addWindow: function NP_WH_addWindow(aWindow) { // Build internal data object _windows.push({ window: aWindow, lastSelectedBrowser: null }); diff --git a/application/palemoon/modules/PopupNotifications.jsm b/application/palemoon/modules/PopupNotifications.jsm index 9b2e8e5d1..d2faf52c3 100644 --- a/application/palemoon/modules/PopupNotifications.jsm +++ b/application/palemoon/modules/PopupNotifications.jsm @@ -18,8 +18,8 @@ const ICON_ATTRIBUTE_SHOWING = "showing"; const PREF_SECURITY_DELAY = "security.notification_enable_delay"; -let popupNotificationsMap = new WeakMap(); -let gNotificationParents = new WeakMap; +var popupNotificationsMap = new WeakMap(); +var gNotificationParents = new WeakMap; function getAnchorFromBrowser(aBrowser) { let anchor = aBrowser.getAttribute("popupnotificationanchor") || diff --git a/application/palemoon/modules/SharedFrame.jsm b/application/palemoon/modules/SharedFrame.jsm index 4d248ae5b..b9d59bfa9 100644 --- a/application/palemoon/modules/SharedFrame.jsm +++ b/application/palemoon/modules/SharedFrame.jsm @@ -18,7 +18,7 @@ const Cu = Components.utils; * when another one of the placeholder is meant to be displayed. * */ -let Frames = new Map(); +var Frames = new Map(); /** * The Frames map is the main data structure that holds information diff --git a/application/palemoon/modules/moz.build b/application/palemoon/modules/moz.build index f7717ef89..67fd22338 100644 --- a/application/palemoon/modules/moz.build +++ b/application/palemoon/modules/moz.build @@ -9,6 +9,7 @@ EXTRA_JS_MODULES += [ 'promise.js' ] EXTRA_JS_MODULES += [ + 'AutoCompletePopup.jsm', 'BrowserNewTabPreloader.jsm', 'CharsetMenu.jsm', 'FormSubmitObserver.jsm', diff --git a/application/palemoon/modules/openLocationLastURL.jsm b/application/palemoon/modules/openLocationLastURL.jsm index 0d653df28..3f58db8ce 100644 --- a/application/palemoon/modules/openLocationLastURL.jsm +++ b/application/palemoon/modules/openLocationLastURL.jsm @@ -10,11 +10,11 @@ Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); this.EXPORTED_SYMBOLS = [ "OpenLocationLastURL" ]; -let prefSvc = Components.classes["@mozilla.org/preferences-service;1"] +var prefSvc = Components.classes["@mozilla.org/preferences-service;1"] .getService(Components.interfaces.nsIPrefBranch); -let gOpenLocationLastURLData = ""; +var gOpenLocationLastURLData = ""; -let observer = { +var observer = { QueryInterface: function (aIID) { if (aIID.equals(Components.interfaces.nsIObserver) || aIID.equals(Components.interfaces.nsISupports) || @@ -35,7 +35,7 @@ let observer = { } }; -let os = Components.classes["@mozilla.org/observer-service;1"] +var os = Components.classes["@mozilla.org/observer-service;1"] .getService(Components.interfaces.nsIObserverService); os.addObserver(observer, "last-pb-context-exited", true); os.addObserver(observer, "browser:purge-session-history", true); diff --git a/application/palemoon/modules/promise.js b/application/palemoon/modules/promise.js index 7c96f02cf..74065c8db 100644 --- a/application/palemoon/modules/promise.js +++ b/application/palemoon/modules/promise.js @@ -24,7 +24,7 @@ module.metadata = { 'stability': 'unstable' }; -let promised = (function() { +var promised = (function() { // Note: Define shortcuts and utility functions here in order to avoid // slower property accesses and unnecessary closure creations on each // call of this popular function. diff --git a/application/palemoon/themes/linux/autocomplete.css b/application/palemoon/themes/linux/autocomplete.css new file mode 100644 index 000000000..fffa2b1f0 --- /dev/null +++ b/application/palemoon/themes/linux/autocomplete.css @@ -0,0 +1,210 @@ +/* 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/. */ + +/* ===== autocomplete.css ================================================= + == Styles used by the autocomplete widget. + ======================================================================= */ + +@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); +@namespace html url("http://www.w3.org/1999/xhtml"); + +/* ::::: autocomplete ::::: */ + +/* .padded is used by autocomplete widgets that don't have an icon. Gross. -dwh */ +textbox:not(.padded) { + cursor: default; + padding: 0; +} + +textbox[enablehistory="true"] { + -moz-appearance: none; + border: 0; + background-color: transparent; +} + +textbox[nomatch="true"][highlightnonmatches="true"] { + color: red; +} + +.autocomplete-textbox-container { + -moz-box-align: center; +} + +textbox[enablehistory="true"] > .autocomplete-textbox-container { + -moz-appearance: menulist-textfield; +} + +textbox:not(.padded) .textbox-input-box { + margin: 0 3px; +} + +.textbox-input-box { + -moz-box-align: center; +} + +/* ::::: autocomplete popups ::::: */ + +panel[type="autocomplete"], +panel[type="autocomplete-richlistbox"], +.autocomplete-history-popup { + border-width: 1px; + -moz-border-top-colors: ThreeDDarkShadow; + -moz-border-right-colors: ThreeDDarkShadow; + -moz-border-bottom-colors: ThreeDDarkShadow; + -moz-border-left-colors: ThreeDDarkShadow; + padding: 0; + background-color: -moz-Field; +} + +.autocomplete-history-popup { + max-height: 180px; +} + +/* ::::: tree ::::: */ + +.autocomplete-tree { + -moz-appearance: none !important; + border: none !important; + background-color: transparent !important; + color: MenuText; +} + +.autocomplete-treecol { + -moz-appearance: none !important; + margin: 0 !important; + border: none !important; + padding: 0 !important; +} + +/* GTK calculates space for a sort arrow */ +.autocomplete-treecol > .treecol-sortdirection { + -moz-appearance: none !important; +} + +.autocomplete-treebody::-moz-tree-cell-text { + -moz-padding-start: 8px; +} + +treechildren.autocomplete-treebody::-moz-tree-row(selected) { + background-color: Highlight; +} + +treechildren.autocomplete-treebody::-moz-tree-cell-text(selected) { + color: HighlightText !important; +} + +.autocomplete-treebody::-moz-tree-image(treecolAutoCompleteValue) { + max-width: 16px; + height: 16px; +} + +/* ::::: richlistbox autocomplete ::::: */ + +.autocomplete-richlistbox { + -moz-appearance: none; + margin: 1px; + background-color: transparent; +} + +.autocomplete-richlistitem[selected="true"] { + background-color: Highlight; + color: HighlightText; +} + +.autocomplete-richlistitem { + padding: 6px 2px; + color: MenuText; +} + +.ac-url-box { + /* When setting a vertical margin here, half of that needs to be added + .ac-title-box's translateY for when .ac-url-box is hidden (see below). */ + margin-top: 1px; +} + +.autocomplete-richlistitem[actiontype="keyword"] .ac-url-box, +.autocomplete-richlistitem[actiontype="searchengine"] .ac-url-box, +.autocomplete-richlistitem[actiontype="visiturl"] .ac-url-box, +.autocomplete-richlistitem[type~="autofill"] .ac-url-box { + visibility: hidden; +} + +.autocomplete-richlistitem[actiontype="keyword"] .ac-title-box, +.autocomplete-richlistitem[actiontype="searchengine"] .ac-title-box, +.autocomplete-richlistitem[actiontype="visiturl"] .ac-title-box, +.autocomplete-richlistitem[type~="autofill"] .ac-title-box { + /* Center the title by moving it down by half of .ac-url-box's height, + including vertical margins (if any). */ + transform: translateY(.5em); +} + +.ac-site-icon { + width: 16px; + height: 16px; + margin-bottom: -2px; + -moz-margin-start: 3px; + -moz-margin-end: 6px; +} + +.ac-type-icon { + width: 16px; + height: 16px; + -moz-margin-start: 6px; + -moz-margin-end: 4px; +} + +.ac-extra > .ac-result-type-tag { + margin: 0 4px; +} + +.ac-extra > .ac-comment { + padding-right: 4px; +} + +.ac-ellipsis-after { + margin: 0 !important; + padding: 0; + min-width: 1em; +} + +.ac-normal-text { + margin: 0 !important; + padding: 0; +} + +.ac-normal-text > html|span { + margin: 0 !important; + padding: 0; +} + +html|span.ac-emphasize-text { + box-shadow: inset 0 0 1px 1px rgba(0,0,0,0.1); + background-color: rgba(0,0,0,0.05); + border-radius: 2px; + text-shadow: 0 0 currentColor; /*faux bold effect*/ +} + +.ac-url-text > html|span.ac-emphasize-text, +.ac-action-text > html|span.ac-emphasize-text { + box-shadow: none; +} + +.ac-normal-text[selected="true"] > html|span.ac-emphasize-text { + box-shadow: inset 0 0 1px 1px rgba(255,255,255,0.3); + background-color: rgba(255,255,255,0.2); +} + +.ac-title, .ac-url { + overflow: hidden; +} + +/* ::::: textboxes inside toolbarpaletteitems ::::: */ + +toolbarpaletteitem > toolbaritem > textbox > hbox > hbox > html|*.textbox-input { + visibility: hidden; +} + +toolbarpaletteitem > toolbaritem > * > textbox > hbox > hbox > html|*.textbox-input { + visibility: hidden; +} diff --git a/application/palemoon/themes/linux/browser.css b/application/palemoon/themes/linux/browser.css index a396ea5fd..334062613 100644 --- a/application/palemoon/themes/linux/browser.css +++ b/application/palemoon/themes/linux/browser.css @@ -1559,29 +1559,6 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action- min-width: 27em; } -.panel-promo-box { - margin: 8px -10px -10px -10px; - padding: 8px 10px; - border-top: 1px solid ThreeDShadow; - background-image: linear-gradient(hsla(0,0%,0%,.15), hsla(0,0%,0%,.08) 6px); -} - -.panel-promo-icon { - list-style-image: url("chrome://browser/skin/sync-notification-24.png"); - -moz-margin-end: 10px; - vertical-align: middle; -} - -.panel-promo-closebutton { - margin-top: 0; - margin-bottom: 0; -} - -.panel-promo-closebutton > .toolbarbutton-text { - padding: 0; - margin: 0; -} - /* Content area */ #sidebar { background-color: Window; diff --git a/application/palemoon/themes/linux/jar.mn b/application/palemoon/themes/linux/jar.mn index 44d837778..7e67d0129 100644 --- a/application/palemoon/themes/linux/jar.mn +++ b/application/palemoon/themes/linux/jar.mn @@ -16,6 +16,7 @@ browser.jar: #ifdef MOZ_SERVICES_SYNC skin/classic/browser/aboutSyncTabs.css #endif +* skin/classic/browser/autocomplete.css skin/classic/browser/actionicon-tab.png * skin/classic/browser/browser.css skin/classic/browser/click-to-play-warning-stripes.png @@ -130,7 +131,6 @@ browser.jar: skin/classic/browser/sync-128.png skin/classic/browser/sync-desktopIcon.png skin/classic/browser/sync-mobileIcon.png - skin/classic/browser/sync-notification-24.png skin/classic/browser/syncSetup.css skin/classic/browser/syncCommon.css skin/classic/browser/syncQuota.css diff --git a/application/palemoon/themes/linux/sync-notification-24.png b/application/palemoon/themes/linux/sync-notification-24.png Binary files differdeleted file mode 100644 index d67eb47ac..000000000 --- a/application/palemoon/themes/linux/sync-notification-24.png +++ /dev/null diff --git a/application/palemoon/themes/osx/autocomplete.css b/application/palemoon/themes/osx/autocomplete.css new file mode 100644 index 000000000..42b4ee430 --- /dev/null +++ b/application/palemoon/themes/osx/autocomplete.css @@ -0,0 +1,198 @@ +/* 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"); + +/* .padded is used by autocomplete widgets that don't have an icon. Gross. -dwh */ +textbox:not(.padded) { + cursor: default; + padding: 0; +} + +textbox[nomatch="true"][highlightnonmatches="true"] { + color: red; +} + +textbox:not(.padded) .textbox-input-box { + margin: 0 3px; +} + +.textbox-input-box { + -moz-box-align: center; +} + +/* ::::: history button ::::: */ + +.autocomplete-history-dropmarker { + -moz-appearance: none !important; + border: none !important; + background-color: transparent !important; + padding: 0px; + list-style-image: url("chrome://global/skin/icons/autocomplete-dropmarker.png"); + margin: 0px; +} + +/* ::::: autocomplete popups ::::: */ + +panel[type="autocomplete"], +panel[type="autocomplete-richlistbox"], +.autocomplete-history-popup { + padding: 0px !important; + color: -moz-FieldText; + background-color: -moz-Field; + font: icon; + -moz-appearance: none; +} + +.autocomplete-history-popup { + max-height: 180px; +} + +/* ::::: tree ::::: */ + +.autocomplete-tree { + -moz-appearance: none !important; + border: none !important; + background-color: transparent !important; +} + +.autocomplete-treecol { + -moz-appearance: none !important; + margin: 0 !important; + border: none !important; + padding: 0 !important; +} + +.autocomplete-treebody::-moz-tree-cell-text { + padding-left: 2px; +} + +.autocomplete-treebody::-moz-tree-row { + border-top: none; +} + +treechildren.autocomplete-treebody::-moz-tree-row(selected) { + background-color: Highlight; +} + +treechildren.autocomplete-treebody::-moz-tree-cell-text(selected) { + color: HighlightText !important; +} + +.autocomplete-treebody::-moz-tree-image(treecolAutoCompleteValue) { + max-width: 16px; + height: 16px; +} + +/* ::::: richlistbox autocomplete ::::: */ + +.autocomplete-richlistbox { + -moz-appearance: none; + margin: 0; +} + +.autocomplete-richlistitem[selected="true"] { + background-color: Highlight; + color: HighlightText; + background-image: linear-gradient(rgba(255,255,255,0.3), transparent); +} + +.autocomplete-richlistitem { + padding: 5px 2px; +} + +.ac-url-box { + /* When setting a vertical margin here, half of that needs to be added + .ac-title-box's translateY for when .ac-url-box is hidden (see below). */ + margin-top: 1px; +} + +.autocomplete-richlistitem[actiontype="keyword"] .ac-url-box, +.autocomplete-richlistitem[actiontype="searchengine"] .ac-url-box, +.autocomplete-richlistitem[actiontype="visiturl"] .ac-url-box, +.autocomplete-richlistitem[type~="autofill"] .ac-url-box { + visibility: hidden; +} + +.autocomplete-richlistitem[actiontype="keyword"] .ac-title-box, +.autocomplete-richlistitem[actiontype="searchengine"] .ac-title-box, +.autocomplete-richlistitem[actiontype="visiturl"] .ac-title-box, +.autocomplete-richlistitem[type~="autofill"] .ac-title-box { + /* Center the title by moving it down by half of .ac-url-box's height, + including vertical margins (if any). */ + transform: translateY(.5em); +} + +.ac-site-icon { + width: 16px; + height: 16px; + margin-bottom: -1px; + -moz-margin-start: 7px; + -moz-margin-end: 5px; +} + +.ac-type-icon { + width: 16px; + height: 16px; + -moz-margin-start: 6px; + -moz-margin-end: 4px; +} + +.ac-url-box > .ac-site-icon, +.ac-url-box > .ac-type-icon { + /* Otherwise the spacer is big enough to stretch its container */ + height: auto; +} + +.ac-extra > .ac-result-type-tag { + margin: 0 4px; +} + +.ac-extra > .ac-comment { + padding-right: 4px; +} + +.ac-ellipsis-after { + margin: 0 !important; + padding: 0; + min-width: 1.1em; +} + +.ac-normal-text { + margin: 0 !important; + padding: 0; +} + +.ac-normal-text > html|span { + margin: 0 !important; + padding: 0; +} + +html|span.ac-emphasize-text { + box-shadow: inset 0 0 1px 1px rgba(208,208,208,0.4); + background-color: rgba(208,208,208,0.2); + border-radius: 2px; + text-shadow: 0 0 currentColor; +} + +.ac-url-text > html|span.ac-emphasize-text, +.ac-action-text > html|span.ac-emphasize-text { + box-shadow: inset 0 0 1px 1px rgba(183,210,226,0.4); + background-color: rgba(183,210,226,0.3); +} + +.ac-title, .ac-url { + overflow: hidden; +} + +/* ::::: textboxes inside toolbarpaletteitems ::::: */ + +toolbarpaletteitem > toolbaritem > textbox > hbox > hbox > html|*.textbox-input { + visibility: hidden; +} + +toolbarpaletteitem > toolbaritem > * > textbox > hbox > hbox > html|*.textbox-input { + visibility: hidden; +} diff --git a/application/palemoon/themes/osx/browser.css b/application/palemoon/themes/osx/browser.css index aa5918bab..a66ddacfc 100644 --- a/application/palemoon/themes/osx/browser.css +++ b/application/palemoon/themes/osx/browser.css @@ -1338,41 +1338,6 @@ richlistitem[type~="action"][actiontype="switchtab"][selected="true"] > .ac-url- min-width: 27em; } -.panel-promo-box { - margin: 10px -10px -10px; - padding: 8px 10px; - border-top: 1px solid ThreeDShadow; - background-image: linear-gradient(hsla(0,0%,0%,.15), hsla(0,0%,0%,.08) 6px); - border-bottom-left-radius: 3px; - border-bottom-right-radius: 3px; -} - -@media (-moz-mac-lion-theme) { - .panel-promo-box { - border-top-style: none; - background: #f1f5fb; - color: GrayText; - box-shadow: 0px 1px 2px rgb(204,214,234) inset; - } -} - -.panel-promo-icon { - list-style-image: url("chrome://browser/skin/sync-notification-24.png"); - -moz-margin-end: 10px; - vertical-align: middle; -} - -.panel-promo-closebutton { - border: none; - -moz-margin-end: -14px; - margin-top: -8px; -} - -.panel-promo-closebutton > .toolbarbutton-text { - padding: 0; - margin: 0; -} - /* ::::: content area ::::: */ #sidebar { diff --git a/application/palemoon/themes/osx/jar.mn b/application/palemoon/themes/osx/jar.mn index 8742f1b87..186cd8a75 100644 --- a/application/palemoon/themes/osx/jar.mn +++ b/application/palemoon/themes/osx/jar.mn @@ -15,6 +15,7 @@ browser.jar: #ifdef MOZ_SERVICES_SYNC skin/classic/browser/aboutSyncTabs.css #endif +* skin/classic/browser/autocomplete.css skin/classic/browser/actionicon-tab.png skin/classic/browser/appmenu-icons.png skin/classic/browser/appmenu-dropmarker.png @@ -163,7 +164,6 @@ browser.jar: skin/classic/browser/sync-bg.png skin/classic/browser/sync-desktopIcon.png skin/classic/browser/sync-mobileIcon.png - skin/classic/browser/sync-notification-24.png skin/classic/browser/syncSetup.css skin/classic/browser/syncCommon.css skin/classic/browser/syncQuota.css diff --git a/application/palemoon/themes/osx/sync-notification-24.png b/application/palemoon/themes/osx/sync-notification-24.png Binary files differdeleted file mode 100644 index fc9a4e63d..000000000 --- a/application/palemoon/themes/osx/sync-notification-24.png +++ /dev/null diff --git a/application/palemoon/themes/windows/autocomplete.css b/application/palemoon/themes/windows/autocomplete.css new file mode 100644 index 000000000..f0f16cc27 --- /dev/null +++ b/application/palemoon/themes/windows/autocomplete.css @@ -0,0 +1,238 @@ +/* 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/. */ + +/* ===== autocomplete.css ================================================= + == Styles used by the autocomplete widget. + ======================================================================= */ + +@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); +@namespace html url("http://www.w3.org/1999/xhtml"); + +/* ::::: autocomplete ::::: */ + +/* .padded is used by autocomplete widgets that don't have an icon. Gross. -dwh */ +textbox:not(.padded) { + cursor: default; + padding: 0; +} + +textbox[nomatch="true"][highlightnonmatches="true"] { + color: red; +} + +.autocomplete-textbox-container { + -moz-box-align: center; +} + +textbox:not(.padded) .textbox-input-box { + margin: 0 3px; +} + +.textbox-input-box { + -moz-box-align: center; +} + +/* ::::: autocomplete popups ::::: */ + +panel[type="autocomplete"], +panel[type="autocomplete-richlistbox"], +.autocomplete-history-popup { + -moz-appearance: none; + border-width: 1px; + -moz-border-top-colors: ThreeDShadow; + -moz-border-right-colors: ThreeDShadow; + -moz-border-bottom-colors: ThreeDShadow; + -moz-border-left-colors: ThreeDShadow; + padding: 0; + color: -moz-FieldText; + background-color: -moz-Field; +} + +.autocomplete-history-popup { + max-height: 180px; +} + +/* ::::: tree ::::: */ + +.autocomplete-tree { + -moz-appearance: none !important; + border: none !important; + background-color: transparent !important; +} + +.autocomplete-treecol { + -moz-appearance: none !important; + margin: 0 !important; + border: none !important; + padding: 0 !important; +} + +/* GTK calculates space for a sort arrow */ +.autocomplete-treecol > .treecol-sortdirection { + -moz-appearance: none !important; +} + +.autocomplete-treebody::-moz-tree-cell-text { + -moz-padding-start: 8px; +} + +treechildren.autocomplete-treebody::-moz-tree-row(selected) { + background-color: Highlight; +} + +treechildren.autocomplete-treebody::-moz-tree-cell-text(selected) { + color: HighlightText !important; +} + +.autocomplete-treebody::-moz-tree-image(treecolAutoCompleteValue) { + max-width: 16px; + height: 16px; +} + +/* ::::: richlistbox autocomplete ::::: */ + +.autocomplete-richlistbox { + -moz-appearance: none; + margin: 0; +} + +.autocomplete-richlistitem { + padding: 1px; +} + +.autocomplete-richlistitem[selected="true"] { + background-color: Highlight; + color: HighlightText; +} + +%ifdef XP_WIN +@media (-moz-os-version: windows-vista) and (-moz-windows-default-theme), + (-moz-os-version: windows-win7) and (-moz-windows-default-theme) { + .autocomplete-richlistitem[selected="true"] { + color: inherit; + background-color: transparent; + /* four gradients for the bevel highlights on each edge, one for blue background */ + background-image: + linear-gradient(to bottom, rgba(255,255,255,0.9) 3px, transparent 3px), + linear-gradient(to right, rgba(255,255,255,0.5) 3px, transparent 3px), + linear-gradient(to left, rgba(255,255,255,0.5) 3px, transparent 3px), + linear-gradient(to top, rgba(255,255,255,0.4) 3px, transparent 3px), + linear-gradient(to bottom, rgba(163,196,247,0.3), rgba(122,180,246,0.3)); + background-clip: content-box; + border-radius: 6px; + outline: 1px solid rgb(124,163,206); + -moz-outline-radius: 3px; + outline-offset: -2px; + } +} +%endif + +.ac-title-box { + margin-top: 4px; +} + +.ac-url-box { + /* When setting a vertical margin here, half of that needs to be added + .ac-title-box's translateY for when .ac-url-box is hidden (see below). */ + margin: 1px 0 4px; +} + +.autocomplete-richlistitem[actiontype="keyword"] .ac-url-box, +.autocomplete-richlistitem[actiontype="searchengine"] .ac-url-box, +.autocomplete-richlistitem[actiontype="visiturl"] .ac-url-box, +.autocomplete-richlistitem[type~="autofill"] .ac-url-box { + visibility: hidden; +} + +.autocomplete-richlistitem[actiontype="keyword"] .ac-title-box, +.autocomplete-richlistitem[actiontype="searchengine"] .ac-title-box, +.autocomplete-richlistitem[actiontype="visiturl"] .ac-title-box, +.autocomplete-richlistitem[type~="autofill"] .ac-title-box { + /* Center the title by moving it down by half of .ac-url-box's height, + including vertical margins (if any). */ + transform: translateY(calc(.5em + 2px)); +} + +.ac-site-icon { + width: 16px; + height: 16px; + margin: 0 5px -2px; +} + +.ac-type-icon { + width: 16px; + height: 16px; + -moz-margin-start: 6px; + -moz-margin-end: 4px; + margin-bottom: -1px; +} + +.ac-url-box > .ac-site-icon, +.ac-url-box > .ac-type-icon { + /* Otherwise the spacer is big enough to stretch its container */ + height: auto; +} + +.ac-extra > .ac-result-type-tag { + margin: 0 4px; +} + +.ac-extra > .ac-comment { + padding-right: 4px; +} + +.ac-ellipsis-after { + margin: 0 !important; + padding: 0; + min-width: 1em; +} + +.ac-normal-text { + margin: 0 !important; + padding: 0; +} + +.ac-normal-text > html|span { + margin: 0 !important; + padding: 0; +} + +html|span.ac-emphasize-text { + box-shadow: inset 0 0 1px 1px rgba(208,208,208,0.5); + background-color: rgba(208,208,208,0.3); + border-radius: 2px; + text-shadow: 0 0 currentColor; +} + +@media (-moz-windows-default-theme) { + @media not all and (-moz-os-version: windows-xp) { + html|span.ac-emphasize-text { + box-shadow: inset 0 0 1px 1px rgba(0,0,0,0.1); + background-color: rgba(0,0,0,0.05); + } + } + + @media (-moz-os-version: windows-xp) { + .ac-url-text > html|span.ac-emphasize-text, + .ac-action-text > html|span.ac-emphasize-text { + box-shadow: inset 0 0 1px 1px rgba(202,214,201,0.3); + background-color: rgba(202,214,201,0.2); + } + } +} + +.ac-title, .ac-url { + overflow: hidden; +} + +/* ::::: textboxes inside toolbarpaletteitems ::::: */ + +toolbarpaletteitem > toolbaritem > textbox > hbox > hbox > html|*.textbox-input { + visibility: hidden; +} + +toolbarpaletteitem > toolbaritem > * > textbox > hbox > hbox > html|*.textbox-input { + visibility: hidden; +} + diff --git a/application/palemoon/themes/windows/browser.css b/application/palemoon/themes/windows/browser.css index 3e8c63af4..a64955f0f 100644 --- a/application/palemoon/themes/windows/browser.css +++ b/application/palemoon/themes/windows/browser.css @@ -1778,42 +1778,6 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action- min-width: 27em; } -.panel-promo-box { - margin: 10px -10px -10px; - padding: 8px 10px; - border-top: 1px solid ThreeDShadow; - background-image: linear-gradient(hsla(0,0%,0%,.15), hsla(0,0%,0%,.08) 6px); - border-bottom-left-radius: 3px; - border-bottom-right-radius: 3px; -} - -@media (-moz-windows-default-theme) { - .panel-promo-box { - border-top-style: none; - background: #f1f5fb; - color: GrayText; - box-shadow: 0px 1px 2px rgb(204,214,234) inset; - } -} - -.panel-promo-icon { - list-style-image: url("chrome://browser/skin/sync-notification-24.png"); - -moz-margin-end: 10px; - vertical-align: middle; -} - -.panel-promo-closebutton { - -moz-appearance: none; - border: none; - -moz-margin-end: -10px; - margin-top: -5px; -} - -.panel-promo-closebutton > .toolbarbutton-text { - padding: 0; - margin: 0; -} - /* ::::: content area ::::: */ #sidebar { diff --git a/application/palemoon/themes/windows/jar.mn b/application/palemoon/themes/windows/jar.mn index 994e87be3..a66714b13 100644 --- a/application/palemoon/themes/windows/jar.mn +++ b/application/palemoon/themes/windows/jar.mn @@ -15,6 +15,7 @@ browser.jar: #ifdef MOZ_SERVICES_SYNC skin/classic/browser/aboutSyncTabs.css #endif +* skin/classic/browser/autocomplete.css skin/classic/browser/actionicon-tab.png skin/classic/browser/appmenu-icons.png skin/classic/browser/appmenu-dropmarker.png @@ -159,7 +160,6 @@ browser.jar: skin/classic/browser/sync-bg.png skin/classic/browser/sync-desktopIcon.png skin/classic/browser/sync-mobileIcon.png - skin/classic/browser/sync-notification-24.png skin/classic/browser/syncSetup.css skin/classic/browser/syncCommon.css skin/classic/browser/syncQuota.css diff --git a/application/palemoon/themes/windows/sync-notification-24.png b/application/palemoon/themes/windows/sync-notification-24.png Binary files differdeleted file mode 100644 index fc9a4e63d..000000000 --- a/application/palemoon/themes/windows/sync-notification-24.png +++ /dev/null diff --git a/browser/base/content/browser.css b/browser/base/content/browser.css index a05b031b2..f03f21c3f 100644 --- a/browser/base/content/browser.css +++ b/browser/base/content/browser.css @@ -557,7 +557,7 @@ toolbar:not(#TabsToolbar) > #personal-bookmarks { transition: none; } -#DateTimePickerPanel { +#DateTimePickerPanel[active="true"] { -moz-binding: url("chrome://global/content/bindings/datetimepopup.xml#datetime-popup"); } @@ -815,7 +815,6 @@ html|*#fullscreen-exit-button { .popup-anchor { /* should occupy space but not be visible */ opacity: 0; - visibility: hidden; pointer-events: none; -moz-stack-sizing: ignore; } diff --git a/browser/base/content/browser.xul b/browser/base/content/browser.xul index ae531e167..5879f2a29 100644 --- a/browser/base/content/browser.xul +++ b/browser/base/content/browser.xul @@ -155,13 +155,17 @@ level="parent" overflowpadding="30" /> + <!-- for date/time picker. consumeoutsideclicks is set to never, so that + clicks on the anchored input box are never consumed. --> <panel id="DateTimePickerPanel" type="arrow" hidden="true" orient="vertical" noautofocus="true" - consumeoutsideclicks="false" - level="parent"> + norolluponanchor="true" + consumeoutsideclicks="never" + level="parent" + tabspecific="true"> <iframe id="dateTimePopupFrame"/> </panel> diff --git a/browser/components/moz.build b/browser/components/moz.build index 0393b6e92..a49580200 100644 --- a/browser/components/moz.build +++ b/browser/components/moz.build @@ -37,11 +37,8 @@ XPIDL_SOURCES += [ XPIDL_MODULE = 'browsercompsbase' -EXTRA_PP_COMPONENTS += [ - 'BrowserComponents.manifest', -] - EXTRA_COMPONENTS += [ + 'BrowserComponents.manifest', 'nsBrowserContentHandler.js', 'nsBrowserGlue.js', ] diff --git a/config/rules.mk b/config/rules.mk index 122bd8a77..a6bd45d11 100644 --- a/config/rules.mk +++ b/config/rules.mk @@ -1129,6 +1129,12 @@ ifneq (,$(JAR_MANIFEST)) ifndef NO_DIST_INSTALL ifdef XPI_NAME +# XXX: Figure out why Pale Moon's defs.mk is not being propigated +ifndef XPI_ROOT_APPID +ifdef MC_PALEMOON +XPI_ROOT_APPID=$(MOZ_APP_ID) +endif +endif ifdef XPI_ROOT_APPID # For add-on packaging we may specify that an application # sub-dir should be added to the root chrome manifest with diff --git a/docshell/test/unit/test_nsDefaultURIFixup_info.js b/docshell/test/unit/test_nsDefaultURIFixup_info.js index 9e33ea484..c606ac32e 100644 --- a/docshell/test/unit/test_nsDefaultURIFixup_info.js +++ b/docshell/test/unit/test_nsDefaultURIFixup_info.js @@ -426,8 +426,6 @@ var testcases = [ { protocolChange: true, }, { input: "?'.com", - fixedURI: "http:///?%27.com", - alternateURI: "http://www..com/?%27.com", keywordLookup: true, protocolChange: true, }, { @@ -436,14 +434,10 @@ var testcases = [ { protocolChange: true }, { input: "?mozilla", - fixedURI: "http:///?mozilla", - alternateURI: "http://www..com/?mozilla", keywordLookup: true, protocolChange: true, }, { input: "??mozilla", - fixedURI: "http:///??mozilla", - alternateURI: "http://www..com/??mozilla", keywordLookup: true, protocolChange: true, }, { diff --git a/docshell/test/unit/test_nsDefaultURIFixup_search.js b/docshell/test/unit/test_nsDefaultURIFixup_search.js index c00b6a85f..c84452b5d 100644 --- a/docshell/test/unit/test_nsDefaultURIFixup_search.js +++ b/docshell/test/unit/test_nsDefaultURIFixup_search.js @@ -74,7 +74,7 @@ var data = [ }, { wrong: 'user:@example.com:8080/this/is/a/test.html', - fixed: 'http://user:@example.com:8080/this/is/a/test.html', + fixed: 'http://user@example.com:8080/this/is/a/test.html', }, { wrong: '//user:pass@example.com:8080/this/is/a/test.html', diff --git a/dom/canvas/CanvasRenderingContext2D.cpp b/dom/canvas/CanvasRenderingContext2D.cpp index 15df2b337..a38c38293 100644 --- a/dom/canvas/CanvasRenderingContext2D.cpp +++ b/dom/canvas/CanvasRenderingContext2D.cpp @@ -105,6 +105,7 @@ #include "mozilla/dom/CanvasPath.h" #include "mozilla/dom/HTMLImageElement.h" #include "mozilla/dom/HTMLVideoElement.h" +#include "mozilla/dom/SVGImageElement.h" #include "mozilla/dom/SVGMatrix.h" #include "mozilla/dom/TextMetrics.h" #include "mozilla/dom/SVGMatrix.h" @@ -2477,10 +2478,10 @@ CanvasRenderingContext2D::CreatePattern(const CanvasImageSource& aSource, return nullptr; } - Element* htmlElement; + Element* element; if (aSource.IsHTMLCanvasElement()) { HTMLCanvasElement* canvas = &aSource.GetAsHTMLCanvasElement(); - htmlElement = canvas; + element = canvas; nsIntSize size = canvas->GetSize(); if (size.width == 0 || size.height == 0) { @@ -2505,7 +2506,7 @@ CanvasRenderingContext2D::CreatePattern(const CanvasImageSource& aSource, } RefPtr<CanvasPattern> pat = - new CanvasPattern(this, srcSurf, repeatMode, htmlElement->NodePrincipal(), canvas->IsWriteOnly(), false); + new CanvasPattern(this, srcSurf, repeatMode, element->NodePrincipal(), canvas->IsWriteOnly(), false); return pat.forget(); } @@ -2516,11 +2517,19 @@ CanvasRenderingContext2D::CreatePattern(const CanvasImageSource& aSource, return nullptr; } - htmlElement = img; + element = img; + } else if (aSource.IsSVGImageElement()) { + SVGImageElement* img = &aSource.GetAsSVGImageElement(); + if (img->IntrinsicState().HasState(NS_EVENT_STATE_BROKEN)) { + aError.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + element = img; } else if (aSource.IsHTMLVideoElement()) { auto& video = aSource.GetAsHTMLVideoElement(); video.MarkAsContentSource(mozilla::dom::HTMLVideoElement::CallerAPI::CREATE_PATTERN); - htmlElement = &video; + element = &video; } else { // Special case for ImageBitmap ImageBitmap& imgBitmap = aSource.GetAsImageBitmap(); @@ -2559,7 +2568,7 @@ CanvasRenderingContext2D::CreatePattern(const CanvasImageSource& aSource, // The canvas spec says that createPattern should use the first frame // of animated images nsLayoutUtils::SurfaceFromElementResult res = - nsLayoutUtils::SurfaceFromElement(htmlElement, + nsLayoutUtils::SurfaceFromElement(element, nsLayoutUtils::SFE_WANT_FIRST_FRAME, mTarget); if (!res.GetSourceSurface()) { @@ -4949,6 +4958,9 @@ CanvasRenderingContext2D::DrawImage(const CanvasImageSource& aImage, if (aImage.IsHTMLImageElement()) { HTMLImageElement* img = &aImage.GetAsHTMLImageElement(); element = img; + } else if (aImage.IsSVGImageElement()) { + SVGImageElement* img = &aImage.GetAsSVGImageElement(); + element = img; } else { HTMLVideoElement* video = &aImage.GetAsHTMLVideoElement(); video->MarkAsContentSource(mozilla::dom::HTMLVideoElement::CallerAPI::DRAW_IMAGE); diff --git a/dom/canvas/CanvasRenderingContext2D.h b/dom/canvas/CanvasRenderingContext2D.h index c3ee3bdcb..d5dff8f3b 100644 --- a/dom/canvas/CanvasRenderingContext2D.h +++ b/dom/canvas/CanvasRenderingContext2D.h @@ -38,8 +38,8 @@ class SourceSurface; } // namespace gl namespace dom { -class HTMLImageElementOrHTMLCanvasElementOrHTMLVideoElementOrImageBitmap; -typedef HTMLImageElementOrHTMLCanvasElementOrHTMLVideoElementOrImageBitmap CanvasImageSource; +class HTMLImageElementOrSVGImageElementOrHTMLCanvasElementOrHTMLVideoElementOrImageBitmap; +typedef HTMLImageElementOrSVGImageElementOrHTMLCanvasElementOrHTMLVideoElementOrImageBitmap CanvasImageSource; class ImageData; class StringOrCanvasGradientOrCanvasPattern; class OwningStringOrCanvasGradientOrCanvasPattern; diff --git a/dom/fetch/InternalHeaders.cpp b/dom/fetch/InternalHeaders.cpp index e81863173..11585615e 100644 --- a/dom/fetch/InternalHeaders.cpp +++ b/dom/fetch/InternalHeaders.cpp @@ -21,12 +21,14 @@ InternalHeaders::InternalHeaders(const nsTArray<Entry>&& aHeaders, HeadersGuardEnum aGuard) : mGuard(aGuard) , mList(aHeaders) + , mListDirty(true) { } InternalHeaders::InternalHeaders(const nsTArray<HeadersEntry>& aHeadersEntryList, HeadersGuardEnum aGuard) : mGuard(aGuard) + , mListDirty(true) { for (const HeadersEntry& headersEntry : aHeadersEntryList) { mList.AppendElement(Entry(headersEntry.name(), headersEntry.value())); @@ -56,6 +58,8 @@ InternalHeaders::Append(const nsACString& aName, const nsACString& aValue, return; } + SetListDirty(); + mList.AppendElement(Entry(lowerName, aValue)); } @@ -69,6 +73,8 @@ InternalHeaders::Delete(const nsACString& aName, ErrorResult& aRv) return; } + SetListDirty(); + // remove in reverse order to minimize copying for (int32_t i = mList.Length() - 1; i >= 0; --i) { if (lowerName == mList[i].mName) { @@ -155,6 +161,8 @@ InternalHeaders::Set(const nsACString& aName, const nsACString& aValue, ErrorRes return; } + SetListDirty(); + int32_t firstIndex = INT32_MAX; // remove in reverse order to minimize copying @@ -177,6 +185,7 @@ InternalHeaders::Set(const nsACString& aName, const nsACString& aValue, ErrorRes void InternalHeaders::Clear() { + SetListDirty(); mList.Clear(); } @@ -419,5 +428,54 @@ InternalHeaders::GetUnsafeHeaders(nsTArray<nsCString>& aNames) const } } +void +InternalHeaders::MaybeSortList() +{ + class Comparator { + public: + bool Equals(const Entry& aA, const Entry& aB) const + { + return aA.mName == aB.mName; + } + + bool LessThan(const Entry& aA, const Entry& aB) const + { + return aA.mName < aB.mName; + } + }; + + if (!mListDirty) { + return; + } + + mListDirty = false; + + Comparator comparator; + + mSortedList.Clear(); + for (const Entry& entry : mList) { + bool found = false; + for (Entry& sortedEntry : mSortedList) { + if (sortedEntry.mName == entry.mName) { + sortedEntry.mValue += ", "; + sortedEntry.mValue += entry.mValue; + found = true; + break; + } + } + + if (!found) { + mSortedList.InsertElementSorted(entry, comparator); + } + } +} + +void +InternalHeaders::SetListDirty() +{ + mSortedList.Clear(); + mListDirty = true; +} + } // namespace dom } // namespace mozilla diff --git a/dom/fetch/InternalHeaders.h b/dom/fetch/InternalHeaders.h index e47066669..9a6d6dae7 100644 --- a/dom/fetch/InternalHeaders.h +++ b/dom/fetch/InternalHeaders.h @@ -45,14 +45,23 @@ private: HeadersGuardEnum mGuard; nsTArray<Entry> mList; + nsTArray<Entry> mSortedList; + + // This boolean is set to true at any writing operation to mList. It's set to + // false when mSortedList is regenerated. This happens when the header is + // iterated. + bool mListDirty; + public: explicit InternalHeaders(HeadersGuardEnum aGuard = HeadersGuardEnum::None) : mGuard(aGuard) + , mListDirty(false) { } explicit InternalHeaders(const InternalHeaders& aOther) : mGuard(HeadersGuardEnum::None) + , mListDirty(true) { ErrorResult result; Fill(aOther, result); @@ -79,19 +88,22 @@ public: bool Has(const nsACString& aName, ErrorResult& aRv) const; void Set(const nsACString& aName, const nsACString& aValue, ErrorResult& aRv); - uint32_t GetIterableLength() const + uint32_t GetIterableLength() { - return mList.Length(); + MaybeSortList(); + return mSortedList.Length(); } - const NS_ConvertASCIItoUTF16 GetKeyAtIndex(unsigned aIndex) const + const NS_ConvertASCIItoUTF16 GetKeyAtIndex(unsigned aIndex) { - MOZ_ASSERT(aIndex < mList.Length()); - return NS_ConvertASCIItoUTF16(mList[aIndex].mName); + MaybeSortList(); + MOZ_ASSERT(aIndex < mSortedList.Length()); + return NS_ConvertASCIItoUTF16(mSortedList[aIndex].mName); } - const NS_ConvertASCIItoUTF16 GetValueAtIndex(unsigned aIndex) const + const NS_ConvertASCIItoUTF16 GetValueAtIndex(unsigned aIndex) { - MOZ_ASSERT(aIndex < mList.Length()); - return NS_ConvertASCIItoUTF16(mList[aIndex].mValue); + MaybeSortList(); + MOZ_ASSERT(aIndex < mSortedList.Length()); + return NS_ConvertASCIItoUTF16(mSortedList[aIndex].mValue); } void Clear(); @@ -152,6 +164,9 @@ private: const nsACString& aValue); static bool IsRevalidationHeader(const nsACString& aName); + + void MaybeSortList(); + void SetListDirty(); }; } // namespace dom diff --git a/dom/html/HTMLInputElement.cpp b/dom/html/HTMLInputElement.cpp index d46eccdbc..52062c4b9 100644 --- a/dom/html/HTMLInputElement.cpp +++ b/dom/html/HTMLInputElement.cpp @@ -114,15 +114,12 @@ #include <limits> #include "nsIColorPicker.h" -#include "nsIDatePicker.h" #include "nsIStringEnumerator.h" #include "HTMLSplitOnSpacesTokenizer.h" #include "nsIController.h" #include "nsIMIMEInfo.h" #include "nsFrameSelection.h" -#include "nsIConsoleService.h" - // input type=date #include "js/Date.h" @@ -543,9 +540,8 @@ GetDOMFileOrDirectoryPath(const OwningFileOrDirectory& aData, bool HTMLInputElement::ValueAsDateEnabled(JSContext* cx, JSObject* obj) { - return Preferences::GetBool("dom.experimental_forms", false) || - Preferences::GetBool("dom.forms.datepicker", false) || - Preferences::GetBool("dom.forms.datetime", false); + return IsExperimentalFormsEnabled() || IsInputDateTimeEnabled() || + IsInputDateTimeOthersEnabled(); } NS_IMETHODIMP @@ -628,7 +624,7 @@ HTMLInputElement::nsFilePickerShownCallback::Done(int16_t aResult) RefPtr<DispatchChangeEventCallback> dispatchChangeEventCallback = new DispatchChangeEventCallback(mInput); - if (Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) && + if (IsWebkitDirPickerEnabled() && mInput->HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory)) { ErrorResult error; GetFilesHelper* helper = mInput->GetOrCreateGetFilesHelper(true, error); @@ -747,59 +743,6 @@ nsColorPickerShownCallback::Done(const nsAString& aColor) NS_IMPL_ISUPPORTS(nsColorPickerShownCallback, nsIColorPickerShownCallback) -class DatePickerShownCallback final : public nsIDatePickerShownCallback -{ - ~DatePickerShownCallback() {} -public: - DatePickerShownCallback(HTMLInputElement* aInput, - nsIDatePicker* aDatePicker) - : mInput(aInput) - , mDatePicker(aDatePicker) - {} - - NS_DECL_ISUPPORTS - - NS_IMETHOD Done(const nsAString& aDate) override; - NS_IMETHOD Cancel() override; - -private: - RefPtr<HTMLInputElement> mInput; - nsCOMPtr<nsIDatePicker> mDatePicker; -}; - -NS_IMETHODIMP -DatePickerShownCallback::Cancel() -{ - mInput->PickerClosed(); - return NS_OK; -} - -NS_IMETHODIMP -DatePickerShownCallback::Done(const nsAString& aDate) -{ - nsAutoString oldValue; - - mInput->PickerClosed(); - mInput->GetValue(oldValue); - - if(!oldValue.Equals(aDate)){ - mInput->SetValue(aDate); - nsContentUtils::DispatchTrustedEvent(mInput->OwnerDoc(), - static_cast<nsIDOMHTMLInputElement*>(mInput.get()), - NS_LITERAL_STRING("input"), true, - false); - return nsContentUtils::DispatchTrustedEvent(mInput->OwnerDoc(), - static_cast<nsIDOMHTMLInputElement*>(mInput.get()), - NS_LITERAL_STRING("change"), true, - false); - } - - return NS_OK; -} - -NS_IMPL_ISUPPORTS(DatePickerShownCallback, nsIDatePickerShownCallback) - - bool HTMLInputElement::IsPopupBlocked() const { @@ -825,56 +768,6 @@ HTMLInputElement::IsPopupBlocked() const } nsresult -HTMLInputElement::InitDatePicker() -{ - if (!Preferences::GetBool("dom.forms.datepicker", false)) { - return NS_OK; - } - - if (mPickerRunning) { - NS_WARNING("Just one nsIDatePicker is allowed"); - return NS_ERROR_FAILURE; - } - - nsCOMPtr<nsIDocument> doc = OwnerDoc(); - - nsCOMPtr<nsPIDOMWindowOuter> win = doc->GetWindow(); - if (!win) { - return NS_ERROR_FAILURE; - } - - if (IsPopupBlocked()) { - win->FirePopupBlockedEvent(doc, nullptr, EmptyString(), EmptyString()); - return NS_OK; - } - - // Get Loc title - nsXPIDLString title; - nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES, - "DatePicker", title); - - nsresult rv; - nsCOMPtr<nsIDatePicker> datePicker = do_CreateInstance("@mozilla.org/datepicker;1", &rv); - if (!datePicker) { - return rv; - } - - nsAutoString initialValue; - GetValueInternal(initialValue); - rv = datePicker->Init(win, title, initialValue); - - nsCOMPtr<nsIDatePickerShownCallback> callback = - new DatePickerShownCallback(this, datePicker); - - rv = datePicker->Open(callback); - if (NS_SUCCEEDED(rv)) { - mPickerRunning = true; - } - - return rv; -} - -nsresult HTMLInputElement::InitColorPicker() { if (mPickerRunning) { @@ -1919,6 +1812,22 @@ HTMLInputElement::ConvertStringToNumber(nsAString& aValue, aResultValue = Decimal::fromDouble(days * kMsPerDay); return true; } + case NS_FORM_INPUT_DATETIME_LOCAL: + { + uint32_t year, month, day, timeInMs; + if (!ParseDateTimeLocal(aValue, &year, &month, &day, &timeInMs)) { + return false; + } + + JS::ClippedTime time = JS::TimeClip(JS::MakeDate(year, month - 1, day, + timeInMs)); + if (!time.isValid()) { + return false; + } + + aResultValue = Decimal::fromDouble(time.toDouble()); + return true; + } default: MOZ_ASSERT(false, "Unrecognized input type"); return false; @@ -2108,21 +2017,17 @@ HTMLInputElement::ConvertNumberToString(Decimal aValue, } case NS_FORM_INPUT_TIME: { + aValue = aValue.floor(); // Per spec, we need to truncate |aValue| and we should only represent // times inside a day [00:00, 24:00[, which means that we should do a // modulo on |aValue| using the number of milliseconds in a day (86400000). - uint32_t value = NS_floorModulo(aValue.floor(), Decimal(86400000)).toDouble(); - - uint16_t milliseconds = value % 1000; - value /= 1000; + uint32_t value = + NS_floorModulo(aValue, Decimal::fromDouble(kMsPerDay)).toDouble(); - uint8_t seconds = value % 60; - value /= 60; - - uint8_t minutes = value % 60; - value /= 60; - - uint8_t hours = value; + uint16_t milliseconds, seconds, minutes, hours; + if (!GetTimeFromMs(value, &hours, &minutes, &seconds, &milliseconds)) { + return false; + } if (milliseconds != 0) { aResultString.AppendPrintf("%02d:%02d:%02d.%03d", @@ -2192,6 +2097,42 @@ HTMLInputElement::ConvertNumberToString(Decimal aValue, aResultString.AppendPrintf("%04.0f-W%02d", year, week); return true; } + case NS_FORM_INPUT_DATETIME_LOCAL: + { + aValue = aValue.floor(); + + uint32_t timeValue = + NS_floorModulo(aValue, Decimal::fromDouble(kMsPerDay)).toDouble(); + + uint16_t milliseconds, seconds, minutes, hours; + if (!GetTimeFromMs(timeValue, + &hours, &minutes, &seconds, &milliseconds)) { + return false; + } + + double year = JS::YearFromTime(aValue.toDouble()); + double month = JS::MonthFromTime(aValue.toDouble()); + double day = JS::DayFromTime(aValue.toDouble()); + + if (IsNaN(year) || IsNaN(month) || IsNaN(day)) { + return false; + } + + if (milliseconds != 0) { + aResultString.AppendPrintf("%04.0f-%02.0f-%02.0fT%02d:%02d:%02d.%03d", + year, month + 1, day, hours, minutes, + seconds, milliseconds); + } else if (seconds != 0) { + aResultString.AppendPrintf("%04.0f-%02.0f-%02.0fT%02d:%02d:%02d", + year, month + 1, day, hours, minutes, + seconds); + } else { + aResultString.AppendPrintf("%04.0f-%02.0f-%02.0fT%02d:%02d", + year, month + 1, day, hours, minutes); + } + + return true; + } default: MOZ_ASSERT(false, "Unrecognized input type"); return false; @@ -2202,8 +2143,7 @@ HTMLInputElement::ConvertNumberToString(Decimal aValue, Nullable<Date> HTMLInputElement::GetValueAsDate(ErrorResult& aRv) { - // TODO: this is temporary until bug 888331 is fixed. - if (!IsDateTimeInputType(mType) || mType == NS_FORM_INPUT_DATETIME_LOCAL) { + if (!IsDateTimeInputType(mType)) { return Nullable<Date>(); } @@ -2261,6 +2201,19 @@ HTMLInputElement::GetValueAsDate(ErrorResult& aRv) return Nullable<Date>(Date(time)); } + case NS_FORM_INPUT_DATETIME_LOCAL: + { + uint32_t year, month, day, timeInMs; + nsAutoString value; + GetValueInternal(value); + if (!ParseDateTimeLocal(value, &year, &month, &day, &timeInMs)) { + return Nullable<Date>(); + } + + JS::ClippedTime time = JS::TimeClip(JS::MakeDate(year, month - 1, day, + timeInMs)); + return Nullable<Date>(Date(time)); + } } MOZ_ASSERT(false, "Unrecognized input type"); @@ -2271,8 +2224,7 @@ HTMLInputElement::GetValueAsDate(ErrorResult& aRv) void HTMLInputElement::SetValueAsDate(Nullable<Date> aDate, ErrorResult& aRv) { - // TODO: this is temporary until bug 888331 is fixed. - if (!IsDateTimeInputType(mType) || mType == NS_FORM_INPUT_DATETIME_LOCAL) { + if (!IsDateTimeInputType(mType)) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return; } @@ -2380,11 +2332,8 @@ HTMLInputElement::GetMaximum() const Decimal HTMLInputElement::GetStepBase() const { - MOZ_ASSERT(mType == NS_FORM_INPUT_NUMBER || - mType == NS_FORM_INPUT_DATE || - mType == NS_FORM_INPUT_TIME || - mType == NS_FORM_INPUT_MONTH || - mType == NS_FORM_INPUT_WEEK || + MOZ_ASSERT(IsDateTimeInputType(mType) || + mType == NS_FORM_INPUT_NUMBER || mType == NS_FORM_INPUT_RANGE, "Check that kDefaultStepBase is correct for this new type"); @@ -2515,11 +2464,8 @@ HTMLInputElement::ApplyStep(int32_t aStep) bool HTMLInputElement::IsExperimentalMobileType(uint8_t aType) { - return (aType == NS_FORM_INPUT_DATE && - !Preferences::GetBool("dom.forms.datetime", false) && - !Preferences::GetBool("dom.forms.datepicker", false)) || - (aType == NS_FORM_INPUT_TIME && - !Preferences::GetBool("dom.forms.datetime", false)); + return (aType == NS_FORM_INPUT_DATE || aType == NS_FORM_INPUT_TIME) && + !IsInputDateTimeEnabled(); } bool @@ -2832,7 +2778,8 @@ HTMLInputElement::GetOwnerDateTimeControl() HTMLInputElement::FromContentOrNull( GetParent()->GetParent()->GetParent()->GetParent()); if (ownerDateTimeControl && - ownerDateTimeControl->mType == NS_FORM_INPUT_TIME) { + (ownerDateTimeControl->mType == NS_FORM_INPUT_TIME || + ownerDateTimeControl->mType == NS_FORM_INPUT_DATE)) { return ownerDateTimeControl; } } @@ -3024,8 +2971,8 @@ HTMLInputElement::GetDisplayFileName(nsAString& aValue) const nsXPIDLString value; if (mFilesOrDirectories.IsEmpty()) { - if ((Preferences::GetBool("dom.input.dirpicker", false) && Allowdirs()) || - (Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) && + if ((IsDirPickerEnabled() && Allowdirs()) || + (IsWebkitDirPickerEnabled() && HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory))) { nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES, "NoDirSelected", value); @@ -3054,7 +3001,7 @@ HTMLInputElement::SetFilesOrDirectories(const nsTArray<OwningFileOrDirectory>& a { ClearGetFilesHelpers(); - if (Preferences::GetBool("dom.webkitBlink.filesystem.enabled", false)) { + if (IsWebkitFileSystemEnabled()) { HTMLInputElementBinding::ClearCachedWebkitEntriesValue(this); mEntries.Clear(); } @@ -3073,7 +3020,7 @@ HTMLInputElement::SetFiles(nsIDOMFileList* aFiles, mFilesOrDirectories.Clear(); ClearGetFilesHelpers(); - if (Preferences::GetBool("dom.webkitBlink.filesystem.enabled", false)) { + if (IsWebkitFileSystemEnabled()) { HTMLInputElementBinding::ClearCachedWebkitEntriesValue(this); mEntries.Clear(); } @@ -3096,14 +3043,14 @@ HTMLInputElement::MozSetDndFilesAndDirectories(const nsTArray<OwningFileOrDirect { SetFilesOrDirectories(aFilesOrDirectories, true); - if (Preferences::GetBool("dom.webkitBlink.filesystem.enabled", false)) { + if (IsWebkitFileSystemEnabled()) { UpdateEntries(aFilesOrDirectories); } RefPtr<DispatchChangeEventCallback> dispatchChangeEventCallback = new DispatchChangeEventCallback(this); - if (Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) && + if (IsWebkitDirPickerEnabled() && HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory)) { ErrorResult rv; GetFilesHelper* helper = GetOrCreateGetFilesHelper(true /* recursionFlag */, @@ -3181,8 +3128,8 @@ HTMLInputElement::GetFiles() return nullptr; } - if (Preferences::GetBool("dom.input.dirpicker", false) && Allowdirs() && - (!Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) || + if (IsDirPickerEnabled() && Allowdirs() && + (!IsWebkitDirPickerEnabled() || !HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory))) { return nullptr; } @@ -3282,7 +3229,8 @@ HTMLInputElement::SetValueInternal(const nsAString& aValue, uint32_t aFlags) if (frame) { frame->UpdateForValueChange(); } - } else if (mType == NS_FORM_INPUT_TIME && + } else if ((mType == NS_FORM_INPUT_TIME || + mType == NS_FORM_INPUT_DATE) && !IsExperimentalMobileType(mType)) { nsDateTimeControlFrame* frame = do_QueryFrame(GetPrimaryFrame()); if (frame) { @@ -3591,7 +3539,8 @@ HTMLInputElement::Blur(ErrorResult& aError) } } - if (mType == NS_FORM_INPUT_TIME && !IsExperimentalMobileType(mType)) { + if ((mType == NS_FORM_INPUT_TIME || mType == NS_FORM_INPUT_DATE) && + !IsExperimentalMobileType(mType)) { nsDateTimeControlFrame* frame = do_QueryFrame(GetPrimaryFrame()); if (frame) { frame->HandleBlurEvent(); @@ -3618,7 +3567,8 @@ HTMLInputElement::Focus(ErrorResult& aError) } } - if (mType == NS_FORM_INPUT_TIME && !IsExperimentalMobileType(mType)) { + if ((mType == NS_FORM_INPUT_TIME || mType == NS_FORM_INPUT_DATE) && + !IsExperimentalMobileType(mType)) { nsDateTimeControlFrame* frame = do_QueryFrame(GetPrimaryFrame()); if (frame) { frame->HandleFocusEvent(); @@ -3956,7 +3906,7 @@ HTMLInputElement::PreHandleEvent(EventChainPreVisitor& aVisitor) } } - if (mType == NS_FORM_INPUT_TIME && + if ((mType == NS_FORM_INPUT_TIME || mType == NS_FORM_INPUT_DATE) && !IsExperimentalMobileType(mType) && aVisitor.mEvent->mMessage == eFocus && aVisitor.mEvent->mOriginalTarget == this) { @@ -4083,7 +4033,8 @@ HTMLInputElement::PreHandleEvent(EventChainPreVisitor& aVisitor) // Stop the event if the related target's first non-native ancestor is the // same as the original target's first non-native ancestor (we are moving // inside of the same element). - if (mType == NS_FORM_INPUT_TIME && !IsExperimentalMobileType(mType) && + if ((mType == NS_FORM_INPUT_TIME || mType == NS_FORM_INPUT_DATE) && + !IsExperimentalMobileType(mType) && (aVisitor.mEvent->mMessage == eFocus || aVisitor.mEvent->mMessage == eFocusIn || aVisitor.mEvent->mMessage == eFocusOut || @@ -4361,8 +4312,8 @@ HTMLInputElement::MaybeInitPickers(EventChainPostVisitor& aVisitor) do_QueryInterface(aVisitor.mEvent->mOriginalTarget); if (target && target->FindFirstNonChromeOnlyAccessContent() == this && - ((Preferences::GetBool("dom.input.dirpicker", false) && Allowdirs()) || - (Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) && + ((IsDirPickerEnabled() && Allowdirs()) || + (IsWebkitDirPickerEnabled() && HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory)))) { type = FILE_PICKER_DIRECTORY; } @@ -4371,9 +4322,6 @@ HTMLInputElement::MaybeInitPickers(EventChainPostVisitor& aVisitor) if (mType == NS_FORM_INPUT_COLOR) { return InitColorPicker(); } - if (mType == NS_FORM_INPUT_DATE) { - return InitDatePicker(); - } return NS_OK; } @@ -5383,6 +5331,29 @@ HTMLInputElement::MaximumWeekInYear(uint32_t aYear) const } bool +HTMLInputElement::GetTimeFromMs(double aValue, uint16_t* aHours, + uint16_t* aMinutes, uint16_t* aSeconds, + uint16_t* aMilliseconds) const { + MOZ_ASSERT(aValue >= 0 && aValue < kMsPerDay, + "aValue must be milliseconds within a day!"); + + uint32_t value = floor(aValue); + + *aMilliseconds = value % 1000; + value /= 1000; + + *aSeconds = value % 60; + value /= 60; + + *aMinutes = value % 60; + value /= 60; + + *aHours = value; + + return true; +} + +bool HTMLInputElement::IsValidWeek(const nsAString& aValue) const { uint32_t year, week; @@ -5730,20 +5701,131 @@ HTMLInputElement::ParseTime(const nsAString& aValue, uint32_t* aResult) return true; } -static bool -IsDateTimeEnabled(int32_t aNewType) +/* static */ bool +HTMLInputElement::IsDateTimeTypeSupported(uint8_t aDateTimeInputType) +{ + return ((aDateTimeInputType == NS_FORM_INPUT_DATE || + aDateTimeInputType == NS_FORM_INPUT_TIME) && + (IsInputDateTimeEnabled() || IsExperimentalFormsEnabled())) || + ((aDateTimeInputType == NS_FORM_INPUT_MONTH || + aDateTimeInputType == NS_FORM_INPUT_WEEK || + aDateTimeInputType == NS_FORM_INPUT_DATETIME_LOCAL) && + IsInputDateTimeOthersEnabled()); +} + +/* static */ bool +HTMLInputElement::IsWebkitDirPickerEnabled() { - return (aNewType == NS_FORM_INPUT_DATE && - (Preferences::GetBool("dom.forms.datetime", false) || - Preferences::GetBool("dom.experimental_forms", false) || - Preferences::GetBool("dom.forms.datepicker", false))) || - (aNewType == NS_FORM_INPUT_TIME && - (Preferences::GetBool("dom.forms.datetime", false) || - Preferences::GetBool("dom.experimental_forms", false))) || - ((aNewType == NS_FORM_INPUT_MONTH || - aNewType == NS_FORM_INPUT_WEEK || - aNewType == NS_FORM_INPUT_DATETIME_LOCAL) && - Preferences::GetBool("dom.forms.datetime", false)); + static bool sWebkitDirPickerEnabled = false; + static bool sWebkitDirPickerPrefCached = false; + if (!sWebkitDirPickerPrefCached) { + sWebkitDirPickerPrefCached = true; + Preferences::AddBoolVarCache(&sWebkitDirPickerEnabled, + "dom.webkitBlink.dirPicker.enabled", + false); + } + + return sWebkitDirPickerEnabled; +} + +/* static */ bool +HTMLInputElement::IsWebkitFileSystemEnabled() +{ + static bool sWebkitFileSystemEnabled = false; + static bool sWebkitFileSystemPrefCached = false; + if (!sWebkitFileSystemPrefCached) { + sWebkitFileSystemPrefCached = true; + Preferences::AddBoolVarCache(&sWebkitFileSystemEnabled, + "dom.webkitBlink.filesystem.enabled", + false); + } + + return sWebkitFileSystemEnabled; +} + +/* static */ bool +HTMLInputElement::IsDirPickerEnabled() +{ + static bool sDirPickerEnabled = false; + static bool sDirPickerPrefCached = false; + if (!sDirPickerPrefCached) { + sDirPickerPrefCached = true; + Preferences::AddBoolVarCache(&sDirPickerEnabled, "dom.input.dirpicker", + false); + } + + return sDirPickerEnabled; +} + +/* static */ bool +HTMLInputElement::IsExperimentalFormsEnabled() +{ + static bool sExperimentalFormsEnabled = false; + static bool sExperimentalFormsPrefCached = false; + if (!sExperimentalFormsPrefCached) { + sExperimentalFormsPrefCached = true; + Preferences::AddBoolVarCache(&sExperimentalFormsEnabled, + "dom.experimental_forms", + false); + } + + return sExperimentalFormsEnabled; +} + +/* static */ bool +HTMLInputElement::IsInputDateTimeEnabled() +{ + static bool sDateTimeEnabled = false; + static bool sDateTimePrefCached = false; + if (!sDateTimePrefCached) { + sDateTimePrefCached = true; + Preferences::AddBoolVarCache(&sDateTimeEnabled, "dom.forms.datetime", + false); + } + + return sDateTimeEnabled; +} + +/* static */ bool +HTMLInputElement::IsInputDateTimeOthersEnabled() +{ + static bool sDateTimeOthersEnabled = false; + static bool sDateTimeOthersPrefCached = false; + if (!sDateTimeOthersPrefCached) { + sDateTimeOthersPrefCached = true; + Preferences::AddBoolVarCache(&sDateTimeOthersEnabled, + "dom.forms.datetime.others", false); + } + + return sDateTimeOthersEnabled; +} + +/* static */ bool +HTMLInputElement::IsInputNumberEnabled() +{ + static bool sInputNumberEnabled = false; + static bool sInputNumberPrefCached = false; + if (!sInputNumberPrefCached) { + sInputNumberPrefCached = true; + Preferences::AddBoolVarCache(&sInputNumberEnabled, "dom.forms.number", + false); + } + + return sInputNumberEnabled; +} + +/* static */ bool +HTMLInputElement::IsInputColorEnabled() +{ + static bool sInputColorEnabled = false; + static bool sInputColorPrefCached = false; + if (!sInputColorPrefCached) { + sInputColorPrefCached = true; + Preferences::AddBoolVarCache(&sInputColorEnabled, "dom.forms.color", + false); + } + + return sInputColorEnabled; } bool @@ -5760,13 +5842,9 @@ HTMLInputElement::ParseAttribute(int32_t aNamespaceID, bool success = aResult.ParseEnumValue(aValue, kInputTypeTable, false); if (success) { newType = aResult.GetEnumValue(); - if ((IsExperimentalMobileType(newType) && - !Preferences::GetBool("dom.experimental_forms", false)) || - (newType == NS_FORM_INPUT_NUMBER && - !Preferences::GetBool("dom.forms.number", false)) || - (newType == NS_FORM_INPUT_COLOR && - !Preferences::GetBool("dom.forms.color", false)) || - (IsDateTimeInputType(newType) && !IsDateTimeEnabled(newType))) { + if ((newType == NS_FORM_INPUT_NUMBER && !IsInputNumberEnabled()) || + (newType == NS_FORM_INPUT_COLOR && !IsInputColorEnabled()) || + (IsDateTimeInputType(newType) && !IsDateTimeTypeSupported(newType))) { newType = kInputDefaultType->value; aResult.SetTo(newType, &aValue); } @@ -7161,13 +7239,15 @@ HTMLInputElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable, int32_t* if (mType == NS_FORM_INPUT_FILE || mType == NS_FORM_INPUT_NUMBER || - mType == NS_FORM_INPUT_TIME) { + mType == NS_FORM_INPUT_TIME || + mType == NS_FORM_INPUT_DATE) { if (aTabIndex) { // We only want our native anonymous child to be tabable to, not ourself. *aTabIndex = -1; } if (mType == NS_FORM_INPUT_NUMBER || - mType == NS_FORM_INPUT_TIME) { + mType == NS_FORM_INPUT_TIME || + mType == NS_FORM_INPUT_DATE) { *aIsFocusable = true; } else { *aIsFocusable = defaultFocusable; @@ -7650,8 +7730,7 @@ HTMLInputElement::HasPatternMismatch() const bool HTMLInputElement::IsRangeOverflow() const { - // TODO: this is temporary until bug 888331 is fixed. - if (!DoesMinMaxApply() || mType == NS_FORM_INPUT_DATETIME_LOCAL) { + if (!DoesMinMaxApply()) { return false; } @@ -7671,8 +7750,7 @@ HTMLInputElement::IsRangeOverflow() const bool HTMLInputElement::IsRangeUnderflow() const { - // TODO: this is temporary until bug 888331 is fixed. - if (!DoesMinMaxApply() || mType == NS_FORM_INPUT_DATETIME_LOCAL) { + if (!DoesMinMaxApply()) { return false; } @@ -8622,6 +8700,7 @@ HTMLInputElement::GetStepScaleFactor() const case NS_FORM_INPUT_RANGE: return kStepScaleFactorNumberRange; case NS_FORM_INPUT_TIME: + case NS_FORM_INPUT_DATETIME_LOCAL: return kStepScaleFactorTime; case NS_FORM_INPUT_MONTH: return kStepScaleFactorMonth; @@ -8646,6 +8725,7 @@ HTMLInputElement::GetDefaultStep() const case NS_FORM_INPUT_RANGE: return kDefaultStep; case NS_FORM_INPUT_TIME: + case NS_FORM_INPUT_DATETIME_LOCAL: return kDefaultStepTime; default: MOZ_ASSERT(false, "Unrecognized input type"); @@ -8680,8 +8760,7 @@ HTMLInputElement::UpdateHasRange() mHasRange = false; - // TODO: this is temporary until bug 888331 is fixed. - if (!DoesMinMaxApply() || mType == NS_FORM_INPUT_DATETIME_LOCAL) { + if (!DoesMinMaxApply()) { return; } diff --git a/dom/html/HTMLInputElement.h b/dom/html/HTMLInputElement.h index 9ca876aee..98a590443 100644 --- a/dom/html/HTMLInputElement.h +++ b/dom/html/HTMLInputElement.h @@ -796,6 +796,15 @@ public: void UpdateDateTimePicker(const DateTimeValue& aValue); void CloseDateTimePicker(); + /* + * The following are called from datetime input box binding to get the + * corresponding computed values. + */ + double GetStepAsDouble() { return GetStep().toDouble(); } + double GetStepBaseAsDouble() { return GetStepBase().toDouble(); } + double GetMinimumAsDouble() { return GetMinimum().toDouble(); } + double GetMaximumAsDouble() { return GetMaximum().toDouble(); } + HTMLInputElement* GetOwnerNumberControl(); HTMLInputElement* GetOwnerDateTimeControl(); @@ -1061,11 +1070,7 @@ protected: /** * Returns if the step attribute apply for the current type. */ - bool DoesStepApply() const - { - // TODO: this is temporary until bug 888331 is fixed. - return DoesMinMaxApply() && mType != NS_FORM_INPUT_DATETIME_LOCAL; - } + bool DoesStepApply() const { return DoesMinMaxApply(); } /** * Returns if stepDown and stepUp methods apply for the current type. @@ -1075,11 +1080,7 @@ protected: /** * Returns if valueAsNumber attribute applies for the current type. */ - bool DoesValueAsNumberApply() const - { - // TODO: this is temporary until bug 888331 is fixed. - return DoesMinMaxApply() && mType != NS_FORM_INPUT_DATETIME_LOCAL; - } + bool DoesValueAsNumberApply() const { return DoesMinMaxApply(); } /** * Returns if autocomplete attribute applies for the current type. @@ -1287,6 +1288,7 @@ protected: * https://html.spec.whatwg.org/multipage/infrastructure.html#valid-normalised-local-date-and-time-string */ void NormalizeDateTimeLocal(nsAString& aValue) const; + /** * This methods returns the number of days since epoch for a given year and * week. @@ -1318,6 +1320,13 @@ protected: uint32_t MaximumWeekInYear(uint32_t aYear) const; /** + * This method converts aValue (milliseconds within a day) to hours, minutes, + * seconds and milliseconds. + */ + bool GetTimeFromMs(double aValue, uint16_t* aHours, uint16_t* aMinutes, + uint16_t* aSeconds, uint16_t* aMilliseconds) const; + + /** * This methods returns true if it's a leap year. */ bool IsLeapYear(uint32_t aYear) const; @@ -1442,7 +1451,6 @@ protected: }; nsresult InitFilePicker(FilePickerType aType); nsresult InitColorPicker(); - nsresult InitDatePicker(); /** * Use this function before trying to open a picker. @@ -1632,9 +1640,73 @@ private: return IsSingleLineTextControl(false, aType) || aType == NS_FORM_INPUT_RANGE || aType == NS_FORM_INPUT_NUMBER || - aType == NS_FORM_INPUT_TIME; + aType == NS_FORM_INPUT_TIME || + aType == NS_FORM_INPUT_DATE; } + /** + * Checks if aDateTimeInputType should be supported based on "dom.forms.datetime", + * and "dom.experimental_forms". + */ + static bool + IsDateTimeTypeSupported(uint8_t aDateTimeInputType); + + /** + * Checks preference "dom.webkitBlink.dirPicker.enabled" to determine if + * webkitdirectory should be supported. + */ + static bool + IsWebkitDirPickerEnabled(); + + /** + * Checks preference "dom.webkitBlink.filesystem.enabled" to determine if + * webkitEntries should be supported. + */ + static bool + IsWebkitFileSystemEnabled(); + + /** + * Checks preference "dom.input.dirpicker" to determine if file and directory + * entries API should be supported. + */ + static bool + IsDirPickerEnabled(); + + /** + * Checks preference "dom.experimental_forms" to determine if experimental + * implementation of input element should be enabled. + */ + static bool + IsExperimentalFormsEnabled(); + + /** + * Checks preference "dom.forms.datetime" to determine if input date and time + * should be supported. + */ + static bool + IsInputDateTimeEnabled(); + + /** + * Checks preference "dom.forms.datetime.others" to determine if input week, + * month and datetime-local should be supported. + */ + static bool + IsInputDateTimeOthersEnabled(); + + /** + * Checks preference "dom.forms.number" to determine if input type=number + * should be supported. + */ + static bool + IsInputNumberEnabled(); + + /** + * Checks preference "dom.forms.color" to determine if date/time related + * types should be supported. + */ + static bool + IsInputColorEnabled(); + struct nsFilePickerFilter { nsFilePickerFilter() : mFilterMask(0) {} diff --git a/dom/html/nsIFormControl.h b/dom/html/nsIFormControl.h index aaa92146c..e07f7c829 100644 --- a/dom/html/nsIFormControl.h +++ b/dom/html/nsIFormControl.h @@ -270,8 +270,8 @@ nsIFormControl::IsSingleLineTextControl(bool aExcludePassword, uint32_t aType) #if defined(MOZ_WIDGET_ANDROID) || defined(MOZ_WIDGET_GONK) // On Android/B2G, date/time input appears as a normal text box. aType == NS_FORM_INPUT_TIME || -#endif aType == NS_FORM_INPUT_DATE || +#endif aType == NS_FORM_INPUT_MONTH || aType == NS_FORM_INPUT_WEEK || aType == NS_FORM_INPUT_DATETIME_LOCAL || diff --git a/dom/html/nsTextEditorState.cpp b/dom/html/nsTextEditorState.cpp index d70199362..187afb66d 100644 --- a/dom/html/nsTextEditorState.cpp +++ b/dom/html/nsTextEditorState.cpp @@ -1418,19 +1418,16 @@ nsTextEditorState::PrepareEditor(const nsAString *aValue) } } - if (shouldInitializeEditor) { - // Initialize the plaintext editor - nsCOMPtr<nsIPlaintextEditor> textEditor(do_QueryInterface(newEditor)); - if (textEditor) { + // Initialize the plaintext editor + nsCOMPtr<nsIPlaintextEditor> textEditor = do_QueryInterface(newEditor); + if (textEditor) { + if (shouldInitializeEditor) { // Set up wrapping textEditor->SetWrapColumn(GetWrapCols()); - - // Set max text field length - int32_t maxLength; - if (GetMaxLength(&maxLength)) { - textEditor->SetMaxTextLength(maxLength); - } } + + // Set max text field length + textEditor->SetMaxTextLength(GetMaxLength()); } nsCOMPtr<nsIContent> content = do_QueryInterface(mTextCtrlElement); @@ -1895,22 +1892,22 @@ be called if @placeholder is the empty string when trimmed from line breaks"); return NS_OK; } -bool -nsTextEditorState::GetMaxLength(int32_t* aMaxLength) +int32_t +nsTextEditorState::GetMaxLength() { nsCOMPtr<nsIContent> content = do_QueryInterface(mTextCtrlElement); nsGenericHTMLElement* element = nsGenericHTMLElement::FromContentOrNull(content); - NS_ENSURE_TRUE(element, false); + if (NS_WARN_IF(!element)) { + return -1; + } const nsAttrValue* attr = element->GetParsedAttr(nsGkAtoms::maxlength); if (attr && attr->Type() == nsAttrValue::eInteger) { - *aMaxLength = attr->GetIntegerValue(); - - return true; + return attr->GetIntegerValue(); } - return false; + return -1; } void diff --git a/dom/html/nsTextEditorState.h b/dom/html/nsTextEditorState.h index 11494f155..caf5e8eed 100644 --- a/dom/html/nsTextEditorState.h +++ b/dom/html/nsTextEditorState.h @@ -203,7 +203,7 @@ public: * @param aMaxLength the value of the max length attr * @returns false if attr not defined */ - bool GetMaxLength(int32_t* aMaxLength); + int32_t GetMaxLength(); void ClearValueCache() { mCachedValue.Truncate(); } diff --git a/dom/html/test/forms/mochitest.ini b/dom/html/test/forms/mochitest.ini index 35955b189..199e4baf8 100644 --- a/dom/html/test/forms/mochitest.ini +++ b/dom/html/test/forms/mochitest.ini @@ -30,8 +30,14 @@ skip-if = os == "android" # up/down arrow keys not supported on android skip-if = android_version == '18' # Android, bug 1147974 [test_input_color_picker_update.html] skip-if = android_version == '18' # Android, bug 1147974 +[test_input_date_key_events.html] +skip-if = os == "android" +[test_input_datetime_input_change_events.html] +skip-if = os == "android" [test_input_datetime_focus_blur.html] skip-if = os == "android" +[test_input_datetime_focus_blur_events.html] +skip-if = os == "android" [test_input_datetime_tabindex.html] skip-if = os == "android" [test_input_defaultValue.html] @@ -61,8 +67,6 @@ skip-if = os == "android" [test_input_textarea_set_value_no_scroll.html] [test_input_time_key_events.html] skip-if = os == "android" -[test_input_time_focus_blur_events.html] -skip-if = os == "android" [test_input_types_pref.html] [test_input_typing_sanitization.html] [test_input_untrusted_key_events.html] diff --git a/dom/html/test/forms/test_input_date_key_events.html b/dom/html/test/forms/test_input_date_key_events.html new file mode 100644 index 000000000..f502d6a4d --- /dev/null +++ b/dom/html/test/forms/test_input_date_key_events.html @@ -0,0 +1,228 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1286182 +--> +<head> + <title>Test key events for time control</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <meta charset="UTF-8"> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1286182">Mozilla Bug 1286182</a> +<p id="display"></p> +<div id="content"> + <input id="input" type="date"> +</div> +<pre id="test"> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +// Turn off Spatial Navigation because it hijacks arrow keydown events: +SimpleTest.waitForFocus(function() { + SpecialPowers.pushPrefEnv({"set":[["snav.enabled", false]]}, function() { + test(); + SimpleTest.finish(); + }); +}); + +var testData = [ + /** + * keys: keys to send to the input element. + * initialVal: initial value set to the input element. + * expectedVal: expected value of the input element after sending the keys. + */ + { + // Type 11222016, default order is month, day, year. + keys: ["11222016"], + initialVal: "", + expectedVal: "2016-11-22" + }, + { + // Type 3 in the month field will automatically advance to the day field, + // then type 5 in the day field will automatically advance to the year + // field. + keys: ["352016"], + initialVal: "", + expectedVal: "2016-03-05" + }, + { + // Type 13 in the month field will set it to the maximum month, which is + // 12. + keys: ["13012016"], + initialVal: "", + expectedVal: "2016-12-01" + }, + { + // Type 00 in the month field will set it to the minimum month, which is 1. + keys: ["00012016"], + initialVal: "", + expectedVal: "2016-01-01" + }, + { + // Type 33 in the day field will set it to the maximum day, which is 31. + keys: ["12332016"], + initialVal: "", + expectedVal: "2016-12-31" + }, + { + // Type 00 in the day field will set it to the minimum day, which is 1. + keys: ["12002016"], + initialVal: "", + expectedVal: "2016-12-01" + }, + { + // Type 275769 in the year field will set it to the maximum year, which is + // 275760. + keys: ["0101275769"], + initialVal: "", + expectedVal: "275760-01-01" + }, + { + // Type 000000 in the year field will set it to the minimum year, which is + // 0001. + keys: ["0101000000"], + initialVal: "", + expectedVal: "0001-01-01" + }, + { + // Advance to year field and decrement. + keys: ["VK_TAB", "VK_TAB", "VK_DOWN"], + initialVal: "2016-11-25", + expectedVal: "2015-11-25" + }, + { + // Right key should do the same thing as TAB key. + keys: ["VK_RIGHT", "VK_RIGHT", "VK_DOWN"], + initialVal: "2016-11-25", + expectedVal: "2015-11-25" + }, + { + // Advance to day field then back to month field and decrement. + keys: ["VK_RIGHT", "VK_LEFT", "VK_DOWN"], + initialVal: "2000-05-01", + expectedVal: "2000-04-01" + }, + { + // Focus starts on the first field, month in this case, and increment. + keys: ["VK_UP"], + initialVal: "2000-03-01", + expectedVal: "2000-04-01" + }, + { + // Advance to day field and decrement. + keys: ["VK_TAB", "VK_DOWN"], + initialVal: "1234-01-01", + expectedVal: "1234-01-31" + }, + { + // Advance to day field and increment. + keys: ["VK_TAB", "VK_UP"], + initialVal: "1234-01-01", + expectedVal: "1234-01-02" + }, + { + // PageUp on month field increments month by 3. + keys: ["VK_PAGE_UP"], + initialVal: "1999-01-01", + expectedVal: "1999-04-01" + }, + { + // PageDown on month field decrements month by 3. + keys: ["VK_PAGE_DOWN"], + initialVal: "1999-01-01", + expectedVal: "1999-10-01" + }, + { + // PageUp on day field increments day by 7. + keys: ["VK_TAB", "VK_PAGE_UP"], + initialVal: "1999-01-01", + expectedVal: "1999-01-08" + }, + { + // PageDown on day field decrements day by 7. + keys: ["VK_TAB", "VK_PAGE_DOWN"], + initialVal: "1999-01-01", + expectedVal: "1999-01-25" + }, + { + // PageUp on year field increments year by 10. + keys: ["VK_TAB", "VK_TAB", "VK_PAGE_UP"], + initialVal: "1999-01-01", + expectedVal: "2009-01-01" + }, + { + // PageDown on year field decrements year by 10. + keys: ["VK_TAB", "VK_TAB", "VK_PAGE_DOWN"], + initialVal: "1999-01-01", + expectedVal: "1989-01-01" + }, + { + // Home key on month field sets it to the minimum month, which is 01. + keys: ["VK_HOME"], + initialVal: "2016-06-01", + expectedVal: "2016-01-01" + }, + { + // End key on month field sets it to the maximum month, which is 12. + keys: ["VK_END"], + initialVal: "2016-06-01", + expectedVal: "2016-12-01" + }, + { + // Home key on day field sets it to the minimum day, which is 01. + keys: ["VK_TAB", "VK_HOME"], + initialVal: "2016-01-10", + expectedVal: "2016-01-01" + }, + { + // End key on day field sets it to the maximum day, which is 31. + keys: ["VK_TAB", "VK_END"], + initialVal: "2016-01-10", + expectedVal: "2016-01-31" + }, + { + // Home key should have no effect on year field. + keys: ["VK_TAB", "VK_TAB", "VK_HOME"], + initialVal: "2016-01-01", + expectedVal: "2016-01-01" + }, + { + // End key should have no effect on year field. + keys: ["VK_TAB", "VK_TAB", "VK_END"], + initialVal: "2016-01-01", + expectedVal: "2016-01-01" + }, +]; + +function sendKeys(aKeys) { + for (let i = 0; i < aKeys.length; i++) { + let key = aKeys[i]; + if (key.startsWith("VK")) { + synthesizeKey(key, {}); + } else { + sendString(key); + } + } +} + +function test() { + var elem = document.getElementById("input"); + + for (let { keys, initialVal, expectedVal } of testData) { + elem.focus(); + elem.value = initialVal; + sendKeys(keys); + elem.blur(); + is(elem.value, expectedVal, + "Test with " + keys + ", result should be " + expectedVal); + elem.value = ""; + } +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_datetime_focus_blur.html b/dom/html/test/forms/test_input_datetime_focus_blur.html index 5b8d95b25..85f7b4bb4 100644 --- a/dom/html/test/forms/test_input_datetime_focus_blur.html +++ b/dom/html/test/forms/test_input_datetime_focus_blur.html @@ -4,7 +4,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1288591 --> <head> - <title>Test focus/blur behaviour for <input type='time'></title> + <title>Test focus/blur behaviour for date/time input types</title> <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> </head> @@ -12,7 +12,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1288591 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1288591">Mozilla Bug 1288591</a> <p id="display"></p> <div id="content"> - <input id="input" type="time"> + <input id="input_time" type="time"> + <input id="input_date" type="date"> </div> <pre id="test"> <script type="application/javascript"> @@ -30,15 +31,15 @@ SimpleTest.waitForFocus(function() { SimpleTest.finish(); }); -function test() { - let time = document.getElementById("input"); - time.focus(); +function testFocusBlur(type) { + let input = document.getElementById("input_" + type); + input.focus(); - // The active element returns the input type=time. + // The active element returns the date/time input element. let activeElement = document.activeElement; - is(activeElement, time, "activeElement should be the time element"); + is(activeElement, input, "activeElement should be the date/time input element"); is(activeElement.localName, "input", "activeElement should be an input element"); - is(activeElement.type, "time", "activeElement should be of type time"); + is(activeElement.type, type, "activeElement should be of type " + type); // Use FocusManager to check that the actual focus is on the anonymous // text control. @@ -48,10 +49,17 @@ function test() { is(focusedElement.localName, "input", "focusedElement should be an input element"); is(focusedElement.type, "text", "focusedElement should be of type text"); - time.blur(); - isnot(document.activeElement, time, "activeElement should no longer be the time element"); + input.blur(); + isnot(document.activeElement, input, "activeElement should no longer be the datetime input element"); } +function test() { + let inputTypes = ["time", "date"]; + + for (let i = 0; i < inputTypes.length; i++) { + testFocusBlur(inputTypes[i]); + } +} </script> </pre> </body> diff --git a/dom/html/test/forms/test_input_datetime_focus_blur_events.html b/dom/html/test/forms/test_input_datetime_focus_blur_events.html new file mode 100644 index 000000000..873dda627 --- /dev/null +++ b/dom/html/test/forms/test_input_datetime_focus_blur_events.html @@ -0,0 +1,90 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1301306 +--> +<head> +<title>Test for Bug 1301306</title> +<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> +<script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1301306">Mozilla Bug 722599</a> +<p id="display"></p> +<div id="content"> +<input type="time" id="input_time" onfocus="++focusEvents[0]" + onblur="++blurEvents[0]" onfocusin="++focusInEvents[0]" + onfocusout="++focusOutEvents[0]"> +<input type="date" id="input_date" onfocus="++focusEvents[1]" + onblur="++blurEvents[1]" onfocusin="++focusInEvents[1]" + onfocusout="++focusOutEvents[1]"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** + * Test for Bug 1301306. + * This test checks that when moving inside the time input element, e.g. jumping + * through the inner text boxes, does not fire extra focus/blur events. + **/ + +var inputTypes = ["time", "date"]; +var focusEvents = [0, 0]; +var focusInEvents = [0, 0]; +var focusOutEvents = [0, 0]; +var blurEvents = [0, 0]; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + test(); + SimpleTest.finish(); +}); + +function test() { + for (var i = 0; i < inputTypes.length; i++) { + var input = document.getElementById("input_" + inputTypes[i]); + + input.focus(); + is(focusEvents[i], 1, inputTypes[i] + " input element should have dispatched focus event."); + is(focusInEvents[i], 1, inputTypes[i] + " input element should have dispatched focusin event."); + is(focusOutEvents[i], 0, inputTypes[i] + " input element should not have dispatched focusout event."); + is(blurEvents[i], 0, inputTypes[i] + " input element should not have dispatched blur event."); + + // Move around inside the input element's input box. + synthesizeKey("VK_TAB", {}); + is(focusEvents[i], 1, inputTypes[i] + " input element should not have dispatched focus event."); + is(focusInEvents[i], 1, inputTypes[i] + " input element should not have dispatched focusin event."); + is(focusOutEvents[i], 0, inputTypes[i] + " input element should not have dispatched focusout event."); + is(blurEvents[i], 0, inputTypes[i] + " time input element should not have dispatched blur event."); + + synthesizeKey("VK_RIGHT", {}); + is(focusEvents[i], 1, inputTypes[i] + " input element should not have dispatched focus event."); + is(focusInEvents[i], 1, inputTypes[i] + " input element should not have dispatched focusin event."); + is(focusOutEvents[i], 0, inputTypes[i] + " input element should not have dispatched focusout event."); + is(blurEvents[i], 0, inputTypes[i] + " input element should not have dispatched blur event."); + + synthesizeKey("VK_LEFT", {}); + is(focusEvents[i], 1,inputTypes[i] + " input element should not have dispatched focus event."); + is(focusInEvents[i], 1, inputTypes[i] + " input element should not have dispatched focusin event."); + is(focusOutEvents[i], 0, inputTypes[i] + " input element should not have dispatched focusout event."); + is(blurEvents[i], 0, inputTypes[i] + " input element should not have dispatched blur event."); + + synthesizeKey("VK_RIGHT", {}); + is(focusEvents[i], 1, inputTypes[i] + " input element should not have dispatched focus event."); + is(focusInEvents[i], 1, inputTypes[i] + " input element should not have dispatched focusin event."); + is(focusOutEvents[i], 0, inputTypes[i] + " input element should not have dispatched focusout event."); + is(blurEvents[i], 0, inputTypes[i] + " input element should not have dispatched blur event."); + + input.blur(); + is(focusEvents[i], 1, inputTypes[i] + " input element should not have dispatched focus event."); + is(focusInEvents[i], 1, inputTypes[i] + " input element should not have dispatched focusin event."); + is(focusOutEvents[i], 1, inputTypes[i] + " input element should have dispatched focusout event."); + is(blurEvents[i], 1, inputTypes[i] + " input element should have dispatched blur event."); + } +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_datetime_input_change_events.html b/dom/html/test/forms/test_input_datetime_input_change_events.html new file mode 100644 index 000000000..e636995d3 --- /dev/null +++ b/dom/html/test/forms/test_input_datetime_input_change_events.html @@ -0,0 +1,88 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1370858 +--> +<head> +<title>Test for Bug 1370858</title> +<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> +<script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1370858">Mozilla Bug 722599</a> +<p id="display"></p> +<div id="content"> +<input type="time" id="input_time" onchange="++changeEvents[0]" + oninput="++inputEvents[0]"> +<input type="date" id="input_date" onchange="++changeEvents[1]" + oninput="++inputEvents[1]"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** + * Test for Bug 1370858. + * Test that change and input events are (not) fired for date/time inputs. + **/ + +var inputTypes = ["time", "date"]; +var changeEvents = [0, 0]; +var inputEvents = [0, 0]; +var values = ["10:30", "2017-06-08"]; +var expectedValues = [["09:30", "01:30"], ["2017-05-08", "2017-01-08"]]; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + test(); + SimpleTest.finish(); +}); + +function test() { + for (var i = 0; i < inputTypes.length; i++) { + var input = document.getElementById("input_" + inputTypes[i]); + var inputRect = input.getBoundingClientRect(); + + // Points over the input's reset button + var resetButton_X = inputRect.width - 15; + var resetButton_Y = inputRect.height / 2; + + is(changeEvents[i], 0, "Number of change events should be 0 at start."); + is(inputEvents[i], 0, "Number of input events should be 0 at start."); + + // Test that change and input events are not dispatched setting .value by + // script. + input.value = values[i]; + is(input.value, values[i], "Check that value was set correctly (0)."); + is(changeEvents[i], 0, "Change event should not have dispatched (0)."); + is(inputEvents[i], 0, "Input event should not have dispatched (0)."); + + // Test that change and input events are fired when changing the value using + // up/down keys. + input.focus(); + synthesizeKey("VK_DOWN", {}); + is(input.value, expectedValues[i][0], "Check that value was set correctly (1)."); + is(changeEvents[i], 1, "Change event should be dispatched (1)."); + is(inputEvents[i], 1, "Input event should ne dispatched (1)."); + + // Test that change and input events are fired when changing the value with + // the keyboard. + synthesizeKey("0", {}); + synthesizeKey("1", {}); + is(input.value, expectedValues[i][1], "Check that value was set correctly (2)."); + is(changeEvents[i], 2, "Change event should be dispatched (2)."); + is(inputEvents[i], 2, "Input event should be dispatched (2)."); + + // Test that change and input events are fired when clearing the value using + // the reset button. + synthesizeMouse(input, resetButton_X, resetButton_Y, {}); + is(input.value, "", "Check that value was set correctly (3)."); + is(changeEvents[i], 3, "Change event should be dispatched (3)."); + is(inputEvents[i], 3, "Input event should be dispatched (3)."); + } +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_datetime_tabindex.html b/dom/html/test/forms/test_input_datetime_tabindex.html index fb7c9b2f1..8023ccf9b 100644 --- a/dom/html/test/forms/test_input_datetime_tabindex.html +++ b/dom/html/test/forms/test_input_datetime_tabindex.html @@ -4,7 +4,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1288591 --> <head> - <title>Test tabindex attribute for <input type='time'></title> + <title>Test tabindex attribute for date/time input types</title> <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> @@ -16,13 +16,16 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1288591 <input id="time1" type="time" tabindex="0"> <input id="time2" type="time" tabindex="-1"> <input id="time3" type="time" tabindex="0"> + <input id="date1" type="date" tabindex="0"> + <input id="date2" type="date" tabindex="-1"> + <input id="date3" type="date" tabindex="0"> </div> <pre id="test"> <script type="application/javascript"> /** * Test for Bug 1288591. - * This test checks whether date/time input types' tabindex attribute works + * This test checks whether date/time input types tabindex attribute works * correctly. **/ SimpleTest.waitForExplicitFinish(); @@ -31,41 +34,49 @@ SimpleTest.waitForFocus(function() { SimpleTest.finish(); }); -function test() { - let time1 = document.getElementById("time1"); - let time2 = document.getElementById("time2"); - let time3 = document.getElementById("time3"); +function testTabindex(type) { + let input1 = document.getElementById(type + "1"); + let input2 = document.getElementById(type + "2"); + let input3 = document.getElementById(type + "3"); - time1.focus(); - is(document.activeElement, time1, + input1.focus(); + is(document.activeElement, input1, "input element with tabindex=0 is focusable"); - // Advance to time1 minute field + // Advance to next inner field synthesizeKey("VK_TAB", {}); - is(document.activeElement, time1, + is(document.activeElement, input1, "input element with tabindex=0 is tabbable"); - // Advance to time1 AM/PM field + // Advance to next inner field synthesizeKey("VK_TAB", {}); - is(document.activeElement, time1, + is(document.activeElement, input1, "input element with tabindex=0 is tabbable"); // Advance to next element synthesizeKey("VK_TAB", {}); - is(document.activeElement, time3, + is(document.activeElement, input3, "input element with tabindex=-1 is not tabbable"); - time2.focus(); - is(document.activeElement, time2, + input2.focus(); + is(document.activeElement, input2, "input element with tabindex=-1 is still focusable"); // Changing the tabindex attribute dynamically. - time3.setAttribute("tabindex", "-1"); - synthesizeKey("VK_TAB", {}); // need only one TAB since time2 is not tabbable - isnot(document.activeElement, time3, + input3.setAttribute("tabindex", "-1"); + synthesizeKey("VK_TAB", {}); // need only one TAB since input2 is not tabbable + isnot(document.activeElement, input3, "element with tabindex changed to -1 should not be tabbable"); } +function test() { + let inputTypes = ["time", "date"]; + + for (let i = 0; i < inputTypes.length; i++) { + testTabindex(inputTypes[i]); + } +} + </script> </pre> </body> diff --git a/dom/html/test/forms/test_input_time_focus_blur_events.html b/dom/html/test/forms/test_input_time_focus_blur_events.html deleted file mode 100644 index 483741477..000000000 --- a/dom/html/test/forms/test_input_time_focus_blur_events.html +++ /dev/null @@ -1,82 +0,0 @@ -<!DOCTYPE HTML> -<html> -<!-- -https://bugzilla.mozilla.org/show_bug.cgi?id=1301306 ---> -<head> -<title>Test for Bug 1301306</title> -<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> -<script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> -<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> -</head> -<body> -<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1301306">Mozilla Bug 722599</a> -<p id="display"></p> -<div id="content"> -<input type="time" id="input_time" onfocus="++focusEvent" onblur="++blurEvent" - onfocusin="++focusInEvent" onfocusout="++focusOutEvent"> -</div> -<pre id="test"> -<script class="testbody" type="text/javascript"> - -/** - * Test for Bug 1301306. - * This test checks that when moving inside the time input element, e.g. jumping - * through the inner text boxes, does not fire extra focus/blur events. - **/ - -var focusEvent = 0; -var focusInEvent = 0; -var focusOutEvent = 0; -var blurEvent = 0; - -SimpleTest.waitForExplicitFinish(); -SimpleTest.waitForFocus(function() { - test(); - SimpleTest.finish(); -}); - -function test() { - var time = document.getElementById("input_time"); - time.focus(); - is(focusEvent, 1, "time input element should have dispatched focus event."); - is(focusInEvent, 1, "time input element should have dispatched focusin event."); - is(focusOutEvent, 0, "time input element should not have dispatched focusout event."); - is(blurEvent, 0, "time input element should not have dispatched blur event."); - - // Move around inside the input element's input box. - synthesizeKey("VK_TAB", {}); - is(focusEvent, 1, "time input element should not have dispatched focus event."); - is(focusInEvent, 1, "time input element should have dispatched focusin event."); - is(focusOutEvent, 0, "time input element should not have dispatched focusout event."); - is(blurEvent, 0, "time input element should not have dispatched blur event."); - - synthesizeKey("VK_RIGHT", {}); - is(focusEvent, 1, "time input element should not have dispatched focus event."); - is(focusInEvent, 1, "time input element should have dispatched focusin event."); - is(focusOutEvent, 0, "time input element should not have dispatched focusout event."); - is(blurEvent, 0, "time input element should not have dispatched blur event."); - - synthesizeKey("VK_LEFT", {}); - is(focusEvent, 1, "time input element should not have dispatched focus event."); - is(focusInEvent, 1, "time input element should have dispatched focusin event."); - is(focusOutEvent, 0, "time input element should not have dispatched focusout event."); - is(blurEvent, 0, "time input element should not have dispatched blur event."); - - synthesizeKey("VK_RIGHT", {}); - is(focusEvent, 1, "time input element should not have dispatched focus event."); - is(focusInEvent, 1, "time input element should have dispatched focusin event."); - is(focusOutEvent, 0, "time input element should not have dispatched focusout event."); - is(blurEvent, 0, "time input element should not have dispatched blur event."); - - time.blur(); - is(focusEvent, 1, "time input element should not have dispatched focus event."); - is(focusInEvent, 1, "time input element should have dispatched focusin event."); - is(focusOutEvent, 1, "time input element should not have dispatched focusout event."); - is(blurEvent, 1, "time input element should have dispatched blur event."); -} - -</script> -</pre> -</body> -</html> diff --git a/dom/html/test/forms/test_input_types_pref.html b/dom/html/test/forms/test_input_types_pref.html index 243836f34..5279d6a2a 100644 --- a/dom/html/test/forms/test_input_types_pref.html +++ b/dom/html/test/forms/test_input_types_pref.html @@ -37,47 +37,63 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=764481 inputType: "color", expectedType: "color" }, { - prefs: [["dom.experimental_forms", false], ["dom.forms.datepicker", false], - ["dom.forms.datetime", false]], + prefs: [["dom.experimental_forms", false], ["dom.forms.datetime", false]], inputType: "date", expectedType: "text" }, { - prefs: [["dom.experimental_forms", true], ["dom.forms.datepicker", false], - ["dom.forms.datetime", false]], + prefs: [["dom.experimental_forms", true], ["dom.forms.datetime", false]], inputType: "date", expectedType: "date" }, { - prefs: [["dom.experimental_forms", false], ["dom.forms.datepicker", true], - ["dom.forms.datetime", false]], + prefs: [["dom.experimental_forms", false], ["dom.forms.datetime", true]], inputType: "date", expectedType: "date" }, { - prefs: [["dom.experimental_forms", false], ["dom.forms.datepicker", false], - ["dom.forms.datetime", true]], - inputType: "date", - expectedType: "date" + prefs: [["dom.experimental_forms", false], ["dom.forms.datetime", false]], + inputType: "time", + expectedType: "text" + }, { + prefs: [["dom.experimental_forms", true], ["dom.forms.datetime", false]], + inputType: "time", + expectedType: "time" + }, { + prefs: [["dom.experimental_forms", false], ["dom.forms.datetime", true]], + inputType: "time", + expectedType: "time" + }, { + prefs: [["dom.forms.datetime", false], ["dom.forms.datetime.others", false]], + inputType: "month", + expectedType: "text" }, { - prefs: [["dom.forms.datetime", false]], + prefs: [["dom.forms.datetime", true], ["dom.forms.datetime.others", false]], inputType: "month", expectedType: "text" }, { - prefs: [["dom.forms.datetime", true]], + prefs: [["dom.forms.datetime", false], ["dom.forms.datetime.others", true]], inputType: "month", expectedType: "month" }, { - prefs: [["dom.forms.datetime", false]], + prefs: [["dom.forms.datetime", false], ["dom.forms.datetime.others", false]], + inputType: "week", + expectedType: "text" + }, { + prefs: [["dom.forms.datetime", true], ["dom.forms.datetime.others", false]], inputType: "week", expectedType: "text" }, { - prefs: [["dom.forms.datetime", true]], + prefs: [["dom.forms.datetime", false], ["dom.forms.datetime.others", true]], inputType: "week", expectedType: "week" }, { - prefs: [["dom.forms.datetime", false]], + prefs: [["dom.forms.datetime", false], ["dom.forms.datetime.others", false]], + inputType: "datetime-local", + expectedType: "text" + }, { + prefs: [["dom.forms.datetime", true], ["dom.forms.datetime.others", false]], inputType: "datetime-local", expectedType: "text" }, { - prefs: [["dom.forms.datetime", true]], + prefs: [["dom.forms.datetime", false], ["dom.forms.datetime.others", true]], inputType: "datetime-local", expectedType: "datetime-local" } diff --git a/dom/html/test/forms/test_input_typing_sanitization.html b/dom/html/test/forms/test_input_typing_sanitization.html index 0896f19df..eee300b33 100644 --- a/dom/html/test/forms/test_input_typing_sanitization.html +++ b/dom/html/test/forms/test_input_typing_sanitization.html @@ -26,6 +26,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=765772 * This test checks that when a user types in some input types, it will not be * in a state where the value will be un-sanitized and usable (by a script). */ +const isDesktop = !/Mobile|Tablet/.test(navigator.userAgent); var input = document.getElementById('i'); var form = document.getElementById('f'); @@ -143,6 +144,7 @@ function runTest() ] }, { + mobileOnly: true, type: 'date', validData: [ '0001-01-01', @@ -161,6 +163,28 @@ function runTest() ] }, { + mobileOnly: true, + type: 'time', + validData: [ + '00:00', + '09:09:00', + '08:23:23.1', + '21:43:56.12', + '23:12:45.100', + ], + invalidData: [ + '00:', + '00:00:', + '25:00', + '-00:00', + '00:00:00.', + '00:60', + '10:58:99', + ':19:10', + '23:08:09.1012', + ] + }, + { type: 'month', validData: [ '0001-01', @@ -218,6 +242,10 @@ function runTest() for (test of data) { gCurrentTest = test; + if (gCurrentTest.mobileOnly && isDesktop) { + continue; + } + input.type = test.type; gValidData = test.validData; gInvalidData = test.invalidData; diff --git a/dom/html/test/forms/test_max_attribute.html b/dom/html/test/forms/test_max_attribute.html index 4007cfad6..091ad321b 100644 --- a/dom/html/test/forms/test_max_attribute.html +++ b/dom/html/test/forms/test_max_attribute.html @@ -31,8 +31,7 @@ var data = [ { type: 'month', apply: true }, { type: 'week', apply: true }, { type: 'time', apply: true }, - // TODO: temporary set to false until bug 888331 is fixed. - { type: 'datetime-local', apply: false }, + { type: 'datetime-local', apply: true }, { type: 'number', apply: true }, { type: 'range', apply: true }, { type: 'color', apply: false }, @@ -71,7 +70,8 @@ function checkValidity(aElement, aValidity, aApply, aRangeApply) "element overflow status should be " + !aValidity); var overflowMsg = (aElement.type == "date" || aElement.type == "time" || - aElement.type == "month" || aElement.type == "week") ? + aElement.type == "month" || aElement.type == "week" || + aElement.type == "datetime-local") ? ("Please select a value that is no later than " + aElement.max + ".") : ("Please select a value that is no more than " + aElement.max + "."); is(aElement.validationMessage, @@ -148,7 +148,7 @@ for (var test of data) { input.max = '2016-W39'; break; case 'datetime-local': - // TODO: this is temporary until bug 888331 is fixed. + input.max = '2016-12-31T23:59:59'; break; default: ok(false, 'please, add a case for this new type (' + input.type + ')'); @@ -421,7 +421,44 @@ for (var test of data) { break; case 'datetime-local': - // TODO: this is temporary until bug 888331 is fixed. + input.value = '2016-01-01T12:00'; + checkValidity(input, true, apply, apply); + + input.value = '2016-12-31T23:59:59'; + checkValidity(input, true, apply, apply); + + input.value = 'foo'; + checkValidity(input, true, apply, apply); + + input.value = '2016-12-31T23:59:59.123'; + checkValidity(input, false, apply, apply); + + input.value = '2017-01-01T10:00'; + checkValidity(input, false, apply, apply); + + input.max = '2017-01-01T10:00'; + checkValidity(input, true, apply, apply); + + input.value = '2017-01-01T10:00:30'; + checkValidity(input, false, apply, apply); + + input.value = '1000-01-01T12:00'; + checkValidity(input, true, apply, apply); + + input.value = '2100-01-01T12:00'; + checkValidity(input, false, apply, apply); + + input.max = '0050-12-31T23:59:59.999'; + checkValidity(input, false, apply, apply); + + input.value = '0050-12-31T23:59:59'; + checkValidity(input, true, apply, apply); + + input.max = ''; + checkValidity(input, true, apply, false); + + input.max = 'foo'; + checkValidity(input, true, apply, false); break; } diff --git a/dom/html/test/forms/test_min_attribute.html b/dom/html/test/forms/test_min_attribute.html index 1258babec..22f21de39 100644 --- a/dom/html/test/forms/test_min_attribute.html +++ b/dom/html/test/forms/test_min_attribute.html @@ -31,8 +31,7 @@ var data = [ { type: 'month', apply: true }, { type: 'week', apply: true }, { type: 'time', apply: true }, - // TODO: temporary set to false until bug 888331 is fixed. - { type: 'datetime-local', apply: false }, + { type: 'datetime-local', apply: true }, { type: 'number', apply: true }, { type: 'range', apply: true }, { type: 'color', apply: false }, @@ -71,7 +70,8 @@ function checkValidity(aElement, aValidity, aApply, aRangeApply) "element underflow status should be " + !aValidity); var underflowMsg = (aElement.type == "date" || aElement.type == "time" || - aElement.type == "month" || aElement.type == "week") ? + aElement.type == "month" || aElement.type == "week" || + aElement.type == "datetime-local") ? ("Please select a value that is no earlier than " + aElement.min + ".") : ("Please select a value that is no less than " + aElement.min + "."); is(aElement.validationMessage, @@ -146,10 +146,10 @@ for (var test of data) { input.min = '2016-06'; break; case 'week': - input.min = "2016-W39"; + input.min = '2016-W39'; break; case 'datetime-local': - // TODO: this is temporary until bug 888331 is fixed. + input.min = '2017-01-01T00:00'; break; default: ok(false, 'please, add a case for this new type (' + input.type + ')'); @@ -420,7 +420,44 @@ for (var test of data) { checkValidity(input, true, apply, false); break; case 'datetime-local': - // TODO: this is temporary until bug 888331 is fixed. + input.value = '2017-12-31T23:59'; + checkValidity(input, true, apply, apply); + + input.value = '2017-01-01T00:00'; + checkValidity(input, true, apply, apply); + + input.value = '2017-01-01T00:00:00.123'; + checkValidity(input, true, apply, apply); + + input.value = 'foo'; + checkValidity(input, true, apply, apply); + + input.value = '2016-12-31T23:59'; + checkValidity(input, false, apply, apply); + + input.min = '2016-01-01T00:00'; + checkValidity(input, true, apply, apply); + + input.value = '2015-12-31T23:59'; + checkValidity(input, false, apply, apply); + + input.value = '1000-01-01T00:00'; + checkValidity(input, false, apply, apply); + + input.value = '10000-01-01T00:00'; + checkValidity(input, true, apply, apply); + + input.min = '0010-01-01T12:00'; + checkValidity(input, true, apply, apply); + + input.value = '0010-01-01T10:00'; + checkValidity(input, false, apply, apply); + + input.min = ''; + checkValidity(input, true, apply, false); + + input.min = 'foo'; + checkValidity(input, true, apply, false); break; default: ok(false, 'write tests for ' + input.type); diff --git a/dom/html/test/forms/test_step_attribute.html b/dom/html/test/forms/test_step_attribute.html index 31277860c..a14afa461 100644 --- a/dom/html/test/forms/test_step_attribute.html +++ b/dom/html/test/forms/test_step_attribute.html @@ -31,8 +31,7 @@ var data = [ { type: 'month', apply: true }, { type: 'week', apply: true }, { type: 'time', apply: true }, - // TODO: temporary set to false until bug 888331 is fixed. - { type: 'datetime-local', apply: false }, + { type: 'datetime-local', apply: true }, { type: 'number', apply: true }, { type: 'range', apply: true }, { type: 'color', apply: false }, @@ -950,7 +949,104 @@ for (var test of data) { break; case 'datetime-local': - // TODO: this is temporary until bug 888331 is fixed. + // When step is invalid, every datetime is valid + input.step = 0; + input.value = '2017-02-06T12:00'; + checkValidity(input, true, apply); + + input.step = 'foo'; + input.value = '1970-01-01T00:00'; + checkValidity(input, true, apply); + + input.step = '-1'; + input.value = '1969-12-12 00:10'; + checkValidity(input, true, apply); + + input.removeAttribute('step'); + input.value = '1500-01-01T12:00'; + checkValidity(input, true, apply); + + input.step = 'any'; + input.value = '1966-12-12T12:00'; + checkValidity(input, true, apply); + + input.step = 'ANY'; + input.value = '2017-01-01 12:00'; + checkValidity(input, true, apply); + + // When min is set to a valid datetime, there is a step base. + input.min = '2017-01-01T00:00:00'; + input.step = '2'; + input.value = '2017-01-01T00:00:02'; + checkValidity(input, true, apply); + + input.value = '2017-01-01T00:00:03'; + checkValidity(input, false, apply, + { low: "2017-01-01T00:00:02", high: "2017-01-01T00:00:04" }); + + input.min = '2017-01-01T00:00:05'; + input.value = '2017-01-01T00:00:08'; + checkValidity(input, false, apply, + { low: "2017-01-01T00:00:07", high: "2017-01-01T00:00:09" }); + + input.min = '2000-01-01T00:00'; + input.step = '120'; + input.value = '2000-01-01T00:02'; + checkValidity(input, true, apply); + + // Without any step attribute the datetime is valid + input.removeAttribute('step'); + checkValidity(input, true, apply); + + input.min = '1950-01-01T00:00'; + input.step = '129600'; // 1.5 day + input.value = '1950-01-02T00:00'; + checkValidity(input, false, apply, + { low: "1950-01-01T00:00", high: "1950-01-02T12:00" }); + + input.step = '259200'; // 3 days + input.value = '1950-01-04T12:00'; + checkValidity(input, false, apply, + { low: "1950-01-04T00:00", high: "1950-01-07T00:00" }); + + input.value = '1950-01-10T00:00'; + checkValidity(input, true, apply); + + input.step = '0.5'; // half a second + input.value = '1950-01-01T00:00:00.123'; + checkValidity(input, false, apply, + { low: "1950-01-01T00:00", high: "1950-01-01T00:00:00.500" }); + + input.value = '2000-01-01T12:30:30.600'; + checkValidity(input, false, apply, + { low: "2000-01-01T12:30:30.500", high: "2000-01-01T12:30:31" }); + + input.value = '1950-01-05T00:00:00.500'; + checkValidity(input, true, apply); + + input.step = '2.1'; + input.min = '1991-01-01T12:00'; + input.value = '1991-01-01T12:00'; + checkValidity(input, true, apply); + + input.value = '1991-01-01T12:00:03'; + checkValidity(input, false, apply, + { low: "1991-01-01T12:00:02.100", high: "1991-01-01T12:00:04.200" }); + + input.value = '1991-01-01T12:00:06.3'; + checkValidity(input, true, apply); + + input.step = '2.1'; + input.min = '1969-12-20T10:00:05'; + input.value = '1969-12-20T10:00:05'; + checkValidity(input, true, apply); + + input.value = '1969-12-20T10:00:08'; + checkValidity(input, false, apply, + { low: "1969-12-20T10:00:07.100", high: "1969-12-20T10:00:09.200" }); + + input.value = '1969-12-20T10:00:09.200'; + checkValidity(input, true, apply); break; default: diff --git a/dom/html/test/forms/test_stepup_stepdown.html b/dom/html/test/forms/test_stepup_stepdown.html index d96895180..21cde58aa 100644 --- a/dom/html/test/forms/test_stepup_stepdown.html +++ b/dom/html/test/forms/test_stepup_stepdown.html @@ -52,13 +52,8 @@ function checkAvailability() ["time", true], ["month", true], ["week", true], - ["color", false], - ]; - - var todoList = - [ - ["datetime", true], ["datetime-local", true], + ["color", false], ]; var element = document.createElement("input"); @@ -82,27 +77,6 @@ function checkAvailability() } is(exceptionCaught, !data[1], "stepUp() availability is not correct"); } - - for (data of todoList) { - var exceptionCaught = false; - element.type = data[0]; - try { - element.stepDown(); - } catch (e) { - exceptionCaught = true; - } - todo_is(exceptionCaught, !data[1], - "stepDown() availability is not correct"); - - exceptionCaught = false; - try { - element.stepUp(); - } catch (e) { - exceptionCaught = true; - } - todo_is(exceptionCaught, !data[1], - "stepUp() availability is not correct"); - } } function checkStepDown() @@ -509,6 +483,80 @@ function checkStepDown() [ '2016-W01', 'AnY', null, null, 1, null, true ], [ '2016-W01', 'aNy', null, null, 1, null, true ], ]}, + { type: 'datetime-local', data: [ + // Regular case. + [ '2017-02-07T09:30', null, null, null, null, '2017-02-07T09:29', false ], + // Argument testing. + [ '2017-02-07T09:30', null, null, null, 1, '2017-02-07T09:29', false ], + [ '2017-02-07T09:30', null, null, null, 5, '2017-02-07T09:25', false ], + [ '2017-02-07T09:30', null, null, null, -1, '2017-02-07T09:31', false ], + [ '2017-02-07T09:30', null, null, null, 0, '2017-02-07T09:30', false ], + // hour/minutes/seconds wrapping. + [ '2000-01-01T05:00', null, null, null, null, '2000-01-01T04:59', false ], + [ '2000-01-01T05:00:00', 1, null, null, null, '2000-01-01T04:59:59', false ], + [ '2000-01-01T05:00:00', 0.1, null, null, null, '2000-01-01T04:59:59.900', false ], + [ '2000-01-01T05:00:00', 0.01, null, null, null, '2000-01-01T04:59:59.990', false ], + [ '2000-01-01T05:00:00', 0.001, null, null, null, '2000-01-01T04:59:59.999', false ], + // month/year wrapping. + [ '2012-08-01T12:00', null, null, null, 1440, '2012-07-31T12:00', false ], + [ '1969-01-02T12:00', null, null, null, 5760, '1968-12-29T12:00', false ], + [ '1969-12-31T00:00', null, null, null, -1440, '1970-01-01T00:00', false ], + [ '2012-02-29T00:00', null, null, null, -1440, '2012-03-01T00:00', false ], + // stepDown() on '00:00' gives '23:59'. + [ '2017-02-07T00:00', null, null, null, 1, '2017-02-06T23:59', false ], + [ '2017-02-07T00:00', null, null, null, 3, '2017-02-06T23:57', false ], + // Some random step values.. + [ '2017-02-07T16:07', '0.5', null, null, null, '2017-02-07T16:06:59.500', false ], + [ '2017-02-07T16:07', '2', null, null, null, '2017-02-07T16:06:58', false ], + [ '2017-02-07T16:07', '0.25', null, null, 4, '2017-02-07T16:06:59', false ], + [ '2017-02-07T16:07', '1.1', '2017-02-07T16:00', null, 1, '2017-02-07T16:06:59.100', false ], + [ '2017-02-07T16:07', '1.1', '2017-02-07T16:00', null, 2, '2017-02-07T16:06:58', false ], + [ '2017-02-07T16:07', '1.1', '2017-02-07T16:00', null, 10, '2017-02-07T16:06:49.200', false ], + [ '2017-02-07T16:07', '129600', '2017-02-01T00:00', null, 2, '2017-02-05T12:00', false ], + // step = 0 isn't allowed (-> step = 1). + [ '2017-02-07T10:15', '0', null, null, null, '2017-02-07T10:14', false ], + // step < 0 isn't allowed (-> step = 1). + [ '2017-02-07T10:15', '-1', null, null, null, '2017-02-07T10:14', false ], + // step = NaN isn't allowed (-> step = 1). + [ '2017-02-07T10:15', 'foo', null, null, null, '2017-02-07T10:14', false ], + // Min values testing. + [ '2012-02-02T17:02', '60', 'foo', null, 2, '2012-02-02T17:00', false ], + [ '2012-02-02T17:10', '60', '2012-02-02T17:09', null, null, '2012-02-02T17:09', false ], + [ '2012-02-02T17:10', '60', '2012-02-02T17:10', null, null, '2012-02-02T17:10', false ], + [ '2012-02-02T17:10', '60', '2012-02-02T17:30', null, 1, '2012-02-02T17:10', false ], + [ '2012-02-02T17:10', '180', '2012-02-02T17:05', null, null, '2012-02-02T17:08', false ], + [ '2012-02-03T20:05', '86400', '2012-02-02T17:05', null, null, '2012-02-03T17:05', false ], + [ '2012-02-03T18:00', '129600', '2012-02-01T00:00', null, null, '2012-02-02T12:00', false ], + // Max values testing. + [ '2012-02-02T17:15', '60', null, 'foo', null, '2012-02-02T17:14', false ], + [ '2012-02-02T17:15', null, null, '2012-02-02T17:20', null, '2012-02-02T17:14', false ], + [ '2012-02-02T17:15', null, null, '2012-02-02T17:15', null, '2012-02-02T17:14', false ], + [ '2012-02-02T17:15', null, null, '2012-02-02T17:13', 4, '2012-02-02T17:11', false ], + [ '2012-02-02T17:15', '120', null, '2012-02-02T17:13', 3, '2012-02-02T17:09', false ], + [ '2012-02-03T20:05', '86400', null, '2012-02-03T20:05', null, '2012-02-02T20:05', false ], + [ '2012-02-03T18:00', '129600', null, '2012-02-03T20:00', null, '2012-02-02T06:00', false ], + // Step mismatch. + [ '2017-02-07T17:19', '120', '2017-02-07T17:10', null, null, '2017-02-07T17:18', false ], + [ '2017-02-07T17:19', '120', '2017-02-07T17:10', null, 2, '2017-02-07T17:16', false ], + [ '2017-02-07T17:19', '120', '2017-02-07T17:18', '2017-02-07T17:25', null, '2017-02-07T17:18', false ], + [ '2017-02-07T17:19', '120', null, null, null, '2017-02-07T17:17', false ], + [ '2017-02-07T17:19', '180', null, null, null, '2017-02-07T17:16', false ], + [ '2017-02-07T17:19', '172800', '2017-02-02T17:19', '2017-02-10T17:19', null, '2017-02-06T17:19', false ], + // Clamping. + [ '2017-02-07T17:22', null, null, '2017-02-07T17:11', null, '2017-02-07T17:11', false ], + [ '2017-02-07T17:22', '120', '2017-02-07T17:20', '2017-02-07T17:22', null, '2017-02-07T17:20', false ], + [ '2017-02-07T17:22', '300', '2017-02-07T17:12', '2017-02-07T17:20', 10, '2017-02-07T17:12', false ], + [ '2017-02-07T17:22', '300', '2017-02-07T17:18', '2017-02-07T17:20', 2, '2017-02-07T17:18', false ], + [ '2017-02-07T17:22', '600', '2017-02-02T17:00', '2017-02-07T17:00', 15, '2017-02-07T15:00', false ], + [ '2017-02-07T17:22', '600', '2017-02-02T17:00', '2017-02-07T17:00', 2, '2017-02-07T17:00', false ], + // value = "" (NaN). + [ '', null, null, null, null, '1969-12-31T23:59', false ], + // With step = 'any'. + [ '2017-02-07T15:20', 'any', null, null, 1, null, true ], + [ '2017-02-07T15:20', 'ANY', null, null, 1, null, true ], + [ '2017-02-07T15:20', 'AnY', null, null, 1, null, true ], + [ '2017-02-07T15:20', 'aNy', null, null, 1, null, true ], + ]}, ]; for (var test of testData) { @@ -958,6 +1006,78 @@ function checkStepUp() [ '2016-W01', 'AnY', null, null, 1, null, true ], [ '2016-W01', 'aNy', null, null, 1, null, true ], ]}, + { type: 'datetime-local', data: [ + // Regular case. + [ '2017-02-07T17:09', null, null, null, null, '2017-02-07T17:10', false ], + // Argument testing. + [ '2017-02-07T17:10', null, null, null, 1, '2017-02-07T17:11', false ], + [ '2017-02-07T17:10', null, null, null, 5, '2017-02-07T17:15', false ], + [ '2017-02-07T17:10', null, null, null, -1, '2017-02-07T17:09', false ], + [ '2017-02-07T17:10', null, null, null, 0, '2017-02-07T17:10', false ], + // hour/minutes/seconds wrapping. + [ '2000-01-01T04:59', null, null, null, null, '2000-01-01T05:00', false ], + [ '2000-01-01T04:59:59', 1, null, null, null, '2000-01-01T05:00', false ], + [ '2000-01-01T04:59:59.900', 0.1, null, null, null, '2000-01-01T05:00', false ], + [ '2000-01-01T04:59:59.990', 0.01, null, null, null, '2000-01-01T05:00', false ], + [ '2000-01-01T04:59:59.999', 0.001, null, null, null, '2000-01-01T05:00', false ], + // month/year wrapping. + [ '2012-07-31T12:00', null, null, null, 1440, '2012-08-01T12:00', false ], + [ '1968-12-29T12:00', null, null, null, 5760, '1969-01-02T12:00', false ], + [ '1970-01-01T00:00', null, null, null, -1440, '1969-12-31T00:00', false ], + [ '2012-03-01T00:00', null, null, null, -1440, '2012-02-29T00:00', false ], + // stepUp() on '23:59' gives '00:00'. + [ '2017-02-07T23:59', null, null, null, 1, '2017-02-08T00:00', false ], + [ '2017-02-07T23:59', null, null, null, 3, '2017-02-08T00:02', false ], + // Some random step values.. + [ '2017-02-07T17:40', '0.5', null, null, null, '2017-02-07T17:40:00.500', false ], + [ '2017-02-07T17:40', '2', null, null, null, '2017-02-07T17:40:02', false ], + [ '2017-02-07T17:40', '0.25', null, null, 4, '2017-02-07T17:40:01', false ], + [ '2017-02-07T17:40', '1.1', '2017-02-07T17:00', null, 1, '2017-02-07T17:40:00.200', false ], + [ '2017-02-07T17:40', '1.1', '2017-02-07T17:00', null, 2, '2017-02-07T17:40:01.300', false ], + [ '2017-02-07T17:40', '1.1', '2017-02-07T17:00', null, 10, '2017-02-07T17:40:10.100', false ], + [ '2017-02-07T17:40', '129600', '2017-02-01T00:00', null, 2, '2017-02-10T00:00', false ], + // step = 0 isn't allowed (-> step = 1). + [ '2017-02-07T17:39', '0', null, null, null, '2017-02-07T17:40', false ], + // step < 0 isn't allowed (-> step = 1). + [ '2017-02-07T17:39', '-1', null, null, null, '2017-02-07T17:40', false ], + // step = NaN isn't allowed (-> step = 1). + [ '2017-02-07T17:39', 'foo', null, null, null, '2017-02-07T17:40', false ], + // Min values testing. + [ '2012-02-02T17:00', '60', 'foo', null, 2, '2012-02-02T17:02', false ], + [ '2012-02-02T17:10', '60', '2012-02-02T17:10', null, null, '2012-02-02T17:11', false ], + [ '2012-02-02T17:10', '60', '2012-02-02T17:30', null, 1, '2012-02-02T17:30', false ], + [ '2012-02-02T17:10', '180', '2012-02-02T17:05', null, null, '2012-02-02T17:11', false ], + [ '2012-02-02T17:10', '86400', '2012-02-02T17:05', null, null, '2012-02-03T17:05', false ], + [ '2012-02-02T17:10', '129600', '2012-02-01T00:00', null, null, '2012-02-04T00:00', false ], + // Max values testing. + [ '2012-02-02T17:15', '60', null, 'foo', null, '2012-02-02T17:16', false ], + [ '2012-02-02T17:15', null, null, '2012-02-02T17:20', null, '2012-02-02T17:16', false ], + [ '2012-02-02T17:15', null, null, '2012-02-02T17:15', null, '2012-02-02T17:15', false ], + [ '2012-02-02T17:15', null, null, '2012-02-02T17:13', 4, '2012-02-02T17:15', false ], + [ '2012-02-02T20:05', '86400', null, '2012-02-03T20:05', null, '2012-02-03T20:05', false ], + [ '2012-02-02T18:00', '129600', null, '2012-02-04T20:00', null, '2012-02-04T06:00', false ], + // Step mismatch. + [ '2017-02-07T17:19', '120', '2017-02-07T17:10', null, null, '2017-02-07T17:20', false ], + [ '2017-02-07T17:19', '120', '2017-02-07T17:10', null, 2, '2017-02-07T17:22', false ], + [ '2017-02-07T17:19', '120', '2017-02-07T17:18', '2017-02-07T17:25', null, '2017-02-07T17:20', false ], + [ '2017-02-07T17:19', '120', null, null, null, '2017-02-07T17:21', false ], + [ '2017-02-07T17:19', '180', null, null, null, '2017-02-07T17:22', false ], + [ '2017-02-03T17:19', '172800', '2017-02-02T17:19', '2017-02-10T17:19', null, '2017-02-04T17:19', false ], + // Clamping. + [ '2017-02-07T17:22', null, null, '2017-02-07T17:11', null, '2017-02-07T17:22', false ], + [ '2017-02-07T17:22', '120', '2017-02-07T17:20', '2017-02-07T17:22', null, '2017-02-07T17:22', false ], + [ '2017-02-07T17:22', '300', '2017-02-07T17:12', '2017-02-07T17:20', 10, '2017-02-07T17:22', false ], + [ '2017-02-07T17:22', '300', '2017-02-07T17:18', '2017-02-07T17:20', 2, '2017-02-07T17:22', false ], + [ '2017-02-06T17:22', '600', '2017-02-02T17:00', '2017-02-07T17:20', 15, '2017-02-06T19:50', false ], + [ '2017-02-06T17:22', '600', '2017-02-02T17:10', '2017-02-07T17:20', 2, '2017-02-06T17:40', false ], + // value = "" (NaN). + [ '', null, null, null, null, '1970-01-01T00:01', false ], + // With step = 'any'. + [ '2017-02-07T17:30', 'any', null, null, 1, null, true ], + [ '2017-02-07T17:30', 'ANY', null, null, 1, null, true ], + [ '2017-02-07T17:30', 'AnY', null, null, 1, null, true ], + [ '2017-02-07T17:30', 'aNy', null, null, 1, null, true ], + ]}, ]; for (var test of testData) { diff --git a/dom/html/test/forms/test_valueAsDate_pref.html b/dom/html/test/forms/test_valueAsDate_pref.html index 8518c291b..0369980e4 100644 --- a/dom/html/test/forms/test_valueAsDate_pref.html +++ b/dom/html/test/forms/test_valueAsDate_pref.html @@ -12,8 +12,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=874640 /** Test for Bug 874640 **/ var states = [ - // dom.experimental_forms, dom.forms.datepicker, dom.forms.datetime, expectedValueAsDate - [ 'true', 'true', 'true', 'true' ], + // dom.experimental_forms, dom.forms.datetime, dom.forms.datetime.others, expectedValueAsDate + [ 'true', 'true', ,'true', 'true' ], [ 'true', 'false', 'false', 'true' ], [ 'false', 'true', 'false', 'true' ], [ 'false', 'false', 'true', 'true' ], @@ -33,8 +33,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=874640 SpecialPowers.pushPrefEnv({"set":[ ["dom.experimental_forms", state[0] === 'true'], - ["dom.forms.datepicker", state[1] === 'true'], - ["dom.forms.datetime", state[2] === 'true']]}, + ["dom.forms.datetime", state[1] === 'true'], + ["dom.forms.datetime.others", state[2] === 'true']]}, function() { iframe.src = 'data:text/html,<script>' + 'parent.is("valueAsDate" in document.createElement("input"), ' + diff --git a/dom/html/test/forms/test_valueasdate_attribute.html b/dom/html/test/forms/test_valueasdate_attribute.html index 8c19fefd9..65cab3b8e 100644 --- a/dom/html/test/forms/test_valueasdate_attribute.html +++ b/dom/html/test/forms/test_valueasdate_attribute.html @@ -47,8 +47,7 @@ var validTypes = ["color", false], ["month", true], ["week", true], - // TODO: temporary set to false until bug 888331 is fixed. - ["datetime-local", false], + ["datetime-local", true], ]; function checkAvailability() @@ -622,6 +621,107 @@ function checkWeekSet() } } +function checkDatetimeLocalGet() +{ + var validData = + [ + // Simple cases. + [ "2016-12-27T10:30", Date.UTC(2016, 11, 27, 10, 30, 0) ], + [ "2016-12-27T10:30:40", Date.UTC(2016, 11, 27, 10, 30, 40) ], + [ "2016-12-27T10:30:40.567", Date.UTC(2016, 11, 27, 10, 30, 40, 567) ], + [ "1969-12-31T12:00:00", Date.UTC(1969, 11, 31, 12, 0, 0) ], + [ "1970-01-01T00:00", 0 ], + // Leap years. + [ "1804-02-29 12:34", Date.UTC(1804, 1, 29, 12, 34, 0) ], + [ "2016-02-29T12:34", Date.UTC(2016, 1, 29, 12, 34, 0) ], + [ "2016-12-31T12:34:56", Date.UTC(2016, 11, 31, 12, 34, 56) ], + [ "2016-01-01T12:34:56.789", Date.UTC(2016, 0, 1, 12, 34, 56, 789) ], + [ "2017-01-01 12:34:56.789", Date.UTC(2017, 0, 1, 12, 34, 56, 789) ], + // Maximum valid datetime-local (limited by the ecma date object range). + [ "275760-09-13T00:00", 8640000000000000 ], + // Minimum valid datetime-local (limited by the input element minimum valid value). + [ "0001-01-01T00:00", -62135596800000 ], + ]; + + var invalidData = + [ + [ "invaliddateime-local" ], + [ "0000-01-01T00:00" ], + [ "2016-12-25T00:00Z" ], + [ "2015-02-29T12:34" ], + [ "1-1-1T12:00" ], + [ "" ], + // This datetime-local is valid for the input element, but is out of the + // date object range. In this case, on getting valueAsDate, a Date object + // will be created, but it will have a NaN internal value, and will return + // the string "Invalid Date". + [ "275760-09-13T12:00", true ], + ]; + + element.type = "datetime-local"; + for (let data of validData) { + element.value = data[0]; + is(element.valueAsDate.valueOf(), data[1], + "valueAsDate should return the " + + "valid date object representing this datetime-local"); + } + + for (let data of invalidData) { + element.value = data[0]; + if (data[1]) { + is(String(element.valueAsDate), "Invalid Date", + "valueAsDate should return an invalid Date object " + + "when the element value is not a valid datetime-local"); + } else { + is(element.valueAsDate, null, + "valueAsDate should return null " + + "when the element value is not a valid datetime-local"); + } + } +} + +function checkDatetimeLocalSet() +{ + var testData = + [ + // Simple cases. + [ Date.UTC(2016, 11, 27, 10, 30, 0), "2016-12-27T10:30" ], + [ Date.UTC(2016, 11, 27, 10, 30, 30), "2016-12-27T10:30:30" ], + [ Date.UTC(1999, 11, 31, 23, 59, 59), "1999-12-31T23:59:59" ], + [ Date.UTC(1999, 11, 31, 23, 59, 59, 999), "1999-12-31T23:59:59.999" ], + [ Date.UTC(123456, 7, 8, 9, 10), "123456-08-08T09:10" ], + [ 0, "1970-01-01T00:00" ], + // Maximum valid datetime-local (limited by the ecma date object range). + [ 8640000000000000, "275760-09-13T00:00" ], + // Minimum valid datetime-local (limited by the input element minimum valid value). + [ -62135596800000, "0001-01-01T00:00" ], + // Leap years. + [ Date.UTC(1804, 1, 29, 12, 34, 0), "1804-02-29T12:34" ], + [ Date.UTC(2016, 1, 29, 12, 34, 0), "2016-02-29T12:34" ], + [ Date.UTC(2016, 11, 31, 12, 34, 56), "2016-12-31T12:34:56" ], + [ Date.UTC(2016, 0, 1, 12, 34, 56, 789), "2016-01-01T12:34:56.789" ], + [ Date.UTC(2017, 0, 1, 12, 34, 56, 789), "2017-01-01T12:34:56.789" ], + // "Values must be truncated to valid datetime-local" + [ 123.123456789123, "1970-01-01T00:00:00.123" ], + [ 1e-1, "1970-01-01T00:00" ], + [ -1.1, "1969-12-31T23:59:59.999" ], + [ -345600000, "1969-12-28T00:00" ], + // Negative years, this is out of range for the input element, + // the corresponding datetime-local string is the empty string + [ -62135596800001, "" ], + ]; + + element.type = "datetime-local"; + for (let data of testData) { + element.valueAsDate = new Date(data[0]); + is(element.value, data[1], "valueAsDate should set the value to " + + data[1]); + element.valueAsDate = new testFrame.Date(data[0]); + is(element.value, data[1], "valueAsDate with other-global date should " + + "set the value to " + data[1]); + } +} + checkAvailability(); checkGarbageValues(); checkWithBustedPrototype(); @@ -642,6 +742,10 @@ checkMonthSet(); checkWeekGet(); checkWeekSet(); +// Test <input type='datetime-local'>. +checkDatetimeLocalGet(); +checkDatetimeLocalSet(); + </script> </pre> </body> diff --git a/dom/html/test/forms/test_valueasnumber_attribute.html b/dom/html/test/forms/test_valueasnumber_attribute.html index d7471502b..2660fc7ed 100644 --- a/dom/html/test/forms/test_valueasnumber_attribute.html +++ b/dom/html/test/forms/test_valueasnumber_attribute.html @@ -46,8 +46,7 @@ function checkAvailability() ["color", false], ["month", true], ["week", true], - // TODO: temporary set to false until bug 888331 is fixed. - ["datetime-local", false], + ["datetime-local", true], ]; var element = document.createElement('input'); @@ -612,7 +611,6 @@ function checkWeekGet() var element = document.createElement('input'); element.type = "week"; for (let data of validData) { - dump("Test: " + data[0]); element.value = data[0]; is(element.valueAsNumber, data[1], "valueAsNumber should return the " + "integer value representing this week"); @@ -698,7 +696,120 @@ function checkWeekSet() try { element.valueAsNumber = data[0]; - is(element.value, data[1], "valueAsNumber should set the value to " + data[1]); + is(element.value, data[1], "valueAsNumber should set the value to " + + data[1]); + } catch(e) { + caught = true; + } + + if (data[2]) { + ok(caught, "valueAsNumber should have thrown"); + is(element.value, data[1], "the value should not have changed"); + } else { + ok(!caught, "valueAsNumber should not have thrown"); + } + } +} + +function checkDatetimeLocalGet() { + var validData = + [ + // Simple cases. + [ "2016-12-20T09:58", Date.UTC(2016, 11, 20, 9, 58) ], + [ "2016-12-20T09:58:30", Date.UTC(2016, 11, 20, 9, 58, 30) ], + [ "2016-12-20T09:58:30.123", Date.UTC(2016, 11, 20, 9, 58, 30, 123) ], + [ "2017-01-01T10:00", Date.UTC(2017, 0, 1, 10, 0, 0) ], + [ "1969-12-31T12:00:00", Date.UTC(1969, 11, 31, 12, 0, 0) ], + [ "1970-01-01T00:00", 0 ], + // Leap years. + [ "1804-02-29 12:34", Date.UTC(1804, 1, 29, 12, 34, 0) ], + [ "2016-02-29T12:34", Date.UTC(2016, 1, 29, 12, 34, 0) ], + [ "2016-12-31T12:34:56", Date.UTC(2016, 11, 31, 12, 34, 56) ], + [ "2016-01-01T12:34:56.789", Date.UTC(2016, 0, 1, 12, 34, 56, 789) ], + [ "2017-01-01 12:34:56.789", Date.UTC(2017, 0, 1, 12, 34, 56, 789) ], + // Maximum valid datetime-local (limited by the ecma date object range). + [ "275760-09-13T00:00", 8640000000000000 ], + // Minimum valid datetime-local (limited by the input element minimum valid value). + [ "0001-01-01T00:00", -62135596800000 ], + ]; + + var invalidData = + [ + "invaliddatetime-local", + "0000-01-01T00:00", + "2016-12-25T00:00Z", + "2015-02-29T12:34", + "1-1-1T12:00", + // Out of range. + "275760-09-13T12:00", + ]; + + var element = document.createElement('input'); + element.type = "datetime-local"; + for (let data of validData) { + element.value = data[0]; + is(element.valueAsNumber, data[1], "valueAsNumber should return the " + + "integer value representing this datetime-local"); + } + + for (let data of invalidData) { + element.value = data; + ok(isNaN(element.valueAsNumber), "valueAsNumber should return NaN " + + "when the element value is not a valid datetime-local"); + } +} + +function checkDatetimeLocalSet() +{ + var testData = + [ + // Simple cases. + [ Date.UTC(2016, 11, 20, 9, 58, 0), "2016-12-20T09:58", ], + [ Date.UTC(2016, 11, 20, 9, 58, 30), "2016-12-20T09:58:30" ], + [ Date.UTC(2016, 11, 20, 9, 58, 30, 123), "2016-12-20T09:58:30.123" ], + [ Date.UTC(2017, 0, 1, 10, 0, 0), "2017-01-01T10:00" ], + [ Date.UTC(1969, 11, 31, 12, 0, 0), "1969-12-31T12:00" ], + [ 0, "1970-01-01T00:00" ], + // Maximum valid week (limited by the ecma date object range). + [ 8640000000000000, "275760-09-13T00:00" ], + // Minimum valid datetime-local (limited by the input element minimum valid value). + [ -62135596800000, "0001-01-01T00:00" ], + // Leap years. + [ Date.UTC(1804, 1, 29, 12, 34, 0), "1804-02-29T12:34" ], + [ Date.UTC(2016, 1, 29, 12, 34, 0), "2016-02-29T12:34" ], + [ Date.UTC(2016, 11, 31, 12, 34, 56), "2016-12-31T12:34:56" ], + [ Date.UTC(2016, 0, 1, 12, 34, 56, 789), "2016-01-01T12:34:56.789" ], + [ Date.UTC(2017, 0, 1, 12, 34, 56, 789), "2017-01-01T12:34:56.789" ], + // "Values must be truncated to valid datetime-local" + [ 0.3, "1970-01-01T00:00" ], + [ 1e-1, "1970-01-01T00:00" ], + [ -1 , "1969-12-31T23:59:59.999" ], + [ -345600000, "1969-12-28T00:00" ], + // Invalid numbers. + // Those are implicitly converted to numbers + [ "", "1970-01-01T00:00" ], + [ true, "1970-01-01T00:00:00.001" ], + [ false, "1970-01-01T00:00" ], + [ null, "1970-01-01T00:00" ], + // Those are converted to NaN, the corresponding week string is the empty string + [ "invaliddatetime-local", "" ], + [ NaN, "" ], + [ undefined, "" ], + // Infinity will keep the current value and throw (so we need to set a current value). + [ Date.UTC(2016, 11, 27, 15, 10, 0), "2016-12-27T15:10" ], + [ Infinity, "2016-12-27T15:10", true ], + [ -Infinity, "2016-12-27T15:10", true ], + ]; + + var element = document.createElement('input'); + element.type = "datetime-local"; + for (let data of testData) { + var caught = false; + + try { + element.valueAsNumber = data[0]; + is(element.value, data[1], "valueAsNumber should set the value to " + + data[1]); } catch(e) { caught = true; } @@ -738,6 +849,10 @@ checkMonthSet(); checkWeekGet(); checkWeekSet(); +// <input type='datetime-local'> test +checkDatetimeLocalGet(); +checkDatetimeLocalSet(); + </script> </pre> </body> diff --git a/dom/html/test/test_bug558788-1.html b/dom/html/test/test_bug558788-1.html index 94b7a5f00..4db61ed73 100644 --- a/dom/html/test/test_bug558788-1.html +++ b/dom/html/test/test_bug558788-1.html @@ -154,13 +154,14 @@ function checkInputURL() sendString("ttp://mozilla.org"); checkValidApplies(element); - for (var i=0; i<13; ++i) { + for (var i=0; i<10; ++i) { synthesizeKey("VK_BACK_SPACE", {}); checkValidApplies(element); } synthesizeKey("VK_BACK_SPACE", {}); - for (var i=0; i<4; ++i) { + // "http://" is now invalid + for (var i=0; i<7; ++i) { checkInvalidApplies(element); synthesizeKey("VK_BACK_SPACE", {}); } diff --git a/dom/ipc/DatePickerParent.cpp b/dom/ipc/DatePickerParent.cpp deleted file mode 100644 index 509944ddd..000000000 --- a/dom/ipc/DatePickerParent.cpp +++ /dev/null @@ -1,87 +0,0 @@ -/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* vim: set ts=8 sts=2 et sw=2 tw=80: */ -/* 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/. */ - -#include "DatePickerParent.h" -#include "nsComponentManagerUtils.h" -#include "nsIDocument.h" -#include "nsIDOMWindow.h" -#include "mozilla/Unused.h" -#include "mozilla/dom/Element.h" -#include "mozilla/dom/TabParent.h" - -using mozilla::Unused; -using namespace mozilla::dom; - -NS_IMPL_ISUPPORTS(DatePickerParent::DatePickerShownCallback, - nsIDatePickerShownCallback); - -NS_IMETHODIMP -DatePickerParent::DatePickerShownCallback::Cancel() -{ - if (mDatePickerParent) { - Unused << mDatePickerParent->SendCancel(); - } - return NS_OK; -} - -NS_IMETHODIMP -DatePickerParent::DatePickerShownCallback::Done(const nsAString& aDate) -{ - if (mDatePickerParent) { - Unused << mDatePickerParent->Send__delete__(mDatePickerParent, - nsString(aDate)); - } - return NS_OK; -} - -void -DatePickerParent::DatePickerShownCallback::Destroy() -{ - mDatePickerParent = nullptr; -} - -bool -DatePickerParent::CreateDatePicker() -{ - mPicker = do_CreateInstance("@mozilla.org/datepicker;1"); - if (!mPicker) { - return false; - } - - Element* ownerElement = TabParent::GetFrom(Manager())->GetOwnerElement(); - if (!ownerElement) { - return false; - } - - nsCOMPtr<mozIDOMWindowProxy> window = do_QueryInterface(ownerElement->OwnerDoc()->GetWindow()); - if (!window) { - return false; - } - - return NS_SUCCEEDED(mPicker->Init(window, mTitle, mInitialDate)); -} - -bool -DatePickerParent::RecvOpen() -{ - if (!CreateDatePicker()) { - Unused << Send__delete__(this, mInitialDate); - return true; - } - - mCallback = new DatePickerShownCallback(this); - - mPicker->Open(mCallback); - return true; -}; - -void -DatePickerParent::ActorDestroy(ActorDestroyReason aWhy) -{ - if (mCallback) { - mCallback->Destroy(); - } -} diff --git a/dom/ipc/DatePickerParent.h b/dom/ipc/DatePickerParent.h deleted file mode 100644 index 73b66f96c..000000000 --- a/dom/ipc/DatePickerParent.h +++ /dev/null @@ -1,61 +0,0 @@ -/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* vim: set ts=8 sts=2 et sw=2 tw=80: */ -/* 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/. */ - -#ifndef mozilla_dom_DatePickerParent_h -#define mozilla_dom_DatePickerParent_h - -#include "mozilla/dom/PDatePickerParent.h" -#include "nsIDatePicker.h" - -namespace mozilla { -namespace dom { - -class DatePickerParent : public PDatePickerParent -{ - public: - DatePickerParent(const nsString& aTitle, - const nsString& aInitialDate) - : mTitle(aTitle) - , mInitialDate(aInitialDate) - {} - - virtual bool RecvOpen() override; - virtual void ActorDestroy(ActorDestroyReason aWhy) override; - - class DatePickerShownCallback final - : public nsIDatePickerShownCallback - { - public: - explicit DatePickerShownCallback(DatePickerParent* aDatePickerParnet) - : mDatePickerParent(aDatePickerParnet) - {} - - NS_DECL_ISUPPORTS - NS_DECL_NSIDATEPICKERSHOWNCALLBACK - - void Destroy(); - - private: - ~DatePickerShownCallback() {} - DatePickerParent* mDatePickerParent; - }; - - private: - virtual ~DatePickerParent() {} - - bool CreateDatePicker(); - - RefPtr<DatePickerShownCallback> mCallback; - nsCOMPtr<nsIDatePicker> mPicker; - - nsString mTitle; - nsString mInitialDate; -}; - -} // namespace dom -} // namespace mozilla - -#endif // mozilla_dom_DatePickerParent_h diff --git a/dom/ipc/PBrowser.ipdl b/dom/ipc/PBrowser.ipdl index 9dfccbc5c..f09e484ee 100644 --- a/dom/ipc/PBrowser.ipdl +++ b/dom/ipc/PBrowser.ipdl @@ -9,7 +9,6 @@ include protocol PBlob; include protocol PColorPicker; include protocol PContent; include protocol PContentBridge; -include protocol PDatePicker; include protocol PDocAccessible; include protocol PFilePicker; include protocol PIndexedDBPermissionRequest; @@ -116,7 +115,6 @@ nested(upto inside_cpow) sync protocol PBrowser manager PContent or PContentBridge; manages PColorPicker; - manages PDatePicker; manages PDocAccessible; manages PFilePicker; manages PIndexedDBPermissionRequest; @@ -441,12 +439,6 @@ parent: */ async PColorPicker(nsString title, nsString initialColor); - /** - * Create an asynchronous date picker on the parent side, - * but don't open it yet. - */ - async PDatePicker(nsString title, nsString initialDate); - async PFilePicker(nsString aTitle, int16_t aMode); /** diff --git a/dom/ipc/PDatePicker.ipdl b/dom/ipc/PDatePicker.ipdl deleted file mode 100644 index 90a2654bb..000000000 --- a/dom/ipc/PDatePicker.ipdl +++ /dev/null @@ -1,27 +0,0 @@ -/* -*- Mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 8 -*- */ -/* vim: set sw=4 ts=8 et tw=80 ft=cpp : */ - -/* 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/. */ - -include protocol PBrowser; - -namespace mozilla { -namespace dom { - -protocol PDatePicker -{ - manager PBrowser; - -parent: - async Open(); - -child: - async Cancel(); - - async __delete__(nsString color); -}; - -} // namespace dom -} // namespace mozilla diff --git a/dom/ipc/TabChild.cpp b/dom/ipc/TabChild.cpp index c8a0c6e3f..705799c54 100644 --- a/dom/ipc/TabChild.cpp +++ b/dom/ipc/TabChild.cpp @@ -96,7 +96,6 @@ #include "LayersLogging.h" #include "nsDOMClassInfoID.h" #include "nsColorPickerProxy.h" -#include "nsDatePickerProxy.h" #include "nsContentPermissionHelper.h" #include "nsPresShell.h" #include "nsIAppsService.h" @@ -2013,21 +2012,6 @@ TabChild::DeallocPColorPickerChild(PColorPickerChild* aColorPicker) return true; } -PDatePickerChild* -TabChild::AllocPDatePickerChild(const nsString&, const nsString&) -{ - NS_RUNTIMEABORT("unused"); - return nullptr; -} - -bool -TabChild::DeallocPDatePickerChild(PDatePickerChild* aDatePicker) -{ - nsDatePickerProxy* picker = static_cast<nsDatePickerProxy*>(aDatePicker); - NS_RELEASE(picker); - return true; -} - PFilePickerChild* TabChild::AllocPFilePickerChild(const nsString&, const int16_t&) { diff --git a/dom/ipc/TabChild.h b/dom/ipc/TabChild.h index 2232ffeaf..d9988a596 100644 --- a/dom/ipc/TabChild.h +++ b/dom/ipc/TabChild.h @@ -451,10 +451,6 @@ public: virtual bool DeallocPColorPickerChild(PColorPickerChild* aActor) override; - virtual PDatePickerChild* - AllocPDatePickerChild(const nsString& title, const nsString& initialDate) override; - virtual bool DeallocPDatePickerChild(PDatePickerChild* actor) override; - virtual PFilePickerChild* AllocPFilePickerChild(const nsString& aTitle, const int16_t& aMode) override; diff --git a/dom/ipc/TabParent.cpp b/dom/ipc/TabParent.cpp index 8e98de3ce..0f190708f 100644 --- a/dom/ipc/TabParent.cpp +++ b/dom/ipc/TabParent.cpp @@ -80,7 +80,6 @@ #include "PermissionMessageUtils.h" #include "StructuredCloneData.h" #include "ColorPickerParent.h" -#include "DatePickerParent.h" #include "FilePickerParent.h" #include "TabChild.h" #include "LoadContext.h" @@ -2424,20 +2423,6 @@ TabParent::DeallocPColorPickerParent(PColorPickerParent* actor) return true; } -PDatePickerParent* -TabParent::AllocPDatePickerParent(const nsString& aTitle, - const nsString& aInitialDate) -{ - return new DatePickerParent(aTitle, aInitialDate); -} - -bool -TabParent::DeallocPDatePickerParent(PDatePickerParent* actor) -{ - delete actor; - return true; -} - PRenderFrameParent* TabParent::AllocPRenderFrameParent() { diff --git a/dom/ipc/TabParent.h b/dom/ipc/TabParent.h index 09bb999f3..3624ce320 100644 --- a/dom/ipc/TabParent.h +++ b/dom/ipc/TabParent.h @@ -347,10 +347,6 @@ public: virtual bool DeallocPColorPickerParent(PColorPickerParent* aColorPicker) override; - virtual PDatePickerParent* - AllocPDatePickerParent(const nsString& aTitle, const nsString& aInitialDate) override; - virtual bool DeallocPDatePickerParent(PDatePickerParent* aDatePicker) override; - virtual PDocAccessibleParent* AllocPDocAccessibleParent(PDocAccessibleParent*, const uint64_t&, const uint32_t&, const IAccessibleHolder&) override; diff --git a/dom/ipc/moz.build b/dom/ipc/moz.build index ff3880bc2..71d193d44 100644 --- a/dom/ipc/moz.build +++ b/dom/ipc/moz.build @@ -55,7 +55,6 @@ UNIFIED_SOURCES += [ 'ContentProcess.cpp', 'ContentProcessManager.cpp', 'CrashReporterParent.cpp', - 'DatePickerParent.cpp', 'FilePickerParent.cpp', 'nsIContentChild.cpp', 'nsIContentParent.cpp', @@ -95,7 +94,6 @@ IPDL_SOURCES += [ 'PContentPermissionRequest.ipdl', 'PCrashReporter.ipdl', 'PCycleCollectWithLogs.ipdl', - 'PDatePicker.ipdl', 'PFilePicker.ipdl', 'PMemoryReportRequest.ipdl', 'PPluginWidget.ipdl', diff --git a/dom/locales/en-US/chrome/layout/HtmlForm.properties b/dom/locales/en-US/chrome/layout/HtmlForm.properties index 621a0b2b3..82274b2bc 100644 --- a/dom/locales/en-US/chrome/layout/HtmlForm.properties +++ b/dom/locales/en-US/chrome/layout/HtmlForm.properties @@ -34,7 +34,6 @@ NoDirSelected=No directory selected. # %S will be a number greater or equal to 2. XFilesSelected=%S files selected. ColorPicker=Choose a color -DatePicker=Choose a date # LOCALIZATION NOTE (AndNMoreFiles): Semi-colon list of plural forms. # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals # This string is shown at the end of the tooltip text for <input type='file' diff --git a/dom/security/nsCSPContext.cpp b/dom/security/nsCSPContext.cpp index 5e435d4ca..a7517f65e 100644 --- a/dom/security/nsCSPContext.cpp +++ b/dom/security/nsCSPContext.cpp @@ -219,14 +219,6 @@ nsCSPContext::permitsInternal(CSPDirective aDir, nsAutoString violatedDirective; for (uint32_t p = 0; p < mPolicies.Length(); p++) { - - // According to the W3C CSP spec, frame-ancestors checks are ignored for - // report-only policies (when "monitoring"). - if (aDir == nsIContentSecurityPolicy::FRAME_ANCESTORS_DIRECTIVE && - mPolicies[p]->getReportOnlyFlag()) { - continue; - } - if (!mPolicies[p]->permits(aDir, aContentLocation, aNonce, diff --git a/dom/security/test/csp/file_frame_ancestors_ro.html b/dom/security/test/csp/file_frame_ancestors_ro.html new file mode 100644 index 000000000..ff5ae9cf9 --- /dev/null +++ b/dom/security/test/csp/file_frame_ancestors_ro.html @@ -0,0 +1 @@ +<html><body>Child Document</body></html> diff --git a/dom/security/test/csp/file_frame_ancestors_ro.html^headers^ b/dom/security/test/csp/file_frame_ancestors_ro.html^headers^ new file mode 100644 index 000000000..d018af3a9 --- /dev/null +++ b/dom/security/test/csp/file_frame_ancestors_ro.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy-Report-Only: frame-ancestors 'none'; report-uri http://mochi.test:8888/foo.sjs diff --git a/dom/security/test/csp/mochitest.ini b/dom/security/test/csp/mochitest.ini index ca5c2c6ea..33b112020 100644 --- a/dom/security/test/csp/mochitest.ini +++ b/dom/security/test/csp/mochitest.ini @@ -91,6 +91,8 @@ support-files = file_bug941404.html file_bug941404_xhr.html file_bug941404_xhr.html^headers^ + file_frame_ancestors_ro.html + file_frame_ancestors_ro.html^headers^ file_hash_source.html file_dual_header_testserver.sjs file_hash_source.html^headers^ @@ -240,6 +242,7 @@ skip-if = toolkit == 'android' # Times out, not sure why (bug 1008445) [test_bug910139.html] [test_bug909029.html] [test_bug1229639.html] +[test_frame_ancestors_ro.html] [test_policyuri_regression_from_multipolicy.html] [test_nonce_source.html] [test_bug941404.html] diff --git a/dom/security/test/csp/test_frame_ancestors_ro.html b/dom/security/test/csp/test_frame_ancestors_ro.html new file mode 100644 index 000000000..90f68e25e --- /dev/null +++ b/dom/security/test/csp/test_frame_ancestors_ro.html @@ -0,0 +1,69 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for frame-ancestors support in Content-Security-Policy-Report-Only</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe style="width: 100%" id="cspframe"></iframe> +<script type="text/javascript"> +const docUri = "http://mochi.test:8888/tests/dom/security/test/csp/file_frame_ancestors_ro.html"; +const frame = document.getElementById("cspframe"); + +let testResults = { + reportFired: false, + frameLoaded: false +}; + +function checkResults(reportObj) { + let cspReport = reportObj["csp-report"]; + is(cspReport["document-uri"], docUri, "Incorrect document-uri"); + + // we can not test for the whole referrer since it includes platform specific information + is(cspReport["referrer"], document.location.toString(), "Incorrect referrer"); + is(cspReport["blocked-uri"], document.location.toString(), "Incorrect blocked-uri"); + is(cspReport["violated-directive"], "frame-ancestors 'none'", "Incorrect violated-directive"); + is(cspReport["original-policy"], "frame-ancestors 'none'; report-uri http://mochi.test:8888/foo.sjs", "Incorrect original-policy"); + testResults.reportFired = true; +} + +let chromeScriptUrl = SimpleTest.getTestFileURL("file_report_chromescript.js"); +let script = SpecialPowers.loadChromeScript(chromeScriptUrl); + +script.addMessageListener('opening-request-completed', function ml(msg) { + if (msg.error) { + ok(false, "Could not query report (exception: " + msg.error + ")"); + } else { + try { + let reportObj = JSON.parse(msg.report); + // test for the proper values in the report object + checkResults(reportObj); + } catch (e) { + ok(false, "Error verifying report object (exception: " + e + ")"); + } + } + + script.removeMessageListener('opening-request-completed', ml); + script.sendAsyncMessage("finish"); + checkTestResults(); +}); + +frame.addEventListener( 'load', () => { + // Make sure the frame is still loaded + testResults.frameLoaded = true; + checkTestResults() +} ); + +function checkTestResults() { + if( testResults.reportFired && testResults.frameLoaded ) { + SimpleTest.finish(); + } +} + +SimpleTest.waitForExplicitFinish(); +frame.src = docUri; + +</script> +</body> +</html> diff --git a/dom/tests/mochitest/fetch/test_headers_common.js b/dom/tests/mochitest/fetch/test_headers_common.js index fe792b25b..8b17b6b12 100644 --- a/dom/tests/mochitest/fetch/test_headers_common.js +++ b/dom/tests/mochitest/fetch/test_headers_common.js @@ -213,12 +213,12 @@ function TestHeadersIterator() { var value_iter = headers.values(); var entries_iter = headers.entries(); - arrayEquals(iterate(key_iter), ["foo", "foo", "foo2"], "Correct key iterator"); - arrayEquals(iterate(value_iter), ["bar", ehsanInflated, "baz2"], "Correct value iterator"); - arrayEquals(iterate(entries_iter), [["foo", "bar"], ["foo", ehsanInflated], ["foo2", "baz2"]], "Correct entries iterator"); + arrayEquals(iterate(key_iter), ["foo", "foo2"], "Correct key iterator"); + arrayEquals(iterate(value_iter), ["bar, " + ehsanInflated, "baz2"], "Correct value iterator"); + arrayEquals(iterate(entries_iter), [["foo", "bar, " + ehsanInflated], ["foo2", "baz2"]], "Correct entries iterator"); - arrayEquals(iterateForOf(headers), [["foo", "bar"], ["foo", ehsanInflated], ["foo2", "baz2"]], "Correct entries iterator"); - arrayEquals(iterateForOf(new Headers(headers)), [["foo", "bar"], ["foo", ehsanInflated], ["foo2", "baz2"]], "Correct entries iterator"); + arrayEquals(iterateForOf(headers), [["foo", "bar, " + ehsanInflated], ["foo2", "baz2"]], "Correct entries iterator"); + arrayEquals(iterateForOf(new Headers(headers)), [["foo", "bar, " + ehsanInflated], ["foo2", "baz2"]], "Correct entries iterator"); } function runTest() { diff --git a/dom/url/tests/test_url.html b/dom/url/tests/test_url.html index d07a752bb..73e75667d 100644 --- a/dom/url/tests/test_url.html +++ b/dom/url/tests/test_url.html @@ -399,6 +399,18 @@ </script> <script> + /** Test for Bug 1275746 **/ + SimpleTest.doesThrow(() => { var url = new URL("http:"); }, "http: is not a valid URL"); + SimpleTest.doesThrow(() => { var url = new URL("http:///"); }, "http: is not a valid URL"); + + var url = new URL("file:"); + is(url.href, "file:///", "Parsing file: should work."); + + url = new URL("file:///"); + is(url.href, "file:///", "Parsing file:/// should work."); + </script> + + <script> var url = new URL("scheme:path/to/file?query#hash"); is(url.href, "scheme:path/to/file?query#hash"); is(url.pathname, "path/to/file"); diff --git a/dom/webidl/CanvasRenderingContext2D.webidl b/dom/webidl/CanvasRenderingContext2D.webidl index 38a30f9e3..1c5564215 100644 --- a/dom/webidl/CanvasRenderingContext2D.webidl +++ b/dom/webidl/CanvasRenderingContext2D.webidl @@ -27,6 +27,9 @@ dictionary HitRegionOptions { }; typedef (HTMLImageElement or + SVGImageElement) HTMLOrSVGImageElement; + +typedef (HTMLOrSVGImageElement or HTMLCanvasElement or HTMLVideoElement or ImageBitmap) CanvasImageSource; diff --git a/dom/webidl/HTMLInputElement.webidl b/dom/webidl/HTMLInputElement.webidl index 050d19510..cf3e9a4c7 100644 --- a/dom/webidl/HTMLInputElement.webidl +++ b/dom/webidl/HTMLInputElement.webidl @@ -238,6 +238,9 @@ partial interface HTMLInputElement { dictionary DateTimeValue { long hour; long minute; + long year; + long month; + long day; }; partial interface HTMLInputElement { @@ -250,6 +253,14 @@ partial interface HTMLInputElement { [Pref="dom.forms.datetime", ChromeOnly] void setDateTimePickerState(boolean open); + [Pref="dom.forms.datetime", ChromeOnly, + BinaryName="getMinimumAsDouble"] + double getMinimum(); + + [Pref="dom.forms.datetime", ChromeOnly, + BinaryName="getMaximumAsDouble"] + double getMaximum(); + [Pref="dom.forms.datetime", Func="IsChromeOrXBL"] void openDateTimePicker(optional DateTimeValue initialValue); @@ -258,4 +269,12 @@ partial interface HTMLInputElement { [Pref="dom.forms.datetime", Func="IsChromeOrXBL"] void closeDateTimePicker(); + + [Pref="dom.forms.datetime", Func="IsChromeOrXBL", + BinaryName="getStepAsDouble"] + double getStep(); + + [Pref="dom.forms.datetime", Func="IsChromeOrXBL", + BinaryName="getStepBaseAsDouble"] + double getStepBase(); }; diff --git a/editor/libeditor/HTMLEditorDataTransfer.cpp b/editor/libeditor/HTMLEditorDataTransfer.cpp index b9cd8adb9..ed350c0dd 100644 --- a/editor/libeditor/HTMLEditorDataTransfer.cpp +++ b/editor/libeditor/HTMLEditorDataTransfer.cpp @@ -2382,27 +2382,26 @@ HTMLEditor::ReplaceOrphanedStructure( } // If we found substructure, paste it instead of its descendants. - // Only replace with the substructure if all the nodes in the list are - // descendants. - bool shouldReplaceNodes = true; - for (uint32_t i = 0; i < aNodeArray.Length(); i++) { + // Postprocess list to remove any descendants of this node so that we don't + // insert them twice. + uint32_t removedCount = 0; + uint32_t originalLength = aNodeArray.Length(); + for (uint32_t i = 0; i < originalLength; i++) { uint32_t idx = aStartOrEnd == StartOrEnd::start ? - i : (aNodeArray.Length() - i - 1); + (i - removedCount) : (originalLength - i - 1); OwningNonNull<nsINode> endpoint = aNodeArray[idx]; - if (!EditorUtils::IsDescendantOf(endpoint, replaceNode)) { - shouldReplaceNodes = false; - break; + if (endpoint == replaceNode || + EditorUtils::IsDescendantOf(endpoint, replaceNode)) { + aNodeArray.RemoveElementAt(idx); + removedCount++; } } - if (shouldReplaceNodes) { - // Now replace the removed nodes with the structural parent - aNodeArray.Clear(); - if (aStartOrEnd == StartOrEnd::end) { - aNodeArray.AppendElement(*replaceNode); - } else { - aNodeArray.InsertElementAt(0, *replaceNode); - } + // Now replace the removed nodes with the structural parent + if (aStartOrEnd == StartOrEnd::end) { + aNodeArray.AppendElement(*replaceNode); + } else { + aNodeArray.InsertElementAt(0, *replaceNode); } } diff --git a/editor/libeditor/tests/mochitest.ini b/editor/libeditor/tests/mochitest.ini index 447fb8b65..4df3f606b 100644 --- a/editor/libeditor/tests/mochitest.ini +++ b/editor/libeditor/tests/mochitest.ini @@ -217,6 +217,9 @@ skip-if = toolkit == 'android' [test_bug1258085.html] [test_bug1268736.html] [test_bug1270235.html] +[test_bug1306532.html] +subsuite = clipboard +skip-if = toolkit == 'android' [test_bug1310912.html] skip-if = toolkit == 'android' # bug 1315898 [test_bug1314790.html] @@ -224,6 +227,7 @@ skip-if = toolkit == 'android' # bug 1315898 [test_bug1328023.html] [test_bug1330796.html] [test_bug1332876.html] +[test_bug1352799.html] [test_CF_HTML_clipboard.html] subsuite = clipboard diff --git a/editor/libeditor/tests/test_bug1306532.html b/editor/libeditor/tests/test_bug1306532.html new file mode 100644 index 000000000..1d7b3e7af --- /dev/null +++ b/editor/libeditor/tests/test_bug1306532.html @@ -0,0 +1,64 @@ +<!DOCTYPE HTML> +<html><head> +<title>Test for bug 1306532</title> +<style src="/tests/SimpleTest/test.css" type="text/css"></style> +<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> +<script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + +<script class="testbody" type="application/javascript"> + +function runTest() { + // Copy content from table. + var selection = getSelection(); + var startRange = document.createRange(); + startRange.setStart(headingone, 0); + startRange.setEnd(celltwo, 0); + selection.removeAllRanges(); + selection.addRange(startRange); + SpecialPowers.wrap(document).execCommand("copy", false, null); + + // Paste content into "pasteframe" + var pasteContainer = pasteframe.contentDocument.body; + var pasteRange = pasteframe.contentDocument.createRange(); + pasteRange.selectNodeContents(pasteContainer); + pasteRange.collapse(false); + selection.removeAllRanges(); + selection.addRange(pasteRange); + SpecialPowers.wrap(pasteframe.contentDocument).execCommand("paste", false, null); + + is(pasteContainer.querySelector("#headingone").textContent, "Month", "First heading should be 'Month'."); + is(pasteContainer.querySelector("#headingtwo").textContent, "Savings", "Second heading should be 'Savings'."); + is(pasteContainer.querySelector("#cellone").textContent, "January", "First cell should be 'January'."); + is(pasteContainer.querySelector("#celltwo").textContent, "$100", "Second cell should be '$100'."); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); + +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1306532">Mozilla Bug 1306532</a> +<p id="display"></p> + +<pre id="test"> +</pre> + +<div id="container"> +<table border="1"> + <tr> + <th id="headingone">Month</th> + <th id="headingtwo">Savings</th> + </tr> + <tr> + <td id="cellone">January</td> + <td id="celltwo">$100</td> + </tr> +</table> +</div> + +<iframe onload="runTest();" id="pasteframe" src="data:text/html,<html><body contenteditable='true'>"></iframe> + +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1352799.html b/editor/libeditor/tests/test_bug1352799.html new file mode 100644 index 000000000..daedc40fc --- /dev/null +++ b/editor/libeditor/tests/test_bug1352799.html @@ -0,0 +1,98 @@ +<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1352799
+-->
+<head>
+ <title>Test for Bug 1352799</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1352799">Mozilla Bug 1352799</a>
+<p id="display"></p>
+<div id="content">
+<div id="input-container" style="display: none;">
+<input id="input" maxlength="1">
+</div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 1352799 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ var input = document.getElementById("input");
+
+ var inputcontainer = document.getElementById('input-container');
+ input.setAttribute('maxlength', 2);
+ inputcontainer.style.display = 'block';
+
+ input.focus();
+
+ synthesizeKey('1', {});
+ synthesizeKey('2', {});
+ synthesizeKey('3', {});
+
+ is(input.value, '12', 'value should be 12 with maxlength = 2');
+
+ input.value = '';
+ inputcontainer.style.display = 'none';
+
+ window.setTimeout(() => {
+ input.setAttribute('maxlength', 4);
+ inputcontainer.style.display = 'block';
+
+ input.focus();
+
+ synthesizeKey('4', {});
+ synthesizeKey('5', {});
+ synthesizeKey('6', {});
+ synthesizeKey('7', {});
+ synthesizeKey('8', {});
+
+ is(input.value, '4567', 'value should be 4567 with maxlength = 4');
+
+ inputcontainer.style.display = 'none';
+
+ window.setTimeout(() => {
+ input.setAttribute('maxlength', 2);
+ inputcontainer.style.display = 'block';
+
+ input.focus();
+
+ synthesizeKey('1', {});
+ synthesizeKey('2', {});
+
+ todo_is(input.value, '45', 'value should be 45 with maxlength = 2');
+
+ input.value = '';
+ inputcontainer.style.display = 'none';
+
+ window.setTimeout(() => {
+ input.removeAttribute('maxlength');
+ inputcontainer.style.display = 'block';
+
+ input.focus();
+
+ synthesizeKey('1', {});
+ synthesizeKey('2', {});
+ synthesizeKey('3', {});
+ synthesizeKey('4', {});
+ synthesizeKey('5', {});
+ synthesizeKey('6', {});
+ synthesizeKey('7', {});
+ synthesizeKey('8', {});
+
+ is(input.value, '12345678', 'value should be 12345678 without maxlength');
+
+ SimpleTest.finish();
+ }, 0);
+ }, 0);
+ }, 0);
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/extensions/cookie/test/unit/test_bug526789.js b/extensions/cookie/test/unit/test_bug526789.js index 0eac1d492..624044577 100644 --- a/extensions/cookie/test/unit/test_bug526789.js +++ b/extensions/cookie/test/unit/test_bug526789.js @@ -79,23 +79,11 @@ function run_test() { cm.removeAll(); - // test that setting an empty or '.' http:// host results in a no-op var uri = NetUtil.newURI("http://baz.com/"); - var emptyuri = NetUtil.newURI("http:///"); - var doturi = NetUtil.newURI("http://./"); do_check_eq(uri.asciiHost, "baz.com"); - do_check_eq(emptyuri.asciiHost, ""); - do_check_eq(doturi.asciiHost, "."); - cs.setCookieString(emptyuri, null, "foo2=bar", null); - do_check_eq(getCookieCount(), 0); - cs.setCookieString(doturi, null, "foo3=bar", null); - do_check_eq(getCookieCount(), 0); cs.setCookieString(uri, null, "foo=bar", null); - do_check_eq(getCookieCount(), 1); do_check_eq(cs.getCookieString(uri, null), "foo=bar"); - do_check_eq(cs.getCookieString(emptyuri, null), null); - do_check_eq(cs.getCookieString(doturi, null), null); do_check_eq(cm.countCookiesFromHost(""), 0); do_check_throws(function() { diff --git a/js/public/Date.h b/js/public/Date.h index cba0ea875..cab36a3de 100644 --- a/js/public/Date.h +++ b/js/public/Date.h @@ -134,6 +134,14 @@ NewDateObject(JSContext* cx, ClippedTime time); JS_PUBLIC_API(double) MakeDate(double year, unsigned month, unsigned day); +// Year is a year, month is 0-11, day is 1-based, and time is in milliseconds. +// The return value is a number of milliseconds since the epoch. +// +// Consistent with the MakeDate algorithm defined in ECMAScript, this value is +// *not* clipped! Use JS::TimeClip if you need a clipped date. +JS_PUBLIC_API(double) +MakeDate(double year, unsigned month, unsigned day, double time); + // Takes an integer number of milliseconds since the epoch and returns the // year. Can return NaN, and will do so if NaN is passed in. JS_PUBLIC_API(double) diff --git a/js/src/builtin/Intl.cpp b/js/src/builtin/Intl.cpp index 3a20c487b..cd808cb10 100644 --- a/js/src/builtin/Intl.cpp +++ b/js/src/builtin/Intl.cpp @@ -102,6 +102,18 @@ Char16ToUChar(char16_t* chars) MOZ_CRASH("Char16ToUChar: Intl API disabled"); } +inline char16_t* +UCharToChar16(UChar* chars) +{ + MOZ_CRASH("UCharToChar16: Intl API disabled"); +} + +inline const char16_t* +UCharToChar16(const UChar* chars) +{ + MOZ_CRASH("UCharToChar16: Intl API disabled"); +} + static int32_t u_strlen(const UChar* s) { @@ -356,6 +368,27 @@ enum UCalendarDateFields { UCAL_DAY_OF_MONTH = UCAL_DATE }; +enum UCalendarMonths { + UCAL_JANUARY, + UCAL_FEBRUARY, + UCAL_MARCH, + UCAL_APRIL, + UCAL_MAY, + UCAL_JUNE, + UCAL_JULY, + UCAL_AUGUST, + UCAL_SEPTEMBER, + UCAL_OCTOBER, + UCAL_NOVEMBER, + UCAL_DECEMBER, + UCAL_UNDECIMBER +}; + +enum UCalendarAMPMs { + UCAL_AM, + UCAL_PM +}; + static UCalendar* ucal_open(const UChar* zoneID, int32_t len, const char* locale, UCalendarType type, UErrorCode* status) @@ -420,6 +453,13 @@ ucal_getDefaultTimeZone(UChar* result, int32_t resultCapacity, UErrorCode* statu MOZ_CRASH("ucal_getDefaultTimeZone: Intl API disabled"); } +enum UDateTimePatternField { + UDATPG_YEAR_FIELD, + UDATPG_MONTH_FIELD, + UDATPG_WEEK_OF_YEAR_FIELD, + UDATPG_DAY_FIELD, +}; + typedef void* UDateTimePatternGenerator; static UDateTimePatternGenerator* @@ -436,6 +476,14 @@ udatpg_getBestPattern(UDateTimePatternGenerator* dtpg, const UChar* skeleton, MOZ_CRASH("udatpg_getBestPattern: Intl API disabled"); } +static const UChar * +udatpg_getAppendItemName(const UDateTimePatternGenerator *dtpg, + UDateTimePatternField field, + int32_t *pLength) +{ + MOZ_CRASH("udatpg_getAppendItemName: Intl API disabled"); +} + static void udatpg_close(UDateTimePatternGenerator* dtpg) { @@ -488,10 +536,46 @@ enum UDateFormatField { }; enum UDateFormatStyle { + UDAT_FULL, + UDAT_LONG, + UDAT_MEDIUM, + UDAT_SHORT, + UDAT_DEFAULT = UDAT_MEDIUM, UDAT_PATTERN = -2, UDAT_IGNORE = UDAT_PATTERN }; +enum UDateFormatSymbolType { + UDAT_ERAS, + UDAT_MONTHS, + UDAT_SHORT_MONTHS, + UDAT_WEEKDAYS, + UDAT_SHORT_WEEKDAYS, + UDAT_AM_PMS, + UDAT_LOCALIZED_CHARS, + UDAT_ERA_NAMES, + UDAT_NARROW_MONTHS, + UDAT_NARROW_WEEKDAYS, + UDAT_STANDALONE_MONTHS, + UDAT_STANDALONE_SHORT_MONTHS, + UDAT_STANDALONE_NARROW_MONTHS, + UDAT_STANDALONE_WEEKDAYS, + UDAT_STANDALONE_SHORT_WEEKDAYS, + UDAT_STANDALONE_NARROW_WEEKDAYS, + UDAT_QUARTERS, + UDAT_SHORT_QUARTERS, + UDAT_STANDALONE_QUARTERS, + UDAT_STANDALONE_SHORT_QUARTERS, + UDAT_SHORTER_WEEKDAYS, + UDAT_STANDALONE_SHORTER_WEEKDAYS, + UDAT_CYCLIC_YEARS_WIDE, + UDAT_CYCLIC_YEARS_ABBREVIATED, + UDAT_CYCLIC_YEARS_NARROW, + UDAT_ZODIAC_NAMES_WIDE, + UDAT_ZODIAC_NAMES_ABBREVIATED, + UDAT_ZODIAC_NAMES_NARROW +}; + static int32_t udat_countAvailable() { @@ -563,6 +647,13 @@ udat_close(UDateFormat* format) MOZ_CRASH("udat_close: Intl API disabled"); } +static int32_t +udat_getSymbols(const UDateFormat *fmt, UDateFormatSymbolType type, int32_t symbolIndex, + UChar *result, int32_t resultLength, UErrorCode *status) +{ + MOZ_CRASH("udat_getSymbols: Intl API disabled"); +} + #endif @@ -2921,6 +3012,296 @@ js::intl_GetCalendarInfo(JSContext* cx, unsigned argc, Value* vp) return true; } +template<size_t N> +inline bool +MatchPart(const char** pattern, const char (&part)[N]) +{ + if (strncmp(*pattern, part, N - 1)) + return false; + + *pattern += N - 1; + return true; +} + +bool +js::intl_ComputeDisplayNames(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 3); + // 1. Assert: locale is a string. + MOZ_ASSERT(args[0].isString()); + // 2. Assert: style is a string. + MOZ_ASSERT(args[1].isString()); + // 3. Assert: keys is an Array. + MOZ_ASSERT(args[2].isObject()); + + JSAutoByteString locale(cx, args[0].toString()); + if (!locale) + return false; + + JSAutoByteString style(cx, args[1].toString()); + if (!style) + return false; + + RootedArrayObject keys(cx, &args[2].toObject().as<ArrayObject>()); + if (!keys) + return false; + + // 4. Let result be ArrayCreate(0). + RootedArrayObject result(cx, NewDenseUnallocatedArray(cx, keys->length())); + if (!result) + return false; + + UErrorCode status = U_ZERO_ERROR; + + UDateFormat* fmt = + udat_open(UDAT_DEFAULT, UDAT_DEFAULT, icuLocale(locale.ptr()), + nullptr, 0, nullptr, 0, &status); + if (U_FAILURE(status)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + return false; + } + ScopedICUObject<UDateFormat, udat_close> datToClose(fmt); + + // UDateTimePatternGenerator will be needed for translations of date and + // time fields like "month", "week", "day" etc. + UDateTimePatternGenerator* dtpg = udatpg_open(icuLocale(locale.ptr()), &status); + if (U_FAILURE(status)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + return false; + } + ScopedICUObject<UDateTimePatternGenerator, udatpg_close> datPgToClose(dtpg); + + RootedValue keyValue(cx); + RootedString keyValStr(cx); + RootedValue wordVal(cx); + Vector<char16_t, INITIAL_CHAR_BUFFER_SIZE> chars(cx); + if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE)) + return false; + + // 5. For each element of keys, + for (uint32_t i = 0; i < keys->length(); i++) { + /** + * We iterate over keys array looking for paths that we have code + * branches for. + * + * For any unknown path branch, the wordVal will keep NullValue and + * we'll throw at the end. + */ + + if (!GetElement(cx, keys, keys, i, &keyValue)) + return false; + + JSAutoByteString pattern; + keyValStr = keyValue.toString(); + if (!pattern.encodeUtf8(cx, keyValStr)) + return false; + + wordVal.setNull(); + + // 5.a. Perform an implementation dependent algorithm to map a key to a + // corresponding display name. + const char* pat = pattern.ptr(); + + if (!MatchPart(&pat, "dates")) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + if (!MatchPart(&pat, "/")) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + if (MatchPart(&pat, "fields")) { + if (!MatchPart(&pat, "/")) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + UDateTimePatternField fieldType; + + if (MatchPart(&pat, "year")) { + fieldType = UDATPG_YEAR_FIELD; + } else if (MatchPart(&pat, "month")) { + fieldType = UDATPG_MONTH_FIELD; + } else if (MatchPart(&pat, "week")) { + fieldType = UDATPG_WEEK_OF_YEAR_FIELD; + } else if (MatchPart(&pat, "day")) { + fieldType = UDATPG_DAY_FIELD; + } else { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + // This part must be the final part with no trailing data. + if (*pat != '\0') { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + int32_t resultSize; + + const UChar* value = udatpg_getAppendItemName(dtpg, fieldType, &resultSize); + if (U_FAILURE(status)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + return false; + } + + JSString* word = NewStringCopyN<CanGC>(cx, UCharToChar16(value), resultSize); + if (!word) + return false; + + wordVal.setString(word); + } else if (MatchPart(&pat, "gregorian")) { + if (!MatchPart(&pat, "/")) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + UDateFormatSymbolType symbolType; + int32_t index; + + if (MatchPart(&pat, "months")) { + if (!MatchPart(&pat, "/")) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + if (equal(style, "narrow")) { + symbolType = UDAT_STANDALONE_NARROW_MONTHS; + } else if (equal(style, "short")) { + symbolType = UDAT_STANDALONE_SHORT_MONTHS; + } else { + MOZ_ASSERT(equal(style, "long")); + symbolType = UDAT_STANDALONE_MONTHS; + } + + if (MatchPart(&pat, "january")) { + index = UCAL_JANUARY; + } else if (MatchPart(&pat, "february")) { + index = UCAL_FEBRUARY; + } else if (MatchPart(&pat, "march")) { + index = UCAL_MARCH; + } else if (MatchPart(&pat, "april")) { + index = UCAL_APRIL; + } else if (MatchPart(&pat, "may")) { + index = UCAL_MAY; + } else if (MatchPart(&pat, "june")) { + index = UCAL_JUNE; + } else if (MatchPart(&pat, "july")) { + index = UCAL_JULY; + } else if (MatchPart(&pat, "august")) { + index = UCAL_AUGUST; + } else if (MatchPart(&pat, "september")) { + index = UCAL_SEPTEMBER; + } else if (MatchPart(&pat, "october")) { + index = UCAL_OCTOBER; + } else if (MatchPart(&pat, "november")) { + index = UCAL_NOVEMBER; + } else if (MatchPart(&pat, "december")) { + index = UCAL_DECEMBER; + } else { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + } else if (MatchPart(&pat, "weekdays")) { + if (!MatchPart(&pat, "/")) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + if (equal(style, "narrow")) { + symbolType = UDAT_STANDALONE_NARROW_WEEKDAYS; + } else if (equal(style, "short")) { + symbolType = UDAT_STANDALONE_SHORT_WEEKDAYS; + } else { + MOZ_ASSERT(equal(style, "long")); + symbolType = UDAT_STANDALONE_WEEKDAYS; + } + + if (MatchPart(&pat, "monday")) { + index = UCAL_MONDAY; + } else if (MatchPart(&pat, "tuesday")) { + index = UCAL_TUESDAY; + } else if (MatchPart(&pat, "wednesday")) { + index = UCAL_WEDNESDAY; + } else if (MatchPart(&pat, "thursday")) { + index = UCAL_THURSDAY; + } else if (MatchPart(&pat, "friday")) { + index = UCAL_FRIDAY; + } else if (MatchPart(&pat, "saturday")) { + index = UCAL_SATURDAY; + } else if (MatchPart(&pat, "sunday")) { + index = UCAL_SUNDAY; + } else { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + } else if (MatchPart(&pat, "dayperiods")) { + if (!MatchPart(&pat, "/")) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + symbolType = UDAT_AM_PMS; + + if (MatchPart(&pat, "am")) { + index = UCAL_AM; + } else if (MatchPart(&pat, "pm")) { + index = UCAL_PM; + } else { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + } else { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + // This part must be the final part with no trailing data. + if (*pat != '\0') { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + int32_t resultSize = + udat_getSymbols(fmt, symbolType, index, Char16ToUChar(chars.begin()), + INITIAL_CHAR_BUFFER_SIZE, &status); + if (status == U_BUFFER_OVERFLOW_ERROR) { + if (!chars.resize(resultSize)) + return false; + status = U_ZERO_ERROR; + udat_getSymbols(fmt, symbolType, index, Char16ToUChar(chars.begin()), + resultSize, &status); + } + if (U_FAILURE(status)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + return false; + } + + JSString* word = NewStringCopyN<CanGC>(cx, chars.begin(), resultSize); + if (!word) + return false; + + wordVal.setString(word); + } else { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + MOZ_ASSERT(wordVal.isString()); + + // 5.b. Append the result string to result. + if (!DefineElement(cx, result, i, wordVal)) + return false; + } + + // 6. Return result. + args.rval().setObject(*result); + return true; +} + /******************** Intl ********************/ const Class js::IntlClass = { diff --git a/js/src/builtin/Intl.h b/js/src/builtin/Intl.h index 54764605b..b2197060d 100644 --- a/js/src/builtin/Intl.h +++ b/js/src/builtin/Intl.h @@ -387,6 +387,48 @@ intl_FormatDateTime(JSContext* cx, unsigned argc, Value* vp); extern MOZ_MUST_USE bool intl_GetCalendarInfo(JSContext* cx, unsigned argc, Value* vp); +/** + * Returns an Array with CLDR-based fields display names. + * The function takes three arguments: + * + * locale + * BCP47 compliant locale string + * style + * A string with values: long or short or narrow + * keys + * An array or path-like strings that identify keys to be returned + * At the moment the following types of keys are supported: + * + * 'dates/fields/{year|month|week|day}' + * 'dates/gregorian/months/{january|...|december}' + * 'dates/gregorian/weekdays/{sunday|...|saturday}' + * 'dates/gregorian/dayperiods/{am|pm}' + * + * Example: + * + * let info = intl_ComputeDisplayNames( + * 'en-US', + * 'long', + * [ + * 'dates/fields/year', + * 'dates/gregorian/months/january', + * 'dates/gregorian/weekdays/monday', + * 'dates/gregorian/dayperiods/am', + * ] + * ); + * + * Returned value: + * + * [ + * 'year', + * 'January', + * 'Monday', + * 'AM' + * ] + */ +extern MOZ_MUST_USE bool +intl_ComputeDisplayNames(JSContext* cx, unsigned argc, Value* vp); + #if ENABLE_INTL_API /** * Cast char16_t* strings to UChar* strings used by ICU. @@ -402,6 +444,18 @@ Char16ToUChar(char16_t* chars) { return reinterpret_cast<UChar*>(chars); } + +inline char16_t* +UCharToChar16(UChar* chars) +{ + return reinterpret_cast<char16_t*>(chars); +} + +inline const char16_t* +UCharToChar16(const UChar* chars) +{ + return reinterpret_cast<const char16_t*>(chars); +} #endif // ENABLE_INTL_API } // namespace js diff --git a/js/src/js.msg b/js/src/js.msg index 8d492f523..a276dab94 100644 --- a/js/src/js.msg +++ b/js/src/js.msg @@ -474,6 +474,8 @@ MSG_DEF(JSMSG_INTL_OBJECT_NOT_INITED, 3, JSEXN_TYPEERR, "Intl.{0}.prototype.{1} MSG_DEF(JSMSG_INTL_OBJECT_REINITED, 0, JSEXN_TYPEERR, "can't initialize object twice as an object of an Intl constructor") MSG_DEF(JSMSG_INVALID_CURRENCY_CODE, 1, JSEXN_RANGEERR, "invalid currency code in NumberFormat(): {0}") MSG_DEF(JSMSG_INVALID_DIGITS_VALUE, 1, JSEXN_RANGEERR, "invalid digits value: {0}") +MSG_DEF(JSMSG_INVALID_KEYS_TYPE, 0, JSEXN_TYPEERR, "calendar info keys must be an object or undefined") +MSG_DEF(JSMSG_INVALID_KEY, 1, JSEXN_RANGEERR, "invalid key: {0}") MSG_DEF(JSMSG_INVALID_LANGUAGE_TAG, 1, JSEXN_RANGEERR, "invalid language tag: {0}") MSG_DEF(JSMSG_INVALID_LOCALES_ELEMENT, 0, JSEXN_TYPEERR, "invalid element in locales argument") MSG_DEF(JSMSG_INVALID_LOCALE_MATCHER, 1, JSEXN_RANGEERR, "invalid locale matcher in supportedLocalesOf(): {0}") diff --git a/js/src/jsdate.cpp b/js/src/jsdate.cpp index ccaeda2a3..00a8abf84 100755 --- a/js/src/jsdate.cpp +++ b/js/src/jsdate.cpp @@ -354,10 +354,22 @@ MakeDate(double day, double time) JS_PUBLIC_API(double) JS::MakeDate(double year, unsigned month, unsigned day) { + MOZ_ASSERT(month <= 11); + MOZ_ASSERT(day >= 1 && day <= 31); + return ::MakeDate(MakeDay(year, month, day), 0); } JS_PUBLIC_API(double) +JS::MakeDate(double year, unsigned month, unsigned day, double time) +{ + MOZ_ASSERT(month <= 11); + MOZ_ASSERT(day >= 1 && day <= 31); + + return ::MakeDate(MakeDay(year, month, day), time); +} + +JS_PUBLIC_API(double) JS::YearFromTime(double time) { return ::YearFromTime(time); diff --git a/js/src/shell/js.cpp b/js/src/shell/js.cpp index cc68c90d5..8d69ca942 100644 --- a/js/src/shell/js.cpp +++ b/js/src/shell/js.cpp @@ -906,6 +906,7 @@ AddIntlExtras(JSContext* cx, unsigned argc, Value* vp) static const JSFunctionSpec funcs[] = { JS_SELF_HOSTED_FN("getCalendarInfo", "Intl_getCalendarInfo", 1, 0), + JS_SELF_HOSTED_FN("getDisplayNames", "Intl_getDisplayNames", 2, 0), JS_FS_END }; diff --git a/js/src/tests/Intl/getDisplayNames.js b/js/src/tests/Intl/getDisplayNames.js new file mode 100644 index 000000000..ad2dbc940 --- /dev/null +++ b/js/src/tests/Intl/getDisplayNames.js @@ -0,0 +1,238 @@ +// |reftest| skip-if(!this.hasOwnProperty('Intl')||!this.hasOwnProperty('addIntlExtras')) +/* 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/. */ + +// Tests the getCalendarInfo function with a diverse set of arguments. + +/* + * Test if getDisplayNames return value matches expected values. + */ +function checkDisplayNames(names, expected) +{ + assertEq(Object.getPrototypeOf(names), Object.prototype); + + assertEq(names.locale, expected.locale); + assertEq(names.style, expected.style); + + const nameValues = names.values; + const expectedValues = expected.values; + + const nameValuesKeys = Object.getOwnPropertyNames(nameValues).sort(); + const expectedValuesKeys = Object.getOwnPropertyNames(expectedValues).sort(); + + assertEqArray(nameValuesKeys, expectedValuesKeys); + + for (let key of expectedValuesKeys) + assertEq(nameValues[key], expectedValues[key]); +} + +addIntlExtras(Intl); + +let gDN = Intl.getDisplayNames; + +assertEq(gDN.length, 2); + +checkDisplayNames(gDN('en-US', { +}), { + locale: 'en-US', + style: 'long', + values: {} +}); + +checkDisplayNames(gDN('en-US', { + keys: [ + 'dates/gregorian/weekdays/wednesday' + ], + style: 'narrow' +}), { + locale: 'en-US', + style: 'narrow', + values: { + 'dates/gregorian/weekdays/wednesday': 'W' + } +}); + +checkDisplayNames(gDN('en-US', { + keys: [ + 'dates/fields/year', + 'dates/fields/month', + 'dates/fields/week', + 'dates/fields/day', + 'dates/gregorian/months/january', + 'dates/gregorian/months/february', + 'dates/gregorian/months/march', + 'dates/gregorian/weekdays/tuesday' + ] +}), { + locale: 'en-US', + style: 'long', + values: { + 'dates/fields/year': 'year', + 'dates/fields/month': 'month', + 'dates/fields/week': 'week', + 'dates/fields/day': 'day', + 'dates/gregorian/months/january': 'January', + 'dates/gregorian/months/february': 'February', + 'dates/gregorian/months/march': 'March', + 'dates/gregorian/weekdays/tuesday': 'Tuesday', + } +}); + +checkDisplayNames(gDN('fr', { + keys: [ + 'dates/fields/year', + 'dates/fields/day', + 'dates/gregorian/months/october', + 'dates/gregorian/weekdays/saturday', + 'dates/gregorian/dayperiods/pm' + ] +}), { + locale: 'fr', + style: 'long', + values: { + 'dates/fields/year': 'année', + 'dates/fields/day': 'jour', + 'dates/gregorian/months/october': 'octobre', + 'dates/gregorian/weekdays/saturday': 'samedi', + 'dates/gregorian/dayperiods/pm': 'PM' + } +}); + +checkDisplayNames(gDN('it', { + style: 'short', + keys: [ + 'dates/gregorian/weekdays/thursday', + 'dates/gregorian/months/august', + 'dates/gregorian/dayperiods/am', + 'dates/fields/month', + ] +}), { + locale: 'it', + style: 'short', + values: { + 'dates/gregorian/weekdays/thursday': 'gio', + 'dates/gregorian/months/august': 'ago', + 'dates/gregorian/dayperiods/am': 'AM', + 'dates/fields/month': 'mese' + } +}); + +checkDisplayNames(gDN('ar', { + style: 'long', + keys: [ + 'dates/gregorian/weekdays/thursday', + 'dates/gregorian/months/august', + 'dates/gregorian/dayperiods/am', + 'dates/fields/month', + ] +}), { + locale: 'ar', + style: 'long', + values: { + 'dates/gregorian/weekdays/thursday': 'الخميس', + 'dates/gregorian/months/august': 'أغسطس', + 'dates/gregorian/dayperiods/am': 'ص', + 'dates/fields/month': 'الشهر' + } +}); + +/* Invalid input */ + +assertThrowsInstanceOf(() => { + gDN('en-US', { + style: '', + keys: [ + 'dates/gregorian/weekdays/thursday', + ] + }); +}, RangeError); + +assertThrowsInstanceOf(() => { + gDN('en-US', { + style: 'bogus', + keys: [ + 'dates/gregorian/weekdays/thursday', + ] + }); +}, RangeError); + +assertThrowsInstanceOf(() => { + gDN('foo-X', { + keys: [ + 'dates/gregorian/weekdays/thursday', + ] + }); +}, RangeError); + +const typeErrorKeys = [ + null, + 'string', + Symbol.iterator, + 15, + 1, + 3.7, + NaN, + Infinity +]; + +for (let keys of typeErrorKeys) { + assertThrowsInstanceOf(() => { + gDN('en-US', { + keys + }); + }, TypeError); +} + +const rangeErrorKeys = [ + [''], + ['foo'], + ['dates/foo'], + ['/dates/foo'], + ['dates/foo/foo'], + ['dates/fields'], + ['dates/fields/'], + ['dates/fields/foo'], + ['dates/fields/foo/month'], + ['/dates/foo/faa/bar/baz'], + ['dates///bar/baz'], + ['dates/gregorian'], + ['dates/gregorian/'], + ['dates/gregorian/foo'], + ['dates/gregorian/months'], + ['dates/gregorian/months/foo'], + ['dates/gregorian/weekdays'], + ['dates/gregorian/weekdays/foo'], + ['dates/gregorian/dayperiods'], + ['dates/gregorian/dayperiods/foo'], + ['dates/gregorian/months/الشهر'], + [3], + [null], + ['d', 'a', 't', 'e', 's'], + ['datesEXTRA'], + ['dates/fieldsEXTRA'], + ['dates/gregorianEXTRA'], + ['dates/gregorian/monthsEXTRA'], + ['dates/gregorian/weekdaysEXTRA'], + ['dates/fields/dayperiods/amEXTRA'], + ['dates/gregori\u1161n/months/january'], + ["dates/fields/year/"], + ["dates/fields/month/"], + ["dates/fields/week/"], + ["dates/fields/day/"], + ["dates/gregorian/months/january/"], + ["dates/gregorian/weekdays/saturday/"], + ["dates/gregorian/dayperiods/am/"], + ["dates/fields/months/january/"], +]; + +for (let keys of rangeErrorKeys) { + assertThrowsInstanceOf(() => { + gDN('en-US', { + keys + }); + }, RangeError); +} + +if (typeof reportCompare === 'function') + reportCompare(0, 0); diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index bf49f2268..9a8ec7679 100644 --- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -2477,6 +2477,7 @@ static const JSFunctionSpec intrinsic_functions[] = { JS_FN("intl_FormatDateTime", intl_FormatDateTime, 2,0), JS_FN("intl_FormatNumber", intl_FormatNumber, 2,0), JS_FN("intl_GetCalendarInfo", intl_GetCalendarInfo, 1,0), + JS_FN("intl_ComputeDisplayNames", intl_ComputeDisplayNames, 3,0), JS_FN("intl_IsValidTimeZoneName", intl_IsValidTimeZoneName, 1,0), JS_FN("intl_NumberFormat", intl_NumberFormat, 2,0), JS_FN("intl_NumberFormat_availableLocales", intl_NumberFormat_availableLocales, 0,0), diff --git a/layout/base/nsCSSFrameConstructor.cpp b/layout/base/nsCSSFrameConstructor.cpp index a118c38f9..f8c7f52a9 100644 --- a/layout/base/nsCSSFrameConstructor.cpp +++ b/layout/base/nsCSSFrameConstructor.cpp @@ -3658,13 +3658,13 @@ nsCSSFrameConstructor::FindInputData(Element* aElement, nsCSSAnonBoxes::buttonContent) }, // TODO: this is temporary until a frame is written: bug 635240. SIMPLE_INT_CREATE(NS_FORM_INPUT_NUMBER, NS_NewNumberControlFrame), - // TODO: this is temporary until a frame is written: bug 888320. - SIMPLE_INT_CREATE(NS_FORM_INPUT_DATE, NS_NewTextControlFrame), #if defined(MOZ_WIDGET_ANDROID) || defined(MOZ_WIDGET_GONK) // On Android/B2G, date/time input appears as a normal text box. SIMPLE_INT_CREATE(NS_FORM_INPUT_TIME, NS_NewTextControlFrame), + SIMPLE_INT_CREATE(NS_FORM_INPUT_DATE, NS_NewTextControlFrame), #else SIMPLE_INT_CREATE(NS_FORM_INPUT_TIME, NS_NewDateTimeControlFrame), + SIMPLE_INT_CREATE(NS_FORM_INPUT_DATE, NS_NewDateTimeControlFrame), #endif // TODO: this is temporary until a frame is written: bug 888320 SIMPLE_INT_CREATE(NS_FORM_INPUT_MONTH, NS_NewTextControlFrame), diff --git a/layout/forms/nsDateTimeControlFrame.cpp b/layout/forms/nsDateTimeControlFrame.cpp index df2e43986..fa22dceba 100644 --- a/layout/forms/nsDateTimeControlFrame.cpp +++ b/layout/forms/nsDateTimeControlFrame.cpp @@ -372,7 +372,8 @@ nsDateTimeControlFrame::AttributeChanged(int32_t aNameSpaceID, auto contentAsInputElem = static_cast<dom::HTMLInputElement*>(mContent); // If script changed the <input>'s type before setting these attributes // then we don't need to do anything since we are going to be reframed. - if (contentAsInputElem->GetType() == NS_FORM_INPUT_TIME) { + if (contentAsInputElem->GetType() == NS_FORM_INPUT_TIME || + contentAsInputElem->GetType() == NS_FORM_INPUT_DATE) { if (aAttribute == nsGkAtoms::value) { nsCOMPtr<nsIDateTimeInputArea> inputAreaContent = do_QueryInterface(mInputAreaContent); diff --git a/layout/reftests/forms/input/datetime/reftest.list b/layout/reftests/forms/input/datetime/reftest.list index 0ce2002bd..a62d56c7c 100644 --- a/layout/reftests/forms/input/datetime/reftest.list +++ b/layout/reftests/forms/input/datetime/reftest.list @@ -11,3 +11,14 @@ skip-if(!Android&&!B2G&&!Mulet) == time-simple-unthemed.html time-simple-untheme # type change skip-if(Android||B2G||Mulet) == to-time-from-other-type-unthemed.html time-simple-unthemed.html skip-if(Android||B2G||Mulet) == from-time-to-other-type-unthemed.html from-time-to-other-type-unthemed-ref.html + +# content should not overflow on small width/height +skip-if(Android) == time-small-width.html time-small-width-ref.html +skip-if(Android) == time-small-height.html time-small-height-ref.html +skip-if(Android) == time-small-width-height.html time-small-width-height-ref.html + +# content (text) should be left aligned +skip-if(Android) == time-content-left-aligned.html time-content-left-aligned-ref.html + +# reset button should be right aligned +skip-if(Android) fails-if(styloVsGecko) == time-reset-button-right-aligned.html time-reset-button-right-aligned-ref.html # bug 1372062 diff --git a/layout/reftests/forms/input/datetime/time-content-left-aligned-ref.html b/layout/reftests/forms/input/datetime/time-content-left-aligned-ref.html new file mode 100644 index 000000000..ad8be9adc --- /dev/null +++ b/layout/reftests/forms/input/datetime/time-content-left-aligned-ref.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> + <body> + <input type="time" style="width: 200px;"> + <!-- div to cover the right area --> + <div style="display:block; position:absolute; background-color:black; + top:0px; left:40px; width:200px; height:100px;"></div> + </body> +</html> diff --git a/layout/reftests/forms/input/datetime/time-content-left-aligned.html b/layout/reftests/forms/input/datetime/time-content-left-aligned.html new file mode 100644 index 000000000..aa910cddf --- /dev/null +++ b/layout/reftests/forms/input/datetime/time-content-left-aligned.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> + <body> + <input type="time" style="width: 50px;"> + <!-- div to cover the right area --> + <div style="display:block; position:absolute; background-color:black; + top:0px; left:40px; width:200px; height:100px;"></div> + </body> +</html> diff --git a/layout/reftests/forms/input/datetime/time-reset-button-right-aligned-ref.html b/layout/reftests/forms/input/datetime/time-reset-button-right-aligned-ref.html new file mode 100644 index 000000000..3d36f2068 --- /dev/null +++ b/layout/reftests/forms/input/datetime/time-reset-button-right-aligned-ref.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> + <body> + <input type="time" value="10:00" style="float: right; color: white;"> + <!-- div to cover the left area --> + <div style="display:block; position:absolute; background-color:black; + top:0px; right:30px; width:500px; height:100px;"></div> + </body> +</html> diff --git a/layout/reftests/forms/input/datetime/time-reset-button-right-aligned.html b/layout/reftests/forms/input/datetime/time-reset-button-right-aligned.html new file mode 100644 index 000000000..72d5cc140 --- /dev/null +++ b/layout/reftests/forms/input/datetime/time-reset-button-right-aligned.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> + <body> + <input type="time" value="10:00" style="width: 150px; float: right; + color: white;"> + <!-- div to cover the left area --> + <div style="display:block; position:absolute; background-color:black; + top:0px; right:30px; width:500px; height:100px;"></div> + </body> +</html> diff --git a/layout/reftests/forms/input/datetime/time-small-height-ref.html b/layout/reftests/forms/input/datetime/time-small-height-ref.html new file mode 100644 index 000000000..fcda93df9 --- /dev/null +++ b/layout/reftests/forms/input/datetime/time-small-height-ref.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> + <head> + <style> +input { + width: 200px; + height: 5px; + outline: 1px dotted black; + /* Disable baseline alignment, so that our y-position isn't influenced by the + * choice of font inside of input: */ + vertical-align: top; +} + </style> + </head> + <body> + <input> + </body> +</html> diff --git a/layout/reftests/forms/input/datetime/time-small-height.html b/layout/reftests/forms/input/datetime/time-small-height.html new file mode 100644 index 000000000..3044822fe --- /dev/null +++ b/layout/reftests/forms/input/datetime/time-small-height.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> + <head> + <style> +input { + width: 200px; + height: 5px; + outline: 1px dotted black; + color: white; + /* Disable baseline alignment, so that our y-position isn't influenced by the + * choice of font inside of input: */ + vertical-align: top; +} + </style> + </head> + <body> + <input type="time"> + </body> +</html> diff --git a/layout/reftests/forms/input/datetime/time-small-width-height-ref.html b/layout/reftests/forms/input/datetime/time-small-width-height-ref.html new file mode 100644 index 000000000..0979243db --- /dev/null +++ b/layout/reftests/forms/input/datetime/time-small-width-height-ref.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> + <head> + <style> +input { + width: 8px; + height: 8px; + outline: 1px dotted black; + /* Disable baseline alignment, so that our y-position isn't influenced by the + * choice of font inside of input: */ + vertical-align: top; +} + </style> + </head> + <body> + <input> + </body> +</html> diff --git a/layout/reftests/forms/input/datetime/time-small-width-height.html b/layout/reftests/forms/input/datetime/time-small-width-height.html new file mode 100644 index 000000000..a221b2819 --- /dev/null +++ b/layout/reftests/forms/input/datetime/time-small-width-height.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> + <head> + <style> +input { + width: 8px; + height: 8px; + outline: 1px dotted black; + color: white; + /* Disable baseline alignment, so that our y-position isn't influenced by the + * choice of font inside of input: */ + vertical-align: top; +} + </style> + </head> + <body> + <input type="time"> + </body> +</html> diff --git a/layout/reftests/forms/input/datetime/time-small-width-ref.html b/layout/reftests/forms/input/datetime/time-small-width-ref.html new file mode 100644 index 000000000..2379c7080 --- /dev/null +++ b/layout/reftests/forms/input/datetime/time-small-width-ref.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> + <head> + <style> +input { + width: 10px; + height: 1.5em; + outline: 1px dotted black; + background: white; + /* Disable baseline alignment, so that our y-position isn't influenced by the + * choice of font inside of input: */ + vertical-align: top; +} + </style> + </head> + <body> + <input> + </body> +</html> diff --git a/layout/reftests/forms/input/datetime/time-small-width.html b/layout/reftests/forms/input/datetime/time-small-width.html new file mode 100644 index 000000000..f76f7fdfa --- /dev/null +++ b/layout/reftests/forms/input/datetime/time-small-width.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html> + <head> + <style> +input { + width: 10px; + height: 1.5em; + outline: 1px dotted black; + color: white; + background: white; + /* Disable baseline alignment, so that our y-position isn't influenced by the + * choice of font inside of input: */ + vertical-align: top; +} + </style> + </head> + <body> + <input type="time"> + </body> +</html> diff --git a/layout/style/res/forms.css b/layout/style/res/forms.css index f045540b1..e7566e183 100644 --- a/layout/style/res/forms.css +++ b/layout/style/res/forms.css @@ -1135,3 +1135,8 @@ input[type="number"] > div > div > div:hover { /* give some indication of hover state for the up/down buttons */ background-color: lightblue; } + +input[type="date"], +input[type="time"] { + overflow: hidden !important; +} diff --git a/layout/style/res/html.css b/layout/style/res/html.css index a779461de..bc3f08210 100644 --- a/layout/style/res/html.css +++ b/layout/style/res/html.css @@ -774,6 +774,11 @@ input[type="time"] > xul|datetimebox { -moz-binding: url("chrome://global/content/bindings/datetimebox.xml#time-input"); } +input[type="date"] > xul|datetimebox { + display: flex; + -moz-binding: url("chrome://global/content/bindings/datetimebox.xml#date-input"); +} + /* details & summary */ /* Need to revert Bug 1259889 Part 2 when removing details preference. */ @supports -moz-bool-pref("dom.details_element.enabled") { diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index 59f8de9ac..e3e23550a 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -1179,10 +1179,13 @@ pref("dom.forms.number", true); // platforms which don't have a color picker implemented yet. pref("dom.forms.color", true); -// Support for input type=date, time, month, week and datetime-local. By -// default, disabled. +// Support for input type=date and type=time. By default, disabled. pref("dom.forms.datetime", false); +// Support for input type=month, type=week and type=datetime-local. By default, +// disabled. +pref("dom.forms.datetime.others", false); + // Enable time picker UI. By default, disabled. pref("dom.forms.datetime.timepicker", false); diff --git a/netwerk/base/nsStandardURL.cpp b/netwerk/base/nsStandardURL.cpp index bc1350f28..21c4cf7fd 100644 --- a/netwerk/base/nsStandardURL.cpp +++ b/netwerk/base/nsStandardURL.cpp @@ -781,11 +781,13 @@ nsStandardURL::BuildNormalizedSpec(const char *spec) i = AppendSegmentToBuf(buf, i, spec, username, mUsername, &encUsername, useEncUsername, &diff); ShiftFromPassword(diff); - if (password.mLen >= 0) { + if (password.mLen > 0) { buf[i++] = ':'; i = AppendSegmentToBuf(buf, i, spec, password, mPassword, &encPassword, useEncPassword, &diff); ShiftFromHost(diff); + } else { + mPassword.mLen = -1; } buf[i++] = '@'; } @@ -1483,6 +1485,11 @@ nsStandardURL::SetSpec(const nsACString &input) rv = BuildNormalizedSpec(spec); } + // Make sure that a URLTYPE_AUTHORITY has a non-empty hostname. + if (mURLType == URLTYPE_AUTHORITY && mHost.mLen == -1) { + rv = NS_ERROR_MALFORMED_URI; + } + if (NS_FAILED(rv)) { Clear(); // If parsing the spec has failed, restore the old URL @@ -1616,7 +1623,7 @@ nsStandardURL::SetUserPass(const nsACString &input) usernameLen), esc_Username | esc_AlwaysCopy, buf, ignoredOut); - if (passwordLen >= 0) { + if (passwordLen > 0) { buf.Append(':'); passwordLen = encoder.EncodeSegmentCount(userpass.get(), URLSegment(passwordPos, @@ -1624,6 +1631,8 @@ nsStandardURL::SetUserPass(const nsACString &input) esc_Password | esc_AlwaysCopy, buf, ignoredOut); + } else { + passwordLen = -1; } if (mUsername.mLen < 0) buf.Append('@'); @@ -1654,8 +1663,10 @@ nsStandardURL::SetUserPass(const nsACString &input) // update positions and lengths mUsername.mLen = usernameLen; mPassword.mLen = passwordLen; - if (passwordLen) + if (passwordLen > 0) { mPassword.mPos = mUsername.mPos + mUsername.mLen + 1; + } + return NS_OK; } @@ -3092,20 +3103,26 @@ nsStandardURL::SetFile(nsIFile *file) rv = net_GetURLSpecFromFile(file, url); if (NS_FAILED(rv)) return rv; - SetSpec(url); + uint32_t oldURLType = mURLType; + uint32_t oldDefaultPort = mDefaultPort; + rv = Init(nsIStandardURL::URLTYPE_NO_AUTHORITY, -1, url, nullptr, nullptr); - rv = Init(mURLType, mDefaultPort, url, nullptr, nullptr); + if (NS_FAILED(rv)) { + // Restore the old url type and default port if the call to Init fails. + mURLType = oldURLType; + mDefaultPort = oldDefaultPort; + return rv; + } // must clone |file| since its value is not guaranteed to remain constant - if (NS_SUCCEEDED(rv)) { - InvalidateCache(); - if (NS_FAILED(file->Clone(getter_AddRefs(mFile)))) { - NS_WARNING("nsIFile::Clone failed"); - // failure to clone is not fatal (GetFile will generate mFile) - mFile = nullptr; - } + InvalidateCache(); + if (NS_FAILED(file->Clone(getter_AddRefs(mFile)))) { + NS_WARNING("nsIFile::Clone failed"); + // failure to clone is not fatal (GetFile will generate mFile) + mFile = nullptr; } - return rv; + + return NS_OK; } //---------------------------------------------------------------------------- diff --git a/netwerk/protocol/http/nsHttpHandler.h b/netwerk/protocol/http/nsHttpHandler.h index d51662db9..35b14a511 100644 --- a/netwerk/protocol/http/nsHttpHandler.h +++ b/netwerk/protocol/http/nsHttpHandler.h @@ -489,8 +489,8 @@ private: nsCString mCompatGecko; bool mCompatGeckoEnabled; nsCString mCompatFirefox; - nsCString mCompatFirefoxVersion; bool mCompatFirefoxEnabled; + nsCString mCompatFirefoxVersion; nsXPIDLCString mCompatDevice; nsCString mDeviceModelId; diff --git a/netwerk/test/unit/test_URIs.js b/netwerk/test/unit/test_URIs.js index b68c4f787..1cad7768f 100644 --- a/netwerk/test/unit/test_URIs.js +++ b/netwerk/test/unit/test_URIs.js @@ -92,18 +92,6 @@ var gTests = [ ref: "", relativeURI: "data/text/plain,2", nsIURL: true, nsINestedURI: false }, - { spec: "ftp://", - scheme: "ftp", - prePath: "ftp://", - path: "/", - ref: "", - nsIURL: true, nsINestedURI: false }, - { spec: "ftp:///", - scheme: "ftp", - prePath: "ftp://", - path: "/", - ref: "", - nsIURL: true, nsINestedURI: false }, { spec: "ftp://ftp.mozilla.org/pub/mozilla.org/README", scheme: "ftp", prePath: "ftp://ftp.mozilla.org", @@ -121,7 +109,7 @@ var gTests = [ nsIURL: true, nsINestedURI: false }, { spec: "ftp://foo:@ftp.mozilla.org:100/pub/mozilla.org/README", scheme: "ftp", - prePath: "ftp://foo:@ftp.mozilla.org:100", + prePath: "ftp://foo@ftp.mozilla.org:100", port: 100, username: "foo", password: "", @@ -135,18 +123,6 @@ var gTests = [ path: "//mozilla.org/", ref: "", nsIURL: false, nsINestedURI: false }, - { spec: "http://", - scheme: "http", - prePath: "http://", - path: "/", - ref: "", - nsIURL: true, nsINestedURI: false }, - { spec: "http:///", - scheme: "http", - prePath: "http://", - path: "/", - ref: "", - nsIURL: true, nsINestedURI: false }, { spec: "http://www.example.com/", scheme: "http", prePath: "http://www.example.com", diff --git a/netwerk/test/unit/test_standardurl.js b/netwerk/test/unit/test_standardurl.js index c4d44f41f..4cc2f393e 100644 --- a/netwerk/test/unit/test_standardurl.js +++ b/netwerk/test/unit/test_standardurl.js @@ -251,6 +251,17 @@ add_test(function test_escapeBrackets() run_next_test(); }); +add_test(function test_escapeQuote() +{ + var url = stringToURL("http://example.com/#'"); + do_check_eq(url.spec, "http://example.com/#'"); + do_check_eq(url.ref, "'"); + url.ref = "test'test"; + do_check_eq(url.spec, "http://example.com/#test'test"); + do_check_eq(url.ref, "test'test"); + run_next_test(); +}); + add_test(function test_apostropheEncoding() { // For now, single quote is escaped everywhere _except_ the path. @@ -335,6 +346,14 @@ add_test(function test_backslashReplacement() run_next_test(); }); +add_test(function test_authority_host() +{ + Assert.throws(() => { stringToURL("http:"); }, "TYPE_AUTHORITY should have host"); + Assert.throws(() => { stringToURL("http:///"); }, "TYPE_AUTHORITY should have host"); + + run_next_test(); +}); + add_test(function test_trim_C0_and_space() { var url = stringToURL("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f http://example.com/ \x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f "); @@ -453,3 +472,23 @@ add_test(function test_invalidHostChars() { // hostname separators, so there is no way to set them and fail. run_next_test(); }); + +add_test(function test_emptyPassword() { + var url = stringToURL("http://a:@example.com"); + do_check_eq(url.spec, "http://a@example.com/"); + url.password = "pp"; + do_check_eq(url.spec, "http://a:pp@example.com/"); + url.password = ""; + do_check_eq(url.spec, "http://a@example.com/"); + url.userPass = "xxx:"; + do_check_eq(url.spec, "http://xxx@example.com/"); + url.password = "zzzz"; + do_check_eq(url.spec, "http://xxx:zzzz@example.com/"); + url.userPass = "xxxxx:yyyyyy"; + do_check_eq(url.spec, "http://xxxxx:yyyyyy@example.com/"); + url.userPass = "z:"; + do_check_eq(url.spec, "http://z@example.com/"); + url.password = "ppppppppppp"; + do_check_eq(url.spec, "http://z:ppppppppppp@example.com/"); + run_next_test(); +}); diff --git a/services/fxaccounts/tests/xpcshell/test_oauth_grant_client.js b/services/fxaccounts/tests/xpcshell/test_oauth_grant_client.js index 244b79a5e..710a65ee5 100644 --- a/services/fxaccounts/tests/xpcshell/test_oauth_grant_client.js +++ b/services/fxaccounts/tests/xpcshell/test_oauth_grant_client.js @@ -143,7 +143,7 @@ add_test(function serverErrorResponse () { add_test(function networkErrorResponse () { let client = new FxAccountsOAuthGrantClient({ - serverURL: "http://", + serverURL: "http://domain.dummy", client_id: "abc123" }); Services.prefs.setBoolPref("identity.fxaccounts.skipDeviceRegistration", true); diff --git a/services/fxaccounts/tests/xpcshell/test_profile_client.js b/services/fxaccounts/tests/xpcshell/test_profile_client.js index 2243da3aa..20ff6efc6 100644 --- a/services/fxaccounts/tests/xpcshell/test_profile_client.js +++ b/services/fxaccounts/tests/xpcshell/test_profile_client.js @@ -268,7 +268,7 @@ add_test(function server401ResponsePersists () { add_test(function networkErrorResponse () { let client = new FxAccountsProfileClient({ - serverURL: "http://", + serverURL: "http://domain.dummy", fxa: mockFxa, }); client.fetchProfile() diff --git a/testing/marionette/interaction.js b/testing/marionette/interaction.js index c8275665d..2392485d7 100644 --- a/testing/marionette/interaction.js +++ b/testing/marionette/interaction.js @@ -76,6 +76,30 @@ const SELECTED_PROPERTY_SUPPORTED_XUL = new Set([ "TAB", ]); +/** + * Common form controls that user can change the value property interactively. + */ +const COMMON_FORM_CONTROLS = new Set([ + "input", + "textarea", + "select", +]); + +/** + * Input elements that do not fire "input" and "change" events when value + * property changes. + */ +const INPUT_TYPES_NO_EVENT = new Set([ + "checkbox", + "radio", + "file", + "hidden", + "image", + "reset", + "button", + "submit", +]); + this.interaction = {}; /** @@ -339,6 +363,32 @@ interaction.uploadFile = function (el, path) { }; /** + * Sets a form element's value. + * + * @param {DOMElement} el + * An form element, e.g. input, textarea, etc. + * @param {string} value + * The value to be set. + * + * @throws TypeError + * If |el| is not an supported form element. + */ +interaction.setFormControlValue = function* (el, value) { + if (!COMMON_FORM_CONTROLS.has(el.localName)) { + throw new TypeError("This function is for form elements only"); + } + + el.value = value; + + if (INPUT_TYPES_NO_EVENT.has(el.type)) { + return; + } + + event.input(el); + event.change(el); +}; + +/** * Send keys to element. * * @param {DOMElement|XULElement} el diff --git a/testing/marionette/listener.js b/testing/marionette/listener.js index b64eb378d..619ac249d 100644 --- a/testing/marionette/listener.js +++ b/testing/marionette/listener.js @@ -30,6 +30,7 @@ Cu.import("chrome://marionette/content/session.js"); Cu.import("chrome://marionette/content/simpletest.js"); Cu.import("resource://gre/modules/FileUtils.jsm"); +Cu.import("resource://gre/modules/Preferences.jsm"); Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); @@ -1465,6 +1466,9 @@ function* sendKeysToElement(id, val) { if (el.type == "file") { let path = val.join(""); yield interaction.uploadFile(el, path); + } else if ((el.type == "date" || el.type == "time") && + Preferences.get("dom.forms.datetime")) { + yield interaction.setFormControlValue(el, val); } else { yield interaction.sendKeysToElement( el, val, false, capabilities.get("moz:accessibilityChecks")); diff --git a/testing/profiles/prefs_general.js b/testing/profiles/prefs_general.js index 91218b5f3..ac2c1e077 100644 --- a/testing/profiles/prefs_general.js +++ b/testing/profiles/prefs_general.js @@ -12,6 +12,7 @@ user_pref("dom.experimental_forms", true); // on for testing user_pref("dom.forms.number", true); // on for testing user_pref("dom.forms.color", true); // on for testing user_pref("dom.forms.datetime", true); // on for testing +user_pref("dom.forms.datetime.others", true); // on for testing user_pref("dom.max_script_run_time", 0); // no slow script dialogs user_pref("hangmonitor.timeout", 0); // no hang monitor user_pref("dom.max_chrome_script_run_time", 0); diff --git a/testing/web-platform/meta/MANIFEST.json b/testing/web-platform/meta/MANIFEST.json index 65626d6b8..ca574833b 100644 --- a/testing/web-platform/meta/MANIFEST.json +++ b/testing/web-platform/meta/MANIFEST.json @@ -39138,6 +39138,18 @@ "url": "/2dcontext/drawing-images-to-the-canvas/drawimage_html_image_9.html" } ], + "2dcontext/drawing-images-to-the-canvas/drawimage_svg_image_1.html": [ + { + "path": "2dcontext/drawing-images-to-the-canvas/drawimage_svg_image_1.html", + "references": [ + [ + "/2dcontext/drawing-images-to-the-canvas/drawimage_svg_image_1_ref.html", + "==" + ] + ], + "url": "/2dcontext/drawing-images-to-the-canvas/drawimage_svg_image_1.html" + } + ], "2dcontext/line-styles/canvas_linestyles_linecap_001.htm": [ { "path": "2dcontext/line-styles/canvas_linestyles_linecap_001.htm", diff --git a/testing/web-platform/meta/XMLHttpRequest/open-url-bogus.htm.ini b/testing/web-platform/meta/XMLHttpRequest/open-url-bogus.htm.ini deleted file mode 100644 index ef7b5d910..000000000 --- a/testing/web-platform/meta/XMLHttpRequest/open-url-bogus.htm.ini +++ /dev/null @@ -1,11 +0,0 @@ -[open-url-bogus.htm] - type: testharness - [XMLHttpRequest: open() - bogus URLs (http:)] - expected: FAIL - - [XMLHttpRequest: open() - bogus URLs (ftp:)] - expected: FAIL - - [XMLHttpRequest: open() - bogus URLs (http:////////////)] - expected: FAIL - diff --git a/testing/web-platform/meta/fetch/api/headers/headers-basic.html.ini b/testing/web-platform/meta/fetch/api/headers/headers-basic.html.ini index 27927b1e9..8c4cddf9c 100644 --- a/testing/web-platform/meta/fetch/api/headers/headers-basic.html.ini +++ b/testing/web-platform/meta/fetch/api/headers/headers-basic.html.ini @@ -1,17 +1,3 @@ [headers-basic.html] type: testharness - [Check keys method] - expected: FAIL - - [Check values method] - expected: FAIL - - [Check entries method] - expected: FAIL - - [Check Symbol.iterator method] - expected: FAIL - - [Check forEach method] - expected: FAIL diff --git a/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-checkValidity.html.ini b/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-checkValidity.html.ini index a8247d5a0..23bd8642c 100644 --- a/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-checkValidity.html.ini +++ b/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-checkValidity.html.ini @@ -32,25 +32,3 @@ [[INPUT in DATETIME status\] The datetime type must be supported.] expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] The datetime-local type must be supported.] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] suffering from an overflow] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] suffering from an overflow (in a form)] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] suffering from an underflow] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] suffering from an underflow (in a form)] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] suffering from a step mismatch] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] suffering from a step mismatch (in a form)] - expected: FAIL - diff --git a/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-reportValidity.html.ini b/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-reportValidity.html.ini index 223667997..5b373cfee 100644 --- a/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-reportValidity.html.ini +++ b/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-reportValidity.html.ini @@ -39,24 +39,3 @@ [[INPUT in DATETIME status\] The datetime type must be supported.] expected: FAIL - [[INPUT in DATETIME-LOCAL status\] The datetime-local type must be supported.] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] suffering from an overflow] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] suffering from an overflow (in a form)] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] suffering from an underflow] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] suffering from an underflow (in a form)] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] suffering from a step mismatch] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] suffering from a step mismatch (in a form)] - expected: FAIL - diff --git a/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-rangeOverflow.html.ini b/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-rangeOverflow.html.ini index 6af2a360e..d8c650632 100644 --- a/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-rangeOverflow.html.ini +++ b/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-rangeOverflow.html.ini @@ -3,21 +3,3 @@ [[INPUT in DATETIME status\] The datetime type must be supported.] expected: FAIL - [[INPUT in DATETIME-LOCAL status\] The datetime-local type must be supported.] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] The value is greater than max] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] The value is greater than max(with millisecond in 1 digit)] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] The value is greater than max(with millisecond in 2 digits)] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] The value is greater than max(with millisecond in 3 digits)] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] The value is greater than max(Year is 10000 should be valid)] - expected: FAIL - diff --git a/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-rangeUnderflow.html.ini b/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-rangeUnderflow.html.ini index 344ee0039..87f6f964e 100644 --- a/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-rangeUnderflow.html.ini +++ b/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-rangeUnderflow.html.ini @@ -3,21 +3,3 @@ [[INPUT in DATETIME status\] The datetime type must be supported.] expected: FAIL - [[INPUT in DATETIME-LOCAL status\] The datetime-local type must be supported.] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] The value is less than min] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] The value is less than min(with millisecond in 1 digit)] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] The value is less than min(with millisecond in 2 digits)] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] The value is less than min(with millisecond in 3 digits)] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] The value is less than min(Year is 10000 should be valid)] - expected: FAIL - diff --git a/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-stepMismatch.html.ini b/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-stepMismatch.html.ini index 0c33bdcbe..527760e60 100644 --- a/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-stepMismatch.html.ini +++ b/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-stepMismatch.html.ini @@ -2,7 +2,3 @@ type: testharness [[INPUT in DATETIME status\] The datetime type must be supported.] expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] The value must mismatch the step] - expected: FAIL - diff --git a/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-valid.html.ini b/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-valid.html.ini index 1cddcd033..8eb7940d9 100644 --- a/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-valid.html.ini +++ b/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-valid.html.ini @@ -23,16 +23,3 @@ [[INPUT in MONTH status\] validity.valid must be false if validity.stepMismatch is true] expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] The datetime-local type must be supported.] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] validity.valid must be false if validity.rangeOverflow is true] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] validity.valid must be false if validity.rangeUnderflow is true] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] validity.valid must be false if validity.stepMismatch is true] - expected: FAIL - diff --git a/testing/web-platform/meta/html/semantics/selectors/pseudo-classes/inrange-outofrange.html.ini b/testing/web-platform/meta/html/semantics/selectors/pseudo-classes/inrange-outofrange.html.ini deleted file mode 100644 index 5e2525ac2..000000000 --- a/testing/web-platform/meta/html/semantics/selectors/pseudo-classes/inrange-outofrange.html.ini +++ /dev/null @@ -1,20 +0,0 @@ -[inrange-outofrange.html] - type: testharness - [':in-range' matches all elements that are candidates for constraint validation, have range limitations, and that are neither suffering from an underflow nor suffering from an overflow] - expected: FAIL - - [':in-range' update number1's value < min] - expected: FAIL - - [':in-range' update number3's min < value] - expected: FAIL - - [':out-of-range' matches all elements that are candidates for constraint validation, have range limitations, and that are either suffering from an underflow or suffering from an overflow] - expected: FAIL - - [':out-of-range' update number1's value < min] - expected: FAIL - - [':out-of-range' update number3's min < value] - expected: FAIL - diff --git a/testing/web-platform/meta/url/url-constructor.html.ini b/testing/web-platform/meta/url/url-constructor.html.ini index 6da03043b..22fddbc15 100644 --- a/testing/web-platform/meta/url/url-constructor.html.ini +++ b/testing/web-platform/meta/url/url-constructor.html.ini @@ -219,6 +219,3 @@ [Parsing: <http://example.com/foo/%2e./%2e%2e/.%2e/%2e.bar> against <about:blank>] expected: FAIL - [Parsing: <http:> against <https://example.org/foo/bar>] - expected: FAIL - diff --git a/testing/web-platform/tests/2dcontext/drawing-images-to-the-canvas/drawimage_html_image_1.html b/testing/web-platform/tests/2dcontext/drawing-images-to-the-canvas/drawimage_svg_image_1.html index b9de85a97..74a00e037 100644 --- a/testing/web-platform/tests/2dcontext/drawing-images-to-the-canvas/drawimage_html_image_1.html +++ b/testing/web-platform/tests/2dcontext/drawing-images-to-the-canvas/drawimage_svg_image_1.html @@ -1,6 +1,6 @@ <!DOCTYPE html> <meta charset="utf-8"> -<link rel=match href=drawimage_html_image_1_ref.html> +<link rel=match href=drawimage_svg_image_1_ref.html> <style> html, body { margin: 0; @@ -13,8 +13,8 @@ var sourceWidth = 100; var sourceHeight = 100; var smoothingEnabled = false; var destCanvas = document.getElementById('dest'); -var sourceImg = document.createElement('img'); -sourceImg.src = '../2x2.png' +var sourceImg = document.createElementNS('http://www.w3.org/2000/svg', 'image'); +sourceImg.setAttributeNS('http://www.w3.org/1999/xlink', 'href', '../2x2.png'); sourceImg.width = sourceWidth; sourceImg.height = sourceHeight; diff --git a/testing/web-platform/tests/2dcontext/drawing-images-to-the-canvas/drawimage_html_image_1_ref.html b/testing/web-platform/tests/2dcontext/drawing-images-to-the-canvas/drawimage_svg_image_1_ref.html index 60545df17..60545df17 100644 --- a/testing/web-platform/tests/2dcontext/drawing-images-to-the-canvas/drawimage_html_image_1_ref.html +++ b/testing/web-platform/tests/2dcontext/drawing-images-to-the-canvas/drawimage_svg_image_1_ref.html diff --git a/toolkit/components/autocomplete/nsAutoCompleteController.cpp b/toolkit/components/autocomplete/nsAutoCompleteController.cpp index 5d69ea1a3..9ca382fe5 100644 --- a/toolkit/components/autocomplete/nsAutoCompleteController.cpp +++ b/toolkit/components/autocomplete/nsAutoCompleteController.cpp @@ -1637,58 +1637,72 @@ nsAutoCompleteController::ProcessResult(int32_t aSearchIndex, nsIAutoCompleteRes MOZ_ASSERT(mResults.Count() >= aSearchIndex + 1, "aSearchIndex should always be valid for mResults"); - uint32_t oldRowCount = mRowCount; - // If the search failed, increase the match count to include the error - // description. - if (searchResult == nsIAutoCompleteResult::RESULT_FAILURE) { - nsAutoString error; - aResult->GetErrorDescription(error); - if (!error.IsEmpty()) { - ++mRowCount; - if (mTree) { - mTree->RowCountChanged(oldRowCount, 1); + bool isTypeAheadResult = false; + aResult->GetTypeAheadResult(&isTypeAheadResult); + + if (!isTypeAheadResult) { + uint32_t oldRowCount = mRowCount; + // If the search failed, increase the match count to include the error + // description. + if (searchResult == nsIAutoCompleteResult::RESULT_FAILURE) { + nsAutoString error; + aResult->GetErrorDescription(error); + if (!error.IsEmpty()) { + ++mRowCount; + if (mTree) { + mTree->RowCountChanged(oldRowCount, 1); + } } - } - } else if (searchResult == nsIAutoCompleteResult::RESULT_SUCCESS || - searchResult == nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING) { - // Increase the match count for all matches in this result. - uint32_t totalMatchCount = 0; - for (uint32_t i = 0; i < mResults.Length(); i++) { - nsIAutoCompleteResult* result = mResults.SafeObjectAt(i); - if (result) { - uint32_t matchCount = 0; - result->GetMatchCount(&matchCount); - totalMatchCount += matchCount; + } else if (searchResult == nsIAutoCompleteResult::RESULT_SUCCESS || + searchResult == nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING) { + // Increase the match count for all matches in this result. + uint32_t totalMatchCount = 0; + for (uint32_t i = 0; i < mResults.Length(); i++) { + nsIAutoCompleteResult* result = mResults.SafeObjectAt(i); + if (result) { + // not all results implement this, so it can likely fail. + bool typeAhead = false; + result->GetTypeAheadResult(&typeAhead); + if (!typeAhead) { + uint32_t matchCount = 0; + result->GetMatchCount(&matchCount); + totalMatchCount += matchCount; + } + } } - } - uint32_t delta = totalMatchCount - oldRowCount; + uint32_t delta = totalMatchCount - oldRowCount; - mRowCount += delta; - if (mTree) { - mTree->RowCountChanged(oldRowCount, delta); + mRowCount += delta; + if (mTree) { + mTree->RowCountChanged(oldRowCount, delta); + } } - } - // Try to autocomplete the default index for this search. - // Do this before invalidating so the binding knows about it. - CompleteDefaultIndex(aSearchIndex); + // Try to autocomplete the default index for this search. + // Do this before invalidating so the binding knows about it. + CompleteDefaultIndex(aSearchIndex); - // Refresh the popup view to display the new search results - nsCOMPtr<nsIAutoCompletePopup> popup; - input->GetPopup(getter_AddRefs(popup)); - NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE); - popup->Invalidate(nsIAutoCompletePopup::INVALIDATE_REASON_NEW_RESULT); + // Refresh the popup view to display the new search results + nsCOMPtr<nsIAutoCompletePopup> popup; + input->GetPopup(getter_AddRefs(popup)); + NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE); + popup->Invalidate(nsIAutoCompletePopup::INVALIDATE_REASON_NEW_RESULT); - uint32_t minResults; - input->GetMinResultsForPopup(&minResults); + uint32_t minResults; + input->GetMinResultsForPopup(&minResults); - // Make sure the popup is open, if necessary, since we now have at least one - // search result ready to display. Don't force the popup closed if we might - // get results in the future to avoid unnecessarily canceling searches. - if (mRowCount || !minResults) { - OpenPopup(); - } else if (mSearchesOngoing == 0) { - ClosePopup(); + // Make sure the popup is open, if necessary, since we now have at least one + // search result ready to display. Don't force the popup closed if we might + // get results in the future to avoid unnecessarily canceling searches. + if (mRowCount || !minResults) { + OpenPopup(); + } else if (mSearchesOngoing == 0) { + ClosePopup(); + } + } else if (searchResult == nsIAutoCompleteResult::RESULT_SUCCESS || + searchResult == nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING) { + // Try to autocomplete the default index for this search. + CompleteDefaultIndex(aSearchIndex); } return NS_OK; @@ -2033,14 +2047,20 @@ nsAutoCompleteController::RowIndexToSearch(int32_t aRowIndex, int32_t *aSearchIn uint32_t rowCount = 0; - uint16_t searchResult; - result->GetSearchResult(&searchResult); + // Skip past the result completely if it is marked as hidden + bool isTypeAheadResult = false; + result->GetTypeAheadResult(&isTypeAheadResult); - // Find out how many results were provided by the - // current nsIAutoCompleteSearch. - if (searchResult == nsIAutoCompleteResult::RESULT_SUCCESS || - searchResult == nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING) { - result->GetMatchCount(&rowCount); + if (!isTypeAheadResult) { + uint16_t searchResult; + result->GetSearchResult(&searchResult); + + // Find out how many results were provided by the + // current nsIAutoCompleteSearch. + if (searchResult == nsIAutoCompleteResult::RESULT_SUCCESS || + searchResult == nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING) { + result->GetMatchCount(&rowCount); + } } // If the given row index is within the results range diff --git a/toolkit/components/autocomplete/nsAutoCompleteSimpleResult.cpp b/toolkit/components/autocomplete/nsAutoCompleteSimpleResult.cpp index 9fd2c0022..683ac462a 100644 --- a/toolkit/components/autocomplete/nsAutoCompleteSimpleResult.cpp +++ b/toolkit/components/autocomplete/nsAutoCompleteSimpleResult.cpp @@ -43,7 +43,8 @@ struct AutoCompleteSimpleResultMatch nsAutoCompleteSimpleResult::nsAutoCompleteSimpleResult() : mDefaultIndex(-1), - mSearchResult(RESULT_NOMATCH) + mSearchResult(RESULT_NOMATCH), + mTypeAheadResult(false) { } @@ -66,6 +67,12 @@ nsAutoCompleteSimpleResult::AppendResult(nsIAutoCompleteResult* aResult) mErrorDescription = errorDescription; } + bool typeAheadResult = false; + if (NS_SUCCEEDED(aResult->GetTypeAheadResult(&typeAheadResult)) && + typeAheadResult) { + mTypeAheadResult = typeAheadResult; + } + int32_t defaultIndex = -1; if (NS_SUCCEEDED(aResult->GetDefaultIndex(&defaultIndex)) && defaultIndex >= 0) { @@ -166,6 +173,20 @@ nsAutoCompleteSimpleResult::SetErrorDescription( return NS_OK; } +// typeAheadResult +NS_IMETHODIMP +nsAutoCompleteSimpleResult::GetTypeAheadResult(bool *aTypeAheadResult) +{ + *aTypeAheadResult = mTypeAheadResult; + return NS_OK; +} +NS_IMETHODIMP +nsAutoCompleteSimpleResult::SetTypeAheadResult(bool aTypeAheadResult) +{ + mTypeAheadResult = aTypeAheadResult; + return NS_OK; +} + NS_IMETHODIMP nsAutoCompleteSimpleResult::InsertMatchAt(int32_t aIndex, const nsAString& aValue, diff --git a/toolkit/components/autocomplete/nsAutoCompleteSimpleResult.h b/toolkit/components/autocomplete/nsAutoCompleteSimpleResult.h index 28968aa57..61ee542e4 100644 --- a/toolkit/components/autocomplete/nsAutoCompleteSimpleResult.h +++ b/toolkit/components/autocomplete/nsAutoCompleteSimpleResult.h @@ -38,6 +38,8 @@ protected: int32_t mDefaultIndex; uint32_t mSearchResult; + bool mTypeAheadResult; + nsCOMPtr<nsIAutoCompleteSimpleResultListener> mListener; }; diff --git a/toolkit/components/autocomplete/nsIAutoCompleteResult.idl b/toolkit/components/autocomplete/nsIAutoCompleteResult.idl index c719d9427..9ae22ade7 100644 --- a/toolkit/components/autocomplete/nsIAutoCompleteResult.idl +++ b/toolkit/components/autocomplete/nsIAutoCompleteResult.idl @@ -50,6 +50,13 @@ interface nsIAutoCompleteResult : nsISupports readonly attribute unsigned long matchCount; /** + * If true, the results will not be displayed in the popup. However, + * if a default index is specified, the default item will still be + * completed in the input. + */ + readonly attribute boolean typeAheadResult; + + /** * Get the value of the result at the given index */ AString getValueAt(in long index); diff --git a/toolkit/components/autocomplete/nsIAutoCompleteSimpleResult.idl b/toolkit/components/autocomplete/nsIAutoCompleteSimpleResult.idl index 5e92e037a..6a8827ab8 100644 --- a/toolkit/components/autocomplete/nsIAutoCompleteSimpleResult.idl +++ b/toolkit/components/autocomplete/nsIAutoCompleteSimpleResult.idl @@ -42,6 +42,12 @@ interface nsIAutoCompleteSimpleResult : nsIAutoCompleteResult void setSearchResult(in unsigned short aSearchResult); /** + * A writer for the readonly attribute 'typeAheadResult', typically set + * because a result is only intended for type-ahead completion. + */ + void setTypeAheadResult(in boolean aHidden); + + /** * Inserts a match consisting of the given value, comment, image, style and * the value to use for defaultIndex completion at a given position. * @param aIndex diff --git a/toolkit/components/autocomplete/tests/unit/head_autocomplete.js b/toolkit/components/autocomplete/tests/unit/head_autocomplete.js index 1443879f0..5a458bdf4 100644 --- a/toolkit/components/autocomplete/tests/unit/head_autocomplete.js +++ b/toolkit/components/autocomplete/tests/unit/head_autocomplete.js @@ -85,6 +85,11 @@ AutoCompleteResultBase.prototype = { defaultIndex: -1, + _typeAheadResult: false, + get typeAheadResult() { + return this._typeAheadResult; + }, + get matchCount() { return this._values.length; }, diff --git a/toolkit/components/autocomplete/tests/unit/test_hiddenResult.js b/toolkit/components/autocomplete/tests/unit/test_hiddenResult.js new file mode 100644 index 000000000..8e2485716 --- /dev/null +++ b/toolkit/components/autocomplete/tests/unit/test_hiddenResult.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function AutoCompleteResult(aValues) { + this._values = aValues; + this.defaultIndex = -1; + this._typeAheadResult = false; +} +AutoCompleteResult.prototype = Object.create(AutoCompleteResultBase.prototype); + +function AutoCompleteTypeAheadResult(aValues) { + this._values = aValues; + this.defaultIndex = 0; + this._typeAheadResult = true; +} +AutoCompleteTypeAheadResult.prototype = Object.create(AutoCompleteResultBase.prototype); + + +/** + * Test AutoComplete with multiple AutoCompleteSearch sources, with one of them + * being hidden from the popup, but can still do typeahead completion. + */ +function run_test() { + do_test_pending(); + + var inputStr = "moz"; + + // Type ahead result + var searchTypeAhead = new AutoCompleteSearchBase("search1", + new AutoCompleteTypeAheadResult(["mozillaTest1"])); + // Regular result + var searchNormal = new AutoCompleteSearchBase("search2", + new AutoCompleteResult(["mozillaTest2"])); + + // Register searches so AutoCompleteController can find them + registerAutoCompleteSearch(searchNormal); + registerAutoCompleteSearch(searchTypeAhead); + + // Make an AutoCompleteInput that uses our searches + // and confirms results on search complete. + var input = new AutoCompleteInputBase([searchTypeAhead.name, searchNormal.name]); + input.completeDefaultIndex = true; + input.textValue = inputStr; + + // Caret must be at the end. Autofill doesn't happen unless you're typing + // characters at the end. + var strLen = inputStr.length; + input.selectTextRange(strLen, strLen); + do_check_eq(input.selectionStart, strLen); + do_check_eq(input.selectionEnd, strLen); + + var controller = Cc["@mozilla.org/autocomplete/controller;1"]. + getService(Ci.nsIAutoCompleteController); + + controller.input = input; + controller.startSearch(inputStr); + + input.onSearchComplete = function() { + // Hidden results should still be able to do inline autocomplete + do_check_eq(input.textValue, "mozillaTest1"); + + // Now, let's fill the textbox with the first result of the popup. + // The first search is marked as hidden, so we must always get the + // second search. + controller.handleEnter(true); + do_check_eq(input.textValue, "mozillaTest2"); + + // Only one item in the popup. + do_check_eq(controller.matchCount, 1); + + // Unregister searches + unregisterAutoCompleteSearch(searchNormal); + unregisterAutoCompleteSearch(searchTypeAhead); + do_test_finished(); + }; +} diff --git a/toolkit/components/autocomplete/tests/unit/test_popupSelectionVsDefaultCompleteValue.js b/toolkit/components/autocomplete/tests/unit/test_popupSelectionVsDefaultCompleteValue.js new file mode 100644 index 000000000..fb4153355 --- /dev/null +++ b/toolkit/components/autocomplete/tests/unit/test_popupSelectionVsDefaultCompleteValue.js @@ -0,0 +1,71 @@ +/* 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/. */ + +function AutoCompleteTypeAheadResult(aValues, aFinalCompleteValues) { + this._values = aValues; + this._finalCompleteValues = aFinalCompleteValues; + this.defaultIndex = 0; + this._typeAheadResult = true; +} +AutoCompleteTypeAheadResult.prototype = Object.create(AutoCompleteResultBase.prototype); + +function AutoCompleteResult(aValues) { + this._values = aValues; +} +AutoCompleteResult.prototype = Object.create(AutoCompleteResultBase.prototype); + +function AutoCompleteInput(aSearches) { + this.searches = aSearches; + this.popupOpen = true; + this.completeDefaultIndex = true; + this.completeSelectedIndex = true; +} +AutoCompleteInput.prototype = Object.create(AutoCompleteInputBase.prototype); + +function run_test() { + run_next_test(); +} + +add_test(function test_handleEnter() { + doSearch("moz", function(aController) { + do_check_eq(aController.input.textValue, "mozilla.com"); + aController.handleEnter(true); + do_check_eq(aController.input.textValue, "mozilla.org"); + }); +}); + +function doSearch(aSearchString, aOnCompleteCallback) { + let typeAheadSearch = new AutoCompleteSearchBase( + "typeAheadSearch", + new AutoCompleteTypeAheadResult([ "mozilla.com" ], [ "http://www.mozilla.com" ]) + ); + registerAutoCompleteSearch(typeAheadSearch); + + let search = new AutoCompleteSearchBase( + "search", + new AutoCompleteResult([ "mozilla.org" ]) + ); + registerAutoCompleteSearch(search); + + let controller = Cc["@mozilla.org/autocomplete/controller;1"]. + getService(Ci.nsIAutoCompleteController); + + // Make an AutoCompleteInput that uses our searches and confirms results. + let input = new AutoCompleteInput([ typeAheadSearch.name, search.name ]); + input.textValue = aSearchString; + + // Caret must be at the end for autofill to happen. + let strLen = aSearchString.length; + input.selectTextRange(strLen, strLen); + controller.input = input; + controller.startSearch(aSearchString); + + input.onSearchComplete = function onSearchComplete() { + aOnCompleteCallback(controller); + + // Clean up. + unregisterAutoCompleteSearch(search); + run_next_test(); + }; +} diff --git a/toolkit/components/autocomplete/tests/unit/xpcshell.ini b/toolkit/components/autocomplete/tests/unit/xpcshell.ini index 4d193965c..daf89db17 100644 --- a/toolkit/components/autocomplete/tests/unit/xpcshell.ini +++ b/toolkit/components/autocomplete/tests/unit/xpcshell.ini @@ -18,7 +18,9 @@ tail = [test_finalCompleteValue_forceComplete.js] [test_finalCompleteValueSelectedIndex.js] [test_finalDefaultCompleteValue.js] +[test_hiddenResult.js] [test_immediate_search.js] [test_insertMatchAt.js] +[test_popupSelectionVsDefaultCompleteValue.js] [test_previousResult.js] [test_stopSearch.js] diff --git a/toolkit/components/filepicker/nsFileView.cpp b/toolkit/components/filepicker/nsFileView.cpp index ad4471e86..9a8278496 100644 --- a/toolkit/components/filepicker/nsFileView.cpp +++ b/toolkit/components/filepicker/nsFileView.cpp @@ -133,6 +133,13 @@ NS_IMETHODIMP nsFileResult::GetMatchCount(uint32_t *aMatchCount) return NS_OK; } +NS_IMETHODIMP nsFileResult::GetTypeAheadResult(bool *aTypeAheadResult) +{ + NS_ENSURE_ARG_POINTER(aTypeAheadResult); + *aTypeAheadResult = false; + return NS_OK; +} + NS_IMETHODIMP nsFileResult::GetValueAt(int32_t index, nsAString & aValue) { aValue = mValues[index]; diff --git a/toolkit/components/mozintl/MozIntl.cpp b/toolkit/components/mozintl/MozIntl.cpp index 9c393c296..9c61c73a6 100644 --- a/toolkit/components/mozintl/MozIntl.cpp +++ b/toolkit/components/mozintl/MozIntl.cpp @@ -48,6 +48,32 @@ MozIntl::AddGetCalendarInfo(JS::Handle<JS::Value> val, JSContext* cx) return NS_OK; } +NS_IMETHODIMP +MozIntl::AddGetDisplayNames(JS::Handle<JS::Value> val, JSContext* cx) +{ + if (!val.isObject()) { + return NS_ERROR_INVALID_ARG; + } + + JS::Rooted<JSObject*> realIntlObj(cx, js::CheckedUnwrap(&val.toObject())); + if (!realIntlObj) { + return NS_ERROR_INVALID_ARG; + } + + JSAutoCompartment ac(cx, realIntlObj); + + static const JSFunctionSpec funcs[] = { + JS_SELF_HOSTED_FN("getDisplayNames", "Intl_getDisplayNames", 2, 0), + JS_FS_END + }; + + if (!JS_DefineFunctions(cx, realIntlObj, funcs)) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + NS_GENERIC_FACTORY_CONSTRUCTOR(MozIntl) NS_DEFINE_NAMED_CID(MOZ_MOZINTL_CID); diff --git a/toolkit/components/mozintl/mozIMozIntl.idl b/toolkit/components/mozintl/mozIMozIntl.idl index 67be184d4..f28824d47 100644 --- a/toolkit/components/mozintl/mozIMozIntl.idl +++ b/toolkit/components/mozintl/mozIMozIntl.idl @@ -9,4 +9,5 @@ interface mozIMozIntl : nsISupports { [implicit_jscontext] void addGetCalendarInfo(in jsval intlObject); + [implicit_jscontext] void addGetDisplayNames(in jsval intlObject); }; diff --git a/toolkit/components/mozintl/test/test_mozintl.js b/toolkit/components/mozintl/test/test_mozintl.js index 0eca2c67e..8d2720bf0 100644 --- a/toolkit/components/mozintl/test/test_mozintl.js +++ b/toolkit/components/mozintl/test/test_mozintl.js @@ -7,6 +7,7 @@ function run_test() { test_this_global(mozIntl); test_cross_global(mozIntl); + test_methods_presence(mozIntl); ok(true); } @@ -30,3 +31,16 @@ function test_cross_global(mozIntl) { equal(waivedX.getCalendarInfo() instanceof Object, false); equal(waivedX.getCalendarInfo() instanceof global.Object, true); } + +function test_methods_presence(mozIntl) { + equal(mozIntl.addGetCalendarInfo instanceof Function, true); + equal(mozIntl.addGetDisplayNames instanceof Function, true); + + let x = {}; + + mozIntl.addGetCalendarInfo(x); + equal(x.getCalendarInfo instanceof Function, true); + + mozIntl.addGetDisplayNames(x); + equal(x.getDisplayNames instanceof Function, true); +} diff --git a/toolkit/components/passwordmgr/test/unit/test_logins_search.js b/toolkit/components/passwordmgr/test/unit/test_logins_search.js index 188c75039..730771981 100644 --- a/toolkit/components/passwordmgr/test/unit/test_logins_search.js +++ b/toolkit/components/passwordmgr/test/unit/test_logins_search.js @@ -192,7 +192,6 @@ add_task(function test_search_all_full_case_sensitive() { checkAllSearches({ hostname: "http://www.example.com" }, 1); checkAllSearches({ hostname: "http://www.example.com/" }, 0); - checkAllSearches({ hostname: "http://" }, 0); checkAllSearches({ hostname: "example.com" }, 0); checkAllSearches({ formSubmitURL: "http://www.example.com" }, 2); diff --git a/toolkit/components/places/UnifiedComplete.js b/toolkit/components/places/UnifiedComplete.js index ad3d35aab..acd358b11 100644 --- a/toolkit/components/places/UnifiedComplete.js +++ b/toolkit/components/places/UnifiedComplete.js @@ -1245,7 +1245,7 @@ Search.prototype = { // * If the protocol differs we should not match. For example if the user // searched https we should not return http. try { - let prefixURI = NetUtil.newURI(this._strippedPrefix); + let prefixURI = NetUtil.newURI(this._strippedPrefix + match.token); let finalURI = NetUtil.newURI(match.url); if (prefixURI.scheme != finalURI.scheme) return false; diff --git a/toolkit/components/places/moz.build b/toolkit/components/places/moz.build index adac79cba..85e1e93e1 100644 --- a/toolkit/components/places/moz.build +++ b/toolkit/components/places/moz.build @@ -78,6 +78,8 @@ if CONFIG['MOZ_PLACES']: EXTRA_COMPONENTS += [ 'ColorAnalyzer.js', 'nsLivemarkService.js', + 'nsPlacesAutoComplete.js', + 'nsPlacesAutoComplete.manifest', 'nsPlacesExpiration.js', 'nsTaggingService.js', 'PageIconProtocolHandler.js', diff --git a/toolkit/components/places/nsNavHistory.cpp b/toolkit/components/places/nsNavHistory.cpp index 8cf3a2e32..7f4007c1a 100644 --- a/toolkit/components/places/nsNavHistory.cpp +++ b/toolkit/components/places/nsNavHistory.cpp @@ -949,6 +949,10 @@ nsresult // static nsNavHistory::AsciiHostNameFromHostString(const nsACString& aHostName, nsACString& aAscii) { + aAscii.Truncate(); + if (aHostName.IsEmpty()) { + return NS_OK; + } // To properly generate a uri we must provide a protocol. nsAutoCString fakeURL("http://"); fakeURL.Append(aHostName); diff --git a/toolkit/components/places/nsPlacesAutoComplete.js b/toolkit/components/places/nsPlacesAutoComplete.js new file mode 100644 index 000000000..29bdae4c1 --- /dev/null +++ b/toolkit/components/places/nsPlacesAutoComplete.js @@ -0,0 +1,1778 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=2 ts=2 sts=2 expandtab + * 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/. */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch", + "resource://gre/modules/TelemetryStopwatch.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); + +//////////////////////////////////////////////////////////////////////////////// +//// Constants + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; + +// This SQL query fragment provides the following: +// - whether the entry is bookmarked (kQueryIndexBookmarked) +// - the bookmark title, if it is a bookmark (kQueryIndexBookmarkTitle) +// - the tags associated with a bookmarked entry (kQueryIndexTags) +const kBookTagSQLFragment = + `EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk = h.id) AS bookmarked, + ( + SELECT title FROM moz_bookmarks WHERE fk = h.id AND title NOTNULL + ORDER BY lastModified DESC LIMIT 1 + ) AS btitle, + ( + SELECT GROUP_CONCAT(t.title, ',') + FROM moz_bookmarks b + JOIN moz_bookmarks t ON t.id = +b.parent AND t.parent = :parent + WHERE b.fk = h.id + ) AS tags`; + +// observer topics +const kTopicShutdown = "places-shutdown"; +const kPrefChanged = "nsPref:changed"; + +// Match type constants. These indicate what type of search function we should +// be using. +const MATCH_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE; +const MATCH_BOUNDARY_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY_ANYWHERE; +const MATCH_BOUNDARY = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY; +const MATCH_BEGINNING = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING; +const MATCH_BEGINNING_CASE_SENSITIVE = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING_CASE_SENSITIVE; + +// AutoComplete index constants. All AutoComplete queries will provide these +// columns in this order. +const kQueryIndexURL = 0; +const kQueryIndexTitle = 1; +const kQueryIndexFaviconURL = 2; +const kQueryIndexBookmarked = 3; +const kQueryIndexBookmarkTitle = 4; +const kQueryIndexTags = 5; +const kQueryIndexVisitCount = 6; +const kQueryIndexTyped = 7; +const kQueryIndexPlaceId = 8; +const kQueryIndexQueryType = 9; +const kQueryIndexOpenPageCount = 10; + +// AutoComplete query type constants. Describes the various types of queries +// that we can process. +const kQueryTypeKeyword = 0; +const kQueryTypeFiltered = 1; + +// This separator is used as an RTL-friendly way to split the title and tags. +// It can also be used by an nsIAutoCompleteResult consumer to re-split the +// "comment" back into the title and the tag. +const kTitleTagsSeparator = " \u2013 "; + +const kBrowserUrlbarBranch = "browser.urlbar."; +// Toggle autocomplete. +const kBrowserUrlbarAutocompleteEnabledPref = "autocomplete.enabled"; +// Toggle autoFill. +const kBrowserUrlbarAutofillPref = "autoFill"; +// Whether to search only typed entries. +const kBrowserUrlbarAutofillTypedPref = "autoFill.typed"; + +// The Telemetry histogram for urlInlineComplete query on domain +const DOMAIN_QUERY_TELEMETRY = "PLACES_AUTOCOMPLETE_URLINLINE_DOMAIN_QUERY_TIME_MS"; + +//////////////////////////////////////////////////////////////////////////////// +//// Globals + +XPCOMUtils.defineLazyServiceGetter(this, "gTextURIService", + "@mozilla.org/intl/texttosuburi;1", + "nsITextToSubURI"); + +//////////////////////////////////////////////////////////////////////////////// +//// Helpers + +/** + * Initializes our temporary table on a given database. + * + * @param aDatabase + * The mozIStorageConnection to set up the temp table on. + */ +function initTempTable(aDatabase) +{ + // Note: this should be kept up-to-date with the definition in + // nsPlacesTables.h. + let stmt = aDatabase.createAsyncStatement( + `CREATE TEMP TABLE moz_openpages_temp ( + url TEXT PRIMARY KEY + , open_count INTEGER + )` + ); + stmt.executeAsync(); + stmt.finalize(); + + // Note: this should be kept up-to-date with the definition in + // nsPlacesTriggers.h. + stmt = aDatabase.createAsyncStatement( + `CREATE TEMPORARY TRIGGER moz_openpages_temp_afterupdate_trigger + AFTER UPDATE OF open_count ON moz_openpages_temp FOR EACH ROW + WHEN NEW.open_count = 0 + BEGIN + DELETE FROM moz_openpages_temp + WHERE url = NEW.url; + END` + ); + stmt.executeAsync(); + stmt.finalize(); +} + +/** + * Used to unescape encoded URI strings, and drop information that we do not + * care about for searching. + * + * @param aURIString + * The text to unescape and modify. + * @return the modified uri. + */ +function fixupSearchText(aURIString) +{ + let uri = stripPrefix(aURIString); + return gTextURIService.unEscapeURIForUI("UTF-8", uri); +} + +/** + * Strip prefixes from the URI that we don't care about for searching. + * + * @param aURIString + * The text to modify. + * @return the modified uri. + */ +function stripPrefix(aURIString) +{ + let uri = aURIString; + + if (uri.indexOf("http://") == 0) { + uri = uri.slice(7); + } + else if (uri.indexOf("https://") == 0) { + uri = uri.slice(8); + } + else if (uri.indexOf("ftp://") == 0) { + uri = uri.slice(6); + } + + if (uri.indexOf("www.") == 0) { + uri = uri.slice(4); + } + return uri; +} + +/** + * safePrefGetter get the pref with type safety. + * This will return the default value provided if no pref is set. + * + * @param aPrefBranch + * The nsIPrefBranch containing the required preference + * @param aName + * A preference name + * @param aDefault + * The preference's default value + * @return the preference value or provided default + */ + +function safePrefGetter(aPrefBranch, aName, aDefault) { + let types = { + boolean: "Bool", + number: "Int", + string: "Char" + }; + let type = types[typeof(aDefault)]; + if (!type) { + throw "Unknown type!"; + } + + // If the pref isn't set, we want to use the default. + if (aPrefBranch.getPrefType(aName) == Ci.nsIPrefBranch.PREF_INVALID) { + return aDefault; + } + try { + return aPrefBranch["get" + type + "Pref"](aName); + } + catch (e) { + return aDefault; + } +} + +/** + * Whether UnifiedComplete is alive. + */ +function isUnifiedCompleteInstantiated() { + try { + return Components.manager.QueryInterface(Ci.nsIServiceManager) + .isServiceInstantiated(Cc["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"], + Ci.mozIPlacesAutoComplete); + } catch (ex) { + return false; + } +} + +//////////////////////////////////////////////////////////////////////////////// +//// AutoCompleteStatementCallbackWrapper class + +/** + * Wraps a callback and ensures that handleCompletion is not dispatched if the + * query is no longer tracked. + * + * @param aAutocomplete + * A reference to a nsPlacesAutoComplete. + * @param aCallback + * A reference to a mozIStorageStatementCallback + * @param aDBConnection + * The database connection to execute the queries on. + */ +function AutoCompleteStatementCallbackWrapper(aAutocomplete, aCallback, + aDBConnection) +{ + this._autocomplete = aAutocomplete; + this._callback = aCallback; + this._db = aDBConnection; +} + +AutoCompleteStatementCallbackWrapper.prototype = { + ////////////////////////////////////////////////////////////////////////////// + //// mozIStorageStatementCallback + + handleResult: function ACSCW_handleResult(aResultSet) + { + this._callback.handleResult.apply(this._callback, arguments); + }, + + handleError: function ACSCW_handleError(aError) + { + this._callback.handleError.apply(this._callback, arguments); + }, + + handleCompletion: function ACSCW_handleCompletion(aReason) + { + // Only dispatch handleCompletion if we are not done searching and are a + // pending search. + if (!this._autocomplete.isSearchComplete() && + this._autocomplete.isPendingSearch(this._handle)) { + this._callback.handleCompletion.apply(this._callback, arguments); + } + }, + + ////////////////////////////////////////////////////////////////////////////// + //// AutoCompleteStatementCallbackWrapper + + /** + * Executes the specified query asynchronously. This object will notify + * this._callback if we should notify (logic explained in handleCompletion). + * + * @param aQueries + * The queries to execute asynchronously. + * @return a mozIStoragePendingStatement that can be used to cancel the + * queries. + */ + executeAsync: function ACSCW_executeAsync(aQueries) + { + return this._handle = this._db.executeAsync(aQueries, aQueries.length, + this); + }, + + ////////////////////////////////////////////////////////////////////////////// + //// nsISupports + + QueryInterface: XPCOMUtils.generateQI([ + Ci.mozIStorageStatementCallback, + ]) +}; + +//////////////////////////////////////////////////////////////////////////////// +//// nsPlacesAutoComplete class +//// @mozilla.org/autocomplete/search;1?name=history + +function nsPlacesAutoComplete() +{ + ////////////////////////////////////////////////////////////////////////////// + //// Shared Constants for Smart Getters + + // TODO bug 412736 in case of a frecency tie, break it with h.typed and + // h.visit_count which is better than nothing. This is slow, so not doing it + // yet... + function baseQuery(conditions = "") { + let query = `SELECT h.url, h.title, f.url, ${kBookTagSQLFragment}, + h.visit_count, h.typed, h.id, :query_type, + t.open_count + FROM moz_places h + LEFT JOIN moz_favicons f ON f.id = h.favicon_id + LEFT JOIN moz_openpages_temp t ON t.url = h.url + WHERE h.frecency <> 0 + AND AUTOCOMPLETE_MATCH(:searchString, h.url, + IFNULL(btitle, h.title), tags, + h.visit_count, h.typed, + bookmarked, t.open_count, + :matchBehavior, :searchBehavior) + ${conditions} + ORDER BY h.frecency DESC, h.id DESC + LIMIT :maxResults`; + return query; + } + + ////////////////////////////////////////////////////////////////////////////// + //// Smart Getters + + XPCOMUtils.defineLazyGetter(this, "_db", function() { + // Get a cloned, read-only version of the database. We'll only ever write + // to our own in-memory temp table, and having a cloned copy means we do not + // run the risk of our queries taking longer due to the main database + // connection performing a long-running task. + let db = PlacesUtils.history.DBConnection.clone(true); + + // Autocomplete often fallbacks to a table scan due to lack of text indices. + // In such cases a larger cache helps reducing IO. The default Storage + // value is MAX_CACHE_SIZE_BYTES in storage/mozStorageConnection.cpp. + let stmt = db.createAsyncStatement("PRAGMA cache_size = -6144"); // 6MiB + stmt.executeAsync(); + stmt.finalize(); + + // Create our in-memory tables for tab tracking. + initTempTable(db); + + // Populate the table with current open pages cache contents. + if (this._openPagesCache.length > 0) { + // Avoid getter re-entrance from the _registerOpenPageQuery lazy getter. + let stmt = this._registerOpenPageQuery = + db.createAsyncStatement(this._registerOpenPageQuerySQL); + let params = stmt.newBindingParamsArray(); + for (let i = 0; i < this._openPagesCache.length; i++) { + let bp = params.newBindingParams(); + bp.bindByName("page_url", this._openPagesCache[i]); + params.addParams(bp); + } + stmt.bindParameters(params); + stmt.executeAsync(); + stmt.finalize(); + delete this._openPagesCache; + } + + return db; + }); + + this._customQuery = (conditions = "") => { + return this._db.createAsyncStatement(baseQuery(conditions)); + }; + + XPCOMUtils.defineLazyGetter(this, "_defaultQuery", function() { + return this._db.createAsyncStatement(baseQuery()); + }); + + XPCOMUtils.defineLazyGetter(this, "_historyQuery", function() { + // Enforce ignoring the visit_count index, since the frecency one is much + // faster in this case. ANALYZE helps the query planner to figure out the + // faster path, but it may not have run yet. + return this._db.createAsyncStatement(baseQuery("AND +h.visit_count > 0")); + }); + + XPCOMUtils.defineLazyGetter(this, "_bookmarkQuery", function() { + return this._db.createAsyncStatement(baseQuery("AND bookmarked")); + }); + + XPCOMUtils.defineLazyGetter(this, "_tagsQuery", function() { + return this._db.createAsyncStatement(baseQuery("AND tags IS NOT NULL")); + }); + + XPCOMUtils.defineLazyGetter(this, "_openPagesQuery", function() { + return this._db.createAsyncStatement( + `SELECT t.url, t.url, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + :query_type, t.open_count, NULL + FROM moz_openpages_temp t + LEFT JOIN moz_places h ON h.url = t.url + WHERE h.id IS NULL + AND AUTOCOMPLETE_MATCH(:searchString, t.url, t.url, NULL, + NULL, NULL, NULL, t.open_count, + :matchBehavior, :searchBehavior) + ORDER BY t.ROWID DESC + LIMIT :maxResults` + ); + }); + + XPCOMUtils.defineLazyGetter(this, "_typedQuery", function() { + return this._db.createAsyncStatement(baseQuery("AND h.typed = 1")); + }); + + XPCOMUtils.defineLazyGetter(this, "_adaptiveQuery", function() { + return this._db.createAsyncStatement( + `/* do not warn (bug 487789) */ + SELECT h.url, h.title, f.url, ${kBookTagSQLFragment}, + h.visit_count, h.typed, h.id, :query_type, t.open_count + FROM ( + SELECT ROUND( + MAX(use_count) * (1 + (input = :search_string)), 1 + ) AS rank, place_id + FROM moz_inputhistory + WHERE input BETWEEN :search_string AND :search_string || X'FFFF' + GROUP BY place_id + ) AS i + JOIN moz_places h ON h.id = i.place_id + LEFT JOIN moz_favicons f ON f.id = h.favicon_id + LEFT JOIN moz_openpages_temp t ON t.url = h.url + WHERE AUTOCOMPLETE_MATCH(NULL, h.url, + IFNULL(btitle, h.title), tags, + h.visit_count, h.typed, bookmarked, + t.open_count, + :matchBehavior, :searchBehavior) + ORDER BY rank DESC, h.frecency DESC` + ); + }); + + XPCOMUtils.defineLazyGetter(this, "_keywordQuery", function() { + return this._db.createAsyncStatement( + `/* do not warn (bug 487787) */ + SELECT REPLACE(h.url, '%s', :query_string) AS search_url, h.title, + IFNULL(f.url, (SELECT f.url + FROM moz_places + JOIN moz_favicons f ON f.id = favicon_id + WHERE rev_host = h.rev_host + ORDER BY frecency DESC + LIMIT 1) + ), 1, NULL, NULL, h.visit_count, h.typed, h.id, + :query_type, t.open_count + FROM moz_keywords k + JOIN moz_places h ON k.place_id = h.id + LEFT JOIN moz_favicons f ON f.id = h.favicon_id + LEFT JOIN moz_openpages_temp t ON t.url = search_url + WHERE k.keyword = LOWER(:keyword)` + ); + }); + + this._registerOpenPageQuerySQL = + `INSERT OR REPLACE INTO moz_openpages_temp (url, open_count) + VALUES (:page_url, + IFNULL( + ( + SELECT open_count + 1 + FROM moz_openpages_temp + WHERE url = :page_url + ), + 1 + ) + )`; + XPCOMUtils.defineLazyGetter(this, "_registerOpenPageQuery", function() { + return this._db.createAsyncStatement(this._registerOpenPageQuerySQL); + }); + + XPCOMUtils.defineLazyGetter(this, "_unregisterOpenPageQuery", function() { + return this._db.createAsyncStatement( + `UPDATE moz_openpages_temp + SET open_count = open_count - 1 + WHERE url = :page_url` + ); + }); + + ////////////////////////////////////////////////////////////////////////////// + //// Initialization + + // load preferences + this._prefs = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService). + getBranch(kBrowserUrlbarBranch); + this._syncEnabledPref(); + this._loadPrefs(true); + + // register observers + this._os = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + this._os.addObserver(this, kTopicShutdown, false); + +} + +nsPlacesAutoComplete.prototype = { + ////////////////////////////////////////////////////////////////////////////// + //// nsIAutoCompleteSearch + + startSearch: function PAC_startSearch(aSearchString, aSearchParam, + aPreviousResult, aListener) + { + // Stop the search in case the controller has not taken care of it. + this.stopSearch(); + + // Note: We don't use aPreviousResult to make sure ordering of results are + // consistent. See bug 412730 for more details. + + // We want to store the original string with no leading or trailing + // whitespace for case sensitive searches. + this._originalSearchString = aSearchString.trim(); + + this._currentSearchString = + fixupSearchText(this._originalSearchString.toLowerCase()); + + let params = new Set(aSearchParam.split(" ")); + this._enableActions = params.has("enable-actions"); + this._disablePrivateActions = params.has("disable-private-actions"); + + this._listener = aListener; + let result = Cc["@mozilla.org/autocomplete/simple-result;1"]. + createInstance(Ci.nsIAutoCompleteSimpleResult); + result.setSearchString(aSearchString); + result.setListener(this); + this._result = result; + + // If we are not enabled, we need to return now. + if (!this._enabled) { + this._finishSearch(true); + return; + } + + // Reset our search behavior to the default. + if (this._currentSearchString) { + this._behavior = this._defaultBehavior; + } + else { + this._behavior = this._emptySearchDefaultBehavior; + } + // For any given search, we run up to four queries: + // 1) keywords (this._keywordQuery) + // 2) adaptive learning (this._adaptiveQuery) + // 3) open pages not supported by history (this._openPagesQuery) + // 4) query from this._getSearch + // (1) only gets ran if we get any filtered tokens from this._getSearch, + // since if there are no tokens, there is nothing to match, so there is no + // reason to run the query). + let {query, tokens} = + this._getSearch(this._getUnfilteredSearchTokens(this._currentSearchString)); + let queries = tokens.length ? + [this._getBoundKeywordQuery(tokens), this._getBoundAdaptiveQuery()] : + [this._getBoundAdaptiveQuery()]; + + if (this._hasBehavior("openpage")) { + queries.push(this._getBoundOpenPagesQuery(tokens)); + } + queries.push(query); + + // Start executing our queries. + this._telemetryStartTime = Date.now(); + this._executeQueries(queries); + + // Set up our persistent state for the duration of the search. + this._searchTokens = tokens; + this._usedPlaces = {}; + }, + + stopSearch: function PAC_stopSearch() + { + // We need to cancel our searches so we do not get any [more] results. + // However, it's possible we haven't actually started any searches, so this + // method may throw because this._pendingQuery may be undefined. + if (this._pendingQuery) { + this._stopActiveQuery(); + } + + this._finishSearch(false); + }, + + ////////////////////////////////////////////////////////////////////////////// + //// nsIAutoCompleteSimpleResultListener + + onValueRemoved: function PAC_onValueRemoved(aResult, aURISpec, aRemoveFromDB) + { + if (aRemoveFromDB) { + PlacesUtils.history.removePage(NetUtil.newURI(aURISpec)); + } + }, + + ////////////////////////////////////////////////////////////////////////////// + //// mozIPlacesAutoComplete + + // If the connection has not yet been started, use this local cache. This + // prevents autocomplete from initing the database till the first search. + _openPagesCache: [], + registerOpenPage: function PAC_registerOpenPage(aURI) + { + if (!this._databaseInitialized) { + this._openPagesCache.push(aURI.spec); + return; + } + + let stmt = this._registerOpenPageQuery; + stmt.params.page_url = aURI.spec; + stmt.executeAsync(); + }, + + unregisterOpenPage: function PAC_unregisterOpenPage(aURI) + { + if (!this._databaseInitialized) { + let index = this._openPagesCache.indexOf(aURI.spec); + if (index != -1) { + this._openPagesCache.splice(index, 1); + } + return; + } + + let stmt = this._unregisterOpenPageQuery; + stmt.params.page_url = aURI.spec; + stmt.executeAsync(); + }, + + ////////////////////////////////////////////////////////////////////////////// + //// mozIStorageStatementCallback + + handleResult: function PAC_handleResult(aResultSet) + { + let row, haveMatches = false; + while ((row = aResultSet.getNextRow())) { + let match = this._processRow(row); + haveMatches = haveMatches || match; + + if (this._result.matchCount == this._maxRichResults) { + // We have enough results, so stop running our search. + this._stopActiveQuery(); + + // And finish our search. + this._finishSearch(true); + return; + } + + } + + // Notify about results if we've gotten them. + if (haveMatches) { + this._notifyResults(true); + } + }, + + handleError: function PAC_handleError(aError) + { + Components.utils.reportError("Places AutoComplete: An async statement encountered an " + + "error: " + aError.result + ", '" + aError.message + "'"); + }, + + handleCompletion: function PAC_handleCompletion(aReason) + { + // If we have already finished our search, we should bail out early. + if (this.isSearchComplete()) { + return; + } + + // If we do not have enough results, and our match type is + // MATCH_BOUNDARY_ANYWHERE, search again with MATCH_ANYWHERE to get more + // results. + if (this._matchBehavior == MATCH_BOUNDARY_ANYWHERE && + this._result.matchCount < this._maxRichResults && !this._secondPass) { + this._secondPass = true; + let queries = [ + this._getBoundAdaptiveQuery(MATCH_ANYWHERE), + this._getBoundSearchQuery(MATCH_ANYWHERE, this._searchTokens), + ]; + this._executeQueries(queries); + return; + } + + this._finishSearch(true); + }, + + ////////////////////////////////////////////////////////////////////////////// + //// nsIObserver + + observe: function PAC_observe(aSubject, aTopic, aData) + { + if (aTopic == kTopicShutdown) { + this._os.removeObserver(this, kTopicShutdown); + + // Remove our preference observer. + this._prefs.removeObserver("", this); + delete this._prefs; + + // Finalize the statements that we have used. + let stmts = [ + "_defaultQuery", + "_historyQuery", + "_bookmarkQuery", + "_tagsQuery", + "_openPagesQuery", + "_typedQuery", + "_adaptiveQuery", + "_keywordQuery", + "_registerOpenPageQuery", + "_unregisterOpenPageQuery", + ]; + for (let i = 0; i < stmts.length; i++) { + // We do not want to create any query we haven't already created, so + // see if it is a getter first. + if (Object.getOwnPropertyDescriptor(this, stmts[i]).value !== undefined) { + this[stmts[i]].finalize(); + } + } + + if (this._databaseInitialized) { + this._db.asyncClose(); + } + } + else if (aTopic == kPrefChanged) { + // Avoid re-entrancy when flipping linked preferences. + if (this._ignoreNotifications) + return; + this._ignoreNotifications = true; + this._loadPrefs(false, aTopic, aData); + this._ignoreNotifications = false; + } + }, + + ////////////////////////////////////////////////////////////////////////////// + //// nsPlacesAutoComplete + + get _databaseInitialized() { + return Object.getOwnPropertyDescriptor(this, "_db").value !== undefined; + }, + + /** + * Generates the tokens used in searching from a given string. + * + * @param aSearchString + * The string to generate tokens from. + * @return an array of tokens. + */ + _getUnfilteredSearchTokens: function PAC_unfilteredSearchTokens(aSearchString) + { + // Calling split on an empty string will return an array containing one + // empty string. We don't want that, as it'll break our logic, so return an + // empty array then. + return aSearchString.length ? aSearchString.split(" ") : []; + }, + + /** + * Properly cleans up when searching is completed. + * + * @param aNotify + * Indicates if we should notify the AutoComplete listener about our + * results or not. + */ + _finishSearch: function PAC_finishSearch(aNotify) + { + // Notify about results if we are supposed to. + if (aNotify) { + this._notifyResults(false); + } + + // Clear our state + delete this._originalSearchString; + delete this._currentSearchString; + delete this._strippedPrefix; + delete this._searchTokens; + delete this._listener; + delete this._result; + delete this._usedPlaces; + delete this._pendingQuery; + this._secondPass = false; + this._enableActions = false; + }, + + /** + * Executes the given queries asynchronously. + * + * @param aQueries + * The queries to execute. + */ + _executeQueries: function PAC_executeQueries(aQueries) + { + // Because we might get a handleCompletion for canceled queries, we want to + // filter out queries we no longer care about (described in the + // handleCompletion implementation of AutoCompleteStatementCallbackWrapper). + + // Create our wrapper object and execute the queries. + let wrapper = new AutoCompleteStatementCallbackWrapper(this, this, this._db); + this._pendingQuery = wrapper.executeAsync(aQueries); + }, + + /** + * Stops executing our active query. + */ + _stopActiveQuery: function PAC_stopActiveQuery() + { + this._pendingQuery.cancel(); + delete this._pendingQuery; + }, + + /** + * Notifies the listener about results. + * + * @param aSearchOngoing + * Indicates if the search is ongoing or not. + */ + _notifyResults: function PAC_notifyResults(aSearchOngoing) + { + let result = this._result; + let resultCode = result.matchCount ? "RESULT_SUCCESS" : "RESULT_NOMATCH"; + if (aSearchOngoing) { + resultCode += "_ONGOING"; + } + result.setSearchResult(Ci.nsIAutoCompleteResult[resultCode]); + this._listener.onSearchResult(this, result); + if (this._telemetryStartTime) { + let elapsed = Date.now() - this._telemetryStartTime; + if (elapsed > 50) { + try { + Services.telemetry + .getHistogramById("PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS") + .add(elapsed); + } catch (ex) { + Components.utils.reportError("Unable to report telemetry."); + } + } + this._telemetryStartTime = null; + } + }, + + /** + * Synchronize suggest.* prefs with autocomplete.enabled. + */ + _syncEnabledPref: function PAC_syncEnabledPref() + { + let suggestPrefs = ["suggest.history", "suggest.bookmark", "suggest.openpage"]; + let types = ["History", "Bookmark", "Openpage"]; + + this._enabled = safePrefGetter(this._prefs, kBrowserUrlbarAutocompleteEnabledPref, + true); + this._suggestHistory = safePrefGetter(this._prefs, "suggest.history", true); + this._suggestBookmark = safePrefGetter(this._prefs, "suggest.bookmark", true); + this._suggestOpenpage = safePrefGetter(this._prefs, "suggest.openpage", true); + + if (this._enabled) { + // If the autocomplete preference is active, activate all suggest + // preferences only if all of them are false. + if (types.every(type => this["_suggest" + type] == false)) { + for (let type of suggestPrefs) { + this._prefs.setBoolPref(type, true); + } + } + } else { + // If the preference was deactivated, deactivate all suggest preferences. + for (let type of suggestPrefs) { + this._prefs.setBoolPref(type, false); + } + } + }, + + /** + * Loads the preferences that we care about. + * + * @param [optional] aRegisterObserver + * Indicates if the preference observer should be added or not. The + * default value is false. + * @param [optional] aTopic + * Observer's topic, if any. + * @param [optional] aSubject + * Observer's subject, if any. + */ + _loadPrefs: function PAC_loadPrefs(aRegisterObserver, aTopic, aData) + { + // Avoid race conditions with UnifiedComplete component. + if (aData && !isUnifiedCompleteInstantiated()) { + // Synchronize suggest.* prefs with autocomplete.enabled. + if (aData == kBrowserUrlbarAutocompleteEnabledPref) { + this._syncEnabledPref(); + } else if (aData.startsWith("suggest.")) { + let suggestPrefs = ["suggest.history", "suggest.bookmark", "suggest.openpage"]; + this._prefs.setBoolPref(kBrowserUrlbarAutocompleteEnabledPref, + suggestPrefs.some(pref => safePrefGetter(this._prefs, pref, true))); + } + } + + this._enabled = safePrefGetter(this._prefs, + kBrowserUrlbarAutocompleteEnabledPref, + true); + this._matchBehavior = safePrefGetter(this._prefs, + "matchBehavior", + MATCH_BOUNDARY_ANYWHERE); + this._filterJavaScript = safePrefGetter(this._prefs, "filter.javascript", true); + this._maxRichResults = safePrefGetter(this._prefs, "maxRichResults", 25); + this._restrictHistoryToken = safePrefGetter(this._prefs, + "restrict.history", "^"); + this._restrictBookmarkToken = safePrefGetter(this._prefs, + "restrict.bookmark", "*"); + this._restrictTypedToken = safePrefGetter(this._prefs, "restrict.typed", "~"); + this._restrictTagToken = safePrefGetter(this._prefs, "restrict.tag", "+"); + this._restrictOpenPageToken = safePrefGetter(this._prefs, + "restrict.openpage", "%"); + this._matchTitleToken = safePrefGetter(this._prefs, "match.title", "#"); + this._matchURLToken = safePrefGetter(this._prefs, "match.url", "@"); + + this._suggestHistory = safePrefGetter(this._prefs, "suggest.history", true); + this._suggestBookmark = safePrefGetter(this._prefs, "suggest.bookmark", true); + this._suggestOpenpage = safePrefGetter(this._prefs, "suggest.openpage", true); + this._suggestTyped = safePrefGetter(this._prefs, "suggest.history.onlyTyped", false); + + // If history is not set, onlyTyped value should be ignored. + if (!this._suggestHistory) { + this._suggestTyped = false; + } + let types = ["History", "Bookmark", "Openpage", "Typed"]; + this._defaultBehavior = types.reduce((memo, type) => { + let prefValue = this["_suggest" + type]; + return memo | (prefValue && + Ci.mozIPlacesAutoComplete["BEHAVIOR_" + type.toUpperCase()]); + }, 0); + + // Further restrictions to apply for "empty searches" (i.e. searches for ""). + // The empty behavior is typed history, if history is enabled. Otherwise, + // it is bookmarks, if they are enabled. If both history and bookmarks are disabled, + // it defaults to open pages. + this._emptySearchDefaultBehavior = Ci.mozIPlacesAutoComplete.BEHAVIOR_RESTRICT; + if (this._suggestHistory) { + this._emptySearchDefaultBehavior |= Ci.mozIPlacesAutoComplete.BEHAVIOR_HISTORY | + Ci.mozIPlacesAutoComplete.BEHAVIOR_TYPED; + } else if (this._suggestBookmark) { + this._emptySearchDefaultBehavior |= Ci.mozIPlacesAutoComplete.BEHAVIOR_BOOKMARK; + } else { + this._emptySearchDefaultBehavior |= Ci.mozIPlacesAutoComplete.BEHAVIOR_OPENPAGE; + } + + // Validate matchBehavior; default to MATCH_BOUNDARY_ANYWHERE. + if (this._matchBehavior != MATCH_ANYWHERE && + this._matchBehavior != MATCH_BOUNDARY && + this._matchBehavior != MATCH_BEGINNING) { + this._matchBehavior = MATCH_BOUNDARY_ANYWHERE; + } + // register observer + if (aRegisterObserver) { + this._prefs.addObserver("", this, false); + } + }, + + /** + * Given an array of tokens, this function determines which query should be + * ran. It also removes any special search tokens. + * + * @param aTokens + * An array of search tokens. + * @return an object with two properties: + * query: the correctly optimized, bound query to search the database + * with. + * tokens: the filtered list of tokens to search with. + */ + _getSearch: function PAC_getSearch(aTokens) + { + let foundToken = false; + let restrict = (behavior) => { + if (!foundToken) { + this._behavior = 0; + this._setBehavior("restrict"); + foundToken = true; + } + this._setBehavior(behavior); + }; + + // Set the proper behavior so our call to _getBoundSearchQuery gives us the + // correct query. + for (let i = aTokens.length - 1; i >= 0; i--) { + switch (aTokens[i]) { + case this._restrictHistoryToken: + restrict("history"); + break; + case this._restrictBookmarkToken: + restrict("bookmark"); + break; + case this._restrictTagToken: + restrict("tag"); + break; + case this._restrictOpenPageToken: + if (!this._enableActions) { + continue; + } + restrict("openpage"); + break; + case this._matchTitleToken: + restrict("title"); + break; + case this._matchURLToken: + restrict("url"); + break; + case this._restrictTypedToken: + restrict("typed"); + break; + default: + // We do not want to remove the token if we did not match. + continue; + } + + aTokens.splice(i, 1); + } + + // Set the right JavaScript behavior based on our preference. Note that the + // preference is whether or not we should filter JavaScript, and the + // behavior is if we should search it or not. + if (!this._filterJavaScript) { + this._setBehavior("javascript"); + } + + return { + query: this._getBoundSearchQuery(this._matchBehavior, aTokens), + tokens: aTokens + }; + }, + + /** + * @return a string consisting of the search query to be used based on the + * previously set urlbar suggestion preferences. + */ + _getSuggestionPrefQuery: function PAC_getSuggestionPrefQuery() + { + if (!this._hasBehavior("restrict") && this._hasBehavior("history") && + this._hasBehavior("bookmark")) { + return this._hasBehavior("typed") ? this._customQuery("AND h.typed = 1") + : this._defaultQuery; + } + let conditions = []; + if (this._hasBehavior("history")) { + // Enforce ignoring the visit_count index, since the frecency one is much + // faster in this case. ANALYZE helps the query planner to figure out the + // faster path, but it may not have up-to-date information yet. + conditions.push("+h.visit_count > 0"); + } + if (this._hasBehavior("typed")) { + conditions.push("h.typed = 1"); + } + if (this._hasBehavior("bookmark")) { + conditions.push("bookmarked"); + } + if (this._hasBehavior("tag")) { + conditions.push("tags NOTNULL"); + } + + return conditions.length ? this._customQuery("AND " + conditions.join(" AND ")) + : this._defaultQuery; + }, + + /** + * Obtains the search query to be used based on the previously set search + * behaviors (accessed by this._hasBehavior). The query is bound and ready to + * execute. + * + * @param aMatchBehavior + * How this query should match its tokens to the search string. + * @param aTokens + * An array of search tokens. + * @return the correctly optimized query to search the database with and the + * new list of tokens to search with. The query has all the needed + * parameters bound, so consumers can execute it without doing any + * additional work. + */ + _getBoundSearchQuery: function PAC_getBoundSearchQuery(aMatchBehavior, + aTokens) + { + let query = this._getSuggestionPrefQuery(); + + // Bind the needed parameters to the query so consumers can use it. + let params = query.params; + params.parent = PlacesUtils.tagsFolderId; + params.query_type = kQueryTypeFiltered; + params.matchBehavior = aMatchBehavior; + params.searchBehavior = this._behavior; + + // We only want to search the tokens that we are left with - not the + // original search string. + params.searchString = aTokens.join(" "); + + // Limit the query to the the maximum number of desired results. + // This way we can avoid doing more work than needed. + params.maxResults = this._maxRichResults; + + return query; + }, + + _getBoundOpenPagesQuery: function PAC_getBoundOpenPagesQuery(aTokens) + { + let query = this._openPagesQuery; + + // Bind the needed parameters to the query so consumers can use it. + let params = query.params; + params.query_type = kQueryTypeFiltered; + params.matchBehavior = this._matchBehavior; + params.searchBehavior = this._behavior; + + // We only want to search the tokens that we are left with - not the + // original search string. + params.searchString = aTokens.join(" "); + params.maxResults = this._maxRichResults; + + return query; + }, + + /** + * Obtains the keyword query with the properly bound parameters. + * + * @param aTokens + * The array of search tokens to check against. + * @return the bound keyword query. + */ + _getBoundKeywordQuery: function PAC_getBoundKeywordQuery(aTokens) + { + // The keyword is the first word in the search string, with the parameters + // following it. + let searchString = this._originalSearchString; + let queryString = ""; + let queryIndex = searchString.indexOf(" "); + if (queryIndex != -1) { + queryString = searchString.substring(queryIndex + 1); + } + // We need to escape the parameters as if they were the query in a URL + queryString = encodeURIComponent(queryString).replace(/%20/g, "+"); + + // The first word could be a keyword, so that's what we'll search. + let keyword = aTokens[0]; + + let query = this._keywordQuery; + let params = query.params; + params.keyword = keyword; + params.query_string = queryString; + params.query_type = kQueryTypeKeyword; + + return query; + }, + + /** + * Obtains the adaptive query with the properly bound parameters. + * + * @return the bound adaptive query. + */ + _getBoundAdaptiveQuery: function PAC_getBoundAdaptiveQuery(aMatchBehavior) + { + // If we were not given a match behavior, use the stored match behavior. + if (arguments.length == 0) { + aMatchBehavior = this._matchBehavior; + } + + let query = this._adaptiveQuery; + let params = query.params; + params.parent = PlacesUtils.tagsFolderId; + params.search_string = this._currentSearchString; + params.query_type = kQueryTypeFiltered; + params.matchBehavior = aMatchBehavior; + params.searchBehavior = this._behavior; + + return query; + }, + + /** + * Processes a mozIStorageRow to generate the proper data for the AutoComplete + * result. This will add an entry to the current result if it matches the + * criteria. + * + * @param aRow + * The row to process. + * @return true if the row is accepted, and false if not. + */ + _processRow: function PAC_processRow(aRow) + { + // Before we do any work, make sure this entry isn't already in our results. + let entryId = aRow.getResultByIndex(kQueryIndexPlaceId); + let escapedEntryURL = aRow.getResultByIndex(kQueryIndexURL); + let openPageCount = aRow.getResultByIndex(kQueryIndexOpenPageCount) || 0; + + // If actions are enabled and the page is open, add only the switch-to-tab + // result. Otherwise, add the normal result. + let [url, action] = this._enableActions && openPageCount > 0 && this._hasBehavior("openpage") ? + ["moz-action:switchtab," + escapedEntryURL, "action "] : + [escapedEntryURL, ""]; + + if (this._inResults(entryId, url)) { + return false; + } + + let entryTitle = aRow.getResultByIndex(kQueryIndexTitle) || ""; + let entryFavicon = aRow.getResultByIndex(kQueryIndexFaviconURL) || ""; + let entryBookmarked = aRow.getResultByIndex(kQueryIndexBookmarked); + let entryBookmarkTitle = entryBookmarked ? + aRow.getResultByIndex(kQueryIndexBookmarkTitle) : null; + let entryTags = aRow.getResultByIndex(kQueryIndexTags) || ""; + + // Always prefer the bookmark title unless it is empty + let title = entryBookmarkTitle || entryTitle; + + let style; + if (aRow.getResultByIndex(kQueryIndexQueryType) == kQueryTypeKeyword) { + style = "keyword"; + title = NetUtil.newURI(escapedEntryURL).host; + } + + // We will always prefer to show tags if we have them. + let showTags = !!entryTags; + + // However, we'll act as if a page is not bookmarked if the user wants + // only history and not bookmarks and there are no tags. + if (this._hasBehavior("history") && !this._hasBehavior("bookmark") && + !showTags) { + showTags = false; + style = "favicon"; + } + + // If we have tags and should show them, we need to add them to the title. + if (showTags) { + title += kTitleTagsSeparator + entryTags; + } + // We have to determine the right style to display. Tags show the tag icon, + // bookmarks get the bookmark icon, and keywords get the keyword icon. If + // the result does not fall into any of those, it just gets the favicon. + if (!style) { + // It is possible that we already have a style set (from a keyword + // search or because of the user's preferences), so only set it if we + // haven't already done so. + if (showTags) { + style = "tag"; + } + else if (entryBookmarked) { + style = "bookmark"; + } + else { + style = "favicon"; + } + } + + this._addToResults(entryId, url, title, entryFavicon, action + style); + return true; + }, + + /** + * Checks to see if the given place has already been added to the results. + * + * @param aPlaceId + * The place id to check for, may be null. + * @param aUrl + * The url to check for. + * @return true if the place has been added, false otherwise. + * + * @note Must check both the id and the url for a negative match, since + * autocomplete may run in the middle of a new page addition. In such + * a case the switch-to-tab query would hash the page by url, then a + * next query, running after the page addition, would hash it by id. + * It's not possible to just rely on url though, since keywords + * dynamically modify the url to include their search string. + */ + _inResults: function PAC_inResults(aPlaceId, aUrl) + { + if (aPlaceId && aPlaceId in this._usedPlaces) { + return true; + } + return aUrl in this._usedPlaces; + }, + + /** + * Adds a result to the AutoComplete results. Also tracks that we've added + * this place_id into the result set. + * + * @param aPlaceId + * The place_id of the item to be added to the result set. This is + * used by _inResults. + * @param aURISpec + * The URI spec for the entry. + * @param aTitle + * The title to give the entry. + * @param aFaviconSpec + * The favicon to give to the entry. + * @param aStyle + * Indicates how the entry should be styled when displayed. + */ + _addToResults: function PAC_addToResults(aPlaceId, aURISpec, aTitle, + aFaviconSpec, aStyle) + { + // Add this to our internal tracker to ensure duplicates do not end up in + // the result. _usedPlaces is an Object that is being used as a set. + // Not all entries have a place id, thus we fallback to the url for them. + // We cannot use only the url since keywords entries are modified to + // include the search string, and would be returned multiple times. Ids + // are faster too. + this._usedPlaces[aPlaceId || aURISpec] = true; + + // Obtain the favicon for this URI. + let favicon; + if (aFaviconSpec) { + let uri = NetUtil.newURI(aFaviconSpec); + favicon = PlacesUtils.favicons.getFaviconLinkForIcon(uri).spec; + } + favicon = favicon || PlacesUtils.favicons.defaultFavicon.spec; + + this._result.appendMatch(aURISpec, aTitle, favicon, aStyle); + }, + + /** + * Determines if the specified AutoComplete behavior is set. + * + * @param aType + * The behavior type to test for. + * @return true if the behavior is set, false otherwise. + */ + _hasBehavior: function PAC_hasBehavior(aType) + { + let behavior = Ci.mozIPlacesAutoComplete["BEHAVIOR_" + aType.toUpperCase()]; + + if (this._disablePrivateActions && + behavior == Ci.mozIPlacesAutoComplete.BEHAVIOR_OPENPAGE) { + return false; + } + + return this._behavior & behavior; + }, + + /** + * Enables the desired AutoComplete behavior. + * + * @param aType + * The behavior type to set. + */ + _setBehavior: function PAC_setBehavior(aType) + { + this._behavior |= + Ci.mozIPlacesAutoComplete["BEHAVIOR_" + aType.toUpperCase()]; + }, + + /** + * Determines if we are done searching or not. + * + * @return true if we have completed searching, false otherwise. + */ + isSearchComplete: function PAC_isSearchComplete() + { + // If _pendingQuery is null, we should no longer do any work since we have + // already called _finishSearch. This means we completed our search. + return this._pendingQuery == null; + }, + + /** + * Determines if the given handle of a pending statement is a pending search + * or not. + * + * @param aHandle + * A mozIStoragePendingStatement to check and see if we are waiting for + * results from it still. + * @return true if it is a pending query, false otherwise. + */ + isPendingSearch: function PAC_isPendingSearch(aHandle) + { + return this._pendingQuery == aHandle; + }, + + ////////////////////////////////////////////////////////////////////////////// + //// nsISupports + + classID: Components.ID("d0272978-beab-4adc-a3d4-04b76acfa4e7"), + + _xpcom_factory: XPCOMUtils.generateSingletonFactory(nsPlacesAutoComplete), + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIAutoCompleteSearch, + Ci.nsIAutoCompleteSimpleResultListener, + Ci.mozIPlacesAutoComplete, + Ci.mozIStorageStatementCallback, + Ci.nsIObserver, + Ci.nsISupportsWeakReference, + ]) +}; + +//////////////////////////////////////////////////////////////////////////////// +//// urlInlineComplete class +//// component @mozilla.org/autocomplete/search;1?name=urlinline + +function urlInlineComplete() +{ + this._loadPrefs(true); + Services.obs.addObserver(this, kTopicShutdown, true); +} + +urlInlineComplete.prototype = { + +///////////////////////////////////////////////////////////////////////////////// +//// Database and query getters + + __db: null, + + get _db() + { + if (!this.__db && this._autofillEnabled) { + this.__db = PlacesUtils.history.DBConnection.clone(true); + } + return this.__db; + }, + + __hostQuery: null, + + get _hostQuery() + { + if (!this.__hostQuery) { + // Add a trailing slash at the end of the hostname, since we always + // want to complete up to and including a URL separator. + this.__hostQuery = this._db.createAsyncStatement( + `/* do not warn (bug no): could index on (typed,frecency) but not worth it */ + SELECT host || '/', prefix || host || '/' + FROM moz_hosts + WHERE host BETWEEN :search_string AND :search_string || X'FFFF' + AND frecency <> 0 + ${this._autofillTyped ? "AND typed = 1" : ""} + ORDER BY frecency DESC + LIMIT 1` + ); + } + return this.__hostQuery; + }, + + __urlQuery: null, + + get _urlQuery() + { + if (!this.__urlQuery) { + this.__urlQuery = this._db.createAsyncStatement( + `/* do not warn (bug no): can't use an index */ + SELECT h.url + FROM moz_places h + WHERE h.frecency <> 0 + ${this._autofillTyped ? "AND h.typed = 1 " : ""} + AND AUTOCOMPLETE_MATCH(:searchString, h.url, + h.title, '', + h.visit_count, h.typed, 0, 0, + :matchBehavior, :searchBehavior) + ORDER BY h.frecency DESC, h.id DESC + LIMIT 1` + ); + } + return this.__urlQuery; + }, + + ////////////////////////////////////////////////////////////////////////////// + //// nsIAutoCompleteSearch + + startSearch: function UIC_startSearch(aSearchString, aSearchParam, + aPreviousResult, aListener) + { + // Stop the search in case the controller has not taken care of it. + if (this._pendingQuery) { + this.stopSearch(); + } + + let pendingSearch = this._pendingSearch = {}; + + // We want to store the original string with no leading or trailing + // whitespace for case sensitive searches. + this._originalSearchString = aSearchString; + this._currentSearchString = + fixupSearchText(this._originalSearchString.toLowerCase()); + // The protocol and the host are lowercased by nsIURI, so it's fine to + // lowercase the typed prefix to add it back to the results later. + this._strippedPrefix = this._originalSearchString.slice( + 0, this._originalSearchString.length - this._currentSearchString.length + ).toLowerCase(); + + this._result = Cc["@mozilla.org/autocomplete/simple-result;1"]. + createInstance(Ci.nsIAutoCompleteSimpleResult); + this._result.setSearchString(aSearchString); + this._result.setTypeAheadResult(true); + + this._listener = aListener; + + Task.spawn(function* () { + // Don't autoFill if the search term is recognized as a keyword, otherwise + // it will override default keywords behavior. Note that keywords are + // hashed on first use, so while the first query may delay a little bit, + // next ones will just hit the memory hash. + let dontAutoFill = this._currentSearchString.length == 0 || !this._db || + (yield PlacesUtils.keywords.fetch(this._currentSearchString)); + if (this._pendingSearch != pendingSearch) + return; + if (dontAutoFill) { + this._finishSearch(); + return; + } + + // Don't try to autofill if the search term includes any whitespace. + // This may confuse completeDefaultIndex cause the AUTOCOMPLETE_MATCH + // tokenizer ends up trimming the search string and returning a value + // that doesn't match it, or is even shorter. + if (/\s/.test(this._currentSearchString)) { + this._finishSearch(); + return; + } + + // Hosts have no "/" in them. + let lastSlashIndex = this._currentSearchString.lastIndexOf("/"); + + // Search only URLs if there's a slash in the search string... + if (lastSlashIndex != -1) { + // ...but not if it's exactly at the end of the search string. + if (lastSlashIndex < this._currentSearchString.length - 1) + this._queryURL(); + else + this._finishSearch(); + return; + } + + // Do a synchronous search on the table of hosts. + let query = this._hostQuery; + query.params.search_string = this._currentSearchString.toLowerCase(); + // This is just to measure the delay to reach the UI, not the query time. + TelemetryStopwatch.start(DOMAIN_QUERY_TELEMETRY); + let wrapper = new AutoCompleteStatementCallbackWrapper(this, { + handleResult: aResultSet => { + if (this._pendingSearch != pendingSearch) + return; + let row = aResultSet.getNextRow(); + let trimmedHost = row.getResultByIndex(0); + let untrimmedHost = row.getResultByIndex(1); + // If the untrimmed value doesn't preserve the user's input just + // ignore it and complete to the found host. + if (untrimmedHost && + !untrimmedHost.toLowerCase().includes(this._originalSearchString.toLowerCase())) { + untrimmedHost = null; + } + + this._result.appendMatch(this._strippedPrefix + trimmedHost, "", "", "", untrimmedHost); + + // handleCompletion() will cause the result listener to be called, and + // will display the result in the UI. + }, + + handleError: aError => { + Components.utils.reportError( + "URL Inline Complete: An async statement encountered an " + + "error: " + aError.result + ", '" + aError.message + "'"); + }, + + handleCompletion: aReason => { + if (this._pendingSearch != pendingSearch) + return; + TelemetryStopwatch.finish(DOMAIN_QUERY_TELEMETRY); + this._finishSearch(); + } + }, this._db); + this._pendingQuery = wrapper.executeAsync([query]); + }.bind(this)); + }, + + /** + * Execute an asynchronous search through places, and complete + * up to the next URL separator. + */ + _queryURL: function UIC__queryURL() + { + // The URIs in the database are fixed up, so we can match on a lowercased + // host, but the path must be matched in a case sensitive way. + let pathIndex = + this._originalSearchString.indexOf("/", this._strippedPrefix.length); + this._currentSearchString = fixupSearchText( + this._originalSearchString.slice(0, pathIndex).toLowerCase() + + this._originalSearchString.slice(pathIndex) + ); + + // Within the standard autocomplete query, we only search the beginning + // of URLs for 1 result. + let query = this._urlQuery; + let params = query.params; + params.matchBehavior = MATCH_BEGINNING_CASE_SENSITIVE; + params.searchBehavior |= Ci.mozIPlacesAutoComplete.BEHAVIOR_HISTORY | + Ci.mozIPlacesAutoComplete.BEHAVIOR_TYPED | + Ci.mozIPlacesAutoComplete.BEHAVIOR_URL; + params.searchString = this._currentSearchString; + + // Execute the query. + let wrapper = new AutoCompleteStatementCallbackWrapper(this, { + handleResult: aResultSet => { + let row = aResultSet.getNextRow(); + let value = row.getResultByIndex(0); + let url = fixupSearchText(value); + + let prefix = value.slice(0, value.length - stripPrefix(value).length); + + // We must complete the URL up to the next separator (which is /, ? or #). + let separatorIndex = url.slice(this._currentSearchString.length) + .search(/[\/\?\#]/); + if (separatorIndex != -1) { + separatorIndex += this._currentSearchString.length; + if (url[separatorIndex] == "/") { + separatorIndex++; // Include the "/" separator + } + url = url.slice(0, separatorIndex); + } + + // Add the result. + // If the untrimmed value doesn't preserve the user's input just + // ignore it and complete to the found url. + let untrimmedURL = prefix + url; + if (untrimmedURL && + !untrimmedURL.toLowerCase().includes(this._originalSearchString.toLowerCase())) { + untrimmedURL = null; + } + + this._result.appendMatch(this._strippedPrefix + url, "", "", "", untrimmedURL); + + // handleCompletion() will cause the result listener to be called, and + // will display the result in the UI. + }, + + handleError: aError => { + Components.utils.reportError( + "URL Inline Complete: An async statement encountered an " + + "error: " + aError.result + ", '" + aError.message + "'"); + }, + + handleCompletion: aReason => { + this._finishSearch(); + } + }, this._db); + this._pendingQuery = wrapper.executeAsync([query]); + }, + + stopSearch: function UIC_stopSearch() + { + delete this._originalSearchString; + delete this._currentSearchString; + delete this._result; + delete this._listener; + delete this._pendingSearch; + + if (this._pendingQuery) { + this._pendingQuery.cancel(); + delete this._pendingQuery; + } + }, + + /** + * Loads the preferences that we care about. + * + * @param [optional] aRegisterObserver + * Indicates if the preference observer should be added or not. The + * default value is false. + */ + _loadPrefs: function UIC_loadPrefs(aRegisterObserver) + { + let prefBranch = Services.prefs.getBranch(kBrowserUrlbarBranch); + let autocomplete = safePrefGetter(prefBranch, + kBrowserUrlbarAutocompleteEnabledPref, + true); + let autofill = safePrefGetter(prefBranch, + kBrowserUrlbarAutofillPref, + true); + this._autofillEnabled = autocomplete && autofill; + this._autofillTyped = safePrefGetter(prefBranch, + kBrowserUrlbarAutofillTypedPref, + true); + if (aRegisterObserver) { + Services.prefs.addObserver(kBrowserUrlbarBranch, this, true); + } + }, + + ////////////////////////////////////////////////////////////////////////////// + //// nsIAutoCompleteSearchDescriptor + + get searchType() { + return Ci.nsIAutoCompleteSearchDescriptor.SEARCH_TYPE_IMMEDIATE; + }, + + get clearingAutoFillSearchesAgain() { + return false; + }, + + ////////////////////////////////////////////////////////////////////////////// + //// nsIObserver + + observe: function UIC_observe(aSubject, aTopic, aData) + { + if (aTopic == kTopicShutdown) { + this._closeDatabase(); + } + else if (aTopic == kPrefChanged && + (aData.substr(kBrowserUrlbarBranch.length) == kBrowserUrlbarAutofillPref || + aData.substr(kBrowserUrlbarBranch.length) == kBrowserUrlbarAutocompleteEnabledPref || + aData.substr(kBrowserUrlbarBranch.length) == kBrowserUrlbarAutofillTypedPref)) { + let previousAutofillTyped = this._autofillTyped; + this._loadPrefs(); + if (!this._autofillEnabled) { + this.stopSearch(); + this._closeDatabase(); + } + else if (this._autofillTyped != previousAutofillTyped) { + // Invalidate the statements to update them for the new typed status. + this._invalidateStatements(); + } + } + }, + + /** + * Finalizes and invalidates cached statements. + */ + _invalidateStatements: function UIC_invalidateStatements() + { + // Finalize the statements that we have used. + let stmts = [ + "__hostQuery", + "__urlQuery", + ]; + for (let i = 0; i < stmts.length; i++) { + // We do not want to create any query we haven't already created, so + // see if it is a getter first. + if (this[stmts[i]]) { + this[stmts[i]].finalize(); + this[stmts[i]] = null; + } + } + }, + + /** + * Closes the database. + */ + _closeDatabase: function UIC_closeDatabase() + { + this._invalidateStatements(); + if (this.__db) { + this._db.asyncClose(); + this.__db = null; + } + }, + + ////////////////////////////////////////////////////////////////////////////// + //// urlInlineComplete + + _finishSearch: function UIC_finishSearch() + { + // Notify the result object + let result = this._result; + + if (result.matchCount) { + result.setDefaultIndex(0); + result.setSearchResult(Ci.nsIAutoCompleteResult["RESULT_SUCCESS"]); + } else { + result.setDefaultIndex(-1); + result.setSearchResult(Ci.nsIAutoCompleteResult["RESULT_NOMATCH"]); + } + + this._listener.onSearchResult(this, result); + this.stopSearch(); + }, + + isSearchComplete: function UIC_isSearchComplete() + { + return this._pendingQuery == null; + }, + + isPendingSearch: function UIC_isPendingSearch(aHandle) + { + return this._pendingQuery == aHandle; + }, + + ////////////////////////////////////////////////////////////////////////////// + //// nsISupports + + classID: Components.ID("c88fae2d-25cf-4338-a1f4-64a320ea7440"), + + _xpcom_factory: XPCOMUtils.generateSingletonFactory(urlInlineComplete), + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIAutoCompleteSearch, + Ci.nsIAutoCompleteSearchDescriptor, + Ci.nsIObserver, + Ci.nsISupportsWeakReference, + ]) +}; + +var components = [nsPlacesAutoComplete, urlInlineComplete]; +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components); diff --git a/toolkit/components/places/nsPlacesAutoComplete.manifest b/toolkit/components/places/nsPlacesAutoComplete.manifest new file mode 100644 index 000000000..eb704f449 --- /dev/null +++ b/toolkit/components/places/nsPlacesAutoComplete.manifest @@ -0,0 +1,6 @@ +component {d0272978-beab-4adc-a3d4-04b76acfa4e7} nsPlacesAutoComplete.js +contract @mozilla.org/autocomplete/search;1?name=history {d0272978-beab-4adc-a3d4-04b76acfa4e7} + +component {c88fae2d-25cf-4338-a1f4-64a320ea7440} nsPlacesAutoComplete.js +contract @mozilla.org/autocomplete/search;1?name=urlinline {c88fae2d-25cf-4338-a1f4-64a320ea7440} + diff --git a/toolkit/components/places/nsTaggingService.js b/toolkit/components/places/nsTaggingService.js index 1fad67a82..e367e6cb3 100644 --- a/toolkit/components/places/nsTaggingService.js +++ b/toolkit/components/places/nsTaggingService.js @@ -528,6 +528,10 @@ TagAutoCompleteResult.prototype = { return this._results.length; }, + get typeAheadResult() { + return false; + }, + /** * Get the value of the result at the given index */ diff --git a/toolkit/components/satchel/test/test_form_autocomplete.html b/toolkit/components/satchel/test/test_form_autocomplete.html index 4cf09117a..d2c22a3db 100644 --- a/toolkit/components/satchel/test/test_form_autocomplete.html +++ b/toolkit/components/satchel/test/test_form_autocomplete.html @@ -172,7 +172,7 @@ function setupFormHistory(aCallback) { { op : "add", fieldname : "field8", value : "value" }, { op : "add", fieldname : "field9", value : "value" }, { op : "add", fieldname : "field10", value : "42" }, - { op : "add", fieldname : "field11", value : "2010-10-10" }, + { op : "add", fieldname : "field11", value : "2010-10-10" }, // not used, since type=date doesn't have autocomplete currently { op : "add", fieldname : "field12", value : "21:21" }, // not used, since type=time doesn't have autocomplete currently { op : "add", fieldname : "field13", value : "32" }, // not used, since type=range doesn't have a drop down menu { op : "add", fieldname : "field14", value : "#ffffff" }, // not used, since type=color doesn't have autocomplete currently @@ -899,15 +899,13 @@ function runTest() { input = $_(14, "field11"); restoreForm(); - expectPopup(); - doKey("down"); + waitForMenuChange(0); break; case 405: - checkMenuEntries(["2010-10-10"]); - doKey("down"); - doKey("return"); - checkForm("2010-10-10"); + checkMenuEntries([]); // type=date with it's own control frame does not + // have a drop down menu for now + checkForm(""); input = $_(15, "field12"); restoreForm(); diff --git a/toolkit/content/browser-content.js b/toolkit/content/browser-content.js index 4ae798fbd..1376f70a3 100644 --- a/toolkit/content/browser-content.js +++ b/toolkit/content/browser-content.js @@ -1714,6 +1714,14 @@ let DateTimePickerListener = { (aEvent.originalTarget.type == "time" && !this.getTimePickerPref())) { return; } + + if (this._inputElement) { + // This happens when we're trying to open a picker when another picker + // is still open. We ignore this request to let the first picker + // close gracefully. + return; + } + this._inputElement = aEvent.originalTarget; this._inputElement.setDateTimePickerState(true); this.addListeners(); @@ -1728,15 +1736,17 @@ let DateTimePickerListener = { // element's value. value: Object.keys(value).length > 0 ? value : this._inputElement.value, - step: this._inputElement.step, - min: this._inputElement.min, - max: this._inputElement.max, + min: this._inputElement.getMinimum(), + max: this._inputElement.getMaximum(), + step: this._inputElement.getStep(), + stepBase: this._inputElement.getStepBase(), }, }); break; } case "MozUpdateDateTimePicker": { let value = this._inputElement.getDateTimeInputBoxValue(); + value.type = this._inputElement.type; sendAsyncMessage("FormDateTime:UpdatePicker", { value }); break; } diff --git a/toolkit/content/customizeToolbar.js b/toolkit/content/customizeToolbar.js index b96b60b98..7400aaadc 100644 --- a/toolkit/content/customizeToolbar.js +++ b/toolkit/content/customizeToolbar.js @@ -2,6 +2,8 @@ * 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/. */ +const gToolbarInfoSeparators = ["|", "-"]; + var gToolboxDocument = null; var gToolbox = null; var gCurrentDragOverItem = null; @@ -173,9 +175,20 @@ function persistCurrentSets() // Remove custom toolbars whose contents have been removed. gToolbox.removeChild(toolbar); } else if (gToolbox.toolbarset) { + var hidingAttribute = toolbar.getAttribute("type") == "menubar" ? + "autohide" : "collapsed"; // Persist custom toolbar info on the <toolbarset/> - gToolbox.toolbarset.setAttribute("toolbar"+(++customCount), - toolbar.toolbarName + ":" + currentSet); + // Attributes: + // Names: "toolbarX" (X - the number of the toolbar) + // Values: "Name|HidingAttributeName-HidingAttributeValue|CurrentSet" + gToolbox.toolbarset.setAttribute("toolbar" + (++customCount), + toolbar.toolbarName + + gToolbarInfoSeparators[0] + + hidingAttribute + + gToolbarInfoSeparators[1] + + toolbar.getAttribute(hidingAttribute) + + gToolbarInfoSeparators[0] + + currentSet); gToolboxDocument.persist(gToolbox.toolbarset.id, "toolbar"+customCount); } } @@ -485,6 +498,11 @@ function addNewToolbar() continue; } + if (name.value.includes(gToolbarInfoSeparators[0])) { + message = stringBundle.getFormattedString("enterToolbarIllegalChars", [name.value]); + continue; + } + var dupeFound = false; // Check for an existing toolbar with the same display name @@ -506,7 +524,7 @@ function addNewToolbar() message = stringBundle.getFormattedString("enterToolbarDup", [name.value]); } - gToolbox.appendCustomToolbar(name.value, ""); + gToolbox.appendCustomToolbar(name.value, "", [null, null]); toolboxChanged(); diff --git a/toolkit/content/datepicker.xhtml b/toolkit/content/datepicker.xhtml new file mode 100644 index 000000000..4da6e398f --- /dev/null +++ b/toolkit/content/datepicker.xhtml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE html [ + <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> + %htmlDTD; +]> +<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<head> + <title>Date Picker</title> + <link rel="stylesheet" href="chrome://global/skin/datetimeinputpickers.css"/> + <script type="application/javascript" src="chrome://global/content/bindings/datekeeper.js"></script> + <script type="application/javascript" src="chrome://global/content/bindings/spinner.js"></script> + <script type="application/javascript" src="chrome://global/content/bindings/calendar.js"></script> + <script type="application/javascript" src="chrome://global/content/bindings/datepicker.js"></script> +</head> +<body> + <div id="date-picker"> + <div class="calendar-container"> + <div class="nav"> + <button class="left"/> + <button class="right"/> + </div> + <div class="week-header"></div> + <div class="days-viewport"> + <div class="days-view"></div> + </div> + </div> + <div class="month-year-container"> + <button class="month-year"/> + </div> + <div class="month-year-view"></div> + </div> + <template id="spinner-template"> + <div class="spinner-container"> + <button class="up"/> + <div class="spinner"></div> + <button class="down"/> + </div> + </template> + <script type="application/javascript"> + // We need to hide the scroll bar but maintain its scrolling + // capability, so using |overflow: hidden| is not an option. + // Instead, we are inserting a user agent stylesheet that is + // capable of selecting scrollbars, and do |display: none|. + var domWinUtls = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor). + getInterface(Components.interfaces.nsIDOMWindowUtils); + domWinUtls.loadSheetUsingURIString('data:text/css,@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); scrollbar { display: none; }', domWinUtls.AGENT_SHEET); + // Create a DatePicker instance and prepare to be + // initialized by the "DatePickerInit" event from datetimepopup.xml + const root = document.getElementById("date-picker"); + new DatePicker({ + monthYear: root.querySelector(".month-year"), + monthYearView: root.querySelector(".month-year-view"), + buttonLeft: root.querySelector(".left"), + buttonRight: root.querySelector(".right"), + weekHeader: root.querySelector(".week-header"), + daysView: root.querySelector(".days-view") + }); + </script> +</body> +</html>
\ No newline at end of file diff --git a/toolkit/content/jar.mn b/toolkit/content/jar.mn index 851c72250..f0d4a62a4 100644 --- a/toolkit/content/jar.mn +++ b/toolkit/content/jar.mn @@ -45,6 +45,7 @@ toolkit.jar: content/global/customizeToolbar.js content/global/customizeToolbar.xul #endif + content/global/datepicker.xhtml #ifndef MOZ_FENNEC content/global/editMenuOverlay.js * content/global/editMenuOverlay.xul @@ -80,8 +81,11 @@ toolkit.jar: content/global/bindings/autocomplete.xml (widgets/autocomplete.xml) content/global/bindings/browser.xml (widgets/browser.xml) content/global/bindings/button.xml (widgets/button.xml) + content/global/bindings/calendar.js (widgets/calendar.js) content/global/bindings/checkbox.xml (widgets/checkbox.xml) content/global/bindings/colorpicker.xml (widgets/colorpicker.xml) + content/global/bindings/datekeeper.js (widgets/datekeeper.js) + content/global/bindings/datepicker.js (widgets/datepicker.js) content/global/bindings/datetimepicker.xml (widgets/datetimepicker.xml) content/global/bindings/datetimepopup.xml (widgets/datetimepopup.xml) content/global/bindings/datetimebox.xml (widgets/datetimebox.xml) diff --git a/toolkit/content/tests/browser/browser.ini b/toolkit/content/tests/browser/browser.ini index 278b2ffe0..67ba2f850 100644 --- a/toolkit/content/tests/browser/browser.ini +++ b/toolkit/content/tests/browser/browser.ini @@ -26,6 +26,7 @@ skip-if = !e10s [browser_contentTitle.js] [browser_crash_previous_frameloader.js] run-if = e10s && crashreporter +[browser_datetime_datepicker.js] [browser_default_image_filename.js] [browser_f7_caret_browsing.js] [browser_findbar.js] diff --git a/toolkit/content/tests/browser/browser_datetime_datepicker.js b/toolkit/content/tests/browser/browser_datetime_datepicker.js new file mode 100644 index 000000000..966a74e7a --- /dev/null +++ b/toolkit/content/tests/browser/browser_datetime_datepicker.js @@ -0,0 +1,284 @@ +/* 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"; + +const MONTH_YEAR = ".month-year", + DAYS_VIEW = ".days-view", + BTN_PREV_MONTH = ".prev", + BTN_NEXT_MONTH = ".next"; +const DATE_FORMAT = new Intl.DateTimeFormat("en-US", { year: "numeric", month: "long", timeZone: "UTC" }).format; + +// Create a list of abbreviations for calendar class names +const W = "weekend", + O = "outside", + S = "selection", + R = "out-of-range", + T = "today", + P = "off-step"; + +// Calendar classlist for 2016-12. Used to verify the classNames are correct. +const calendarClasslist_201612 = [ + [W, O], [O], [O], [O], [], [], [W], + [W], [], [], [], [], [], [W], + [W], [], [], [], [S], [], [W], + [W], [], [], [], [], [], [W], + [W], [], [], [], [], [], [W], + [W, O], [O], [O], [O], [O], [O], [W, O], +]; + +function getCalendarText() { + return helper.getChildren(DAYS_VIEW).map(child => child.textContent); +} + +function getCalendarClassList() { + return helper.getChildren(DAYS_VIEW).map(child => Array.from(child.classList)); +} + +function mergeArrays(a, b) { + return a.map((classlist, index) => classlist.concat(b[index])); +} + +let helper = new DateTimeTestHelper(); + +registerCleanupFunction(() => { + helper.cleanup(); +}); + +/** + * Test that date picker opens to today's date when input field is blank + */ +add_task(async function test_datepicker_today() { + const date = new Date(); + + await helper.openPicker("data:text/html, <input type='date'>"); + + Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(date)); + + await helper.tearDown(); +}); + +/** + * Test that date picker opens to the correct month, with calendar days + * displayed correctly, given a date value is set. + */ +add_task(async function test_datepicker_open() { + const inputValue = "2016-12-15"; + + await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}">`); + + Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(new Date(inputValue))); + Assert.deepEqual( + getCalendarText(), + [ + "27", "28", "29", "30", "1", "2", "3", + "4", "5", "6", "7", "8", "9", "10", + "11", "12", "13", "14", "15", "16", "17", + "18", "19", "20", "21", "22", "23", "24", + "25", "26", "27", "28", "29", "30", "31", + "1", "2", "3", "4", "5", "6", "7", + ], + "2016-12", + ); + Assert.deepEqual( + getCalendarClassList(), + calendarClasslist_201612, + "2016-12 classNames" + ); + + await helper.tearDown(); +}); + +/** + * When the prev month button is clicked, calendar should display the dates for + * the previous month. + */ +add_task(async function test_datepicker_prev_month_btn() { + const inputValue = "2016-12-15"; + const prevMonth = "2016-11-01"; + + await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}">`); + helper.click(helper.getElement(BTN_PREV_MONTH)); + + Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(new Date(prevMonth))); + Assert.deepEqual( + getCalendarText(), + [ + "30", "31", "1", "2", "3", "4", "5", + "6", "7", "8", "9", "10", "11", "12", + "13", "14", "15", "16", "17", "18", "19", + "20", "21", "22", "23", "24", "25", "26", + "27", "28", "29", "30", "1", "2", "3", + "4", "5", "6", "7", "8", "9", "10", + ], + "2016-11", + ); + + await helper.tearDown(); +}); + +/** + * When the next month button is clicked, calendar should display the dates for + * the next month. + */ +add_task(async function test_datepicker_next_month_btn() { + const inputValue = "2016-12-15"; + const nextMonth = "2017-01-01"; + + await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}">`); + helper.click(helper.getElement(BTN_NEXT_MONTH)); + + Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(new Date(nextMonth))); + Assert.deepEqual( + getCalendarText(), + [ + "25", "26", "27", "28", "29", "30", "31", + "1", "2", "3", "4", "5", "6", "7", + "8", "9", "10", "11", "12", "13", "14", + "15", "16", "17", "18", "19", "20", "21", + "22", "23", "24", "25", "26", "27", "28", + "29", "30", "31", "1", "2", "3", "4", + ], + "2017-01", + ); + + await helper.tearDown(); +}); + +/** + * When a date on the calendar is clicked, date picker should close and set + * value to the input box. + */ +add_task(async function test_datepicker_clicked() { + const inputValue = "2016-12-15"; + const firstDayOnCalendar = "2016-11-27"; + + await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}">`); + // Click the first item (top-left corner) of the calendar + helper.click(helper.getElement(DAYS_VIEW).children[0]); + await ContentTask.spawn(helper.tab.linkedBrowser, {}, async function() { + let inputEl = content.document.querySelector("input"); + await ContentTaskUtils.waitForEvent(inputEl, "input"); + }); + + Assert.equal(content.document.querySelector("input").value, firstDayOnCalendar); + + await helper.tearDown(); +}); + +/** + * Make sure picker is in correct state when it is reopened. + */ +add_task(async function test_datepicker_reopen_state() { + const inputValue = "2016-12-15"; + const nextMonth = "2017-01-01"; + + await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}">`); + // Navigate to the next month but does not commit the change + Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(new Date(inputValue))); + helper.click(helper.getElement(BTN_NEXT_MONTH)); + Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(new Date(nextMonth))); + EventUtils.synthesizeKey("VK_ESCAPE", {}, window); + + // Ensures the picker opens to the month of the input value + await BrowserTestUtils.synthesizeMouseAtCenter("input", {}, gBrowser.selectedBrowser); + await helper.waitForPickerReady(); + Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(new Date(inputValue))); + + await helper.tearDown(); +}); + +/** + * When min and max attributes are set, calendar should show some dates as + * out-of-range. + */ +add_task(async function test_datepicker_min_max() { + const inputValue = "2016-12-15"; + const inputMin = "2016-12-05"; + const inputMax = "2016-12-25"; + + await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}" min="${inputMin}" max="${inputMax}">`); + + Assert.deepEqual( + getCalendarClassList(), + mergeArrays(calendarClasslist_201612, [ + // R denotes out-of-range + [R], [R], [R], [R], [R], [R], [R], + [R], [], [], [], [], [], [], + [], [], [], [], [], [], [], + [], [], [], [], [], [], [], + [], [R], [R], [R], [R], [R], [R], + [R], [R], [R], [R], [R], [R], [R], + ]), + "2016-12 with min & max", + ); + + await helper.tearDown(); +}); + +/** + * When step attribute is set, calendar should show some dates as off-step. + */ +add_task(async function test_datepicker_step() { + const inputValue = "2016-12-15"; + const inputStep = "5"; + + await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}" step="${inputStep}">`); + + Assert.deepEqual( + getCalendarClassList(), + mergeArrays(calendarClasslist_201612, [ + // P denotes off-step + [P], [P], [P], [], [P], [P], [P], + [P], [], [P], [P], [P], [P], [], + [P], [P], [P], [P], [], [P], [P], + [P], [P], [], [P], [P], [P], [P], + [], [P], [P], [P], [P], [], [P], + [P], [P], [P], [], [P], [P], [P], + ]), + "2016-12 with step", + ); + + await helper.tearDown(); +}); + +add_task(async function test_datepicker_abs_min() { + const inputValue = "0001-01-01"; + await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}">`); + + Assert.deepEqual( + getCalendarText(), + [ + "", "1", "2", "3", "4", "5", "6", + "7", "8", "9", "10", "11", "12", "13", + "14", "15", "16", "17", "18", "19", "20", + "21", "22", "23", "24", "25", "26", "27", + "28", "29", "30", "31", "1", "2", "3", + "4", "5", "6", "7", "8", "9", "10", + ], + "0001-01", + ); + + await helper.tearDown(); +}); + +add_task(async function test_datepicker_abs_max() { + const inputValue = "275760-09-13"; + await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}">`); + + Assert.deepEqual( + getCalendarText(), + [ + "31", "1", "2", "3", "4", "5", "6", + "7", "8", "9", "10", "11", "12", "13", + "", "", "", "", "", "", "", + "", "", "", "", "", "", "", + "", "", "", "", "", "", "", + "", "", "", "", "", "", "", + ], + "275760-09", + ); + + await helper.tearDown(); +}); diff --git a/toolkit/content/tests/browser/head.js b/toolkit/content/tests/browser/head.js index 1c6c2b54f..d7ed7a9ff 100644 --- a/toolkit/content/tests/browser/head.js +++ b/toolkit/content/tests/browser/head.js @@ -31,3 +31,93 @@ function pushPrefs(...aPrefs) { SpecialPowers.pushPrefEnv({"set": aPrefs}, deferred.resolve); return deferred.promise; } + +/** + * Helper class for testing datetime input picker widget + */ +class DateTimeTestHelper { + constructor() { + this.panel = document.getElementById("DateTimePickerPanel"); + this.panel.setAttribute("animate", false); + this.tab = null; + this.frame = null; + } + + /** + * Opens a new tab with the URL of the test page, and make sure the picker is + * ready for testing. + * + * @param {String} pageUrl + */ + async openPicker(pageUrl) { + this.tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); + await BrowserTestUtils.synthesizeMouseAtCenter("input", {}, gBrowser.selectedBrowser); + // If dateTimePopupFrame doesn't exist yet, wait for the binding to be attached + if (!this.panel.dateTimePopupFrame) { + await BrowserTestUtils.waitForEvent(this.panel, "DateTimePickerBindingReady") + } + this.frame = this.panel.dateTimePopupFrame; + await this.waitForPickerReady(); + } + + async waitForPickerReady() { + await BrowserTestUtils.waitForEvent(this.frame, "load", true); + // Wait for picker elements to be ready + await BrowserTestUtils.waitForEvent(this.frame.contentDocument, "PickerReady"); + } + + /** + * Find an element on the picker. + * + * @param {String} selector + * @return {DOMElement} + */ + getElement(selector) { + return this.frame.contentDocument.querySelector(selector); + } + + /** + * Find the children of an element on the picker. + * + * @param {String} selector + * @return {Array<DOMElement>} + */ + getChildren(selector) { + return Array.from(this.getElement(selector).children); + } + + /** + * Click on an element + * + * @param {DOMElement} element + */ + click(element) { + EventUtils.synthesizeMouseAtCenter(element, {}, this.frame.contentWindow); + } + + /** + * Close the panel and the tab + */ + async tearDown() { + if (!this.panel.hidden) { + let pickerClosePromise = new Promise(resolve => { + this.panel.addEventListener("popuphidden", resolve, {once: true}); + }); + this.panel.hidePopup(); + this.panel.closePicker(); + await pickerClosePromise; + } + await BrowserTestUtils.removeTab(this.tab); + this.tab = null; + } + + /** + * Clean up after tests. Remove the frame to prevent leak. + */ + cleanup() { + this.frame.remove(); + this.frame = null; + this.panel.removeAttribute("animate"); + this.panel = null; + } +} diff --git a/toolkit/content/timepicker.xhtml b/toolkit/content/timepicker.xhtml index 1396223f1..77b9fba41 100644 --- a/toolkit/content/timepicker.xhtml +++ b/toolkit/content/timepicker.xhtml @@ -6,7 +6,7 @@ <html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> <head> <title>Time Picker</title> - <link rel="stylesheet" href="chrome://global/skin/timepicker.css"/> + <link rel="stylesheet" href="chrome://global/skin/datetimeinputpickers.css"/> <script type="application/javascript" src="chrome://global/content/bindings/timekeeper.js"></script> <script type="application/javascript" src="chrome://global/content/bindings/spinner.js"></script> <script type="application/javascript" src="chrome://global/content/bindings/timepicker.js"></script> diff --git a/toolkit/content/widgets/calendar.js b/toolkit/content/widgets/calendar.js new file mode 100644 index 000000000..44ba67501 --- /dev/null +++ b/toolkit/content/widgets/calendar.js @@ -0,0 +1,171 @@ +/* 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"; + +/** + * Initialize the Calendar and generate nodes for week headers and days, and + * attach event listeners. + * + * @param {Object} options + * { + * {Number} calViewSize: Number of days to appear on a calendar view + * {Function} getDayString: Transform day number to string + * {Function} getWeekHeaderString: Transform day of week number to string + * {Function} setSelection: Set selection for dateKeeper + * } + * @param {Object} context + * { + * {DOMElement} weekHeader + * {DOMElement} daysView + * } + */ +function Calendar(options, context) { + const DAYS_IN_A_WEEK = 7; + + this.context = context; + this.state = { + days: [], + weekHeaders: [], + setSelection: options.setSelection, + getDayString: options.getDayString, + getWeekHeaderString: options.getWeekHeaderString + }; + this.elements = { + weekHeaders: this._generateNodes(DAYS_IN_A_WEEK, context.weekHeader), + daysView: this._generateNodes(options.calViewSize, context.daysView) + }; + + this._attachEventListeners(); +} + +{ + Calendar.prototype = { + + /** + * Set new properties and render them. + * + * @param {Object} props + * { + * {Boolean} isVisible: Whether or not the calendar is in view + * {Array<Object>} days: Data for days + * { + * {Date} dateObj + * {Number} content + * {Array<String>} classNames + * {Boolean} enabled + * } + * {Array<Object>} weekHeaders: Data for weekHeaders + * { + * {Number} content + * {Array<String>} classNames + * } + * } + */ + setProps(props) { + if (props.isVisible) { + // Transform the days and weekHeaders array for rendering + const days = props.days.map(({ dateObj, content, classNames, enabled }) => { + return { + dateObj, + textContent: this.state.getDayString(content), + className: classNames.join(" "), + enabled + }; + }); + const weekHeaders = props.weekHeaders.map(({ content, classNames }) => { + return { + textContent: this.state.getWeekHeaderString(content), + className: classNames.join(" ") + }; + }); + // Update the DOM nodes states + this._render({ + elements: this.elements.daysView, + items: days, + prevState: this.state.days + }); + this._render({ + elements: this.elements.weekHeaders, + items: weekHeaders, + prevState: this.state.weekHeaders, + }); + // Update the state to current + this.state.days = days; + this.state.weekHeaders = weekHeaders; + } + }, + + /** + * Render the items onto the DOM nodes + * @param {Object} + * { + * {Array<DOMElement>} elements + * {Array<Object>} items + * {Array<Object>} prevState: state of items from last render + * } + */ + _render({ elements, items, prevState }) { + for (let i = 0, l = items.length; i < l; i++) { + let el = elements[i]; + + // Check if state from last render has changed, if so, update the elements + if (!prevState[i] || prevState[i].textContent != items[i].textContent) { + el.textContent = items[i].textContent; + } + if (!prevState[i] || prevState[i].className != items[i].className) { + el.className = items[i].className; + } + } + }, + + /** + * Generate DOM nodes + * + * @param {Number} size: Number of nodes to generate + * @param {DOMElement} context: Element to append the nodes to + * @return {Array<DOMElement>} + */ + _generateNodes(size, context) { + let frag = document.createDocumentFragment(); + let refs = []; + + for (let i = 0; i < size; i++) { + let el = document.createElement("div"); + el.dataset.id = i; + refs.push(el); + frag.appendChild(el); + } + context.appendChild(frag); + + return refs; + }, + + /** + * Handle events + * @param {DOMEvent} event + */ + handleEvent(event) { + switch (event.type) { + case "click": { + if (event.target.parentNode == this.context.daysView) { + let targetId = event.target.dataset.id; + let targetObj = this.state.days[targetId]; + if (targetObj.enabled) { + this.state.setSelection(targetObj.dateObj); + } + } + break; + } + } + }, + + /** + * Attach event listener to daysView + */ + _attachEventListeners() { + this.context.daysView.addEventListener("click", this); + } + }; +} diff --git a/toolkit/content/widgets/datekeeper.js b/toolkit/content/widgets/datekeeper.js new file mode 100644 index 000000000..5d70416a9 --- /dev/null +++ b/toolkit/content/widgets/datekeeper.js @@ -0,0 +1,336 @@ +/* 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"; + +/** + * DateKeeper keeps track of the date states. + */ +function DateKeeper(props) { + this.init(props); +} + +{ + const DAYS_IN_A_WEEK = 7, + MONTHS_IN_A_YEAR = 12, + YEAR_VIEW_SIZE = 200, + YEAR_BUFFER_SIZE = 10, + // The min value is 0001-01-01 based on HTML spec: + // https://html.spec.whatwg.org/#valid-date-string + MIN_DATE = -62135596800000, + // The max value is derived from the ECMAScript spec (275760-09-13): + // http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.1 + MAX_DATE = 8640000000000000, + MAX_YEAR = 275760, + MAX_MONTH = 9; + + DateKeeper.prototype = { + get year() { + return this.state.dateObj.getUTCFullYear(); + }, + + get month() { + return this.state.dateObj.getUTCMonth(); + }, + + get selection() { + return this.state.selection; + }, + + /** + * Initialize DateKeeper + * @param {Number} year + * @param {Number} month + * @param {Number} day + * @param {Number} min + * @param {Number} max + * @param {Number} step + * @param {Number} stepBase + * @param {Number} firstDayOfWeek + * @param {Array<Number>} weekends + * @param {Number} calViewSize + */ + init({ year, month, day, min, max, step, stepBase, firstDayOfWeek = 0, weekends = [0], calViewSize = 42 }) { + const today = new Date(); + + this.state = { + step, firstDayOfWeek, weekends, calViewSize, + // min & max are NaN if empty or invalid + min: new Date(Number.isNaN(min) ? MIN_DATE : min), + max: new Date(Number.isNaN(max) ? MAX_DATE : max), + stepBase: new Date(stepBase), + today: this._newUTCDate(today.getFullYear(), today.getMonth(), today.getDate()), + weekHeaders: this._getWeekHeaders(firstDayOfWeek, weekends), + years: [], + dateObj: new Date(0), + selection: { year, month, day }, + }; + + this.setCalendarMonth({ + year: year === undefined ? today.getFullYear() : year, + month: month === undefined ? today.getMonth() : month + }); + }, + /** + * Set new calendar month. The year is always treated as full year, so the + * short-form is not supported. + * @param {Object} date parts + * { + * {Number} year [optional] + * {Number} month [optional] + * } + */ + setCalendarMonth({ year = this.year, month = this.month }) { + // Make sure the date is valid before setting. + // Use setUTCFullYear so that year 99 doesn't get parsed as 1999 + if (year > MAX_YEAR || year === MAX_YEAR && month >= MAX_MONTH) { + this.state.dateObj.setUTCFullYear(MAX_YEAR, MAX_MONTH - 1, 1); + } else if (year < 1 || year === 1 && month < 0) { + this.state.dateObj.setUTCFullYear(1, 0, 1); + } else { + this.state.dateObj.setUTCFullYear(year, month, 1); + } + }, + + /** + * Set selection date + * @param {Number} year + * @param {Number} month + * @param {Number} day + */ + setSelection({ year, month, day }) { + this.state.selection.year = year; + this.state.selection.month = month; + this.state.selection.day = day; + }, + + /** + * Set month. Makes sure the day is <= the last day of the month + * @param {Number} month + */ + setMonth(month) { + this.setCalendarMonth({ year: this.year, month }); + }, + + /** + * Set year. Makes sure the day is <= the last day of the month + * @param {Number} year + */ + setYear(year) { + this.setCalendarMonth({ year, month: this.month }); + }, + + /** + * Set month by offset. Makes sure the day is <= the last day of the month + * @param {Number} offset + */ + setMonthByOffset(offset) { + this.setCalendarMonth({ year: this.year, month: this.month + offset }); + }, + + /** + * Generate the array of months + * @return {Array<Object>} + * { + * {Number} value: Month in int + * {Boolean} enabled + * } + */ + getMonths() { + let months = []; + + for (let i = 0; i < MONTHS_IN_A_YEAR; i++) { + months.push({ + value: i, + enabled: true + }); + } + + return months; + }, + + /** + * Generate the array of years + * @return {Array<Object>} + * { + * {Number} value: Year in int + * {Boolean} enabled + * } + */ + getYears() { + let years = []; + + const firstItem = this.state.years[0]; + const lastItem = this.state.years[this.state.years.length - 1]; + const currentYear = this.year; + + // Generate new years array when the year is outside of the first & + // last item range. If not, return the cached result. + if (!firstItem || !lastItem || + currentYear <= firstItem.value + YEAR_BUFFER_SIZE || + currentYear >= lastItem.value - YEAR_BUFFER_SIZE) { + // The year is set in the middle with items on both directions + for (let i = -(YEAR_VIEW_SIZE / 2); i < YEAR_VIEW_SIZE / 2; i++) { + const year = currentYear + i; + if (year >= 1 && year <= MAX_YEAR) { + years.push({ + value: year, + enabled: true + }); + } + } + this.state.years = years; + } + return this.state.years; + }, + + /** + * Get days for calendar + * @return {Array<Object>} + * { + * {Date} dateObj + * {Number} content + * {Array<String>} classNames + * {Boolean} enabled + * } + */ + getDays() { + const firstDayOfMonth = this._getFirstCalendarDate(this.state.dateObj, this.state.firstDayOfWeek); + const month = this.month; + let days = []; + + for (let i = 0; i < this.state.calViewSize; i++) { + const dateObj = this._newUTCDate(firstDayOfMonth.getUTCFullYear(), + firstDayOfMonth.getUTCMonth(), + firstDayOfMonth.getUTCDate() + i); + + let classNames = []; + let enabled = true; + + const isValid = dateObj.getTime() >= MIN_DATE && dateObj.getTime() <= MAX_DATE; + if (!isValid) { + classNames.push("out-of-range"); + enabled = false; + + days.push({ + classNames, + enabled, + }); + continue; + } + + const isWeekend = this.state.weekends.includes(dateObj.getUTCDay()); + const isCurrentMonth = month == dateObj.getUTCMonth(); + const isSelection = this.state.selection.year == dateObj.getUTCFullYear() && + this.state.selection.month == dateObj.getUTCMonth() && + this.state.selection.day == dateObj.getUTCDate(); + const isOutOfRange = dateObj.getTime() < this.state.min.getTime() || + dateObj.getTime() > this.state.max.getTime(); + const isToday = this.state.today.getTime() == dateObj.getTime(); + const isOffStep = this._checkIsOffStep(dateObj, + this._newUTCDate(dateObj.getUTCFullYear(), + dateObj.getUTCMonth(), + dateObj.getUTCDate() + 1)); + + if (isWeekend) { + classNames.push("weekend"); + } + if (!isCurrentMonth) { + classNames.push("outside"); + } + if (isSelection && !isOutOfRange && !isOffStep) { + classNames.push("selection"); + } + if (isOutOfRange) { + classNames.push("out-of-range"); + enabled = false; + } + if (isToday) { + classNames.push("today"); + } + if (isOffStep) { + classNames.push("off-step"); + enabled = false; + } + days.push({ + dateObj, + content: dateObj.getUTCDate(), + classNames, + enabled, + }); + } + return days; + }, + + /** + * Check if a date is off step given a starting point and the next increment + * @param {Date} start + * @param {Date} next + * @return {Boolean} + */ + _checkIsOffStep(start, next) { + // If the increment is larger or equal to the step, it must not be off-step. + if (next - start >= this.state.step) { + return false; + } + // Calculate the last valid date + const lastValidStep = Math.floor((next - 1 - this.state.stepBase) / this.state.step); + const lastValidTimeInMs = lastValidStep * this.state.step + this.state.stepBase.getTime(); + // The date is off-step if the last valid date is smaller than the start date + return lastValidTimeInMs < start.getTime(); + }, + + /** + * Get week headers for calendar + * @param {Number} firstDayOfWeek + * @param {Array<Number>} weekends + * @return {Array<Object>} + * { + * {Number} content + * {Array<String>} classNames + * } + */ + _getWeekHeaders(firstDayOfWeek, weekends) { + let headers = []; + let dayOfWeek = firstDayOfWeek; + + for (let i = 0; i < DAYS_IN_A_WEEK; i++) { + headers.push({ + content: dayOfWeek % DAYS_IN_A_WEEK, + classNames: weekends.includes(dayOfWeek % DAYS_IN_A_WEEK) ? ["weekend"] : [] + }); + dayOfWeek++; + } + return headers; + }, + + /** + * Get the first day on a calendar month + * @param {Date} dateObj + * @param {Number} firstDayOfWeek + * @return {Date} + */ + _getFirstCalendarDate(dateObj, firstDayOfWeek) { + const daysOffset = 1 - DAYS_IN_A_WEEK; + let firstDayOfMonth = this._newUTCDate(dateObj.getUTCFullYear(), dateObj.getUTCMonth()); + let dayOfWeek = firstDayOfMonth.getUTCDay(); + + return this._newUTCDate( + firstDayOfMonth.getUTCFullYear(), + firstDayOfMonth.getUTCMonth(), + // When first calendar date is the same as first day of the week, add + // another row on top of it. + firstDayOfWeek == dayOfWeek ? daysOffset : (firstDayOfWeek - dayOfWeek + daysOffset) % DAYS_IN_A_WEEK); + }, + + /** + * Helper function for creating UTC dates + * @param {...[Number]} parts + * @return {Date} + */ + _newUTCDate(...parts) { + return new Date(new Date(0).setUTCFullYear(...parts)); + }, + }; +} diff --git a/toolkit/content/widgets/datepicker.js b/toolkit/content/widgets/datepicker.js new file mode 100644 index 000000000..0e9c9a6e6 --- /dev/null +++ b/toolkit/content/widgets/datepicker.js @@ -0,0 +1,376 @@ +/* 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 DatePicker(context) { + this.context = context; + this._attachEventListeners(); +} + +{ + const CAL_VIEW_SIZE = 42; + + DatePicker.prototype = { + /** + * Initializes the date picker. Set the default states and properties. + * @param {Object} props + * { + * {Number} year [optional] + * {Number} month [optional] + * {Number} date [optional] + * {Number} min + * {Number} max + * {Number} step + * {Number} stepBase + * {Number} firstDayOfWeek + * {Array<Number>} weekends + * {Array<String>} monthStrings + * {Array<String>} weekdayStrings + * {String} locale [optional]: User preferred locale + * } + */ + init(props = {}) { + this.props = props; + this._setDefaultState(); + this._createComponents(); + this._update(); + document.dispatchEvent(new CustomEvent("PickerReady")); + }, + + /* + * Set initial date picker states. + */ + _setDefaultState() { + const { year, month, day, min, max, step, stepBase, firstDayOfWeek, weekends, + monthStrings, weekdayStrings, locale } = this.props; + const dateKeeper = new DateKeeper({ + year, month, day, min, max, step, stepBase, firstDayOfWeek, weekends, + calViewSize: CAL_VIEW_SIZE + }); + + this.state = { + dateKeeper, + locale, + isMonthPickerVisible: false, + getDayString: day => day ? new Intl.NumberFormat(locale).format(day) : "", + getWeekHeaderString: weekday => weekdayStrings[weekday], + getMonthString: month => monthStrings[month], + setSelection: date => { + dateKeeper.setSelection({ + year: date.getUTCFullYear(), + month: date.getUTCMonth(), + day: date.getUTCDate(), + }); + this._update(); + this._dispatchState(); + this._closePopup(); + }, + setYear: year => { + dateKeeper.setYear(year); + dateKeeper.setSelection({ + year, + month: dateKeeper.selection.month, + day: dateKeeper.selection.day, + }); + this._update(); + this._dispatchState(); + }, + setMonth: month => { + dateKeeper.setMonth(month); + dateKeeper.setSelection({ + year: dateKeeper.selection.year, + month, + day: dateKeeper.selection.day, + }); + this._update(); + this._dispatchState(); + }, + toggleMonthPicker: () => { + this.state.isMonthPickerVisible = !this.state.isMonthPickerVisible; + this._update(); + } + }; + }, + + /** + * Initalize the date picker components. + */ + _createComponents() { + this.components = { + calendar: new Calendar({ + calViewSize: CAL_VIEW_SIZE, + locale: this.state.locale, + setSelection: this.state.setSelection, + getDayString: this.state.getDayString, + getWeekHeaderString: this.state.getWeekHeaderString + }, { + weekHeader: this.context.weekHeader, + daysView: this.context.daysView + }), + monthYear: new MonthYear({ + setYear: this.state.setYear, + setMonth: this.state.setMonth, + getMonthString: this.state.getMonthString, + locale: this.state.locale + }, { + monthYear: this.context.monthYear, + monthYearView: this.context.monthYearView + }) + }; + }, + + /** + * Update date picker and its components. + */ + _update(options = {}) { + const { dateKeeper, isMonthPickerVisible } = this.state; + + if (isMonthPickerVisible) { + this.state.months = dateKeeper.getMonths(); + this.state.years = dateKeeper.getYears(); + } else { + this.state.days = dateKeeper.getDays(); + } + + this.components.monthYear.setProps({ + isVisible: isMonthPickerVisible, + dateObj: dateKeeper.state.dateObj, + months: this.state.months, + years: this.state.years, + toggleMonthPicker: this.state.toggleMonthPicker, + noSmoothScroll: options.noSmoothScroll + }); + this.components.calendar.setProps({ + isVisible: !isMonthPickerVisible, + days: this.state.days, + weekHeaders: dateKeeper.state.weekHeaders + }); + + isMonthPickerVisible ? + this.context.monthYearView.classList.remove("hidden") : + this.context.monthYearView.classList.add("hidden"); + }, + + /** + * Use postMessage to close the picker. + */ + _closePopup() { + window.postMessage({ + name: "ClosePopup" + }, "*"); + }, + + /** + * Use postMessage to pass the state of picker to the panel. + */ + _dispatchState() { + const { year, month, day } = this.state.dateKeeper.selection; + // The panel is listening to window for postMessage event, so we + // do postMessage to itself to send data to input boxes. + window.postMessage({ + name: "PickerPopupChanged", + detail: { + year, + month, + day, + } + }, "*"); + }, + + /** + * Attach event listeners + */ + _attachEventListeners() { + window.addEventListener("message", this); + document.addEventListener("mouseup", this, { passive: true }); + document.addEventListener("mousedown", this); + }, + + /** + * Handle events. + * + * @param {Event} event + */ + handleEvent(event) { + switch (event.type) { + case "message": { + this.handleMessage(event); + break; + } + case "mousedown": { + // Use preventDefault to keep focus on input boxes + event.preventDefault(); + event.target.setCapture(); + + if (event.target == this.context.buttonLeft) { + event.target.classList.add("active"); + this.state.dateKeeper.setMonthByOffset(-1); + this._update(); + } else if (event.target == this.context.buttonRight) { + event.target.classList.add("active"); + this.state.dateKeeper.setMonthByOffset(1); + this._update(); + } + break; + } + case "mouseup": { + if (event.target == this.context.buttonLeft || event.target == this.context.buttonRight) { + event.target.classList.remove("active"); + } + + } + } + }, + + /** + * Handle postMessage events. + * + * @param {Event} event + */ + handleMessage(event) { + switch (event.data.name) { + case "PickerSetValue": { + this.set(event.data.detail); + break; + } + case "PickerInit": { + this.init(event.data.detail); + break; + } + } + }, + + /** + * Set the date state and update the components with the new state. + * + * @param {Object} dateState + * { + * {Number} year [optional] + * {Number} month [optional] + * {Number} date [optional] + * } + */ + set({ year, month, day }) { + const { dateKeeper } = this.state; + + dateKeeper.setCalendarMonth({ + year, month + }); + dateKeeper.setSelection({ + year, month, day + }); + this._update({ noSmoothScroll: true }); + } + }; + + /** + * MonthYear is a component that handles the month & year spinners + * + * @param {Object} options + * { + * {String} locale + * {Function} setYear + * {Function} setMonth + * {Function} getMonthString + * } + * @param {DOMElement} context + */ + function MonthYear(options, context) { + const spinnerSize = 5; + const yearFormat = new Intl.DateTimeFormat(options.locale, { year: "numeric", + timeZone: "UTC" }).format; + const dateFormat = new Intl.DateTimeFormat(options.locale, { year: "numeric", + month: "long", + timeZone: "UTC" }).format; + this.context = context; + this.state = { dateFormat }; + this.props = {}; + this.components = { + month: new Spinner({ + setValue: month => { + this.state.isMonthSet = true; + options.setMonth(month); + }, + getDisplayString: options.getMonthString, + viewportSize: spinnerSize + }, context.monthYearView), + year: new Spinner({ + setValue: year => { + this.state.isYearSet = true; + options.setYear(year); + }, + getDisplayString: year => yearFormat(new Date(new Date(0).setUTCFullYear(year))), + viewportSize: spinnerSize + }, context.monthYearView) + }; + + this._attachEventListeners(); + } + + MonthYear.prototype = { + + /** + * Set new properties and pass them to components + * + * @param {Object} props + * { + * {Boolean} isVisible + * {Date} dateObj + * {Array<Object>} months + * {Array<Object>} years + * {Function} toggleMonthPicker + * } + */ + setProps(props) { + this.context.monthYear.textContent = this.state.dateFormat(props.dateObj); + + if (props.isVisible) { + this.context.monthYear.classList.add("active"); + this.components.month.setState({ + value: props.dateObj.getUTCMonth(), + items: props.months, + isInfiniteScroll: true, + isValueSet: this.state.isMonthSet, + smoothScroll: !(this.state.firstOpened || props.noSmoothScroll) + }); + this.components.year.setState({ + value: props.dateObj.getUTCFullYear(), + items: props.years, + isInfiniteScroll: false, + isValueSet: this.state.isYearSet, + smoothScroll: !(this.state.firstOpened || props.noSmoothScroll) + }); + this.state.firstOpened = false; + } else { + this.context.monthYear.classList.remove("active"); + this.state.isMonthSet = false; + this.state.isYearSet = false; + this.state.firstOpened = true; + } + + this.props = Object.assign(this.props, props); + }, + + /** + * Handle events + * @param {DOMEvent} event + */ + handleEvent(event) { + switch (event.type) { + case "click": { + this.props.toggleMonthPicker(); + break; + } + } + }, + + /** + * Attach event listener to monthYear button + */ + _attachEventListeners() { + this.context.monthYear.addEventListener("click", this); + } + }; +} diff --git a/toolkit/content/widgets/datetimebox.css b/toolkit/content/widgets/datetimebox.css index 4a9593a69..ce638078f 100644 --- a/toolkit/content/widgets/datetimebox.css +++ b/toolkit/content/widgets/datetimebox.css @@ -8,9 +8,17 @@ .datetime-input-box-wrapper { -moz-appearance: none; display: inline-flex; + flex: 1; cursor: default; background-color: inherit; color: inherit; + min-width: 0; + justify-content: space-between; +} + +.datetime-input-edit-wrapper { + overflow: hidden; + white-space: nowrap; } .datetime-input { @@ -20,6 +28,8 @@ border: 0; margin: 0; ime-mode: disabled; + cursor: default; + -moz-user-select: none; } .datetime-separator { @@ -41,5 +51,5 @@ height: 12px; width: 12px; align-self: center; - justify-content: flex-end; + flex: none; } diff --git a/toolkit/content/widgets/datetimebox.xml b/toolkit/content/widgets/datetimebox.xml index 05591e65a..94574038a 100644 --- a/toolkit/content/widgets/datetimebox.xml +++ b/toolkit/content/widgets/datetimebox.xml @@ -4,12 +4,466 @@ - 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 % datetimeboxDTD SYSTEM "chrome://global/locale/datetimebox.dtd"> +%datetimeboxDTD; +]> + +<!-- +TODO +Bug 1446342: +Input type="date" not working if the other form elements has name="document" + +Any alternative solution: +document === window.document +document === this.ownerDocument +--> + <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="date-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[ + /* eslint-disable no-multi-spaces */ + this.mYearPlaceHolder = ]]>"&date.year.placeholder;"<![CDATA[; + this.mMonthPlaceHolder = ]]>"&date.month.placeholder;"<![CDATA[; + this.mDayPlaceHolder = ]]>"&date.day.placeholder;"<![CDATA[; + this.mSeparatorText = "/"; + /* eslint-enable no-multi-spaces */ + + this.mMinMonth = 1; + this.mMaxMonth = 12; + this.mMinDay = 1; + this.mMaxDay = 31; + this.mMinYear = 1; + // Maximum year limited by ECMAScript date object range, year <= 275760. + this.mMaxYear = 275760; + this.mMonthDayLength = 2; + this.mYearLength = 4; + this.mMonthPageUpDownInterval = 3; + this.mDayPageUpDownInterval = 7; + this.mYearPageUpDownInterval = 10; + + // Default to en-US, month-day-year order. + this.mMonthField = + window.document.getAnonymousElementByAttribute(this, "anonid", "input-one"); + this.mDayField = + window.document.getAnonymousElementByAttribute(this, "anonid", "input-two"); + this.mYearField = + window.document.getAnonymousElementByAttribute(this, "anonid", "input-three"); + this.mYearField.size = this.mYearLength; + this.mYearField.maxLength = this.mMaxYear.toString().length; + + this.mMonthField.placeholder = this.mMonthPlaceHolder; + this.mDayField.placeholder = this.mDayPlaceHolder; + this.mYearField.placeholder = this.mYearPlaceHolder; + + this.mMonthField.setAttribute("min", this.mMinMonth); + this.mMonthField.setAttribute("max", this.mMaxMonth); + this.mMonthField.setAttribute("pginterval", + this.mMonthPageUpDownInterval); + this.mDayField.setAttribute("min", this.mMinDay); + this.mDayField.setAttribute("max", this.mMaxDay); + this.mDayField.setAttribute("pginterval", this.mDayPageUpDownInterval); + this.mYearField.setAttribute("min", this.mMinYear); + this.mYearField.setAttribute("max", this.mMaxYear); + this.mYearField.setAttribute("pginterval", + this.mYearPageUpDownInterval); + + this.mDaySeparator = + window.document.getAnonymousElementByAttribute(this, "anonid", "sep-first"); + this.mDaySeparator.textContent = this.mSeparatorText; + this.mYearSeparator = + window.document.getAnonymousElementByAttribute(this, "anonid", "sep-second"); + this.mYearSeparator.textContent = this.mSeparatorText; + + if (this.mInputElement.value) { + this.setFieldsFromInputValue(); + } + this.updateResetButtonVisibility(); + ]]> + </constructor> + + <method name="clearInputFields"> + <parameter name="aFromInputElement"/> + <body> + <![CDATA[ + this.log("clearInputFields"); + + if (this.isDisabled() || this.isReadonly()) { + return; + } + + if (this.mMonthField && !this.mMonthField.disabled && + !this.mMonthField.readOnly) { + this.mMonthField.value = ""; + this.mMonthField.setAttribute("typeBuffer", ""); + } + + if (this.mDayField && !this.mDayField.disabled && + !this.mDayField.readOnly) { + this.mDayField.value = ""; + this.mDayField.setAttribute("typeBuffer", ""); + } + + if (this.mYearField && !this.mYearField.disabled && + !this.mYearField.readOnly) { + this.mYearField.value = ""; + this.mYearField.setAttribute("typeBuffer", ""); + } + + if (!aFromInputElement && this.mInputElement.value) { + this.mInputElement.setUserInput(""); + } + + this.updateResetButtonVisibility(); + ]]> + </body> + </method> + + <method name="setFieldsFromInputValue"> + <body> + <![CDATA[ + let value = this.mInputElement.value; + if (!value) { + this.clearInputFields(true); + return; + } + + this.log("setFieldsFromInputValue: " + value); + let [year, month, day] = value.split("-"); + + this.setFieldValue(this.mYearField, year); + this.setFieldValue(this.mMonthField, month); + this.setFieldValue(this.mDayField, day); + + this.notifyPicker(); + ]]> + </body> + </method> + + <method name="getDaysInMonth"> + <parameter name="aMonth"/> + <parameter name="aYear"/> + <body> + <![CDATA[ + // Javascript's month is 0-based, so this means last day of the + // previous month. + return new Date(aYear, aMonth, 0).getDate(); + ]]> + </body> + </method> + + <method name="isFieldInvalid"> + <parameter name="aField"/> + <body> + <![CDATA[ + if (this.isEmpty(aField.value)) { + return true; + } + + let min = Number(aField.getAttribute("min")); + let max = Number(aField.getAttribute("max")); + + if (Number(aField.value) < min || Number(aField.value) > max) { + return true; + } + + return false; + ]]> + </body> + </method> + + <method name="setInputValueFromFields"> + <body> + <![CDATA[ + if (!this.isAnyValueAvailable(false) && this.mInputElement.value) { + // Values in the input box was cleared, clear the input element's + // value if not empty. + this.mInputElement.setUserInput(""); + return; + } + + if (this.isFieldInvalid(this.mYearField) || + this.isFieldInvalid(this.mMonthField) || + this.isFieldInvalid(this.mDayField)) { + // 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 year = this.mYearField.value; + let month = this.mMonthField.value; + let day = this.mDayField.value; + + if (day > this.getDaysInMonth(month, year)) { + // Don't set invalid date, otherwise input element's value will be + // set to empty. + return; + } + + let date = [year, month, day].join("-"); + + if (date == this.mInputElement.value) { + return; + } + + this.log("setInputValueFromFields: " + date); + this.mInputElement.setUserInput(date); + ]]> + </body> + </method> + + <method name="setFieldsFromPicker"> + <parameter name="aValue"/> + <body> + <![CDATA[ + let year = aValue.year; + let month = aValue.month; + let day = aValue.day; + + if (!this.isEmpty(year)) { + this.setFieldValue(this.mYearField, year); + } + + if (!this.isEmpty(month)) { + this.setFieldValue(this.mMonthField, month); + } + + if (!this.isEmpty(day)) { + this.setFieldValue(this.mDayField, day); + } + + // Update input element's .value if needed. + 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 (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="incrementFieldValue"> + <parameter name="aTargetField"/> + <parameter name="aTimes"/> + <body> + <![CDATA[ + let value; + + // Use current date if field is empty. + if (this.isEmpty(aTargetField.value)) { + let now = new Date(); + + if (aTargetField == this.mYearField) { + value = now.getFullYear(); + } else if (aTargetField == this.mMonthField) { + value = now.getMonth() + 1; + } else if (aTargetField == this.mDayField) { + value = now.getDate(); + } else { + this.log("Field not supported in incrementFieldValue."); + return; + } + } else { + value = Number(aTargetField.value); + } + + let min = Number(aTargetField.getAttribute("min")); + let max = Number(aTargetField.getAttribute("max")); + + value += Number(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; + + // Home/End key does nothing on year field. + if (targetField == this.mYearField && (key == "Home" || + key == "End")) { + return; + } + + switch (key) { + case "ArrowUp": + this.incrementFieldValue(targetField, 1); + break; + case "ArrowDown": + this.incrementFieldValue(targetField, -1); + break; + case "PageUp": { + let interval = targetField.getAttribute("pginterval"); + this.incrementFieldValue(targetField, interval); + break; + } + case "PageDown": { + let interval = targetField.getAttribute("pginterval"); + this.incrementFieldValue(targetField, 0 - interval); + 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="getCurrentValue"> + <body> + <![CDATA[ + let year; + if (!this.isEmpty(this.mYearField.value)) { + year = Number(this.mYearField.value); + } + + let month; + if (!this.isEmpty(this.mMonthField.value)) { + month = Number(this.mMonthField.value); + } + + let day; + if (!this.isEmpty(this.mDayField.value)) { + day = Number(this.mDayField.value); + } + + let date = { year, month, day }; + + this.log("getCurrentValue: " + JSON.stringify(date)); + return date; + ]]> + </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 (aValue.length == aField.maxLength) { + let min = Number(aField.getAttribute("min")); + let max = Number(aField.getAttribute("max")); + + if (aValue < min) { + value = min; + } else if (aValue > max) { + value = max; + } + } + + if (aField == this.mMonthField || + aField == this.mDayField) { + // prepend zero + if (value < 10) { + value = "0" + value; + } + } else { + // prepend zeroes + if (value < 10) { + value = "000" + value; + } else if (value < 100) { + value = "00" + value; + } else if (value < 1000) { + value = "0" + value; + } + + if (value.toString().length > this.mYearLength && + value.toString().length <= this.mMaxYear.toString().length) { + this.mYearField.size = value.toString().length; + } + } + + aField.value = value; + this.updateResetButtonVisibility(); + ]]> + </body> + </method> + + <method name="isAnyValueAvailable"> + <parameter name="aForPicker"/> + <body> + <![CDATA[ + return !this.isEmpty(this.mMonthField.value) || + !this.isEmpty(this.mDayField.value) || + !this.isEmpty(this.mYearField.value); + ]]> + </body> + </method> + + </implementation> + </binding> + <binding id="time-input" extends="chrome://global/content/bindings/datetimebox.xml#datetime-input-base"> <resources> @@ -45,13 +499,13 @@ this.mMinSecPageUpDownInterval = 10; this.mHourField = - document.getAnonymousElementByAttribute(this, "anonid", "input-one"); + window.document.getAnonymousElementByAttribute(this, "anonid", "input-one"); this.mHourField.setAttribute("typeBuffer", ""); this.mMinuteField = - document.getAnonymousElementByAttribute(this, "anonid", "input-two"); + window.document.getAnonymousElementByAttribute(this, "anonid", "input-two"); this.mMinuteField.setAttribute("typeBuffer", ""); this.mDayPeriodField = - document.getAnonymousElementByAttribute(this, "anonid", "input-three"); + window.document.getAnonymousElementByAttribute(this, "anonid", "input-three"); this.mDayPeriodField.classList.remove("numeric"); this.mHourField.placeholder = this.mPlaceHolder; @@ -64,10 +518,10 @@ this.mMinuteField.setAttribute("max", this.mMaxMinute); this.mMinuteSeparator = - document.getAnonymousElementByAttribute(this, "anonid", "sep-first"); + window.document.getAnonymousElementByAttribute(this, "anonid", "sep-first"); this.mMinuteSeparator.textContent = this.mSeparatorText; this.mSpaceSeparator = - document.getAnonymousElementByAttribute(this, "anonid", "sep-second"); + window.document.getAnonymousElementByAttribute(this, "anonid", "sep-second"); // space between time and am/pm field this.mSpaceSeparator.textContent = " "; @@ -79,6 +533,7 @@ if (this.mInputElement.value) { this.setFieldsFromInputValue(); } + this.updateResetButtonVisibility(); ]]> </constructor> @@ -138,7 +593,7 @@ } this.log("setFieldsFromInputValue: " + value); - let [hour, minute, second] = value.split(':'); + let [hour, minute, second] = value.split(":"); this.setFieldValue(this.mHourField, hour); this.setFieldValue(this.mMinuteField, minute); @@ -204,6 +659,13 @@ <method name="setInputValueFromFields"> <body> <![CDATA[ + if (!this.isAnyValueAvailable(false) && this.mInputElement.value) { + // Values in the input box was cleared, clear the input element's + // value if not empty. + this.mInputElement.setUserInput(""); + return; + } + if (this.isEmpty(this.mHourField.value) || this.isEmpty(this.mMinuteField.value) || (this.mDayPeriodField && this.isEmpty(this.mDayPeriodField.value)) || @@ -239,6 +701,10 @@ time += "." + this.mMillisecField.value; } + if (time == this.mInputElement.value) { + return; + } + this.log("setInputValueFromFields: " + time); this.mInputElement.setUserInput(time); ]]> @@ -265,6 +731,9 @@ if (!this.isEmpty(minute)) { this.setFieldValue(this.mMinuteField, minute); } + + // Update input element's .value if needed. + this.setInputValueFromFields(); ]]> </body> </method> @@ -282,21 +751,25 @@ if (this.mHourField && !this.mHourField.disabled && !this.mHourField.readOnly) { this.mHourField.value = ""; + this.mHourField.setAttribute("typeBuffer", ""); } if (this.mMinuteField && !this.mMinuteField.disabled && !this.mMinuteField.readOnly) { this.mMinuteField.value = ""; + this.mMinuteField.setAttribute("typeBuffer", ""); } if (this.mSecondField && !this.mSecondField.disabled && !this.mSecondField.readOnly) { this.mSecondField.value = ""; + this.mSecondField.setAttribute("typeBuffer", ""); } if (this.mMillisecField && !this.mMillisecField.disabled && !this.mMillisecField.readOnly) { this.mMillisecField.value = ""; + this.mMillisecField.setAttribute("typeBuffer", ""); } if (this.mDayPeriodField && !this.mDayPeriodField.disabled && @@ -304,9 +777,11 @@ this.mDayPeriodField.value = ""; } - if (!aFromInputElement) { + if (!aFromInputElement && this.mInputElement.value) { this.mInputElement.setUserInput(""); } + + this.updateResetButtonVisibility(); ]]> </body> </method> @@ -376,6 +851,7 @@ this.mDayPeriodField.value == this.mAMIndicator ? this.mPMIndicator : this.mAMIndicator; this.mDayPeriodField.select(); + this.updateResetButtonVisibility(); this.setInputValueFromFields(); return; } @@ -433,6 +909,7 @@ this.mDayPeriodField.value = this.mPMIndicator; this.mDayPeriodField.select(); } + this.updateResetButtonVisibility(); return; } @@ -488,16 +965,30 @@ } aField.value = value; + this.updateResetButtonVisibility(); ]]> </body> </method> - <method name="isValueAvailable"> + <method name="isAnyValueAvailable"> + <parameter name="aForPicker"/> <body> <![CDATA[ + let available = !this.isEmpty(this.mHourField.value) || + !this.isEmpty(this.mMinuteField.value); + + if (available) { + return true; + } + // Picker only cares about hour:minute. - return !this.isEmpty(this.mHourField.value) || - !this.isEmpty(this.mMinuteField.value); + if (aForPicker) { + return false; + } + + return (this.mDayPeriodField && !this.isEmpty(this.mDayPeriodField.value)) || + (this.mSecondField && !this.isEmpty(this.mSecondField.value)) || + (this.mMillisecField && !this.isEmpty(this.mMillisecField.value)); ]]> </body> </method> @@ -546,7 +1037,8 @@ <content> <html:div class="datetime-input-box-wrapper" xbl:inherits="context,disabled,readonly"> - <html:span> + <html:span class="datetime-input-edit-wrapper" + anonid="edit-wrapper"> <html:input anonid="input-one" class="textbox-input datetime-input numeric" size="2" maxlength="2" @@ -563,9 +1055,8 @@ 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:button class="datetime-reset-button" anonid="reset-button" + tabindex="-1" xbl:inherits="disabled"/> </html:div> </content> @@ -579,9 +1070,49 @@ this.mMax = this.mInputElement.max; this.mStep = this.mInputElement.step; this.mIsPickerOpen = false; + + this.mResetButton = + window.document.getAnonymousElementByAttribute(this, "anonid", "reset-button"); + + this.EVENTS.forEach((eventName) => { + this.addEventListener(eventName, this, { mozSystemGroup: true }); + }); + // Handle keypress separately since we need to catch it on capturing. + this.addEventListener("keypress", this, { + capture: true, + mozSystemGroup: true + }); + // This is to open the picker when input element is clicked (this + // includes padding area). + this.mInputElement.addEventListener("click", this, + { mozSystemGroup: true }); ]]> </constructor> + <destructor> + <![CDATA[ + this.EVENTS.forEach((eventName) => { + this.removeEventListener(eventName, this, { mozSystemGroup: true }); + }); + this.removeEventListener("keypress", this, { + capture: true, + mozSystemGroup: true + }); + this.mInputElement.removeEventListener("click", this, + { mozSystemGroup: true }); + + this.mInputElement = null; + ]]> + </destructor> + + <property name="EVENTS" readonly="true"> + <getter> + <![CDATA[ + return ["focus", "blur", "copy", "cut", "paste", "mousedown"]; + ]]> + </getter> + </property> + <method name="log"> <parameter name="aMsg"/> <body> @@ -593,11 +1124,23 @@ </body> </method> + <method name="updateResetButtonVisibility"> + <body> + <![CDATA[ + if (this.isAnyValueAvailable(false)) { + this.mResetButton.style.visibility = "visible"; + } else { + this.mResetButton.style.visibility = "hidden"; + } + ]]> + </body> + </method> + <method name="focusInnerTextBox"> <body> <![CDATA[ this.log("focusInnerTextBox"); - document.getAnonymousElementByAttribute(this, "anonid", "input-one").focus(); + window.document.getAnonymousElementByAttribute(this, "anonid", "input-one").focus(); ]]> </body> </method> @@ -710,10 +1253,22 @@ </body> </method> + <method name="getCurrentValue"> + <body> + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + </body> + </method> + + <method name="isAnyValueAvailable"> + <body> + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + </body> + </method> + <method name="notifyPicker"> <body> <![CDATA[ - if (this.mIsPickerOpen && this.isValueAvailable()) { + if (this.mIsPickerOpen && this.isAnyValueAvailable(true)) { this.mInputElement.updateDateTimePicker(this.getCurrentValue()); } ]]> @@ -736,72 +1291,153 @@ </body> </method> - </implementation> - - <handlers> - <handler event="focus"> - <![CDATA[ - this.log("focus on: " + event.originalTarget); + <method name="handleEvent"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + this.log("handleEvent: " + aEvent.type); - let target = event.originalTarget; - if (target.type == "text") { - this.mLastFocusedField = target; - target.select(); - } - ]]> - </handler> + switch (aEvent.type) { + case "keypress": { + this.onKeyPress(aEvent); + break; + } + case "click": { + this.onClick(aEvent); + break; + } + case "focus": { + this.onFocus(aEvent); + break; + } + case "blur": { + this.onBlur(aEvent); + break; + } + case "mousedown": { + if (aEvent.originalTarget == this.mResetButton) { + aEvent.preventDefault(); + } + break; + } + case "copy": + case "cut": + case "paste": { + aEvent.preventDefault(); + break; + } + default: + break; + } + ]]> + </body> + </method> - <handler event="blur"> - <![CDATA[ - this.setInputValueFromFields(); - ]]> - </handler> + <method name="onFocus"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + this.log("onFocus originalTarget: " + aEvent.originalTarget); - <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; - } + let target = aEvent.originalTarget; + if ((target instanceof HTMLInputElement) && target.type == "text") { + this.mLastFocusedField = target; + target.select(); + } + ]]> + </body> + </method> - if (!(event.originalTarget instanceof HTMLButtonElement)) { - this.mInputElement.openDateTimePicker(this.getCurrentValue()); - } - ]]> - </handler> + <method name="onBlur"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + this.log("onBlur originalTarget: " + aEvent.originalTarget + + " target: " + aEvent.target); - <handler event="keypress" phase="capturing"> - <![CDATA[ - let key = event.key; - this.log("keypress: " + key); + let target = aEvent.originalTarget; + target.setAttribute("typeBuffer", ""); + this.setInputValueFromFields(); + ]]> + </body> + </method> - if (key == "Backspace" || key == "Tab") { - return; - } + <method name="onKeyPress"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + this.log("onKeyPress key: " + aEvent.key); + + switch (aEvent.key) { + // Close picker on Enter, Escape or Space key. + case "Enter": + case "Escape": + case " ": { + if (this.mIsPickerOpen) { + this.mInputElement.closeDateTimePicker(); + aEvent.preventDefault(); + } + break; + } + case "Backspace": { + let targetField = aEvent.originalTarget; + targetField.value = ""; + targetField.setAttribute("typeBuffer", ""); + this.updateResetButtonVisibility(); + this.setInputValueFromFields(); + aEvent.preventDefault(); + break; + } + case "ArrowRight": + case "ArrowLeft": { + this.advanceToNextField(aEvent.key == "ArrowRight" ? false : true); + aEvent.preventDefault(); + break; + } + case "ArrowUp": + case "ArrowDown": + case "PageUp": + case "PageDown": + case "Home": + case "End": { + this.handleKeyboardNav(aEvent); + aEvent.preventDefault(); + break; + } + default: { + // printable characters + if (aEvent.keyCode == 0 && + !(aEvent.ctrlKey || aEvent.altKey || aEvent.metaKey)) { + this.handleKeypress(aEvent); + aEvent.preventDefault(); + } + break; + } + } + ]]> + </body> + </method> - if (key == "Enter" || key == " ") { - // Close picker on Enter and Space. - this.mInputElement.closeDateTimePicker(); - } + <method name="onClick"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + this.log("onClick originalTarget: " + aEvent.originalTarget + + " target: " + aEvent.target); - 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); - } + if (aEvent.defaultPrevented || this.isDisabled() || this.isReadonly()) { + return; + } - event.preventDefault(); - ]]> - </handler> - </handlers> + if (aEvent.originalTarget == this.mResetButton) { + this.clearInputFields(false); + } else if (!this.mIsPickerOpen) { + this.mInputElement.openDateTimePicker(this.getCurrentValue()); + } + ]]> + </body> + </method> + </implementation> </binding> </bindings> diff --git a/toolkit/content/widgets/datetimepicker.xml b/toolkit/content/widgets/datetimepicker.xml index 5f16f1ff0..1d6a5e772 100644 --- a/toolkit/content/widgets/datetimepicker.xml +++ b/toolkit/content/widgets/datetimepicker.xml @@ -999,13 +999,13 @@ <body> <![CDATA[ var locale = Intl.DateTimeFormat().resolvedOptions().locale + "-u-ca-gregory"; - var dtfMonth = Intl.DateTimeFormat(locale, {month: "long"}); + var dtfMonth = Intl.DateTimeFormat(locale, {month: "long", timeZone: "UTC"}); var dtfWeekday = Intl.DateTimeFormat(locale, {weekday: "narrow"}); var monthLabel = this.monthField.firstChild; - var tempDate = new Date(2005, 0, 1); + var tempDate = new Date(Date.UTC(2005, 0, 1)); for (var month = 0; month < 12; month++) { - tempDate.setMonth(month); + tempDate.setUTCMonth(month); monthLabel.setAttribute("value", dtfMonth.format(tempDate)); monthLabel = monthLabel.nextSibling; } diff --git a/toolkit/content/widgets/datetimepopup.xml b/toolkit/content/widgets/datetimepopup.xml index 327f45368..b4335e1ce 100644 --- a/toolkit/content/widgets/datetimepopup.xml +++ b/toolkit/content/widgets/datetimepopup.xml @@ -11,17 +11,31 @@ xmlns:xbl="http://www.mozilla.org/xbl"> <binding id="datetime-popup" extends="chrome://global/content/bindings/popup.xml#arrowpanel"> + <resources> + <stylesheet src="chrome://global/skin/datetimepopup.css"/> + </resources> <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"> + <field name="DATE_PICKER_WIDTH" readonly="true">"23.1em"</field> + <field name="DATE_PICKER_HEIGHT" readonly="true">"20.7em"</field> + <constructor><![CDATA[ + this.l10n = {}; + const mozIntl = Components.classes["@mozilla.org/mozintl;1"] + .getService(Components.interfaces.mozIMozIntl); + mozIntl.addGetCalendarInfo(l10n); + mozIntl.addGetDisplayNames(l10n); + // Notify DateTimePickerHelper.jsm that binding is ready. + this.dispatchEvent(new CustomEvent("DateTimePickerBindingReady")); + ]]></constructor> + <method name="openPicker"> <parameter name="type"/> + <parameter name="anchor"/> <parameter name="detail"/> <body><![CDATA[ - this.hidden = false; this.type = type; this.pickerState = {}; // TODO: Resize picker according to content zoom level @@ -35,18 +49,28 @@ this.dateTimePopupFrame.style.height = this.TIME_PICKER_HEIGHT; break; } + case "date": { + this.detail = detail; + this.dateTimePopupFrame.addEventListener("load", this, true); + this.dateTimePopupFrame.setAttribute("src", "chrome://global/content/datepicker.xhtml"); + this.dateTimePopupFrame.style.width = this.DATE_PICKER_WIDTH; + this.dateTimePopupFrame.style.height = this.DATE_PICKER_HEIGHT; + break; + } } + this.hidden = false; + this.openPopup(anchor, "after_start", 0, 0); ]]></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.contentDocument.removeEventListener("message", this, false); this.dateTimePopupFrame.setAttribute("src", ""); + this.hidden = true; ]]></body> </method> <method name="setPopupValue"> @@ -55,25 +79,39 @@ switch (this.type) { case "time": { this.postMessageToPicker({ - name: "TimePickerSetValue", + name: "PickerSetValue", detail: data.value }); break; } + case "date": { + const { year, month, day } = data.value; + this.postMessageToPicker({ + name: "PickerSetValue", + detail: { + year, + // Month value from input box starts from 1 instead of 0 + month: month == undefined ? undefined : month - 1, + day + } + }); + break; + } } ]]></body> </method> <method name="initPicker"> <parameter name="detail"/> <body><![CDATA[ + const locale = Components.classes["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIXULChromeRegistry).getSelectedLocale("global"); + 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", + name: "PickerInit", detail: { hour, minute, @@ -86,6 +124,56 @@ }); break; } + case "date": { + const { year, month, day } = detail.value; + const { firstDayOfWeek, weekends } = + this.getCalendarInfo(locale); + const monthStrings = this.getDisplayNames( + locale, [ + "dates/gregorian/months/january", + "dates/gregorian/months/february", + "dates/gregorian/months/march", + "dates/gregorian/months/april", + "dates/gregorian/months/may", + "dates/gregorian/months/june", + "dates/gregorian/months/july", + "dates/gregorian/months/august", + "dates/gregorian/months/september", + "dates/gregorian/months/october", + "dates/gregorian/months/november", + "dates/gregorian/months/december", + ], "short"); + const weekdayStrings = this.getDisplayNames( + locale, [ + "dates/gregorian/weekdays/sunday", + "dates/gregorian/weekdays/monday", + "dates/gregorian/weekdays/tuesday", + "dates/gregorian/weekdays/wednesday", + "dates/gregorian/weekdays/thursday", + "dates/gregorian/weekdays/friday", + "dates/gregorian/weekdays/saturday", + ], "short"); + + this.postMessageToPicker({ + name: "PickerInit", + detail: { + year, + // Month value from input box starts from 1 instead of 0 + month: month == undefined ? undefined : month - 1, + day, + firstDayOfWeek, + weekends, + monthStrings, + weekdayStrings, + locale, + min: detail.min, + max: detail.max, + step: detail.step, + stepBase: detail.stepBase, + } + }); + break; + } } ]]></body> </method> @@ -109,6 +197,10 @@ } break; } + case "date": { + this.sendPickerValueChanged(this.pickerState); + break; + } } ]]></body> </method> @@ -125,9 +217,60 @@ })); break; } + case "date": { + this.dispatchEvent(new CustomEvent("DateTimePickerValueChanged", { + detail: { + year: value.year, + // Month value from input box starts from 1 instead of 0 + month: value.month == undefined ? undefined : value.month + 1, + day: value.day + } + })); + break; + } } ]]></body> </method> + <method name="getCalendarInfo"> + <parameter name="locale"/> + <body><![CDATA[ + const calendarInfo = this.l10n.getCalendarInfo(locale); + + // Day of week from calendarInfo starts from 1 as Sunday to 7 as Saturday, + // so they need to be mapped to JavaScript convention with 0 as Sunday + // and 6 as Saturday + let firstDayOfWeek = calendarInfo.firstDayOfWeek - 1, + weekendStart = calendarInfo.weekendStart - 1, + weekendEnd = calendarInfo.weekendEnd - 1; + + let weekends = []; + + // Make sure weekendEnd is greater than weekendStart + if (weekendEnd < weekendStart) { + weekendEnd += 7; + } + + // We get the weekends by incrementing weekendStart up to weekendEnd. + // If the start and end is the same day, then weekends only has one day. + for (let day = weekendStart; day <= weekendEnd; day++) { + weekends.push(day % 7); + } + + return { + firstDayOfWeek, + weekends + } + ]]></body> + </method> + <method name="getDisplayNames"> + <parameter name="locale"/> + <parameter name="keys"/> + <parameter name="style"/> + <body><![CDATA[ + const displayNames = this.l10n.getDisplayNames(locale, {keys, style}); + return keys.map(key => displayNames.values[key]); + ]]></body> + </method> <method name="handleEvent"> <parameter name="aEvent"/> <body><![CDATA[ @@ -152,11 +295,16 @@ } switch (aEvent.data.name) { - case "TimePickerPopupChanged": { + case "PickerPopupChanged": { this.pickerState = aEvent.data.detail; this.setInputBoxValue(); break; } + case "ClosePopup": { + this.hidePopup(); + this.closePicker(); + break; + } } ]]></body> </method> @@ -170,12 +318,5 @@ </method> </implementation> - <handlers> - <handler event="popuphiding"> - <![CDATA[ - this.closePicker(); - ]]> - </handler> - </handlers> </binding> </bindings> diff --git a/toolkit/content/widgets/spinner.js b/toolkit/content/widgets/spinner.js index 208ab1931..4901320b5 100644 --- a/toolkit/content/widgets/spinner.js +++ b/toolkit/content/widgets/spinner.js @@ -98,7 +98,7 @@ function Spinner(props, context) { setState(newState) { const { spinner } = this.elements; const { value, items } = this.state; - const { value: newValue, items: newItems, isValueSet, isInvalid } = newState; + const { value: newValue, items: newItems, isValueSet, isInvalid, smoothScroll = true } = newState; if (this._isArrayDiff(newItems, items)) { this.state = Object.assign(this.state, newState); @@ -106,23 +106,23 @@ function Spinner(props, context) { this._scrollTo(newValue, true); } else if (newValue != value) { this.state = Object.assign(this.state, newState); - this._smoothScrollTo(newValue); - } - - if (isValueSet) { - if (isInvalid) { - this._removeSelection(); + if (smoothScroll) { + this._smoothScrollTo(newValue, true); } else { - this._updateSelection(); + this._scrollTo(newValue, true); } } + + if (isValueSet && !isInvalid) { + this._updateSelection(); + } else { + this._removeSelection(); + } }, /** * 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 */ @@ -135,14 +135,8 @@ function Spinner(props, context) { 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) { + // Call setValue if value has changed + if (this.state.value != value) { this.state.value = value; this.props.setValue(value); } @@ -266,11 +260,11 @@ function Spinner(props, context) { * Attach event listeners to the spinner and buttons. */ _attachEventListeners() { - const { spinner } = this.elements; + const { spinner, container } = this.elements; spinner.addEventListener("scroll", this, { passive: true }); - document.addEventListener("mouseup", this, { passive: true }); - document.addEventListener("mousedown", this); + container.addEventListener("mouseup", this, { passive: true }); + container.addEventListener("mousedown", this, { passive: true }); }, /** @@ -288,9 +282,6 @@ function Spinner(props, context) { break; } case "mousedown": { - // Use preventDefault to keep focus on input boxes - event.preventDefault(); - event.target.setCapture(); this.state.mouseState = { down: true, layerX: event.layerX, @@ -300,11 +291,11 @@ function Spinner(props, context) { // An "active" class is needed to simulate :active pseudo-class // because element is not focused. event.target.classList.add("active"); - this._smoothScrollToIndex(index + 1); + this._smoothScrollToIndex(index - 1); } if (event.target == down) { event.target.classList.add("active"); - this._smoothScrollToIndex(index - 1); + this._smoothScrollToIndex(index + 1); } if (event.target.parentNode == spinner) { // Listen to dragging events @@ -444,10 +435,6 @@ function Spinner(props, context) { _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" }); diff --git a/toolkit/content/widgets/timekeeper.js b/toolkit/content/widgets/timekeeper.js index 2234c9e50..3b4e7eb0a 100644 --- a/toolkit/content/widgets/timekeeper.js +++ b/toolkit/content/widgets/timekeeper.js @@ -14,7 +14,7 @@ * { * {Date} min * {Date} max - * {Number} stepInMs + * {Number} step * {String} format: Either "12" or "24" * } */ @@ -286,15 +286,15 @@ function TimeKeeper(props) { * } */ _getSteps(startValue, endValue, minStep, formatter) { - const { min, max, stepInMs } = this.props; + const { min, max, step } = 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); + const timeStep = Math.max(minStep, step); // 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 maxValue = min.valueOf() + Math.floor((max.valueOf() - min.valueOf()) / step) * step; let steps = []; // Increment by timeStep until reaching the end of the range. @@ -410,9 +410,9 @@ function TimeKeeper(props) { * @return {Boolean} */ _isOffStep(time) { - const { min, stepInMs } = this.props; + const { min, step } = this.props; - return (time.valueOf() - min.valueOf()) % stepInMs != 0; + return (time.valueOf() - min.valueOf()) % step != 0; } }; } diff --git a/toolkit/content/widgets/timepicker.js b/toolkit/content/widgets/timepicker.js index f438e9ec6..1f0463fe4 100644 --- a/toolkit/content/widgets/timepicker.js +++ b/toolkit/content/widgets/timepicker.js @@ -13,8 +13,6 @@ function TimePicker(context) { 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 = { @@ -24,9 +22,9 @@ function TimePicker(context) { * { * {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. + * {Number} min: Minimum time, in ms + * {Number} max: Maximum time, in ms + * {Number} step: Step size in ms * {String} format [optional]: "12" for 12 hours, "24" for 24 hours format * {String} locale [optional]: User preferred locale * } @@ -51,11 +49,10 @@ function TimePicker(context) { 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, + min: new Date(Number.isNaN(min) ? 0 : min), + max: new Date(Number.isNaN(max) ? DAY_IN_MS - 1 : max), + step, format: format || "12" }); timeKeeper.setState({ hour: timerHour, minute: timerMinute }); @@ -64,17 +61,6 @@ function TimePicker(context) { }, /** - * 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() { @@ -206,7 +192,7 @@ function TimePicker(context) { // 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", + name: "PickerPopupChanged", detail: { hour, minute, @@ -218,6 +204,7 @@ function TimePicker(context) { }, _attachEventListeners() { window.addEventListener("message", this); + document.addEventListener("mousedown", this); }, /** @@ -231,6 +218,12 @@ function TimePicker(context) { this.handleMessage(event); break; } + case "mousedown": { + // Use preventDefault to keep focus on input boxes + event.preventDefault(); + event.target.setCapture(); + break; + } } }, @@ -241,11 +234,11 @@ function TimePicker(context) { */ handleMessage(event) { switch (event.data.name) { - case "TimePickerSetValue": { + case "PickerSetValue": { this.set(event.data.detail); break; } - case "TimePickerInit": { + case "PickerInit": { this.init(event.data.detail); break; } diff --git a/toolkit/content/widgets/toolbar.xml b/toolkit/content/widgets/toolbar.xml index 548504e24..e1f58f7aa 100644 --- a/toolkit/content/widgets/toolbar.xml +++ b/toolkit/content/widgets/toolbar.xml @@ -30,7 +30,7 @@ </field> <field name="externalToolbars"> - [] + [] </field> <!-- Set by customizeToolbar.js --> @@ -49,18 +49,54 @@ <constructor> <![CDATA[ + this.toolbarInfoSeparators = ["|", "-"]; + this.toolbarInfoLegacySeparator = ":"; // Look to see if there is a toolbarset. this.toolbarset = this.firstChild; - while (this.toolbarset && this.toolbarset.localName != "toolbarset") + 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]); + while (this.toolbarset.hasAttribute("toolbar" + (++index))) { + let hiddingAttribute = + this.toolbarset.getAttribute("type") == "menubar" + ? "autohide" : "collapsed"; + let toolbarInfo = this.toolbarset.getAttribute("toolbar" + index); + let infoSplit = toolbarInfo.split(this.toolbarInfoSeparators[0]); + if (infoSplit.length == 1) { + infoSplit = toolbarInfo.split(this.toolbarInfoLegacySeparator); + } + let infoName = infoSplit[0]; + let infoHidingAttribute = [null, null]; + let infoCurrentSet = ""; + let infoSplitLen = infoSplit.length; + switch (infoSplitLen) { + case 3: + // Pale Moon 27.2+ + // Basilisk (UXP) + infoHidingAttribute = infoSplit[1] + .split(this.toolbarInfoSeparators[1]); + infoCurrentSet = infoSplit[2]; + break; + case 2: + // Legacy: + // - toolbars from Pale Moon 27.0 - 27.1.x + // - Basilisk (moebius) + // The previous value (hiddingAttribute) isn't stored. + infoHidingAttribute = [hiddingAttribute, "false"]; + infoCurrentSet = infoSplit[1]; + break; + default: + Components.utils.reportError( + "Customizable toolbars - an invalid value:" + "\n" + + '"toolbar' + index + '" = "' + toolbarInfo + '"'); + break; + } + this.appendCustomToolbar( + infoName, infoCurrentSet, infoHidingAttribute); } } ]]> @@ -69,6 +105,7 @@ <method name="appendCustomToolbar"> <parameter name="aName"/> <parameter name="aCurrentSet"/> + <parameter name="aHidingAttribute"/> <body> <![CDATA[ if (!this.toolbarset) @@ -84,6 +121,10 @@ toolbar.setAttribute("iconsize", this.getAttribute("iconsize")); toolbar.setAttribute("context", this.toolbarset.getAttribute("context")); toolbar.setAttribute("class", "chromeclass-toolbar"); + // Restore persist the hiding attribute. + if (aHidingAttribute[0]) { + toolbar.setAttribute(aHidingAttribute[0], aHidingAttribute[1]); + } this.insertBefore(toolbar, this.toolbarset); return toolbar; diff --git a/toolkit/locales/en-US/chrome/global/autocomplete.properties b/toolkit/locales/en-US/chrome/global/autocomplete.properties index 5c7c84b96..44da643ac 100644 --- a/toolkit/locales/en-US/chrome/global/autocomplete.properties +++ b/toolkit/locales/en-US/chrome/global/autocomplete.properties @@ -1,6 +1,11 @@ # 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/. +switchToTab = Switch to tab + +# LOCALIZATION NOTE (visitURL): +# %S is the URL to visit. +visitURL = Visit %S # LOCALIZATION NOTE (searchWithEngine): %S will be replaced with # the search engine provider's name. This format was chosen because diff --git a/toolkit/locales/en-US/chrome/global/customizeToolbar.properties b/toolkit/locales/en-US/chrome/global/customizeToolbar.properties index 0ec6d2c1d..b19152fab 100644 --- a/toolkit/locales/en-US/chrome/global/customizeToolbar.properties +++ b/toolkit/locales/en-US/chrome/global/customizeToolbar.properties @@ -5,6 +5,7 @@ enterToolbarTitle=New Toolbar enterToolbarName=Enter a name for this toolbar: enterToolbarDup=There is already a toolbar with the name “%S”. Please enter a different name. +enterToolbarIllegalChars=The name contains illegal character "|". Please enter a different name. enterToolbarBlank=You must enter a name to create a new toolbar. separatorTitle=Separator springTitle=Flexible Space diff --git a/toolkit/locales/en-US/chrome/global/datetimebox.dtd b/toolkit/locales/en-US/chrome/global/datetimebox.dtd new file mode 100644 index 000000000..0deffa6b3 --- /dev/null +++ b/toolkit/locales/en-US/chrome/global/datetimebox.dtd @@ -0,0 +1,9 @@ +<!-- 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/. --> + +<!-- Placeholders for input type=date --> + +<!ENTITY date.year.placeholder "yyyy"> +<!ENTITY date.month.placeholder "mm"> +<!ENTITY date.day.placeholder "dd"> diff --git a/toolkit/locales/jar.mn b/toolkit/locales/jar.mn index e49e978f5..abc96086f 100644 --- a/toolkit/locales/jar.mn +++ b/toolkit/locales/jar.mn @@ -39,6 +39,7 @@ locale/@AB_CD@/global/customizeToolbar.dtd (%chrome/global/customizeToolbar.dtd) locale/@AB_CD@/global/customizeToolbar.properties (%chrome/global/customizeToolbar.properties) #endif + locale/@AB_CD@/global/datetimebox.dtd (%chrome/global/datetimebox.dtd) locale/@AB_CD@/global/datetimepicker.dtd (%chrome/global/datetimepicker.dtd) locale/@AB_CD@/global/dateFormat.properties (%chrome/global/dateFormat.properties) locale/@AB_CD@/global/dialogOverlay.dtd (%chrome/global/dialogOverlay.dtd) diff --git a/toolkit/modules/DateTimePickerHelper.jsm b/toolkit/modules/DateTimePickerHelper.jsm index 398687988..b509742b0 100644 --- a/toolkit/modules/DateTimePickerHelper.jsm +++ b/toolkit/modules/DateTimePickerHelper.jsm @@ -21,6 +21,7 @@ this.EXPORTED_SYMBOLS = [ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); /* * DateTimePickerHelper receives message from content side (input box) and @@ -63,9 +64,13 @@ this.DateTimePickerHelper = { return; } this.picker.closePicker(); + this.close(); break; } case "FormDateTime:UpdatePicker": { + if (!this.picker) { + return; + } this.picker.setPopupValue(aMessage.data); break; } @@ -87,6 +92,7 @@ this.DateTimePickerHelper = { if (browser) { browser.messageManager.sendAsyncMessage("FormDateTime:PickerClosed"); } + this.picker.closePicker(); this.close(); break; } @@ -97,18 +103,15 @@ this.DateTimePickerHelper = { // Called when picker value has changed, notify input box about it. updateInputBoxValue: function(aEvent) { - // TODO: parse data based on input type. - const { hour, minute } = aEvent.detail; - debug("hour: " + hour + ", minute: " + minute); let browser = this.weakBrowser ? this.weakBrowser.get() : null; if (browser) { browser.messageManager.sendAsyncMessage( - "FormDateTime:PickerValueChanged", { hour, minute }); + "FormDateTime:PickerValueChanged", aEvent.detail); } }, // Get picker from browser and show it anchored to the input box. - showPicker: function(aBrowser, aData) { + showPicker: Task.async(function* (aBrowser, aData) { let rect = aData.rect; let dir = aData.dir; let type = aData.type; @@ -138,13 +141,23 @@ this.DateTimePickerHelper = { debug("aBrowser.dateTimePicker not found, exiting now."); return; } - this.picker.loadPicker(type, detail); + // The datetimepopup binding is only attached when it is needed. + // Check if openPicker method is present to determine if binding has + // been attached. If not, attach the binding first before calling it. + if (!this.picker.openPicker) { + let bindingPromise = new Promise(resolve => { + this.picker.addEventListener("DateTimePickerBindingReady", + resolve, {once: true}); + }); + this.picker.setAttribute("active", true); + yield bindingPromise; + } // The arrow panel needs an anchor to work. The popupAnchor (this._anchor) // is a transparent div that the arrow can point to. - this.picker.openPopup(this._anchor, "after_start", rect.left, rect.top); + this.picker.openPicker(type, this._anchor, detail); this.addPickerListeners(); - }, + }), // Picker is closed, do some cleanup. close: function() { diff --git a/toolkit/mozapps/extensions/AddonManager.jsm b/toolkit/mozapps/extensions/AddonManager.jsm index 681c4240a..3913c2088 100644 --- a/toolkit/mozapps/extensions/AddonManager.jsm +++ b/toolkit/mozapps/extensions/AddonManager.jsm @@ -88,9 +88,9 @@ Cu.import("resource://gre/modules/Log.jsm"); // Configure a logger at the parent 'addons' level to format // messages for all the modules under addons.* const PARENT_LOGGER_ID = "addons"; -let parentLogger = Log.repository.getLogger(PARENT_LOGGER_ID); +var parentLogger = Log.repository.getLogger(PARENT_LOGGER_ID); parentLogger.level = Log.Level.Warn; -let formatter = new Log.BasicFormatter(); +var formatter = new Log.BasicFormatter(); // Set parent logger (and its children) to append to // the Javascript section of the Browser Console parentLogger.addAppender(new Log.ConsoleAppender(formatter)); @@ -101,7 +101,7 @@ parentLogger.addAppender(new Log.DumpAppender(formatter)); // Create a new logger (child of 'addons' logger) // for use by the Addons Manager const LOGGER_ID = "addons.manager"; -let logger = Log.repository.getLogger(LOGGER_ID); +var logger = Log.repository.getLogger(LOGGER_ID); // Provide the ability to enable/disable logging // messages at runtime. diff --git a/toolkit/mozapps/extensions/DeferredSave.jsm b/toolkit/mozapps/extensions/DeferredSave.jsm index d7f5b8864..7587ce83b 100644 --- a/toolkit/mozapps/extensions/DeferredSave.jsm +++ b/toolkit/mozapps/extensions/DeferredSave.jsm @@ -12,7 +12,7 @@ Cu.import("resource://gre/modules/osfile.jsm"); Cu.import("resource://gre/modules/Promise.jsm"); // Make it possible to mock out timers for testing -let MakeTimer = () => Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); +var MakeTimer = () => Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); this.EXPORTED_SYMBOLS = ["DeferredSave"]; @@ -23,9 +23,9 @@ Cu.import("resource://gre/modules/Log.jsm"); //Configure a logger at the parent 'DeferredSave' level to format //messages for all the modules under DeferredSave.* const DEFERREDSAVE_PARENT_LOGGER_ID = "DeferredSave"; -let parentLogger = Log.repository.getLogger(DEFERREDSAVE_PARENT_LOGGER_ID); +var parentLogger = Log.repository.getLogger(DEFERREDSAVE_PARENT_LOGGER_ID); parentLogger.level = Log.Level.Warn; -let formatter = new Log.BasicFormatter(); +var formatter = new Log.BasicFormatter(); //Set parent logger (and its children) to append to //the Javascript section of the Browser Console parentLogger.addAppender(new Log.ConsoleAppender(formatter)); diff --git a/toolkit/mozapps/extensions/addonManager.js b/toolkit/mozapps/extensions/addonManager.js index 862b1ea69..731e70c6c 100644 --- a/toolkit/mozapps/extensions/addonManager.js +++ b/toolkit/mozapps/extensions/addonManager.js @@ -31,9 +31,9 @@ const CHILD_SCRIPT = "resource://gre/modules/addons/Content.js"; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); -let gSingleton = null; +var gSingleton = null; -let gParentMM = null; +var gParentMM = null; function amManager() { diff --git a/toolkit/mozapps/extensions/amInstallTrigger.js b/toolkit/mozapps/extensions/amInstallTrigger.js index b83cbe60b..a18fe84c4 100644 --- a/toolkit/mozapps/extensions/amInstallTrigger.js +++ b/toolkit/mozapps/extensions/amInstallTrigger.js @@ -18,7 +18,7 @@ const MSG_INSTALL_ADDONS = "WebInstallerInstallAddonsFromWebpage"; const MSG_INSTALL_CALLBACK = "WebInstallerInstallCallback"; -let log = Log.repository.getLogger("AddonManager.InstallTrigger"); +var log = Log.repository.getLogger("AddonManager.InstallTrigger"); log.level = Log.Level[Preferences.get("extensions.logging.enabled", false) ? "Warn" : "Trace"]; function CallbackObject(id, callback, urls, mediator) { diff --git a/toolkit/mozapps/extensions/amWebInstallListener.js b/toolkit/mozapps/extensions/amWebInstallListener.js index 901beef07..ac6e2495d 100644 --- a/toolkit/mozapps/extensions/amWebInstallListener.js +++ b/toolkit/mozapps/extensions/amWebInstallListener.js @@ -37,7 +37,7 @@ const LOGGER_ID = "addons.weblistener"; // Create a new logger for use by the Addons Web Listener // (Requires AddonManager.jsm) -let logger = Log.repository.getLogger(LOGGER_ID); +var logger = Log.repository.getLogger(LOGGER_ID); function notifyObservers(aTopic, aBrowser, aUri, aInstalls) { let info = { diff --git a/toolkit/mozapps/extensions/content/extensions.js b/toolkit/mozapps/extensions/content/extensions.js index 6f2a47482..8d9c132e6 100644 --- a/toolkit/mozapps/extensions/content/extensions.js +++ b/toolkit/mozapps/extensions/content/extensions.js @@ -2456,7 +2456,7 @@ var gSearchView = { this._allResultsLink.setAttribute("href", AddonRepository.getSearchURL(this._lastQuery)); this._allResultsLink.hidden = false; - }, + }, updateListAttributes: function gSearchView_updateListAttributes() { var item = this._listBox.querySelector("richlistitem[remote='true'][first]"); diff --git a/toolkit/mozapps/extensions/content/update.js b/toolkit/mozapps/extensions/content/update.js index 3d87f4d4b..afc74dca8 100644 --- a/toolkit/mozapps/extensions/content/update.js +++ b/toolkit/mozapps/extensions/content/update.js @@ -22,7 +22,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository", "resource://gre/modul XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Promise", "resource://gre/modules/Promise.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Log", "resource://gre/modules/Log.jsm"); -let logger = null; +var logger = null; var gUpdateWizard = { // When synchronizing app compatibility info this contains all installed @@ -169,7 +169,7 @@ var gOfflinePage = { } // Addon listener to count addons enabled/disabled by metadata checks -let listener = { +var listener = { onDisabled: function listener_onDisabled(aAddon) { gUpdateWizard.affectedAddonIDs.add(aAddon.id); gUpdateWizard.metadataDisabled++; diff --git a/toolkit/mozapps/extensions/internal/AddonRepository.jsm b/toolkit/mozapps/extensions/internal/AddonRepository.jsm index adcecbee7..76a7528c7 100644 --- a/toolkit/mozapps/extensions/internal/AddonRepository.jsm +++ b/toolkit/mozapps/extensions/internal/AddonRepository.jsm @@ -72,7 +72,7 @@ const LOGGER_ID = "addons.repository"; // Create a new logger for use by the Addons Repository // (Requires AddonManager.jsm) -let logger = Log.repository.getLogger(LOGGER_ID); +var logger = Log.repository.getLogger(LOGGER_ID); // A map between XML keys to AddonSearchResult keys for string values // that require no extra parsing from XML @@ -101,7 +101,7 @@ const INTEGER_KEY_MAP = { }; // Wrap the XHR factory so that tests can override with a mock -let XHRequest = Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1", +var XHRequest = Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1", "nsIXMLHttpRequest"); function convertHTMLToPlainText(html) { diff --git a/toolkit/mozapps/extensions/internal/AddonRepository_SQLiteMigrator.jsm b/toolkit/mozapps/extensions/internal/AddonRepository_SQLiteMigrator.jsm index 128146bbe..11944ddf5 100644 --- a/toolkit/mozapps/extensions/internal/AddonRepository_SQLiteMigrator.jsm +++ b/toolkit/mozapps/extensions/internal/AddonRepository_SQLiteMigrator.jsm @@ -30,7 +30,7 @@ const LOGGER_ID = "addons.repository.sqlmigrator"; // Create a new logger for use by the Addons Repository SQL Migrator // (Requires AddonManager.jsm) -let logger = Log.repository.getLogger(LOGGER_ID); +var logger = Log.repository.getLogger(LOGGER_ID); this.EXPORTED_SYMBOLS = ["AddonRepository_SQLiteMigrator"]; diff --git a/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm b/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm index d68a0f175..939e2e269 100644 --- a/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm +++ b/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm @@ -52,7 +52,7 @@ const LOGGER_ID = "addons.update-checker"; // Create a new logger for use by the Addons Update Checker // (Requires AddonManager.jsm) -let logger = Log.repository.getLogger(LOGGER_ID); +var logger = Log.repository.getLogger(LOGGER_ID); /** * A serialisation method for RDF data that produces an identical string diff --git a/toolkit/mozapps/extensions/internal/Content.js b/toolkit/mozapps/extensions/internal/Content.js index 29c0ed8ce..61a8b0323 100644 --- a/toolkit/mozapps/extensions/internal/Content.js +++ b/toolkit/mozapps/extensions/internal/Content.js @@ -8,9 +8,9 @@ const {classes: Cc, interfaces: Ci, utils: Cu} = Components; -let {Services} = Cu.import("resource://gre/modules/Services.jsm", {}); +var {Services} = Cu.import("resource://gre/modules/Services.jsm", {}); -let nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile", +var nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile", "initWithPath"); const MSG_JAR_FLUSH = "AddonJarFlush"; diff --git a/toolkit/mozapps/extensions/internal/GMPProvider.jsm b/toolkit/mozapps/extensions/internal/GMPProvider.jsm index a55457f6e..52affa9ba 100644 --- a/toolkit/mozapps/extensions/internal/GMPProvider.jsm +++ b/toolkit/mozapps/extensions/internal/GMPProvider.jsm @@ -64,11 +64,11 @@ XPCOMUtils.defineLazyGetter(this, "pluginsBundle", XPCOMUtils.defineLazyGetter(this, "gmpService", () => Cc["@mozilla.org/gecko-media-plugin-service;1"].getService(Ci.mozIGeckoMediaPluginChromeService)); -let messageManager = Cc["@mozilla.org/globalmessagemanager;1"] +var messageManager = Cc["@mozilla.org/globalmessagemanager;1"] .getService(Ci.nsIMessageListenerManager); -let gLogger; -let gLogAppenderDump = null; +var gLogger; +var gLogAppenderDump = null; function configureLogging() { if (!gLogger) { @@ -443,7 +443,7 @@ GMPWrapper.prototype = { }, }; -let GMPProvider = { +var GMPProvider = { get name() { return "GMPProvider"; }, _plugins: null, diff --git a/toolkit/mozapps/extensions/internal/LightweightThemeImageOptimizer.jsm b/toolkit/mozapps/extensions/internal/LightweightThemeImageOptimizer.jsm index fccde9a81..1e7d6b0d8 100644 --- a/toolkit/mozapps/extensions/internal/LightweightThemeImageOptimizer.jsm +++ b/toolkit/mozapps/extensions/internal/LightweightThemeImageOptimizer.jsm @@ -49,7 +49,7 @@ this.LightweightThemeImageOptimizer = { Object.freeze(LightweightThemeImageOptimizer); -let ImageCropper = { +var ImageCropper = { _inProgress: {}, getCroppedImageURL: @@ -119,7 +119,7 @@ let ImageCropper = { } }; -let ImageFile = { +var ImageFile = { read: function ImageFile_read(aURI, aCallback) { this._netUtil.asyncFetch2( aURI, @@ -158,7 +158,7 @@ let ImageFile = { XPCOMUtils.defineLazyModuleGetter(ImageFile, "_netUtil", "resource://gre/modules/NetUtil.jsm", "NetUtil"); -let ImageTools = { +var ImageTools = { decode: function ImageTools_decode(aInputStream, aContentType) { let outParam = {value: null}; @@ -187,7 +187,7 @@ let ImageTools = { XPCOMUtils.defineLazyServiceGetter(ImageTools, "_imgTools", "@mozilla.org/image/tools;1", "imgITools"); -let Utils = { +var Utils = { createCopy: function Utils_createCopy(aData) { let copy = {}; for (let [k, v] in Iterator(aData)) { diff --git a/toolkit/mozapps/extensions/internal/PluginProvider.jsm b/toolkit/mozapps/extensions/internal/PluginProvider.jsm index 04a4f9d7c..cb07dcb12 100644 --- a/toolkit/mozapps/extensions/internal/PluginProvider.jsm +++ b/toolkit/mozapps/extensions/internal/PluginProvider.jsm @@ -23,7 +23,7 @@ const LOGGER_ID = "addons.plugins"; // Create a new logger for use by the Addons Plugin Provider // (Requires AddonManager.jsm) -let logger = Log.repository.getLogger(LOGGER_ID); +var logger = Log.repository.getLogger(LOGGER_ID); function getIDHashForString(aStr) { // return the two-digit hexadecimal code for a byte diff --git a/toolkit/mozapps/extensions/internal/XPIProvider.jsm b/toolkit/mozapps/extensions/internal/XPIProvider.jsm index d5f1ab5dd..2c5e3dfa7 100644 --- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm +++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm @@ -129,7 +129,7 @@ const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#"; const TOOLKIT_ID = "toolkit@mozilla.org"; #ifdef MOZ_PHOENIX_EXTENSIONS const FIREFOX_ID = "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}" -const FIREFOX_APPCOMPATVERSION = "27.9" +const FIREFOX_APPCOMPATVERSION = "56.9" #endif // The value for this is in Makefile.in @@ -222,7 +222,7 @@ const LOGGER_ID = "addons.xpi"; // Create a new logger for use by all objects in this Addons XPI Provider module // (Requires AddonManager.jsm) -let logger = Log.repository.getLogger(LOGGER_ID); +var logger = Log.repository.getLogger(LOGGER_ID); const LAZY_OBJECTS = ["XPIDatabase"]; @@ -7822,7 +7822,7 @@ WinRegInstallLocation.prototype = { }; #endif -let addonTypes = [ +var addonTypes = [ new AddonManagerPrivate.AddonType("extension", URI_EXTENSION_STRINGS, STRING_TYPE_NAME, AddonManager.VIEW_TYPE_LIST, 4000, diff --git a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js index 2cef907f1..d26029455 100644 --- a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js +++ b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js @@ -29,7 +29,7 @@ const LOGGER_ID = "addons.xpi-utils"; // Create a new logger for use by the Addons XPI Provider Utils // (Requires AddonManager.jsm) -let logger = Log.repository.getLogger(LOGGER_ID); +var logger = Log.repository.getLogger(LOGGER_ID); const KEY_PROFILEDIR = "ProfD"; const FILE_DATABASE = "extensions.sqlite"; diff --git a/toolkit/mozapps/extensions/test/browser/browser-common.ini b/toolkit/mozapps/extensions/test/browser/browser-common.ini index eaab29f75..3e88833ef 100644 --- a/toolkit/mozapps/extensions/test/browser/browser-common.ini +++ b/toolkit/mozapps/extensions/test/browser/browser-common.ini @@ -39,8 +39,6 @@ skip-if = true # Bug 1093190 - Disabled due to leak [browser_discovery.js] skip-if = e10s # Bug ?????? - test times out on try on all platforms, but works locally for markh! [browser_dragdrop.js] -skip-if = buildapp == 'mulet' -[browser_experiments.js] skip-if = e10s [browser_list.js] [browser_metadataTimeout.js] diff --git a/toolkit/mozapps/installer/find-dupes.py b/toolkit/mozapps/installer/find-dupes.py index bd0561c97..34ef675f4 100644 --- a/toolkit/mozapps/installer/find-dupes.py +++ b/toolkit/mozapps/installer/find-dupes.py @@ -4,15 +4,8 @@ import sys import hashlib -import re -from mozbuild.preprocessor import Preprocessor -from mozbuild.util import DefinesAction from mozpack.packager.unpack import UnpackFinder -from mozpack.files import DeflatedFile from collections import OrderedDict -from StringIO import StringIO -import argparse -import buildconfig ''' Find files duplicated in a given packaged directory, independently of its @@ -20,116 +13,36 @@ package format. ''' -def normalize_osx_path(p): - ''' - Strips the first 3 elements of an OSX app path - - >>> normalize_osx_path('Nightly.app/foo/bar/baz') - 'baz' - ''' - bits = p.split('/') - if len(bits) > 3 and bits[0].endswith('.app'): - return '/'.join(bits[3:]) - return p - - -def normalize_l10n_path(p): - ''' - Normalizes localized paths to en-US - - >>> normalize_l10n_path('chrome/es-ES/locale/branding/brand.properties') - 'chrome/en-US/locale/branding/brand.properties' - >>> normalize_l10n_path('chrome/fr/locale/fr/browser/aboutHome.dtd') - 'chrome/en-US/locale/en-US/browser/aboutHome.dtd' - ''' - # Keep a trailing slash here! e.g. locales like 'br' can transform - # 'chrome/br/locale/branding/' into 'chrome/en-US/locale/en-USanding/' - p = re.sub(r'chrome/(\S+)/locale/\1/', - 'chrome/en-US/locale/en-US/', - p) - p = re.sub(r'chrome/(\S+)/locale/', - 'chrome/en-US/locale/', - p) - return p - - -def normalize_path(p): - return normalize_osx_path(normalize_l10n_path(p)) - - -def find_dupes(source, allowed_dupes, bail=True): - allowed_dupes = set(allowed_dupes) +def find_dupes(source): md5s = OrderedDict() for p, f in UnpackFinder(source): content = f.open().read() m = hashlib.md5(content).digest() - if m not in md5s: - if isinstance(f, DeflatedFile): - compressed = f.file.compressed_size - else: - compressed = len(content) - md5s[m] = (len(content), compressed, []) - md5s[m][2].append(p) + if not m in md5s: + md5s[m] = (len(content), []) + md5s[m][1].append(p) total = 0 - total_compressed = 0 num_dupes = 0 - unexpected_dupes = [] - for m, (size, compressed, paths) in sorted(md5s.iteritems(), - key=lambda x: x[1][1]): + for m, (size, paths) in md5s.iteritems(): if len(paths) > 1: - print 'Duplicates %d bytes%s%s:' % (size, - ' (%d compressed)' % compressed if compressed != size else '', + print 'Duplicates %d bytes%s:' % (size, ' (%d times)' % (len(paths) - 1) if len(paths) > 2 else '') print ''.join(' %s\n' % p for p in paths) total += (len(paths) - 1) * size - total_compressed += (len(paths) - 1) * compressed num_dupes += 1 - - unexpected_dupes.extend([p for p in paths if normalize_path(p) not in allowed_dupes]) - if num_dupes: - print "WARNING: Found %d duplicated files taking %d bytes (%s)" % \ - (num_dupes, total, - '%d compressed' % total_compressed if total_compressed != total - else 'uncompressed') - - if unexpected_dupes: - errortype = "ERROR" if bail else "WARNING" - print "%s: The following duplicated files are not allowed:" % errortype - print "\n".join(unexpected_dupes) - if bail: - sys.exit(1) + print "WARNING: Found %d duplicated files taking %d bytes" % \ + (num_dupes, total) + " (uncompressed)" def main(): - parser = argparse.ArgumentParser(description='Find duplicate files in directory.') - parser.add_argument('--warning', '-w', action='store_true', - help='Only warn about duplicates, do not exit with an error') - parser.add_argument('--file', '-f', action='append', dest='dupes_files', default=[], - help='Add exceptions to the duplicate list from this file') - parser.add_argument('-D', action=DefinesAction) - parser.add_argument('-U', action='append', default=[]) - parser.add_argument('directory', - help='The directory to check for duplicates in') - - args = parser.parse_args() - - allowed_dupes = [] - for filename in args.dupes_files: - pp = Preprocessor() - pp.context.update(buildconfig.defines) - if args.D: - pp.context.update(args.D) - for undefine in args.U: - if undefine in pp.context: - del pp.context[undefine] - pp.out = StringIO() - pp.do_filter('substitution') - pp.do_include(filename) - allowed_dupes.extend([line.partition('#')[0].rstrip() - for line in pp.out.getvalue().splitlines()]) + if len(sys.argv) != 2: + import os + print >>sys.stderr, "Usage: %s directory" % \ + os.path.basename(sys.argv[0]) + sys.exit(1) - find_dupes(args.directory, bail=not args.warning, allowed_dupes=allowed_dupes) + find_dupes(sys.argv[1]) if __name__ == "__main__": main() diff --git a/toolkit/mozapps/installer/packager.mk b/toolkit/mozapps/installer/packager.mk index 68247e7df..71a956aa4 100644 --- a/toolkit/mozapps/installer/packager.mk +++ b/toolkit/mozapps/installer/packager.mk @@ -54,7 +54,7 @@ stage-package: $(MOZ_PKG_MANIFEST) $(MOZ_PKG_MANIFEST_DEPS) $(addprefix --unify ,$(UNIFY_DIST)) \ $(MOZ_PKG_MANIFEST) $(DIST) $(DIST)/$(STAGEPATH)$(MOZ_PKG_DIR)$(if $(MOZ_PKG_MANIFEST),,$(_BINPATH)) \ $(if $(filter omni,$(MOZ_PACKAGER_FORMAT)),$(if $(NON_OMNIJAR_FILES),--non-resource $(NON_OMNIJAR_FILES))) - $(PYTHON) $(MOZILLA_DIR)/toolkit/mozapps/installer/find-dupes.py $(DEFINES) $(ACDEFINES) $(MOZ_PKG_DUPEFLAGS) $(DIST)/$(STAGEPATH)$(MOZ_PKG_DIR) + $(PYTHON) $(MOZILLA_DIR)/toolkit/mozapps/installer/find-dupes.py $(DIST)/$(STAGEPATH)$(MOZ_PKG_DIR) ifdef MOZ_PACKAGE_JSSHELL # Package JavaScript Shell @echo 'Packaging JavaScript Shell...' diff --git a/toolkit/mozapps/webextensions/test/browser/browser-common.ini b/toolkit/mozapps/webextensions/test/browser/browser-common.ini index eda266e2f..83920465b 100644 --- a/toolkit/mozapps/webextensions/test/browser/browser-common.ini +++ b/toolkit/mozapps/webextensions/test/browser/browser-common.ini @@ -34,7 +34,6 @@ skip-if = true # Bug 1093190 - Disabled due to leak [browser_discovery.js] [browser_dragdrop.js] skip-if = buildapp == 'mulet' -[browser_experiments.js] [browser_list.js] [browser_metadataTimeout.js] [browser_searching.js] diff --git a/toolkit/themes/shared/datetimeinputpickers.css b/toolkit/themes/shared/datetimeinputpickers.css new file mode 100644 index 000000000..f0c4315e5 --- /dev/null +++ b/toolkit/themes/shared/datetimeinputpickers.css @@ -0,0 +1,377 @@ +/* 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/. */ + +:root { + --font-size-default: 1.1rem; + --spinner-width: 3rem; + --spinner-margin-top-bottom: 0.4rem; + --spinner-item-height: 2.4rem; + --spinner-item-margin-bottom: 0.1rem; + --spinner-button-height: 1.2rem; + --colon-width: 2rem; + --day-period-spacing-width: 1rem; + --calendar-width: 23.1rem; + --date-picker-item-height: 2.5rem; + --date-picker-item-width: 3.3rem; + + --border: 0.1rem solid #D6D6D6; + --border-radius: 0.3rem; + --border-active-color: #B1B1B1; + + --font-color: #191919; + --fill-color: #EBEBEB; + + --today-fill-color: rgb(212, 212, 212); + + --selected-font-color: #FFFFFF; + --selected-fill-color: #0996F8; + + --button-font-color: #858585; + --button-font-color-hover: #4D4D4D; + --button-font-color-active: #191919; + --button-fill-color-active: #D4D4D4; + + --weekday-header-font-color: #6C6C6C; + --weekend-header-font-color: rgb(218, 78, 68); + + --weekend-font-color: rgb(218, 78, 68); + --weekday-outside-font-color: rgb(153, 153, 153); + --weekend-outside-font-color: rgb(255, 152, 143); + + --weekday-disabled-font-color: rgba(25, 25, 25, 0.2); + --weekend-disabled-font-color: rgba(218, 78, 68, 0.2); + --disabled-fill-color: rgba(235, 235, 235, 0.8); + + --disabled-opacity: 0.2; +} + +html { + font-size: 10px; +} + +body { + margin: 0; + color: var(--font-color); + font: message-box; + font-size: var(--font-size-default); +} + +button { + -moz-appearance: none; + background: none; + border: none; +} + +.nav { + display: flex; + width: var(--calendar-width); + height: 2.4rem; + margin-bottom: 0.8rem; + justify-content: space-between; +} + +.nav > button { + width: 3rem; + height: var(--date-picker-item-height); + background-color: var(--button-font-color); +} + +.nav > button:hover { + background-color: var(--button-font-color-hover); +} + +.nav > button.active { + background-color: var(--button-font-color-active); +} + +.nav > button.left { + background: url("chrome://global/skin/icons/calendar-arrows.svg#left") no-repeat 50% 50%; +} + +.nav > button.right { + background: url("chrome://global/skin/icons/calendar-arrows.svg#right") no-repeat 50% 50%; +} + +.month-year-container { + position: absolute; + display: flex; + justify-content: center; + align-items: center; + top: 0; + left: 3rem; + width: 17.1rem; + height: var(--date-picker-item-height); + z-index: 10; +} + +button.month-year { + font-size: 1.3rem; + border: var(--border); + border-radius: 0.3rem; + padding: 0.2rem 2.6rem 0.2rem 1.2rem; +} + +button.month-year:hover { + background: var(--fill-color); +} + +button.month-year.active { + border-color: var(--border-active-color); + background: var(--button-fill-color-active); +} + +button.month-year::after { + position: absolute; + content: ""; + width: 2.6rem; + height: 1.6rem; + background: url("chrome://global/skin/icons/spinner-arrows.svg#down") no-repeat 50% 50%; +} + +button.month-year.active::after { + background: url("chrome://global/skin/icons/spinner-arrows.svg#up") no-repeat 50% 50%; +} + +.month-year-view { + position: absolute; + z-index: 5; + padding-top: 3.2rem; + top: 0; + left: 0; + bottom: 0; + width: var(--calendar-width); + background: window; + opacity: 1; + transition: opacity 0.15s; +} + +.month-year-view.hidden { + visibility: hidden; + opacity: 0; +} + +.month-year-view > .spinner-container { + width: 5.5rem; + margin: 0 0.5rem; +} + +.month-year-view .spinner { + transform: scaleY(1); + transform-origin: top; + transition: transform 0.15s; +} + +.month-year-view.hidden .spinner { + transform: scaleY(0); + transition: none; +} + +.month-year-view .spinner > div { + transform: scaleY(1); + transition: transform 0.15s; +} + +.month-year-view.hidden .spinner > div { + transform: scaleY(2.5); + transition: none; +} + +.calendar-container { + cursor: default; + display: flex; + flex-direction: column; + width: var(--calendar-width); +} + +.week-header { + display: flex; +} + +.week-header > div { + color: var(--weekday-header-font-color); +} + +.week-header > div.weekend { + color: var(--weekend-header-font-color); +} + +.days-viewport { + height: 15rem; + overflow: hidden; + position: relative; +} + +.days-view { + position: absolute; + display: flex; + flex-wrap: wrap; + flex-direction: row; +} + +.week-header > div, +.days-view > div { + align-items: center; + display: flex; + height: var(--date-picker-item-height); + position: relative; + justify-content: center; + width: var(--date-picker-item-width); +} + +.days-view > .outside { + color: var(--weekday-outside-font-color); +} + +.days-view > .weekend { + color: var(--weekend-font-color); +} + +.days-view > .weekend.outside { + color: var(--weekend-outside-font-color); +} + +.days-view > .out-of-range, +.days-view > .off-step { + color: var(--weekday-disabled-font-color); + background: var(--disabled-fill-color); +} + +.days-view > .out-of-range.weekend, +.days-view > .off-step.weekend { + color: var(--weekend-disabled-font-color); +} + +.days-view > .today { + font-weight: bold; +} + +.days-view > .out-of-range::before, +.days-view > .off-step::before { + display: none; +} + +.days-view > div:hover::before, +.days-view > .select::before, +.days-view > .today::before { + top: 5%; + bottom: 5%; + left: 5%; + right: 5%; +} + +#time-picker, +.month-year-view { + display: flex; + flex-direction: row; + justify-content: center; +} + +.spinner-container { + display: flex; + flex-direction: column; + width: var(--spinner-width); +} + +.spinner-container > button { + background-color: var(--button-font-color); + height: var(--spinner-button-height); +} + +.spinner-container > button:hover { + background-color: var(--button-font-color-hover); +} + +.spinner-container > button.active { + background-color: var(--button-font-color-active); +} + +.spinner-container > button.up { + background: url("chrome://global/skin/icons/spinner-arrows.svg#up") no-repeat 50% 50%; +} + +.spinner-container > button.down { + background: url("chrome://global/skin/icons/spinner-arrows.svg#down") no-repeat 50% 50%; +} + +.spinner-container.hide-buttons > button { + visibility: hidden; +} + +.spinner-container > .spinner { + position: relative; + width: 100%; + margin: var(--spinner-margin-top-bottom) 0; + cursor: default; + overflow-y: scroll; + scroll-snap-type: mandatory; + scroll-snap-points-y: repeat(100%); +} + +.spinner-container > .spinner > div { + box-sizing: border-box; + position: relative; + text-align: center; + padding: calc((var(--spinner-item-height) - var(--font-size-default)) / 2) 0; + margin-bottom: var(--spinner-item-margin-bottom); + height: var(--spinner-item-height); + -moz-user-select: none; + scroll-snap-coordinate: 0 0; +} + +.spinner-container > .spinner > div::before, +.calendar-container .days-view > div::before { + position: absolute; + top: 5%; + bottom: 5%; + left: 5%; + right: 5%; + z-index: -10; + border-radius: var(--border-radius); +} + +.spinner-container > .spinner > div:hover::before, +.calendar-container .days-view > div:hover::before { + background: var(--fill-color); + border: var(--border); + content: ""; +} + +.calendar-container .days-view > div.today::before { + background: var(--today-fill-color); + content: ""; +} + +.spinner-container > .spinner:not(.scrolling) > div.selection, +.calendar-container .days-view > div.selection { + color: var(--selected-font-color); +} + +.spinner-container > .spinner > div.selection::before, +.calendar-container .days-view > div.selection::before { + background: var(--selected-fill-color); + border: none; + content: ""; +} + +.spinner-container > .spinner > div.disabled::before, +.spinner-container > .spinner.scrolling > div.selection::before, +.spinner-container > .spinner.scrolling > div:hover::before { + display: none; +} + +.spinner-container > .spinner > div.disabled { + opacity: var(--disabled-opacity); +} + +.colon { + display: flex; + justify-content: center; + align-items: center; + width: var(--colon-width); + margin-bottom: 0.3rem; +} + +.spacer { + width: var(--day-period-spacing-width); +}
\ No newline at end of file diff --git a/toolkit/themes/shared/datetimepopup.css b/toolkit/themes/shared/datetimepopup.css new file mode 100644 index 000000000..52f6fc7a2 --- /dev/null +++ b/toolkit/themes/shared/datetimepopup.css @@ -0,0 +1,11 @@ +/* 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"); + +panel[type="arrow"][side="top"], +panel[type="arrow"][side="bottom"] { + margin-left: 0; + margin-right: 0; +} diff --git a/toolkit/themes/shared/icons/calendar-arrows.svg b/toolkit/themes/shared/icons/calendar-arrows.svg new file mode 100644 index 000000000..858676f55 --- /dev/null +++ b/toolkit/themes/shared/icons/calendar-arrows.svg @@ -0,0 +1,13 @@ +<?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/. --> +<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"> + <style> + path:not(:target) { + display: none; + } + </style> + <path id="right" d="M4.8 14L3 12.3 8.5 7 3 1.7 4.8 0 12 7"/> + <path id="left" d="M9.2 0L11 1.7 5.5 7 11 12.3 9.2 14 2 7"/> +</svg> diff --git a/toolkit/themes/shared/icons/spinner-arrows.svg b/toolkit/themes/shared/icons/spinner-arrows.svg new file mode 100644 index 000000000..a8ba72d6b --- /dev/null +++ b/toolkit/themes/shared/icons/spinner-arrows.svg @@ -0,0 +1,13 @@ +<?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/. --> +<svg xmlns="http://www.w3.org/2000/svg" width="10" height="6" viewBox="0 0 10 6"> + <style> + path:not(:target) { + display: none; + } + </style> + <path id="down" d="M0 1l1-1 4 4 4-4 1 1-5 5"/> + <path id="up" d="M0 5l1 1 4-4 4 4 1-1-5-5"/> +</svg> diff --git a/toolkit/themes/shared/jar.inc.mn b/toolkit/themes/shared/jar.inc.mn index 9c3d86a40..bdfca2a05 100644 --- a/toolkit/themes/shared/jar.inc.mn +++ b/toolkit/themes/shared/jar.inc.mn @@ -21,12 +21,15 @@ toolkit.jar: skin/classic/global/aboutSupport.css (../../shared/aboutSupport.css) skin/classic/global/appPicker.css (../../shared/appPicker.css) skin/classic/global/config.css (../../shared/config.css) - skin/classic/global/timepicker.css (../../shared/timepicker.css) + skin/classic/global/datetimeinputpickers.css (../../shared/datetimeinputpickers.css) + skin/classic/global/datetimepopup.css (../../shared/datetimepopup.css) + skin/classic/global/icons/calendar-arrows.svg (../../shared/icons/calendar-arrows.svg) skin/classic/global/icons/find-arrows.svg (../../shared/icons/find-arrows.svg) skin/classic/global/icons/info.svg (../../shared/incontent-icons/info.svg) skin/classic/global/icons/input-clear.svg (../../shared/icons/input-clear.svg) skin/classic/global/icons/loading.png (../../shared/icons/loading.png) skin/classic/global/icons/loading@2x.png (../../shared/icons/loading@2x.png) + skin/classic/global/icons/spinner-arrows.svg (../../shared/icons/spinner-arrows.svg) skin/classic/global/icons/warning.svg (../../shared/incontent-icons/warning.svg) skin/classic/global/icons/blocked.svg (../../shared/incontent-icons/blocked.svg) skin/classic/global/alerts/alert-common.css (../../shared/alert-common.css) diff --git a/toolkit/themes/shared/timepicker.css b/toolkit/themes/shared/timepicker.css deleted file mode 100644 index e8d081b30..000000000 --- a/toolkit/themes/shared/timepicker.css +++ /dev/null @@ -1,153 +0,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/. */ - -:root { - --font-size-default: 1.1rem; - --spinner-width: 3rem; - --spinner-margin-top-bottom: 0.4rem; - --spinner-item-height: 2.4rem; - --spinner-item-margin-bottom: 0.1rem; - --spinner-button-height: 1.2rem; - --colon-width: 2rem; - --day-period-spacing-width: 1rem; - - --border: 0.1rem solid #D6D6D6; - --border-radius: 0.3rem; - - --font-color: #191919; - --fill-color: #EBEBEB; - - --selected-font-color: #FFFFFF; - --selected-fill-color: #0996F8; - - --button-font-color: #858585; - --button-font-color-hover: #4D4D4D; - --button-font-color-active: #191919; - - --disabled-opacity: 0.2; -} - -html { - font-size: 10px; -} - -body { - margin: 0; - color: var(--font-color); - font-size: var(--font-size-default); -} - -#time-picker { - display: flex; - flex-direction: row; - justify-content: space-around; -} - -.spinner-container { - font-family: sans-serif; - display: flex; - flex-direction: column; - width: var(--spinner-width); -} - -.spinner-container > button { - -moz-appearance: none; - border: none; - background: none; - background-color: var(--button-font-color); - height: var(--spinner-button-height); -} - -.spinner-container > button:hover { - background-color: var(--button-font-color-hover); -} - -.spinner-container > button.active { - background-color: var(--button-font-color-active); -} - -.spinner-container > button.up { - mask: url("chrome://global/skin/icons/find-arrows.svg#glyph-find-previous") no-repeat 50% 50%; -} - -.spinner-container > button.down { - mask: url("chrome://global/skin/icons/find-arrows.svg#glyph-find-next") no-repeat 50% 50%; -} - -.spinner-container.hide-buttons > button { - visibility: hidden; -} - -.spinner-container > .spinner { - position: relative; - width: 100%; - margin: var(--spinner-margin-top-bottom) 0; - cursor: default; - overflow-y: scroll; - scroll-snap-type: mandatory; - scroll-snap-points-y: repeat(100%); -} - -.spinner-container > .spinner > div { - box-sizing: border-box; - position: relative; - text-align: center; - padding: calc((var(--spinner-item-height) - var(--font-size-default)) / 2) 0; - margin-bottom: var(--spinner-item-margin-bottom); - height: var(--spinner-item-height); - -moz-user-select: none; - scroll-snap-coordinate: 0 0; -} - -.spinner-container > .spinner > div:hover::before { - background: var(--fill-color); - border: var(--border); - border-radius: var(--border-radius); - content: ""; - position: absolute; - top: 0%; - bottom: 0%; - left: 0%; - right: 0%; - z-index: -10; -} - -.spinner-container > .spinner:not(.scrolling) > div.selection { - color: var(--selected-font-color); -} - -.spinner-container > .spinner > div.selection::before { - background: var(--selected-fill-color); - border: none; - border-radius: var(--border-radius); - content: ""; - position: absolute; - top: 0%; - bottom: 0%; - left: 0%; - right: 0%; - z-index: -10; -} - -.spinner-container > .spinner > div.disabled::before, -.spinner-container > .spinner.scrolling > div.selection::before, -.spinner-container > .spinner.scrolling > div:hover::before { - display: none; -} - -.spinner-container > .spinner > div.disabled { - opacity: var(--disabled-opacity); -} - -.colon { - display: flex; - justify-content: center; - align-items: center; - width: var(--colon-width); - margin-bottom: 0.3rem; -} - -.spacer { - width: var(--day-period-spacing-width); -}
\ No newline at end of file diff --git a/widget/moz.build b/widget/moz.build index 09192179f..f69f2d87c 100644 --- a/widget/moz.build +++ b/widget/moz.build @@ -65,7 +65,6 @@ XPIDL_SOURCES += [ 'nsIClipboardHelper.idl', 'nsIClipboardOwner.idl', 'nsIColorPicker.idl', - 'nsIDatePicker.idl', 'nsIDisplayInfo.idl', 'nsIDragService.idl', 'nsIDragSession.idl', @@ -151,7 +150,6 @@ UNIFIED_SOURCES += [ 'nsClipboardProxy.cpp', 'nsColorPickerProxy.cpp', 'nsContentProcessWidgetFactory.cpp', - 'nsDatePickerProxy.cpp', 'nsDragServiceProxy.cpp', 'nsFilePickerProxy.cpp', 'nsHTMLFormatConverter.cpp', diff --git a/widget/nsContentProcessWidgetFactory.cpp b/widget/nsContentProcessWidgetFactory.cpp index f8eaee250..2f00f1c83 100644 --- a/widget/nsContentProcessWidgetFactory.cpp +++ b/widget/nsContentProcessWidgetFactory.cpp @@ -9,7 +9,6 @@ #include "nsWidgetsCID.h" #include "nsClipboardProxy.h" #include "nsColorPickerProxy.h" -#include "nsDatePickerProxy.h" #include "nsDragServiceProxy.h" #include "nsFilePickerProxy.h" #include "nsScreenManagerProxy.h" @@ -22,7 +21,6 @@ using namespace mozilla::widget; NS_GENERIC_FACTORY_CONSTRUCTOR(nsClipboardProxy) NS_GENERIC_FACTORY_CONSTRUCTOR(nsColorPickerProxy) -NS_GENERIC_FACTORY_CONSTRUCTOR(nsDatePickerProxy) NS_GENERIC_FACTORY_CONSTRUCTOR(nsDragServiceProxy) NS_GENERIC_FACTORY_CONSTRUCTOR(nsFilePickerProxy) NS_GENERIC_FACTORY_CONSTRUCTOR(nsScreenManagerProxy) @@ -30,7 +28,6 @@ NS_GENERIC_FACTORY_CONSTRUCTOR(PuppetBidiKeyboard) NS_DEFINE_NAMED_CID(NS_CLIPBOARD_CID); NS_DEFINE_NAMED_CID(NS_COLORPICKER_CID); -NS_DEFINE_NAMED_CID(NS_DATEPICKER_CID); NS_DEFINE_NAMED_CID(NS_DRAGSERVICE_CID); NS_DEFINE_NAMED_CID(NS_FILEPICKER_CID); NS_DEFINE_NAMED_CID(PUPPETBIDIKEYBOARD_CID); @@ -41,8 +38,6 @@ static const mozilla::Module::CIDEntry kWidgetCIDs[] = { Module::CONTENT_PROCESS_ONLY }, { &kNS_COLORPICKER_CID, false, nullptr, nsColorPickerProxyConstructor, Module::CONTENT_PROCESS_ONLY }, - { &kNS_DATEPICKER_CID, false, nullptr, nsDatePickerProxyConstructor, - Module::CONTENT_PROCESS_ONLY }, { &kNS_DRAGSERVICE_CID, false, nullptr, nsDragServiceProxyConstructor, Module::CONTENT_PROCESS_ONLY }, { &kNS_FILEPICKER_CID, false, nullptr, nsFilePickerProxyConstructor, @@ -57,7 +52,6 @@ static const mozilla::Module::CIDEntry kWidgetCIDs[] = { static const mozilla::Module::ContractIDEntry kWidgetContracts[] = { { "@mozilla.org/widget/clipboard;1", &kNS_CLIPBOARD_CID, Module::CONTENT_PROCESS_ONLY }, { "@mozilla.org/colorpicker;1", &kNS_COLORPICKER_CID, Module::CONTENT_PROCESS_ONLY }, - { "@mozilla.org/datepicker;1", &kNS_DATEPICKER_CID, Module::CONTENT_PROCESS_ONLY }, { "@mozilla.org/filepicker;1", &kNS_FILEPICKER_CID, Module::CONTENT_PROCESS_ONLY }, { "@mozilla.org/gfx/screenmanager;1", &kNS_SCREENMANAGER_CID, Module::CONTENT_PROCESS_ONLY }, { "@mozilla.org/widget/dragservice;1", &kNS_DRAGSERVICE_CID, Module::CONTENT_PROCESS_ONLY }, diff --git a/widget/nsDatePickerProxy.cpp b/widget/nsDatePickerProxy.cpp deleted file mode 100644 index e6b88f1be..000000000 --- a/widget/nsDatePickerProxy.cpp +++ /dev/null @@ -1,61 +0,0 @@ -/* -*- Mode: C++; 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/. */ - -#include "nsDatePickerProxy.h" - -#include "mozilla/dom/TabChild.h" - -using namespace mozilla::dom; - -NS_IMPL_ISUPPORTS(nsDatePickerProxy, nsIDatePicker) - -/* void init (in nsIDOMWindow parent, in AString title, in short mode); */ -NS_IMETHODIMP -nsDatePickerProxy::Init(mozIDOMWindowProxy* aParent, const nsAString& aTitle, - const nsAString& aInitialDate) -{ - TabChild* tabChild = TabChild::GetFrom(aParent); - if (!tabChild) { - return NS_ERROR_FAILURE; - } - - tabChild->SendPDatePickerConstructor(this, - nsString(aTitle), - nsString(aInitialDate)); - NS_ADDREF_THIS(); //Released in DeallocPDatePickerChild - return NS_OK; -} - -/* void open (in nsIDatePickerShownCallback aDatePickerShownCallback); */ -NS_IMETHODIMP -nsDatePickerProxy::Open(nsIDatePickerShownCallback* aDatePickerShownCallback) -{ - NS_ENSURE_STATE(!mCallback); - mCallback = aDatePickerShownCallback; - - SendOpen(); - return NS_OK; -} - -bool -nsDatePickerProxy::RecvCancel() -{ - if (mCallback) { - mCallback->Cancel(); - mCallback = nullptr; - } - return true; -} - -bool -nsDatePickerProxy::Recv__delete__(const nsString& aDate) -{ - if (mCallback) { - mCallback->Done(aDate); - mCallback = nullptr; - } - return true; -} diff --git a/widget/nsDatePickerProxy.h b/widget/nsDatePickerProxy.h deleted file mode 100644 index 71475932c..000000000 --- a/widget/nsDatePickerProxy.h +++ /dev/null @@ -1,33 +0,0 @@ -/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* 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/. */ - -#ifndef nsDatePickerProxy_h -#define nsDatePickerProxy_h - -#include "nsIDatePicker.h" - -#include "mozilla/dom/PDatePickerChild.h" - -class nsDatePickerProxy final : public nsIDatePicker, - public mozilla::dom::PDatePickerChild -{ -public: - NS_DECL_ISUPPORTS - NS_DECL_NSIDATEPICKER - - nsDatePickerProxy() {} - - virtual bool RecvCancel() override; - virtual bool Recv__delete__(const nsString& aDate) override; - -private: - ~nsDatePickerProxy() {} - - nsCOMPtr<nsIDatePickerShownCallback> mCallback; - nsString mTitle; - nsString mInitialDate; -}; - -#endif // nsDatePickerProxy_h diff --git a/widget/nsIDatePicker.idl b/widget/nsIDatePicker.idl deleted file mode 100644 index d6be60c95..000000000 --- a/widget/nsIDatePicker.idl +++ /dev/null @@ -1,50 +0,0 @@ -/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- - * - * 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/. */ - -#include "nsISupports.idl" - -interface mozIDOMWindowProxy; - -[scriptable, uuid(13388a28-1b0b-4218-a31b-588f7a4ec26c)] -interface nsIDatePickerShownCallback : nsISupports -{ - /** - * Callback called when the user selects cancel in the date picker - * This callback can not be called after done() is called. - */ - void cancel(); - - /** - * Callback called when the user has finished selecting the date - * - * @param date The new selected date value following the format "YYYY-MM-DD" - */ - void done(in AString date); -}; - -[scriptable, uuid(7becfc64-966b-4d53-87d2-9161f36bd3b3)] -interface nsIDatePicker : nsISupports -{ - /** - * Initialize the date picker widget. The date picker will not be shown until - * open() is called. - * If the initialDate parameter does not follow the format "YYYY-MM-DD" then - * the behavior will be unspecified. - * - * @param parent nsIDOMWindow parent. This dialog will be dependent - * on this parent. parent may be null. - * @param title The title for the date picker widget. - * @param initialDate The date to show when the widget is opened. The - * parameter has to follow the format "YYYY-MM-DD" - */ - void init(in mozIDOMWindowProxy parent, in AString title, in AString initialDate); - - /** - * Opens the date dialog asynchrounously. - * The results are provided via the callback object. - */ - void open(in nsIDatePickerShownCallback callback); -}; diff --git a/widget/nsWidgetsCID.h b/widget/nsWidgetsCID.h index 2589b59e0..54ebe63ab 100644 --- a/widget/nsWidgetsCID.h +++ b/widget/nsWidgetsCID.h @@ -33,11 +33,6 @@ { 0x0f872c8c, 0x3ee6, 0x46bd, \ { 0x92, 0xa2, 0x69, 0x65, 0x2c, 0x6b, 0x47, 0x4e } } -/* 0ca832f8-978a-4dc7-a57d-adb803925d39 */ -#define NS_DATEPICKER_CID \ -{ 0x0ca832f8, 0x978a, 0x4dc7, \ - { 0xa5, 0x7d, 0xad, 0xb8, 0x03, 0x92, 0x5d, 0x39 } } - /* 2d96b3df-c051-11d1-a827-0040959a28c9 */ #define NS_APPSHELL_CID \ { 0x2d96b3df, 0xc051, 0x11d1, \ diff --git a/xpcom/io/nsEscape.cpp b/xpcom/io/nsEscape.cpp index f16edc4ce..117e20d60 100644 --- a/xpcom/io/nsEscape.cpp +++ b/xpcom/io/nsEscape.cpp @@ -346,7 +346,7 @@ static const uint32_t EscapeChars[256] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1x - 0,1023, 0, 512,1023, 0,1023, 112,1023,1023,1023,1023,1023,1023, 953, 784, // 2x !"#$%&'()*+,-./ + 0,1023, 0, 512,1023, 0,1023, 624,1023,1023,1023,1023,1023,1023, 953, 784, // 2x !"#$%&'()*+,-./ 1023,1023,1023,1023,1023,1023,1023,1023,1023,1023,1008,1008, 0,1008, 0, 768, // 3x 0123456789:;<=>? 1008,1023,1023,1023,1023,1023,1023,1023,1023,1023,1023,1023,1023,1023,1023,1023, // 4x @ABCDEFGHIJKLMNO 1023,1023,1023,1023,1023,1023,1023,1023,1023,1023,1023,1008, 896,1008, 896,1023, // 5x PQRSTUVWXYZ[\]^_ |