Bug 1522473 - Migrate attendees-list binding to custom-element. r=philipp
authorArshad Khan <arshdkhn1@gmail.com>
Wed, 17 Apr 2019 18:42:41 +0200
changeset 26372 1552305b49b0db4fd9e88586adf85202165a3963
parent 26371 4a2e39cfc82034032a116e2d0ab34ce821b91298
child 26373 42139c6ab12273f0a1ff95b03e74d23e50d7c484
push id15807
push usermozilla@jorgk.com
push dateWed, 17 Apr 2019 16:43:14 +0000
treeherdercomm-central@1552305b49b0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersphilipp
bugs1522473
Bug 1522473 - Migrate attendees-list binding to custom-element. r=philipp
calendar/base/content/dialogs/calendar-event-dialog-attendees-custom-elements.js
calendar/base/content/dialogs/calendar-event-dialog-attendees.js
calendar/base/content/dialogs/calendar-event-dialog-attendees.xml
calendar/base/content/dialogs/calendar-event-dialog-attendees.xul
calendar/base/content/dialogs/calendar-event-dialog.css
calendar/base/themes/common/dialogs/calendar-event-dialog.css
--- a/calendar/base/content/dialogs/calendar-event-dialog-attendees-custom-elements.js
+++ b/calendar/base/content/dialogs/calendar-event-dialog-attendees-custom-elements.js
@@ -1,13 +1,15 @@
 /* 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/. */
 
-/* global MozElements, Services */
+/* global MozElements, Services, cal, setElementValue */
+
+var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm");
 
 /**
  * MozCalendarEventFreebusyTimebar is a widget showing the time slot labels - dates and a number of
  * times instances of each date. It is typically used in combination with a grid showing free and
  * busy times for attendees going to an event, as used in the Invite Attendees dialog.
  *
  * @extends {MozElements.RichListBox}
  */
@@ -258,8 +260,1091 @@ class MozCalendarEventFreebusyTimebar ex
         } else {
             this.mStartHour = Services.prefs.getIntPref("calendar.view.daystarthour", 8);
             this.mEndHour = Services.prefs.getIntPref("calendar.view.dayendhour", 19);
         }
     }
 }
 
 customElements.define("calendar-event-freebusy-timebar", MozCalendarEventFreebusyTimebar);
