<commit_before>import { createElement } from "lwc";
import SingleLookup from "c/singleLookup";
import SingleLookupOption from "../../singleLookupOption/singleLookupOption";

const ICON_VM = {
    alternativeText: "An error utility",
    icon: "utility:error",
    label: "title1",
    value: "1",
};

const AVATAR_VM = {
    alternativeText: "An error utility",
    icon: "utility:error",
    initials: "AA",
    imageUrl: "https://www.salesforce.com",
    label: "Adam Advisor",
    meta: "meta1",
    value: "00500000012300a",
    active: false,
};

const LABEL = "My Lookup Field";
const REMOVE_LABEL = "Remove Me";
const PLACEHOLDER = "Search here";

const OPTIONS_EMPTY = [];
const OPTIONS_TWO = [ICON_VM, AVATAR_VM];

jest.useFakeTimers();

describe("c-single-lookup", () => {
    afterEach(() => {
        // The jsdom instance is shared across test cases in a single file so reset the DOM
        while (document.body.firstChild) {
            document.body.removeChild(document.body.firstChild);
        }
    });

    function flushPromises() {
        // eslint-disable-next-line no-undef
        return new Promise((resolve) => setImmediate(resolve));
    }

    //INITIAL LOAD
    it("renders in an empty state with no value", () => {
        const element = createElement("c-single-lookup", {
            is: SingleLookup,
        });
        element.label = LABEL;
        element.required = true;
        element.placeholder = PLACEHOLDER;
        element.removeLabel = REMOVE_LABEL;
        document.body.appendChild(element);

        return Promise.resolve().then(() => {
            expect(element).toBeAccessible();
            //rendered: label, input, decorative search icon
            const formLabel = element.shadowRoot.querySelector("label");
            const requiredLabel = "*" + LABEL;
            expect(formLabel.textContent).toBe(requiredLabel);
            const input = element.shadowRoot.querySelector("input");
            expect(input.placeholder).toEqual(PLACEHOLDER);
            const lookupIcon = element.shadowRoot.querySelector("lightning-icon");
            expect(lookupIcon).not.toBeNull();
            const listbox = element.shadowRoot.querySelector(".slds-dropdown");
            expect(listbox).toBeNull();

            //not rendered: avatar, close button, search line
            const avatar = element.shadowRoot.querySelector("lightning-avatar");
            expect(avatar).toBeNull();
            const closeButton = element.shadowRoot.querySelector("button");
            expect(closeButton).toBeNull();
            const searchLine = element.shadowRoot.querySelector(".showSearchLine");
            expect(searchLine).toBeNull();
        });
    });

    it("renders in a populated state with a selected value", () => {
        const element = createElement("c-single-lookup", {
            is: SingleLookup,
        });
        element.label = LABEL;
        element.required = false;
        element.placeholder = PLACEHOLDER;
        element.removeLabel = REMOVE_LABEL;
        element.value = AVATAR_VM;
        document.body.appendChild(element);

        return Promise.resolve().then(() => {
            expect(element).toBeAccessible();
            const comboBox = element.shadowRoot.querySelector(".slds-combobox");
            expect(comboBox).not.toBeNull();
            //rendered: label, avatar, close button
            const formLabel = element.shadowRoot.querySelector("label");
            expect(formLabel.textContent).toBe(LABEL);

            const avatar = element.shadowRoot.querySelector("lightning-avatar");
            expect(avatar).not.toBeNull();

            const closeButton = element.shadowRoot.querySelector("button");
            expect(closeButton.title).toEqual(REMOVE_LABEL);

            //not rendered: search, dropdown
            const searchIcon = element.shadowRoot.querySelector(".searchIcon");
            expect(searchIcon).toBeNull();
            const searchBox = element.shadowRoot.querySelector(
                ".slds-dropdown slds-dropdown_length-with-icon-7 slds-dropdown_fluid"
            );
            expect(searchBox).toBeNull();
        });
    });

    //UI INTERACTION
    it("clicking clear button removes the selected option", () => {
        const element = createElement("c-single-lookup", {
            is: SingleLookup,
        });
        element.label = LABEL;
        element.required = false;
        element.placeholder = PLACEHOLDER;
        element.removeLabel = REMOVE_LABEL;
        element.value = AVATAR_VM;
        document.body.appendChild(element);

        const handler = jest.fn();
        element.addEventListener("inputchange", handler);

        return Promise.resolve()
            .then(() => {
                const avatarOnLoad = element.shadowRoot.querySelector("lightning-avatar");
                expect(avatarOnLoad).not.toBeNull();
                const closeButton = element.shadowRoot.querySelector("button");
                closeButton.click();
            })
            .then(() => {
                const avatarAfterClear = element.shadowRoot.querySelector("lightning-avatar");
                expect(avatarAfterClear).toBeNull();
                const closeButton = element.shadowRoot.querySelector("button");
                expect(closeButton).toBeNull();
                expect(handler).toHaveBeenCalledTimes(1);
                expect(handler.mock.calls[0][0].detail.value).toBeFalsy();
            });
    });

    it("supports keyboard navigation", () => {
        const element = createElement("c-single-lookup", {
            is: SingleLookup,
        });
        document.body.appendChild(element);
        element.setOptions(OPTIONS_TWO);
        const handler = jest.fn();
        element.addEventListener("inputchange", handler);
        const input = element.shadowRoot.querySelector("input");

        return Promise.resolve()
            .then(() => {
                const input = element.shadowRoot.querySelector("input");
                input.value = "abcd";
                input.dispatchEvent(new CustomEvent("input"));
            })
            .then(() => {
                const options = element.shadowRoot.querySelectorAll("c-single-lookup-option");
                expect(options).toHaveLength(2);
                input.focus();
                input.dispatchEvent(new KeyboardEvent("keydown", { keyCode: 40 }));
            })
            .then(() => {
                //expect the first element to be selected
                input.dispatchEvent(new KeyboardEvent("keydown", { keyCode: 40 }));
                const options = element.shadowRoot.querySelectorAll("c-single-lookup-option");
                expect(options[0].shadowRoot.querySelector(".slds-has-focus")).toBeTruthy();
            })
            .then(() => {
                //expect the second element to be selected
                input.dispatchEvent(new KeyboardEvent("keydown", { keyCode: 40 }));
                const options = element.shadowRoot.querySelectorAll("c-single-lookup-option");
                expect(options[1].shadowRoot.querySelector(".slds-has-focus")).toBeTruthy();
                expect(options[0].shadowRoot.querySelector(".slds-has-focus")).toBeFalsy();
            })
            .then(() => {
                //expect to loop and be on the 1st option
                input.dispatchEvent(new KeyboardEvent("keydown", { keyCode: 38 }));
                const options = element.shadowRoot.querySelectorAll("c-single-lookup-option");
                expect(options[0].shadowRoot.querySelector(".slds-has-focus")).toBeTruthy();
                expect(options[0].shadowRoot.querySelector(".slds-has-blur")).toBeFalsy();
            })
            .then(() => {
                //hit enter key to select 2nd option (finally!)
                input.dispatchEvent(new KeyboardEvent("keydown", { keyCode: 13 }));
            })
            .then(() => {
                //options are no longer visible - value has been selected
                const options = element.shadowRoot.querySelectorAll("c-single-lookup-option");
                expect(options).toHaveLength(0);
                const avatar = element.shadowRoot.querySelector("lightning-avatar");
                expect(avatar).not.toBeNull();
                expect(handler).toHaveBeenCalledTimes(1);
                expect(handler.mock.calls[0][0].detail.value).toEqual(OPTIONS_TWO[1]);
            });
    });

    it("returns focus to input element when an option is clicked", () => {
        const element = createElement("c-single-lookup", {
            is: SingleLookup,
        });
        document.body.appendChild(element);
        element.setOptions(OPTIONS_TWO);
        const handler = jest.fn();
        const input = element.shadowRoot.querySelector("input");
        input.addEventListener("focus", handler);

        return Promise.resolve().then(() => {
            const listbox = element.shadowRoot.querySelector("div.slds-dropdown");
            listbox.dispatchEvent(new CustomEvent("mousedown"));
            expect(handler).not.toHaveBeenCalled();
            listbox.dispatchEvent(new CustomEvent("mouseup"));
            expect(handler).toHaveBeenCalled();
        });
    });

    it("clears options after esc key", () => {
        const element = createElement("c-single-lookup", {
            is: SingleLookup,
        });
        document.body.appendChild(element);
        element.setOptions(OPTIONS_TWO);
        const handler = jest.fn();
        element.addEventListener("inputchange", handler);
        const input = element.shadowRoot.querySelector("input");

        return Promise.resolve().then(() => {
            //esc key
            input.dispatchEvent(new KeyboardEvent("keydown", { keyCode: 27 }));
            expect(input.value).toBe("");
        });
    });

    //EVENT INTERACTION
    it("searches based on input text and returns NO results", () => {
        const element = createElement("c-single-lookup", {
            is: SingleLookup,
        });
        document.body.appendChild(element);
        const handler = jest.fn();
        element.addEventListener("search", handler);

        return Promise.resolve()
            .then(() => {
                const input = element.shadowRoot.querySelector("input");
                input.value = "abcd";
                input.dispatchEvent(new CustomEvent("input"));
                jest.runOnlyPendingTimers();
                input.focus();
                const keyDown = element.shadowRoot.querySelector("onkeydown");
                expect(keyDown).toBeNull();
                input.dispatchEvent(new KeyboardEvent("keydown", { keyCode: 13 }));
                expect(input.Value).toBeUndefined();
            })
            .then(() => {
                expect(handler).toHaveBeenCalledTimes(1);
                expect(handler.mock.calls[0][0].detail.inputValue).toBe("abcd");
            })
            .then(() => {
                element.setOptions(OPTIONS_EMPTY);
            })
            .then(() => {
                //input text is retained and no options are visible
                const options = element.shadowRoot.querySelectorAll("c-single-lookup-option");
                expect(options).toHaveLength(0);
            });
    });

    it("searches based on input text and returns results", () => {
        const element = createElement("c-single-lookup", {
            is: SingleLookup,
        });
        document.body.appendChild(element);
        const handler = jest.fn();
        element.addEventListener("search", handler);

        return Promise.resolve()
            .then(() => {
                const input = element.shadowRoot.querySelector("input");
                input.value = "Advisor";
                input.dispatchEvent(new CustomEvent("input"));
                jest.runOnlyPendingTimers();
            })
            .then(() => {
                expect(handler).toHaveBeenCalledTimes(1);
                expect(handler.mock.calls[0][0].detail.inputValue).toBe("Advisor");
            })
            .then(() => {
                element.setOptions(OPTIONS_TWO);
            })
            .then(() => {
                const options = element.shadowRoot.querySelectorAll("c-single-lookup-option");
                expect(options).toHaveLength(2);
            });
    });

    it("clears the searchbar and options when searching an empty string", () => {
        const element = createElement("c-single-lookup", {
            is: SingleLookup,
        });
        document.body.appendChild(element);

        return Promise.resolve()
            .then(() => {
                element.setOptions(OPTIONS_TWO);
            })
            .then(() => {
                const input = element.shadowRoot.querySelector("input");

                input.value = "";
                input.dispatchEvent(new CustomEvent("input"));
                jest.runOnlyPendingTimers();
            })
            .then(() => {
                const options = element.shadowRoot.querySelectorAll("c-single-lookup-option");
                expect(options).toHaveLength(0);
            });
    });

    it("prevents loss of focus when locked", () => {
        const element = createElement("c-single-lookup", {
            is: SingleLookup,
        });
        document.body.appendChild(element);
        element.setOptions(OPTIONS_TWO);

        const input = element.shadowRoot.querySelector("input");

        return Promise.resolve()
            .then(() => {
                input.focus();
            })
            .then(() => {
                expect(input.classList).toContain("slds-has-focus");
                const listbox = element.shadowRoot.querySelector("div.slds-dropdown");
                listbox.dispatchEvent(new CustomEvent("mousedown"));
            })
            .then(() => {
                input.blur();
            })
            .then(() => {
                expect(input.classList).toContain("slds-has-focus");
                const listbox = element.shadowRoot.querySelector("div.slds-dropdown");
                listbox.dispatchEvent(new CustomEvent("mouseup"));
            })
            .then(() => {
                input.blur();
            })
            .then(() => {
                expect(input.classList).not.toContain("slds-has-focus");
            });
    });

    it("should not display options when null is passed in", () => {
        const element = createElement("c-single-lookup", {
            is: SingleLookup,
        });
        document.body.appendChild(element);
        element.setOptions(null);

        return Promise.resolve().then(() => {
            const listbox = element.shadowRoot.querySelector("div.slds-dropdown");
            expect(listbox).toBeFalsy();
        });
    });

    it("does not perform search with limited input", () => {
        const element = createElement("c-single-lookup", {
            is: SingleLookup,
        });
        document.body.appendChild(element);
        const handler = jest.fn();
        element.addEventListener("search", handler);

        return Promise.resolve()
            .then(() => {
                const input = element.shadowRoot.querySelector("input");
                input.value = "Ad";
                input.dispatchEvent(new CustomEvent("input"));
                jest.runOnlyPendingTimers();
            })
            .then(() => {
                expect(handler).toHaveBeenCalledTimes(1);
                expect(handler.mock.calls[0][0].detail.inputValue).toBe("Ad");
            })
            .then(() => {
                const options = element.shadowRoot.querySelectorAll("c-single-lookup-option");
                expect(options).toHaveLength(0);
            });
    });

    //END-TO-END
    it("highlights active option when selected by child component", () => {
        jest.useFakeTimers();
        const element = createElement("c-single-lookup", {
            is: SingleLookup,
        });
        document.body.appendChild(element);

        const handler = jest.fn();
        element.addEventListener("inputchange", handler);

        return Promise.resolve()
            .then(() => {
                const input = element.shadowRoot.querySelector("input");
                input.value = "Advisor";
                input.dispatchEvent(new CustomEvent("input"));
                jest.runOnlyPendingTimers();
            })
            .then(() => {
                element.setOptions(OPTIONS_TWO);
            })
            .then(() => {
                const options = element.shadowRoot.querySelectorAll("c-single-lookup-option");
                expect(options).toHaveLength(2);
                options[0].dispatchEvent(new CustomEvent("optionview", { detail: 0 }));
            })
            .then(() => {
                const options = element.shadowRoot.querySelectorAll("c-single-lookup-option");
                expect(options).toHaveLength(2);
            });
    });

    it("fires change event and clears options when option is selected/confirmed by child", () => {
        const element = createElement("c-single-lookup", {
            is: SingleLookup,
        });
        document.body.appendChild(element);

        const handler = jest.fn();
        element.addEventListener("inputchange", handler);

        return Promise.resolve()
            .then(() => {
                //typing in field
                const input = element.shadowRoot.querySelector("input");
                input.value = "Advisor";
                input.dispatchEvent(new CustomEvent("input"));
                jest.runOnlyPendingTimers();
            })
            .then(() => {
                //simulate typing in field
                element.setOptions(OPTIONS_TWO);
            })
            .then(() => {
                const options = element.shadowRoot.querySelectorAll("c-single-lookup-option");
                expect(options).toHaveLength(2);
                options[1].dispatchEvent(new CustomEvent("optionconfirm", { detail: 1 }));
            })
            .then(() => {
                //options are no longer visible
                const options = element.shadowRoot.querySelectorAll("c-single-lookup-option");
                expect(options).toHaveLength(0);
            })
            .then(() => {
                expect(handler).toHaveBeenCalledTimes(1);
                expect(handler.mock.calls[0][0].detail.value).toEqual(OPTIONS_TWO[1]);
            });
    });
});
<commit_msg>consistent aftereach and flush<commit_after>import { createElement } from "lwc";
import SingleLookup from "c/singleLookup";
import SingleLookupOption from "../../singleLookupOption/singleLookupOption";

