diff options
Diffstat (limited to 'browser/components/preferences/in-content/search.js')
-rw-r--r-- | browser/components/preferences/in-content/search.js | 604 |
1 files changed, 604 insertions, 0 deletions
diff --git a/browser/components/preferences/in-content/search.js b/browser/components/preferences/in-content/search.js new file mode 100644 index 000000000..55aa2c18c --- /dev/null +++ b/browser/components/preferences/in-content/search.js @@ -0,0 +1,604 @@ +/* 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"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); + +const ENGINE_FLAVOR = "text/x-moz-search-engine"; + +var gEngineView = null; + +var gSearchPane = { + + /** + * Initialize autocomplete to ensure prefs are in sync. + */ + _initAutocomplete: function () { + Components.classes["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"] + .getService(Components.interfaces.mozIPlacesAutoComplete); + }, + + init: function () + { + gEngineView = new EngineView(new EngineStore()); + document.getElementById("engineList").view = gEngineView; + this.buildDefaultEngineDropDown(); + + let addEnginesLink = document.getElementById("addEngines"); + let searchEnginesURL = Services.wm.getMostRecentWindow('navigator:browser') + .BrowserSearch.searchEnginesURL; + addEnginesLink.setAttribute("href", searchEnginesURL); + + window.addEventListener("click", this, false); + window.addEventListener("command", this, false); + window.addEventListener("dragstart", this, false); + window.addEventListener("keypress", this, false); + window.addEventListener("select", this, false); + window.addEventListener("blur", this, true); + + Services.obs.addObserver(this, "browser-search-engine-modified", false); + window.addEventListener("unload", () => { + Services.obs.removeObserver(this, "browser-search-engine-modified", false); + }); + + this._initAutocomplete(); + + let suggestsPref = + document.getElementById("browser.search.suggest.enabled"); + suggestsPref.addEventListener("change", () => { + this.updateSuggestsCheckbox(); + }); + this.updateSuggestsCheckbox(); + }, + + updateSuggestsCheckbox() { + let suggestsPref = + document.getElementById("browser.search.suggest.enabled"); + let permanentPB = + Services.prefs.getBoolPref("browser.privatebrowsing.autostart"); + let urlbarSuggests = document.getElementById("urlBarSuggestion"); + urlbarSuggests.disabled = !suggestsPref.value || permanentPB; + + let urlbarSuggestsPref = + document.getElementById("browser.urlbar.suggest.searches"); + urlbarSuggests.checked = urlbarSuggestsPref.value; + if (urlbarSuggests.disabled) { + urlbarSuggests.checked = false; + } + + let permanentPBLabel = + document.getElementById("urlBarSuggestionPermanentPBLabel"); + permanentPBLabel.hidden = urlbarSuggests.hidden || !permanentPB; + }, + + buildDefaultEngineDropDown: function() { + // This is called each time something affects the list of engines. + let list = document.getElementById("defaultEngine"); + // Set selection to the current default engine. + let currentEngine = Services.search.currentEngine.name; + + // If the current engine isn't in the list any more, select the first item. + let engines = gEngineView._engineStore._engines; + if (!engines.some(e => e.name == currentEngine)) + currentEngine = engines[0].name; + + // Now clean-up and rebuild the list. + list.removeAllItems(); + gEngineView._engineStore._engines.forEach(e => { + let item = list.appendItem(e.name); + item.setAttribute("class", "menuitem-iconic searchengine-menuitem menuitem-with-favicon"); + if (e.iconURI) { + item.setAttribute("image", e.iconURI.spec); + } + item.engine = e; + if (e.name == currentEngine) + list.selectedItem = item; + }); + }, + + handleEvent: function(aEvent) { + switch (aEvent.type) { + case "click": + if (aEvent.target.id != "engineChildren" && + !aEvent.target.classList.contains("searchEngineAction")) { + let engineList = document.getElementById("engineList"); + // We don't want to toggle off selection while editing keyword + // so proceed only when the input field is hidden. + // We need to check that engineList.view is defined here + // because the "click" event listener is on <window> and the + // view might have been destroyed if the pane has been navigated + // away from. + if (engineList.inputField.hidden && engineList.view) { + let selection = engineList.view.selection; + if (selection.count > 0) { + selection.toggleSelect(selection.currentIndex); + } + engineList.blur(); + } + } + break; + case "command": + switch (aEvent.target.id) { + case "": + if (aEvent.target.parentNode && + aEvent.target.parentNode.parentNode && + aEvent.target.parentNode.parentNode.id == "defaultEngine") { + gSearchPane.setDefaultEngine(); + } + break; + case "restoreDefaultSearchEngines": + gSearchPane.onRestoreDefaults(); + break; + case "removeEngineButton": + Services.search.removeEngine(gEngineView.selectedEngine.originalEngine); + break; + } + break; + case "dragstart": + if (aEvent.target.id == "engineChildren") { + onDragEngineStart(aEvent); + } + break; + case "keypress": + if (aEvent.target.id == "engineList") { + gSearchPane.onTreeKeyPress(aEvent); + } + break; + case "select": + if (aEvent.target.id == "engineList") { + gSearchPane.onTreeSelect(); + } + break; + case "blur": + if (aEvent.target.id == "engineList" && + aEvent.target.inputField == document.getBindingParent(aEvent.originalTarget)) { + gSearchPane.onInputBlur(); + } + break; + } + }, + + observe: function(aEngine, aTopic, aVerb) { + if (aTopic == "browser-search-engine-modified") { + aEngine.QueryInterface(Components.interfaces.nsISearchEngine); + switch (aVerb) { + case "engine-added": + gEngineView._engineStore.addEngine(aEngine); + gEngineView.rowCountChanged(gEngineView.lastIndex, 1); + gSearchPane.buildDefaultEngineDropDown(); + break; + case "engine-changed": + gEngineView._engineStore.reloadIcons(); + gEngineView.invalidate(); + break; + case "engine-removed": + gSearchPane.remove(aEngine); + break; + case "engine-current": + // If the user is going through the drop down using up/down keys, the + // dropdown may still be open (eg. on Windows) when engine-current is + // fired, so rebuilding the list unconditionally would get in the way. + let selectedEngine = + document.getElementById("defaultEngine").selectedItem.engine; + if (selectedEngine.name != aEngine.name) + gSearchPane.buildDefaultEngineDropDown(); + break; + case "engine-default": + // Not relevant + break; + } + } + }, + + onInputBlur: function(aEvent) { + let tree = document.getElementById("engineList"); + if (!tree.hasAttribute("editing")) + return; + + // Accept input unless discarded. + let accept = aEvent.charCode != KeyEvent.DOM_VK_ESCAPE; + tree.stopEditing(accept); + }, + + onTreeSelect: function() { + document.getElementById("removeEngineButton").disabled = + !gEngineView.isEngineSelectedAndRemovable(); + }, + + onTreeKeyPress: function(aEvent) { + let index = gEngineView.selectedIndex; + let tree = document.getElementById("engineList"); + if (tree.hasAttribute("editing")) + return; + + if (aEvent.charCode == KeyEvent.DOM_VK_SPACE) { + // Space toggles the checkbox. + let newValue = !gEngineView._engineStore.engines[index].shown; + gEngineView.setCellValue(index, tree.columns.getFirstColumn(), + newValue.toString()); + // Prevent page from scrolling on the space key. + aEvent.preventDefault(); + } + else { + let isMac = Services.appinfo.OS == "Darwin"; + if ((isMac && aEvent.keyCode == KeyEvent.DOM_VK_RETURN) || + (!isMac && aEvent.keyCode == KeyEvent.DOM_VK_F2)) { + tree.startEditing(index, tree.columns.getLastColumn()); + } else if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE || + (isMac && aEvent.shiftKey && + aEvent.keyCode == KeyEvent.DOM_VK_BACK_SPACE && + gEngineView.isEngineSelectedAndRemovable())) { + // Delete and Shift+Backspace (Mac) removes selected engine. + Services.search.removeEngine(gEngineView.selectedEngine.originalEngine); + } + } + }, + + onRestoreDefaults: function() { + let num = gEngineView._engineStore.restoreDefaultEngines(); + gEngineView.rowCountChanged(0, num); + gEngineView.invalidate(); + }, + + showRestoreDefaults: function(aEnable) { + document.getElementById("restoreDefaultSearchEngines").disabled = !aEnable; + }, + + remove: function(aEngine) { + let index = gEngineView._engineStore.removeEngine(aEngine); + gEngineView.rowCountChanged(index, -1); + gEngineView.invalidate(); + gEngineView.selection.select(Math.min(index, gEngineView.lastIndex)); + gEngineView.ensureRowIsVisible(gEngineView.currentIndex); + document.getElementById("engineList").focus(); + }, + + editKeyword: Task.async(function* (aEngine, aNewKeyword) { + let keyword = aNewKeyword.trim(); + if (keyword) { + let eduplicate = false; + let dupName = ""; + + // Check for duplicates in Places keywords. + let bduplicate = !!(yield PlacesUtils.keywords.fetch(keyword)); + + // Check for duplicates in changes we haven't committed yet + let engines = gEngineView._engineStore.engines; + for (let engine of engines) { + if (engine.alias == keyword && + engine.name != aEngine.name) { + eduplicate = true; + dupName = engine.name; + break; + } + } + + // Notify the user if they have chosen an existing engine/bookmark keyword + if (eduplicate || bduplicate) { + let strings = document.getElementById("engineManagerBundle"); + let dtitle = strings.getString("duplicateTitle"); + let bmsg = strings.getString("duplicateBookmarkMsg"); + let emsg = strings.getFormattedString("duplicateEngineMsg", [dupName]); + + Services.prompt.alert(window, dtitle, eduplicate ? emsg : bmsg); + return false; + } + } + + gEngineView._engineStore.changeEngine(aEngine, "alias", keyword); + gEngineView.invalidate(); + return true; + }), + + saveOneClickEnginesList: function () { + let hiddenList = []; + for (let engine of gEngineView._engineStore.engines) { + if (!engine.shown) + hiddenList.push(engine.name); + } + document.getElementById("browser.search.hiddenOneOffs").value = + hiddenList.join(","); + }, + + setDefaultEngine: function () { + Services.search.currentEngine = + document.getElementById("defaultEngine").selectedItem.engine; + } +}; + +function onDragEngineStart(event) { + var selectedIndex = gEngineView.selectedIndex; + var tree = document.getElementById("engineList"); + var row = { }, col = { }, child = { }; + tree.treeBoxObject.getCellAt(event.clientX, event.clientY, row, col, child); + if (selectedIndex >= 0 && !gEngineView.isCheckBox(row.value, col.value)) { + event.dataTransfer.setData(ENGINE_FLAVOR, selectedIndex.toString()); + event.dataTransfer.effectAllowed = "move"; + } +} + + +function EngineStore() { + let pref = document.getElementById("browser.search.hiddenOneOffs").value; + this.hiddenList = pref ? pref.split(",") : []; + + this._engines = Services.search.getVisibleEngines().map(this._cloneEngine, this); + this._defaultEngines = Services.search.getDefaultEngines().map(this._cloneEngine, this); + + // check if we need to disable the restore defaults button + var someHidden = this._defaultEngines.some(e => e.hidden); + gSearchPane.showRestoreDefaults(someHidden); +} +EngineStore.prototype = { + _engines: null, + _defaultEngines: null, + + get engines() { + return this._engines; + }, + set engines(val) { + this._engines = val; + return val; + }, + + _getIndexForEngine: function ES_getIndexForEngine(aEngine) { + return this._engines.indexOf(aEngine); + }, + + _getEngineByName: function ES_getEngineByName(aName) { + return this._engines.find(engine => engine.name == aName); + }, + + _cloneEngine: function ES_cloneEngine(aEngine) { + var clonedObj={}; + for (var i in aEngine) + clonedObj[i] = aEngine[i]; + clonedObj.originalEngine = aEngine; + clonedObj.shown = this.hiddenList.indexOf(clonedObj.name) == -1; + return clonedObj; + }, + + // Callback for Array's some(). A thisObj must be passed to some() + _isSameEngine: function ES_isSameEngine(aEngineClone) { + return aEngineClone.originalEngine == this.originalEngine; + }, + + addEngine: function ES_addEngine(aEngine) { + this._engines.push(this._cloneEngine(aEngine)); + }, + + moveEngine: function ES_moveEngine(aEngine, aNewIndex) { + if (aNewIndex < 0 || aNewIndex > this._engines.length - 1) + throw new Error("ES_moveEngine: invalid aNewIndex!"); + var index = this._getIndexForEngine(aEngine); + if (index == -1) + throw new Error("ES_moveEngine: invalid engine?"); + + if (index == aNewIndex) + return; // nothing to do + + // Move the engine in our internal store + var removedEngine = this._engines.splice(index, 1)[0]; + this._engines.splice(aNewIndex, 0, removedEngine); + + Services.search.moveEngine(aEngine.originalEngine, aNewIndex); + }, + + removeEngine: function ES_removeEngine(aEngine) { + if (this._engines.length == 1) { + throw new Error("Cannot remove last engine!"); + } + + let engineName = aEngine.name; + let index = this._engines.findIndex(element => element.name == engineName); + + if (index == -1) + throw new Error("invalid engine?"); + + let removedEngine = this._engines.splice(index, 1)[0]; + + if (this._defaultEngines.some(this._isSameEngine, removedEngine)) + gSearchPane.showRestoreDefaults(true); + gSearchPane.buildDefaultEngineDropDown(); + return index; + }, + + restoreDefaultEngines: function ES_restoreDefaultEngines() { + var added = 0; + + for (var i = 0; i < this._defaultEngines.length; ++i) { + var e = this._defaultEngines[i]; + + // If the engine is already in the list, just move it. + if (this._engines.some(this._isSameEngine, e)) { + this.moveEngine(this._getEngineByName(e.name), i); + } else { + // Otherwise, add it back to our internal store + + // The search service removes the alias when an engine is hidden, + // so clear any alias we may have cached before unhiding the engine. + e.alias = ""; + + this._engines.splice(i, 0, e); + let engine = e.originalEngine; + engine.hidden = false; + Services.search.moveEngine(engine, i); + added++; + } + } + Services.search.resetToOriginalDefaultEngine(); + gSearchPane.showRestoreDefaults(false); + gSearchPane.buildDefaultEngineDropDown(); + return added; + }, + + changeEngine: function ES_changeEngine(aEngine, aProp, aNewValue) { + var index = this._getIndexForEngine(aEngine); + if (index == -1) + throw new Error("invalid engine?"); + + this._engines[index][aProp] = aNewValue; + aEngine.originalEngine[aProp] = aNewValue; + }, + + reloadIcons: function ES_reloadIcons() { + this._engines.forEach(function (e) { + e.uri = e.originalEngine.uri; + }); + } +}; + +function EngineView(aEngineStore) { + this._engineStore = aEngineStore; +} +EngineView.prototype = { + _engineStore: null, + tree: null, + + get lastIndex() { + return this.rowCount - 1; + }, + get selectedIndex() { + var seln = this.selection; + if (seln.getRangeCount() > 0) { + var min = {}; + seln.getRangeAt(0, min, {}); + return min.value; + } + return -1; + }, + get selectedEngine() { + return this._engineStore.engines[this.selectedIndex]; + }, + + // Helpers + rowCountChanged: function (index, count) { + this.tree.rowCountChanged(index, count); + }, + + invalidate: function () { + this.tree.invalidate(); + }, + + ensureRowIsVisible: function (index) { + this.tree.ensureRowIsVisible(index); + }, + + getSourceIndexFromDrag: function (dataTransfer) { + return parseInt(dataTransfer.getData(ENGINE_FLAVOR)); + }, + + isCheckBox: function(index, column) { + return column.id == "engineShown"; + }, + + isEngineSelectedAndRemovable: function() { + return this.selectedIndex != -1 && this.lastIndex != 0; + }, + + // nsITreeView + get rowCount() { + return this._engineStore.engines.length; + }, + + getImageSrc: function(index, column) { + if (column.id == "engineName") { + if (this._engineStore.engines[index].iconURI) + return this._engineStore.engines[index].iconURI.spec; + + if (window.devicePixelRatio > 1) + return "chrome://browser/skin/search-engine-placeholder@2x.png"; + return "chrome://browser/skin/search-engine-placeholder.png"; + } + + return ""; + }, + + getCellText: function(index, column) { + if (column.id == "engineName") + return this._engineStore.engines[index].name; + else if (column.id == "engineKeyword") + return this._engineStore.engines[index].alias; + return ""; + }, + + setTree: function(tree) { + this.tree = tree; + }, + + canDrop: function(targetIndex, orientation, dataTransfer) { + var sourceIndex = this.getSourceIndexFromDrag(dataTransfer); + return (sourceIndex != -1 && + sourceIndex != targetIndex && + sourceIndex != targetIndex + orientation); + }, + + drop: function(dropIndex, orientation, dataTransfer) { + var sourceIndex = this.getSourceIndexFromDrag(dataTransfer); + var sourceEngine = this._engineStore.engines[sourceIndex]; + + const nsITreeView = Components.interfaces.nsITreeView; + if (dropIndex > sourceIndex) { + if (orientation == nsITreeView.DROP_BEFORE) + dropIndex--; + } else if (orientation == nsITreeView.DROP_AFTER) { + dropIndex++; + } + + this._engineStore.moveEngine(sourceEngine, dropIndex); + gSearchPane.showRestoreDefaults(true); + gSearchPane.buildDefaultEngineDropDown(); + + // Redraw, and adjust selection + this.invalidate(); + this.selection.select(dropIndex); + }, + + selection: null, + getRowProperties: function(index) { return ""; }, + getCellProperties: function(index, column) { return ""; }, + getColumnProperties: function(column) { return ""; }, + isContainer: function(index) { return false; }, + isContainerOpen: function(index) { return false; }, + isContainerEmpty: function(index) { return false; }, + isSeparator: function(index) { return false; }, + isSorted: function(index) { return false; }, + getParentIndex: function(index) { return -1; }, + hasNextSibling: function(parentIndex, index) { return false; }, + getLevel: function(index) { return 0; }, + getProgressMode: function(index, column) { }, + getCellValue: function(index, column) { + if (column.id == "engineShown") + return this._engineStore.engines[index].shown; + return undefined; + }, + toggleOpenState: function(index) { }, + cycleHeader: function(column) { }, + selectionChanged: function() { }, + cycleCell: function(row, column) { }, + isEditable: function(index, column) { return column.id != "engineName"; }, + isSelectable: function(index, column) { return false; }, + setCellValue: function(index, column, value) { + if (column.id == "engineShown") { + this._engineStore.engines[index].shown = value == "true"; + gEngineView.invalidate(); + gSearchPane.saveOneClickEnginesList(); + } + }, + setCellText: function(index, column, value) { + if (column.id == "engineKeyword") { + gSearchPane.editKeyword(this._engineStore.engines[index], value) + .then(valid => { + if (!valid) + document.getElementById("engineList").startEditing(index, column); + }); + } + }, + performAction: function(action) { }, + performActionOnRow: function(action, index) { }, + performActionOnCell: function(action, index, column) { } +}; |