/* 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/. */ "use strict"; /* global MozXULElement, MozElements */ // Wrap in a block to prevent leaking to window scope. { /** * The MozChatGroupRichlistitem widget displays chat group name and behave as a * expansion twisty for groups such as "Conversations", * "Online Contacts" and "Offline Contacts". * * @augments {MozElements.MozRichlistitem} */ class MozChatGroupRichlistitem extends MozElements.MozRichlistitem { static get inheritedAttributes() { return { label: "value=name", }; } connectedCallback() { if (this.delayConnectedCallback() || this.hasChildNodes()) { return; } this.setAttribute("is", "chat-group-richlistitem"); this.setAttribute("collapsed", "true"); /* Here we use a div, rather than the usual img because the icon image * relies on CSS -moz-locale-dir(rtl). The corresponding icon * twisty-collapsed-rtl icon is not a simple mirror transformation of * twisty-collapsed. * Currently, CSS sets the background-image based on the "closed" state. * The element is a visual decoration and does not require any alt text * since the aria-expanded attribute describes its state. */ this._image = document.createElement("div"); this._image.classList.add("twisty"); this._label = document.createXULElement("label"); this._label.setAttribute("flex", "1"); this._label.setAttribute("crop", "end"); this.appendChild(this._image); this.appendChild(this._label); this.contacts = []; this.contactsById = {}; this.displayName = ""; this.addEventListener("click", event => { // Check if there was 1 click on the image or 2 clicks on the label if ( (event.detail == 1 && event.target.classList.contains("twisty")) || (event.detail == 2 && event.target.localName == "label") ) { this.toggleClosed(); } else if (event.target.localName == "button") { this.hide(); } }); this.addEventListener("contextmenu", event => { event.preventDefault(); }); if (this.classList.contains("closed")) { this.setAttribute("aria-expanded", "true"); } else { this.setAttribute("aria-expanded", "false"); } this.initializeAttributeInheritance(); } /** * Takes as input two contact elements (imIContact type) and compares * their nicknames alphabetically (case insensitive). This method * behaves as a callback that Array.prototype.sort accepts as a * parameter. */ sortComparator(contactA, contactB) { if (contactA.statusType != contactB.statusType) { return contactB.statusType - contactA.statusType; } let a = contactA.displayName.toLowerCase(); let b = contactB.displayName.toLowerCase(); return a.localeCompare(b); } addContact(contact, tagName) { if (this.contactsById.hasOwnProperty(contact.id)) { return null; } let contactElt; if (tagName) { contactElt = document.createXULElement("richlistitem", { is: "chat-imconv-richlistitem", }); } else { contactElt = document.createXULElement("richlistitem", { is: "chat-contact-richlistitem", }); } if (this.classList.contains("closed")) { contactElt.setAttribute("collapsed", "true"); } let end = this.contacts.length; // Avoid the binary search loop if the contacts were already sorted. if ( end != 0 && this.sortComparator(contact, this.contacts[end - 1].contact) < 0 ) { let start = 0; while (start < end) { let middle = start + Math.floor((end - start) / 2); if (this.sortComparator(contact, this.contacts[middle].contact) < 0) { end = middle; } else { start = middle + 1; } } } let last = end == 0 ? this : this.contacts[end - 1]; this.parentNode.insertBefore(contactElt, last.nextElementSibling); contactElt.build(contact); contactElt.group = this; this.contacts.splice(end, 0, contactElt); this.contactsById[contact.id] = contactElt; this.removeAttribute("collapsed"); this._updateGroupLabel(); return contactElt; } updateContactPosition(subject, tagName) { let contactElt = this.contactsById[subject.id]; let index = this.contacts.indexOf(contactElt); if (index == -1) { // Sometimes we get a display-name-changed notification for // an offline contact, if it's not in the list, just ignore it. return; } // See if the position of the contact should be changed. if ( (index != 0 && this.sortComparator( contactElt.contact, this.contacts[index - 1].contact ) < 0) || (index != this.contacts.length - 1 && this.sortComparator( contactElt.contact, this.contacts[index + 1].contact ) > 0) ) { let list = this.parentNode; let selectedItem = list.selectedItem; let oldItem = this.removeContact(subject); let newItem = this.addContact(subject, tagName); if (selectedItem == oldItem) { list.selectedItem = newItem; } } } removeContact(contactForID) { let contact = this.contactsById[contactForID.id]; if (!contact) { throw new Error("Can't remove contact for id=" + contactForID.id); } // create a new array to remove without breaking for each loops. this.contacts = this.contacts.filter(c => c !== contact); delete this.contactsById[contact.contact.id]; contact.destroy(); // Check if some contacts remain in the group, if empty hide it. if (!this.contacts.length) { this.setAttribute("collapsed", "true"); } else { this._updateGroupLabel(); } return contact; } _updateClosedState(closed) { for (let contact of this.contacts) { contact.collapsed = closed; } } toggleClosed() { if (this.classList.contains("closed")) { this.classList.remove("closed"); this.setAttribute("aria-expanded", "true"); this._updateClosedState(false); } else { this.classList.add("closed"); this.setAttribute("aria-expanded", "false"); this._updateClosedState(true); } this._updateGroupLabel(); } _updateGroupLabel() { if (!this.displayName) { this.displayName = this.getAttribute("name"); } let name = this.displayName; if (this.classList.contains("closed")) { name += " (" + this.contacts.length + ")"; } this.setAttribute("name", name); } keyPress(event) { switch (event.keyCode) { case event.DOM_VK_RETURN: this.toggleClosed(); break; case event.DOM_VK_LEFT: if (!this.classList.contains("closed")) { this.toggleClosed(); } break; case event.DOM_VK_RIGHT: if (this.classList.contains("closed")) { this.toggleClosed(); } break; } } } MozXULElement.implementCustomInterface(MozChatGroupRichlistitem, [ Ci.nsIDOMXULSelectControlItemElement, ]); customElements.define("chat-group-richlistitem", MozChatGroupRichlistitem, { extends: "richlistitem", }); }