<commit_before>/**
 * Autocomplete dialog code.
 */

PubSub.subscribe('focus', function (action, element) {
    if (action === 'off') {
        if (element === null) {
            App.autocomplete.dialog.close();
        } else if ($(element).attr('class') !== $(App.autocomplete.dialog.searchSelector).attr('class')) {
            App.autocomplete.dialog.close();
        }
    }
});

App.autocomplete.dialog = {
    isActive: false,
    isEmpty: true,
    RESULTS_LIMIT: 5, // only show 5 results at a time
    editor: null,
    qaBtn: null,
    prevFocus: null,
    dialogSelector: ".qt-dropdown",
    contentSelector: ".qt-dropdown-content",
    searchSelector: ".qt-dropdown-search",

    completion: function (e, params) {
        params = params || {};

        if(e.preventDefault) {
            e.preventDefault();
        }

        if(e.stopPropagation) {
            e.stopPropagation();
        }

        var element = params.focusNode || e.target;

        // if it's not an editable element
        // don't trigger anything
        if(!App.autocomplete.isEditable(element)) {
            return false;
        }

        App.autocomplete.cursorPosition = App.autocomplete.getCursorPosition(element);
        var word = App.autocomplete.getSelectedWord({
            element: element
        });

        App.autocomplete.cursorPosition.word = word;

        App.settings.getFiltered("", App.autocomplete.dialog.RESULTS_LIMIT, function (quicktexts) {
            App.autocomplete.quicktexts = quicktexts;

            params.quicktexts = App.autocomplete.quicktexts;

            App.autocomplete.dialog.populate(params);

        });

    },
    create: function () {

        // Create only once in the root of the document
        var container = $('body');

        // Add loading dropdown
        var dialog = $(this.template);
        container.append(dialog);

        //Gmail HACK: set z-index to auto to a parent, otherwise the autocomplete
        //      dropdown will not be displayed with the correct stacking
        dialog.parents('.qz').css('z-index', 'auto');

        // Handle mouse hover and click
        dialog.on('mouseover mousedown', 'li.qt-item', function (e) {
            e.preventDefault();
            e.stopPropagation();

            App.autocomplete.dialog.selectItem($(this).index());
            if (e.type === 'mousedown') {
                App.autocomplete.dialog.selectActive();
                //App.autocomplete.dialog.close();
            }
        });

        dialog.on('keyup', this.searchSelector, function (e) {
            // ignore modifier keys because they manipulate
            if (_.contains([KEY_ENTER, KEY_UP, KEY_DOWN], e.keyCode)) {
                return;
            }

            App.autocomplete.cursorPosition.word.text = $(this).val();

            App.settings.getFiltered(App.autocomplete.cursorPosition.word.text, App.autocomplete.dialog.RESULTS_LIMIT, function (quicktexts) {

                App.autocomplete.quicktexts = quicktexts;
                App.autocomplete.dialog.populate({
                    quicktexts: App.autocomplete.quicktexts
                });

            });
        });

        // when scrolling the element or the page
        // set the autocomplete dialog position

        // TODO we probably don't need this any more
        // because we no longer use position fixed
        /*
        window.addEventListener('scroll', function() {
            App.autocomplete.dialog.setDialogPosition();
        });
        */

    },
    createQaBtn: function() {

        var container = $('body');

        // add the dialog quick access icon
        this.qaBtn = $(this.qaBtnTemplate);
        container.append(this.qaBtn);

        // move the quick access button around
        // to the focused text field
        // the focus event doesn't support bubbling
        container.on('focusin', this.showQaBtn);

        container.on('focusout', this.hideQaBtn);

        this.qaBtn.on('mouseup', function(e) {

            // return the focus to the element focused
            // before clicking the qa button
            App.autocomplete.dialog.prevFocus.focus();

            // position the dialog under the qa button.
            // since the focus node is now the button
            // we have to pass the previous focus (the text node).
            App.autocomplete.dialog.completion(e, {
                focusNode: App.autocomplete.dialog.prevFocus,
                dialogPositionNode: e.target
            });

            $(App.autocomplete.dialog.dialogSelector).addClass('qa-btn-dropdown-show');

        });

    },
    bindKeyboardEvents: function () {
        Mousetrap.bindGlobal('up', function (e) {
            if (App.autocomplete.dialog.isActive) {
                App.autocomplete.dialog.changeSelection('prev');
            }
        });
        Mousetrap.bindGlobal('down', function (e) {
            if (App.autocomplete.dialog.isActive) {
                App.autocomplete.dialog.changeSelection('next');
            }
        });
        Mousetrap.bindGlobal('escape', function (e) {
            if (App.autocomplete.dialog.isActive) {
                App.autocomplete.dialog.close();
                App.autocomplete.focusEditor(App.autocomplete.dialog.editor);

                // restore the previous caret position
                // since we didn't select any quicktext
                var selection = window.getSelection();
                var caretRange = document.createRange();
                caretRange.setStartAfter(App.autocomplete.dialog.focusNode);
                caretRange.collapse(true);
                selection.removeAllRanges();
                selection.addRange(caretRange);
            }
        });
        Mousetrap.bindGlobal('enter', function (e) {
            if (App.autocomplete.dialog.isActive) {
                App.autocomplete.dialog.selectActive();
                App.autocomplete.dialog.close();
                App.autocomplete.focusEditor(App.autocomplete.dialog.editor);
            }
        });

    },
    populate: function (params) {
        params = params || {};

        App.autocomplete.quicktexts = params.quicktexts;
        if (!App.autocomplete.dialog.isActive) {
            App.autocomplete.dialog.show(params);
        }

        // clone the elements
        // so we can safely highlight the matched text
        // without breaking the generated handlebars markup
        var clonedElements = jQuery.extend(true, [], App.autocomplete.quicktexts);

        // highlight found string in element title, body and shortcut
        var searchRe = new RegExp(App.autocomplete.cursorPosition.word.text, 'gi');

        var highlightMatch = function (match) {
            return '' + match + '';
        };

        // only match if we have a search string
        if(App.autocomplete.cursorPosition.word.text) {
            clonedElements.forEach(function (elem) {
                elem.title = elem.title.replace(searchRe, highlightMatch);
                elem.originalBody = elem.body;
                elem.body = elem.body.replace(searchRe, highlightMatch);
                elem.shortcut = elem.shortcut.replace(searchRe, highlightMatch);
            });
        }

        var content = Handlebars.compile(App.autocomplete.dialog.liTemplate)({
            elements: clonedElements
        });

        $(this.contentSelector).html(content);
        App.autocomplete.dialog.isEmpty = false;

        // Set first element active
        App.autocomplete.dialog.selectItem(0);
    },
    show: function (params) {
        params = params || {};

        // get current focused element - the editor
        App.autocomplete.dialog.editor = document.activeElement;

        var selection = window.getSelection();
        var focusNode = selection.focusNode;
        App.autocomplete.dialog.focusNode = focusNode;

        App.autocomplete.dialog.isActive = true;
        App.autocomplete.dialog.isEmpty = true;

        $(this.dialogSelector).addClass('qt-dropdown-show');

        // TODO see why this triggers show
        //$(this.searchSelector).focus();

        $(App.autocomplete.dialog.contentSelector).scrollTop();

        App.autocomplete.dialog.setDialogPosition(params.dialogPositionNode);

        // if we scroll the content element.
        // only if we're positioning next the cursor,
        // not another element.

        // TODO refactor this since we now use position absolute
        // on the dialog
        /*
        if(!params.dialogPositionNode) {

            // remove it just in case we added it previously
            App.autocomplete.dialog.editor.removeEventListener('scroll', App.autocomplete.dialog.setDialogPosition);

            App.autocomplete.dialog.editor.addEventListener('scroll', App.autocomplete.dialog.setDialogPosition);

        }
        */

    },
    setDialogPosition: function(positionNode) {

        if(!App.autocomplete.dialog.isActive) {
            return;
        }

        var dialogMaxHeight = 250;
        var pageHeight = window.innerHeight;
        var scrollTop = $(window).scrollTop();
        var scrollLeft = $(window).scrollLeft();

        //scrollTop += $(App.autocomplete.dialog.editor).scrollTop();
        //scrollLeft += $(App.autocomplete.dialog.editor).scrollLeft();

        var $dialog = $(App.autocomplete.dialog.dialogSelector);

        var dialogMetrics = $dialog.get(0).getBoundingClientRect();

        var topPos = 0;
        var leftPos = 0;

        // in case we want to position the dialog next to
        // another element,
        // not next to the cursor.
        // eg. when we position it next to the qa button.

        var metrics;

        if(positionNode && positionNode.tagName) {

            metrics = JSON.parse(JSON.stringify(positionNode.getBoundingClientRect()));

            leftPos -=  dialogMetrics.width;

        } else {

            // TODO contenteditable has scrolltop in it
            // textarea does not

            metrics = App.autocomplete.cursorPosition.absolute;

        }

        console.log(metrics);

        topPos += metrics.top + metrics.height;
        leftPos += metrics.left + metrics.width;

        // check if we have enough space at the bottom
        // for the maximum dialog height
        if((pageHeight - topPos) > dialogMaxHeight) {

            console.log('bottom', topPos);

            //topPos = topPos - dialogMetrics.height;

            //topPos = topPos - dialogMetrics.height - App.autocomplete.cursorPosition.absolute.height;

        } else {

            console.log('top', topPos);

            //topPos -= dialogMetrics.height;

            if(positionNode && positionNode.tagName) {

            } else {

            }
        }

        //topPos += scrollTop;
        //leftPos += scrollLeft;

        $dialog.css({
            top: topPos,
            left: leftPos
        });

    },
    selectItem: function (index) {
        if (App.autocomplete.dialog.isActive && !App.autocomplete.dialog.isEmpty) {
            var content = $(this.contentSelector);
            var $element = content.children().eq(index);

            content.children()
                .removeClass('active')
                .eq(index);

            $element.addClass('active');
        }
    },
    selectActive: function () {
        if (App.autocomplete.dialog.isActive && !this.isEmpty && App.autocomplete.quicktexts.length) {
            var activeItemId = $(this.contentSelector).find('.active').data('id');
            var quicktext = App.autocomplete.quicktexts.filter(function (quicktext) {
                return quicktext.id === activeItemId;
            })[0];

            App.autocomplete.replaceWith({
                element: App.autocomplete.dialog.editor,
                quicktext: quicktext,
                focusNode: App.autocomplete.dialog.focusNode
            });
        }
    },
    changeSelection: function (direction) {
        var index_diff = direction === 'prev' ? -1 : 1,
            content = $(this.contentSelector),
            elements_count = content.children().length,
            index_active = content.find('.active').index(),
            index_new = Math.max(0, Math.min(elements_count - 1, index_active + index_diff));

        App.autocomplete.dialog.selectItem(index_new);

        // scroll the active element into view
        var $element = content.children().eq(index_new);
        $element.get(0).scrollIntoView();
    },
    // remove dropdown and cleanup
    close: function (callback) {

        if(!App.autocomplete.dialog.isActive) {

            return;

            /*
            if(callback) {
                return callback();
            }
            */

        }

        $(this.dialogSelector).removeClass('qt-dropdown-show');
        $(this.dialogSelector).removeClass('qa-btn-dropdown-show');
        $(this.searchSelector).val('');

        App.autocomplete.dialog.isActive = false;
        App.autocomplete.dialog.isEmpty = null;

        App.autocomplete.dialog.quicktexts = [];
        App.autocomplete.dialog.cursorPosition = null;

    },
    showQaForElement: function(elem) {

        var show = false;

        // if the element is not a textarea
        // input[type=text] or contenteditable
        if($(elem).is('textarea, input[type=text], [contenteditable]')) {
            show = true;
        }

        // if the quick access button is focused/clicked
        if(elem.className.indexOf('gorgias-qa-btn') !== -1) {
            show = false;
        }

        // if the dialog search field is focused
        if(elem.className.indexOf('qt-dropdown-search') !== -1) {
            show = false;
        }

        return show;

    },
    showQaBtn: function(e) {

        // TODO only show the qa button for large textfields

        var textfield = e.target;

        // only show it for valid elements
        if(!App.autocomplete.dialog.showQaForElement(textfield)) {
            return false;
        }

        App.autocomplete.dialog.prevFocus = textfield;

        var qaBtn = App.autocomplete.dialog.qaBtn.get(0);

        // padding from the top-right corner of the textfield
        var padding = 5;

        var metrics = JSON.parse(JSON.stringify(textfield.getBoundingClientRect()));

        metrics.top += $(window).scrollTop();
        metrics.left += $(window).scrollLeft();

        metrics.top += padding;
        metrics.left -= padding;

        // move the quick access button to the right
        // of the textfield
        metrics.left += textfield.offsetWidth - qaBtn.offsetWidth;

        // move the btn using transforms
        // for performance
        var transform = 'translate3d(' + metrics.left + 'px, ' + metrics.top + 'px, 0)';

        qaBtn.style.transform = transform;
        qaBtn.style.msTransform = transform;
        qaBtn.style.mozTransform = transform;
        qaBtn.style.webkitTransform = transform;

        $('body').addClass('gorgias-show-qa-btn');

    },
    hideQaBtn: function() {

        $('body').removeClass('gorgias-show-qa-btn');

    }
};

