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.

1769 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) {