const ICON_VM = {
    alternativeText: "An error utility",
    icon: "utility:error",
    label: "title1",
    value: "1",
};

const AVATAR_VM = {
    alternativeText: "An error utility",
    icon: "utility:error",
    initials: "AA",
    imageUrl: "https://www.salesforce.com",
    label: "Adam Advisor",
    meta: "meta1",
    value: "00500000012300a",
    active: false,
};

const LABEL = "My Lookup Field";
const REMOVE_LABEL = "Remove Me";
const PLACEHOLDER = "Search here";

const OPTIONS_EMPTY = [];
const OPTIONS_TWO = [ICON_VM, AVATAR_VM];

jest.useFakeTimers();

describe("c-single-lookup", () => {
    afterEach(() => {
        clearDOM();
    });

    async function flushPromises() {
        return Promise.resolve();
    }

    //INITIAL LOAD
    it("renders in an empty state with no value", () => {
        const element = createElement("c-single-lookup", {
            is: SingleLookup,
        });
        element.label = LABEL;
        element.required = true;
        element.placeholder = PLACEHOLDER;
        element.removeLabel = REMOVE_LABEL;
        document.body.appendChild(element);

        return Promise.resolve().then(() => {
            expect(element).toBeAccessible();
            //rendered: label, input, decorative search icon
            const formLabel = element.shadowRoot.querySelector("label");
            const requiredLabel = "*" + LABEL;
            expect(formLabel.textContent).toBe(requiredLabel);
            const input = element.shadowRoot.querySelector("input");
            expect(input.placeholder).toEqual(PLACEHOLDER);
            const lookupIcon = element.shadowRoot.querySelector("lightning-icon");
            expect(lookupIcon).not.toBeNull();
            const listbox = element.shadowRoot.querySelector(".slds-dropdown");
            expect(listbox).toBeNull();

            //not rendered: avatar, close button, search line
            const avatar = element.shadowRoot.querySelector("lightning-avatar");
            expect(avatar).toBeNull();
            const closeButton = element.shadowRoot.querySelector("button");
            expect(closeButton).toBeNull();
            const searchLine = element.shadowRoot.querySelector(".showSearchLine");
            expect(searchLine).toBeNull();
        });
    });

    it("renders in a populated state with a selected value", () => {
        const element = createElement("c-single-lookup", {
            is: SingleLookup,
        });
        element.label = LABEL;
        element.required = false;
        element.placeholder = PLACEHOLDER;
        element.removeLabel = REMOVE_LABEL;
        element.value = AVATAR_VM;
        document.body.appendChild(element);

        return Promise.resolve().then(() => {
            expect(element).toBeAccessible();
            const comboBox = element.shadowRoot.querySelector(".slds-combobox");
            expect(comboBox).not.toBeNull();
            //rendered: label, avatar, close button
            const formLabel = element.shadowRoot.querySelector("label");
            expect(formLabel.textContent).toBe(LABEL);

            const avatar = element.shadowRoot.querySelector("lightning-avatar");
            expect(avatar).not.toBeNull();

            const closeButton = element.shadowRoot.querySelector("button");
            expect(closeButton.title).toEqual(REMOVE_LABEL);

            //not rendered: search, dropdown
            const searchIcon = element.shadowRoot.querySelector(".searchIcon");
            expect(searchIcon).toBeNull();
            const searchBox = element.shadowRoot.querySelector(
                ".slds-dropdown slds-dropdown_length-with-icon-7 slds-dropdown_fluid"
            );
            expect(searchBox).toBeNull();
        });
    });

    //UI INTERACTION
    it("clicking clear button removes the selected option", () => {
        const element = createElement("c-single-lookup", {
            is: SingleLookup,
        });
        element.label = LABEL;
        element.required = false;
        element.placeholder = PLACEHOLDER;
        element.removeLabel = REMOVE_LABEL;
        element.value = AVATAR_VM;
        document.body.appendChild(element);

        const handler = jest.fn();
        element.addEventListener("inputchange", handler);

        return Promise.resolve()
            .then(() => {
                const avatarOnLoad = element.shadowRoot.querySelector("lightning-avatar");
                expect(avatarOnLoad).not.toBeNull();
                const closeButton = element.shadowRoot.querySelector("button");
                closeButton.click();
            })
            .then(() => {
                const avatarAfterClear = element.shadowRoot.querySelector("lightning-avatar");
                expect(avatarAfterClear).toBeNull();
                const closeButton = element.shadowRoot.querySelector("button");
                expect(closeButton).toBeNull();
                expect(handler).toHaveBeenCalledTimes(1);
                expect(handler.mock.calls[0][0].detail.value).toBeFalsy();
            });
    });

    it("supports keyboard navigation", () => {
        const element = createElement("c-single-lookup", {
            is: SingleLookup,
        });
        document.body.appendChild(element);
        element.setOptions(OPTIONS_TWO);
        const handler = jest.fn();
        element.addEventListener("inputchange", handler);
        const input = element.shadowRoot.querySelector("input");

        return Promise.resolve()
            .then(() => {
                const input = element.shadowRoot.querySelector("input");
                input.value = "abcd";
                input.dispatchEvent(new CustomEvent("input"));
            })
            .then(() => {
                const options = element.shadowRoot.querySelectorAll("c-single-lookup-option");
                expect(options).toHaveLength(2);
                input.focus();
                input.dispatchEvent(new KeyboardEvent("keydown", { keyCode: 40 }));
            })
            .then(() => {
                //expect the first element to be selected
                input.dispatchEvent(new KeyboardEvent("keydown", { keyCode: 40 }));
                const options = element.shadowRoot.querySelectorAll("c-single-lookup-option");
                expect(options[0].shadowRoot.querySelector(".slds-has-focus")).toBeTruthy();
            })
            .then(() => {
                //expect the second element to be selected
                input.dispatchEvent(new KeyboardEvent("keydown", { keyCode: 40 }));
                const options = element.shadowRoot.querySelectorAll("c-single-lookup-option");
                expect(options[1].shadowRoot.querySelector(".slds-has-focus")).toBeTruthy();
                expect(options[0].shadowRoot.querySelector(".slds-has-focus")).toBeFalsy();
            })
            .then(() => {
                //expect to loop and be on the 1st option
                input.dispatchEvent(new KeyboardEvent("keydown", { keyCode: 38 }));
                const options = element.shadowRoot.querySelectorAll("c-single-lookup-option");
                expect(options[0].shadowRoot.querySelector(".slds-has-focus")).toBeTruthy();
                expect(options[0].shadowRoot.querySelector(".slds-has-blur")).toBeFalsy();
            })
            .then(() => {
                //hit enter key to select 2nd option (finally!)
                input.dispatchEvent(new KeyboardEvent("keydown", { keyCode: 13 }));
            })
            .then(() => {
                //options are no longer visible - value has been selected
                const options = element.shadowRoot.querySelectorAll("c-single-lookup-option");
                expect(options).toHaveLength(0);
                const avatar = element.shadowRoot.querySelector("lightning-avatar");
                expect(avatar).not.toBeNull();
                expect(handler).toHaveBeenCalledTimes(1);
                expect(handler.mock.calls[0][0].detail.value).toEqual(OPTIONS_TWO[1]);
            });
    });

    it("returns focus to input element when an option is clicked", () => {
        const element = createElement("c-single-lookup", {
            is: SingleLookup,
        });
        document.body.appendChild(element);
        element.setOptions(OPTIONS_TWO);
        const handler = jest.fn();
        const input = element.shadowRoot.querySelector("input");
        input.addEventListener("focus", handler);

        return Promise.resolve().then(() => {
            const listbox = element.shadowRoot.querySelector("div.slds-dropdown");
            listbox.dispatchEvent(new CustomEvent("mousedown"));
            expect(handler).not.toHaveBeenCalled();
            listbox.dispatchEvent(new CustomEvent("mouseup"));
            expect(handler).toHaveBeenCalled();
        });
    });

    it("clears options after esc key", () => {
        const element = createElement("c-single-lookup", {
            is: SingleLookup,
        });
        document.body.appendChild(element);
        element.setOptions(OPTIONS_TWO);
        const handler = jest.fn();
        element.addEventListener("inputchange", handler);
        const input = element.shadowRoot.querySelector("input");

        return Promise.resolve().then(() => {
            //esc key
            input.dispatchEvent(new KeyboardEvent("keydown", { keyCode: 27 }));
            expect(input.value).toBe("");
        });
    });

    //EVENT INTERACTION
    it("searches based on input text and returns NO results", () => {
        const element = createElement("c-single-lookup", {
            is: SingleLookup,
        });
        document.body.appendChild(element);
        const handler = jest.fn();
        element.addEventListener("search", handler);

        return Promise.resolve()
            .then(() => {
                const input = element.shadowRoot.querySelector("input");
                input.value = "abcd";
                input.dispatchEvent(new CustomEvent("input"));
                jest.runOnlyPendingTimers();
                input.focus();
                const keyDown = element.shadowRoot.querySelector("onkeydown");
                expect(keyDown).toBeNull();
                input.dispatchEvent(new KeyboardEvent("keydown", { keyCode: 13 }));
                expect(input.Value).toBeUndefined();
            })
            .then(() => {
                expect(handler).toHaveBeenCalledTimes(1);
                expect(handler.mock.calls[0][0].detail.inputValue).toBe("abcd");
            })
            .then(() => {
                element.setOptions(OPTIONS_EMPTY);
            })
            .then(() => {
                //input text is retained and no options are visible
                const options = element.shadowRoot.querySelectorAll("c-single-lookup-option");
                expect(options).toHaveLength(0);
            });
    });

    it("searches based on input text and returns results", () => {
        const element = createElement("c-single-lookup", {
            is: SingleLookup,
        });
        document.body.appendChild(element);
        const handler = jest.fn();
        element.addEventListener("search", handler);

        return Promise.resolve()
            .then(() => {
                const input = element.shadowRoot.querySelector("input");
                input.value = "Advisor";
                input.dispatchEvent(new CustomEvent("input"));
                jest.runOnlyPendingTimers();
            })
            .then(() => {
                expect(handler).toHaveBeenCalledTimes(1);
                expect(handler.mock.calls[0][0].detail.inputValue).toBe("Advisor");
            })
            .then(() => {
                element.setOptions(OPTIONS_TWO);
            })
            .then(() => {
                const options = element.shadowRoot.querySelectorAll("c-single-lookup-option");
                expect(options).toHaveLength(2);
            });
    });

    it("clears the searchbar and options when searching an empty string", () => {
        const element = createElement("c-single-lookup", {
            is: SingleLookup,
        });
        document.body.appendChild(element);

        return Promise.resolve()
            .then(() => {
                element.setOptions(OPTIONS_TWO);
            })
            .then(() => {
                const input = element.shadowRoot.querySelector("input");

                input.value = "";
                input.dispatchEvent(new CustomEvent("input"));
                jest.runOnlyPendingTimers();
            })
            .then(() => {
                const options = element.shadowRoot.querySelectorAll("c-single-lookup-option");
                expect(options).toHaveLength(0);
            });
    });

    it("prevents loss of focus when locked", () => {
        const element = createElement("c-single-lookup", {
            is: SingleLookup,
        });
        document.body.appendChild(element);
        element.setOptions(OPTIONS_TWO);

        const input = element.shadowRoot.querySelector("input");

        return Promise.resolve()
            .then(() => {
                input.focus();
            })
            .then(() => {
                expect(input.classList).toContain("slds-has-focus");
                const listbox = element.shadowRoot.querySelector("div.slds-dropdown");
                listbox.dispatchEvent(new CustomEvent("mousedown"));
            })
            .then(() => {
                input.blur();
            })
            .then(() => {
                expect(input.classList).toContain("slds-has-focus");
                const listbox = element.shadowRoot.querySelector("div.slds-dropdown");
                listbox.dispatchEvent(new CustomEvent("mouseup"));
            })
            .then(() => {
                input.blur();
            })
            .then(() => {
                expect(input.classList).not.toContain("slds-has-focus");
            });
    });

    it("should not display options when null is passed in", () => {
        const element = createElement("c-single-lookup", {
            is: SingleLookup,
        });
        document.body.appendChild(element);
        element.setOptions(null);

        return Promise.resolve().then(() => {
            const listbox = element.shadowRoot.querySelector("div.slds-dropdown");
            expect(listbox).toBeFalsy();
        });
    });

    it("does not perform search with limited input", () => {
        const element = createElement("c-single-lookup", {
            is: SingleLookup,
        });
        document.body.appendChild(element);
        const handler = jest.fn();
        element.addEventListener("search", handler);

        return Promise.resolve()
            .then(() => {
                const input = element.shadowRoot.querySelector("input");
                input.value = "Ad";
                input.dispatchEvent(new CustomEvent("input"));
                jest.runOnlyPendingTimers();
            })
            .then(() => {
                expect(handler).toHaveBeenCalledTimes(1);
                expect(handler.mock.calls[0][0].detail.inputValue).toBe("Ad");
            })
            .then(() => {
                const options = element.shadowRoot.querySelectorAll("c-single-lookup-option");
                expect(options).toHaveLength(0);
            });
    });

    //END-TO-END
    it("highlights active option when selected by child component", () => {
        jest.useFakeTimers();
        const element = createElement("c-single-lookup", {
            is: SingleLookup,
        });
        document.body.appendChild(element);

        const handler = jest.fn();
        element.addEventListener("inputchange", handler);

        return Promise.resolve()
            .then(() => {
                const input = element.shadowRoot.querySelector("input");
                input.value = "Advisor";
                input.dispatchEvent(new CustomEvent("input"));
                jest.runOnlyPendingTimers();
            })
            .then(() => {
                element.setOptions(OPTIONS_TWO);
            })
            .then(() => {
                const options = element.shadowRoot.querySelectorAll("c-single-lookup-option");
                expect(options).toHaveLength(2);
                options[0].dispatchEvent(new CustomEvent("optionview", { detail: 0 }));
            })
            .then(() => {
                const options = element.shadowRoot.querySelectorAll("c-single-lookup-option");
                expect(options).toHaveLength(2);
            });
    });

    it("fires change event and clears options when option is selected/confirmed by child", () => {
        const element = createElement("c-single-lookup", {
            is: SingleLookup,
        });
        document.body.appendChild(element);

        const handler = jest.fn();
        element.addEventListener("inputchange", handler);

        return Promise.resolve()
            .then(() => {
                //typing in field
                const input = element.shadowRoot.querySelector("input");
                input.value = "Advisor";
                input.dispatchEvent(new CustomEvent("input"));
                jest.runOnlyPendingTimers();
            })
            .then(() => {
                //simulate typing in field
                element.setOptions(OPTIONS_TWO);
            })
            .then(() => {
                const options = element.shadowRoot.querySelectorAll("c-single-lookup-option");
                expect(options).toHaveLength(2);
                options[1].dispatchEvent(new CustomEvent("optionconfirm", { detail: 1 }));
            })
            .then(() => {
                //options are no longer visible
                const options = element.shadowRoot.querySelectorAll("c-single-lookup-option");
                expect(options).toHaveLength(0);
            })
            .then(() => {
                expect(handler).toHaveBeenCalledTimes(1);
                expect(handler.mock.calls[0][0].detail.value).toEqual(OPTIONS_TWO[1]);
            });
    });
});
<|endoftext|>


