gecko/browser/metro/base/content/bindings/urlbar.xml
Rodrigo Silveira adb59720b0 Bug 891667 - Define and implement keyboard interaction with half-height autocomplete.
--HG--
extra : rebase_source : b79131c2937a3c64db3a7b2dbf7940df9fc8e45d
2013-09-18 21:27:11 -07:00

882 lines
27 KiB
XML

<?xml version="1.0" encoding="Windows-1252" ?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<!DOCTYPE bindings [
<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
%browserDTD;
]>
<bindings
xmlns="http://www.mozilla.org/xbl"
xmlns:xbl="http://www.mozilla.org/xbl"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<binding id="urlbar" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete">
<implementation implements="nsIObserver">
<constructor>
<![CDATA[
this._mayFormat = Services.prefs.getBoolPref("browser.urlbar.formatting.enabled");
this._mayTrimURLs = Services.prefs.getBoolPref("browser.urlbar.trimURLs");
this._maySelectAll = Services.prefs.getBoolPref("browser.urlbar.doubleClickSelectsAll");
Services.prefs.addObserver("browser.urlbar.formatting.enabled", this, false);
Services.prefs.addObserver("browser.urlbar.trimURLs", this, false);
Services.prefs.addObserver("browser.urlbar.doubleClickSelectsAll", this, false);
this.inputField.controllers.insertControllerAt(0, this._copyCutValueController);
this.minResultsForPopup = 0;
this.popup._input = this;
]]>
</constructor>
<destructor>
<![CDATA[
Services.prefs.removeObserver("browser.urlbar.formatting.enabled", this);
Services.prefs.removeObserver("browser.urlbar.trimURLs", this);
Services.prefs.removeObserver("browser.urlbar.doubleClickSelectsAll", this);
]]>
</destructor>
<field name="_mayFormat"/>
<field name="_mayTrimURLs"/>
<field name="_maySelectAll"/>
<field name="_lastKnownGoodURL"/>
<method name="openPopup">
<body>
<![CDATA[
this.popup.openAutocompletePopup(this, null);
]]>
</body>
</method>
<method name="closePopup">
<body>
<![CDATA[
this.popup.closePopup(this, null);
]]>
</body>
</method>
<!-- URI Display: Domain Highlighting -->
<method name="_clearFormatting">
<body>
<![CDATA[
let controller = this.editor.selectionController;
let selection = controller.getSelection(controller.SELECTION_URLSECONDARY);
selection.removeAllRanges();
]]>
</body>
</method>
<method name="formatValue">
<body>
<![CDATA[
if (!this._mayFormat || this.isEditing)
return;
let controller = this.editor.selectionController;
let selection = controller.getSelection(controller.SELECTION_URLSECONDARY);
selection.removeAllRanges();
let textNode = this.editor.rootElement.firstChild;
let value = textNode.textContent;
let protocol = value.match(/^[a-z\d.+\-]+:(?=[^\d])/);
if (protocol &&
["http:", "https:", "ftp:"].indexOf(protocol[0]) == -1)
return;
let matchedURL = value.match(/^((?:[a-z]+:\/\/)?(?:[^\/]+@)?)(.+?)(?::\d+)?(?:\/|$)/);
if (!matchedURL)
return;
let [, preDomain, domain] = matchedURL;
let baseDomain = domain;
let subDomain = "";
// getBaseDomainFromHost doesn't recognize IPv6 literals in brackets as IPs (bug 667159)
if (domain[0] != "[") {
try {
baseDomain = Services.eTLD.getBaseDomainFromHost(domain);
if (!domain.endsWith(baseDomain)) {
// getBaseDomainFromHost converts its resultant to ACE.
let IDNService = Cc["@mozilla.org/network/idn-service;1"]
.getService(Ci.nsIIDNService);
baseDomain = IDNService.convertACEtoUTF8(baseDomain);
}
} catch (e) {}
}
if (baseDomain != domain) {
subDomain = domain.slice(0, -baseDomain.length);
}
let rangeLength = preDomain.length + subDomain.length;
if (rangeLength) {
let range = document.createRange();
range.setStart(textNode, 0);
range.setEnd(textNode, rangeLength);
selection.addRange(range);
}
let startRest = preDomain.length + domain.length;
if (startRest < value.length) {
let range = document.createRange();
range.setStart(textNode, startRest);
range.setEnd(textNode, value.length);
selection.addRange(range);
}
]]>
</body>
</method>
<!-- URI Display: Scheme and Trailing Slash Triming -->
<method name="_trimURL">
<parameter name="aURL"/>
<body>
<![CDATA[
// This function must not modify the given URL such that calling
// nsIURIFixup::createFixupURI with the rfdesult will produce a different URI.
return aURL /* remove single trailing slash for http/https/ftp URLs */
.replace(/^((?:http|https|ftp):\/\/[^/]+)\/$/, "$1")
/* remove http:// unless the host starts with "ftp\d*\." or contains "@" */
.replace(/^http:\/\/((?!ftp\d*\.)[^\/@]+(?:\/|$))/, "$1");
]]>
</body>
</method>
<method name="_getSelectedValueForClipboard">
<body>
<![CDATA[
// Grab the actual input field's value, not our value, which could include moz-action:
let inputVal = this.inputField.value;
let selectedVal = inputVal.substring(this.selectionStart, this.selectionEnd);
// If the selection doesn't start at the beginning or doesn't span the full domain or
// the URL bar is modified, nothing else to do here.
if (this.selectionStart > 0 || this.valueIsTyped)
return selectedVal;
// The selection doesn't span the full domain if it doesn't contain a slash and is
// followed by some character other than a slash.
if (!selectedVal.contains("/")) {
let remainder = inputVal.replace(selectedVal, "");
if (remainder != "" && remainder[0] != "/")
return selectedVal;
}
let uriFixup = Cc["@mozilla.org/docshell/urifixup;1"].getService(Ci.nsIURIFixup);
let uri;
try {
uri = uriFixup.createFixupURI(inputVal, Ci.nsIURIFixup.FIXUP_FLAG_USE_UTF8);
} catch (e) {}
if (!uri)
return selectedVal;
// Only copy exposable URIs
try {
uri = uriFixup.createExposableURI(uri);
} catch (ex) {}
// If the entire URL is selected, just use the actual loaded URI.
if (inputVal == selectedVal) {
// ... but only if isn't a javascript: or data: URI, since those
// are hard to read when encoded
if (!uri.schemeIs("javascript") && !uri.schemeIs("data")) {
// Parentheses are known to confuse third-party applications (bug 458565).
selectedVal = uri.spec.replace(/[()]/g, function (c) escape(c));
}
return selectedVal;
}
// Just the beginning of the URL is selected, check for a trimmed value
let spec = uri.spec;
let trimmedSpec = this._trimURL(spec);
if (spec != trimmedSpec) {
// Prepend the portion that trimURL removed from the beginning.
// This assumes trimURL will only truncate the URL at
// the beginning or end (or both).
let trimmedSegments = spec.split(trimmedSpec);
selectedVal = trimmedSegments[0] + selectedVal;
}
return selectedVal;
]]>
</body>
</method>
<field name="_copyCutValueController">
<![CDATA[
({
urlbar: this,
doCommand: function(aCommand) {
let urlbar = this.urlbar;
let val = urlbar._getSelectedValueForClipboard();
if (!val)
return;
if (aCommand == "cmd_cut" && this.isCommandEnabled(aCommand)) {
let start = urlbar.selectionStart;
let end = urlbar.selectionEnd;
urlbar.inputField.value = urlbar.inputField.value.substring(0, start) +
urlbar.inputField.value.substring(end);
urlbar.selectionStart = urlbar.selectionEnd = start;
}
Cc["@mozilla.org/widget/clipboardhelper;1"]
.getService(Ci.nsIClipboardHelper)
.copyString(val, document);
},
supportsCommand: function(aCommand) {
switch (aCommand) {
case "cmd_copy":
case "cmd_cut":
return true;
}
return false;
},
isCommandEnabled: function(aCommand) {
let urlbar = this.urlbar;
return this.supportsCommand(aCommand) &&
(aCommand != "cmd_cut" || !urlbar.readOnly) &&
urlbar.selectionStart < urlbar.selectionEnd;
},
onEvent: function(aEventName) {}
})
]]>
</field>
<method name="trimValue">
<parameter name="aURL"/>
<body>
<![CDATA[
return (this._mayTrimURLs) ? this._trimURL(aURL) : aURL;
]]>
</body>
</method>
<!-- URI Editing -->
<property name="isEditing" readonly="true">
<getter>
<![CDATA[
return Elements.urlbarState.hasAttribute("editing");
]]>
</getter>
</property>
<method name="beginEditing">
<parameter name="aShouldDismiss"/>
<body>
<![CDATA[
if (this.isEditing)
return;
Elements.urlbarState.setAttribute("editing", true);
this._lastKnownGoodURL = this.value;
if (!this.focused)
this.focus();
this._clearFormatting();
this.select();
if (aShouldDismiss)
ContextUI.dismissTabs();
]]>
</body>
</method>
<method name="endEditing">
<parameter name="aShouldRevert"/>
<body>
<![CDATA[
if (!this.isEditing)
return;
Elements.urlbarState.removeAttribute("editing");
this.closePopup();
this.formatValue();
if (this.focused)
this.blur();
if (aShouldRevert)
this.value = this._lastKnownGoodURL;
]]>
</body>
</method>
<!-- URI Submission -->
<method name="_canonizeURL">
<parameter name="aURL"/>
<parameter name="aTriggeringEvent"/>
<body>
<![CDATA[
if (!aURL)
return "";
// Only add the suffix when the URL bar value isn't already "URL-like",
// and only if we get a keyboard event, to match user expectations.
if (/^\s*[^.:\/\s]+(?:\/.*|\s*)$/i.test(aURL)) {
let accel = aTriggeringEvent.ctrlKey;
let shift = aTriggeringEvent.shiftKey;
let suffix = "";
switch (true) {
case (accel && shift):
suffix = ".org/";
break;
case (shift):
suffix = ".net/";
break;
case (accel):
try {
suffix = gPrefService.getCharPref("browser.fixup.alternate.suffix");
if (suffix.charAt(suffix.length - 1) != "/")
suffix += "/";
} catch(e) {
suffix = ".com/";
}
break;
}
if (suffix) {
// trim leading/trailing spaces (bug 233205)
aURL = aURL.trim();
// Tack www. and suffix on. If user has appended directories, insert
// suffix before them (bug 279035). Be careful not to get two slashes.
let firstSlash = aURL.indexOf("/");
if (firstSlash >= 0) {
aURL = aURL.substring(0, firstSlash) + suffix + aURL.substring(firstSlash + 1);
} else {
aURL = aURL + suffix;
}
aURL = "http://www." + aURL;
}
}
return aURL;
]]>
</body>
</method>
<method name="submitURL">
<parameter name="aEvent"/>
<body>
<![CDATA[
// If the address was typed in by a user, tidy it up
if (aEvent instanceof KeyEvent)
this.value = this._canonizeURL(this.value, aEvent);
this.endEditing();
BrowserUI.goToURI(this.value);
return true;
]]>
</body>
</method>
<method name="submitSearch">
<parameter name="anEngineName"/>
<body>
<![CDATA[
this.endEditing();
BrowserUI.doOpenSearch(anEngineName);
return true;
]]>
</body>
</method>
<!-- nsIObserver -->
<method name="observe">
<parameter name="aSubject"/>
<parameter name="aTopic"/>
<parameter name="aData"/>
<body>
<![CDATA[
if (aTopic != "nsPref:changed")
return;
switch (aData) {
case "browser.urlbar.formatting.enabled":
this._mayFormat = Services.prefs.getBoolPref(aData);
if (!this._mayFormat) this._clearFormatting();
break;
case "browser.urlbar.trimURLs":
this._mayTrimURLs = Services.prefs.getBoolPref(aData);
break;
case "browser.urlbar.doubleClickSelectsAll":
this._maySelectAll = Services.prefs.getBoolPref(aData);
break;
}
]]>
</body>
</method>
</implementation>
<handlers>
<!-- Entering editing mode -->
<handler event="focus" phase="capturing">
<![CDATA[
this.beginEditing();
]]>
</handler>
<handler event="input" phase="capturing">
<![CDATA[
// Ensures that paste-and-go actually brings the URL bar into editing mode
// and displays the half-height autocomplete popup.
this.beginEditing();
this.openPopup();
]]>
</handler>
<handler event="click" phase="capturing">
<![CDATA[
this.beginEditing(true);
]]>
</handler>
<!-- Editing mode behaviors -->
<handler event="dblclick" phase="capturing">
<![CDATA[
if (this._maySelectAll) this.select();
]]>
</handler>
<handler event="contextmenu" phase="capturing">
<![CDATA[
let box = this.inputField.parentNode;
box.showContextMenu(this, event, true);
]]>
</handler>
<!-- Leaving editing mode -->
<handler event="blur" phase="capturing">
<![CDATA[
this.endEditing();
]]>
</handler>
<handler event="keypress" phase="capturing" keycode="VK_RETURN"
modifiers="accel shift any">
<![CDATA[
if (this.popup.submitSelected())
return;
if (this.submitURL(event))
return;
]]>
</handler>
<handler event="keypress" phase="capturing" keycode="VK_UP">
<![CDATA[
if (!this.popup.hasSelection) {
// Treat the first up as a down key to trick handleKeyNavigation() to start
// keyboard navigation on autocomplete popup.
this.mController.handleKeyNavigation(KeyEvent.DOM_VK_DOWN);
event.preventDefault();
}
]]>
</handler>
</handlers>
</binding>
<binding id="urlbar-autocomplete">
<content class="meta-section-container">
<xul:vbox class="meta-section" anonid="results-container" flex="1">
<xul:label class="meta-section-title"
value="&autocompleteResultsHeader.label;"/>
<richgrid anonid="results" rows="3" flex="1"
seltype="single" deferlayout="true"/>
</xul:vbox>
<xul:vbox class="meta-section" flex="1">
<xul:label anonid="searches-header" class="meta-section-title"/>
<richgrid anonid="searches" rows="3" flex="1"
seltype="single" deferlayout="true"/>
</xul:vbox>
</content>
<implementation implements="nsIAutoCompletePopup, nsIObserver">
<constructor>
<![CDATA[
this.hidden = true;
Services.obs.addObserver(this, "browser-search-engine-modified", false);
this._results.controller = this;
this._searches.controller = this;
]]>
</constructor>
<destructor>
<![CDATA[
Services.obs.removeObserver(this, "browser-search-engine-modified");
]]>
</destructor>
<!-- nsIAutocompleteInput -->
<field name="_input">null</field>
<property name="input" readonly="true" onget="return this._input;"/>
<property name="matchCount" readonly="true" onget="return this.input.controller.matchCount;"/>
<property name="popupOpen" readonly="true" onget="return !this.hidden"/>
<property name="overrideValue" readonly="true" onget="return null;"/>
<property name="selectedItem">
<getter>
<![CDATA[
return this._isGridBound(this._results) ? this._results.selectedItem : null;
]]>
</getter>
<setter>
<![CDATA[
return this._isGridBound(this._results) ? this._results.selectedItem : null;
]]>
</setter>
</property>
<property name="selectedIndex">
<getter>
<![CDATA[
return this._isGridBound(this._results) ? this._results.selectedIndex : -1;
]]>
</getter>
<setter>
<![CDATA[
return this._isGridBound(this._results) ? this._results.selectedIndex : -1;
]]>
</setter>
</property>
<property name="hasSelection">
<getter>
<![CDATA[
if (!this._isGridBound(this._results) ||
!this._isGridBound(this._searches))
return false;
return (this._results.selectedIndex >= 0 ||
this._searches.selectedIndex >= 0);
]]>
</getter>
</property>
<method name="openAutocompletePopup">
<parameter name="aInput"/>
<parameter name="aElement"/>
<body>
<![CDATA[
if (this.popupOpen)
return;
ContextUI.dismissContextAppbar();
this._input = aInput;
this._grid = this._results;
this.clearSelection();
this.invalidate();
this._results.arrangeItemsNow();
this._searches.arrangeItemsNow();
this.hidden = false;
Elements.urlbarState.setAttribute("autocomplete", "true");
]]>
</body>
</method>
<method name="closePopup">
<body>
<![CDATA[
if (!this.popupOpen)
return;
this.input.controller.stopSearch();
this.hidden = true;
Elements.urlbarState.removeAttribute("autocomplete");
]]>
</body>
</method>
<!-- Updating grid content -->
<field name="_grid">null</field>
<field name="_results" readonly="true">document.getAnonymousElementByAttribute(this, 'anonid', 'results');</field>
<field name="_resultsContainer" readonly="true">document.getAnonymousElementByAttribute(this, 'anonid', 'results-container');</field>
<field name="_searchesHeader" readonly="true">document.getAnonymousElementByAttribute(this, 'anonid', 'searches-header');</field>
<field name="_searches" readonly="true">document.getAnonymousElementByAttribute(this, 'anonid', 'searches');</field>
<property name="_otherGrid" readonly="true">
<getter>
<![CDATA[
if (this._grid == null)
return null;
return (this._grid == this._results) ? this._searches : this._results;
]]>
</getter>
</property>
<method name="_isGridBound">
<parameter name="aGrid"/>
<body>
<![CDATA[
return aGrid && aGrid.isBound;
]]>
</body>
</method>
<method name="invalidate">
<body>
<![CDATA[
if (!this.popupOpen)
return;
this.updateResults();
this.updateSearchEngineHeader();
]]>
</body>
</method>
<!-- Updating grid content: results -->
<method name="updateResults">
<body>
<![CDATA[
if (!this._isGridBound(this._results))
return;
if (!this.input)
return;
let haveNoResults = (this.matchCount == 0);
this._resultsContainer.hidden = haveNoResults;
if (haveNoResults) {
this._results.clearAll();
return;
}
let controller = this.input.controller;
let lastMatch = this.matchCount - 1;
let iterCount = Math.max(this._results.itemCount, this.matchCount);
// Swap out existing items for new search hit results
for (let i = 0; i < iterCount; i++) {
if (i > lastMatch) {
let lastItem = this._results.itemCount - 1;
this._results.removeItemAt(lastItem, true);
continue;
}
let value = controller.getValueAt(i);
let label = controller.getCommentAt(i) || value;
let iconURI = controller.getImageAt(i);
let item = this._results.getItemAtIndex(i);
if (item == null) {
item = this._results.appendItem(label, value, true);
item.setAttribute("autocomplete", "true");
} else {
item.setAttribute("label", label);
item.setAttribute("value", value);
}
item.setAttribute("iconURI", iconURI);
}
this._results.arrangeItems();
]]>
</body>
</method>
<!-- Updating grid content: search engines -->
<field name="_engines">[]</field>
<method name="_initSearchEngines">
<body>
<![CDATA[
Services.search.init(this.updateSearchEngineGrid.bind(this));
]]>
</body>
</method>
<method name="updateSearchEngineGrid">
<body>
<![CDATA[
if (!this._isGridBound(this._searches))
return;
this._engines = Services.search.getVisibleEngines();
while (this._searches.itemCount > 0)
this._searches.removeItemAt(0, true);
this._engines.forEach(function (anEngine) {
let item = this._searches.appendItem(anEngine.name, anEngine.name, true);
item.setAttribute("autocomplete", "true");
item.setAttribute("search", "true");
let iconURI = anEngine.iconURI ? anEngine.iconURI.spec : "";
item.setAttribute("iconURI", iconURI);
}.bind(this));
this._searches.arrangeItems();
]]>
</body>
</method>
<method name="updateSearchEngineHeader">
<body>
<![CDATA[
if (!this._isGridBound(this._searches))
return;
let searchString = this.input.controller.searchString;
let label = Strings.browser.formatStringFromName(
"opensearch.search.header", [searchString], 1);
this._searchesHeader.value = label;
]]>
</body>
</method>
<!-- Selecting results -->
<method name="selectBy">
<parameter name="aReverse"/>
<parameter name="aPage"/>
<body>
<![CDATA[
if (!this._isGridBound(this._results) ||
!this._isGridBound(this._searches))
return;
// Move between grids if we're at the edge of one.
if ((this._grid.isSelectionAtEnd && !aReverse) ||
(this._grid.isSelectionAtStart && aReverse)) {
let index = !aReverse ? 0 : this._otherGrid.itemCount - 1;
this._otherGrid.selectedIndex = index;
} else {
this._grid.offsetSelection(aReverse ? -1 : 1);
}
]]>
</body>
</method>
<method name="clearSelection">
<body>
<![CDATA[
if (this._isGridBound(this._results))
this._results.clearSelection();
if (this._isGridBound(this._searches))
this._searches.clearSelection();
]]>
</body>
</method>
<!-- Submitting selected results -->
<method name="submitSelected">
<body>
<![CDATA[
if (this._isGridBound(this._results) &&
this._results.selectedIndex >= 0) {
let url = this.input.controller.getValueAt(this._results.selectedIndex);
this.input.value = url;
return this.input.submitURL();
}
if (this._isGridBound(this._searches) &&
this._searches.selectedIndex >= 0) {
let engine = this._engines[this._searches.selectedIndex];
return this.input.submitSearch(engine.name);
}
return false;
]]>
</body>
</method>
<method name="handleItemClick">
<parameter name="aItem"/>
<parameter name="aEvent"/>
<body>
<![CDATA[
this.submitSelected();
]]>
</body>
</method>
<!-- nsIObserver -->
<method name="observe">
<parameter name="aSubject"/>
<parameter name="aTopic"/>
<parameter name="aData"/>
<body>
<![CDATA[
switch (aTopic) {
case "browser-search-engine-modified":
this.updateSearchEngineGrid();
break;
}
]]>
</body>
</method>
</implementation>
<handlers>
<handler event="contentgenerated">
<![CDATA[
let grid = event.originalTarget;
if (grid == this._searches)
this._initSearchEngines();
if (grid == this._results)
this.updateResults();
]]>
</handler>
<handler event="select">
<![CDATA[
let grid = event.originalTarget;
// If a selection was made on a different grid,
// remove selection from the current grid.
if (grid != this._grid) {
this._grid.clearSelection();
this._grid = this._otherGrid;
}
]]>
</handler>
</handlers>
</binding>
</bindings>