App.autocomplete.dialog.template = '' +
    '
' +
    '<input type="search" class="qt-dropdown-search" value="" placeholder="Search templates...">' +
    '

' +
    '
' +
    '';

// quick access button for the dialog
App.autocomplete.dialog.qaBtnTemplate = '' +
    '<button class="gorgias-qa-btn"></button>' +
    '';

App.autocomplete.dialog.liTemplate = '' +
    '{{#if elements.length}}' +
    '{{#each elements}}' +
    '
' +
    '{{{title}}}' +
    '{{#if this.shortcut}}' +
    '{{{this.shortcut}}}' +
    '{{/if}}' +
    '{{{body}}}' +
    '
' +
    '{{/each}}' +
    '{{else}}' +
    '
' +
    'No templates found.' +
    '
' +
    '{{/if}}' +
    '';

<commit_msg>Show the dialog only after the markup for the found quicktexts is appended. So we can get the real height, in the dialog positioning method.<commit_after>/**
 * Autocomplete dialog code.
 */

PubSub.subscribe('focus', function (action, element) {
    if (action === 'off') {
        if (element === null) {
            App.autocomplete.dialog.close();
        } else if ($(element).attr('class') !== $(App.autocomplete.dialog.searchSelector).attr('class')) {
            App.autocomplete.dialog.close();
        }
    }
});