<commit_before>(function() {
    'use strict';

    angular
        .module('<%=angularAppName%>')
        .provider('AlertService', AlertService);

    function AlertService () {
        this.toast = false;
        /*jshint validthis: true */
        this.$get = getService;

        this.showAsToast = function(isToast) {
            this.toast = isToast;
        };

        getService.$inject = ['$timeout', '$sce'<% if (enableTranslation) { %>, '$translate'<% } %>];

        function getService ($timeout, $sce<% if (enableTranslation) { %>,$translate<% } %>) {
            var toast = this.toast,
                alertId = 0, // unique id for each alert. Starts from 0.
                alerts = [],
                timeout = 5000; // default timeout

            return {
                factory: factory,
                isToast: isToast,
                add: addAlert,
                closeAlert: closeAlert,
                closeAlertByIndex: closeAlertByIndex,
                clear: clear,
                get: get,
                success: success,
                error: error,
                info: info,
                warning : warning
            };

            function isToast() {
                return toast;
            }

            function clear() {
                alerts = [];
            }

            function get() {
                return alerts;
            }

            function success(msg, params, position) {
                return this.add({
                    type: 'success',
                    msg: msg,
                    params: params,
                    timeout: timeout,
                    toast: toast,
                    position: position
                });
            }

            function error(msg, params, position) {
                return this.add({
                    type: 'danger',
                    msg: msg,
                    params: params,
                    timeout: timeout,
                    toast: toast,
                    position: position
                });
            }

            function warning(msg, params, position) {
                return this.add({
                    type: 'warning',
                    msg: msg,
                    params: params,
                    timeout: timeout,
                    toast: toast,
                    position: position
                });
            }

            function info(msg, params, position) {
                return this.add({
                    type: 'info',
                    msg: msg,
                    params: params,
                    timeout: timeout,
                    toast: toast,
                    position: position
                });
            }

            function factory(alertOptions) {
                var alert = {
                    type: alertOptions.type,
                    msg: $sce.trustAsHtml(alertOptions.msg),
                    id: alertOptions.alertId,
                    timeout: alertOptions.timeout,
                    toast: alertOptions.toast,
                    position: alertOptions.position ? alertOptions.position : 'top right',
                    scoped: alertOptions.scoped,
                    close: function (alerts) {
                        return exports.closeAlert(this.id, alerts);
                    }
                };
                if(!alert.scoped) {
                    alerts.push(alert);
                }
                return alert;
            }

            function addAlert(alertOptions, extAlerts) {
                alertOptions.alertId = alertId++;
            <%_ if (enableTranslation) { _%>
                alertOptions.msg = $translate.instant(alertOptions.msg, alertOptions.params);
            <%_ } _%>
                var that = this;
                var alert = this.factory(alertOptions);
                if (alertOptions.timeout && alertOptions.timeout > 0) {
                    $timeout(function () {
                        that.closeAlert(alertOptions.alertId, extAlerts);
                    }, alertOptions.timeout);
                }
                return alert;
            }

            function closeAlert(id, extAlerts) {
                var thisAlerts = extAlerts ? extAlerts : alerts;
                return this.closeAlertByIndex(thisAlerts.map(function(e) { return e.id; }).indexOf(id), thisAlerts);
            }

            function closeAlertByIndex(index, thisAlerts) {
                return thisAlerts.splice(index, 1);
            }
        }
    }
})();
<commit_msg>fixing exports undefined bug, closing alert now working<commit_after>(function() {
    'use strict';

    angular
        .module('<%=angularAppName%>')
        .provider('AlertService', AlertService);

    function AlertService () {
        this.toast = false;
        /*jshint validthis: true */
        this.$get = getService;

        this.showAsToast = function(isToast) {
            this.toast = isToast;
        };

        getService.$inject = ['$timeout', '$sce'<% if (enableTranslation) { %>, '$translate'<% } %>];

        function getService ($timeout, $sce<% if (enableTranslation) { %>,$translate<% } %>) {
            var toast = this.toast,
                alertId = 0, // unique id for each alert. Starts from 0.
                alerts = [],
                timeout = 5000; // default timeout

            return {
                factory: factory,
                isToast: isToast,
                add: addAlert,
                closeAlert: closeAlert,
                closeAlertByIndex: closeAlertByIndex,
                clear: clear,
                get: get,
                success: success,
                error: error,
                info: info,
                warning : warning
            };

            function isToast() {
                return toast;
            }

            function clear() {
                alerts = [];
            }

            function get() {
                return alerts;
            }

            function success(msg, params, position) {
                return this.add({
                    type: 'success',
                    msg: msg,
                    params: params,
                    timeout: timeout,
                    toast: toast,
                    position: position
                });
            }

            function error(msg, params, position) {
                return this.add({
                    type: 'danger',
                    msg: msg,
                    params: params,
                    timeout: timeout,
                    toast: toast,
                    position: position
                });
            }

            function warning(msg, params, position) {
                return this.add({
                    type: 'warning',
                    msg: msg,
                    params: params,
                    timeout: timeout,
                    toast: toast,
                    position: position
                });
            }

            function info(msg, params, position) {
                return this.add({
                    type: 'info',
                    msg: msg,
                    params: params,
                    timeout: timeout,
                    toast: toast,
                    position: position
                });
            }

            function factory(alertOptions) {
                var alert = {
                    type: alertOptions.type,
                    msg: $sce.trustAsHtml(alertOptions.msg),
                    id: alertOptions.alertId,
                    timeout: alertOptions.timeout,
                    toast: alertOptions.toast,
                    position: alertOptions.position ? alertOptions.position : 'top right',
                    scoped: alertOptions.scoped,
                    close: function (alerts) {
                        return closeAlert(this.id, alerts);
                    }
                };
                if(!alert.scoped) {
                    alerts.push(alert);
                }
                return alert;
            }

            function addAlert(alertOptions, extAlerts) {
                alertOptions.alertId = alertId++;
            <%_ if (enableTranslation) { _%>
                alertOptions.msg = $translate.instant(alertOptions.msg, alertOptions.params);
            <%_ } _%>
                var that = this;
                var alert = this.factory(alertOptions);
                if (alertOptions.timeout && alertOptions.timeout > 0) {
                    $timeout(function () {
                        that.closeAlert(alertOptions.alertId, extAlerts);
                    }, alertOptions.timeout);
                }
                return alert;
            }

            function closeAlert(id, extAlerts) {
                var thisAlerts = extAlerts ? extAlerts : alerts;
                return closeAlertByIndex(thisAlerts.map(function(e) { return e.id; }).indexOf(id), thisAlerts);
            }

            function closeAlertByIndex(index, thisAlerts) {
                return thisAlerts.splice(index, 1);
            }
        }
    }
})();
<|endoftext|>


