442 строки
17 KiB
JavaScript
442 строки
17 KiB
JavaScript
/* ***** BEGIN LICENSE BLOCK *****
|
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
*
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
|
* the License. You may obtain a copy of the License at
|
|
* http://www.mozilla.org/MPL/
|
|
*
|
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
* for the specific language governing rights and limitations under the
|
|
* License.
|
|
*
|
|
* The Original Code is the SeaMonkey Find As You Type.
|
|
*
|
|
* The Initial Developer of the Original Code is
|
|
* Neil Rashbrook <neil@parkwaycc.co.uk>
|
|
*
|
|
* Portions created by the Initial Developer are Copyright (C) 2008
|
|
* the Initial Developer. All Rights Reserved.
|
|
*
|
|
* Contributor(s):
|
|
*
|
|
* Alternatively, the contents of this file may be used under the terms of
|
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
|
* of those above. If you wish to allow use of your version of this file only
|
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
|
* use your version of this file under the terms of the MPL, indicate your
|
|
* decision by deleting the provisions above and replace them with the notice
|
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
|
* the provisions above, a recipient may use your version of this file under
|
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
|
*
|
|
* ***** END LICENSE BLOCK ***** */
|
|
|
|
const kSpace = " ".charCodeAt(0);
|
|
const kSlash = "/".charCodeAt(0);
|
|
const kApostrophe = "'".charCodeAt(0);
|
|
|
|
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
function findTypeController(aTypeAheadFind, aWindow)
|
|
{
|
|
this.mTypeAheadFind = aTypeAheadFind;
|
|
this.mWindow = aWindow;
|
|
}
|
|
|
|
findTypeController.prototype = {
|
|
/* nsIController */
|
|
supportsCommand: function(aCommand) {
|
|
return aCommand == "cmd_findTypeText" || aCommand == "cmd_findTypeLinks";
|
|
},
|
|
|
|
isCommandEnabled: function(aCommand) {
|
|
// We can always find if there's a primary content window in which to find.
|
|
if (this.mWindow.content)
|
|
return true;
|
|
|
|
// We can also find if the focused window is a content window.
|
|
// Note: this gets called during a focus change
|
|
// so the new window might not have focus yet.
|
|
var commandDispatcher = this.mWindow.document.commandDispatcher;
|
|
var e = commandDispatcher.focusedElement;
|
|
var w = e ? e.ownerDocument.defaultView : commandDispatcher.focusedWindow;
|
|
return w.top != this.mWindow;
|
|
},
|
|
|
|
doCommand: function(aCommand) {
|
|
this.mTypeAheadFind.startFind(this.mWindow, aCommand != "cmd_findTypeText");
|
|
},
|
|
|
|
onEvent: function(aEvent) {
|
|
}
|
|
}
|
|
|
|
function typeAheadFind()
|
|
{
|
|
}
|
|
|
|
typeAheadFind.prototype = {
|
|
/* properties required for XPCOMUtils */
|
|
classID: Components.ID("{45c8f75b-a299-4178-a461-f63690389055}"),
|
|
|
|
/* members */
|
|
mBadKeysSinceMatch: 0,
|
|
mBundle: null,
|
|
mCurrentWindow: null,
|
|
mEventTarget: null,
|
|
mFind: null,
|
|
mFindService: null,
|
|
mFound: null,
|
|
mLinks: false,
|
|
mSearchString: "",
|
|
mSelection: null,
|
|
mTimer: null,
|
|
mXULBrowserWindow: null,
|
|
|
|
/* nsISupports */
|
|
QueryInterface: XPCOMUtils.generateQI([
|
|
Components.interfaces.nsISupportsWeakReference,
|
|
Components.interfaces.nsIObserver,
|
|
Components.interfaces.nsITimerCallback,
|
|
Components.interfaces.nsIDOMEventListener,
|
|
Components.interfaces.nsISelectionListener]),
|
|
|
|
/* nsIObserver */
|
|
observe: function(aSubject, aTopic, aData) {
|
|
if (aTopic == "app-startup") {
|
|
// It's now safe to get our pref branch.
|
|
this.mPrefs = Components.classes["@mozilla.org/preferences-service;1"]
|
|
.getService(Components.interfaces.nsIPrefService)
|
|
.getBranch("accessibility.typeaheadfind.");
|
|
// We need to add our event listeners to all windows.
|
|
Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
|
|
.getService(Components.interfaces.nsIWindowWatcher)
|
|
.registerNotification(this);
|
|
// We also need to listen for find again commands
|
|
Components.classes["@mozilla.org/observer-service;1"]
|
|
.getService(Components.interfaces.nsIObserverService)
|
|
.addObserver(this, "nsWebBrowserFind_FindAgain", true);
|
|
}
|
|
if (aTopic == "domwindowopened") {
|
|
// Add our listeners. They get automatically removed on window teardown.
|
|
aSubject.controllers.appendController(new findTypeController(this, aSubject));
|
|
aSubject.addEventListener("keypress", this, false);
|
|
}
|
|
if (aTopic == "nsWebBrowserFind_FindAgain" &&
|
|
aSubject instanceof Components.interfaces.nsISupportsInterfacePointer &&
|
|
aSubject.data instanceof Components.interfaces.nsIDOMWindow &&
|
|
aSubject.data.top == this.mCurrentWindow &&
|
|
this.mSearchString) {
|
|
// It's a find again. Was it one that we just searched for?
|
|
var w = aSubject.data;
|
|
var find = w.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
|
|
.getInterface(Components.interfaces.nsIWebNavigation)
|
|
.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
|
|
.getInterface(Components.interfaces.nsIWebBrowserFind);
|
|
if (find.searchString.toLowerCase() == this.mSearchString) {
|
|
var reverse = aData == "up";
|
|
this.stopFind(false);
|
|
var result = Components.interfaces.nsITypeAheadFind.FIND_NOTFOUND;
|
|
if (!this.mBadKeysSinceMatch)
|
|
result = this.mFind.findAgain(reverse, this.mLinks);
|
|
this.showStatusMatch(result, reverse ? "prevmatch" : "nextmatch");
|
|
// Don't let anyone else try to find again.
|
|
aSubject.data = null;
|
|
}
|
|
}
|
|
},
|
|
|
|
/* nsITimerCallback */
|
|
notify: function(aTimer) {
|
|
this.stopFind(false);
|
|
},
|
|
|
|
/* nsIDOMEventListener */
|
|
handleEvent: function(aEvent) {
|
|
if (aEvent.type != "keypress") {
|
|
this.stopFind(false);
|
|
return true;
|
|
}
|
|
|
|
// We don't care about these keys.
|
|
if (aEvent.altKey || aEvent.ctrlKey || aEvent.metaKey)
|
|
return true;
|
|
|
|
// Are we already in a find?
|
|
if (aEvent.eventPhase == Components.interfaces.nsIDOMEvent.CAPTURING_PHASE)
|
|
return this.processKey(aEvent);
|
|
|
|
// Check whether we want to start a new find.
|
|
if (aEvent.getPreventDefault())
|
|
return true;
|
|
|
|
// We don't want to start a find on a control character.
|
|
// We also don't want to start on a space, since that scrolls the page.
|
|
if (aEvent.keyCode || aEvent.charCode <= kSpace)
|
|
return true;
|
|
|
|
// Don't start a find if the focus is an editable element.
|
|
var window = aEvent.currentTarget;
|
|
var element = window.document.commandDispatcher.focusedElement;
|
|
if (element instanceof Components.interfaces.nsIDOMNSHTMLElement &&
|
|
element.contentEditable == "true")
|
|
return true;
|
|
|
|
// Don't start a find if the focus is on a form element.
|
|
if (element instanceof Components.interfaces.nsIDOMXULElement ||
|
|
element instanceof Components.interfaces.nsIDOMHTMLEmbedElement ||
|
|
element instanceof Components.interfaces.nsIDOMHTMLObjectElement ||
|
|
element instanceof Components.interfaces.nsIDOMHTMLIsIndexElement ||
|
|
element instanceof Components.interfaces.nsIDOMHTMLSelectElement ||
|
|
element instanceof Components.interfaces.nsIDOMHTMLTextAreaElement)
|
|
return true;
|
|
|
|
// Don't start a find if the focus is on an editable field
|
|
if (element instanceof Components.interfaces.nsIDOMHTMLInputElement &&
|
|
element.mozIsTextField(false))
|
|
return true;
|
|
|
|
// Don't start a find if the focus isn't or can't be set to content
|
|
var w = window.document.commandDispatcher.focusedWindow;
|
|
if (w.top == window)
|
|
w = window.content;
|
|
if (!w)
|
|
return true;
|
|
|
|
var webNav = w.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
|
|
.getInterface(Components.interfaces.nsIWebNavigation);
|
|
try {
|
|
// Don't start a find if the window is in design mode
|
|
if (webNav.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
|
|
.getInterface(Components.interfaces.nsIEditingSession)
|
|
.windowIsEditable(w))
|
|
return true;
|
|
} catch (e) {
|
|
}
|
|
|
|
switch (aEvent.charCode) {
|
|
// Start finding text as you type
|
|
case kSlash:
|
|
aEvent.preventDefault();
|
|
this.startFind(window, false);
|
|
break;
|
|
|
|
// Start finding links as you type
|
|
case kApostrophe:
|
|
aEvent.preventDefault();
|
|
this.startFind(window, true);
|
|
break;
|
|
|
|
default:
|
|
// Don't start if typeahead find is disabled
|
|
if (!this.mPrefs.getBoolPref("autostart"))
|
|
return true;
|
|
// Don't start in windows that don't want autostart
|
|
if (webNav.QueryInterface(Components.interfaces.nsIDocShell)
|
|
.chromeEventHandler.getAttribute("autofind") == "false")
|
|
return true;
|
|
this.startFind(window, this.mPrefs.getBoolPref("linksonly"));
|
|
this.processKey(aEvent);
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/* nsISelectionListener */
|
|
notifySelectionChanged: function(aDoc, aSelection, aReason) {
|
|
this.stopFind(false);
|
|
},
|
|
|
|
/* private methods */
|
|
showStatus: function(aText) {
|
|
if (this.mXULBrowserWindow)
|
|
this.mXULBrowserWindow.setOverLink(aText, null);
|
|
},
|
|
showStatusString: function(aString) {
|
|
// Set the status text from a localised string
|
|
this.showStatus(aString && this.mBundle.GetStringFromName(aString));
|
|
},
|
|
showStatusMatch: function(aResult, aExtra) {
|
|
// Set the status text from a find result
|
|
// link|text "..." [not] found [next|previous match] [(href)]
|
|
if (aExtra)
|
|
aExtra = " " + this.mBundle.GetStringFromName(aExtra);
|
|
var url = "";
|
|
var string = this.mLinks ? "link" : "text";
|
|
if (aResult == Components.interfaces.nsITypeAheadFind.FIND_NOTFOUND)
|
|
string += "not";
|
|
else if (this.mFind.foundLink && this.mFind.foundLink.href)
|
|
url = " " + this.mBundle.GetStringFromName("openparen") +
|
|
this.mFind.foundLink.href +
|
|
this.mBundle.GetStringFromName("closeparen");
|
|
string += "found";
|
|
this.showStatus(this.mBundle.GetStringFromName(string) +
|
|
this.mSearchString +
|
|
this.mBundle.GetStringFromName("closequote") +
|
|
aExtra + url);
|
|
},
|
|
startTimer: function() {
|
|
if (this.mPrefs.getBoolPref("enabletimeout")) {
|
|
if (!this.mTimer)
|
|
this.mTimer = Components.classes["@mozilla.org/timer;1"]
|
|
.createInstance(Components.interfaces.nsITimer);
|
|
this.mTimer.initWithCallback(this,
|
|
this.mPrefs.getIntPref("timeout"),
|
|
Components.interfaces.nsITimer.TYPE_ONE_SHOT);
|
|
}
|
|
},
|
|
processKey: function(aEvent) {
|
|
// Escape always cancels the find.
|
|
if (aEvent.keyCode == Components.interfaces.nsIDOMKeyEvent.DOM_VK_ESCAPE) {
|
|
aEvent.preventDefault();
|
|
this.stopFind(false);
|
|
return false;
|
|
}
|
|
|
|
var result = Components.interfaces.nsITypeAheadFind.FIND_NOTFOUND;
|
|
if (aEvent.keyCode == Components.interfaces.nsIDOMKeyEvent.DOM_VK_BACK_SPACE) {
|
|
aEvent.preventDefault();
|
|
this.mSearchString = this.mSearchString.slice(0, -1);
|
|
// Backspacing past the start of the string cancels the find.
|
|
if (!this.mSearchString) {
|
|
this.stopFind(true);
|
|
return false;
|
|
}
|
|
this.startTimer();
|
|
// The find will change the selection, so stop listening for changes
|
|
this.mEventTarget.removeEventListener("blur", this, true);
|
|
if (this.mSelection)
|
|
this.mSelection.removeSelectionListener(this);
|
|
// Don't bother finding until we get back to a working string
|
|
if (!this.mBadKeysSinceMatch || !--this.mBadKeysSinceMatch)
|
|
result = this.mFind.find(this.mSearchString, this.mLinks);
|
|
} else {
|
|
// Ignore control characters.
|
|
if (aEvent.keyCode || aEvent.charCode < kSpace)
|
|
return true;
|
|
|
|
this.startTimer();
|
|
aEvent.preventDefault();
|
|
|
|
// It looks as if the cat walked on the keyboard.
|
|
if (this.mBadKeysSinceMatch >= 3)
|
|
return false;
|
|
|
|
// The find will change the selection/focus, so stop listening for changes
|
|
this.mEventTarget.removeEventListener("blur", this, true);
|
|
if (this.mSelection)
|
|
this.mSelection.removeSelectionListener(this);
|
|
var previousString = this.mSearchString;
|
|
this.mSearchString += String.fromCharCode(aEvent.charCode).toLowerCase();
|
|
if (!this.mBadKeysSinceMatch) {
|
|
result = this.mFind.find(this.mSearchString, this.mLinks);
|
|
if (previousString &&
|
|
result == Components.interfaces.nsITypeAheadFind.FIND_NOTFOUND)
|
|
// Use a separate find instance to rehighlight the previous match
|
|
// until bug 463294 is fixed.
|
|
this.mFound.find(previousString, this.mLinks);
|
|
}
|
|
if (result == Components.interfaces.nsITypeAheadFind.FIND_NOTFOUND)
|
|
this.mBadKeysSinceMatch++;
|
|
}
|
|
|
|
// Ensure that the correct frame is focused (work around for bug 485213).
|
|
if (this.mFind.currentWindow)
|
|
this.mFind.currentWindow.focus();
|
|
|
|
this.showStatusMatch(result, "");
|
|
if (!this.mFindService)
|
|
this.mFindService = Components.classes["@mozilla.org/find/find_service;1"]
|
|
.getService(Components.interfaces.nsIFindService);
|
|
this.mFindService.searchString = this.mSearchString;
|
|
// Watch for blur changes in case the cursor leaves the current field.
|
|
this.mEventTarget.addEventListener("blur", this, true);
|
|
// Also watch for the cursor moving within the current field or window.
|
|
var commandDispatcher = this.mEventTarget.ownerDocument.commandDispatcher;
|
|
var editable = commandDispatcher.focusedElement;
|
|
if (editable instanceof Components.interfaces.nsIDOMNSEditableElement)
|
|
this.mSelection = editable.editor.selection;
|
|
else
|
|
this.mSelection = commandDispatcher.focusedWindow.getSelection();
|
|
this.mSelection.QueryInterface(Components.interfaces.nsISelectionPrivate)
|
|
.addSelectionListener(this);
|
|
return false;
|
|
},
|
|
startFind: function(aWindow, aLinks) {
|
|
if (this.mEventTarget)
|
|
this.stopFind(true);
|
|
// Try to get the status bar for the specified window
|
|
this.mXULBrowserWindow =
|
|
aWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
|
|
.getInterface(Components.interfaces.nsIWebNavigation)
|
|
.QueryInterface(Components.interfaces.nsIDocShellTreeItem)
|
|
.treeOwner
|
|
.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
|
|
.getInterface(Components.interfaces.nsIXULWindow)
|
|
.XULBrowserWindow;
|
|
|
|
// If the current window is chrome then focus content instead
|
|
var w = aWindow.document.commandDispatcher.focusedWindow.top;
|
|
if (w == aWindow)
|
|
(w = aWindow.content).focus();
|
|
|
|
// Get two toolkit typeaheadfind instances if we don't have them already.
|
|
var docShell = w.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
|
|
.getInterface(Components.interfaces.nsIWebNavigation)
|
|
.QueryInterface(Components.interfaces.nsIDocShell);
|
|
if (!this.mFind) {
|
|
this.mFind = Components.classes["@mozilla.org/typeaheadfind;1"]
|
|
.createInstance(Components.interfaces.nsITypeAheadFind);
|
|
this.mFind.init(docShell);
|
|
this.mFound = Components.classes["@mozilla.org/typeaheadfind;1"]
|
|
.createInstance(Components.interfaces.nsITypeAheadFind);
|
|
this.mFound.init(docShell);
|
|
}
|
|
|
|
// Get the string bundle if we don't have it already.
|
|
if (!this.mBundle)
|
|
this.mBundle = Components.classes["@mozilla.org/intl/stringbundle;1"]
|
|
.getService(Components.interfaces.nsIStringBundleService)
|
|
.createBundle("chrome://communicator/locale/typeaheadfind.properties");
|
|
|
|
// Set up all our properties
|
|
this.mFind.setDocShell(docShell);
|
|
this.mFound.setDocShell(docShell);
|
|
this.mEventTarget = docShell.chromeEventHandler;
|
|
this.mEventTarget.addEventListener("keypress", this, true);
|
|
this.mEventTarget.addEventListener("pagehide", this, true);
|
|
this.mCurrentWindow = w;
|
|
this.mBadKeysSinceMatch = 0;
|
|
this.mSearchString = "";
|
|
this.mLinks = aLinks;
|
|
this.showStatusString(this.mLinks ? "startlinkfind" : "starttextfind");
|
|
this.startTimer();
|
|
},
|
|
stopFind: function(aClear) {
|
|
if (this.mTimer)
|
|
this.mTimer.cancel();
|
|
if (this.mFind)
|
|
this.mFind.setSelectionModeAndRepaint(
|
|
Components.interfaces.nsISelectionController.SELECTION_ON);
|
|
if (this.mEventTarget) {
|
|
this.mEventTarget.removeEventListener("blur", this, true);
|
|
this.mEventTarget.removeEventListener("pagehide", this, true);
|
|
this.mEventTarget.removeEventListener("keypress", this, true);
|
|
}
|
|
this.mEventTarget = null;
|
|
if (this.mSelection)
|
|
this.mSelection.removeSelectionListener(this);
|
|
this.mSelection = null;
|
|
this.showStatusString(aClear ? "" : "stopfind");
|
|
if (aClear)
|
|
this.mSearchString = "";
|
|
if (aClear && this.mFind)
|
|
this.mFind.collapseSelection();
|
|
},
|
|
};
|
|
|
|
var NSGetFactory = XPCOMUtils.generateNSGetFactory([typeAheadFind]);
|