App.autocomplete.dialog = {
    isActive: false,
    isEmpty: true,
    RESULTS_LIMIT: 5, // only show 5 results at a time
    editor: null,
    qaBtn: null,
    prevFocus: null,
    dialogSelector: ".qt-dropdown",
    contentSelector: ".qt-dropdown-content",
    searchSelector: ".qt-dropdown-search",

    completion: function (e, params) {

        if(typeof params !== 'object') {
            params = {};
        }

        params = params || {};

        if(e.preventDefault) {
            e.preventDefault();
        }

        if(e.stopPropagation) {
            e.stopPropagation();
        }

        var element = params.focusNode || e.target;

        // if it's not an editable element
        // don't trigger anything
        if(!App.autocomplete.isEditable(element)) {
            return false;
        }

        App.autocomplete.cursorPosition = App.autocomplete.getCursorPosition(element);
        var word = App.autocomplete.getSelectedWord({
            element: element
        });

        App.autocomplete.cursorPosition.word = word;

        App.settings.getFiltered("", App.autocomplete.dialog.RESULTS_LIMIT, function (quicktexts) {
            App.autocomplete.quicktexts = quicktexts;

            params.quicktexts = App.autocomplete.quicktexts;

            App.autocomplete.dialog.populate(params);

        });

    },
    create: function () {

        // Create only once in the root of the document
        var container = $('body');

        // Add loading dropdown
        var dialog = $(this.template);
        container.append(dialog);

        //Gmail HACK: set z-index to auto to a parent, otherwise the autocomplete
        //      dropdown will not be displayed with the correct stacking
        dialog.parents('.qz').css('z-index', 'auto');

        // Handle mouse hover and click
        dialog.on('mouseover mousedown', 'li.qt-item', function (e) {
            e.preventDefault();
            e.stopPropagation();

            App.autocomplete.dialog.selectItem($(this).index());
            if (e.type === 'mousedown') {
                App.autocomplete.dialog.selectActive();
                //App.autocomplete.dialog.close();
            }
        });

        dialog.on('keyup', this.searchSelector, function (e) {
            // ignore modifier keys because they manipulate
            if (_.contains([KEY_ENTER, KEY_UP, KEY_DOWN], e.keyCode)) {
                return;
            }

            App.autocomplete.cursorPosition.word.text = $(this).val();

            App.settings.getFiltered(App.autocomplete.cursorPosition.word.text, App.autocomplete.dialog.RESULTS_LIMIT, function (quicktexts) {

                App.autocomplete.quicktexts = quicktexts;
                App.autocomplete.dialog.populate({
                    quicktexts: App.autocomplete.quicktexts
                });

            });
        });

        // when scrolling the element or the page
        // set the autocomplete dialog position

        // TODO we probably don't need this any more
        // because we no longer use position fixed
        /*
        window.addEventListener('scroll', function() {
            App.autocomplete.dialog.setDialogPosition();
        });
        */

    },
    createQaBtn: function() {

        var container = $('body');

        // add the dialog quick access icon
        this.qaBtn = $(this.qaBtnTemplate);
        container.append(this.qaBtn);

        // move the quick access button around
        // to the focused text field
        // the focus event doesn't support bubbling
        container.on('focusin', this.showQaBtn);

        container.on('focusout', this.hideQaBtn);

        this.qaBtn.on('mouseup', function(e) {

            // return the focus to the element focused
            // before clicking the qa button
            App.autocomplete.dialog.prevFocus.focus();

            // position the dialog under the qa button.
            // since the focus node is now the button
            // we have to pass the previous focus (the text node).
            App.autocomplete.dialog.completion(e, {
                focusNode: App.autocomplete.dialog.prevFocus,
                dialogPositionNode: e.target
            });

            $(App.autocomplete.dialog.dialogSelector).addClass('qa-btn-dropdown-show');

        });

    },
    bindKeyboardEvents: function () {
        Mousetrap.bindGlobal('up', function (e) {
            if (App.autocomplete.dialog.isActive) {
                App.autocomplete.dialog.changeSelection('prev');
            }
        });
        Mousetrap.bindGlobal('down', function (e) {
            if (App.autocomplete.dialog.isActive) {
                App.autocomplete.dialog.changeSelection('next');
            }
        });
        Mousetrap.bindGlobal('escape', function (e) {
            if (App.autocomplete.dialog.isActive) {
                App.autocomplete.dialog.close();
                App.autocomplete.focusEditor(App.autocomplete.dialog.editor);

                // restore the previous caret position
                // since we didn't select any quicktext
                var selection = window.getSelection();
                var caretRange = document.createRange();
                caretRange.setStartAfter(App.autocomplete.dialog.focusNode);
                caretRange.collapse(true);
                selection.removeAllRanges();
                selection.addRange(caretRange);
            }
        });
        Mousetrap.bindGlobal('enter', function (e) {
            if (App.autocomplete.dialog.isActive) {
                App.autocomplete.dialog.selectActive();
                App.autocomplete.dialog.close();
                App.autocomplete.focusEditor(App.autocomplete.dialog.editor);
            }
        });

    },
    populate: function (params) {
        params = params || {};

        App.autocomplete.quicktexts = params.quicktexts;

        // clone the elements
        // so we can safely highlight the matched text
        // without breaking the generated handlebars markup
        var clonedElements = jQuery.extend(true, [], App.autocomplete.quicktexts);

        // highlight found string in element title, body and shortcut
        var searchRe = new RegExp(App.autocomplete.cursorPosition.word.text, 'gi');

        var highlightMatch = function (match) {
            return '' + match + '';
        };

        // only match if we have a search string
        if(App.autocomplete.cursorPosition.word.text) {
            clonedElements.forEach(function (elem) {
                elem.title = elem.title.replace(searchRe, highlightMatch);
                elem.originalBody = elem.body;
                elem.body = elem.body.replace(searchRe, highlightMatch);
                elem.shortcut = elem.shortcut.replace(searchRe, highlightMatch);
            });
        }

        var content = Handlebars.compile(App.autocomplete.dialog.liTemplate)({
            elements: clonedElements
        });

        $(this.contentSelector).html(content);

        if (!App.autocomplete.dialog.isActive) {
            App.autocomplete.dialog.show(params);
        }

        App.autocomplete.dialog.isEmpty = false;

        // Set first element active
        App.autocomplete.dialog.selectItem(0);

    },
    show: function (params) {
        params = params || {};

        // get current focused element - the editor
        App.autocomplete.dialog.editor = document.activeElement;

        var selection = window.getSelection();
        var focusNode = selection.focusNode;
        App.autocomplete.dialog.focusNode = focusNode;

        App.autocomplete.dialog.isActive = true;
        App.autocomplete.dialog.isEmpty = true;

        $(this.dialogSelector).addClass('qt-dropdown-show');

        // TODO see why this triggers show
        //$(this.searchSelector).focus();

        $(App.autocomplete.dialog.contentSelector).scrollTop();

        App.autocomplete.dialog.setDialogPosition(params.dialogPositionNode);

        // if we scroll the content element.
        // only if we're positioning next the cursor,
        // not another element.

        // TODO refactor this since we now use position absolute
        // on the dialog
        /*
        if(!params.dialogPositionNode) {

            // remove it just in case we added it previously
            App.autocomplete.dialog.editor.removeEventListener('scroll', App.autocomplete.dialog.setDialogPosition);

            App.autocomplete.dialog.editor.addEventListener('scroll', App.autocomplete.dialog.setDialogPosition);

        }
        */

    },
    setDialogPosition: function(positionNode) {

        if(!App.autocomplete.dialog.isActive) {
            return;
        }

        var dialogMaxHeight = 250;
        var pageHeight = window.innerHeight;
        var scrollTop = $(window).scrollTop();
        var scrollLeft = $(window).scrollLeft();

        //scrollTop += $(App.autocomplete.dialog.editor).scrollTop();
        //scrollLeft += $(App.autocomplete.dialog.editor).scrollLeft();

        var $dialog = $(App.autocomplete.dialog.dialogSelector);

        var dialogMetrics = $dialog.get(0).getBoundingClientRect();

        var topPos = 0;
        var leftPos = 0;

        // in case we want to position the dialog next to
        // another element,
        // not next to the cursor.
        // eg. when we position it next to the qa button.

        var metrics;

        if(positionNode && positionNode.tagName) {

            metrics = JSON.parse(JSON.stringify(positionNode.getBoundingClientRect()));

            leftPos -=  dialogMetrics.width;

            // because we use getBoundingClientRect
            // we need to add the scroll position
            topPos += scrollTop;
            leftPos += scrollLeft;

        } else {

            // cursorPosition doesn't need scrollTop/Left
            // because it uses the absolute page offset positions
            metrics = App.autocomplete.cursorPosition.absolute;

        }

        topPos += metrics.top + metrics.height;
        leftPos += metrics.left + metrics.width;

        // check if we have enough sp            ace at the bottom
        // for the maximum dialog height
        if((pageHeight - (topPos - scrollTop)) > dialogMaxHeight) {

            console.log('bottom', topPos);

            //topPos = topPos - dialogMetrics.height;

            //topPos = topPos - dialogMetrics.height - App.autocomplete.cursorPosition.absolute.height;

        } else {

            console.log('top', topPos);

            topPos -= dialogMetrics.height;

            // TODO still places it at the bottom
        }

        $dialog.css({
            top: topPos,
            left: leftPos
        });

    },
    selectItem: function (index) {
        if (App.autocomplete.dialog.isActive && !App.autocomplete.dialog.isEmpty) {
            var content = $(this.contentSelector);
            var $element = content.children().eq(index);

            content.children()
                .removeClass('active')
                .eq(index);

            $element.addClass('active');
        }
    },
    selectActive: function () {
        if (App.autocomplete.dialog.isActive && !this.isEmpty && App.autocomplete.quicktexts.length) {
            var activeItemId = $(this.contentSelector).find('.active').data('id');
            var quicktext = App.autocomplete.quicktexts.filter(function (quicktext) {
                return quicktext.id === activeItemId;
            })[0];

            App.autocomplete.replaceWith({
                element: App.autocomplete.dialog.editor,
                quicktext: quicktext,
                focusNode: App.autocomplete.dialog.focusNode
            });
        }
    },
    changeSelection: function (direction) {
        var index_diff = direction === 'prev' ? -1 : 1,
            content = $(this.contentSelector),
            elements_count = content.children().length,
            index_active = content.find('.active').index(),
            index_new = Math.max(0, Math.min(elements_count - 1, index_active + index_diff));

        App.autocomplete.dialog.selectItem(index_new);

        // scroll the active element into view
        var $element = content.children().eq(index_new);
        $element.get(0).scrollIntoView();
    },
    // remove dropdown and cleanup
    close: function (callback) {

        if(!App.autocomplete.dialog.isActive) {

            return;

            /*
            if(callback) {
                return callback();
            }
            */

        }

        $(this.dialogSelector).removeClass('qt-dropdown-show');
        $(this.dialogSelector).removeClass('qa-btn-dropdown-show');
        $(this.searchSelector).val('');

        App.autocomplete.dialog.isActive = false;
        App.autocomplete.dialog.isEmpty = null;

        App.autocomplete.dialog.quicktexts = [];
        App.autocomplete.dialog.cursorPosition = null;

    },
    showQaForElement: function(elem) {

        var show = false;

        // if the element is not a textarea
        // input[type=text] or contenteditable
        if($(elem).is('textarea, input[type=text], [contenteditable]')) {
            show = true;
        }

        // if the quick access button is focused/clicked
        if(elem.className.indexOf('gorgias-qa-btn') !== -1) {
            show = false;
        }

        // if the dialog search field is focused
        if(elem.className.indexOf('qt-dropdown-search') !== -1) {
            show = false;
        }

        return show;

    },
    showQaBtn: function(e) {

        // TODO only show the qa button for large textfields

        var textfield = e.target;

        // only show it for valid elements
        if(!App.autocomplete.dialog.showQaForElement(textfield)) {
            return false;
        }

        App.autocomplete.dialog.prevFocus = textfield;

        var qaBtn = App.autocomplete.dialog.qaBtn.get(0);

        // padding from the top-right corner of the textfield
        var padding = 5;

        var metrics = JSON.parse(JSON.stringify(textfield.getBoundingClientRect()));

        metrics.top += $(window).scrollTop();
        metrics.left += $(window).scrollLeft();

        metrics.top += padding;
        metrics.left -= padding;

        // move the quick access button to the right
        // of the textfield
        metrics.left += textfield.offsetWidth - qaBtn.offsetWidth;

        // move the btn using transforms
        // for performance
        var transform = 'translate3d(' + metrics.left + 'px, ' + metrics.top + 'px, 0)';

        qaBtn.style.transform = transform;
        qaBtn.style.msTransform = transform;
        qaBtn.style.mozTransform = transform;
        qaBtn.style.webkitTransform = transform;

        $('body').addClass('gorgias-show-qa-btn');

    },
    hideQaBtn: function() {

        $('body').removeClass('gorgias-show-qa-btn');

    }
};

App.autocomplete.dialog.template = '' +
    '
' +
    '<input type="search" class="qt-dropdown-search" value="" placeholder="Search templates...">' +
    '

' +
    '
' +
    '';

// quick access button for the dialog
App.autocomplete.dialog.qaBtnTemplate = '' +
    '<button class="gorgias-qa-btn"></button>' +
    '';

App.autocomplete.dialog.liTemplate = '' +
    '{{#if elements.length}}' +
    '{{#each elements}}' +
    '
' +
    '{{{title}}}' +
    '{{#if this.shortcut}}' +
    '{{{this.shortcut}}}' +
    '{{/if}}' +
    '{{{body}}}' +
    '
' +
    '{{/each}}' +
    '{{else}}' +
    '
' +
    'No templates found.' +
    '
' +
    '{{/if}}' +
    '';

<|endoftext|>


<commit_before>/**
 * XadillaX created at 2015-03-11 12:05:19
 *
 * Copyright (c) 2015 Huaban.com, all rights
 * reserved
 */
require("sugar");

var async = require("async");
var util = require("util");
var helper = require("./helper");

var ISocket = require("./isocket");
var EventEmitter = require("events").EventEmitter;
var zookeeper = require("node-zookeeper-client");

var MAX_MSG_ID = 2147483647;

/**
 * illyria client
 * @param {String} [host] the hostname
 * @param {Number} [port] the port number
 * @param {Object} [options] the socket options
 * @constructor
 */