<commit_before>import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
import { Meteor } from 'meteor/meteor';

import { dbDirectors } from '/db/dbDirectors';
import { inheritedShowLoadingOnSubscribing } from '/client/layout/loading';
import { alertDialog } from '/client/layout/alertDialog';
import { getCurrentUserOwnedStockAmount, paramCompany, paramCompanyId } from './helpers';

inheritedShowLoadingOnSubscribing(Template.companyDirectorList);

Template.companyDirectorList.onCreated(function() {
  this.offset = new ReactiveVar(0);

  this.autorunWithIdleSupport(() => {
    const companyId = paramCompanyId();
    if (companyId) {
      this.subscribe('companyDirector', companyId, this.offset.get());
    }
  });
});

Template.companyDirectorList.helpers({
  company() {
    return paramCompany();
  },
  directorList() {
    const companyId = paramCompanyId();

    return dbDirectors.find({ companyId }, { sort: { stocks: -1, createdAt: 1 } });
  },
  getPercentage(stocks) {
    return Math.round(stocks / paramCompany().totalRelease * 10000) / 100;
  },
  getMessage(message) {
    return message || '無';
  },
  paginationData() {
    return {
      useVariableForTotalCount: 'totalCountOfCompanyDirector',
      dataNumberPerPage: 10,
      offset: Template.instance().offset
    };
  },
  getCurrentUserDirectorMessage() {
    const userId = Meteor.user()._id;
    const companyId = paramCompanyId();

    return dbDirectors.findOne({ companyId, userId }).message;
  },
  isDirectorInVacation(userId) {
    const user = Meteor.users.findOne(userId);

    return user ? user.profile.isInVacation : false;
  }
});

