mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-23 00:47:31 +09:00
544 lines
18 KiB
JavaScript
544 lines
18 KiB
JavaScript
/* 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 Cc = Components.classes;
|
|
var Ci = Components.interfaces;
|
|
var Cr = Components.results;
|
|
var Cu = Components.utils;
|
|
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
Cu.import("resource:///modules/errUtils.js");
|
|
|
|
var Gloda = null;
|
|
var GlodaUtils = null;
|
|
var MultiSuffixTree = null;
|
|
var TagNoun = null;
|
|
var FreeTagNoun = null;
|
|
|
|
function ResultRowFullText(aItem, words, typeForStyle) {
|
|
this.item = aItem;
|
|
this.words = words;
|
|
this.typeForStyle = "gloda-fulltext-" + typeForStyle;
|
|
}
|
|
ResultRowFullText.prototype = {
|
|
multi: false,
|
|
fullText: true
|
|
};
|
|
|
|
function ResultRowSingle(aItem, aCriteriaType, aCriteria, aExplicitNounID) {
|
|
this.nounID = aExplicitNounID || aItem.NOUN_ID;
|
|
this.nounDef = Gloda._nounIDToDef[this.nounID];
|
|
this.criteriaType = aCriteriaType;
|
|
this.criteria = aCriteria;
|
|
this.item = aItem;
|
|
this.typeForStyle = "gloda-single-" + this.nounDef.name;
|
|
}
|
|
ResultRowSingle.prototype = {
|
|
multi: false,
|
|
fullText: false
|
|
};
|
|
|
|
function ResultRowMulti(aNounID, aCriteriaType, aCriteria, aQuery) {
|
|
this.nounID = aNounID;
|
|
this.nounDef = Gloda._nounIDToDef[aNounID];
|
|
this.criteriaType = aCriteriaType;
|
|
this.criteria = aCriteria;
|
|
this.collection = aQuery.getCollection(this);
|
|
this.collection.becomeExplicit();
|
|
this.renderer = null;
|
|
}
|
|
ResultRowMulti.prototype = {
|
|
multi: true,
|
|
typeForStyle: "gloda-multi",
|
|
fullText: false,
|
|
onItemsAdded: function(aItems) {
|
|
if (this.renderer) {
|
|
for (let [iItem, item] of aItems.entries()) {
|
|
this.renderer.renderItem(item);
|
|
}
|
|
}
|
|
},
|
|
onItemsModified: function(aItems) {
|
|
},
|
|
onItemsRemoved: function(aItems) {
|
|
},
|
|
onQueryCompleted: function() {
|
|
}
|
|
};
|
|
|
|
function nsAutoCompleteGlodaResult(aListener, aCompleter, aString) {
|
|
this.listener = aListener;
|
|
this.completer = aCompleter;
|
|
this.searchString = aString;
|
|
this._results = [];
|
|
this._pendingCount = 0;
|
|
this._problem = false;
|
|
// Track whether we have reported anything to the complete controller so
|
|
// that we know not to send notifications to it during calls to addRows
|
|
// prior to that point.
|
|
this._initiallyReported = false;
|
|
|
|
this.wrappedJSObject = this;
|
|
}
|
|
nsAutoCompleteGlodaResult.prototype = {
|
|
getObjectAt: function(aIndex) {
|
|
return this._results[aIndex] || null;
|
|
},
|
|
markPending: function ACGR_markPending(aCompleter) {
|
|
this._pendingCount++;
|
|
},
|
|
markCompleted: function ACGR_markCompleted(aCompleter) {
|
|
if (--this._pendingCount == 0 && this.active) {
|
|
this.listener.onSearchResult(this.completer, this);
|
|
}
|
|
},
|
|
announceYourself: function ACGR_announceYourself() {
|
|
this._initiallyReported = true;
|
|
this.listener.onSearchResult(this.completer, this);
|
|
},
|
|
addRows: function ACGR_addRows(aRows) {
|
|
if (!aRows.length)
|
|
return;
|
|
this._results.push.apply(this._results, aRows);
|
|
if (this._initiallyReported && this.active) {
|
|
this.listener.onSearchResult(this.completer, this);
|
|
}
|
|
},
|
|
// ==== nsIAutoCompleteResult
|
|
searchString: null,
|
|
get searchResult() {
|
|
if (this._problem)
|
|
return Ci.nsIAutoCompleteResult.RESULT_FAILURE;
|
|
if (this._results.length)
|
|
return (!this._pendingCount) ? Ci.nsIAutoCompleteResult.RESULT_SUCCESS
|
|
: Ci.nsIAutoCompleteResult.RESULT_SUCCESS_ONGOING;
|
|
else
|
|
return (!this._pendingCount) ? Ci.nsIAutoCompleteResult.RESULT_NOMATCH
|
|
: Ci.nsIAutoCompleteResult.RESULT_NOMATCH_ONGOING;
|
|
},
|
|
active: false,
|
|
defaultIndex: -1,
|
|
errorDescription: null,
|
|
get matchCount() {
|
|
return (this._results === null) ? 0 : this._results.length;
|
|
},
|
|
// this is the lower text, (shows the url in firefox)
|
|
// we try and show the contact's name here.
|
|
getValueAt: function(aIndex) {
|
|
let thing = this._results[aIndex];
|
|
return thing.name || thing.value || thing.subject || null;
|
|
},
|
|
getLabelAt: function(aIndex) {
|
|
return this.getValueAt(aIndex);
|
|
},
|
|
// rich uses this to be the "title". it is the upper text
|
|
// we try and show the identity here.
|
|
getCommentAt: function(aIndex) {
|
|
let thing = this._results[aIndex];
|
|
if (thing.value) // identity
|
|
return thing.contact.name;
|
|
else
|
|
return thing.name || thing.subject;
|
|
},
|
|
// rich uses this to be the "type"
|
|
getStyleAt: function(aIndex) {
|
|
let row = this._results[aIndex];
|
|
return row.typeForStyle;
|
|
},
|
|
// rich uses this to be the icon
|
|
getImageAt: function(aIndex) {
|
|
let thing = this._results[aIndex];
|
|
if (!thing.value)
|
|
return null;
|
|
|
|
return ""; // we don't want to use gravatars as is.
|
|
/*
|
|
let md5hash = GlodaUtils.md5HashString(thing.value);
|
|
let gravURL = "http://www.gravatar.com/avatar/" + md5hash +
|
|
"?d=identicon&s=32&r=g";
|
|
return gravURL;
|
|
*/
|
|
},
|
|
getFinalCompleteValueAt: function(aIndex) {
|
|
return this.getValueAt(aIndex);
|
|
},
|
|
removeValueAt: function() {},
|
|
|
|
_stop: function() {
|
|
}
|
|
};
|
|
|
|
var MAX_POPULAR_CONTACTS = 200;
|
|
|
|
/**
|
|
* Complete contacts/identities based on name/email. Instant phase is based on
|
|
* a suffix-tree built of popular contacts/identities. Delayed phase relies
|
|
* on a LIKE search of all known contacts.
|
|
*/
|
|
function ContactIdentityCompleter() {
|
|
// get all the contacts
|
|
let contactQuery = Gloda.newQuery(Gloda.NOUN_CONTACT);
|
|
contactQuery.orderBy("-popularity").limit(MAX_POPULAR_CONTACTS);
|
|
this.contactCollection = contactQuery.getCollection(this, null);
|
|
this.contactCollection.becomeExplicit();
|
|
}
|
|
ContactIdentityCompleter.prototype = {
|
|
_popularitySorter: function(a, b){ return b.popularity - a.popularity; },
|
|
complete: function ContactIdentityCompleter_complete(aResult, aString) {
|
|
if (aString.length < 3) {
|
|
// In CJK, first name or last name is sometime used as 1 character only.
|
|
// So we allow autocompleted search even if 1 character.
|
|
//
|
|
// [U+3041 - U+9FFF ... Full-width Katakana, Hiragana
|
|
// and CJK Ideograph
|
|
// [U+AC00 - U+D7FF ... Hangul
|
|
// [U+F900 - U+FFDC ... CJK compatibility ideograph
|
|
if (!aString.match(/[\u3041-\u9fff\uac00-\ud7ff\uf900-\uffdc]/))
|
|
return false;
|
|
}
|
|
|
|
let matches;
|
|
if (this.suffixTree) {
|
|
matches = this.suffixTree.findMatches(aString.toLowerCase());
|
|
}
|
|
else
|
|
matches = [];
|
|
|
|
// let's filter out duplicates due to identity/contact double-hits by
|
|
// establishing a map based on the contact id for these guys.
|
|
// let's also favor identities as we do it, because that gets us the
|
|
// most accurate gravat, potentially
|
|
let contactToThing = {};
|
|
for (let iMatch = 0; iMatch < matches.length; iMatch++) {
|
|
let thing = matches[iMatch];
|
|
if (thing.NOUN_ID == Gloda.NOUN_CONTACT && !(thing.id in contactToThing))
|
|
contactToThing[thing.id] = thing;
|
|
else if (thing.NOUN_ID == Gloda.NOUN_IDENTITY)
|
|
contactToThing[thing.contactID] = thing;
|
|
}
|
|
// and since we can now map from contacts down to identities, map contacts
|
|
// to the first identity for them that we find...
|
|
matches = Object.keys(contactToThing).map(id => contactToThing[id]).
|
|
map(val => val.NOUN_ID == Gloda.NOUN_IDENTITY ? val : val.identities[0]);
|
|
|
|
let rows = matches.
|
|
map(match => new ResultRowSingle(match, "text", aResult.searchString));
|
|
aResult.addRows(rows);
|
|
|
|
// - match against database contacts / identities
|
|
let pending = {contactToThing: contactToThing, pendingCount: 2};
|
|
|
|
let contactQuery = Gloda.newQuery(Gloda.NOUN_CONTACT);
|
|
contactQuery.nameLike(contactQuery.WILDCARD, aString,
|
|
contactQuery.WILDCARD);
|
|
pending.contactColl = contactQuery.getCollection(this, aResult);
|
|
pending.contactColl.becomeExplicit();
|
|
|
|
let identityQuery = Gloda.newQuery(Gloda.NOUN_IDENTITY);
|
|
identityQuery.kind("email").valueLike(identityQuery.WILDCARD, aString,
|
|
identityQuery.WILDCARD);
|
|
pending.identityColl = identityQuery.getCollection(this, aResult);
|
|
pending.identityColl.becomeExplicit();
|
|
|
|
aResult._contactCompleterPending = pending;
|
|
|
|
return true;
|
|
},
|
|
onItemsAdded: function(aItems, aCollection) {
|
|
},
|
|
onItemsModified: function(aItems, aCollection) {
|
|
},
|
|
onItemsRemoved: function(aItems, aCollection) {
|
|
},
|
|
onQueryCompleted: function(aCollection) {
|
|
// handle the initial setup case...
|
|
if (aCollection.data == null) {
|
|
// cheat and explicitly add our own contact...
|
|
if (Gloda.myContact &&
|
|
!(Gloda.myContact.id in this.contactCollection._idMap))
|
|
this.contactCollection._onItemsAdded([Gloda.myContact]);
|
|
|
|
// the set of identities owned by the contacts is automatically loaded as part
|
|
// of the contact loading...
|
|
// (but only if we actually have any contacts)
|
|
this.identityCollection =
|
|
this.contactCollection.subCollections[Gloda.NOUN_IDENTITY];
|
|
|
|
let contactNames = this.contactCollection.items.
|
|
map(c => c.name.replace(" ", "").toLowerCase() || "x");
|
|
// if we had no contacts, we will have no identity collection!
|
|
let identityMails;
|
|
if (this.identityCollection)
|
|
identityMails = this.identityCollection.items.
|
|
map(i => i.value.toLowerCase());
|
|
|
|
// The suffix tree takes two parallel lists; the first contains strings
|
|
// while the second contains objects that correspond to those strings.
|
|
// In the degenerate case where identityCollection does not exist, it will
|
|
// be undefined. Calling concat with an argument of undefined simply
|
|
// duplicates the list we called concat on, and is thus harmless. Our
|
|
// use of && on identityCollection allows its undefined value to be
|
|
// passed through to concat. identityMails will likewise be undefined.
|
|
this.suffixTree = new MultiSuffixTree(contactNames.concat(identityMails),
|
|
this.contactCollection.items.concat(this.identityCollection &&
|
|
this.identityCollection.items));
|
|
|
|
return;
|
|
}
|
|
|
|
// handle the completion case
|
|
let result = aCollection.data;
|
|
let pending = result._contactCompleterPending;
|
|
|
|
if (--pending.pendingCount == 0) {
|
|
let possibleDudes = [];
|
|
|
|
let contactToThing = pending.contactToThing;
|
|
|
|
let items;
|
|
|
|
// check identities first because they are better than contacts in terms
|
|
// of display
|
|
items = pending.identityColl.items;
|
|
for (let iIdentity = 0; iIdentity < items.length; iIdentity++){
|
|
let identity = items[iIdentity];
|
|
if (!(identity.contactID in contactToThing)) {
|
|
contactToThing[identity.contactID] = identity;
|
|
possibleDudes.push(identity);
|
|
// augment the identity with its contact's popularity
|
|
identity.popularity = identity.contact.popularity;
|
|
}
|
|
}
|
|
items = pending.contactColl.items;
|
|
for (let iContact = 0; iContact < items.length; iContact++) {
|
|
let contact = items[iContact];
|
|
if (!(contact.id in contactToThing)) {
|
|
contactToThing[contact.id] = contact;
|
|
possibleDudes.push(contact.identities[0]);
|
|
}
|
|
}
|
|
|
|
// sort in order of descending popularity
|
|
possibleDudes.sort(this._popularitySorter);
|
|
let rows = possibleDudes.
|
|
map(dude => new ResultRowSingle(dude, "text", result.searchString));
|
|
result.addRows(rows);
|
|
result.markCompleted(this);
|
|
|
|
// the collections no longer care about the result, make it clear.
|
|
delete pending.identityColl.data;
|
|
delete pending.contactColl.data;
|
|
// the result object no longer needs us or our data
|
|
delete result._contactCompleterPending;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Complete tags that are used on contacts.
|
|
*/
|
|
function ContactTagCompleter() {
|
|
FreeTagNoun.populateKnownFreeTags();
|
|
this._buildSuffixTree();
|
|
FreeTagNoun.addListener(this);
|
|
}
|
|
ContactTagCompleter.prototype = {
|
|
_buildSuffixTree: function() {
|
|
let tagNames = [], tags = [];
|
|
for (let [tagName, tag] in Iterator(FreeTagNoun.knownFreeTags)) {
|
|
tagNames.push(tagName.toLowerCase());
|
|
tags.push(tag);
|
|
}
|
|
this._suffixTree = new MultiSuffixTree(tagNames, tags);
|
|
this._suffixTreeDirty = false;
|
|
},
|
|
onFreeTagAdded: function(aTag) {
|
|
this._suffixTreeDirty = true;
|
|
},
|
|
complete: function ContactTagCompleter_complete(aResult, aString) {
|
|
// now is not the best time to do this; have onFreeTagAdded use a timer.
|
|
if (this._suffixTreeDirty)
|
|
this._buildSuffixTree();
|
|
|
|
if (aString.length < 2)
|
|
return false; // no async mechanism that will add new rows
|
|
|
|
let tags = this._suffixTree.findMatches(aString.toLowerCase());
|
|
let rows = [];
|
|
for (let tag of tags) {
|
|
let query = Gloda.newQuery(Gloda.NOUN_CONTACT);
|
|
query.freeTags(tag);
|
|
let resRow = new ResultRowMulti(Gloda.NOUN_CONTACT, "tag", tag.name,
|
|
query);
|
|
rows.push(resRow);
|
|
}
|
|
aResult.addRows(rows);
|
|
|
|
return false; // no async mechanism that will add new rows
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Complete tags that are used on messages
|
|
*/
|
|
function MessageTagCompleter() {
|
|
this._buildSuffixTree();
|
|
}
|
|
MessageTagCompleter.prototype = {
|
|
_buildSuffixTree: function MessageTagCompleter__buildSufficeTree() {
|
|
let tagNames = [], tags = [];
|
|
let tagArray = TagNoun.getAllTags();
|
|
for (let iTag = 0; iTag < tagArray.length; iTag++) {
|
|
let tag = tagArray[iTag];
|
|
tagNames.push(tag.tag.toLowerCase());
|
|
tags.push(tag);
|
|
}
|
|
this._suffixTree = new MultiSuffixTree(tagNames, tags);
|
|
this._suffixTreeDirty = false;
|
|
},
|
|
complete: function MessageTagCompleter_complete(aResult, aString) {
|
|
if (aString.length < 2)
|
|
return false;
|
|
|
|
let tags = this._suffixTree.findMatches(aString.toLowerCase());
|
|
let rows = [];
|
|
for (let tag of tags) {
|
|
let resRow = new ResultRowSingle(tag, "tag", tag.tag, TagNoun.id);
|
|
rows.push(resRow);
|
|
}
|
|
aResult.addRows(rows);
|
|
|
|
return false; // no async mechanism that will add new rows
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Complete with helpful hints about full-text search
|
|
*/
|
|
function FullTextCompleter() {
|
|
}
|
|
FullTextCompleter.prototype = {
|
|
complete: function FullTextCompleter_complete(aResult, aSearchString) {
|
|
if (aSearchString.length < 4)
|
|
return false;
|
|
// We use code very similar to that in msg_search.js, except that we
|
|
// need to detect when we found phrases, as well as strip commas.
|
|
aSearchString = aSearchString.trim();
|
|
let terms = [];
|
|
let phraseFound = false;
|
|
while (aSearchString) {
|
|
let term = "";
|
|
if (aSearchString.startsWith('"')) {
|
|
let endIndex = aSearchString.indexOf(aSearchString[0], 1);
|
|
// eat the quote if it has no friend
|
|
if (endIndex == -1) {
|
|
aSearchString = aSearchString.substring(1);
|
|
continue;
|
|
}
|
|
phraseFound = true;
|
|
term = aSearchString.substring(1, endIndex).trim();
|
|
if (term)
|
|
terms.push(term);
|
|
aSearchString = aSearchString.substring(endIndex + 1);
|
|
continue;
|
|
}
|
|
|
|
let spaceIndex = aSearchString.indexOf(" ");
|
|
if (spaceIndex == -1) {
|
|
terms.push(aSearchString.replace(/,/g, ""));
|
|
break;
|
|
}
|
|
|
|
term = aSearchString.substring(0, spaceIndex).replace(/,/g, "");
|
|
if (term)
|
|
terms.push(term);
|
|
aSearchString = aSearchString.substring(spaceIndex+1);
|
|
}
|
|
|
|
if (terms.length == 1 && !phraseFound)
|
|
aResult.addRows([new ResultRowFullText(aSearchString, terms, "single")]);
|
|
else
|
|
aResult.addRows([new ResultRowFullText(aSearchString, terms, "all")]);
|
|
|
|
return false; // no async mechanism that will add new rows
|
|
}
|
|
};
|
|
|
|
var LOG;
|
|
|
|
function nsAutoCompleteGloda() {
|
|
this.wrappedJSObject = this;
|
|
try {
|
|
// set up our awesome globals!
|
|
if (Gloda === null) {
|
|
let loadNS = {};
|
|
Cu.import("resource:///modules/gloda/public.js", loadNS);
|
|
Gloda = loadNS.Gloda;
|
|
|
|
Cu.import("resource:///modules/gloda/utils.js", loadNS);
|
|
GlodaUtils = loadNS.GlodaUtils;
|
|
Cu.import("resource:///modules/gloda/suffixtree.js", loadNS);
|
|
MultiSuffixTree = loadNS.MultiSuffixTree;
|
|
Cu.import("resource:///modules/gloda/noun_tag.js", loadNS);
|
|
TagNoun = loadNS.TagNoun;
|
|
Cu.import("resource:///modules/gloda/noun_freetag.js", loadNS);
|
|
FreeTagNoun = loadNS.FreeTagNoun;
|
|
|
|
Cu.import("resource:///modules/gloda/log4moz.js", loadNS);
|
|
LOG = loadNS["Log4Moz"].repository.getLogger("gloda.autocomp");
|
|
}
|
|
|
|
this.completers = [];
|
|
this.curResult = null;
|
|
|
|
this.completers.push(new FullTextCompleter()); // not async.
|
|
this.completers.push(new ContactIdentityCompleter()); // potentially async.
|
|
this.completers.push(new ContactTagCompleter()); // not async.
|
|
this.completers.push(new MessageTagCompleter()); // not async.
|
|
} catch (e) {
|
|
logException(e);
|
|
}
|
|
}
|
|
|
|
nsAutoCompleteGloda.prototype = {
|
|
classID: Components.ID("{3bbe4d77-3f70-4252-9500-bc00c26f476d}"),
|
|
QueryInterface: XPCOMUtils.generateQI([
|
|
Components.interfaces.nsIAutoCompleteSearch]),
|
|
|
|
startSearch: function(aString, aParam, aResult, aListener) {
|
|
try {
|
|
let result = new nsAutoCompleteGlodaResult(aListener, this, aString);
|
|
// save this for hacky access to the search. I somewhat suspect we simply
|
|
// should not be using the formal autocomplete mechanism at all.
|
|
// Used in glodacomplete.xml.
|
|
this.curResult = result;
|
|
|
|
// Guard against late async results being sent.
|
|
this.curResult.active = true;
|
|
|
|
if (aParam == "global") {
|
|
for (let completer of this.completers) {
|
|
// they will return true if they have something pending.
|
|
if (completer.complete(result, aString))
|
|
result.markPending(completer);
|
|
}
|
|
//} else {
|
|
// It'd be nice to do autocomplete in the quicksearch modes based
|
|
// on the specific values for that mode in the current view.
|
|
// But we don't do that yet.
|
|
}
|
|
|
|
result.announceYourself();
|
|
} catch (e) {
|
|
logException(e);
|
|
}
|
|
},
|
|
|
|
stopSearch: function() {
|
|
this.curResult.active = false;
|
|
}
|
|
};
|
|
|
|
var components = [nsAutoCompleteGloda];
|
|
var NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
|