var IllyriaClient = function(host, port, options) {
    EventEmitter.call(this);

    // we consider this situation is using zookeeper
    if(typeof host === "object") {
        options = host;
        host = port = undefined;
    }

    options = options || {};

    // using zookeeper will ignore `host` and `port`
    if(options.zookeeper) {
        host = port = undefined;

        this.zookeeperOptions = options.zookeeper || {};
        this.zookeeperOptions.connectString = this.zookeeperOptions.connectString || "127.0.0.1:2181";
        this.zookeeperOptions.root = this.zookeeperOptions.root || "/illyria";
        this.zookeeperOptions.prefix = this.zookeeperOptions.prefix || "/HB_";

        if(this.zookeeperOptions.connectString instanceof Array) {
            this.zookeeperOptions.connectString = this.zookeeperOptions.connectString.join(",");
        }

        if(!this.zookeeperOptions.root.startsWith("/")) {
            this.zookeeperOptions.root = "/" + this.zookeeperOptions.root;
        }

        if(!this.zookeeperOptions.prefix.startsWith("/")) {
            this.zookeeperOptions.prefix = "/" + this.zookeeperOptions.prefix;
        }

    }

    this.options = options;
    this.socket = new ISocket(options);
    this.status = "NOT_CONNECTED";

    this.port = port;
    this.host = host;

    this.runTimeout = options.runTimeout || 10000;
    this.msgId = 0;

    this.socket.addListener("error", this.emit.bind(this, "error"));
    this.socket.addListener("close", this.emit.bind(this, "close"));
    this.socket.addListener("tryReconnect", this.emit.bind(this, "tryReconnect"));
    this.socket.addListener("connected", this.emit.bind(this, "connected"));

    var self = this;
    this.on("connected", function() {
        self.status = "CONNECTED";
    });
    this.on("tryReconnect", function() {
        self.status = "RECONNECTING";
    });
    this.on("close", function(hadError) {
        if(hadError) {
            if(!process.env.ZK_NO_WARN) console.warn("Illyria client closed with an error.");
        }

        self.status = "DISCONNECTED";
    });

    this.on("error", function() {});
};

util.inherits(IllyriaClient, EventEmitter);

IllyriaClient.prototype.connectWithZookeeper = function(callback) {
    if(!this.zookeeperOptions) {
        return process.nextTick(function() {
            callback(new Error("No zookeeper options."));
        });
    }

    var self = this;

    // using zookeeper
    var root = this.zookeeperOptions.root;
    var prefix = this.zookeeperOptions.prefix;
    var _path = root + prefix;

    var _zookeeper = zookeeper.createClient(
        this.zookeeperOptions.connectString,
        this.zookeeperOptions);

    _zookeeper.once("connected", function() {
        // get children
        _zookeeper.getChildren(root, function(err, children/**, stat*/) {
            if(err) return callback(err);

            // get all children
            var servers = [];
            async.eachLimit(children, 10, function(child, callback) {
                var path = root + "/" + child;

                // not this time's node
                // eg.
                //   we are searching `/foo/bar_*`
                //   and current node is `/foo/baz_*`
                //   we should ignore it
                if(!path.startsWith(_path)) return callback();

                _zookeeper.getData(root + "/" + child, function(err, data) {
                    if(err) return callback(err);
                    var res = data.toString().split(",");
                    res[1] = parseInt(res[1]);
                    res[2] = parseInt(res[2]);
                    servers.push(res);
                    callback();
                });
            }, function(err) {
                // close zookeeper
                _zookeeper.close();

                if(err && !servers.length) {
                    if(err.name === "NO_NODE") {
                        err.message = "No available server found.";
                    }

                    return callback(err);
                }

                if(!servers.length) return callback(new Error("No available server found."));
                var server = servers[0];
                for(var i = 1; i < servers.length; i++) {
                    if(servers[i][2] < server[2]) {
                        server = servers[i];
                    }
                }

                // now `server` is our available server!
                if(self.socket) {
                    self.host = server[0];
                    self.port = server[1];
                    self.socket.connect(server[1], server[0], callback);
                } else {
                    callback(new Error("Connect with a null socket. (maybe this socket has been destroyed before)"));
                }
            });
        });
    });

    _zookeeper.connect();
};

/**
 * connect to server
 * @param {Function} callback the callback function
 */
IllyriaClient.prototype.connect = function(callback) {
    if(undefined === callback) callback = helper.emptyFunc;
    if(null === this.socket || undefined === this.socket) {
        this.socket = new ISocket(this.options);
    }

    if(this.zookeeperOptions) {
        return this.connectWithZookeeper(callback);
    } else {
        // no zookeeper
        if(this.socket) {
            return this.socket.connect(this.port, this.host, callback);
        } else {
            return process.nextTick(function() {
                callback(new Error("Connect with a null socket. (maybe this socket has been destroyed before)"));
            });
        }
    }
};

/**
 * send a message to server
 * @param {String} module module name
 * @param {String} method method name
 * @param {Object} params the message body object
 * @param {Function} callback the callback function
 */
IllyriaClient.prototype.send = function(module, method, params, callback) {
    var self = this;
    var msgId = this._nextMessageId();
    var timer;

    var onData = function() {
        if(timer) {
            clearTimeout(timer);
        }

        var data = arguments[0] || [];
        if(data.length !== 2) {
            return callback(new Error("Invalid return data: " + JSON.stringify(data)));
        }

        var type = data.shift();

        if("reply" === type) return callback(undefined, data.shift());
        if("error" === type) {
            var err = data.shift();
            return callback(new Error(err.message || err));
        }

        return callback(new Error("Received an unknown type: " + type + "."), data);
    };

    timer = setTimeout(function() {
        self.socket.undata([ msgId ], onData);
        callback(new Error(
            "Timeout when send and wait for response after " +
            self.runTimeout +
            "ms."));
    }, this.runTimeout);

    this.socket.send([ msgId, "call", module, method ], params);
    this.socket.dataOnce([ msgId ], onData);
};

/**
 * close the connection
 */
IllyriaClient.prototype.close = function() {
    if(this.socket) {
        this.socket.end();
        this.socket.destroy();
    }
    this.socket = null;
    this.status = "CLOSED";
};

/**
 * generate the next message id
 * @private
 */
IllyriaClient.prototype._nextMessageId = function() {
    if(++this.msgId >= MAX_MSG_ID) {
        this.msgId = 1;
    }

    return this.msgId.toString();
};

/**
 * return the connect status
 * @return {String} the connect status
 */
IllyriaClient.prototype.connectStatus = function() {
    return this.status;
};

/**
 * create an illyria client
 * @param {String} [host] the hostname
 * @param {Number} [port] the port number
 * @param {Object} [options] the socket options
 * @return {IllyriaClient} the client object
 */
IllyriaClient.createClient = function(host, port, options) {
    return new IllyriaClient(host, port, options);
};

module.exports = IllyriaClient;
<commit_msg>update error message<commit_after>/**
 * XadillaX created at 2015-03-11 12:05:19
 *
 * Copyright (c) 2015 Huaban.com, all rights
 * reserved
 */
require("sugar");

var async = require("async");
var util = require("util");
var helper = require("./helper");

var ISocket = require("./isocket");
var EventEmitter = require("events").EventEmitter;
var zookeeper = require("node-zookeeper-client");

var MAX_MSG_ID = 2147483647;

/**
 * illyria client
 * @param {String} [host] the hostname
 * @param {Number} [port] the port number
 * @param {Object} [options] the socket options
 * @constructor
 */
var IllyriaClient = function(host, port, options) {
    EventEmitter.call(this);

    // we consider this situation is using zookeeper
    if(typeof host === "object") {
        options = host;
        host = port = undefined;
    }

    options = options || {};

    // using zookeeper will ignore `host` and `port`
    if(options.zookeeper) {
        host = port = undefined;

        this.zookeeperOptions = options.zookeeper || {};
        this.zookeeperOptions.connectString = this.zookeeperOptions.connectString || "127.0.0.1:2181";
        this.zookeeperOptions.root = this.zookeeperOptions.root || "/illyria";
        this.zookeeperOptions.prefix = this.zookeeperOptions.prefix || "/HB_";

        if(this.zookeeperOptions.connectString instanceof Array) {
            this.zookeeperOptions.connectString = this.zookeeperOptions.connectString.join(",");
        }

        if(!this.zookeeperOptions.root.startsWith("/")) {
            this.zookeeperOptions.root = "/" + this.zookeeperOptions.root;
        }

        if(!this.zookeeperOptions.prefix.startsWith("/")) {
            this.zookeeperOptions.prefix = "/" + this.zookeeperOptions.prefix;
        }

    }

    this.options = options;
    this.socket = new ISocket(options);
    this.status = "NOT_CONNECTED";

    this.port = port;
    this.host = host;

    this.runTimeout = options.runTimeout || 10000;
    this.msgId = 0;

    this.socket.addListener("error", this.emit.bind(this, "error"));
    this.socket.addListener("close", this.emit.bind(this, "close"));
    this.socket.addListener("tryReconnect", this.emit.bind(this, "tryReconnect"));
    this.socket.addListener("connected", this.emit.bind(this, "connected"));

    var self = this;
    this.on("connected", function() {
        self.status = "CONNECTED";
    });
    this.on("tryReconnect", function() {
        self.status = "RECONNECTING";
    });
    this.on("close", function(hadError) {
        if(hadError) {
            if(!process.env.ZK_NO_WARN) console.warn("Illyria client closed with an error.");
        }

        self.status = "DISCONNECTED";
    });

    this.on("error", function() {});
};

util.inherits(IllyriaClient, EventEmitter);

IllyriaClient.prototype.connectWithZookeeper = function(callback) {
    if(!this.zookeeperOptions) {
        return process.nextTick(function() {
            callback(new Error("No zookeeper options."));
        });
    }

    var self = this;

    // using zookeeper
    var root = this.zookeeperOptions.root;
    var prefix = this.zookeeperOptions.prefix;
    var _path = root + prefix;

    var _zookeeper = zookeeper.createClient(
        this.zookeeperOptions.connectString,
        this.zookeeperOptions);

    _zookeeper.once("connected", function() {
        // get children
        _zookeeper.getChildren(root, function(err, children/**, stat*/) {
            if(err) return callback(err);

            // get all children
            var servers = [];
            async.eachLimit(children, 10, function(child, callback) {
                var path = root + "/" + child;

                // not this time's node
                // eg.
                //   we are searching `/foo/bar_*`
                //   and current node is `/foo/baz_*`
                //   we should ignore it
                if(!path.startsWith(_path)) return callback();

                _zookeeper.getData(root + "/" + child, function(err, data) {
                    if(err) return callback(err);
                    var res = data.toString().split(",");
                    res[1] = parseInt(res[1]);
                    res[2] = parseInt(res[2]);
                    servers.push(res);
                    callback();
                });
            }, function(err) {
                // close zookeeper
                _zookeeper.close();

                if(err && !servers.length) {
                    return callback(err);
                }

                if(!servers.length) return callback(new Error("No available server found."));
                var server = servers[0];
                for(var i = 1; i < servers.length; i++) {
                    if(servers[i][2] < server[2]) {
                        server = servers[i];
                    }
                }

                // now `server` is our available server!
                if(self.socket) {
                    self.host = server[0];
                    self.port = server[1];
                    self.socket.connect(server[1], server[0], callback);
                } else {
                    callback(new Error("Connect with a null socket. (maybe this socket has been destroyed before)"));
                }
            });
        });
    });

    _zookeeper.connect();
};