Template.companyDirectorList.events({
  'submit form'(event, templateInstance) {
    event.preventDefault();
    const message = templateInstance.$('[name="message"]').val();
    if (message.length > 100) {
      alertDialog.alert('輸入訊息過長！');
    }
    else if (Meteor.user() && message.length) {
      Meteor.customCall('directorMessage', paramCompanyId(), message);
    }
  }
});
<commit_msg>修正未登入使用者開啟董事會名單時 client 會報錯的問題<commit_after>import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
import { Meteor } from 'meteor/meteor';

import { dbDirectors } from '/db/dbDirectors';
import { inheritedShowLoadingOnSubscribing } from '/client/layout/loading';
import { alertDialog } from '/client/layout/alertDialog';
import { getCurrentUserOwnedStockAmount, paramCompany, paramCompanyId } from './helpers';

inheritedShowLoadingOnSubscribing(Template.companyDirectorList);

Template.companyDirectorList.onCreated(function() {
  this.offset = new ReactiveVar(0);

  this.autorunWithIdleSupport(() => {
    const companyId = paramCompanyId();
    if (companyId) {
      this.subscribe('companyDirector', companyId, this.offset.get());
    }
  });
});

Template.companyDirectorList.helpers({
  company() {
    return paramCompany();
  },
  directorList() {
    const companyId = paramCompanyId();

    return dbDirectors.find({ companyId }, { sort: { stocks: -1, createdAt: 1 } });
  },
  getPercentage(stocks) {
    return Math.round(stocks / paramCompany().totalRelease * 10000) / 100;
  },
  getMessage(message) {
    return message || '無';
  },
  paginationData() {
    return {
      useVariableForTotalCount: 'totalCountOfCompanyDirector',
      dataNumberPerPage: 10,
      offset: Template.instance().offset
    };
  },
  getCurrentUserDirectorMessage() {
    const userId = Meteor.userId();
    const companyId = paramCompanyId();

    return dbDirectors.findOne({ companyId, userId }).message;
  },
  isDirectorInVacation(userId) {
    const user = Meteor.users.findOne(userId);

    return user ? user.profile.isInVacation : false;
  }
});

Template.companyDirectorList.events({
  'submit form'(event, templateInstance) {
    event.preventDefault();
    const message = templateInstance.$('[name="message"]').val();
    if (message.length > 100) {
      alertDialog.alert('輸入訊息過長！');
    }
    else if (Meteor.user() && message.length) {
      Meteor.customCall('directorMessage', paramCompanyId(), message);
    }
  }
});
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';

class SelectableButtonComponent extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            editMode: false
        };
        this.handleToggleEdit = this.handleToggleEdit.bind(this);
        this.handleToggleStatic = this.handleToggleStatic.bind(this);
        this.handleChange = this.handleChange.bind(this);
    }

    handleChange(event) {
        this.props.onSelectId(event.target.value);
    }

    handleToggleEdit() {
        this.setState({ editMode: true });
    }

    handleToggleStatic() {
        this.setState({ editMode: false });
    }

    render() {
        let selectedId = this.props.selectedId();
        let [label, color] = this.props.idToDescriptor(selectedId);

        let options = [];
        if (this.state.editMode) {
            options = this.props.optionsArray.map(o => (
                <option key={o.id} value={o.id}>
                    {this.props.idToDescriptor(o.id)[0]}
                </option>
            ));
        } else {
            options = [
                <option key={selectedId} value={selectedId}>
                    {label}
                </option>
            ];
        }

        let borderColor;
        if (color) {
            borderColor = { borderRight: `5px solid ${color}` };
        }

        return (
            <select
                className="form-control btn-transparent"
                style={borderColor}
                onChange={this.handleChange}
                onClick={this.handleToggleEdit}
                onFocus={this.handleToggleEdit}
                onBlur={this.handleToggleStatic}
                defaultValue={selectedId}>
                {options}
            </select>
        );
    }
}

SelectableButtonComponent.propTypes = {
    // Callback whenever a new option is selected; will be called with the id
    // of the selected option.
    onSelectId: PropTypes.func.isRequired,

    // Which option (referred by id) is selected by default.
    selectedId: PropTypes.func.isRequired,

    // A function mapping option id to descriptors.
    idToDescriptor: PropTypes.func.isRequired,

    // An array of options of the form {id: String}.
    optionsArray: PropTypes.array.isRequired
};

export default SelectableButtonComponent;
<commit_msg>Delete button-select as it is not used anymore<commit_after><|endoftext|>


<commit_before>var {createStore} = require('fluxible/addons'),
  Immutable = require('immutable')

var BikeshedBuilderStore = createStore({
  storeName: 'BikeshedBuilderStore',
  handlers: {
    'ADD_BIKES_TO_BIKESHED_BUILDER': 'addBikes',
    'REMOVE_BIKE_FROM_BIKESHED_BUILDER': 'removeBike',
    'CLOSE_BIKE_PREVIEW': 'closeBikePreview',
    'OPEN_BIKE_PREVIEW': 'openBikePreview'
  },

  initialize: function () {
    this.preview = Immutable.fromJS({bike: null})
    this.bikes = Immutable.OrderedMap()
  },

  openBikePreview: function (name) {
    this.preview = Immutable.fromJS({bike: this.bikes.get(name)})
    this.emitChange()
  },

  closeBikePreview: function () {
    this.preview = Immutable.fromJS({bike: null})
    this.emitChange()
  },

  addBikes: function (newBikeList) {
    var bikes = this.bikes
    newBikeList.forEach(bike => {
      if (!bikes.has(bike.name)) {
        bike.url = URL.createObjectURL(bike.file)
        bikes = bikes.set(bike.name, Immutable.Map(bike))
      }
    })
    if (bikes !== this.bikes) {
      this.bikes = bikes
      this.emitChange()
    }
  },

  removeBike: function (name) {
    var bikes = this.bikes
    if (bikes.has(name)) {
      URL.revokeObjectURL(bikes.get(name).get('url'))
      this.bikes = bikes.delete(name)
      this.emitChange()
    }
  },

  hasBike: function (name) {
    return this.bikes.has(name)
  },

  getBikes: function () {
    return this.bikes.toArray().map(bike => bike.toObject())
  },

  getPreview: function () {
    return this.preview.toJS()
  },

  getState: function () {
    return {
      bikes: this.getBikes(),
      preview: this.getPreview()
    }
  },

  dehydrate: function () {
    return this.getState()
  },

  rehydrate: function (state) {
    this.bikes = Immutable.OrderedMap(state.bikes)
    this.preview = Immutable.Map(state.preview)
  }
})

