mirror of https://github.com/roytam1/boc-uxp.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1768 lines
66 KiB
1768 lines
66 KiB
<?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="glodaFacetBindings" |
|
xmlns="http://www.mozilla.org/xbl" |
|
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" |
|
xmlns:html="http://www.w3.org/1999/xhtml" |
|
xmlns:xbl="http://www.mozilla.org/xbl" |
|
xmlns:svg="http://www.w3.org/2000/svg"> |
|
|
|
<!-- ===== Constraints ===== --> |
|
|
|
<binding id="query-explanation"> |
|
<content> |
|
</content> |
|
<implementation> |
|
<constructor><![CDATA[ |
|
Components.utils.import("resource:///modules/mailServices.js"); |
|
]]></constructor> |
|
<!-- Indicate that we are based on a fulltext search--> |
|
<method name="setFulltext"> |
|
<parameter name="aMsgSearcher" /> |
|
<body><![CDATA[ |
|
while (this.hasChildNodes()) |
|
this.lastChild.remove(); |
|
|
|
let dis = this; |
|
let spanify = function(aText, aClass) { |
|
let span = document.createElement("span"); |
|
span.setAttribute("class", aClass); |
|
span.textContent = aText; |
|
dis.appendChild(span); |
|
return span; |
|
} |
|
|
|
let searchLabel = glodaFacetStrings.get( |
|
"glodaFacetView.search.label"); |
|
spanify(searchLabel, "explanation-fulltext-label"); |
|
|
|
let criteriaText = glodaFacetStrings.get( |
|
"glodaFacetView.constraints.query.fulltext." + |
|
(aMsgSearcher.andTerms ? "and" : "or") + "JoinWord"); |
|
for (let [iTerm, term] of aMsgSearcher.fulltextTerms.entries()) { |
|
if (iTerm) |
|
spanify(criteriaText, "explanation-fulltext-criteria"); |
|
spanify(term, "explanation-fulltext-term"); |
|
} |
|
]]></body> |
|
</method> |
|
<method name="setQuery"> |
|
<parameter name="aMsgQuery" /> |
|
<body><![CDATA[ |
|
try { |
|
while (this.hasChildNodes()) |
|
this.lastChild.remove(); |
|
|
|
let dis = this; |
|
let spanify = function(aText, aClass) { |
|
let span = document.createElement("span"); |
|
span.setAttribute("class", aClass); |
|
span.textContent = aText; |
|
dis.appendChild(span); |
|
return span; |
|
} |
|
|
|
let label = glodaFacetStrings.get( |
|
"glodaFacetView.search.label"); |
|
spanify(label, "explanation-query-label"); |
|
|
|
let constraintStrings = []; |
|
for (let constraint of aMsgQuery._constraints) { |
|
if (constraint[0] != 1) return; // no idea what this is about |
|
if (constraint[1].attributeName == 'involves') { |
|
let involvesLabel = glodaFacetStrings.get( |
|
"glodaFacetView.constraints.query.involves.label"); |
|
involvesLabel = involvesLabel.replace("#1", constraint[2].value) |
|
spanify(involvesLabel, "explanation-query-involves"); |
|
} else if (constraint[1].attributeName == 'tag') { |
|
let tagLabel = glodaFacetStrings.get( |
|
"glodaFacetView.constraints.query.tagged.label"); |
|
let tag = constraint[2]; |
|
let tagNode = document.createElement("span"); |
|
let colorClass = "blc-" + MailServices.tags.getColorForKey(tag.key).substr(1); |
|
tagNode.setAttribute("class", "message-tag tag " + colorClass); |
|
tagNode.textContent = tag.tag; |
|
spanify(tagLabel, "explanation-query-tagged"); |
|
this.appendChild(tagNode); |
|
} |
|
} |
|
label = label + constraintStrings.join(', '); // XXX l10n? |
|
} catch (e) { |
|
logException(e); |
|
} |
|
]]></body> |
|
</method> |
|
</implementation> |
|
</binding> |
|
|
|
<!-- ===== Facets ===== --> |
|
|
|
<binding id="facets"> |
|
<content> |
|
</content> |
|
<implementation> |
|
<method name="clearFacets"> |
|
<body><![CDATA[ |
|
while (this.hasChildNodes()) |
|
this.lastChild.remove(); |
|
]]></body> |
|
</method> |
|
<method name="addFacet"> |
|
<parameter name="aType" /> |
|
<parameter name="aAttrDef" /> |
|
<parameter name="aArgs" /> |
|
<body><![CDATA[ |
|
let facets = this; |
|
|
|
let facet = document.createElement("div"); |
|
facet.attrDef = aAttrDef; |
|
facet.nounDef = aAttrDef.objectNounDef; |
|
for (let key in aArgs) { |
|
facet[key] = aArgs[key]; |
|
} |
|
facet.setAttribute("class", "facetious"); |
|
facet.setAttribute("type", aType); |
|
facet.setAttribute("name", aAttrDef.attributeName); |
|
facets.appendChild(facet); |
|
|
|
return facet; |
|
]]></body> |
|
</method> |
|
</implementation> |
|
</binding> |
|
|
|
<binding id="facet-base"> |
|
<implementation> |
|
<method name="brushItems"> |
|
<body><![CDATA[ |
|
]]></body> |
|
</method> |
|
<method name="clearBrushedItems"> |
|
<body><![CDATA[ |
|
]]></body> |
|
</method> |
|
</implementation> |
|
</binding> |
|
|
|
<!-- |
|
- A boolean facet; we presume orderedGroups contains at most two entries, and |
|
- that their group values consist of true or false. |
|
- |
|
- The implication of this UI is that you only want to filter to true values |
|
- and would never want to filter out the false values. Consistent with this |
|
- assumption we disable the UI if there are no true values. |
|
- |
|
- This depressingly |
|
--> |
|
<binding id="facet-boolean" |
|
extends="chrome://messenger/content/glodaFacetBindings.xml#facet-base"> |
|
<content> |
|
<html:span anonid="bubble" class="facet-checkbox-bubble"> |
|
<html:input anonid="checkbox" type="checkbox" /> |
|
<html:span anonid="label" class="facet-checkbox-label" /> |
|
<html:span anonid="count" class="facet-checkbox-count" /> |
|
</html:span> |
|
</content> |
|
<implementation> |
|
<constructor><![CDATA[ |
|
this.bubble = document.getAnonymousElementByAttribute(this, "anonid", |
|
"bubble"); |
|
this.checkbox = document.getAnonymousElementByAttribute(this, "anonid", |
|
"checkbox"); |
|
this.labelNode = document.getAnonymousElementByAttribute(this, "anonid", |
|
"label"); |
|
this.countNode = document.getAnonymousElementByAttribute(this, "anonid", |
|
"count"); |
|
|
|
let dis = this; |
|
this.bubble.addEventListener("click", function (event) { |
|
return dis.bubbleClicked(event); |
|
}, true); |
|
|
|
this.extraSetup(); |
|
|
|
if ("faceter" in this) |
|
this.build(true); |
|
]]></constructor> |
|
<field name="canUpdate" readonly="true">true</field> |
|
<property name="disabled"> |
|
<getter><![CDATA[ |
|
return this.getAttribute("disabled") == "true"; |
|
]]></getter> |
|
<setter><![CDATA[ |
|
if (val) { |
|
this.setAttribute("disabled", "true"); |
|
this.checkbox.setAttribute("disabled", true); |
|
} |
|
else { |
|
this.removeAttribute("disabled"); |
|
this.checkbox.removeAttribute("disabled"); |
|
} |
|
]]></setter> |
|
</property> |
|
<property name="checked"> |
|
<getter><![CDATA[ |
|
return this.getAttribute("checked") == "true"; |
|
]]></getter> |
|
<setter><![CDATA[ |
|
if (this.checked == val) |
|
return; |
|
this.checkbox.checked = val; |
|
if (val) { |
|
// the XBL inherits magic appears to fail if we explicitly check the |
|
// box itself rather than via our click handler, presumably because |
|
// we unshadow something. So manually apply changes ourselves. |
|
this.setAttribute("checked", "true"); |
|
this.checkbox.setAttribute("checked", "true"); |
|
if (!this.disabled) |
|
FacetContext.addFacetConstraint(this.faceter, true, |
|
this.trueGroups); |
|
} |
|
else { |
|
this.removeAttribute("checked"); |
|
this.checkbox.removeAttribute("checked"); |
|
if (!this.disabled) |
|
FacetContext.removeFacetConstraint(this.faceter, true, |
|
this.trueGroups); |
|
} |
|
this.checkStateChanged(); |
|
]]></setter> |
|
</property> |
|
<method name="extraSetup"> |
|
<body><![CDATA[ |
|
]]></body> |
|
</method> |
|
<method name="checkStateChanged"> |
|
<body><![CDATA[ |
|
]]></body> |
|
</method> |
|
<method name="build"> |
|
<parameter name="aFirstTime" /> |
|
<body><![CDATA[ |
|
if (aFirstTime) { |
|
this.labelNode.textContent = this.facetDef.strings.facetNameLabel; |
|
this.checkbox.setAttribute("aria-label", |
|
this.facetDef.strings.facetNameLabel); |
|
this.trueValues = []; |
|
} |
|
|
|
// If we do not currently have a constraint applied and there is only |
|
// one (or no) group, then: disable us, but reflect the underlying |
|
// state of the data (checked or non-checked) |
|
if (!this.faceter.constraint && (this.orderedGroups.length <= 1)){ |
|
this.disabled = true; |
|
let count = 0; |
|
if (this.orderedGroups.length) { |
|
// true case? |
|
if (this.orderedGroups[0][0]) { |
|
count = this.orderedGroups[0][1].length; |
|
this.checked = true; |
|
} |
|
else { |
|
this.checked = false; |
|
} |
|
} |
|
this.countNode.textContent = count.toLocaleString(); |
|
return; |
|
} |
|
// if we were disabled checked before, clear ourselves out |
|
if (this.disabled && this.checked) |
|
this.checked = false; |
|
this.disabled = false; |
|
|
|
// if we are here, we have our 2 groups, find true... |
|
// (note: it is possible to get jerked around by null values |
|
// currently, so leave a reasonable failure case) |
|
this.trueValues = []; |
|
this.trueGroups = [true]; |
|
for (let groupPair of this.orderedGroups) { |
|
if (groupPair[0] == true) |
|
this.trueValues = groupPair[1]; |
|
} |
|
|
|
this.countNode.textContent = this.trueValues.length.toLocaleString(); |
|
]]></body> |
|
</method> |
|
<method name="bubbleClicked"> |
|
<parameter name="event" /> |
|
<body><![CDATA[ |
|
if (!this.disabled) |
|
this.checked = !this.checked; |
|
event.stopPropagation(); |
|
]]></body> |
|
</method> |
|
</implementation> |
|
<handlers> |
|
<handler event="mouseover"><![CDATA[ |
|
FacetContext.hoverFacet(this.faceter, this.faceter.attrDef, |
|
true, this.trueValues); |
|
]]></handler> |
|
<handler event="mouseout"><![CDATA[ |
|
FacetContext.unhoverFacet(this.faceter, this.faceter.attrDef, |
|
true, this.trueValues); |
|
]]></handler> |
|
</handlers> |
|
</binding> |
|
|
|
<!-- |
|
- A check-box with filter-box front-end to a standard discrete faceter. If |
|
- there are no non-null values, we disable the UI. If there are non-null |
|
- values the checkbox is enabled and the filter is hidden. Once you check |
|
- the box we apply the facet and show the filtering mechanism. |
|
--> |
|
<binding id="facet-boolean-filtered" |
|
extends="chrome://messenger/content/glodaFacetBindings.xml#facet-boolean"> |
|
<content> |
|
<html:span anonid="bubble" class="facet-checkbox-bubble"> |
|
<html:input anonid="checkbox" type="checkbox" |
|
xbl:inherits="checked,disabled"/> |
|
<html:span anonid="label" class="facet-checkbox-label" /> |
|
<html:span anonid="count" class="facet-checkbox-count" /> |
|
</html:span> |
|
<html:select anonid="filter" class="facet-filter-list" /> |
|
</content> |
|
<implementation> |
|
<method name="extraSetup"> |
|
<body><![CDATA[ |
|
this.filterNode = document.getAnonymousElementByAttribute( |
|
this, "anonid", "filter"); |
|
this.groupDisplayProperty = this.getAttribute("groupDisplayProperty"); |
|
|
|
let dis = this; |
|
this.filterNode.addEventListener("change", function(event) { |
|
return dis.filterChanged(event); |
|
}, false); |
|
|
|
this.selectedValue = "all"; |
|
]]></body> |
|
</method> |
|
<method name="build"> |
|
<parameter name="aFirstTime" /> |
|
<body><![CDATA[ |
|
if (aFirstTime) { |
|
this.labelNode.textContent = this.facetDef.strings.facetNameLabel; |
|
this.checkbox.setAttribute("aria-label", |
|
this.facetDef.strings.facetNameLabel); |
|
this.trueValues = []; |
|
} |
|
|
|
// Only update count if anything other than "all" is selected. |
|
// Otherwise we lose the set of attachment types in our select box, |
|
// and that makes us sad. We do want to update on "all" though |
|
// because other facets may further reduce the number of attachments |
|
// we see. (Or if this is not just being used for attachments, it |
|
// still holds.) |
|
if (this.selectedValue != "all") { |
|
let count = 0; |
|
for (let groupPair of this.orderedGroups) { |
|
if (groupPair[0] != null) |
|
count += groupPair[1].length; |
|
} |
|
this.countNode.textContent = count.toLocaleString(); |
|
return; |
|
} |
|
|
|
while (this.filterNode.hasChildNodes()) |
|
this.filterNode.lastChild.remove(); |
|
let allNode = document.createElement("option"); |
|
allNode.textContent = |
|
glodaFacetStrings.get("glodaFacetView.facets.filter." + |
|
this.attrDef.attributeName + ".allLabel"); |
|
allNode.setAttribute("value", "all"); |
|
if (this.selectedValue == "all") |
|
allNode.setAttribute("selected", "selected"); |
|
this.filterNode.appendChild(allNode); |
|
|
|
// if we are here, we have our 2 groups, find true... |
|
// (note: it is possible to get jerked around by null values |
|
// currently, so leave a reasonable failure case) |
|
// empty true groups is for the checkbox |
|
this.trueGroups = []; |
|
// the real true groups is the actual true values for our explicit |
|
// filtering |
|
this.realTrueGroups = []; |
|
this.trueValues = []; |
|
this.falseValues = []; |
|
let selectNodes = []; |
|
for (let groupPair of this.orderedGroups) { |
|
if (groupPair[0] == null) |
|
this.falseValues.push.apply(this.falseValues, groupPair[1]); |
|
else { |
|
this.trueValues.push.apply(this.trueValues, groupPair[1]); |
|
|
|
let groupValue = groupPair[0]; |
|
let selNode = document.createElement("option"); |
|
selNode.textContent = groupValue[this.groupDisplayProperty]; |
|
selNode.setAttribute("value", this.realTrueGroups.length); |
|
if (this.selectedValue == groupValue.category) |
|
selNode.setAttribute("selected", "selected"); |
|
selectNodes.push(selNode); |
|
|
|
this.realTrueGroups.push(groupValue); |
|
} |
|
} |
|
selectNodes.sort(function(a, b) { |
|
return a.textContent.localeCompare(b.textContent); |
|
}); |
|
selectNodes.forEach(function(selNode) { this.filterNode.appendChild(selNode); }, this); |
|
|
|
this.disabled = !this.trueValues.length; |
|
|
|
this.countNode.textContent = this.trueValues.length.toLocaleString(); |
|
]]></body> |
|
</method> |
|
<method name="checkStateChanged"> |
|
<body><![CDATA[ |
|
// if they un-check us, revert our value to all. |
|
if (!this.checked) |
|
this.selectedValue = "all"; |
|
]]></body> |
|
</method> |
|
<method name="filterChanged"> |
|
<parameter name="event" /> |
|
<body><![CDATA[ |
|
if (!this.checked) |
|
return; |
|
if (this.filterNode.value == "all") { |
|
this.selectedValue = "all"; |
|
FacetContext.addFacetConstraint(this.faceter, true, |
|
this.trueGroups, false, true); |
|
} |
|
else { |
|
let groupValue = this.realTrueGroups[parseInt(this.filterNode.value)]; |
|
this.selectedValue = groupValue.category; |
|
FacetContext.addFacetConstraint(this.faceter, true, |
|
[groupValue], false, true); |
|
} |
|
]]></body> |
|
</method> |
|
</implementation> |
|
</binding> |
|
|
|
<binding id="popup-menu"> |
|
<content> |
|
<html:div anonid="parent" class="parent" |
|
tabindex="0" |
|
><html:div anonid="include-item" class="popup-menuitem top" |
|
tabindex="0" |
|
onmouseover="this.focus()" |
|
onkeypress="if (event.keyCode == event.DOM_VK_RETURN) this.parentNode.parentNode.doInclude()" |
|
onmouseup="this.parentNode.parentNode.doInclude()"></html:div |
|
><html:div anonid="exclude-item" class="popup-menuitem bottom" |
|
tabindex="0" |
|
onmouseover="this.focus()" |
|
onkeypress="if (event.keyCode == event.DOM_VK_RETURN) this.parentNode.parentNode.doExclude()" |
|
onmouseup="this.parentNode.parentNode.doExclude()"></html:div |
|
><html:div anonid="undo-item" class="popup-menuitem undo" |
|
tabindex="0" |
|
onmouseover="this.focus()" |
|
onkeypress="if (event.keyCode == event.DOM_VK_RETURN) this.parentNode.parentNode.doUndo()" |
|
onmouseup="this.parentNode.parentNode.doUndo()"></html:div |
|
></html:div> |
|
</content> |
|
<handlers> |
|
<handler event="keypress" keycode="VK_ESCAPE" |
|
action="this.hide();"/> |
|
<handler event="keypress" keycode="VK_DOWN" |
|
action="this.moveFocus(event, 1);"/> |
|
<handler event="keypress" keycode="VK_TAB" |
|
action="this.moveFocus(event, 1);"/> |
|
<handler event="keypress" keycode="VK_TAB" modifiers="shift" |
|
action="this.moveFocus(event, -1);"/> |
|
<handler event="keypress" keycode="VK_UP" |
|
action="this.moveFocus(event, -1);"/> |
|
</handlers> |
|
<implementation> |
|
<constructor><![CDATA[ |
|
this.includeNode = document.getAnonymousElementByAttribute(this, "anonid", |
|
"include-item"); |
|
this.excludeNode = document.getAnonymousElementByAttribute(this, "anonid", |
|
"exclude-item"); |
|
this.undoNode = document.getAnonymousElementByAttribute(this, "anonid", |
|
"undo-item"); |
|
]]></constructor> |
|
<method name="_getLabel"> |
|
<parameter name="facetDef"/> |
|
<parameter name="facetValue"/> |
|
<parameter name="groupValue"/> |
|
<parameter name="stringName"/> |
|
<body><![CDATA[ |
|
let label, labelFormat; |
|
if (stringName in facetDef.strings) |
|
labelFormat = facetDef.strings[stringName]; |
|
else |
|
labelFormat = glodaFacetStrings.get( |
|
"glodaFacetView.facets."+stringName+".fallbackLabel"); |
|
if (!labelFormat.includes("#1")) |
|
return labelFormat; |
|
else |
|
return labelFormat.replace("#1", facetValue); |
|
]]></body> |
|
</method> |
|
<method name="build"> |
|
<parameter name="facetDef"/> |
|
<parameter name="facetValue"/> |
|
<parameter name="groupValue"/> |
|
<body><![CDATA[ |
|
try { |
|
if (groupValue) |
|
this.includeNode.textContent = this._getLabel(facetDef, facetValue, |
|
groupValue, "mustMatchLabel"); |
|
else |
|
this.includeNode.textContent = this._getLabel(facetDef, facetValue, |
|
groupValue, "mustMatchNoneLabel"); |
|
if (groupValue) |
|
this.excludeNode.textContent = this._getLabel(facetDef, facetValue, |
|
groupValue, "cantMatchLabel"); |
|
else |
|
this.excludeNode.textContent = this._getLabel(facetDef, facetValue, |
|
groupValue, "mustMatchSomeLabel"); |
|
if (groupValue) |
|
this.undoNode.textContent = this._getLabel(facetDef, facetValue, |
|
groupValue, "mayMatchLabel"); |
|
else |
|
this.undoNode.textContent = this._getLabel(facetDef, facetValue, |
|
groupValue, "mayMatchAnyLabel"); |
|
} catch (e) { |
|
logException(e); |
|
} |
|
]]> |
|
</body> |
|
</method> |
|
<method name="moveFocus"> |
|
<parameter name="event"/> |
|
<parameter name="delta"/> |
|
<body><![CDATA[ |
|
try { |
|
let parent = document.getAnonymousElementByAttribute(this, |
|
"anonid", "parent"); |
|
// We probably want something quite generic in the long term, but that |
|
// is way too much for now (needs to skip over invisible items, etc) |
|
let focused = document.activeElement; |
|
if (focused == this.includeNode) |
|
this.excludeNode.focus(); |
|
else if (focused == this.excludeNode) |
|
this.includeNode.focus(); |
|
event.preventDefault(); |
|
event.stopPropagation(); |
|
} catch (e) { |
|
logException(e); |
|
} |
|
]]> |
|
</body> |
|
</method> |
|
<method name="selectItem"> |
|
<parameter name="event"/> |
|
<body><![CDATA[ |
|
try { |
|
let focused = document.activeElement; |
|
if (focused == this.includeNode) |
|
this.doInclude(); |
|
else if (focused == this.excludeNode) |
|
this.doExclude(); |
|
else |
|
this.doUndo(); |
|
} catch (e) { |
|
logException(e); |
|
} |
|
]]> |
|
</body> |
|
</method> |
|
<method name="show"> |
|
<parameter name="event"/> |
|
<parameter name="facetNode"/> |
|
<parameter name="barNode"/> |
|
<body><![CDATA[ |
|
try { |
|
this.node = barNode; |
|
this.facetNode = facetNode; |
|
let facetDef = facetNode.facetDef; |
|
let groupValue = barNode.groupValue; |
|
let variety = barNode.getAttribute("variety"); |
|
let label = barNode.querySelector(".bar-link").textContent; |
|
this.build(facetDef, label, groupValue); |
|
this.node.setAttribute("selected", "true"); |
|
var rtl = window.getComputedStyle(this, null).direction == "rtl"; |
|
/* We show different menus if we're on an "unselected" facet value, |
|
or if we're on a preselected facet value, whether included or |
|
excluded. The variety attribute handles that through CSS */ |
|
this.setAttribute("variety", variety); |
|
let rect = barNode.getBoundingClientRect(); |
|
let X, Y; |
|
if (event.type == "click") { |
|
// center the menu on the mouse click |
|
if (rtl) |
|
X = event.pageX + 10; |
|
else |
|
X = event.pageX - 10; |
|
Y = Math.max(20, event.pageY - 15); |
|
} else { |
|
if (rtl) |
|
X = rect.left + rect.width / 2 + 20; |
|
else |
|
X = rect.left + rect.width / 2 - 20; |
|
Y = rect.top - 10; |
|
} |
|
if (rtl) |
|
this.style.left = (X - this.getBoundingClientRect().width) + "px"; |
|
else |
|
this.style.left = X + "px"; |
|
this.style.top = Y + "px"; |
|
|
|
if (variety == "remainder") |
|
// include |
|
document.getAnonymousElementByAttribute(this, |
|
"anonid", "parent").firstChild.focus(); |
|
else |
|
// undo |
|
document.getAnonymousElementByAttribute(this, |
|
"anonid", "parent").lastChild.focus(); |
|
} catch (e) { |
|
logException(e); |
|
} |
|
]]> |
|
</body> |
|
</method> |
|
<method name="hide"> |
|
<body><![CDATA[ |
|
try { |
|
this.setAttribute("variety", "invisible"); |
|
if (this.node) { |
|
this.node.removeAttribute("selected"); |
|
this.node.focus(); |
|
} |
|
} catch (e) { |
|
logException(e); |
|
} |
|
]]> |
|
</body> |
|
</method> |
|
<method name="doInclude"> |
|
<body><![CDATA[ |
|
try { |
|
this.facetNode.includeFacet(this.node); |
|
this.hide(); |
|
} catch (e) { |
|
logException(e); |
|
} |
|
]]> |
|
</body> |
|
</method> |
|
<method name="doExclude"> |
|
<body><![CDATA[ |
|
this.facetNode.excludeFacet(this.node); |
|
this.hide(); |
|
]]> |
|
</body> |
|
</method> |
|
<method name="doUndo"> |
|
<body><![CDATA[ |
|
this.facetNode.undoFacet(this.node); |
|
this.hide(); |
|
]]> |
|
</body> |
|
</method> |
|
</implementation> |
|
</binding> |
|
|
|
<binding id="facet-discrete"> |
|
<content> |
|
<!-- without this explicit div here, the sibling selectors used to span |
|
included-label/included and excluded-label/excluded fail to apply. |
|
so. magic! (this is why our binding node's class is facetious. --> |
|
<html:div class="facet"> |
|
<html:h2 anonid="name"></html:h2> |
|
<html:div anonid="content-box" class="facet-content"> |
|
<html:h3 anonid="included-label" class="facet-included-header"></html:h3> |
|
<html:ul anonid="included" class="facet-included barry"></html:ul> |
|
<html:h3 anonid="remainder-label" class="facet-remaindered-header"></html:h3> |
|
<html:ul anonid="remainder" class="facet-remaindered barry"></html:ul> |
|
<html:h3 anonid="excluded-label" class="facet-excluded-header"></html:h3> |
|
<html:ul anonid="excluded" class="facet-excluded barry"></html:ul> |
|
<html:div anonid="more" class="facet-more" needed="false" tabindex="0" |
|
role="button"/> |
|
</html:div> |
|
</html:div> |
|
</content> |
|
<implementation> |
|
<constructor><![CDATA[ |
|
if ("faceter" in this) |
|
this.build(true); |
|
]]></constructor> |
|
<field name="canUpdate" readonly="true">false</field> |
|
<method name="build"> |
|
<parameter name="aFirstTime" /> |
|
<body><![CDATA[ |
|
// -- Header Building |
|
let nameNode = document.getAnonymousElementByAttribute(this, "anonid", |
|
"name"); |
|
nameNode.textContent = this.facetDef.strings.facetNameLabel; |
|
|
|
// - include |
|
// setup the include label |
|
this.includeLabel = document.getAnonymousElementByAttribute( |
|
this, "anonid", "included-label"); |
|
if ("includeLabel" in this.facetDef.strings) |
|
this.includeLabel.textContent = this.facetDef.strings.includeLabel; |
|
else |
|
this.includeLabel.textContent = |
|
glodaFacetStrings.get( |
|
"glodaFacetView.facets.included.fallbackLabel"); |
|
this.includeLabel.setAttribute("state", "empty"); |
|
|
|
// include list ref |
|
this.includeList = document.getAnonymousElementByAttribute( |
|
this, "anonid", "included"); |
|
|
|
// - exclude |
|
// setup the exclude label |
|
this.excludeLabel = document.getAnonymousElementByAttribute( |
|
this, "anonid", "excluded-label"); |
|
if ("excludeLabel" in this.facetDef.strings) |
|
this.excludeLabel.textContent = this.facetDef.strings.excludeLabel; |
|
else |
|
this.excludeLabel.textContent = |
|
glodaFacetStrings.get( |
|
"glodaFacetView.facets.excluded.fallbackLabel"); |
|
this.excludeLabel.setAttribute("state", "empty"); |
|
|
|
// exclude list ref |
|
this.excludeList = document.getAnonymousElementByAttribute( |
|
this, "anonid", "excluded"); |
|
|
|
// - remainder |
|
// setup the remainder label |
|
this.remainderLabel = document.getAnonymousElementByAttribute( |
|
this, "anonid", "remainder-label"); |
|
if ("remainderLabel" in this.facetDef.strings) |
|
this.remainderLabel.textContent = this.facetDef.strings.remainderLabel; |
|
else |
|
this.remainderLabel.textContent = |
|
glodaFacetStrings.get( |
|
"glodaFacetView.facets.remainder.fallbackLabel"); |
|
// remainder list ref |
|
this.remainderList = document.getAnonymousElementByAttribute( |
|
this, "anonid", "remainder"); |
|
|
|
// - more button |
|
this.moreButton = document.getAnonymousElementByAttribute( |
|
this, "anonid", "more"); |
|
|
|
// we need to know who the content box is for flying fun |
|
this.contentBox = document.getAnonymousElementByAttribute( |
|
this, "anonid", "content-box"); |
|
|
|
|
|
// -- House-cleaning |
|
// -- All/Top mode decision |
|
this.modes = ["all"]; |
|
if (this.maxDisplayRows >= this.orderedGroups.length) { |
|
this.mode = "all"; |
|
} |
|
else { |
|
// top mode must be used |
|
this.modes.push("top"); |
|
this.mode = "top"; |
|
this.topGroups = FacetUtils.makeTopGroups(this.attrDef, |
|
this.orderedGroups, |
|
this.maxDisplayRows); |
|
// setup the more button string |
|
let groupCount = this.orderedGroups.length; |
|
this.moreButton.textContent = |
|
PluralForm.get(groupCount, |
|
glodaFacetStrings.get( |
|
"glodaFacetView.facets.mode.top.listAllLabel")) |
|
.replace("#1", groupCount); |
|
} |
|
|
|
// -- Row Building |
|
this.buildRows(); |
|
|
|
]]></body> |
|
</method> |
|
<method name="changeMode"> |
|
<parameter name="aNewMode" /> |
|
<body><![CDATA[ |
|
this.mode = aNewMode; |
|
this.setAttribute("mode", aNewMode); |
|
this.buildRows(); |
|
]]></body> |
|
</method> |
|
<method name="buildRows"> |
|
<body><![CDATA[ |
|
let nounDef = this.nounDef; |
|
let useGroups = (this.mode == "all") ? this.orderedGroups |
|
: this.topGroups; |
|
|
|
// should we just rely on automatic string coercion? |
|
this.moreButton.setAttribute("needed", |
|
(this.mode == "top") ? "true" : "false"); |
|
|
|
let constraint = this.faceter.constraint; |
|
|
|
// -- empty all of our display buckets... |
|
let remainderList = this.remainderList; |
|
while (remainderList.hasChildNodes()) |
|
remainderList.lastChild.remove(); |
|
let includeList = this.includeList, excludeList = this.excludeList; |
|
while (includeList.hasChildNodes()) |
|
includeList.lastChild.remove(); |
|
while (excludeList.hasChildNodes()) |
|
excludeList.lastChild.remove(); |
|
|
|
// -- first pass, check for ambiguous labels |
|
// It's possible that multiple groups are identified by the same short |
|
// string, in which case we want to use the longer string to |
|
// disambiguate. For example, un-merged contacts can result in |
|
// multiple identities having contacts with the same name. In that |
|
// case we want to display both the contact name and the identity |
|
// name. |
|
// This is generically addressed by using the userVisibleString function |
|
// defined on the noun type if it is defined. It takes an argument |
|
// indicating whether it should be a short string or a long string. |
|
// Our algorithm is somewhat dumb. We get the short strings, put them |
|
// in a dictionary that maps to whether they are ambiguous or not. We |
|
// do not attempt to map based on their id, so then when it comes time |
|
// to actually build the labels, we must build the short string and |
|
// then re-call for the long name. We could be smarter by building |
|
// a list of the input values that resulted in the output string and |
|
// then using that to back-update the id map, but it's more compelx and |
|
// the performance difference is unlikely to be meaningful. |
|
let ambiguousKeyValues; |
|
if ("userVisibleString" in nounDef) { |
|
ambiguousKeyValues = {}; |
|
for (let groupPair of useGroups) { |
|
let [groupValue, groupItems] = groupPair; |
|
|
|
// skip null values, they are handled by the none special-case |
|
if (groupValue == null) |
|
continue; |
|
|
|
let groupStr = nounDef.userVisibleString(groupValue, false); |
|
// We use hasOwnProperty because it is possible that groupStr could |
|
// be the same as the name of one of the attributes on |
|
// Object.prototype. |
|
if (ambiguousKeyValues.hasOwnProperty(groupStr)) |
|
ambiguousKeyValues[groupStr] = true; |
|
else |
|
ambiguousKeyValues[groupStr] = false; |
|
} |
|
} |
|
|
|
// -- create the items, assigning them to the right list based on |
|
// existing constraint values |
|
for (let groupPair of useGroups) { |
|
let [groupValue, groupItems] = groupPair; |
|
let li = document.createElement("li"); |
|
li.setAttribute("class", "bar"); |
|
li.setAttribute("tabindex", "0"); |
|
li.setAttribute("role", "link"); |
|
li.setAttribute("aria-haspopup", "true"); |
|
li.groupValue = groupValue; |
|
li.setAttribute("groupValue", groupValue); |
|
li.groupItems = groupItems; |
|
|
|
let countSpan = document.createElement("span"); |
|
countSpan.setAttribute("class", "bar-count"); |
|
countSpan.textContent = groupItems.length.toLocaleString(); |
|
li.appendChild(countSpan); |
|
|
|
let label = document.createElement("span"); |
|
label.setAttribute("class", "bar-link"); |
|
|
|
// Set a tooltip with this label's textContent on the binding when |
|
// the mouse enters the label. This lets us show the full contents of |
|
// the label even if we overflowed. We set it on the binding because |
|
// anonymous XBL nodes can't be the source of HTML tooltips. Shadow |
|
// DOMs are screwy! (We also clear out the tooltip when the mouse |
|
// leaves the label, just to be polite.) |
|
let rootBinding = this; |
|
label.addEventListener("mouseenter", function() { |
|
rootBinding.setAttribute("title", this.textContent); |
|
}); |
|
label.addEventListener("mouseleave", function() { |
|
rootBinding.setAttribute("title", ""); |
|
}); |
|
|
|
// The null value is a special indicator for 'none' |
|
if (groupValue == null) { |
|
label.textContent = |
|
glodaFacetStrings.get("glodaFacetView.facets.noneLabel"); |
|
} |
|
// Otherwise stringify the group object |
|
else { |
|
let labelStr; |
|
if (ambiguousKeyValues) { |
|
labelStr = nounDef.userVisibleString(groupValue, false); |
|
if (ambiguousKeyValues[labelStr]) |
|
labelStr = nounDef.userVisibleString(groupValue, true); |
|
} |
|
else if ("labelFunc" in this.facetDef) { |
|
labelStr = this.facetDef.labelFunc(groupValue); |
|
} |
|
else { |
|
labelStr = groupValue.toLocaleString().substring(0, 80); |
|
} |
|
label.textContent = labelStr; |
|
} |
|
li.appendChild(label); |
|
|
|
// root it under the appropriate list |
|
if (constraint) { |
|
if (constraint.isIncludedGroup(groupValue)) { |
|
li.setAttribute("variety", "include"); |
|
includeList.appendChild(li); |
|
} |
|
else if (constraint.isExcludedGroup(groupValue)) { |
|
li.setAttribute("variety", "exclude"); |
|
excludeList.appendChild(li); |
|
} |
|
else { |
|
li.setAttribute("variety", "remainder"); |
|
remainderList.appendChild(li); |
|
} |
|
} |
|
else { |
|
li.setAttribute("variety", "remainder"); |
|
remainderList.appendChild(li); |
|
} |
|
} |
|
|
|
this.updateHeaderStates(); |
|
]]></body> |
|
</method> |
|
<!-- |
|
- Mark the include/exclude headers as "some" if there is anything in their |
|
- lists, mark the remainder header as "needed" if either of include / |
|
- exclude exist so we need that label. |
|
--> |
|
<method name="updateHeaderStates"> |
|
<parameter name="aItems" /> |
|
<body><![CDATA[ |
|
this.includeLabel.setAttribute("state", |
|
this.includeList.childElementCount ? "some" : "empty"); |
|
this.excludeLabel.setAttribute("state", |
|
this.excludeList.childElementCount ? "some" : "empty"); |
|
this.remainderLabel.setAttribute("needed", |
|
((this.includeList.childElementCount || |
|
this.excludeList.childElementCount) && |
|
this.remainderList.childElementCount) ? "true" : "false"); |
|
|
|
// nuke the style attributes. |
|
this.includeLabel.removeAttribute("style"); |
|
this.excludeLabel.removeAttribute("style"); |
|
this.remainderLabel.removeAttribute("style"); |
|
]]></body> |
|
</method> |
|
<method name="brushItems"> |
|
<parameter name="aItems" /> |
|
<body><![CDATA[ |
|
]]></body> |
|
</method> |
|
<method name="clearBrushedItems"> |
|
<body><![CDATA[ |
|
]]></body> |
|
</method> |
|
<method name="afterListVisible"> |
|
<parameter name="aVariety" /> |
|
<parameter name="aCallback" /> |
|
<body><![CDATA[ |
|
let labelNode = this[aVariety + "Label"]; |
|
let listNode = this[aVariety + "List"]; |
|
|
|
// if there are already things displayed, no need |
|
if (listNode.childElementCount) { |
|
aCallback(); |
|
return; |
|
} |
|
|
|
let remListVisible = |
|
this.remainderLabel.getAttribute("needed") == "true"; |
|
let remListShouldBeVisible = |
|
this.remainderList.childElementCount > 1; |
|
|
|
labelNode.setAttribute("state", "some"); |
|
|
|
// We used to use jQuery here, but it turns out that it has issues with |
|
// XML namespaces and stringification, so we now resort to poking the |
|
// nodes directly. |
|
let showNodes = [labelNode]; |
|
if (remListVisible != remListShouldBeVisible) |
|
showNodes = [labelNode, this.remainderLabel]; |
|
|
|
showNodes.forEach(node => node.style.display = "block"); |
|
|
|
aCallback(); |
|
]]></body> |
|
</method> |
|
<method name="_flyBarAway"> |
|
<parameter name="aBarNode" /> |
|
<parameter name="aVariety" /> |
|
<parameter name="aCallback" /> |
|
<body><![CDATA[ |
|
function getRect(aElement) { |
|
let box = aElement.getBoundingClientRect(); |
|
let documentElement = aElement.ownerDocument.documentElement; |
|
return { |
|
top: box.top + window.pageYOffset - documentElement.clientTop, |
|
left: box.left + window.pageXOffset - documentElement.clientLeft, |
|
width: box.width, |
|
height: box.height |
|
}; |
|
}; |
|
// figure out our origin location prior to adding the target or it |
|
// will shift us down. |
|
let origin = getRect(aBarNode); |
|
|
|
// clone the node into its target location |
|
let targetNode = aBarNode.cloneNode(true); |
|
targetNode.groupValue = aBarNode.groupValue; |
|
targetNode.groupItems = aBarNode.groupItems; |
|
targetNode.setAttribute("variety", aVariety); |
|
|
|
let targetParent = this[aVariety + "List"]; |
|
targetParent.appendChild(targetNode); |
|
|
|
// create a flying clone |
|
let flyingNode = aBarNode.cloneNode(true); |
|
|
|
let dest = getRect(targetNode); |
|
|
|
// if the flying box wants to go higher than the content box goes, just |
|
// send it to the top of the content box instead. |
|
let contentRect = getRect(this.contentBox); |
|
if (dest.top < contentRect.top) |
|
dest.top = contentRect.top; |
|
// likewise if it wants to go further south than the content box, stop |
|
// that |
|
if (dest.top > (contentRect.top + contentRect.height)) |
|
dest.top = contentRect.top + contentRect.height - dest.height; |
|
|
|
flyingNode.style.position = "absolute"; |
|
flyingNode.style.width = origin.width + "px"; |
|
flyingNode.style.height = origin.height + "px"; |
|
flyingNode.style.top = origin.top + "px"; |
|
flyingNode.style.left = origin.left + "px"; |
|
flyingNode.style.zIndex = 1000; |
|
|
|
flyingNode.style.transitionDuration = (Math.abs(dest.top - origin.top) * 2) + "ms"; |
|
flyingNode.style.transitionProperty = "top, left"; |
|
|
|
flyingNode.addEventListener("transitionend", function() { |
|
aBarNode.parentNode.removeChild(aBarNode); |
|
targetNode.style.display = "block"; |
|
flyingNode.parentNode.removeChild(flyingNode); |
|
|
|
if (aCallback) |
|
setTimeout(aCallback, 50); |
|
}); |
|
|
|
document.body.appendChild(flyingNode); |
|
|
|
// animate the flying clone... flying! |
|
window.requestAnimationFrame(function() { |
|
flyingNode.style.top = dest.top + "px"; |
|
flyingNode.style.left = dest.left + "px"; |
|
}); |
|
|
|
// hide the target (cloned) node |
|
targetNode.style.display = "none"; |
|
|
|
// hide the original node and remove its JS properties |
|
aBarNode.style.visibility = "hidden"; |
|
delete aBarNode.groupValue; |
|
delete aBarNode.groupItems; |
|
]]></body> |
|
</method> |
|
<method name="barClicked"> |
|
<parameter name="aBarNode" /> |
|
<parameter name="aVariety" /> |
|
<body><![CDATA[ |
|
let groupValue = aBarNode.groupValue; |
|
let groupItems = aBarNode.groupItems; |
|
let dis = this; |
|
// These determine what goAnimate actually does. |
|
// flyAway allows us to cancel flying in the case the constraint is |
|
// being fully dropped and so the facet is just going to get rebuilt |
|
let flyAway = true; |
|
|
|
function goAnimate() { |
|
setTimeout(function () { |
|
if (flyAway) { |
|
dis.afterListVisible(aVariety, function() { |
|
dis._flyBarAway(aBarNode, aVariety, function() { |
|
dis.updateHeaderStates(); |
|
}); |
|
}); |
|
} |
|
}, 0); |
|
}; |
|
|
|
// Immediately apply the facet change, triggering the animation after |
|
// the faceting completes. |
|
if (aVariety == "remainder") { |
|
let currentVariety = aBarNode.getAttribute("variety"); |
|
let constraintGone = FacetContext.removeFacetConstraint( |
|
this.faceter, |
|
currentVariety == "include", |
|
[groupValue], |
|
goAnimate); |
|
// we will automatically rebuild if the constraint is gone, so |
|
// just make the animation a no-op. |
|
if (constraintGone) |
|
flyAway = false; |
|
} |
|
// include/exclude |
|
else { |
|
let revalidate = FacetContext.addFacetConstraint( |
|
this.faceter, |
|
aVariety == "include", |
|
[groupValue], |
|
false, false, goAnimate); |
|
// revalidate means we need to blow away the other dudes, in which |
|
// case it makes the most sense to just trigger a rebuild of ourself |
|
if (revalidate) { |
|
flyAway = false; |
|
this.build(false); |
|
} |
|
} |
|
]]></body> |
|
</method> |
|
<method name="barHovered"> |
|
<parameter name="aBarNode" /> |
|
<parameter name="aInclude" /> |
|
<body><![CDATA[ |
|
let groupValue = aBarNode.groupValue; |
|
let groupItems = aBarNode.groupItems; |
|
|
|
FacetContext.hoverFacet(this.faceter, this.attrDef, groupValue, groupItems); |
|
]]></body> |
|
</method> |
|
<!-- HoverGone! HoverGone! |
|
We know it's gone, but where has it gone? --> |
|
<method name="barHoverGone"> |
|
<parameter name="aBarNode" /> |
|
<parameter name="aInclude" /> |
|
<body><![CDATA[ |
|
let groupValue = aBarNode.groupValue; |
|
let groupItems = aBarNode.groupItems; |
|
|
|
FacetContext.unhoverFacet(this.faceter, this.attrDef, groupValue, groupItems); |
|
]]></body> |
|
</method> |
|
<method name="includeFacet"> |
|
<parameter name="node"/> |
|
<body> |
|
<![CDATA[ |
|
this.barClicked(node, |
|
(node.getAttribute("variety") == "remainder") ? |
|
"include" : "remainder"); |
|
]]> |
|
</body> |
|
</method> |
|
<method name="undoFacet"> |
|
<parameter name="node"/> |
|
<body> |
|
<![CDATA[ |
|
this.barClicked(node, |
|
(node.getAttribute("variety") == "remainder") ? |
|
"include" : "remainder"); |
|
]]> |
|
</body> |
|
</method> |
|
<method name="excludeFacet"> |
|
<parameter name="node"/> |
|
<body> |
|
<![CDATA[ |
|
this.barClicked(node, "exclude"); |
|
]]> |
|
</body> |
|
</method> |
|
<method name="showPopup"> |
|
<parameter name="event"/> |
|
<body> |
|
<![CDATA[ |
|
try { |
|
// event.originalTarget could be the <li> node, or a span inside |
|
// of it, or perhaps the facet-more button, or maybe something |
|
// else that we'll handle in the next version. We walk up its |
|
// parent chain until we get to the right level of the DOM |
|
// hierarchy, or the facet-content which seems to be the root. |
|
if (this.currentNode) |
|
this.currentNode.removeAttribute("selected"); |
|
|
|
let node = event.originalTarget; |
|
while ((! (node && node.hasAttribute && node.hasAttribute("class"))) || |
|
(!node.classList.contains("bar") && |
|
!node.classList.contains("facet-more") && |
|
!node.classList.contains("facet-content"))) |
|
node = node.parentNode; |
|
|
|
if (! (node && node.hasAttribute && node.hasAttribute("class"))) |
|
return false; |
|
|
|
this.currentNode = node; |
|
node.setAttribute("selected", "true"); |
|
if (node.classList.contains("bar")) |
|
document.getElementById("popup-menu").show(event, this, node); |
|
else if (node.classList.contains("facet-more")) |
|
this.changeMode("all"); |
|
return false; |
|
} catch (e) { |
|
logException(e); |
|
} |
|
]]> |
|
</body> |
|
</method> |
|
<method name="activateLink"> |
|
<parameter name="event"/> |
|
<body> |
|
<![CDATA[ |
|
try { |
|
let node = event.originalTarget; |
|
while ((!node.hasAttribute("class")) || |
|
(!node.classList.contains("facet-more") && |
|
!node.classList.contains("facet-content"))) |
|
node = node.parentNode; |
|
if (node.classList.contains("facet-more")) |
|
this.changeMode("all"); |
|
return false; |
|
} catch (e) { |
|
logException(e); |
|
} |
|
]]> |
|
</body> |
|
</method> |
|
</implementation> |
|
<handlers> |
|
<handler event="click" phase="target" action="this.showPopup(event)"/> |
|
<handler event="keypress" keycode="VK_RETURN" |
|
action="this.showPopup(event);" |
|
phase="target" preventdefault="true"/> |
|
<handler event="keypress" key=" " |
|
action="this.activateLink(event);" |
|
phase="target" preventdefault="true"/> |
|
<handler event="mouseover"><![CDATA[ |
|
// we dispatch based on the class of the thing we clicked on. |
|
// there are other ways we could accomplish this, but they all sorta suck. |
|
if (event.originalTarget.hasAttribute("class") && |
|
event.originalTarget.classList.contains("bar-link")) { |
|
this.barHovered(event.originalTarget.parentNode, true); |
|
} |
|
]]></handler> |
|
<handler event="mouseout"><![CDATA[ |
|
// we dispatch based on the class of the thing we clicked on. |
|
// there are other ways we could accomplish this, but they all sorta suck. |
|
if (event.originalTarget.hasAttribute("class") && |
|
event.originalTarget.classList.contains("bar-link")) { |
|
this.barHoverGone(event.originalTarget.parentNode, true); |
|
} |
|
]]></handler> |
|
</handlers> |
|
</binding> |
|
|
|
<binding id="facet-date"> |
|
<content> |
|
<html:div class="facet date-wrapper"> |
|
<html:h2 anonid="name"></html:h2> |
|
<!-- we need to do this because of something protovis is doing where |
|
it attempts to re-interpret the text and the html namespace no |
|
longer exists in that context. --> |
|
<html:div anonid="canvas" class="date-vis-frame"></html:div> |
|
<html:div class="facet-date-zoom-out" onclick="FacetContext.zoomOut()" |
|
role="image"/> |
|
</html:div> |
|
</content> |
|
<implementation> |
|
<constructor><![CDATA[ |
|
this.canvasNode = document.getAnonymousElementByAttribute( |
|
this, "anonid", "canvas"); |
|
this.vis = null; |
|
if ("faceter" in this) |
|
this.build(true); |
|
]]></constructor> |
|
<field name="canUpdate" readonly="true">true</field> |
|
<method name="build"> |
|
<parameter name="aDoSize" /> |
|
<body><![CDATA[ |
|
if (!this.vis) { |
|
this.vis = new DateFacetVis(this, this.canvasNode); |
|
this.vis.build(); |
|
} |
|
else { |
|
while (this.canvasNode.hasChildNodes()) |
|
this.canvasNode.lastChild.remove(); |
|
if (aDoSize) |
|
this.vis.build() |
|
else |
|
this.vis.rebuild(); |
|
} |
|
]]></body> |
|
</method> |
|
<method name="brushItems"> |
|
<parameter name="aItems" /> |
|
<body><![CDATA[ |
|
this.vis.hoverItems(aItems); |
|
]]></body> |
|
</method> |
|
<method name="clearBrushedItems"> |
|
<body><![CDATA[ |
|
this.vis.clearHover(); |
|
]]></body> |
|
</method> |
|
</implementation> |
|
</binding> |
|
|
|
<!-- ===== Results ===== --> |
|
|
|
<binding id="results-message"> |
|
<content> |
|
<html:div class="results-message-header"> |
|
<html:h2 class="results-message-count" anonid="count"></html:h2> |
|
<html:div class="results-message-showall"> |
|
<html:span class="results-message-showall-button" |
|
tabindex="0" anonid="showall" role="button" |
|
onclick="FacetContext.showActiveSetInTab()"></html:span> |
|
</html:div> |
|
<html:div anonid="sort" class="results-message-sort-bar"> |
|
<html:span anonid="sort-label" class="results-message-sort-label"/> |
|
<html:span anonid="sort-relevance" class="results-message-sort-value" |
|
tabindex="0" role="button"/> |
|
<html:span anonid="sort-date" class="results-message-sort-value" |
|
tabindex="0" role="button"/> |
|
</html:div> |
|
</html:div> |
|
<html:div class="messages" anonid="messages"> |
|
</html:div> |
|
</content> |
|
<implementation> |
|
<method name="setMessages"> |
|
<parameter name="aMessages" /> |
|
<body><![CDATA[ |
|
// -- Count |
|
let countNode = document.getAnonymousElementByAttribute( |
|
this, "anonid", "count"); |
|
let topMessagesPluralFormat = glodaFacetStrings.get( |
|
"glodaFacetView.results.header.countLabel.NMessages"); |
|
let outOfPluralFormat = glodaFacetStrings.get( |
|
"glodaFacetView.results.header.countLabel.ofN"); |
|
let groupingFormat = glodaFacetStrings.get( |
|
"glodaFacetView.results.header.countLabel.grouping"); |
|
|
|
let displayCount = aMessages.length; |
|
let totalCount = FacetContext.activeSet.length; |
|
|
|
// set the count so CSS selectors can know what the results look like |
|
this.setAttribute("state", (totalCount <= 0)? "empty" : "some"); |
|
|
|
let topMessagesStr = PluralForm.get(displayCount, |
|
topMessagesPluralFormat) |
|
.replace("#1", |
|
displayCount.toLocaleString()); |
|
let outOfStr = PluralForm.get(totalCount, |
|
outOfPluralFormat) |
|
.replace("#1", totalCount.toLocaleString()); |
|
countNode.textContent = groupingFormat.replace("#1", topMessagesStr) |
|
.replace("#2", outOfStr); |
|
|
|
// -- Show All |
|
let showNode = document.getAnonymousElementByAttribute( |
|
this, "anonid", "showall"); |
|
const GlodaMessage = Gloda.lookupNounDef("message").clazz; |
|
let visible = aMessages.some(m => m instanceof GlodaMessage); |
|
showNode.style.display = visible ? "inline" : "none"; |
|
showNode.textContent = glodaFacetStrings.get( |
|
"glodaFacetView.results.message.openEmailAsList.label"); |
|
showNode.setAttribute("title", glodaFacetStrings.get( |
|
"glodaFacetView.results.message.openEmailAsList.tooltip")); |
|
showNode.onkeypress = function(event) { |
|
if (event.charCode == KeyEvent.DOM_VK_SPACE) { |
|
FacetContext.showActiveSetInTab() |
|
event.preventDefault(); |
|
} |
|
} |
|
|
|
let sortLabelNode = document.getAnonymousElementByAttribute( |
|
this, "anonid", "sort-label"); |
|
sortLabelNode.textContent = glodaFacetStrings.get( |
|
"glodaFacetView.results.message.sort.label"); |
|
|
|
let sortRelevanceNode = document.getAnonymousElementByAttribute( |
|
this, "anonid", "sort-relevance"); |
|
sortRelevanceNode.textContent = glodaFacetStrings.get( |
|
"glodaFacetView.results.message.sort.relevance"); |
|
|
|
let dis = this; |
|
sortRelevanceNode.onclick = function() { |
|
FacetContext.sortBy = '-dascore'; |
|
dis.updateSortLabels(); |
|
} |
|
sortRelevanceNode.onkeypress = function(event) { |
|
if (event.charCode == KeyEvent.DOM_VK_SPACE) { |
|
FacetContext.sortBy = '-dascore'; |
|
dis.updateSortLabels(); |
|
event.preventDefault(); |
|
} |
|
} |
|
|
|
let sortDateNode = document.getAnonymousElementByAttribute( |
|
this, "anonid", "sort-date"); |
|
sortDateNode.textContent = glodaFacetStrings.get( |
|
"glodaFacetView.results.message.sort.date"); |
|
sortDateNode.onclick = function() { |
|
FacetContext.sortBy = '-date'; |
|
dis.updateSortLabels(); |
|
} |
|
sortDateNode.onkeypress = function(event) { |
|
if (event.charCode == KeyEvent.DOM_VK_SPACE) { |
|
FacetContext.sortBy = '-date'; |
|
dis.updateSortLabels(); |
|
event.preventDefault(); |
|
} |
|
} |
|
|
|
this.updateSortLabels(FacetContext.sortBy); |
|
|
|
let messagesNode = document.getAnonymousElementByAttribute( |
|
this, "anonid", "messages"); |
|
while (messagesNode.hasChildNodes()) |
|
messagesNode.lastChild.remove(); |
|
try { |
|
// -- Messages |
|
for (let message of aMessages) { |
|
let msgNode = document.createElement("message"); |
|
msgNode.message = message; |
|
msgNode.setAttribute("class", "message"); |
|
messagesNode.appendChild(msgNode); |
|
} |
|
} catch (e) { |
|
logException(e); |
|
} |
|
]]></body> |
|
</method> |
|
<method name="ensureNodeVisible"> |
|
<parameter name="messageIndex"/> |
|
<body><![CDATA[ |
|
let messagesNode = document.getAnonymousElementByAttribute( |
|
this, "anonid", "messages"); |
|
let message = messagesNode.childNodes[messageIndex]; |
|
window.scrollTo(0, $(message).position()['top'] + $(window).scrollTop()); |
|
]]></body> |
|
</method> |
|
|
|
<method name="updateSortLabels"> |
|
<body><![CDATA[ |
|
try { |
|
let sortBy = FacetContext.sortBy; |
|
let sortRelevanceNode = document.getAnonymousElementByAttribute( |
|
this, "anonid", "sort-relevance"); |
|
let sortDateNode = document.getAnonymousElementByAttribute( |
|
this, "anonid", "sort-date"); |
|
|
|
if (sortBy == "-dascore") { |
|
sortRelevanceNode.setAttribute("selected", "true"); |
|
sortDateNode.removeAttribute("selected"); |
|
} else if (sortBy == "-date") { |
|
sortRelevanceNode.removeAttribute("selected"); |
|
sortDateNode.setAttribute("selected", "true"); |
|
} |
|
} catch (e ) { |
|
logException(e); |
|
} |
|
]]></body> |
|
</method> |
|
</implementation> |
|
</binding> |
|
|
|
<binding id="result-message"> |
|
<content> |
|
<html:div class="message-header"> |
|
<html:div class="message-line"> |
|
<html:div class="message-meta"> |
|
<html:div anonid="addresses-group" class="message-addresses-group"> |
|
<html:div anonid="author-group" class="message-author-group"> |
|
<html:span anonid="author" class="message-author"></html:span> |
|
<html:div anonid="date" class="message-date"></html:div> |
|
</html:div> |
|
</html:div> |
|
</html:div> |
|
<html:div class="message-subject-group"> |
|
<html:span anonid="star" class="message-star"></html:span> |
|
<html:span anonid="subject" class="message-subject" tabindex="0" |
|
role="link"></html:span> |
|
<html:span anonid="tags" class="message-tags"></html:span> |
|
<html:div anonid="recipients-group" class="message-recipients-group"> |
|
<html:span anonid="to" class="message-to-label"></html:span> |
|
<html:div anonid="recipients" class="message-recipients"/> |
|
</html:div> |
|
</html:div> |
|
</html:div> |
|
</html:div> |
|
<html:pre anonid="snippet" class="message-body"></html:pre> |
|
<html:div anonid="attachments" class="message-attachments"></html:div> |
|
</content> |
|
<implementation> |
|
<constructor><![CDATA[ |
|
Components.utils.import("resource:///modules/mailServices.js"); |
|
this.build(); |
|
]]></constructor> |
|
<method name="build"> |
|
<body><![CDATA[ |
|
let message = this.message; |
|
let dis = this; |
|
function anonElem(aAnonId) { |
|
return document.getAnonymousElementByAttribute(dis, "anonid", |
|
aAnonId); |
|
} |
|
let subject = anonElem("subject"); |
|
// -- eventify |
|
subject.onclick = function(aEvent) { |
|
FacetContext.showConversationInTab(this, |
|
aEvent.button == 1); |
|
}.bind(this); |
|
subject.onkeypress = function(aEvent) { |
|
if (aEvent.keyCode == aEvent.DOM_VK_RETURN) |
|
FacetContext.showConversationInTab(this, |
|
aEvent.shiftKey == true); |
|
}.bind(this); |
|
|
|
// -- Content Poking |
|
if (message.subject.trim() == "") |
|
subject.textContent = glodaFacetStrings.get("glodaFacetView.result.message.noSubject"); |
|
else |
|
subject.textContent = message.subject; |
|
let authorNode = anonElem("author"); |
|
authorNode.setAttribute("title", message.from.value); |
|
authorNode.textContent = message.from.contact.name |
|
let toNode = anonElem("to"); |
|
toNode.textContent = glodaFacetStrings.get("glodaFacetView.result.message.toLabel"); |
|
|
|
//anonElem("author").textContent = ; |
|
anonElem("date").textContent = makeFriendlyDateAgo(message.date); |
|
|
|
// - Recipients |
|
try { |
|
let recipientsNode = anonElem("recipients"); |
|
if (message.recipients) { |
|
let recipientCount = 0; |
|
const MAX_RECIPIENTS = 3; |
|
let totalRecipientCount = message.recipients.length; |
|
let recipientSeparator = glodaFacetStrings.get( |
|
"glodaFacetView.results.message.recipientSeparator") |
|
for (let index in message.recipients) { |
|
let recipNode = document.createElement("span"); |
|
recipNode.setAttribute("class", "message-recipient"); |
|
recipNode.textContent = message.recipients[index].contact.name; |
|
recipientsNode.appendChild(recipNode); |
|
recipientCount++; |
|
if (recipientCount == MAX_RECIPIENTS) |
|
break; |
|
if (index != totalRecipientCount - 1) { |
|
// add separators (usually commas) |
|
let sepNode = document.createElement("span"); |
|
sepNode.setAttribute("class", "message-recipient-separator"); |
|
sepNode.textContent = recipientSeparator; |
|
recipientsNode.appendChild(sepNode); |
|
} |
|
} |
|
if (totalRecipientCount > MAX_RECIPIENTS) { |
|
let nOthers = totalRecipientCount - recipientCount; |
|
let andNOthers = document.createElement("span"); |
|
andNOthers.setAttribute("class", "message-recipients-andothers"); |
|
|
|
let andOthersLabel= PluralForm.get(nOthers, glodaFacetStrings.get( |
|
"glodaFacetView.results.message.andOthers")) |
|
.replace("#1", nOthers); |
|
|
|
andNOthers.textContent = andOthersLabel; |
|
recipientsNode.appendChild(andNOthers); |
|
} |
|
} |
|
} catch (e) { |
|
logException(e); |
|
} |
|
|
|
// - Starred |
|
let starNode = anonElem("star"); |
|
if (message.starred) { |
|
starNode.setAttribute("starred", "true") |
|
} |
|
|
|
// - Attachments |
|
if (message.attachmentNames) { |
|
let attachmentsNode = anonElem("attachments"); |
|
let imgNode = document.createElement("div"); |
|
imgNode.setAttribute("class", "message-attachment-icon"); |
|
attachmentsNode.appendChild(imgNode); |
|
for (let attach of message.attachmentNames) { |
|
let attachNode = document.createElement("div"); |
|
attachNode.setAttribute("class", "message-attachment"); |
|
if (attach.length >= 28) |
|
attach = attach.substring(0, 24) + "…"; |
|
attachNode.textContent = attach; |
|
attachmentsNode.appendChild(attachNode); |
|
} |
|
} |
|
|
|
// - Tags |
|
let tagsNode = anonElem("tags"); |
|
if ("tags" in message && message.tags.length) { |
|
for (let tag of message.tags) { |
|
let tagNode = document.createElement("span"); |
|
let colorClass = "blc-" + MailServices.tags.getColorForKey(tag.key).substr(1); |
|
tagNode.setAttribute("class", "message-tag tag " + colorClass); |
|
tagNode.textContent = tag.tag; |
|
tagsNode.appendChild(tagNode); |
|
} |
|
} |
|
|
|
// - Body |
|
if (message.indexedBodyText) { |
|
let bodyText = message.indexedBodyText; |
|
|
|
let matches = []; |
|
if ("stashedColumns" in FacetContext.collection) { |
|
let collection; |
|
if ("IMCollection" in FacetContext && |
|
message instanceof Gloda.lookupNounDef("im-conversation").clazz) |
|
collection = FacetContext.IMCollection; |
|
else |
|
collection = FacetContext.collection; |
|
let offsets = collection.stashedColumns[message.id][0]; |
|
let offsetNums = offsets.split(" ").map(x => parseInt(x)); |
|
for (let i = 0; i < offsetNums.length; i += 4) { |
|
// i is the column index. The indexedBodyText is in the column 0. |
|
// Ignore matches for other columns. |
|
if (offsetNums[i] != 0) |
|
continue; |
|
|
|
// i+1 is the term index, indicating which queried term was found. |
|
// We can ignore for now... |
|
|
|
// i+2 is the *byte* offset at which the term is in the string. |
|
// i+3 is the term's length. |
|
matches.push([offsetNums[i + 2], offsetNums[i + 3]]); |
|
} |
|
|
|
// Sort the matches by index, just to be sure. |
|
// They are probably already sorted, but if they aren't it could |
|
// mess things up at the next step. |
|
matches.sort((a, b) => a[0] - b[0]); |
|
|
|
// Convert the byte offsets and lengths into character indexes. |
|
let charCodeToByteCount = function(c) { |
|
// UTF-8 stores: |
|
// - code points below U+0080 on 1 byte, |
|
// - code points below U+0800 on 2 bytes, |
|
// - code points U+D800 through U+DFFF are UTF-16 surrogate halves |
|
// (they indicate that JS has split a 4 bytes UTF-8 character |
|
// in two halves of 2 bytes each), |
|
// - other code points on 3 bytes. |
|
return c < 0x80 ? 1 : (c < 0x800 || (c >= 0xD800 && c <= 0xDFFF)) ? 2 : 3; |
|
} |
|
let byteOffset = 0; |
|
let offset = 0; |
|
for (let match of matches) { |
|
while (byteOffset < match[0]) |
|
byteOffset += charCodeToByteCount(bodyText.charCodeAt(offset++)); |
|
match[0] = offset; |
|
for (let i = offset; i < offset + match[1]; ++i) { |
|
let size = charCodeToByteCount(bodyText.charCodeAt(i)); |
|
if (size > 1) |
|
match[1] -= size - 1; |
|
} |
|
} |
|
} |
|
|
|
// how many lines of context we want before the first match: |
|
const kContextLines = 2; |
|
|
|
let startIndex = 0; |
|
if (matches.length > 0) { |
|
// Find where the snippet should begin to show at least the |
|
// first match and kContextLines of context before the match. |
|
startIndex = matches[0][0]; |
|
for (let context = kContextLines; context >= 0; --context) { |
|
startIndex = bodyText.lastIndexOf("\n", startIndex - 1); |
|
if (startIndex == -1) { |
|
startIndex = 0; |
|
break; |
|
} |
|
} |
|
} |
|
|
|
// start assuming it's just one line that we want to show |
|
let idxNewline = -1; |
|
let ellipses = "…"; |
|
|
|
let maxLineCount = 5; |
|
if (startIndex != 0) { |
|
// Avoid displaying an ellipses followed by an empty line. |
|
while (bodyText[startIndex + 1] == "\n") |
|
++startIndex; |
|
bodyText = ellipses + bodyText.substring(startIndex); |
|
// The first line will only contain the ellipsis as the character |
|
// at startIndex is always \n, so we show an additional line. |
|
++maxLineCount; |
|
} |
|
|
|
for (let newlineCount = 0; newlineCount < maxLineCount; newlineCount++) { |
|
idxNewline = bodyText.indexOf("\n", idxNewline+1); |
|
if (idxNewline == -1) { |
|
ellipses = ''; |
|
break; |
|
} |
|
} |
|
let snippet = ""; |
|
if (idxNewline > -1) |
|
snippet = bodyText.substring(0, idxNewline); |
|
else |
|
snippet = bodyText; |
|
if (ellipses) |
|
snippet = snippet.trimRight() + ellipses; |
|
|
|
let parent = anonElem("snippet"); |
|
let node = document.createTextNode(snippet); |
|
parent.appendChild(node); |
|
|
|
let offset = startIndex ? startIndex - 1 : 0; // The ellipsis takes 1 character. |
|
for (let match of matches) { |
|
if (idxNewline > -1 && match[0] > startIndex + idxNewline) |
|
break; |
|
let secondNode = node.splitText(match[0] - offset); |
|
node = secondNode.splitText(match[1]); |
|
offset += match[0] + match[1] - offset; |
|
let span = document.createElement("span"); |
|
span.textContent = secondNode.data; |
|
if (!this.firstMatchText) |
|
this.firstMatchText = secondNode.data; |
|
span.setAttribute("class", "message-body-fulltext-match"); |
|
parent.replaceChild(span, secondNode); |
|
} |
|
} |
|
|
|
// - Misc attributes |
|
if (!message.read) |
|
this.setAttribute("unread", "true"); |
|
]]></body> |
|
</method> |
|
</implementation> |
|
<handlers> |
|
<handler event="mouseover"><![CDATA[ |
|
FacetContext.hoverFacet(FacetContext.fakeResultFaceter, |
|
FacetContext.fakeResultAttr, |
|
this.message, [this.message]); |
|
]]></handler> |
|
<handler event="mouseout"><![CDATA[ |
|
FacetContext.unhoverFacet(FacetContext.fakeResultFaceter, |
|
FacetContext.fakeResultAttr, |
|
this.message, [this.message]); |
|
]]></handler> |
|
</handlers> |
|
</binding> |
|
|
|
</bindings>
|
|
|