/**
 * connect to server
 * @param {Function} callback the callback function
 */
IllyriaClient.prototype.connect = function(callback) {
    if(undefined === callback) callback = helper.emptyFunc;
    if(null === this.socket || undefined === this.socket) {
        this.socket = new ISocket(this.options);
    }

    if(this.zookeeperOptions) {
        return this.connectWithZookeeper(callback);
    } else {
        // no zookeeper
        if(this.socket) {
            return this.socket.connect(this.port, this.host, callback);
        } else {
            return process.nextTick(function() {
                callback(new Error("Connect with a null socket. (maybe this socket has been destroyed before)"));
            });
        }
    }
};

/**
 * send a message to server
 * @param {String} module module name
 * @param {String} method method name
 * @param {Object} params the message body object
 * @param {Function} callback the callback function
 */
IllyriaClient.prototype.send = function(module, method, params, callback) {
    var self = this;
    var msgId = this._nextMessageId();
    var timer;

    var onData = function() {
        if(timer) {
            clearTimeout(timer);
        }

        var data = arguments[0] || [];
        if(data.length !== 2) {
            return callback(new Error("Invalid return data: " + JSON.stringify(data)));
        }

        var type = data.shift();

        if("reply" === type) return callback(undefined, data.shift());
        if("error" === type) {
            var err = data.shift();
            return callback(new Error(err.message || err));
        }

        return callback(new Error("Received an unknown type: " + type + "."), data);
    };

    timer = setTimeout(function() {
        self.socket.undata([ msgId ], onData);
        callback(new Error(
            "Timeout when send and wait for response after " +
            self.runTimeout +
            "ms."));
    }, this.runTimeout);

    this.socket.send([ msgId, "call", module, method ], params);
    this.socket.dataOnce([ msgId ], onData);
};

/**
 * close the connection
 */
IllyriaClient.prototype.close = function() {
    if(this.socket) {
        this.socket.end();
        this.socket.destroy();
    }
    this.socket = null;
    this.status = "CLOSED";
};

/**
 * generate the next message id
 * @private
 */
IllyriaClient.prototype._nextMessageId = function() {
    if(++this.msgId >= MAX_MSG_ID) {
        this.msgId = 1;
    }

    return this.msgId.toString();
};

/**
 * return the connect status
 * @return {String} the connect status
 */
IllyriaClient.prototype.connectStatus = function() {
    return this.status;
};

/**
 * create an illyria client
 * @param {String} [host] the hostname
 * @param {Number} [port] the port number
 * @param {Object} [options] the socket options
 * @return {IllyriaClient} the client object
 */
IllyriaClient.createClient = function(host, port, options) {
    return new IllyriaClient(host, port, options);
};

module.exports = IllyriaClient;
<|endoftext|>


<commit_before>/**
 * @fileoverview This test suite runs tests on the V1 to V2 converter.
 */

var _ = require('lodash'),
    expect = require('chai').expect,
    transformer = require('../../../index');

/* global describe, it */
describe('v2.1.0 to v1.0.0', function () {
    describe('api', function () {
        it('should have a .convertSingle() function', function () {
            expect(transformer.convertSingle).to.be.a('function');
            expect(transformer.convertSingle.length).to.equal(3);
        });
    });

    describe('transformer', function () {
        describe('.convertSingle()', function () {
            it('should work as intended', function (done) {
                var fixture = require('../fixtures/single-request'),
                    options = {
                        inputVersion: '2.1.0',
                        outputVersion: '1.0.0',
                        retainIds: true
                    };

                transformer.convertSingle(fixture.v21, options, function (err, converted) {
                    expect(err).to.not.be.ok;

                    // remove `undefined` properties for testing
                    converted = JSON.parse(JSON.stringify(converted));
                    [
                        'id',
                        'name',
                        'description',
                        'method',
                        'headers',
                        'dataMode',
                        'data',
                        'rawModeData',
                        'tests',
                        'preRequestScript',
                        'url',
                        'responses'
                    ].forEach(function (p) {
                        expect(converted).to.have.property(p);
                    });
                    done();
                });
            });
        });

        describe('.convertResponse()', function () {
            it('should work as intended', function (done) {
                var fixture = require('../fixtures/single-response'),
                    options = {
                        inputVersion: '2.1.0',
                        outputVersion: '1.0.0',
                        retainIds: true
                    };

                transformer.convertResponse(fixture.v21, options, function (err, converted) {
                    if (err) { return done(err); }

                    // remove `undefined` properties for testing
                    converted = JSON.parse(JSON.stringify(converted));

                    expect(converted.requestObject).to.be.a('string');
                    expect(function () {
                        JSON.parse(converted.requestObject);
                    }).to.not.throw();

                    expect(_.omit(converted, [
                        'rawDataType',
                        'request',
                        'requestObject',
                        'responseCode',
                        'status'
                    ])).to.eql(_.omit(fixture.v1, [
                        'code',
                        'dataURI',
                        'empty',
                        'failed',
                        'fileName',
                        'forceNoPretty',
                        'mime',
                        'mimeType',
                        'request',
                        'responseCode',
                        'responseSize',
                        'searchResultScrolledTo',
                        'state',
                        'status',
                        'write'
                    ]));
                    done();
                });
            });
        });

        describe('path variables', function () {
            it('should work with id as indexing property', function (done) {
                var fixture = {
                        id: 'some-id',
                        name: 'some-name',
                        request: {
                            url: {
                                host: ['postman-echo', 'com'],
                                path: [':method'],
                                variable: [{
                                    id: 'method',
                                    value: 'get'
                                }]
                            },
                            method: 'GET'
                        }
                    },
                    options = {
                        inputVersion: '2.1.0',
                        outputVersion: '1.0.0',
                        retainIds: true
                    };

                transformer.convertSingle(fixture, options, function (err, converted) {
                    expect(err).to.not.be.ok;

                    expect(converted.pathVariables).to.eql({
                        method: 'get'
                    });

                    expect(converted.pathVariableData).to.eql([{
                        key: 'method',
                        value: 'get'
                    }]);
                    done();
                });
            });

            it('should work with key as indexing property', function (done) {
                var fixture = {
                        id: 'some-id',
                        name: 'some-name',
                        request: {
                            url: {
                                host: ['postman-echo', 'com'],
                                path: [':method'],
                                variable: [{
                                    key: 'method',
                                    value: 'get'
                                }]
                            },
                            method: 'GET'
                        }
                    },
                    options = {
                        inputVersion: '2.1.0',
                        outputVersion: '1.0.0',
                        retainIds: true
                    };

                transformer.convertSingle(fixture, options, function (err, converted) {
                    expect(err).to.not.be.ok;

                    expect(converted.pathVariables).to.eql({
                        method: 'get'
                    });

                    expect(converted.pathVariableData).to.eql([{
                        key: 'method',
                        value: 'get'
                    }]);
                    done();
                });
            });
        });
    });

    describe('descriptions', function () {
        it('should correctly handle descriptions whilst converting from v2.1.0 to v1', function (done) {
            var fixture = require('../fixtures/sample-description'),
                options = {
                    inputVersion: '2.1.0',
                    outputVersion: '1.0.0',
                    retainIds: true
                };

            transformer.convert(fixture.v21, options, function (err, converted) {
                expect(err).to.not.be.ok;

                // remove `undefined` properties for testing
                converted = JSON.parse(JSON.stringify(converted));

                expect(converted).to.eql(fixture.v1);
                done();
            });
        });
    });

    describe('request file body', function () {
        it('should correctly handle request file bodies whilst converting from v2.1.0 to v1', function (done) {
            var fixture = require('../fixtures/request-body-file'),
                options = {
                    inputVersion: '2.1.0',
                    outputVersion: '1.0.0',
                    retainIds: true
                };

            transformer.convert(fixture.v21, options, function (err, converted) {
                expect(err).to.not.be.ok;

                // remove `undefined` properties for testing
                converted = JSON.parse(JSON.stringify(converted));

                expect(converted).to.eql(fixture.v1);
                done();
            });
        });
    });

    describe('auth', function () {
        it('should be handled correctly in v2.1.0 -> v1 conversions', function (done) {
            var fixture = require('../fixtures/sample-auth'),
                options = {
                    inputVersion: '2.1.0',
                    outputVersion: '1.0.0',
                    retainIds: true
                };

            transformer.convert(fixture.v21, options, function (err, converted) {
                expect(err).to.not.be.ok;

                // remove `undefined` properties for testing
                converted = JSON.parse(JSON.stringify(converted));

                expect(converted).to.eql(fixture.v1);
                done();
            });
        });
    });

    describe('nested entities', function () {
        it('should be handled correctly in v2.1 -> v1 conversions', function (done) {
            var fixture = require('../fixtures/nested-entities'),
                options = {
                    inputVersion: '2.1.0',
                    outputVersion: '1.0.0',
                    retainIds: true
                };

            transformer.convert(fixture.v21, options, function (err, converted) {
                expect(err).to.not.be.ok;

                // remove `undefined` properties for testing
                converted = JSON.parse(JSON.stringify(converted));

                expect(converted).to.eql(fixture.v1);
                done();
            });
        });
    });
});
<commit_msg>Updated v2.1 based converter unit tests :white_check_mark:<commit_after>/**
 * @fileoverview This test suite runs tests on the V1 to V2 converter.
 */