module.exports = BikeshedBuilderStore
<commit_msg>Add form to BikeshedBuilderStore<commit_after>var {createStore} = require('fluxible/addons'),
  Immutable = require('immutable')

var BikeshedBuilderStore = createStore({
  storeName: 'BikeshedBuilderStore',
  handlers: {
    'ADD_BIKES_TO_BIKESHED_BUILDER': 'addBikes',
    'REMOVE_BIKE_FROM_BIKESHED_BUILDER': 'removeBike',
    'CLOSE_BIKE_PREVIEW': 'closeBikePreview',
    'OPEN_BIKE_PREVIEW': 'openBikePreview',
    'BIKESHED_BUILDER_FORM_CHANGE': 'handleFormChange'
  },

  initialize: function () {
    this.preview = Immutable.fromJS({bike: null})
    this.bikes = Immutable.OrderedMap()
    this.form = this.defaultForm()
  },

  defaultForm: function () {
    return Immutable.fromJS({
      name: {
        errorText: '',
        value: ''
      }
    })
  },

  handleFormChange: function ({name, value}={}) {
    if (this.form.keys(name)) {
      this.form = this.form.updateIn([name, 'value'], () => value)
      this.emitChange()
    }
  },

  openBikePreview: function (name) {
    this.preview = Immutable.fromJS({bike: this.bikes.get(name)})
    this.emitChange()
  },

  closeBikePreview: function () {
    this.preview = Immutable.fromJS({bike: null})
    this.emitChange()
  },

  addBikes: function (newBikeList) {
    var bikes = this.bikes
    newBikeList.forEach(bike => {
      if (!bikes.has(bike.name)) {
        bike.url = URL.createObjectURL(bike.file)
        bikes = bikes.set(bike.name, Immutable.Map(bike))
      }
    })
    if (bikes !== this.bikes) {
      this.bikes = bikes
      this.emitChange()
    }
  },

  removeBike: function (name) {
    var bikes = this.bikes
    if (bikes.has(name)) {
      URL.revokeObjectURL(bikes.get(name).get('url'))
      this.bikes = bikes.delete(name)
      this.emitChange()
    }
  },

  hasBike: function (name) {
    return this.bikes.has(name)
  },

  getForm: function () {
    return this.form.toJS()
  },

  getBikes: function () {
    return this.bikes.toArray().map(bike => bike.toObject())
  },

  getPreview: function () {
    return this.preview.toJS()
  },

  getState: function () {
    return {
      form: this.getForm(),
      bikes: this.getBikes(),
      preview: this.getPreview()
    }
  },

  dehydrate: function () {
    return this.getState()
  },

  rehydrate: function (state) {
    this.bikes = Immutable.OrderedMap(state.bikes)
    this.preview = Immutable.Map(state.preview)
  }
})

module.exports = BikeshedBuilderStore
<|endoftext|>


<commit_before>// Copyright 2007 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview Emoji Palette implementation. This provides a UI widget for
 * choosing an emoji from a palette of possible choices. EmojiPalettes are
 * contained within EmojiPickers.
 *
 * See ../demos/popupemojipicker.html for an example of how to instantiate
 * an emoji picker.
 *
 * Based on goog.ui.ColorPicker (colorpicker.js).
 *
 */

goog.provide('goog.ui.emoji.EmojiPalette');

goog.require('goog.events.Event');
goog.require('goog.events.EventType');
goog.require('goog.net.ImageLoader');
goog.require('goog.ui.Palette');
goog.require('goog.ui.emoji.Emoji');
goog.require('goog.ui.emoji.EmojiPaletteRenderer');

/**
 * A page of emoji to be displayed in an EmojiPicker.
 *
 * @param {Array.<Array>} emoji List of emoji for this page.
  * @param {?string=} opt_urlPrefix Prefix that should be prepended to all URL.
 * @param {goog.ui.PaletteRenderer=} opt_renderer Renderer used to render or
 *     decorate the palette; defaults to {@link goog.ui.PaletteRenderer}.
 * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
 * @extends {goog.ui.Palette}
 * @constructor
 */
goog.ui.emoji.EmojiPalette = function(emoji,
                                      opt_urlPrefix,
                                      opt_renderer,
                                      opt_domHelper) {
  goog.ui.Palette.call(this,
                       null,
                       opt_renderer ||
                       new goog.ui.emoji.EmojiPaletteRenderer(null),
                       opt_domHelper);
  /**
   * All the different emoji that this palette can display. Maps emoji ids
   * (string) to the goog.ui.emoji.Emoji for that id.
   *
   * @type {Object}
   * @private
   */
  this.emojiCells_ = {};

  /**
   * Map of emoji id to index into this.emojiCells_.
   *
   * @type {Object}
   * @private
   */
  this.emojiMap_ = {};

  /**
   * List of the animated emoji in this palette. Each internal array is of type
   * [HTMLDivElement, goog.ui.emoji.Emoji], and represents the palette item
   * for that animated emoji, and the Emoji object.
   *
   * @type {Array.<Array.<HTMLDivElement, goog.ui.emoji.Emoji>>}
   * @private
   */
  this.animatedEmoji_ = [];

  this.urlPrefix_ = opt_urlPrefix || '';

  /**
   * Palette items that are displayed on this page of the emoji picker. Each
   * item is a div wrapped around a div or an img.
   *
   * @type {Array.<HTMLDivElement>}
   * @private
   */
  this.emoji_ = this.getEmojiArrayFromProperties_(emoji);

  this.setContent(this.emoji_);
};
goog.inherits(goog.ui.emoji.EmojiPalette, goog.ui.Palette);

/**
 * Indicates a prefix that should be prepended to all URLs of images in this
 * emojipalette. This provides an optimization if the URLs are long, so that
 * the client does not have to send a long string for each emoji.
 *
 * @type {string}
 * @private
 */
goog.ui.emoji.EmojiPalette.prototype.urlPrefix_ = '';

/**
 * Whether the emoji images have been loaded.
 *
 * @type {boolean}
 * @private
 */
goog.ui.emoji.EmojiPalette.prototype.imagesLoaded_ = false;

/**
 * Image loader for loading animated emoji.
 *
 * @type {goog.net.ImageLoader}
 * @private
 */
goog.ui.emoji.EmojiPalette.prototype.imageLoader_;

/**
 * Helps create an array of emoji palette items from an array of emoji
 * properties. Each element will be either a div with background-image set to
 * a sprite, or an img element pointing directly to an emoji, and all elements
 * are wrapped with an outer div for alignment issues (i.e., this allows
 * centering the inner div).
 *
 * @param {Object} emojiGroup The group of emoji for this page.
 * @return {Array.<HTMLDivElement>} The emoji items.
 * @private
 */
goog.ui.emoji.EmojiPalette.prototype.getEmojiArrayFromProperties_ =
    function(emojiGroup) {
  var emojiItems = [];

  for (var i = 0; i < emojiGroup.length; i++) {
    var url = emojiGroup[i][0];
    var id = emojiGroup[i][1];
    var spriteInfo = emojiGroup[i][2];
    var displayUrl = spriteInfo ? spriteInfo.getUrl() :
                     this.urlPrefix_ + url;

    var item = this.getRenderer().createPaletteItem(
        this.getDomHelper(), id, spriteInfo, displayUrl);
    emojiItems.push(item);

    var emoji = new goog.ui.emoji.Emoji(url, id);
    this.emojiCells_[id] = emoji;
    this.emojiMap_[id] = i;

    // Keep track of sprited emoji that are animated, for later loading.
    if (spriteInfo && spriteInfo.isAnimated()) {
      this.animatedEmoji_.push([item, emoji]);
    }
  }

  // Create the image loader now so that tests can access it before it has
  // started loading images.
  if (this.animatedEmoji_.length > 0) {
    this.imageLoader_ = new goog.net.ImageLoader();
  }

  this.imagesLoaded_ = true;
  return emojiItems;
};

/**
 * Sends off requests for all the animated emoji and replaces their static
 * sprites when the images are done downloading.
 */
goog.ui.emoji.EmojiPalette.prototype.loadAnimatedEmoji = function() {
  if (this.animatedEmoji_.length > 0) {
    for (var i = 0; i < this.animatedEmoji_.length; i++) {
      var paletteItem = this.animatedEmoji_[i][0];
      var emoji = this.animatedEmoji_[i][1];
      var url = this.urlPrefix_ + emoji.getUrl();

      this.imageLoader_.addImage(emoji.getId(), url);
    }

    this.getHandler().listen(this.imageLoader_, goog.events.EventType.LOAD,
        this.handleImageLoad_);
    this.imageLoader_.start();
  }
};

