true false = 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(); ]]> 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(); ]]> (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; ]]> 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); } ]]> true 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); } ]]> 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"); ]]>