var _ = require('lodash'),
    expect = require('chai').expect,
    transformer = require('../../../index');

/* global describe, it */
describe('v2.1.0 to v1.0.0', function () {
    var options = {
        inputVersion: '2.1.0',
        outputVersion: '1.0.0',
        retainIds: true
    };

    describe('api', function () {
        it('should have a .convertSingle() function', function () {
            expect(transformer.convertSingle).to.be.a('function');
            expect(transformer.convertSingle.length).to.equal(3);
        });
    });

    describe('transformer', function () {
        describe('.convertSingle()', function () {
            it('should work as intended', function (done) {
                var fixture = require('../fixtures/single-request'),
                    options = {
                        inputVersion: '2.1.0',
                        outputVersion: '1.0.0',
                        retainIds: true
                    };

                transformer.convertSingle(fixture.v21, options, function (err, converted) {
                    expect(err).to.not.be.ok;

                    // remove `undefined` properties for testing
                    converted = JSON.parse(JSON.stringify(converted));
                    [
                        'id',
                        'name',
                        'description',
                        'method',
                        'headers',
                        'dataMode',
                        'data',
                        'rawModeData',
                        'tests',
                        'preRequestScript',
                        'url',
                        'responses'
                    ].forEach(function (p) {
                        expect(converted).to.have.property(p);
                    });
                    done();
                });
            });
        });

        describe('.convertResponse()', function () {
            it('should work as intended', function (done) {
                var fixture = require('../fixtures/single-response'),
                    options = {
                        inputVersion: '2.1.0',
                        outputVersion: '1.0.0',
                        retainIds: true
                    };

                transformer.convertResponse(fixture.v21, options, function (err, converted) {
                    if (err) { return done(err); }

                    // remove `undefined` properties for testing
                    converted = JSON.parse(JSON.stringify(converted));

                    expect(converted.requestObject).to.be.a('string');
                    expect(function () {
                        JSON.parse(converted.requestObject);
                    }).to.not.throw();

                    expect(_.omit(converted, [
                        'rawDataType',
                        'request',
                        'requestObject',
                        'responseCode',
                        'status'
                    ])).to.eql(_.omit(fixture.v1, [
                        'code',
                        'dataURI',
                        'empty',
                        'failed',
                        'fileName',
                        'forceNoPretty',
                        'mime',
                        'mimeType',
                        'request',
                        'responseCode',
                        'responseSize',
                        'searchResultScrolledTo',
                        'state',
                        'status',
                        'write'
                    ]));
                    done();
                });
            });
        });

        describe('path variables', function () {
            it('should work with id as indexing property', function (done) {
                var fixture = {
                        id: 'some-id',
                        name: 'some-name',
                        request: {
                            url: {
                                host: ['postman-echo', 'com'],
                                path: [':method'],
                                variable: [{
                                    id: 'method',
                                    value: 'get'
                                }]
                            },
                            method: 'GET'
                        }
                    },
                    options = {
                        inputVersion: '2.1.0',
                        outputVersion: '1.0.0',
                        retainIds: true
                    };

                transformer.convertSingle(fixture, options, function (err, converted) {
                    expect(err).to.not.be.ok;

                    expect(converted.pathVariables).to.eql({
                        method: 'get'
                    });

                    expect(converted.pathVariableData).to.eql([{
                        key: 'method',
                        value: 'get'
                    }]);
                    done();
                });
            });

            it('should work with key as indexing property', function (done) {
                var fixture = {
                        id: 'some-id',
                        name: 'some-name',
                        request: {
                            url: {
                                host: ['postman-echo', 'com'],
                                path: [':method'],
                                variable: [{
                                    key: 'method',
                                    value: 'get'
                                }]
                            },
                            method: 'GET'
                        }
                    },
                    options = {
                        inputVersion: '2.1.0',
                        outputVersion: '1.0.0',
                        retainIds: true
                    };

                transformer.convertSingle(fixture, options, function (err, converted) {
                    expect(err).to.not.be.ok;

                    expect(converted.pathVariables).to.eql({
                        method: 'get'
                    });

                    expect(converted.pathVariableData).to.eql([{
                        key: 'method',
                        value: 'get'
                    }]);
                    done();
                });
            });
        });
    });

    describe('descriptions', function () {
        it('should correctly handle descriptions whilst converting from v2.1.0 to v1', function (done) {
            var fixture = require('../fixtures/sample-description'),
                options = {
                    inputVersion: '2.1.0',
                    outputVersion: '1.0.0',
                    retainIds: true
                };

            transformer.convert(fixture.v21, options, function (err, converted) {
                expect(err).to.not.be.ok;

                // remove `undefined` properties for testing
                converted = JSON.parse(JSON.stringify(converted));

                expect(converted).to.eql(fixture.v1);
                done();
            });
        });
    });

    describe('request file body', function () {
        it('should correctly handle request file bodies whilst converting from v2.1.0 to v1', function (done) {
            var fixture = require('../fixtures/request-body-file'),
                options = {
                    inputVersion: '2.1.0',
                    outputVersion: '1.0.0',
                    retainIds: true
                };

            transformer.convert(fixture.v21, options, function (err, converted) {
                expect(err).to.not.be.ok;

                // remove `undefined` properties for testing
                converted = JSON.parse(JSON.stringify(converted));

                expect(converted).to.eql(fixture.v1);
                done();
            });
        });
    });

    describe('auth', function () {
        it('should be handled correctly in v2.1.0 -> v1 conversions', function (done) {
            var fixture = require('../fixtures/sample-auth'),
                options = {
                    inputVersion: '2.1.0',
                    outputVersion: '1.0.0',
                    retainIds: true
                };

            transformer.convert(fixture.v21, options, function (err, converted) {
                expect(err).to.not.be.ok;

                // remove `undefined` properties for testing
                converted = JSON.parse(JSON.stringify(converted));

                expect(converted).to.eql(fixture.v1);
                done();
            });
        });

        describe('with requests', function () {
            it('should correctly infer a noauth type from the auth object for requests with noauth', function (done) {
                var source = {
                    _postman_id: '969e90b1-0742-41b5-8602-e137d25274ac',
                    request: {
                        auth: {
                            type: 'noauth'
                        }
                    }
                };

                transformer.convertSingle(source, options, function (err, converted) {
                    expect(err).to.not.be.ok;

                    // remove `undefined` properties for testing
                    converted = JSON.parse(JSON.stringify(converted));

                    expect(converted).to.eql({
                        id: '969e90b1-0742-41b5-8602-e137d25274ac',
                        currentHelper: null,
                        auth: null,
                        headers: '',
                        data: [],
                        rawModeData: '',
                        url: '',
                        headerData: []
                    });
                    done();
                });
            });

            it('should correctly infer a noauth type from the auth object for requests with null', function (done) {
                var source = {
                    _postman_id: '969e90b1-0742-41b5-8602-e137d25274ac',
                    request: {
                        auth: null
                    }
                };

                transformer.convertSingle(source, options, function (err, converted) {
                    expect(err).to.not.be.ok;

                    // remove `undefined` properties for testing
                    converted = JSON.parse(JSON.stringify(converted));

                    expect(converted).to.eql({
                        id: '969e90b1-0742-41b5-8602-e137d25274ac',
                        currentHelper: null,
                        auth: null,
                        headers: '',
                        data: [],
                        rawModeData: '',
                        url: '',
                        headerData: []
                    });
                    done();
                });
            });
        });

        describe('with collections', function () {
            it('should correctly infer a noauth type from the auth object for requests', function (done) {
                var source = {
                    info: {
                        _postman_id: '969e90b1-0742-41b5-8602-e137d25274ac'
                    },
                    auth: { type: 'noauth' },
                    item: [{
                        _postman_id: 'a9832f4d-657c-4cd2-a5a4-7ddd6bc4948e',
                        auth: { type: 'noauth' },
                        item: []
                    }]
                };

                transformer.convert(source, options, function (err, converted) {
                    expect(err).to.not.be.ok;

                    // remove `undefined` properties for testing
                    converted = JSON.parse(JSON.stringify(converted));

                    expect(converted).to.eql({
                        id: '969e90b1-0742-41b5-8602-e137d25274ac',
                        folders: [{
                            id: 'a9832f4d-657c-4cd2-a5a4-7ddd6bc4948e',
                            auth: null,
                            folders_order: [],
                            order: []
                        }],
                        order: [],
                        requests: [],
                        folders_order: ['a9832f4d-657c-4cd2-a5a4-7ddd6bc4948e']
                    });
                    done();
                });
            });

            it('should correctly infer a noauth type from the null auth object for requests', function (done) {
                var source = {
                    info: {
                        _postman_id: '969e90b1-0742-41b5-8602-e137d25274ac'
                    },
                    auth: null,
                    item: [{
                        _postman_id: 'a9832f4d-657c-4cd2-a5a4-7ddd6bc4948e',
                        auth: null,
                        item: []
                    }]
                };

                transformer.convert(source, options, function (err, converted) {
                    expect(err).to.not.be.ok;

                    // remove `undefined` properties for testing
                    converted = JSON.parse(JSON.stringify(converted));

                    expect(converted).to.eql({
                        id: '969e90b1-0742-41b5-8602-e137d25274ac',
                        folders: [{
                            id: 'a9832f4d-657c-4cd2-a5a4-7ddd6bc4948e',
                            auth: null,
                            folders_order: [],
                            order: []
                        }],
                        order: [],
                        requests: [],
                        folders_order: ['a9832f4d-657c-4cd2-a5a4-7ddd6bc4948e']
                    });
                    done();
                });
            });
        });
    });

    describe('nested entities', function () {
        it('should be handled correctly in v2.1 -> v1 conversions', function (done) {
            var fixture = require('../fixtures/nested-entities'),
                options = {
                    inputVersion: '2.1.0',
                    outputVersion: '1.0.0',
                    retainIds: true
                };

            transformer.convert(fixture.v21, options, function (err, converted) {
                expect(err).to.not.be.ok;

                // remove `undefined` properties for testing
                converted = JSON.parse(JSON.stringify(converted));

                expect(converted).to.eql(fixture.v1);
                done();
            });
        });
    });
});
<|endoftext|>