/**
 * Handles image load events from the ImageLoader.
 *
 * @param {goog.events.Event} e The event object.
 * @private
 */
goog.ui.emoji.EmojiPalette.prototype.handleImageLoad_ = function(e) {
  var id = e.target.id;
  var url = e.target.src;
  // Just to be safe, we check to make sure we have an id and src url from
  // the event target, which the ImageLoader sets to an Image object.
  if (id && url) {
    var item = this.emoji_[this.emojiMap_[id]];
    if (item) {
      this.getRenderer().updateAnimatedPaletteItem(item, e.target);
    }
  }
};

/**
 * Returns the image loader that this palette uses. Used for testing.
 *
 * @return {goog.net.ImageLoader} the image loader.
 */
goog.ui.emoji.EmojiPalette.prototype.getImageLoader = function() {
  return this.imageLoader_;
};

/** @inheritDoc */
goog.ui.emoji.EmojiPalette.prototype.disposeInternal = function() {
  goog.ui.emoji.EmojiPalette.superClass_.disposeInternal.call(this);

  if (this.imageLoader_) {
    this.imageLoader_.dispose();
    this.imageLoader_ = null;
  }
  this.animatedEmoji_ = null;
  this.emojiCells_ = null;
  this.emojiMap_ = null;
  this.emoji_ = null;
};

/**
 * Returns a goomoji id from an img or the containing td, or null if none
 * exists for that element.
 *
 * @param {Element} el The element to get the Goomoji id from.
 * @return {?string} A goomoji id from an img or the containing td, or null if
 *     none exists for that element.
 * @private
 */
goog.ui.emoji.EmojiPalette.prototype.getGoomojiIdFromElement_ = function(el) {
  if (!el) {
    return null;
  }

  var item = this.getRenderer().getContainingItem(this, el);
  return item ? item.getAttribute(goog.ui.emoji.Emoji.ATTRIBUTE) : null;
};

/**
 * @return {goog.ui.emoji.Emoji} The currently selected emoji from this palette.
 */
goog.ui.emoji.EmojiPalette.prototype.getSelectedEmoji = function() {
  var elem = /** @type {Element} */ (this.getSelectedItem());
  var goomojiId = this.getGoomojiIdFromElement_(elem);
  return this.emojiCells_[goomojiId];
};

/**
 * @return {number} The number of emoji managed by this palette.
 */
goog.ui.emoji.EmojiPalette.prototype.getNumberOfEmoji = function() {
  return this.emojiCells_.length;
};

/**
 * Returns the index of the specified emoji within this palette.
 *
 * @param {string} id Id of the emoji to look up.
 * @return {number} The index of the specified emoji within this palette.
 */
goog.ui.emoji.EmojiPalette.prototype.getEmojiIndex = function(id) {
  return this.emojiMap_[id];
};
<commit_msg><commit_after>// Copyright 2007 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview Emoji Palette implementation. This provides a UI widget for
 * choosing an emoji from a palette of possible choices. EmojiPalettes are
 * contained within EmojiPickers.
 *
 * See ../demos/popupemojipicker.html for an example of how to instantiate
 * an emoji picker.
 *
 * Based on goog.ui.ColorPicker (colorpicker.js).
 *
 */

goog.provide('goog.ui.emoji.EmojiPalette');

goog.require('goog.events.Event');
goog.require('goog.events.EventType');
goog.require('goog.net.ImageLoader');
goog.require('goog.ui.Palette');
goog.require('goog.ui.emoji.Emoji');
goog.require('goog.ui.emoji.EmojiPaletteRenderer');

/**
 * A page of emoji to be displayed in an EmojiPicker.
 *
 * @param {Array.<Array>} emoji List of emoji for this page.
  * @param {?string=} opt_urlPrefix Prefix that should be prepended to all URL.
 * @param {goog.ui.PaletteRenderer=} opt_renderer Renderer used to render or
 *     decorate the palette; defaults to {@link goog.ui.PaletteRenderer}.
 * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
 * @extends {goog.ui.Palette}
 * @constructor
 */
goog.ui.emoji.EmojiPalette = function(emoji,
                                      opt_urlPrefix,
                                      opt_renderer,
                                      opt_domHelper) {
  goog.ui.Palette.call(this,
                       null,
                       opt_renderer ||
                       new goog.ui.emoji.EmojiPaletteRenderer(null),
                       opt_domHelper);
  /**
   * All the different emoji that this palette can display. Maps emoji ids
   * (string) to the goog.ui.emoji.Emoji for that id.
   *
   * @type {Object}
   * @private
   */
  this.emojiCells_ = {};

  /**
   * Map of emoji id to index into this.emojiCells_.
   *
   * @type {Object}
   * @private
   */
  this.emojiMap_ = {};

  /**
   * List of the animated emoji in this palette. Each internal array is of type
   * [HTMLDivElement, goog.ui.emoji.Emoji], and represents the palette item
   * for that animated emoji, and the Emoji object.
   *
   * @type {Array.<Array.<(HTMLDivElement|goog.ui.emoji.Emoji)>>}
   * @private
   */
  this.animatedEmoji_ = [];

  this.urlPrefix_ = opt_urlPrefix || '';

  /**
   * Palette items that are displayed on this page of the emoji picker. Each
   * item is a div wrapped around a div or an img.
   *
   * @type {Array.<HTMLDivElement>}
   * @private
   */
  this.emoji_ = this.getEmojiArrayFromProperties_(emoji);

  this.setContent(this.emoji_);
};
goog.inherits(goog.ui.emoji.EmojiPalette, goog.ui.Palette);

/**
 * Indicates a prefix that should be prepended to all URLs of images in this
 * emojipalette. This provides an optimization if the URLs are long, so that
 * the client does not have to send a long string for each emoji.
 *
 * @type {string}
 * @private
 */
goog.ui.emoji.EmojiPalette.prototype.urlPrefix_ = '';

/**
 * Whether the emoji images have been loaded.
 *
 * @type {boolean}
 * @private
 */
goog.ui.emoji.EmojiPalette.prototype.imagesLoaded_ = false;

/**
 * Image loader for loading animated emoji.
 *
 * @type {goog.net.ImageLoader}
 * @private
 */
goog.ui.emoji.EmojiPalette.prototype.imageLoader_;

/**
 * Helps create an array of emoji palette items from an array of emoji
 * properties. Each element will be either a div with background-image set to
 * a sprite, or an img element pointing directly to an emoji, and all elements
 * are wrapped with an outer div for alignment issues (i.e., this allows
 * centering the inner div).
 *
 * @param {Object} emojiGroup The group of emoji for this page.
 * @return {Array.<HTMLDivElement>} The emoji items.
 * @private
 */
goog.ui.emoji.EmojiPalette.prototype.getEmojiArrayFromProperties_ =
    function(emojiGroup) {
  var emojiItems = [];

  for (var i = 0; i < emojiGroup.length; i++) {
    var url = emojiGroup[i][0];
    var id = emojiGroup[i][1];
    var spriteInfo = emojiGroup[i][2];
    var displayUrl = spriteInfo ? spriteInfo.getUrl() :
                     this.urlPrefix_ + url;

    var item = this.getRenderer().createPaletteItem(
        this.getDomHelper(), id, spriteInfo, displayUrl);
    emojiItems.push(item);

    var emoji = new goog.ui.emoji.Emoji(url, id);
    this.emojiCells_[id] = emoji;
    this.emojiMap_[id] = i;

    // Keep track of sprited emoji that are animated, for later loading.
    if (spriteInfo && spriteInfo.isAnimated()) {
      this.animatedEmoji_.push([item, emoji]);
    }
  }

  // Create the image loader now so that tests can access it before it has
  // started loading images.
  if (this.animatedEmoji_.length > 0) {
    this.imageLoader_ = new goog.net.ImageLoader();
  }

  this.imagesLoaded_ = true;
  return emojiItems;
};

/**
 * Sends off requests for all the animated emoji and replaces their static
 * sprites when the images are done downloading.
 */
goog.ui.emoji.EmojiPalette.prototype.loadAnimatedEmoji = function() {
  if (this.animatedEmoji_.length > 0) {
    for (var i = 0; i < this.animatedEmoji_.length; i++) {
      var paletteItem = /** @type {Element} */ (this.animatedEmoji_[i][0]);
      var emoji =
          /** @type {goog.ui.emoji.Emoji} */ (this.animatedEmoji_[i][1]);
      var url = this.urlPrefix_ + emoji.getUrl();

      this.imageLoader_.addImage(emoji.getId(), url);
    }

    this.getHandler().listen(this.imageLoader_, goog.events.EventType.LOAD,
        this.handleImageLoad_);
    this.imageLoader_.start();
  }
};

/**
 * Handles image load events from the ImageLoader.
 *
 * @param {goog.events.Event} e The event object.
 * @private
 */
goog.ui.emoji.EmojiPalette.prototype.handleImageLoad_ = function(e) {
  var id = e.target.id;
  var url = e.target.src;
  // Just to be safe, we check to make sure we have an id and src url from
  // the event target, which the ImageLoader sets to an Image object.
  if (id && url) {
    var item = this.emoji_[this.emojiMap_[id]];
    if (item) {
      this.getRenderer().updateAnimatedPaletteItem(item, e.target);
    }
  }
};