+
+/**
+ * MozCalendarEventAttendeesList is a widget allowing adding and removing of attendees of an event.
+ * It shows if attendee if required or optional, the attendee status, type and adddress.
+ * It is typically found in the Invite Attendees dialog.
+ *
+ * @extends {MozElements.RichListBox}
+ */
+class MozCalendarEventAttendeesList extends MozElements.RichListBox {
+    constructor() {
+        super();
+
+        this.mMaxAttendees = 0;
+        this.mContentHeight = 0;
+        this.mRowHeight = 0;
+        this.mNumColumns = 3;
+        this.mIsOffline = 0;
+        this.mIsReadOnly = false;
+        this.mIsInvitation = false;
+        this.mPopupOpen = false;
+        this.mMaxAttendees = 0;
+
+        this.addEventListener("click", this.onClick.bind(this));
+
+        this.addEventListener("popupshown", (event) => {
+            this.mPopupOpen = true;
+        });
+
+        this.addEventListener("popuphidden", (event) => {
+            this.mPopupOpen = false;
+        });
+
+        this.addEventListener("keydown", (event) => {
+            if (this.mIsReadOnly || this.mIsInvitation) {
+                return;
+            }
+            if (event.originalTarget.localName == "input") {
+                switch (event.key) {
+                    case "Delete":
+                    case "Backspace":
+                        {
+                            let curRowId = this.getRowByInputElement(event.originalTarget);
+                            let allSelected = (event.originalTarget.textLength ==
+                                event.originalTarget.selectionEnd -
+                                event.originalTarget.selectionStart);
+
+                            if (!event.originalTarget.value ||
+                                event.originalTarget.textLength < 2 ||
+                                allSelected) {
+                                // if the user selected the entire attendee string, only one character was
+                                // left or the row was already empty before hitting the key, we remove the
+                                //  entire row to assure the attendee is deleted
+                                this.deleteHit(event.originalTarget);
+
+                                // if the last row was removed, we append an empty one which has the focus
+                                // to enable adding a new attendee directly with freebusy information cleared
+                                let targetRowId = (event.key == "Backspace" && curRowId > 2) ?
+                                    curRowId - 1 : curRowId;
+                                if (this.mMaxAttendees == 1) {
+                                    this.appendNewRow(true);
+                                } else {
+                                    this.setFocus(targetRowId);
+                                }
+
+                                // set cursor to begin or end of focused input box based on deletion direction
+                                let cPos = 0;
+                                let input = this.getListItem(targetRowId).querySelector(".textbox-addressingWidget");
+                                if (targetRowId != curRowId) {
+                                    cPos = input.textLength;
+                                }
+                                input.setSelectionRange(cPos, cPos);
+                            }
+
+                            event.stopPropagation();
+                            break;
+                        }
+                }
+            }
+        });
+
+        this.addEventListener("keypress", (event) => {
+            // In case we're currently showing the autocompletion popup
+            // don't care about keypress-events and let them go. Otherwise
+            // this event indicates the user wants to travel between
+            // the different attendees. In this case we set the focus
+            // appropriately and stop the event propagation.
+            if (this.mPopupOpen || this.mIsReadOnly || this.mIsInvitation) {
+                return;
+            }
+            if (event.originalTarget.localName == "input") {
+                switch (event.key) {
+                    case "ArrowUp":
+                        this.arrowHit(event.originalTarget, -1);
+                        event.stopPropagation();
+                        break;
+                    case "ArrowDown":
+                        this.arrowHit(event.originalTarget, 1);
+                        event.stopPropagation();
+                        break;
+                    case "Tab":
+                        this.arrowHit(event.originalTarget, event.shiftKey ? -1 : +1);
+                        break;
+                }
+            }
+        }, true);
+    }
+
+    /**
+     * Property telling whether the calendar-event-attendees-list is read-only or not.
+     *
+     * @returns {Boolean}       isReadOnly value
+     */
+    get isReadOnly() {
+        return this.mIsReadOnly;
+    }
+
+    /**
+     * Controls the read-only state of the attendee list by not allowing to add or remove or edit
+     * attendees.
+     *
+     * @param {Boolean} val     New isReadOnly value
+     * @returns {Boolean}       New isReadOnly value
+     */
+    set isReadOnly(val) {
+        this.mIsReadOnly = val;
+        return val;
+    }
+
+    /**
+     * Flag that tells whether the event is an invitation or not.
+     *
+     * @param {Boolean} val     New isInvitation value
+     * @returns {Boolean}       New isInvitation value
+     */
+    set isInvitation(val) {
+        this.mIsInvitation = val;
+        return val;
+    }
+
+    /**
+     * Returns flags that tells whether the event is an invitation or not.
+     *
+     * @returns {Boolean}       isInvitation value
+     */
+    get isInvitation() {
+        return this.mIsInvitation;
+    }
+
+    /**
+     * The attendees shown in this attendee list.
+     *
+     * @returns {calIAttendee[]}        The attendees of the list
+     */
+    get attendees() {
+        let attendees = [];
+
+        for (let i = 1; true; i++) {
+            let inputField = this.getInputElement(i);
+            if (!inputField) {
+                break;
+            } else if (inputField.value == "") {
+                continue;
+            }
+
+            // The inputfield already has a reference to the attendee object, we just need to fill
+            // in the name.
+            let attendee = inputField.attendee.clone();
+            if (attendee.isOrganizer) {
+                continue;
+            }
+
+            attendee.role = this.getRoleElement(i).getAttribute("role");
+            // attendee.participationStatus = this.getStatusElement(i).getAttribute("status");
+            let userType = this.getUserTypeElement(i).getAttribute("cutype");
+            attendee.userType = (userType == "INDIVIDUAL" ? null : userType); // INDIVIDUAL is the default
+
+            // Break the list of potentially many attendees back into individual names. This
+            // is required in case the user entered comma-separated attendees in one field and
+            // then clicked OK without switching to the next line.
+            let parsedInput = MailServices.headerParser.makeFromDisplayAddress(inputField.value);
+            let j = 0;
+            let addAttendee = (aAddress) => {
+                if (j > 0) {
+                    attendee = attendee.clone();
+                }
+                attendee.id = cal.email.prependMailTo(aAddress.email);
+                let commonName = null;
+                if (aAddress.name.length > 0) {
+                    // We remove any double quotes within CN due to bug 1209399.
+                    let name = aAddress.name.replace(/(?:(?:[\\]")|(?:"))/g, "");
+                    if (aAddress.email != name) {
+                        commonName = name;
+                    }
+                }
+                attendee.commonName = commonName;
+                attendees.push(attendee);
+                j++;
+            };
+            parsedInput.forEach(addAttendee);
+        }
+        return attendees;
+    }
+
+    /**
+     * Returns an attendee node if there is an organizer else returns null.
+     *
+     * @returns {?calIAttendee}     Organizer of the event or null
+     */
+    get organizer() {
+        for (let i = 1; true; i++) {
+            let inputField = this.getInputElement(i);
+            if (!inputField) {
+                break;
+            } else if (inputField.value == "") {
+                continue;
+            }
+
+            // The inputfield already has a reference to the attendee
+            // object, we just need to fill in the name.
+            let attendee = inputField.attendee.clone();
+
+            // attendee.role = this.getRoleElement(i).getAttribute("role");
+            attendee.participationStatus = this.getStatusElement(i).getAttribute("status");
+            // Organizers do not have a CUTYPE
+            attendee.userType = null;
+
+            // Break the list of potentially many attendees back into individual names.
+            let parsedInput = MailServices.headerParser.makeFromDisplayAddress(inputField.value);
+            if (parsedInput[0].email > 0) {
+                attendee.id = cal.email.prependMailTo(parsedInput[0].email);
+            }
+            let commonName = null;
+            if (parsedInput[0].name.length > 0) {
+                let name = parsedInput[0].name.replace(/(?:(?:[\\]")|(?:"))/g, "");
+                if (attendee.email != name) {
+                    commonName = name;
+                }
+            }
+            attendee.commonName = commonName;
+
+            if (attendee.isOrganizer) {
+                return attendee;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Gets document size of calendar-event-attendees-list.
+     *
+     * @returns {Number}        Document size
+     */
+    get documentSize() {
+        return this.mRowHeight * this.mMaxAttendees;
+    }
+
+    /**
+     * Returns the index of first row element that is visible in the view box. Scrolling will
+     * change the first visible row.
+     *
+     * @returns {Number}        First visible row
+     */
+    get firstVisibleRow() {
+        return this.getIndexOfFirstVisibleRow();
+    }
+
+    /**
+     * Scrolls to the row with the index calculated in the method.
+     *
+     * @param {Number} val      Decimal number between 0 and 1
+     * @returns {Number}        Decimal number between 0 and 1
+     */
+    set ratio(val) {
+        let rowcount = this.getRowCount();
+        this.scrollToIndex(Math.floor(rowcount * val));
+        return val;
+    }
+
+    /**
+     * Depending upon the original target of click event, toolip is updated or new row is appended
+     * or nothing happens.
+     *
+     * @param {Object} event        Event object containing click event information
+     */
+    onClick(event) {
+        if (event.button != 0) {
+            return;
+        }
+
+        const cycle = (values, current) => {
+            let nextIndex = (values.indexOf(current) + 1) % values.length;
+            return values[nextIndex];
+        };
+
+        let target = event.originalTarget;
+        if (target.classList.contains("role-icon")) {
+            if (target.getAttribute("disabled") != "true") {
+                const roleCycle = [
+                    "REQ-PARTICIPANT", "OPT-PARTICIPANT",
+                    "NON-PARTICIPANT", "CHAIR"
+                ];
+
+                let nextValue = cycle(roleCycle, target.getAttribute("role"));
+                target.setAttribute("role", nextValue);
+                this.updateTooltip(target);
+            }
+        } else if (target.classList.contains("status-icon")) {
+            if (target.getAttribute("disabled") != "true") {
+                const statusCycle = ["ACCEPTED", "DECLINED", "TENTATIVE"];
+
+                let nextValue = cycle(statusCycle, target.getAttribute("status"));
+                target.setAttribute("status", nextValue);
+                this.updateTooltip(target);
+            }
+        } else if (target.classList.contains("usertype-icon")) {
+            let row = target.closest("richlistitem");
+            let inputField = row.querySelector(".textbox-addressingWidget");
+            if (target.getAttribute("disabled") != "true" &&
+                !inputField.attendee.isOrganizer) {
+                const cutypeCycle = ["INDIVIDUAL", "GROUP", "RESOURCE", "ROOM"];
+
+                let nextValue = cycle(cutypeCycle, target.getAttribute("cutype"));
+                target.setAttribute("cutype", nextValue);
+                this.updateTooltip(target);
+            }
+        } else if (this.mIsReadOnly || this.mIsInvitation || target == null || target.closest("richlistitem")) {
+            // These are cases where we don't want to append a new row, keep
+            // them here so we can put the rest in the else case.
+        } else {
+            let lastInput = this.getInputElement(this.mMaxAttendees);
+            if (lastInput && lastInput.value) {
+                this.appendNewRow(true);
+            }
+        }
+    }
+
+    /**
+     * This trigger the continous update chain, which effectively calls this.onModify() on
+     * predefined time intervals [each second].
+     */
+    init() {
+        let callback = () => {
+            setTimeout(callback, 1000);
+            this.onModify();
+        };
+        callback();
+    }
+
+    /**
+     * Appends a new row using an existing attendee structure.
+     *
+     * @param {calIAttendee} attendee           Attendee object
+     * @param {Element} templateNode            Template node that need to be cloned
+     * @param {Boolean} disableIfOrganizer      Flag that is truthy if attendee is organizer
+     * @return {Boolean}                        Truthy flag showing that attendee is appended
+     *                                          successfully
+     */
+    appendAttendee(attendee, templateNode, disableIfOrganizer) {
+        // create a new listbox item and append it to our parent control.
+        let newNode = templateNode.cloneNode(true);
+
+        this.appendChild(newNode);
+
+        let input = newNode.querySelector(".textbox-addressingWidget");
+        let roleStatusIcon = newNode.querySelector(".status-icon");
+        let userTypeIcon = newNode.querySelector(".usertype-icon");
+
+        // We always clone the first row. The problem is that the first row
+        // could be focused. When we clone that row, we end up with a cloned
+        // XUL textbox that has a focused attribute set.  Therefore we think
+        // we're focused and don't properly refocus.  The best solution to this
+        // would be to clone a template row that didn't really have any presentation,
+        // rather than using the real visible first row of the listbox.
+        // For now we'll just put in a hack that ensures the focused attribute
+        // is never copied when the node is cloned.
+        if (input.getAttribute("focused") != "") {
+            input.removeAttribute("focused");
+        }
+
+        // The template could have its fields disabled,
+        // that's why we need to reset their status.
+        input.removeAttribute("disabled");
+        userTypeIcon.removeAttribute("disabled");
+        roleStatusIcon.removeAttribute("disabled");
+
+        if (this.mIsReadOnly || this.mIsInvitation) {
+            input.setAttribute("disabled", "true");
+            userTypeIcon.setAttribute("disabled", "true");
+            roleStatusIcon.setAttribute("disabled", "true");
+        }
+
+        // Disable the input-field [name <email>] if this attendee
+        // appears to be the organizer.
+        if (disableIfOrganizer && attendee && attendee.isOrganizer) {
+            input.setAttribute("disabled", "true");
+        }
+
+        this.mMaxAttendees++;
+
+        if (!attendee) {
+            attendee = this.createAttendee();
+        }
+
+        // Construct the display string from common name and/or email address.
+        let commonName = attendee.commonName || "";
+        let inputValue = cal.email.removeMailTo(attendee.id || "");
+        if (commonName.length) {
+            // Make the commonName appear in quotes if it contains a
+            // character that could confuse the header parser
+            if (commonName.search(/[,;<>@]/) != -1) {
+                commonName = '"' + commonName + '"';
+            }
+            inputValue = inputValue.length ? commonName + " <" + inputValue + ">" : commonName;
+        }
+
+        // Trim spaces if any.
+        inputValue = inputValue.trim();
+
+        // Don't set value with null, otherwise autocomplete stops working,
+        // but make sure attendee and dirty are set.
+        if (inputValue.length) {
+            input.setAttribute("value", inputValue);
+            input.value = inputValue;
+        }
+        input.attendee = attendee;
+        input.setAttribute("dirty", "true");
+
+        if (attendee) {
+            // Set up userType.
+            setElementValue(userTypeIcon, attendee.userType || false, "cutype");
+            this.updateTooltip(userTypeIcon);
+
+            // Set up role/status icon.
+            if (attendee.isOrganizer) {
+                roleStatusIcon.setAttribute("class", "status-icon");
+                setElementValue(roleStatusIcon, attendee.participationStatus || false, "status");
+            } else {
+                roleStatusIcon.setAttribute("class", "role-icon");
+                setElementValue(roleStatusIcon, attendee.role || false, "role");
+            }
+            this.updateTooltip(roleStatusIcon);
+        }
+
+        return true;
+    }
+
+    /**
+     * Appends a new row.
+     *
+     * @param {Boolean} setFocus        Flag that decides whether the focus has to be set on new node
+     *                                  or not
+     * @param {Element} insertAfter     Element after which row has to be appended
+     * @returns {Node}                  Newly appended row
+     */
+    appendNewRow(setFocus, insertAfter) {
+        let listitem1 = this.getListItem(1);
+        let newNode = null;
+
+        if (listitem1) {
+            let newAttendee = this.createAttendee();
+            let nextDummy = this.getNextDummyRow();
+            newNode = listitem1.cloneNode(true);
+
+            if (insertAfter) {
+                this.insertBefore(newNode, insertAfter.nextSibling);
+            } else if (nextDummy) {
+                this.replaceChild(newNode, nextDummy);
+            } else {
+                this.appendChild(newNode);
+            }
+
+            let input = newNode.querySelector(".textbox-addressingWidget");
+            let roleStatusIcon = newNode.querySelector(".status-icon");
+            let userTypeIcon = newNode.querySelector(".usertype-icon");
+
+            // The template could have its fields disabled, that's why we need to reset their
+            // status.
+            input.removeAttribute("disabled");
+            roleStatusIcon.removeAttribute("disabled");
+            userTypeIcon.removeAttribute("disabled");
+
+            if (this.mIsReadOnly || this.mIsInvitation) {
+                input.setAttribute("disabled", "true");
+                roleStatusIcon.setAttribute("disabled", "true");
+                userTypeIcon.setAttribute("disabled", "true");
+            }
+
+            this.mMaxAttendees++;
+
+            input.value = null;
+            input.removeAttribute("value");
+            input.attendee = newAttendee;
+
+            // Set role and participation status.
+            roleStatusIcon.setAttribute("class", "role-icon");
+            roleStatusIcon.setAttribute("role", "REQ-PARTICIPANT");
+            userTypeIcon.setAttribute("cutype", "INDIVIDUAL");
+
+            // Set tooltip for rolenames and usertype icon.
+            this.updateTooltip(roleStatusIcon);
+            this.updateTooltip(userTypeIcon);
+
+            // We always clone the first row. The problem is that the first row could be focused.
+            // When we clone that row, we end up with a cloned XUL textbox that has a focused
+            // attribute set. Therefore we think we're focused and don't properly refocus.
+            // The best solution to this would be to clone a template row that didn't really have
+            // any presentation, rather than using the real visible first row of the listbox.
+            // For now we'll just put in a hack that ensures the focused attribute is never copied
+            // when the node is cloned.
+            if (input.getAttribute("focused") != "") {
+                input.removeAttribute("focused");
+            }
+
+            // focus on new input widget
+            if (setFocus) {
+                this.setFocus(newNode);
+            }
+        }
+        return newNode;
+    }
+
+    /**
+     * Resolves list by the value that is passed and return the list or null if not resolved.
+     *
+     * @param {String} value        Value against which enteries are checked
+     * @returns {?Object}           Found list or null
+     */
+    _resolveListByName(value) {
+        let entries = MailServices.headerParser.makeFromDisplayAddress(value);
+        return entries.length ? this._findListInAddrBooks(entries[0].name) : null;
+    }
+
+    /**
+     * Finds list in the address books.
+     *
+     * @param {String} entryName        Value against which dirName is checked
+     * @returns {Object}                Found list or null
+     */
+    _findListInAddrBooks(entryname) {
+        let allAddressBooks = MailServices.ab.directories;
+
+        while (allAddressBooks.hasMoreElements()) {
+            let abDir = null;
+            try {
+                abDir = allAddressBooks.getNext()
+                    .QueryInterface(Ci.nsIAbDirectory);
+            } catch (ex) {
+                cal.WARN("[eventDialog] Error Encountered" + ex);
+            }
+
+            if (abDir != null && abDir.supportsMailingLists) {
+                let childNodes = abDir.childNodes;
+                while (childNodes.hasMoreElements()) {
+                    let dir = null;
+                    try {
+                        dir = childNodes.getNext().QueryInterface(Ci.nsIAbDirectory);
+                    } catch (ex) {
+                        cal.WARN("[eventDialog] Error Encountered" + ex);
+                    }
+
+                    if (dir && dir.isMailList && (dir.dirName == entryname)) {
+                        return dir;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Finds attendees in the address lists.
+     *
+     * @param {Object} mailingList              Mailing list having address lists
+     * @param {calIAttendee[]} attendees        List of attendees
+     * @param {String[]} allListsUri            URI of all lists
+     * @returns {calIAttendee[]}                List entries found
+     */
+    _getListEntriesInt(mailingList, attendees, allListsUri) {
+        let addressLists = mailingList.addressLists;
+        for (let i = 0; i < addressLists.length; i++) {
+            let abCard = addressLists.queryElementAt(i, Ci.nsIAbCard);
+            let thisId = abCard.primaryEmail;
+            if (abCard.displayName.length > 0) {
+                let rCn = abCard.displayName;
+                if (rCn.includes(",")) {
+                    rCn = '"' + rCn + '"';
+                }
+                thisId = rCn + " <" + thisId + ">";
+            }
+            if (attendees.some(att => att == thisId)) {
+                continue;
+            }
+
+            if (abCard.displayName.length > 0) {
+                let list = this._findListInAddrBooks(abCard.displayName);
+                if (list) {
+                    if (allListsUri.some(uri => uri == list.URI)) {
+                        continue;
+                    }
+                    allListsUri.push(list.URI);
+
+                    this._getListEntriesInt(list, attendees, allListsUri);
+
+                    continue;
+                }
+            }
+
+            attendees.push(thisId);
+        }
+
+        return attendees;
+    }
+
+    /**
+     * Finds enteries in a mailing list.
+     *
+     * @param {Object} mailingList      Mailing list having address lists
+     * @returns {calIAttendee[]}        List entries found
+     */
+    _getListEntries(mailingList) {
+        let attendees = [];
+        let allListsUri = [];
+
+        allListsUri.push(mailingList.URI);
+
+        this._getListEntriesInt(mailingList, attendees, allListsUri);
+
+        return attendees;
+    }
+
+    /**
+     * Fills list item with the entry.
+     *
+     * @param {Element} listitem        Listitem into which attendee entry has to be added
+     * @param {String} entry            Entry item
+     */
+    _fillListItemWithEntry(listitem, entry) {
+        let newAttendee = this.createAttendee(entry);
+        let input = listitem.querySelector(".textbox-addressingWidget");
+        input.removeAttribute("disabled");
+
+        input.attendee = newAttendee;
+        input.value = entry;
+        input.setAttribute("value", entry);
+        input.setAttribute("dirty", "true");
+        if (input.getAttribute("focused") != "") {
+            input.removeAttribute("focused");
+        }
+
+        let roleStatusIcon = listitem.querySelector(".status-icon");
+        roleStatusIcon.removeAttribute("disabled");
+        roleStatusIcon.setAttribute("class", "role-icon");
+        roleStatusIcon.setAttribute("role", newAttendee.role);
+
+        let userTypeIcon = listitem.querySelector(".usertype-icon");
+        userTypeIcon.removeAttribute("disabled");
+        userTypeIcon.setAttribute("cutype", newAttendee.userType);
+    }
+
+    /**
+     * Resolves list.
+     *
+     * @param {Element} input       Node using which list has to be resolved.
+     */
+    resolvePotentialList(input) {
+        let fieldValue = input.value;
+        if (input.id.length > 0 && fieldValue.length > 0) {
+            let mailingList = this._resolveListByName(fieldValue);
+            if (mailingList) {
+                let entries = this._getListEntries(mailingList);
+                if (entries.length > 0) {
+                    let currentIndex = parseInt(input.id.substr(13), 10);
+                    let template = this.querySelector(".addressingWidgetItem");
+                    let currentNode = template.parentNode.childNodes[currentIndex];
+                    this._fillListItemWithEntry(currentNode, entries[0], currentIndex);
+                    entries.shift();
+                    let nextNode = template.parentNode.childNodes[currentIndex + 1];
+                    currentIndex++;
+                    for (let entry of entries) {
+                        currentNode = template.cloneNode(true);
+                        template.parentNode.insertBefore(currentNode, nextNode);
+                        this._fillListItemWithEntry(currentNode, entry, currentIndex);
+                        currentIndex++;
+                    }
+                    this.mMaxAttendees += entries.length;
+                    for (let i = currentIndex; i <= this.mMaxAttendees; i++) {
+                        let row = template.parentNode.childNodes[i];
+                        let textboxInput = row.querySelector(".textbox-addressingWidget");
+                        textboxInput.setAttribute("dirty", "true");
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Emits modify method with list having attendees data.
+     */
+    onModify() {
+        let list = [];
+        for (let i = 1; i <= this.mMaxAttendees; i++) {
+            // Retrieve the string from the appropriate row.
+            let input = this.getInputElement(i);
+            if (input && input.value) {
+                // Parse the string to break this down to individual names and addresses.
+                let parsedInput = MailServices.headerParser.makeFromDisplayAddress(input.value);
+                let email = cal.email.prependMailTo(parsedInput[0].email);
+
+                let isdirty = false;
+                if (input.hasAttribute("dirty")) {
+                    isdirty = input.getAttribute("dirty");
+                }
+                input.removeAttribute("dirty");
+                let entry = {
+                    dirty: isdirty,
+                    calid: email
+                };
+                list.push(entry);
+            }
+        }
+
+        let event = document.createEvent("Events");
+        event.initEvent("modify", true, false);
+        event.details = list;
+        this.dispatchEvent(event);
+    }
+
+    /**
+     * Method setting the tooltip of attendee icons based on their role.
+     *
+     * @param {Element} targetIcon      target-icon node
+     */
+    updateTooltip(targetIcon) {
+        if (targetIcon.classList.contains("role-icon")) {
+            let role = targetIcon.getAttribute("role");
+            // Set tooltip for rolenames.
+
+            const roleMap = {
+                "REQ-PARTICIPANT": "required",
+                "OPT-PARTICIPANT": "optional",
+                "NON-PARTICIPANT": "nonparticipant",
+                "CHAIR": "chair"
+            };
+
+            let roleNameString = "event.attendee.role." + (role in roleMap ? roleMap[role] : "unknown");
+            let tooltip = cal.l10n.getString("calendar-event-dialog-attendees",
+                roleNameString,
+                role in roleMap ? [] : [role]);
+            targetIcon.setAttribute("tooltiptext", tooltip);
+        } else if (targetIcon.classList.contains("usertype-icon")) {
+            let cutype = targetIcon.getAttribute("cutype");
+            const cutypeMap = {
+                INDIVIDUAL: "individual",
+                GROUP: "group",
+                RESOURCE: "resource",
+                ROOM: "room",
+                // I've decided UNKNOWN will not be handled.
+            };
+
+            let cutypeString = "event.attendee.usertype." + (cutype in cutypeMap ? cutypeMap[cutype] : "unknown");
+            let tooltip = cal.l10n.getString("calendar-event-dialog-attendees",
+                cutypeString,
+                cutype in cutypeMap ? [] : [cutype]);
+            targetIcon.setAttribute("tooltiptext", tooltip);
+        }
+    }
+
+    /**
+     * Fits dummy rows in the attendee list.
+     */
+    fitDummyRows() {
+        setTimeout(() => {
+            this.calcContentHeight();
+            this.createOrRemoveDummyRows();
+        }, 0);
+    }
+
+    /**
+     * Calculates attendee list content height.
+     */
+    calcContentHeight() {
+        let items = this.getElementsByTagName("richlistitem");
+        this.mContentHeight = 0;
+        if (items.length > 0) {
+            let i = 0;
+            do {
+                this.mRowHeight = items[i].boxObject.height;
+                ++i;
+            } while (i < items.length && !this.mRowHeight);
+            this.mContentHeight = this.mRowHeight * items.length;
+        }
+    }
+
+    /**
+     * Creates or removes dummy rows from the calendar-event-attendees-list.
+     */
+    createOrRemoveDummyRows() {
+        let listboxHeight = this.boxObject.height;
+
+        // Remove rows to remove scrollbar.
+        let kids = this.childNodes;
+        for (let i = kids.length - 1; this.mContentHeight > listboxHeight && i >= 0; --i) {
+            if (kids[i].hasAttribute("_isDummyRow")) {
+                this.mContentHeight -= this.mRowHeight;
+                kids[i].remove();
+            }
+        }
+
+        // Add rows to fill space.
+        if (this.mRowHeight) {
+            while (this.mContentHeight + this.mRowHeight < listboxHeight) {
+                this.createDummyItem();
+                this.mContentHeight += this.mRowHeight;
+            }
+        }
+    }
+
+    /**
+     * Creates dummy item.
+     *
+     * @returns {Node}       Dummy item
+     */
+    createDummyItem() {
+        let titem = document.createElement("richlistitem");
+        titem.setAttribute("_isDummyRow", "true");
+        titem.setAttribute("class", "dummy-row");
+        for (let i = this.mNumColumns; i > 0; i--) {
+            let cell = document.createElement("hbox");
+            cell.setAttribute("class", "addressingWidgetCell dummy-row-cell");
+            titem.appendChild(cell);
+        }
+        this.appendChild(titem);
+        return titem;
+    }
+
+    /**
+     * Returns the next dummy row from the top.
+     *
+     * @return {?Node}       Next row from the top down
+     */
+    getNextDummyRow() {
+        let kids = this.childNodes;
+        for (let i = 0; i < kids.length; ++i) {
+            if (kids[i].hasAttribute("_isDummyRow")) {
+                return kids[i];
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns richlistitem at row numer `row`.
+     *
+     * @returns {Element}       richlistitem
+     */
+    getListItem(row) {
+        return this.getElementsByTagName("richlistitem")[row - 1];
+    }
+
+    /**
+     * Returns textbox node in first row
+     *
+     * @returns {Object}        textbox node
+     */
+    getInputFromListitem(listItem) {
+        return listItem.getElementsByTagName("textbox")[0];
+    }
+
+    /**
+     * Returns richlistitem closest to node `element`.
+     *
+     * @param {Element} element     Element closest to which <xul-richlistitem> has to be found
+     * @returns {Number}            Total number of rows in a list
+     */
+    getRowByInputElement(element) {
+        let row = 0;
+        element = element.closest("richlistitem");
+        if (element) {
+            while (element) {
+                if (element.localName == "richlistitem") {
+                    ++row;
+                }
+                element = element.previousSibling;
+            }
+        }
+        return row;
+    }
+
+    /**
+     * Returns textbox that contains the name of the attendee at row number `row`.
+     *
+     * @param {Element} row     Row element
+     * @returns {Element}       Textbox element
+     */
+    getInputElement(row) {
+        return this.getListItem(row).querySelector(".textbox-addressingWidget");
+    }
+
+    /**
+     * Returns textbox that contains the name of the attendee at row number `row`.
+     *
+     * @param {Element} row     Row element
+     * @returns {Element}       Textbox element
+     */
+    getRoleElement(row) {
+        return this.getListItem(row).querySelector(".role-icon, .status-icon");
+    }
+
+    /**
+     * Returns status element in the row `row`.
+     *
+     * @param {Element} row     Row element
+     * @returns {Element}       Status element in the row
+     */
+    getStatusElement(row) {
+        return this.getListItem(row).querySelector(".role-icon, .status-icon");
+    }
+
+    /**
+     * Returns usertype-icon element in the row `row`.
+     *
+     * @param {Element} row     Row element
+     * @returns {Element}       Usertype-icon element in the row
+     */
+    getUserTypeElement(row) {
+        return this.getListItem(row).querySelector(".usertype-icon");
+    }
+
+    /**
+     * Sets foucs on the textbox in the row `row`.
+     *
+     * @param {Element|Number} row      Row number or row
+     */
+    setFocus(row) {
+        // See https://stackoverflow.com/questions/779379/why-is-settimeoutfn-0-sometimes-useful to
+        // know why setTimeout is helpful here.
+        setTimeout(() => {
+            let node;
+            if (typeof row == "number") {
+                node = this.getListItem(row);
+            } else {
+                node = row;
+            }
+
+            this.ensureElementIsVisible(node);
+
+            let input = node.querySelector(".textbox-addressingWidget");
+            input.focus();
+        }, 0);
+    }
+
+    /**
+     * Creates attendee.
+     *
+     * @return {Node}       Newly created attendee
+     */
+    createAttendee() {
+        let attendee = cal.createAttendee();
+        attendee.id = "";
+        attendee.rsvp = "TRUE";
+        attendee.role = "REQ-PARTICIPANT";
+        attendee.participationStatus = "NEEDS-ACTION";
+        return attendee;
+    }
+
+    /**
+     * If the element `element` has valid headerValue then a new attendee row is created and cursor
+     * is moved to the next row, else just cursor is moved to the next row.
+     *
+     * @param {Element} element     Element upon which event occurred
+     * @param {Boolean} noAdvance   Flag that decides whether arrowHit method has to be executed in
+     *                              the end or not
+     */
+    returnHit(element, noAdvance) {
+        const parseHeaderValue = (aMsgIAddressObject) => {
+            if (aMsgIAddressObject.name.match(/[<>@,]/)) {
+                // Special handling only needed for a name with a comma which are not already quoted.
+                return (aMsgIAddressObject.name.match(/^".*"$/) ?
+                    aMsgIAddressObject.name
+                    : '"' + aMsgIAddressObject.name + '"'
+                ) + " <" + aMsgIAddressObject.email + ">";
+            }
+
+            return aMsgIAddressObject.toString();
+        };
+
+        let arrowLength = 1;
+        if (element.value.includes(",") || element.value.match(/^[^"].*[<>@,].*[^"] <.+@.+>$/)) {
+            let strippedAddresses = element.value.replace(/.* >> /, "");
+            let addresses = MailServices.headerParser.makeFromDisplayAddress(strippedAddresses);
+            element.value = parseHeaderValue(addresses[0]);
+
+            // The following code is needed to split attendees, if the user enters a comma
+            // separated list of attendees without using autocomplete functionality.
+            let insertAfterItem = this.getListItem(this.getRowByInputElement(element));
+            for (let key in addresses) {
+                if (key > 0) {
+                    insertAfterItem = this.appendNewRow(false, insertAfterItem);
+                    let textinput = this.getInputFromListitem(insertAfterItem);
+                    textinput.value = parseHeaderValue(addresses[key]);
+                }
+            }
+            arrowLength = addresses.length;
+        }
+
+        if (!noAdvance) {
+            this.arrowHit(element, arrowLength);
+        }
+    }
+
+    /**
+     * Navigates up and down through the attendees row.
+     *
+     * @param {Element} element     Element upon which event occurred
+     * @param {Number} direction    (-1 or 1) number representing left and right arrow key
+     */
+    arrowHit(element, direction) {
+        let row = this.getRowByInputElement(element) + direction;
+        if (row) {
+            if (row > this.mMaxAttendees) {
+                this.appendNewRow(true);
+            } else {
+                let input = this.getInputElement(row);
+                if (input.hasAttribute("disabled")) {
+                    return;
+                }
+                this.setFocus(row);
+            }
+            let event = document.createEvent("Events");
+            event.initEvent("rowchange", true, false);
+            event.details = row;
+            this.dispatchEvent(event);
+        }
+    }
+
+    /**
+     * Deletes the attendee row of the element `element`.
+     *
+     * @param {Element} element     Element upon which event occurred
+     */
+    deleteHit(element) {
+        // Don't delete the row if only the organizer is remaining.
+        if (this.mMaxAttendees <= 1) {
+            return;
+        }
+
+        let row = this.getRowByInputElement(element);
+        this.deleteRow(row);
+        if (row > 0) {
+            row = row - 1;
+        }
+        this.setFocus(row);
+        this.onModify();
+
+        let event = document.createEvent("Events");
+        event.initEvent("rowchange", true, false);
+        event.details = row;
+        this.dispatchEvent(event);
+    }
+
+    /**
+     * Deletes row `row`.
+     *
+     * @param {Element} row     Row that has to be deleted
+     */
+    deleteRow(row) {
+        this.removeRow(row);
+    }
+
+    /**
+     * Removes row `row` and adds dummy row on its place.
+     *
+     * @param {Element} row      Row that has to be removed
+     */
+    removeRow(row) {
+        this.getListItem(row).remove();
+        this.fitDummyRows();
+        this.mMaxAttendees--;
+    }
+}
+
+customElements.define("calendar-event-attendees-list", MozCalendarEventAttendeesList);
--- a/calendar/base/content/dialogs/calendar-event-dialog-attendees.js
+++ b/calendar/base/content/dialogs/calendar-event-dialog-attendees.js
@@ -31,16 +31,18 @@ var gZoomFactor = 100;
  */
 function onLoad() {
     // set calendar-event-freebusy-timebar date and time properties
     setFreebusyTimebarTime();
 
     // set up some calendar-event-freebusy-timebar properties
     onFreebusyTimebarInit();
 
+    onCalendarEventAttendeesListLoad();
+
     // first of all, attach all event handlers
     window.addEventListener("resize", onResize, true);
     window.addEventListener("modify", onModify, true);
     window.addEventListener("rowchange", onRowChange, true);
     window.addEventListener("DOMAttrModified", onAttrModified, true);
     window.addEventListener("timebar", onTimebar, true);
     window.addEventListener("timechange", onTimeChange, true);
 
@@ -818,30 +820,30 @@ function initTimeRange() {
         gEndHour = 24;
     } else {
         gStartHour = Services.prefs.getIntPref("calendar.view.daystarthour", 8);
         gEndHour = Services.prefs.getIntPref("calendar.view.dayendhour", 19);
     }
 }
 
 /**
- * Handler function for the "modify" event, emitted from the attendees-list
+ * Handler function for the "modify" event, emitted from the calendar-event-attendees-list
  * binding. event.details is an array of objects containing the user's email
  * (calid) and a flag that tells if the user has entered text before the last
  * onModify was called (dirty).
  *
  * @param event     The DOM event that caused the modification.
  */
 function onModify(event) {
     onResize();
     document.getElementById("freebusy-grid").onModify(event);
 }
 
 /**
- * Handler function for the "rowchange" event, emitted from the attendees-list
+ * Handler function for the "rowchange" event, emitted from the calendar-event-attendees-list
  * binding. event.details is the row that was changed to.
  *
  * @param event     The DOM event caused by the row change.
  */
 function onRowChange(event) {
     let scrollbar = document.getElementById("vertical-scrollbar");
     let attendees = document.getElementById("attendees-list");
     let maxpos = scrollbar.getAttribute("maxpos");
@@ -1050,8 +1052,80 @@ function onFreebusyTimebarInit() {
                 newNode.endDate = timebar.mEndDate;
                 newNode.date = date;
             }
         }
     }
 
     timebar.dispatchTimebarEvent();
 }
+
+function onCalendarEventAttendeesListLoad() {
+    let attendeesList = document.getElementById("attendees-list");
+    let args = window.arguments[0];
+    let organizer = args.organizer;
+    let attendees = args.attendees;
+    let calendar = args.calendar;
+
+    attendeesList.isReadOnly = calendar.readOnly;
+
+    // assume we're the organizer [in case that the calendar
+    // does not support the concept of identities].
+    let organizerID = ((organizer && organizer.id) ?
+        organizer.id
+        : calendar.getProperty("organizerId"));
+
+    calendar = cal.wrapInstance(calendar, Ci.calISchedulingSupport);
+    attendeesList.isInvitation = (calendar && calendar.isInvitation(args.item));
+
+    let template = attendeesList.querySelector(".addressingWidgetItem");
+    template.focus();
+
+    if (attendeesList.isReadOnly || attendeesList.isInvitation) {
+        attendeesList.setAttribute("disabled", "true");
+    }
+
+    // TODO: the organizer should show up in the attendee list, but this information
+    // should be based on the organizer contained in the appropriate field of calIItemBase.
+    // This is currently not supported, since we're still missing calendar identities.
+    if (organizerID && organizerID != "") {
+        if (organizer) {
+            if (!organizer.id) {
+                organizer.id = organizerID;
+            }
+            if (!organizer.role) {
+                organizer.role = "CHAIR";
+            }
+            if (!organizer.participationStatus) {
+                organizer.participationStatus = "ACCEPTED";
+            }
+        } else {
+            organizer = attendeesList.createAttendee();
+            organizer.id = organizerID;
+            organizer.role = "CHAIR";
+            organizer.participationStatus = "ACCEPTED";
+        }
+        if (!organizer.commonName || !organizer.commonName.length) {
+            organizer.commonName = calendar.getProperty("organizerCN");
+        }
+        organizer.isOrganizer = true;
+        attendeesList.appendAttendee(organizer, template, true);
+    }
+
+    let numRowsAdded = 0;
+    if (attendees.length > 0) {
+        for (let attendee of attendees) {
+            attendeesList.appendAttendee(attendee, template, false);
+            numRowsAdded++;
+        }
+    }
+    if (numRowsAdded == 0) {
+        attendeesList.appendAttendee(null, template, false);
+    }
+
+    // detach the template item from the listbox, but hold the reference.
+    // until this function returns we add at least a single copy of this template back again.
+    template.remove();
+
+    attendeesList.setFocus(attendeesList.mMaxAttendees);
+
+    attendeesList.init();
+}
--- a/calendar/base/content/dialogs/calendar-event-dialog-attendees.xml
+++ b/calendar/base/content/dialogs/calendar-event-dialog-attendees.xml
@@ -10,1045 +10,16 @@
   <!ENTITY % dtd1 SYSTEM "chrome://calendar/locale/global.dtd" > %dtd1;
   <!ENTITY % dtd2 SYSTEM "chrome://calendar/locale/calendar.dtd" > %dtd2;
   <!ENTITY % dtd3 SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd"> %dtd3;
 ]>
 
 <bindings xmlns="http://www.mozilla.org/xbl"
           xmlns:xbl="http://www.mozilla.org/xbl"
           xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
-  <binding id="attendees-list" extends="chrome://messenger/content/richlistbox.xml#xbl-richlistbox">
-    <implementation>
-      <field name="mMaxAttendees">0</field>
-      <field name="mContentHeight">0</field>
-      <field name="mRowHeight">0</field>
-      <field name="mNumColumns">3</field>
-      <field name="mIsOffline">0</field>
-      <field name="mIsReadOnly">false</field>
-      <field name="mIsInvitation">false</field>
-      <field name="mPopupOpen">false</field>
-
-      <constructor><![CDATA[
-          this.mMaxAttendees = 0;
-
-          window.addEventListener("load", this.onLoad.bind(this), true);
-      ]]></constructor>
-
-      <method name="onLoad">
-        <body><![CDATA[
-            this.onInitialize();
-
-            // this trigger the continuous update chain, which
-            // effectively calls this.onModify() on predefined
-            // time intervals [each second].
-            let self = this;
-            let callback = function() {
-                setTimeout(callback, 1000);
-                self.onModify();
-            };
-            callback();
-        ]]></body>
-      </method>
-
-      <method name="onInitialize">
-        <body><![CDATA[
-            let args = window.arguments[0];
-            let organizer = args.organizer;
-            let attendees = args.attendees;
-            let calendar = args.calendar;
-
-            this.mIsReadOnly = calendar.readOnly;
-
-            // assume we're the organizer [in case that the calendar
-            // does not support the concept of identities].
-            let organizerID = ((organizer && organizer.id)
-                               ? organizer.id
-                               : calendar.getProperty("organizerId"));
-
-            calendar = cal.wrapInstance(calendar, Ci.calISchedulingSupport);
-            this.mIsInvitation = (calendar && calendar.isInvitation(args.item));
-
-            let template = this.querySelector(".addressingWidgetItem");
-            template.focus();
-
-            if (this.mIsReadOnly || this.mIsInvitation) {
-                this.setAttribute("disabled", "true");
-            }
-
-            // TODO: the organizer should show up in the attendee list, but this information
-            // should be based on the organizer contained in the appropriate field of calIItemBase.
-            // This is currently not supported, since we're still missing calendar identities.
-            if (organizerID && organizerID != "") {
-                if (organizer) {
-                    if (!organizer.id) {
-                        organizer.id = organizerID;
-                    }
-                    if (!organizer.role) {
-                        organizer.role = "CHAIR";
-                    }
-                    if (!organizer.participationStatus) {
-                        organizer.participationStatus = "ACCEPTED";
-                    }
-                } else {
-                    organizer = this.createAttendee();
-                    organizer.id = organizerID;
-                    organizer.role = "CHAIR";
-                    organizer.participationStatus = "ACCEPTED";
-                }
-                if (!organizer.commonName || !organizer.commonName.length) {
-                    organizer.commonName = calendar.getProperty("organizerCN");
-                }
-                organizer.isOrganizer = true;
-                this.appendAttendee(organizer, template, true);
-            }
-
-            let numRowsAdded = 0;
-            if (attendees.length > 0) {
-                for (let attendee of attendees) {
-                    this.appendAttendee(attendee, template, false);
-                    numRowsAdded++;
-                }
-            }
-            if (numRowsAdded == 0) {
-                this.appendAttendee(null, template, false);
-            }
-
-            // detach the template item from the listbox, but hold the reference.
-            // until this function returns we add at least a single copy of this template back again.
-            template.remove();
-
-            this.setFocus(this.mMaxAttendees);
-        ]]></body>
-      </method>
-
-      <!-- appends a new row using an existing attendee structure -->
-      <method name="appendAttendee">
-        <parameter name="aAttendee"/>
-        <parameter name="aTemplateNode"/>
-        <parameter name="aDisableIfOrganizer"/>
-        <body><![CDATA[
-            // create a new listbox item and append it to our parent control.
-            let newNode = aTemplateNode.cloneNode(true);
-
-            this.appendChild(newNode);
-
-            let input = newNode.querySelector(".textbox-addressingWidget");
-            let roleStatusIcon = newNode.querySelector(".status-icon");
-            let userTypeIcon = newNode.querySelector(".usertype-icon");
-
-            // We always clone the first row. The problem is that the first row
-            // could be focused. When we clone that row, we end up with a cloned
-            // XUL textbox that has a focused attribute set.  Therefore we think
-            // we're focused and don't properly refocus.  The best solution to this
-            // would be to clone a template row that didn't really have any presentation,
-            // rather than using the real visible first row of the listbox.
-            // For now we'll just put in a hack that ensures the focused attribute
-            // is never copied when the node is cloned.
-            if (input.getAttribute("focused") != "") {
-                input.removeAttribute("focused");
-            }
-
-            // the template could have its fields disabled,
-            // that's why we need to reset their status.
-            input.removeAttribute("disabled");
-            userTypeIcon.removeAttribute("disabled");
-            roleStatusIcon.removeAttribute("disabled");
-
-            if (this.mIsReadOnly || this.mIsInvitation) {
-                input.setAttribute("disabled", "true");
-                userTypeIcon.setAttribute("disabled", "true");
-                roleStatusIcon.setAttribute("disabled", "true");
-            }
-
-            // disable the input-field [name <email>] if this attendee
-            // appears to be the organizer.
-            if (aDisableIfOrganizer && aAttendee && aAttendee.isOrganizer) {
-                input.setAttribute("disabled", "true");
-            }
-
-            this.mMaxAttendees++;
-
-            if (!aAttendee) {
-                aAttendee = this.createAttendee();
-            }
-
-            // construct the display string from common name and/or email address.
-            let commonName = aAttendee.commonName || "";
-            let inputValue = cal.email.removeMailTo(aAttendee.id || "");
-            if (commonName.length) {
-                // Make the commonName appear in quotes if it contains a
-                // character that could confuse the header parser
-                if (commonName.search(/[,;<>@]/) != -1) {
-                    commonName = '"' + commonName + '"';
-                }
-                inputValue = inputValue.length ? commonName + " <" + inputValue + ">" : commonName;
-            }
-
-            // trim spaces if any
-            inputValue = inputValue.trim();
-
-            // don't set value with null, otherwise autocomplete stops working,
-            // but make sure attendee and dirty are set
-            if (inputValue.length) {
-                input.setAttribute("value", inputValue);
-                input.value = inputValue;
-            }
-            input.attendee = aAttendee;
-            input.setAttribute("dirty", "true");
-
-            if (aAttendee) {
-                // Set up userType
-                setElementValue(userTypeIcon, aAttendee.userType || false, "cutype");
-                this.updateTooltip(userTypeIcon);
-
-                // Set up role/status icon
-                if (aAttendee.isOrganizer) {
-                    roleStatusIcon.setAttribute("class", "status-icon");
-                    setElementValue(roleStatusIcon, aAttendee.participationStatus || false, "status");
-                } else {
-                    roleStatusIcon.setAttribute("class", "role-icon");
-                    setElementValue(roleStatusIcon, aAttendee.role || false, "role");
-                }
-                this.updateTooltip(roleStatusIcon);
-            }
-
-            return true;
-        ]]></body>
-      </method>
-
-      <method name="appendNewRow">
-        <parameter name="aSetFocus"/>
-        <parameter name="aInsertAfter"/>
-        <body><![CDATA[
-            let listitem1 = this.getListItem(1);
-            let newNode = null;
-
-            if (listitem1) {
-                let newAttendee = this.createAttendee();
-                let nextDummy = this.getNextDummyRow();
-                newNode = listitem1.cloneNode(true);
-
-                if (aInsertAfter) {
-                    this.insertBefore(newNode, aInsertAfter.nextSibling);
-                } else if (nextDummy) {
-                    this.replaceChild(newNode, nextDummy);
-                } else {
-                    this.appendChild(newNode);
-                }
-
-                let input = newNode.querySelector(".textbox-addressingWidget");
-                let roleStatusIcon = newNode.querySelector(".status-icon");
-                let userTypeIcon = newNode.querySelector(".usertype-icon");
-
-                // the template could have its fields disabled,
-                // that's why we need to reset their status.
-                input.removeAttribute("disabled");
-                roleStatusIcon.removeAttribute("disabled");
-                userTypeIcon.removeAttribute("disabled");
-
-                if (this.mIsReadOnly || this.mIsInvitation) {
-                    input.setAttribute("disabled", "true");
-                    roleStatusIcon.setAttribute("disabled", "true");
-                    userTypeIcon.setAttribute("disabled", "true");
-                }
-
-                this.mMaxAttendees++;
-
-                input.value = null;
-                input.removeAttribute("value");
-                input.attendee = newAttendee;
-
-                // set role and participation status
-                roleStatusIcon.setAttribute("class", "role-icon");
-                roleStatusIcon.setAttribute("role", "REQ-PARTICIPANT");
-                userTypeIcon.setAttribute("cutype", "INDIVIDUAL");
-
-                // Set tooltip for rolenames and usertype icon
-                this.updateTooltip(roleStatusIcon);
-                this.updateTooltip(userTypeIcon);
-
-                // We always clone the first row.  The problem is that the first row
-                // could be focused.  When we clone that row, we end up with a cloned
-                // XUL textbox that has a focused attribute set.  Therefore we think
-                // we're focused and don't properly refocus.  The best solution to this
-                // would be to clone a template row that didn't really have any presentation,
-                // rather than using the real visible first row of the listbox.
-                // For now we'll just put in a hack that ensures the focused attribute
-                // is never copied when the node is cloned.
-                if (input.getAttribute("focused") != "") {
-                    input.removeAttribute("focused");
-                }
-
-                // focus on new input widget
-                if (aSetFocus) {
-                    this.setFocus(newNode);
-                }
-            }
-            return newNode;
-        ]]></body>
-      </method>
-
-      <property name="attendees">
-        <getter><![CDATA[
-            const { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm");
-
-            let attendees = [];
-
-            for (let i = 1; true; i++) {
-                let inputField = this.getInputElement(i);
-                if (!inputField) {
-                    break;
-                } else if (inputField.value == "") {
-                    continue;
-                }
-
-                // the inputfield already has a reference to the attendee
-                // object, we just need to fill in the name.
-                let attendee = inputField.attendee.clone();
-                if (attendee.isOrganizer) {
-                    continue;
-                }
-
-                attendee.role = this.getRoleElement(i).getAttribute("role");
-                // attendee.participationStatus = this.getStatusElement(i).getAttribute("status");
-                let userType = this.getUserTypeElement(i).getAttribute("cutype");
-                attendee.userType = (userType == "INDIVIDUAL" ? null : userType); // INDIVIDUAL is the default
-
-                // break the list of potentially many attendees back into individual names. This
-                // is required in case the user entered comma-separated attendees in one field and
-                // then clicked OK without switching to the next line.
-                let parsedInput = MailServices.headerParser.makeFromDisplayAddress(inputField.value);
-                let j = 0;
-                let addAttendee = function(aAddress) {
-                    if (j > 0) {
-                        attendee = attendee.clone();
-                    }
-                    attendee.id = cal.email.prependMailTo(aAddress.email);
-                    let commonName = null;
-                    if (aAddress.name.length > 0) {
-                        // we remove any double quotes within CN due to bug 1209399
-                        let name = aAddress.name.replace(/(?:(?:[\\]")|(?:"))/g, "");
-                        if (aAddress.email != name) {
-                            commonName = name;
-                        }
-                    }
-                    attendee.commonName = commonName;
-                    attendees.push(attendee);
-                    j++;
-                };
-                parsedInput.forEach(addAttendee);
-            }
-            return attendees;
-        ]]></getter>
-      </property>
-
-      <property name="organizer">
-        <getter><![CDATA[
-            const { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm");
-
-            for (let i = 1; true; i++) {
-                let inputField = this.getInputElement(i);
-                if (!inputField) {
-                    break;
-                } else if (inputField.value == "") {
-                    continue;
-                }
-
-                // The inputfield already has a reference to the attendee
-                // object, we just need to fill in the name.
-                let attendee = inputField.attendee.clone();
-
-                // attendee.role = this.getRoleElement(i).getAttribute("role");
-                attendee.participationStatus = this.getStatusElement(i).getAttribute("status");
-                // Organizers do not have a CUTYPE
-                attendee.userType = null;
-
-                // break the list of potentially many attendees back into individual names
-                let parsedInput = MailServices.headerParser.makeFromDisplayAddress(inputField.value);
-                if (parsedInput[0].email > 0) {
-                    attendee.id = cal.email.prependMailTo(parsedInput[0].email);
-                }
-                let commonName = null;
-                if (parsedInput[0].name.length > 0) {
-                    let name = parsedInput[0].name.replace(/(?:(?:[\\]")|(?:"))/g, "");
-                    if (attendee.email != name) {
-                        commonName = name;
-                    }
-                }
-                attendee.commonName = commonName;
-
-                if (attendee.isOrganizer) {
-                    return attendee;
-                }
-            }
-
-            return null;
-        ]]></getter>
-      </property>
-
-      <method name="_resolveListByName">
-        <parameter name="value"/>
-        <body><![CDATA[
-            const { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm");
-
-            let entries = MailServices.headerParser.makeFromDisplayAddress(value);
-            return entries.length ? this._findListInAddrBooks(entries[0].name) : null;
-        ]]></body>
-      </method>
-
-      <method name="_findListInAddrBooks">
-        <parameter name="entryname"/>
-        <body><![CDATA[
-            const { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm");
-
-            let allAddressBooks = MailServices.ab.directories;
-
-            while (allAddressBooks.hasMoreElements()) {
-                let abDir = null;
-                try {
-                    abDir = allAddressBooks.getNext()
-                                           .QueryInterface(Ci.nsIAbDirectory);
-                } catch (ex) {
-                    cal.WARN("[eventDialog] Error Encountered" + ex);
-                }
-
-                if (abDir != null && abDir.supportsMailingLists) {
-                    let childNodes = abDir.childNodes;
-                    while (childNodes.hasMoreElements()) {
-                        let dir = null;
-                        try {
-                            dir = childNodes.getNext().QueryInterface(Ci.nsIAbDirectory);
-                        } catch (ex) {
-                            cal.WARN("[eventDialog] Error Encountered" + ex);
-                        }
-
-                        if (dir && dir.isMailList && (dir.dirName == entryname)) {
-                            return dir;
-                        }
-                    }
-                }
-            }
-            return null;
-        ]]></body>
-      </method>
-
-      <method name="_getListEntriesInt">
-        <parameter name="mailingList"/>
-        <parameter name="attendees"/>
-        <parameter name="allListsUri"/>
-        <body><![CDATA[
-            let addressLists = mailingList.addressLists;
-            for (let i = 0; i < addressLists.length; i++) {
-                let abCard = addressLists.queryElementAt(i, Ci.nsIAbCard);
-                let thisId = abCard.primaryEmail;
-                if (abCard.displayName.length > 0) {
-                    let rCn = abCard.displayName;
-                    if (rCn.includes(",")) {
-                        rCn = '"' + rCn + '"';
-                    }
-                    thisId = rCn + " <" + thisId + ">";
-                }
-                if (attendees.some(att => att == thisId)) {
-                    continue;
-                }
-
-                if (abCard.displayName.length > 0) {
-                    let list = this._findListInAddrBooks(abCard.displayName);
-                    if (list) {
-                        if (allListsUri.some(uri => uri == list.URI)) {
-                            continue;
-                        }
-                        allListsUri.push(list.URI);
-
-                        this._getListEntriesInt(list, attendees, allListsUri);
-
-                        continue;
-                    }
-                }
-
-                attendees.push(thisId);
-            }
-
-            return attendees;
-        ]]></body>
-      </method>
-
-      <method name="_getListEntries">
-        <parameter name="mailingList"/>
-        <body><![CDATA[
-            let attendees = [];
-            let allListsUri = [];
-
-            allListsUri.push(mailingList.URI);
-
-            this._getListEntriesInt(mailingList, attendees, allListsUri);
-
-            return attendees;
-        ]]></body>
-      </method>
-
-      <method name="_fillListItemWithEntry">
-        <parameter name="listitem"/>
-        <parameter name="entry"/>
-        <parameter name="rowNumber"/>
-        <body><![CDATA[
-            let newAttendee = this.createAttendee(entry);
-            let input = listitem.querySelector(".textbox-addressingWidget");
-            input.removeAttribute("disabled");
-
-            input.attendee = newAttendee;
-            input.value = entry;
-            input.setAttribute("value", entry);
-            input.setAttribute("dirty", "true");
-            if (input.getAttribute("focused") != "") {
-                input.removeAttribute("focused");
-            }
-
-            let roleStatusIcon = listitem.querySelector(".status-icon");
-            roleStatusIcon.removeAttribute("disabled");
-            roleStatusIcon.setAttribute("class", "role-icon");
-            roleStatusIcon.setAttribute("role", newAttendee.role);
-
-            let userTypeIcon = listitem.querySelector(".usertype-icon");
-            userTypeIcon.removeAttribute("disabled");
-            userTypeIcon.setAttribute("cutype", newAttendee.userType);
-        ]]></body>
-      </method>
-
-      <method name="resolvePotentialList">
-        <parameter name="aInput"/>
-        <body><![CDATA[
-            let fieldValue = aInput.value;
-            if (aInput.id.length > 0 && fieldValue.length > 0) {
-                let mailingList = this._resolveListByName(fieldValue);
-                if (mailingList) {
-                    let entries = this._getListEntries(mailingList);
-                    if (entries.length > 0) {
-                        let currentIndex = parseInt(aInput.id.substr(13), 10);
-                        let template = this.querySelector(".addressingWidgetItem");
-                        let currentNode = template.parentNode.childNodes[currentIndex];
-                        this._fillListItemWithEntry(currentNode, entries[0], currentIndex);
-                        entries.shift();
-                        let nextNode = template.parentNode.childNodes[currentIndex + 1];
-                        currentIndex++;
-                        for (let entry of entries) {
-                            currentNode = template.cloneNode(true);
-                            template.parentNode.insertBefore(currentNode, nextNode);
-                            this._fillListItemWithEntry(currentNode, entry, currentIndex);
-                            currentIndex++;
-                        }
-                        this.mMaxAttendees += entries.length;
-                        for (let i = currentIndex; i <= this.mMaxAttendees; i++) {
-                            let row = template.parentNode.childNodes[i];
-                            let input = row.querySelector(".textbox-addressingWidget");
-                            input.setAttribute("dirty", "true");
-                        }
-                    }
-                }
-            }
-        ]]></body>
-      </method>
-
-      <method name="onModify">
-        <body><![CDATA[
-            const { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm");
-
-            let list = [];
-            for (let i = 1; i <= this.mMaxAttendees; i++) {
-                // retrieve the string from the appropriate row
-                let input = this.getInputElement(i);
-                if (input && input.value) {
-                    // parse the string to break this down to individual names and addresses
-                    let parsedInput = MailServices.headerParser.makeFromDisplayAddress(input.value);
-                    let email = cal.email.prependMailTo(parsedInput[0].email);
-
-                    let isdirty = false;
-                    if (input.hasAttribute("dirty")) {
-                        isdirty = input.getAttribute("dirty");
-                    }
-                    input.removeAttribute("dirty");
-                    let entry = {
-                        dirty: isdirty,
-                        calid: email
-                    };
-                    list.push(entry);
-                }
-            }
-
-            let event = document.createEvent("Events");
-            event.initEvent("modify", true, false);
-            event.details = list;
-            this.dispatchEvent(event);
-        ]]></body>
-      </method>
-
-      <method name="updateTooltip">
-        <parameter name="targetIcon"/>
-        <body><![CDATA[
-            // Function setting the tooltip of attendeeicons based on their role
-            if (targetIcon.classList.contains("role-icon")) {
-                let role = targetIcon.getAttribute("role");
-                // Set tooltip for rolenames
-
-                const roleMap = {
-                    "REQ-PARTICIPANT": "required",
-                    "OPT-PARTICIPANT": "optional",
-                    "NON-PARTICIPANT": "nonparticipant",
-                    "CHAIR": "chair"
-                };
-
-                let roleNameString = "event.attendee.role." + (role in roleMap ? roleMap[role] : "unknown");
-                let tooltip = cal.l10n.getString("calendar-event-dialog-attendees",
-                                                 roleNameString,
-                                                 role in roleMap ? []: [role]);
-                targetIcon.setAttribute("tooltiptext", tooltip);
-            } else if (targetIcon.classList.contains("usertype-icon")) {
-                let cutype = targetIcon.getAttribute("cutype");
-                const cutypeMap = {
-                    INDIVIDUAL: "individual",
-                    GROUP: "group",
-                    RESOURCE: "resource",
-                    ROOM: "room",
-                    // I've decided UNKNOWN will not be handled.
-                };
-
-                let cutypeString = "event.attendee.usertype." + (cutype in cutypeMap ? cutypeMap[cutype] : "unknown");
-                let tooltip = cal.l10n.getString("calendar-event-dialog-attendees",
-                                                 cutypeString,
-                                                 cutype in cutypeMap ? [] : [cutype]);
-                targetIcon.setAttribute("tooltiptext", tooltip);
-            }
-        ]]></body>
-      </method>
-
-      <property name="documentSize">
-        <getter><![CDATA[
-            return this.mRowHeight * this.mMaxAttendees;
-        ]]></getter>
-      </property>
-
-      <method name="fitDummyRows">
-        <body><![CDATA[
-            setTimeout(() => {
-                this.calcContentHeight();
-                this.createOrRemoveDummyRows();
-            }, 0);
-        ]]></body>
-      </method>
-
-      <method name="calcContentHeight">
-        <body><![CDATA[
-            let items = this.getElementsByTagName("richlistitem");
-            this.mContentHeight = 0;
-            if (items.length > 0) {
-                let i = 0;
-                do {
-                    this.mRowHeight = items[i].boxObject.height;
-                    ++i;
-                } while (i < items.length && !this.mRowHeight);
-                this.mContentHeight = this.mRowHeight * items.length;
-            }
-        ]]></body>
-      </method>
-
-      <method name="createOrRemoveDummyRows">
-        <body><![CDATA[
-            let listboxHeight = this.boxObject.height;
-
-            // remove rows to remove scrollbar
-            let kids = this.childNodes;
-            for (let i = kids.length - 1; this.mContentHeight > listboxHeight && i >= 0; --i) {
-                if (kids[i].hasAttribute("_isDummyRow")) {
-                    this.mContentHeight -= this.mRowHeight;
-                    kids[i].remove();
-                }
-            }
-
-            // add rows to fill space
-            if (this.mRowHeight) {
-                while (this.mContentHeight + this.mRowHeight < listboxHeight) {
-                    this.createDummyItem();
-                    this.mContentHeight += this.mRowHeight;
-                }
-            }
-        ]]></body>
-      </method>
-
-      <method name="createDummyItem">
-        <body><![CDATA[
-            let titem = document.createElement("richlistitem");
-            titem.setAttribute("_isDummyRow", "true");
-            titem.setAttribute("class", "dummy-row");
-            for (let i = this.mNumColumns; i > 0; i--) {
-                let cell = document.createElement("hbox");
-                cell.setAttribute("flex", "1");
-                cell.setAttribute("class", "addressingWidgetCell dummy-row-cell");
-                titem.appendChild(cell);
-            }
-            this.appendChild(titem);
-            return titem;
-        ]]></body>
-      </method>
-
-      <!-- gets the next row from the top down -->
-      <method name="getNextDummyRow">
-        <body><![CDATA[
-            let kids = this.childNodes;
-            for (let i = 0; i < kids.length; ++i) {
-                if (kids[i].hasAttribute("_isDummyRow")) {
-                    return kids[i];
-                }
-            }
-            return null;
-        ]]></body>
-      </method>
-
-      <!-- This method returns the <xul:richlistitem> at row number 'aRow' -->
-      <method name="getListItem">
-        <parameter name="aRow"/>
-        <body><![CDATA[
-            return this.getElementsByTagName("richlistitem")[aRow - 1];
-        ]]></body>
-      </method>
-
-      <method name="getInputFromListitem">
-        <parameter name="aListItem"/>
-        <body><![CDATA[
-            return aListItem.getElementsByTagName("textbox")[0];
-        ]]></body>
-      </method>
-
-      <method name="getRowByInputElement">
-        <parameter name="aElement"/>
-        <body><![CDATA[
-            let row = 0;
-            aElement = aElement.closest("richlistitem");
-            if (aElement) {
-                while (aElement) {
-                    if (aElement.localName == "richlistitem") {
-                        ++row;
-                    }
-                    aElement = aElement.previousSibling;
-                }
-            }
-            return row;
-        ]]></body>
-      </method>
-
-      <!-- This method returns the <xul:textbox> that contains
-           the name of the attendee at row number 'aRow' -->
-      <method name="getInputElement">
-        <parameter name="aRow"/>
-        <body><![CDATA[
-            return this.getListItem(aRow).querySelector(".textbox-addressingWidget");
-        ]]></body>
-      </method>
-
-      <method name="getRoleElement">
-        <parameter name="aRow"/>
-        <body><![CDATA[
-            return this.getListItem(aRow).querySelector(".role-icon, .status-icon");
-        ]]></body>
-      </method>
-
-      <method name="getStatusElement">
-        <parameter name="aRow"/>
-        <body><![CDATA[
-            return this.getListItem(aRow).querySelector(".role-icon, .status-icon");
-        ]]></body>
-      </method>
-
-      <method name="getUserTypeElement">
-        <parameter name="aRow"/>
-        <body><![CDATA[
-            return this.getListItem(aRow).querySelector(".usertype-icon");
-        ]]></body>
-      </method>
-
-      <method name="setFocus">
-        <parameter name="aRow"/>
-        <body><![CDATA[
-            setTimeout(() => {
-                let node;
-                if (typeof aRow == "number") {
-                    node = this.getListItem(aRow);
-                } else {
-                    node = aRow;
-                }
-
-                this.ensureElementIsVisible(node);
-
-                let input = node.querySelector(".textbox-addressingWidget");
-                input.focus();
-            }, 0);
-        ]]></body>
-      </method>
-
-      <property name="firstVisibleRow">
-        <getter><![CDATA[
-            return this.getIndexOfFirstVisibleRow();
-        ]]></getter>
-      </property>
-
-      <method name="createAttendee">
-        <body><![CDATA[
-            let attendee = cal.createAttendee();
-            attendee.id = "";
-            attendee.rsvp = "TRUE";
-            attendee.role = "REQ-PARTICIPANT";
-            attendee.participationStatus = "NEEDS-ACTION";
-            return attendee;
-        ]]></body>
-      </method>
-
-      <property name="ratio">
-        <setter><![CDATA[
-            let rowcount = this.getRowCount();
-            this.scrollToIndex(Math.floor(rowcount * val));
-            return val;
-        ]]></setter>
-      </property>
-
-      <method name="returnHit">
-        <parameter name="element"/>
-        <parameter name="noAdvance"/>
-        <body><![CDATA[
-            const { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm");
-
-            function parseHeaderValue(aMsgIAddressObject) {
-                if (aMsgIAddressObject.name.match(/[<>@,]/)) {
-                    // special handling only needed for a name with a comma which are not already quoted
-                    return (aMsgIAddressObject.name.match(/^".*"$/)
-                            ? aMsgIAddressObject.name
-                            : '"' + aMsgIAddressObject.name + '"'
-                           ) + " <" + aMsgIAddressObject.email + ">";
-                } else {
-                    return aMsgIAddressObject.toString();
-                }
-            }
-
-            let arrowLength = 1;
-            if (element.value.includes(",") || element.value.match(/^[^"].*[<>@,].*[^"] <.+@.+>$/)) {
-                let strippedAddresses = element.value.replace(/.* >> /, "");
-                let addresses = MailServices.headerParser.makeFromDisplayAddress(strippedAddresses);
-                element.value = parseHeaderValue(addresses[0]);
-
-                // the following code is needed to split attendees, if the user enters a comma
-                // separated list of attendees without using autocomplete functionality
-                let insertAfterItem = this.getListItem(this.getRowByInputElement(element));
-                for (let key in addresses) {
-                    if (key > 0) {
-                        insertAfterItem = this.appendNewRow(false, insertAfterItem);
-                        let textinput = this.getInputFromListitem(insertAfterItem);
-                        textinput.value = parseHeaderValue(addresses[key]);
-                    }
-                }
-                arrowLength = addresses.length;
-            }
-
-            if (!noAdvance) {
-                this.arrowHit(element, arrowLength);
-            }
-        ]]></body>
-      </method>
-
-      <method name="arrowHit">
-        <parameter name="aElement"/>
-        <parameter name="aDirection"/>
-        <body><![CDATA[
-            let row = this.getRowByInputElement(aElement) + aDirection;
-            if (row) {
-                if (row > this.mMaxAttendees) {
-                    this.appendNewRow(true);
-                } else {
-                    let input = this.getInputElement(row);
-                    if (input.hasAttribute("disabled")) {
-                        return;
-                    }
-                    this.setFocus(row);
-                }
-                let event = document.createEvent("Events");
-                event.initEvent("rowchange", true, false);
-                event.details = row;
-                this.dispatchEvent(event);
-            }
-        ]]></body>
-      </method>
-
-      <method name="deleteHit">
-        <parameter name="aElement"/>
-        <body><![CDATA[
-            // don't delete the row if only the organizer is remaining
-            if (this.mMaxAttendees <= 1) {
-                return;
-            }
-
-            let row = this.getRowByInputElement(aElement);
-            this.deleteRow(row);
-            if (row > 0) {
-                row = row - 1;
-            }
-            this.setFocus(row);
-            this.onModify();
-
-            let event = document.createEvent("Events");
-            event.initEvent("rowchange", true, false);
-            event.details = row;
-            this.dispatchEvent(event);
-        ]]></body>
-      </method>
-
-      <method name="deleteRow">
-        <parameter name="aRow"/>
-        <body><![CDATA[
-            this.removeRow(aRow);
-        ]]></body>
-      </method>
-
-      <method name="removeRow">
-        <parameter name="aRow"/>
-        <body><![CDATA[
-            this.getListItem(aRow).remove();
-            this.fitDummyRows();
-            this.mMaxAttendees--;
-        ]]></body>
-      </method>
-    </implementation>
-
-    <handlers>
-      <handler event="click" button="0"><![CDATA[
-          function cycle(values, current) {
-              let nextIndex = (values.indexOf(current) + 1) % values.length;
-              return values[nextIndex];
-          }
-
-          let target = event.originalTarget;
-          if (target.classList.contains("role-icon")) {
-              if (target.getAttribute("disabled") != "true") {
-                  const roleCycle = [
-                      "REQ-PARTICIPANT", "OPT-PARTICIPANT",
-                      "NON-PARTICIPANT", "CHAIR"
-                  ];
-
-                  let nextValue = cycle(roleCycle, target.getAttribute("role"));
-                  target.setAttribute("role", nextValue);
-                  this.updateTooltip(target);
-              }
-          } else if (target.classList.contains("status-icon")) {
-              if (target.getAttribute("disabled") != "true") {
-                  const statusCycle = ["ACCEPTED", "DECLINED", "TENTATIVE"];
-
-                  let nextValue = cycle(statusCycle, target.getAttribute("status"));
-                  target.setAttribute("status", nextValue);
-                  this.updateTooltip(target);
-              }
-          } else if (target.classList.contains("usertype-icon")) {
-              let row = target.closest("richlistitem");
-              let inputField = row.querySelector(".textbox-addressingWidget");
-              if (target.getAttribute("disabled") != "true" &&
-                  !inputField.attendee.isOrganizer) {
-                  const cutypeCycle = ["INDIVIDUAL", "GROUP", "RESOURCE", "ROOM"];
-
-                  let nextValue = cycle(cutypeCycle, target.getAttribute("cutype"));
-                  target.setAttribute("cutype", nextValue);
-                  this.updateTooltip(target);
-              }
-          } else if (this.mIsReadOnly || this.mIsInvitation || target == null || target.closest("richlistitem")) {
-              // These are cases where we don't want to append a new row, keep
-              // them here so we can put the rest in the else case.
-          } else {
-              let lastInput = this.getInputElement(this.mMaxAttendees);
-              if (lastInput && lastInput.value) {
-                  this.appendNewRow(true);
-              }
-          }
-      ]]></handler>
-
-      <handler event="popupshown"><![CDATA[
-          this.mPopupOpen = true;
-      ]]></handler>
-
-      <handler event="popuphidden"><![CDATA[
-          this.mPopupOpen = false;
-      ]]></handler>
-
-      <handler event="keydown"><![CDATA[
-          if (this.mIsReadOnly || this.mIsInvitation) {
-              return;
-          }
-          if (event.originalTarget.localName == "input") {
-              switch (event.key) {
-                  case "Delete":
-                  case "Backspace": {
-                      let curRowId = this.getRowByInputElement(event.originalTarget);
-                      let allSelected = (event.originalTarget.textLength ==
-                                         event.originalTarget.selectionEnd -
-                                         event.originalTarget.selectionStart);
-
-                      if (!event.originalTarget.value ||
-                          event.originalTarget.textLength < 2 ||
-                          allSelected) {
-                          // if the user selected the entire attendee string, only one character was
-                          // left or the row was already empty before hitting the key, we remove the
-                          //  entire row to assure the attendee is deleted
-                          this.deleteHit(event.originalTarget);
-
-                          // if the last row was removed, we append an empty one which has the focus
-                          // to enable adding a new attendee directly with freebusy information cleared
-                          let targetRowId = (event.key == "Backspace" && curRowId > 2)
-                                            ? curRowId - 1 : curRowId;
-                          if (this.mMaxAttendees == 1) {
-                              this.appendNewRow(true);
-                          } else {
-                              this.setFocus(targetRowId);
-                          }
-
-                          // set cursor to begin or end of focused input box based on deletion direction
-                          let cPos = 0;
-                          let input = this.getListItem(targetRowId).querySelector(".textbox-addressingWidget");
-                          if (targetRowId != curRowId) {
-                              cPos = input.textLength;
-                          }
-                          input.setSelectionRange(cPos, cPos);
-                      }
-
-                      event.stopPropagation();
-                      break;
-                  }
-              }
-          }
-      ]]></handler>
-
-      <handler event="keypress" phase="capturing"><![CDATA[
-          // In case we're currently showing the autocompletion popup
-          // don't care about keypress-events and let them go. Otherwise
-          // this event indicates the user wants to travel between
-          // the different attendees. In this case we set the focus
-          // appropriately and stop the event propagation.
-          if (this.mPopupOpen || this.mIsReadOnly || this.mIsInvitation) {
-              return;
-          }
-          if (event.originalTarget.localName == "input") {
-              switch (event.key) {
-                  case "ArrowUp":
-                      this.arrowHit(event.originalTarget, -1);
-                      event.stopPropagation();
-                      break;
-                  case "ArrowDown":
-                      this.arrowHit(event.originalTarget, 1);
-                      event.stopPropagation();
-                      break;
-                  case "Tab":
-                      this.arrowHit(event.originalTarget, event.shiftKey ? -1 : +1);
-                      break;
-              }
-          }
-      ]]></handler>
-    </handlers>
-  </binding>
 
   <!-- the 'selection-bar' binding implements the vertical bar that provides
        a visual indication for the time range the event is configured for. -->
   <binding id="selection-bar">
     <content>
       <xul:scrollbox anonid="scrollbox" width="0" orient="horizontal" flex="1">
         <xul:box class="selection-bar" anonid="selection-bar">
           <xul:box class="selection-bar-left" anonid="leftbox"/>
--- a/calendar/base/content/dialogs/calendar-event-dialog-attendees.xul
+++ b/calendar/base/content/dialogs/calendar-event-dialog-attendees.xul
@@ -67,17 +67,17 @@
     </menulist>
     <toolbarbutton id="zoom-in-button"
                    class="zoom-in-icon"
                    oncommand="zoomWithButtons(false);"/>
   </hbox>
   <hbox flex="1">
     <vbox id="attendees-container" flex="1" width="250" persist="width">
       <box class="attendee-spacer-top"/>
-      <attendees-list flex="1" id="attendees-list" class="listbox-noborder">
+      <calendar-event-attendees-list flex="1" id="attendees-list" class="listbox-noborder">
         <richlistitem class="addressingWidgetItem" allowevents="true">
           <hbox class="addressingWidgetCell" width="27" align="center" pack="center">
             <image class="status-icon"/>
           </hbox>
           <hbox class="addressingWidgetCell" width="16">
             <image class="usertype-icon"/>
           </hbox>
           <hbox class="addressingWidgetCell" flex="1">
@@ -87,24 +87,24 @@
                      autocompletesearch="addrbook ldap"
                      autocompletesearchparam="{}"
                      timeout="300"
                      maxrows="4"
                      completedefaultindex="true"
                      forcecomplete="true"
                      completeselectedindex="true"
                      minresultsforpopup="1"
-                     onblur="if (this.localName == 'textbox') { this.closest(&quot;attendees-list&quot;).returnHit(this, true); }"
+                     onblur="if (this.localName == 'textbox') { this.closest(&quot;calendar-event-attendees-list&quot;).returnHit(this, true) }"
                      ignoreblurwhilesearching="true"
                      oninput="this.setAttribute('dirty', 'true');"
-                     ontextentered="this.closest(&quot;attendees-list&quot;).returnHit(this);">
+                     ontextentered="this.closest(&quot;calendar-event-attendees-list&quot;).returnHit(this);">
             </textbox>
           </hbox>
         </richlistitem>
-      </attendees-list>
+      </calendar-event-attendees-list>
       <box class="attendee-spacer-bottom"/>
     </vbox>
     <splitter id="splitter"/>
     <vbox id="freebusy-container" width="750" persist="width">
       <stack flex="1">
         <vbox flex="1">
           <calendar-event-freebusy-timebar id="timebar"
                                            class="listbox-noborder"
--- a/calendar/base/content/dialogs/calendar-event-dialog.css
+++ b/calendar/base/content/dialogs/calendar-event-dialog.css
@@ -7,21 +7,16 @@
 }
 
 .daypicker-monthday {
     -moz-user-focus: normal;
 }
 
 /****************************************************************************************/
 
-attendees-list {
-    -moz-binding: url(chrome://calendar/content/calendar-event-dialog-attendees.xml#attendees-list);
-    -moz-user-focus: normal;
-}
-
 selection-bar {
     -moz-binding: url(chrome://calendar/content/calendar-event-dialog-attendees.xml#selection-bar);
     -moz-user-focus: normal;
 }
 
 /****************************************************************************************/
 
 scroll-container {
--- a/calendar/base/themes/common/dialogs/calendar-event-dialog.css
+++ b/calendar/base/themes/common/dialogs/calendar-event-dialog.css
@@ -501,17 +501,17 @@ freebusy-day > box {
     -moz-user-focus: none;
 }
 
 #typecol-addressingWidget {
     min-width: 9em;
     border-right: 1px solid var(--eventWidgetBorderColor);
 }
 
-/* This applies to rows of the attendee-list and the freebusy-grid */
+/* This applies to rows of the calendar-event-attendees-list and the freebusy-grid */
 .addressingWidgetItem,
 .dummy-row {
     border: none !important;
     background-color: inherit !important;
     color: inherit !important;
 
     /* we set the minimal height to the height of the
      largest icon [the usertype-icon in this case] to
@@ -616,16 +616,44 @@ freebusy-day > box {
 #content-frame {
     border-left: 1px solid ThreeDDarkShadow;
     border-right: 1px solid ThreeDLightShadow;
     min-width: 10px;
     min-height: 10px;
     height: 400px;
 }
 
+#attendees-list {
+    -moz-user-focus: normal;
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+}
+
+#attendees-list > richlistitem {
+    max-height: 17px;
+    min-height: 17px;
+    flex: 1;
+    display: flex;
+}
+
+#attendees-list .addressingWidgetCell {
+    width: 27px;
+    text-align: center;
+}
+
+#attendees-list hbox:nth-child(3) {
+    flex: 1;
+    text-align: left;
+}
+
+#attendees-list hbox textbox {
+    width: 100%;
+}
+
 .attendees-list-listbox > listboxbody {
     overflow-y: hidden !important;
 }
 
 .selection-bar-left {
     width: 3px;
     cursor: w-resize;
 }