<commit_before>'use strict';

const path = require('path');
const App = require('../../src/app/base');

describe('DI container', () => {
    let app;
    let basePath = path.join(__dirname, '..', '..');
    let testKey = 'testKey';

    beforeEach(() => {
        app = new App(basePath);
    });

    it('registers instance', done => {
        let obj = { test: 'value' };
        let get = () => {
            return app.get(testKey);
        };

        expect(get).toThrow();
        app.registerInstance(obj, testKey);
        expect(get()).toBe(obj);

        done();
    });

    it('registers class', done => {
        class TestClass {
            static get provides() {
                return testKey;
            }
        }

        let get = () => {
            return app.get(testKey);
        };

        expect(get).toThrow();
        app.registerClass(TestClass);
        expect(get() instanceof TestClass).toBeTruthy();

        done();
    });

    it('resolves dependencies', done => {
        class ClassA {
            constructor(b, c) {
                this.b = b;
                this.c = c;
            }

            static get provides() {
                return 'a';
            }

            static get requires() {
                return [ 'b', 'c' ];
            }
        }

        class ClassB {
            constructor(d) {
                this.d = d;
            }

            static get provides() {
                return 'b';
            }

            static get requires() {
                return [ 'd' ];
            }
        }

        class ClassC {
            constructor(d) {
                this.d = d;
            }

            static get provides() {
                return 'c';
            }

            static get requires() {
                return [ 'd' ];
            }
        }

        class ClassD {
            static get provides() {
                return 'd';
            }
        }

        app.registerClass(ClassA);
        app.registerClass(ClassB);
        app.registerClass(ClassC);

        let get = () => {
            return app.get('a');
        };

        expect(get).toThrow();

        app.registerClass(ClassD);

        let a = get();
        expect(a instanceof ClassA).toBeTruthy();
        expect(a.b instanceof ClassB).toBeTruthy();
        expect(a.c instanceof ClassC).toBeTruthy();
        expect(a.b.d instanceof ClassD).toBeTruthy();
        expect(a.c.d instanceof ClassD).toBeTruthy();
        expect(a.b.d).toBe(a.c.d);

        done();
    });

    it('detects cyclic dependencies', done => {
        let withError;

        class ClassA {
            static get provides() {
                return 'a';
            }

            static get requires() {
                return [ 'b' ];
            }
        }

        class ClassB {
            static get provides() {
                return 'b';
            }

            static get requires() {
                return [ 'c' ];
            }
        }

        class ClassC {
            static get provides() {
                return 'c';
            }

            static get requires() {
                return withError ? [ 'b' ] : [];
            }
        }

        app.registerClass(ClassA);
        app.registerClass(ClassB);

        let get = () => {
            return app.get('a');
        };

        withError = true;
        app.registerClass(ClassC);
        expect(get).toThrow();

        withError = false;
        app.registerClass(ClassC);
        expect(get).not.toThrow();

        done();
    });
});<commit_msg>More tests<commit_after>'use strict';

const path = require('path');
const App = require('../../src/app/base');

describe('DI container', () => {
    let app;
    let basePath = path.join(__dirname, '..', '..');
    let testKey = 'testKey';

    beforeEach(() => {
        app = new App(basePath);
    });

    it('registers instance', done => {
        let obj = { test: 'value' };
        let get = () => {
            return app.get(testKey);
        };

        expect(get).toThrow();
        app.registerInstance(obj, testKey);
        expect(get()).toBe(obj);

        done();
    });

    it('registers class', done => {
        class TestClass {
            static get provides() {
                return testKey;
            }
        }

        let get = () => {
            return app.get(testKey);
        };

        expect(get).toThrow();
        app.registerClass(TestClass);
        expect(get() instanceof TestClass).toBeTruthy();

        done();
    });

    it('resolves dependencies', done => {
        let lifecycle;

        class ClassA {
            constructor(b, c) {
                this.b = b;
                this.c = c;
            }

            static get provides() {
                return 'a';
            }

            static get requires() {
                return [ 'b', 'c' ];
            }
        }

        class ClassB {
            constructor(d) {
                this.d = d;
            }

            static get provides() {
                return 'b';
            }

            static get requires() {
                return [ 'd' ];
            }
        }

        class ClassC {
            constructor(d) {
                this.d = d;
            }

            static get provides() {
                return 'c';
            }

            static get requires() {
                return [ 'd' ];
            }
        }

        class ClassD {
            static get provides() {
                return 'd';
            }

            static get lifecycle() {
                return lifecycle;
            }
        }

        app.registerClass(ClassA);
        app.registerClass(ClassB);
        app.registerClass(ClassC);

        let get = () => {
            return app.get('a');
        };

        expect(get).toThrow();

        app.registerClass(ClassD);

        let a = get();
        expect(a instanceof ClassA).toBeTruthy();
        expect(a.b instanceof ClassB).toBeTruthy();
        expect(a.c instanceof ClassC).toBeTruthy();
        expect(a.b.d instanceof ClassD).toBeTruthy();
        expect(a.c.d instanceof ClassD).toBeTruthy();
        expect(a.b.d).toBe(a.c.d);

        lifecycle = 'singleton';
        app.registerClass(ClassD);

        a = get();
        expect(a instanceof ClassA).toBeTruthy();
        expect(a.b instanceof ClassB).toBeTruthy();
        expect(a.c instanceof ClassC).toBeTruthy();
        expect(a.b.d instanceof ClassD).toBeTruthy();
        expect(a.c.d instanceof ClassD).toBeTruthy();
        expect(a.b.d).toBe(a.c.d);
        expect(a.b.d).toBe(app.get('d'));

        lifecycle = 'unique';
        app.registerClass(ClassD);

        a = get();
        expect(a instanceof ClassA).toBeTruthy();
        expect(a.b instanceof ClassB).toBeTruthy();
        expect(a.c instanceof ClassC).toBeTruthy();
        expect(a.b.d instanceof ClassD).toBeTruthy();
        expect(a.c.d instanceof ClassD).toBeTruthy();
        expect(a.b.d).not.toBe(a.c.d);

        done();
    });

    it('detects cyclic dependencies', done => {
        let withError;

        class ClassA {
            static get provides() {
                return 'a';
            }

            static get requires() {
                return [ 'b' ];
            }
        }

        class ClassB {
            static get provides() {
                return 'b';
            }

            static get requires() {
                return [ 'c' ];
            }
        }

        class ClassC {
            static get provides() {
                return 'c';
            }

            static get requires() {
                return withError ? [ 'b' ] : [];
            }
        }

        app.registerClass(ClassA);
        app.registerClass(ClassB);

        let get = () => {
            return app.get('a');
        };

        withError = true;
        app.registerClass(ClassC);
        expect(get).toThrow();

        withError = false;
        app.registerClass(ClassC);
        expect(get).not.toThrow();

        done();
    });
});<|endoftext|>


<commit_before>import React, { useState, useMemo, useEffect } from 'react';
import PropTypes from 'prop-types';

import {
  ENTERED,
  ENTERING,
  EXITED,
  EXITING,
  ANIMATE_DEFAULT,
  DURATION_DEFAULT
} from '../constants';
import { expandAnimatorDuration } from '../utils/expandAnimatorDuration';
import { AnimatorContext } from '../AnimatorContext';
import { useAnimatorGeneralSettings } from '../useAnimatorGeneralSettings';
import { useAnimator } from '../useAnimator';

function Component (props) {
  const { animator, children } = props;

  const parentAnimatorGeneralSettings = useAnimatorGeneralSettings();
  const parentAnimator = useAnimator();

  const duration = useMemo(() => {
    return Object.freeze({
      ...DURATION_DEFAULT,
      ...parentAnimatorGeneralSettings?.duration,
      ...expandAnimatorDuration(animator.duration)
    });
  }, [animator.duration, parentAnimatorGeneralSettings]);

  // Since the expected boolean values applicable to the node are provided down
  // to the next child node, they are converted to booleans always to prevent
  // possible data leaking.

  const isParentAnimated = parentAnimator ? !!parentAnimator.animate : ANIMATE_DEFAULT;
  const animate = animator.animate !== undefined ? !!animator.animate : isParentAnimated;

  const root = parentAnimator ? !!animator.root : true;

  const merge = !root && !!animator.merge;

  const parentAnimatorFlowValue = parentAnimator?.flow?.value;
  const isParentActivatingMe = merge
    ? parentAnimatorFlowValue === ENTERED || parentAnimatorFlowValue === ENTERING
    : parentAnimatorFlowValue === ENTERED;

  // An animated root node is by default activated.
  const providedActivate = root ? animator.activate !== false : isParentActivatingMe;

  // If this node is not animated, its initial flow state is ENTERED,
  // so it is activated.
  const activate = animate ? providedActivate : true;

  // The flow object is not directly updated. It is always updated based on
  // its next value.
  const [flow, _setFlow] = useState(() => {
    const value = animate ? EXITED : ENTERED;
    const hasEntered = value === ENTERED;
    const hasExited = value === EXITED;
    return Object.freeze({ value, [value]: true, hasEntered, hasExited });
  });

  const setFlowValue = value => {
    const hasEntered = flow.hasEntered || value === ENTERED;
    const hasExited = flow.hasExited || value === EXITED;
    _setFlow(Object.freeze({ value, [value]: true, hasEntered, hasExited }));
  };

  const animateRefs = React.createRef();

  const toProvideAnimator = useMemo(() => {
    const setupAnimateRefs = parameter => (animateRefs.current = parameter);

    return Object.freeze({
      duration,
      animate,
      root,
      merge,
      flow,
      setupAnimateRefs
    });
  }, [duration, animate, root, merge, flow]);

  useEffect(() => {
    // The flow should not be transitioned when it is not animated.
    // Even if this early return is removed, it should still work, but it is added
    // to improve performance.
    if (!animate) {
      return;
    }

    let timeout;

    if (activate) {
      if (flow.value === ENTERING || flow.value === ENTERED) {
        return;
      }

      clearTimeout(timeout);
      timeout = setTimeout(() => {
        setFlowValue(ENTERING);

        clearTimeout(timeout);
        timeout = setTimeout(() => setFlowValue(ENTERED), duration.enter);
      }, duration.delay);
    }
    else {
      if (flow.value === EXITING || flow.value === EXITED) {
        return;
      }

      clearTimeout(timeout);
      timeout = setTimeout(() => {
        setFlowValue(EXITING);

        clearTimeout(timeout);
        timeout = setTimeout(() => setFlowValue(EXITED), duration.exit);
      }, 0);
    }

    return () => clearTimeout(timeout);
  }, [activate]);

  useEffect(() => {
    animator.useAnimateMount?.(toProvideAnimator, animateRefs.current);

    return () => {
      animator.useAnimateUnmount?.(toProvideAnimator, animateRefs.current);
    };
  }, []);

  useEffect(() => {
    if (!animate) {
      return;
    }

    animator.onTransition?.(flow);

    switch (flow.value) {
      case ENTERING: animator.useAnimateEntering?.(toProvideAnimator, animateRefs.current); break;
      case ENTERED: animator.useAnimateEntered?.(toProvideAnimator, animateRefs.current); break;
      case EXITING: animator.useAnimateExiting?.(toProvideAnimator, animateRefs.current); break;
      case EXITED: animator.useAnimateExited?.(toProvideAnimator, animateRefs.current); break;
    }
  }, [flow.value]);

  return (
    <AnimatorContext.Provider value={toProvideAnimator}>
      {children}
    </AnimatorContext.Provider>
  );
}