/**
 * Returns the image loader that this palette uses. Used for testing.
 *
 * @return {goog.net.ImageLoader} the image loader.
 */
goog.ui.emoji.EmojiPalette.prototype.getImageLoader = function() {
  return this.imageLoader_;
};

/** @inheritDoc */
goog.ui.emoji.EmojiPalette.prototype.disposeInternal = function() {
  goog.ui.emoji.EmojiPalette.superClass_.disposeInternal.call(this);

  if (this.imageLoader_) {
    this.imageLoader_.dispose();
    this.imageLoader_ = null;
  }
  this.animatedEmoji_ = null;
  this.emojiCells_ = null;
  this.emojiMap_ = null;
  this.emoji_ = null;
};

/**
 * Returns a goomoji id from an img or the containing td, or null if none
 * exists for that element.
 *
 * @param {Element} el The element to get the Goomoji id from.
 * @return {?string} A goomoji id from an img or the containing td, or null if
 *     none exists for that element.
 * @private
 */
goog.ui.emoji.EmojiPalette.prototype.getGoomojiIdFromElement_ = function(el) {
  if (!el) {
    return null;
  }

  var item = this.getRenderer().getContainingItem(this, el);
  return item ? item.getAttribute(goog.ui.emoji.Emoji.ATTRIBUTE) : null;
};

/**
 * @return {goog.ui.emoji.Emoji} The currently selected emoji from this palette.
 */
goog.ui.emoji.EmojiPalette.prototype.getSelectedEmoji = function() {
  var elem = /** @type {Element} */ (this.getSelectedItem());
  var goomojiId = this.getGoomojiIdFromElement_(elem);
  return this.emojiCells_[goomojiId];
};

/**
 * @return {number} The number of emoji managed by this palette.
 */
goog.ui.emoji.EmojiPalette.prototype.getNumberOfEmoji = function() {
  return this.emojiCells_.length;
};

/**
 * Returns the index of the specified emoji within this palette.
 *
 * @param {string} id Id of the emoji to look up.
 * @return {number} The index of the specified emoji within this palette.
 */
goog.ui.emoji.EmojiPalette.prototype.getEmojiIndex = function(id) {
  return this.emojiMap_[id];
};
<|endoftext|>


<commit_before>(function(window, angular, undefined) {

  'use strict';

  var controllers = angular.module('clubadm.controllers', []);

  /**
   * Контроллер главной страницы. Перенаправляет пользователя на последний
   * доступный сезон.
   */
  controllers.controller('HomeController', [
    '$state', 'SeasonService',
    function($state, SeasonService) {
      SeasonService.getLatest().then(function(response) {
        $state.go('season.welcome', {
          year: response.data.year
        });
      });
    }]);

  /**
   * Промежуточный контроллер, который загружает информацию о сезоне.
   */
  controllers.controller('SeasonController', [
    '$scope', '$state', '$stateParams', 'SeasonService',
    function($scope, $state, $stateParams, SeasonService) {
      SeasonService.getByYear($stateParams.year).then(function(response) {
        $scope.season = response.data;
      }, function(response) {
        $state.go('home');
      });
    }]);

  /**
   * Контроллер страницы с предложением принять участие в акции.
   */
  controllers.controller('WelcomeController', [
    '$state', '$stateParams', 'UserService',
    function($state, $stateParams, UserService) {
      if (UserService.isUserLoggedIn()) {
        $state.go('season.profile', $stateParams);
      }
    }]);

  /**
   * Контроллер личной страницы участника.
   */
  controllers.controller('ProfileController', [
    '$scope', '$state', '$stateParams', 'UserService', 'SeasonService',
    function($scope, $state, $stateParams, UserService, SeasonService) {
      if (!UserService.isUserLoggedIn()) {
        $state.go('season.welcome', $stateParams);
      }

      SeasonService.getMemberByYear($stateParams.year).then(function(response) {
        $scope.member = response.data;
        $scope.form = angular.copy(response.data);

        // Начинаем грузить чат только если загрузился профиль.
        SeasonService.getMails($stateParams.year).then(function(response) {
          $scope.chat = response.data;
        });
      }, function(response) {
        $scope.form = {};
      });

      /**
       * Написать письмо.
       */
      $scope.sendMail = function(receptient) {
        var year = $scope.season.year;
        if (receptient == 'giftee') {
          var message = $scope.chat.giftee_message;
          $scope.chat.giftee_message = '';
        } else {
          var message = $scope.chat.santa_message;
          $scope.chat.santa_message = '';
        }
        SeasonService.sendMail(year, message, receptient).then(function(response) {
          $scope.chat = response.data;
        });
      };

      /**
       * Отправить подарок.
       */
      $scope.sendGift = function() {
        SeasonService.sendGift($scope.season.year).then(function(response) {
          $scope.member = response.data;
          $scope.season.sent++;
        });
      };

      /**
       * Получить подарок.
       */
      $scope.receiveGift = function() {
        SeasonService.receiveGift($scope.season.year).then(function(response) {
          $scope.member = response.data;
          $scope.season.received++;
        });
      };

      /**
       * Записаться в участники.
       */
      $scope.signUp = function() {
        SeasonService.signUp($scope.season.year, $scope.form).then(function(response) {
          $scope.member = response.data;
          $scope.season.members++;
        }, function(response) {
          alert(response.data);
        });
      };

      /**
       * Отказаться от участия.
       */
      $scope.signOut = function() {
        SeasonService.signOut($scope.season.year).then(function() {
          $scope.member = null;
          $scope.season.members--;
        });
      };
    }]);

})(window, window.angular);
<commit_msg>controllers.js: add error handlers<commit_after>(function(window, angular, undefined) {

  'use strict';

  var controllers = angular.module('clubadm.controllers', []);

  /**
   * Контроллер главной страницы. Перенаправляет пользователя на последний
   * доступный сезон.
   */
  controllers.controller('HomeController', [
    '$state', 'SeasonService',
    function($state, SeasonService) {
      SeasonService.getLatest().then(function(response) {
        $state.go('season.welcome', {
          year: response.data.year
        });
      });
    }]);

  /**
   * Промежуточный контроллер, который загружает информацию о сезоне.
   */
  controllers.controller('SeasonController', [
    '$scope', '$state', '$stateParams', 'SeasonService',
    function($scope, $state, $stateParams, SeasonService) {
      SeasonService.getByYear($stateParams.year).then(function(response) {
        $scope.season = response.data;
      }, function(response) {
        $state.go('home');
      });
    }]);

  /**
   * Контроллер страницы с предложением принять участие в акции.
   */
  controllers.controller('WelcomeController', [
    '$state', '$stateParams', 'UserService',
    function($state, $stateParams, UserService) {
      if (UserService.isUserLoggedIn()) {
        $state.go('season.profile', $stateParams);
      }
    }]);

  /**
   * Контроллер личной страницы участника.
   */
  controllers.controller('ProfileController', [
    '$scope', '$state', '$stateParams', 'UserService', 'SeasonService',
    function($scope, $state, $stateParams, UserService, SeasonService) {
      if (!UserService.isUserLoggedIn()) {
        $state.go('season.welcome', $stateParams);
      }

      SeasonService.getMemberByYear($stateParams.year).then(function(response) {
        $scope.member = response.data;
        $scope.form = angular.copy(response.data);

        // Начинаем грузить чат только если загрузился профиль.
        SeasonService.getMails($stateParams.year).then(function(response) {
          $scope.chat = response.data;
        });
      }, function(response) {
        $scope.form = {};
      });

      /**
       * Написать письмо.
       */
      $scope.sendMail = function(receptient) {
        var year = $scope.season.year;
        if (receptient == 'giftee') {
          var message = $scope.chat.giftee_message;
          $scope.chat.giftee_message = '';
        } else {
          var message = $scope.chat.santa_message;
          $scope.chat.santa_message = '';
        }
        SeasonService.sendMail(year, message, receptient).then(function(response) {
          $scope.chat = response.data;
        }, function(response) {
          alert(response.data);
        });
      };

      /**
       * Отправить подарок.
       */
      $scope.sendGift = function() {
        SeasonService.sendGift($scope.season.year).then(function(response) {
          $scope.member = response.data;
          $scope.season.sent++;
        }, function(response) {
          alert(response.data);
        });
      };

      /**
       * Получить подарок.
       */
      $scope.receiveGift = function() {
        SeasonService.receiveGift($scope.season.year).then(function(response) {
          $scope.member = response.data;
          $scope.season.received++;
        }, function(response) {
          alert(response.data);
        });
      };

      /**
       * Записаться в участники.
       */
      $scope.signUp = function() {
        SeasonService.signUp($scope.season.year, $scope.form).then(function(response) {
          $scope.member = response.data;
          $scope.season.members++;
        }, function(response) {
          alert(response.data);
        });
      };

      /**
       * Отказаться от участия.
       */
      $scope.signOut = function() {
        SeasonService.signOut($scope.season.year).then(function() {
          $scope.member = null;
          $scope.season.members--;
        }, function(response) {
          alert(response.data);
        });
      };
    }]);

})(window, window.angular);
<|endoftext|>