Component.displayName = 'Animator';

Component.propTypes = {
  animator: PropTypes.shape({
    duration: PropTypes.oneOfType([
      PropTypes.number,
      PropTypes.shape({
        enter: PropTypes.number,
        exit: PropTypes.number,
        stagger: PropTypes.number,
        delay: PropTypes.number,
        offset: PropTypes.number
      })
    ]),
    animate: PropTypes.bool,
    root: PropTypes.bool,
    merge: PropTypes.bool,
    activate: PropTypes.bool,
    onTransition: PropTypes.func,
    useAnimateMount: PropTypes.func,
    useAnimateEntering: PropTypes.func,
    useAnimateEntered: PropTypes.func,
    useAnimateExiting: PropTypes.func,
    useAnimateExited: PropTypes.func,
    useAnimateUnmount: PropTypes.func
  }),
  children: PropTypes.any
};

Component.defaultProps = {
  animator: {}
};

export { Component };
<commit_msg>feat(animation): animator subscription support<commit_after>import React, { useState, createRef, useMemo, useEffect } from 'react';
import PropTypes from 'prop-types';

import {
  ENTERED,
  ENTERING,
  EXITED,
  EXITING,
  ANIMATE_DEFAULT,
  DURATION_DEFAULT
} from '../constants';
import { expandAnimatorDuration } from '../utils/expandAnimatorDuration';
import { AnimatorContext } from '../AnimatorContext';
import { useAnimatorGeneralSettings } from '../useAnimatorGeneralSettings';
import { useAnimator } from '../useAnimator';

let classInstanceIdCounter = 0;

function Component (props) {
  const { animator, children } = props;

  const parentAnimatorGeneralSettings = useAnimatorGeneralSettings();
  const parentAnimator = useAnimator();

  const [instanceId] = useState(() => classInstanceIdCounter++);

  const duration = useMemo(() => {
    return Object.freeze({
      ...DURATION_DEFAULT,
      ...parentAnimatorGeneralSettings?.duration,
      ...expandAnimatorDuration(animator.duration)
    });
  }, [parentAnimatorGeneralSettings, animator.duration]);

  // Since the expected boolean values applicable to the node are provided down
  // to the next child node, they are converted to booleans always to prevent
  // possible data leaking.

  const isParentAnimated = parentAnimator ? !!parentAnimator.animate : ANIMATE_DEFAULT;
  const animate = animator.animate !== undefined ? !!animator.animate : isParentAnimated;

  const root = parentAnimator ? !!animator.root : true;

  const merge = !root && !!animator.merge;

  // If this node is not animated, its initial flow state is ENTERED,
  // so it is activated.
  const [activate, setActivate] = useState(() => !animate);

  // The flow object is not directly updated. It is always updated based on
  // its next value.
  const [flow, _setFlow] = useState(() => {
    const value = animate ? EXITED : ENTERED;
    const hasEntered = value === ENTERED;
    const hasExited = value === EXITED;
    return Object.freeze({ value, [value]: true, hasEntered, hasExited });
  });

  const setFlowValue = value => {
    const hasEntered = flow.hasEntered || value === ENTERED;
    const hasExited = flow.hasExited || value === EXITED;
    _setFlow(Object.freeze({ value, [value]: true, hasEntered, hasExited }));
  };

  // All the <Animator/> children non-root instances.
  const [childrenNodesMap] = useState(() => new Map());

  // This variable is supposed to be defined by the component using this
  // <Animator/>. It will contain the reference(s) to the actual HTML element(s)
  // to animate on the flow transitions and component lifecycle.
  const animateRefs = createRef();

  const providedAnimator = useMemo(() => {
    const setupAnimateRefs = refs => (animateRefs.current = refs);

    const _id = instanceId;
    const _subscribe = (id, child) => childrenNodesMap.set(id, child);
    const _unsubscribe = id => childrenNodesMap.delete(id);

    return Object.freeze({
      duration,
      animate,
      root,
      merge,
      flow,
      setupAnimateRefs,
      _id,
      _subscribe,
      _unsubscribe
    });
  }, [duration, animate, root, merge, flow]);

  useEffect(() => {
    if (!animate) {
      return;
    }

    // TODO: What should be the interface reference from this child?
    // TODO: What if it is changed to root and needs to be unsuscribed?
    // TODO: Test when "duration" is changed before its activation is updated.
    // TODO: Test when "merge" is changed before its activation is updated.
    if (!root) {
      const getDuration = () => duration;
      const getIsMerge = () => merge;
      const child = Object.freeze({ getDuration, getIsMerge, setActivate });

      parentAnimator?._subscribe(instanceId, child);
    }

    animator.useAnimateMount?.(providedAnimator, animateRefs.current);

    return () => {
      animator.useAnimateUnmount?.(providedAnimator, animateRefs.current);

      if (!root) {
        parentAnimator?._unsubscribe(instanceId);
      }
    };
  }, []);

  useEffect(() => {
    // An animated root node is by default activated.
    if (animate && root) {
      setActivate(animator.activate !== false);
    }
  }, [root, animator.activate]);

  useEffect(() => {
    if (!animate) {
      return;
    }

    const childrenNodes = Array.from(childrenNodesMap.values());

    animator.onTransition?.(flow);

    switch (flow.value) {
      case ENTERING: {
        animator.useAnimateEntering?.(providedAnimator, animateRefs.current);

        childrenNodes
          .filter(child => child.getIsMerge())
          .forEach(child => child.setActivate(true));
        break;
      }
      case ENTERED: {
        animator.useAnimateEntered?.(providedAnimator, animateRefs.current);

        childrenNodes
          .filter(child => !child.getIsMerge())
          .forEach(child => child.setActivate(true));
        break;
      }
      case EXITING: {
        animator.useAnimateExiting?.(providedAnimator, animateRefs.current);

        childrenNodes.forEach(child => child.setActivate(false));
        break;
      }
      case EXITED: {
        animator.useAnimateExited?.(providedAnimator, animateRefs.current);
        break;
      }
    }
  }, [flow.value]);

  useEffect(() => {
    // The flow should not be transitioned when it is not animated.
    // Even if this early return is removed, it should still work, but it is added
    // to improve performance.
    if (!animate) {
      return;
    }

    let timeout;

    if (activate) {
      if (flow.value === ENTERING || flow.value === ENTERED) {
        return;
      }

      clearTimeout(timeout);
      timeout = setTimeout(() => {
        setFlowValue(ENTERING);

        clearTimeout(timeout);
        timeout = setTimeout(() => setFlowValue(ENTERED), duration.enter);
      }, duration.delay);
    }
    else {
      if (flow.value === EXITING || flow.value === EXITED) {
        return;
      }

      clearTimeout(timeout);
      timeout = setTimeout(() => {
        setFlowValue(EXITING);

        clearTimeout(timeout);
        timeout = setTimeout(() => setFlowValue(EXITED), duration.exit);
      }, 0);
    }

    return () => clearTimeout(timeout);
  }, [activate]);

  return (
    <AnimatorContext.Provider value={providedAnimator}>
      {children}
    </AnimatorContext.Provider>
  );
}

Component.displayName = 'Animator';

Component.propTypes = {
  animator: PropTypes.shape({
    duration: PropTypes.oneOfType([
      PropTypes.number,
      PropTypes.shape({
        enter: PropTypes.number,
        exit: PropTypes.number,
        stagger: PropTypes.number,
        delay: PropTypes.number,
        offset: PropTypes.number
      })
    ]),
    animate: PropTypes.bool,
    root: PropTypes.bool,
    merge: PropTypes.bool,
    activate: PropTypes.bool,
    onTransition: PropTypes.func,
    useAnimateMount: PropTypes.func,
    useAnimateEntering: PropTypes.func,
    useAnimateEntered: PropTypes.func,
    useAnimateExiting: PropTypes.func,
    useAnimateExited: PropTypes.func,
    useAnimateUnmount: PropTypes.func
  }),
  children: PropTypes.any
};

Component.defaultProps = {
  animator: {}
};

export { Component };
<|endoftext|>