<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) {

        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
                });

            });
        });

    },
    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');

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

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

        // focus the input focus after setting the position
        // because it messes with the window scroll focused
        $(App.autocomplete.dialog.searchSelector).focus();

    },
    setDialogPosition: function(positionNode) {

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

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

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

        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;

        topPos += paddingTop;

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

            topPos -= dialogMetrics.height;
            topPos -= metrics.height;

            topPos -= paddingTop * 2;

            // add class for qa button styling
            $(this.dialogSelector).addClass('qt-dropdown-show-top');

        }

        $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('qt-dropdown-show-top');
        $(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;
        }

        // check if the element is big enough
        // to only show the qa button for large textfields
        if(show === true) {

            var metrics = elem.getBoundingClientRect();

            // only show for elements
            if(metrics.width < 100 || metrics.height < 100) {
                show = false;
            }

        }

        return show;

    },
    showQaBtn: function(e) {

        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>Add a 500ms delay before showing the qa button, in case the textfield's styles change its position on focus. eg. Gmail when you have multiple addresses configured, and the From field shows/hides on focus.<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
                });

            });
        });

    },
    createQaBtn: function() {

        var container = $('body');

        var instance = this;

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

        var showQaBtnTimer;

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

            if(showQaBtnTimer) {
                clearTimeout(showQaBtnTimer);
            }

            // add a small delay for showing the qa button.
            // in case the element's styles change its position on focus.
            // eg. gmail when you have multiple addresses configured,
            // and the from fields shows/hides on focus.
            showQaBtnTimer = setTimeout(function() {
                instance.showQaBtn(e);
            }, 500);

        });

        container.on('focusout', function(e) {
            if(showQaBtnTimer) {
                clearTimeout(showQaBtnTimer);
            }

            instance.hideQaBtn(e);
        });

        instance.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');

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

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

        // focus the input focus after setting the position
        // because it messes with the window scroll focused
        $(App.autocomplete.dialog.searchSelector).focus();

    },
    setDialogPosition: function(positionNode) {

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

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

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

        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;

        topPos += paddingTop;

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

            topPos -= dialogMetrics.height;
            topPos -= metrics.height;

            topPos -= paddingTop * 2;

            // add class for qa button styling
            $(this.dialogSelector).addClass('qt-dropdown-show-top');

        }

        $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('qt-dropdown-show-top');
        $(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;
        }

        // check if the element is big enough
        // to only show the qa button for large textfields
        if(show === true) {

            var metrics = elem.getBoundingClientRect();

            // only show for elements
            if(metrics.width < 100 || metrics.height < 100) {
                show = false;
            }

        }

        return show;

    },
    showQaBtn: function(e) {

        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>// Requires
var Q = require('q');
var exec = require('../utils').exec;

var REGEX = /\S+/gi;
function split(str) {
    return str.match(REGEX).slice(1);
}

function lines(str) {
    return str.split('\n').filter(Boolean);
}

function osx() {
    return exec('lsof -i -P | grep -i "listen"')
    .then(function(out) {
        return lines(out.stdout).map(function(line) {
            console.log(line);

            var parts = split(line);

            // *:XXXX or localhost:XXXX
            return parts[parts.length - 2];
        });

    })
    .fail(Q([]));
}

function linux() {
    return exec('netstat -nat | grep -i "listen"')
    .then(function(out) {
        console.log(out.stdout);
        return out.stdout.split('\n').filter(Boolean)
        .map(function(line) {
            var parts = split(line);

            // Listening address
            return parts[2];
        });
    })
    .fail(Q[[]]);
}

var ADDR_MAP = {
    // IPv4
    '0.0.0.0': '*',

    // IPv6
    '::': '*'
};

// Split into bind address and port
function normalize(addr) {
    var parts = addr.split(':');
    var port = parseInt(parts.pop(), 10);
    var bind = parts.join(':');

    return [
        // The address the process is bound to
        ADDR_MAP[bind] || bind,

        // The port it's listening on
        port
    ];
}

// Is a pair reachable
function reachable(pair) {
    var bind = pair[0], port = pair[1];
    return bind === '*';
}

// looks like http
function looksHttp(pair) {
    var bind = pair[0], port = pair[1];
    return (
        port == 80 ||
        port > 1024 && port < 10000
    );
}

var METHODS = {
    'darwin': osx,
    'linux': linux
};

// Get list of bind addr & ports
function list() {
    var method = METHODS[process.platform];

    return method()
    .then(function(addrs) {
        return addrs
        .map(normalize)
        .filter(reachable)
        .filter(looksHttp);
    });
}

// Exports
exports.osx = osx;
exports.linux = linux;
exports.list = list;
<commit_msg>Remove useless console.logs cb.proc/http<commit_after>// Requires
var Q = require('q');
var exec = require('../utils').exec;

var REGEX = /\S+/gi;
function split(str) {
    return str.match(REGEX).slice(1);
}

function lines(str) {
    return str.split('\n').filter(Boolean);
}

function osx() {
    return exec('lsof -i -P | grep -i "listen"')
    .then(function(out) {
        return lines(out.stdout).map(function(line) {
            var parts = split(line);

            // *:XXXX or localhost:XXXX
            return parts[parts.length - 2];
        });

    })
    .fail(Q([]));
}

function linux() {
    return exec('netstat -nat | grep -i "listen"')
    .then(function(out) {
        return out.stdout.split('\n').filter(Boolean)
        .map(function(line) {
            var parts = split(line);

            // Listening address
            return parts[2];
        });
    })
    .fail(Q[[]]);
}

var ADDR_MAP = {
    // IPv4
    '0.0.0.0': '*',

    // IPv6
    '::': '*'
};

// Split into bind address and port
function normalize(addr) {
    var parts = addr.split(':');
    var port = parseInt(parts.pop(), 10);
    var bind = parts.join(':');

    return [
        // The address the process is bound to
        ADDR_MAP[bind] || bind,

        // The port it's listening on
        port
    ];
}

// Is a pair reachable
function reachable(pair) {
    var bind = pair[0], port = pair[1];
    return bind === '*';
}

// looks like http
function looksHttp(pair) {
    var bind = pair[0], port = pair[1];
    return (
        port == 80 ||
        port > 1024 && port < 10000
    );
}

var METHODS = {
    'darwin': osx,
    'linux': linux
};

// Get list of bind addr & ports
function list() {
    var method = METHODS[process.platform];

    return method()
    .then(function(addrs) {
        return addrs
        .map(normalize)
        .filter(reachable)
        .filter(looksHttp);
    });
}

// Exports
exports.osx = osx;
exports.linux = linux;
exports.list = list;
<|endoftext|>


<commit_before>import { window, document } from 'ssr-window';

const Device = (function Device() {
  const platform = window.navigator.platform;
  const ua = window.navigator.userAgent;

  const device = {
    ios: false,
    android: false,
    androidChrome: false,
    desktop: false,
    windowsPhone: false,
    iphone: false,
    iphoneX: false,
    ipod: false,
    ipad: false,
    edge: false,
    ie: false,
    firefox: false,
    macos: false,
    windows: false,
    cordova: !!(window.cordova || window.phonegap),
    phonegap: !!(window.cordova || window.phonegap),
    electron: false,
  };

  const screenWidth = window.screen.width;
  const screenHeight = window.screen.height;

  const windowsPhone = ua.match(/(Windows Phone);?[\s\/]+([\d.]+)?/); // eslint-disable-line
  const android = ua.match(/(Android);?[\s\/]+([\d.]+)?/); // eslint-disable-line
  const ipad = ua.match(/(iPad).*OS\s([\d_]+)/);
  const ipod = ua.match(/(iPod)(.*OS\s([\d_]+))?/);
  const iphone = !ipad && ua.match(/(iPhone\sOS|iOS)\s([\d_]+)/);
  const iphoneX = iphone && (
    (screenWidth === 375 && screenHeight === 812) // X/XS
    || (screenWidth === 414 && screenHeight === 896) // XR / XS Max
  );
  const ie = ua.indexOf('MSIE ') >= 0 || ua.indexOf('Trident/') >= 0;
  const edge = ua.indexOf('Edge/') >= 0;
  const firefox = ua.indexOf('Gecko/') >= 0 && ua.indexOf('Firefox/') >= 0;
  const macos = platform === 'MacIntel';
  const windows = platform === 'Win32';
  const electron = ua.toLowerCase().indexOf('electron') >= 0;

  device.ie = ie;
  device.edge = edge;
  device.firefox = firefox;

  // Windows
  if (windowsPhone) {
    device.os = 'windowsPhone';
    device.osVersion = windowsPhone[2];
    device.windowsPhone = true;
  }
  // Android
  if (android && !windows) {
    device.os = 'android';
    device.osVersion = android[2];
    device.android = true;
    device.androidChrome = ua.toLowerCase().indexOf('chrome') >= 0;
  }
  if (ipad || iphone || ipod) {
    device.os = 'ios';
    device.ios = true;
  }
  // iOS
  if (iphone && !ipod) {
    device.osVersion = iphone[2].replace(/_/g, '.');
    device.iphone = true;
    device.iphoneX = iphoneX;
  }
  if (ipad) {
    device.osVersion = ipad[2].replace(/_/g, '.');
    device.ipad = true;
  }
  if (ipod) {
    device.osVersion = ipod[3] ? ipod[3].replace(/_/g, '.') : null;
    device.iphone = true;
  }
  // iOS 8+ changed UA
  if (device.ios && device.osVersion && ua.indexOf('Version/') >= 0) {
    if (device.osVersion.split('.')[0] === '10') {
      device.osVersion = ua.toLowerCase().split('version/')[1].split(' ')[0];
    }
  }

  // Webview
  device.webView = !!((iphone || ipad || ipod) && (ua.match(/.*AppleWebKit(?!.*Safari)/i) || window.navigator.standalone))
    || (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches);
  device.webview = device.webView;
  device.standalone = device.webView;

  // Desktop
  device.desktop = !(device.ios || device.android || device.windowsPhone) || electron;
  if (device.desktop) {
    device.electron = electron;
    device.macos = macos;
    device.windows = windows;
  }

  // Meta statusbar
  const metaStatusbar = document.querySelector('meta[name="apple-mobile-web-app-status-bar-style"]');

  // Check for status bar and fullscreen app mode
  device.needsStatusbarOverlay = function needsStatusbarOverlay() {
    if (device.desktop) return false;
    if (device.standalone && device.ios && metaStatusbar && metaStatusbar.content === 'black-translucent') {
      return true;
    }
    if ((device.webView || (device.android && device.cordova)) && (window.innerWidth * window.innerHeight === window.screen.width * window.screen.height)) {
      if (device.iphoneX && (window.orientation === 90 || window.orientation === -90)) {
        return false;
      }
      return true;
    }
    return false;
  };
  device.statusbar = device.needsStatusbarOverlay();

  // Pixel Ratio
  device.pixelRatio = window.devicePixelRatio || 1;

  // Export object
  return device;
}());

export default Device;
<commit_msg>Update device.js<commit_after>import { window, document } from 'ssr-window';

const Device = (function Device() {
  const platform = window.navigator.platform;
  const ua = window.navigator.userAgent;

  const device = {
    ios: false,
    android: false,
    androidChrome: false,
    desktop: false,
    windowsPhone: false,
    iphone: false,
    iphoneX: false,
    ipod: false,
    ipad: false,
    edge: false,
    ie: false,
    firefox: false,
    macos: false,
    windows: false,
    cordova: !!(window.cordova || window.phonegap),
    phonegap: !!(window.cordova || window.phonegap),
    electron: false,
  };

  const screenWidth = window.screen.width;
  const screenHeight = window.screen.height;

  const windowsPhone = ua.match(/(Windows Phone);?[\s\/]+([\d.]+)?/); // eslint-disable-line
  const android = ua.match(/(Android);?[\s\/]+([\d.]+)?/); // eslint-disable-line
  const ipad = ua.match(/(iPad).*OS\s([\d_]+)/);
  const ipod = ua.match(/(iPod)(.*OS\s([\d_]+))?/);
  const iphone = !ipad && ua.match(/(iPhone\sOS|iOS)\s([\d_]+)/);
  const iphoneX = iphone && (
    (screenWidth === 375 && screenHeight === 812) // X/XS
    || (screenWidth === 414 && screenHeight === 896) // XR / XS Max
  );
  const ie = ua.indexOf('MSIE ') >= 0 || ua.indexOf('Trident/') >= 0;
  const edge = ua.indexOf('Edge/') >= 0;
  const firefox = ua.indexOf('Gecko/') >= 0 && ua.indexOf('Firefox/') >= 0;
  const macos = platform === 'MacIntel';
  const windows = platform === 'Win32';
  const electron = ua.toLowerCase().indexOf('electron') >= 0;

  device.ie = ie;
  device.edge = edge;
  device.firefox = firefox;

  // Windows
  if (windowsPhone) {
    device.os = 'windowsPhone';
    device.osVersion = windowsPhone[2];
    device.windowsPhone = true;
  }
  // Android
  if (android && !windows) {
    device.os = 'android';
    device.osVersion = android[2];
    device.android = true;
    device.androidChrome = ua.toLowerCase().indexOf('chrome') >= 0;
  }
  if (ipad || iphone || ipod) {
    device.os = 'ios';
    device.ios = true;
  }
  // iOS
  if (iphone && !ipod) {
    device.osVersion = iphone[2].replace(/_/g, '.');
    device.iphone = true;
    device.iphoneX = iphoneX;
  }
  if (ipad) {
    device.osVersion = ipad[2].replace(/_/g, '.');
    device.ipad = true;
  }
  if (ipod) {
    device.osVersion = ipod[3] ? ipod[3].replace(/_/g, '.') : null;
    device.ipod = true;
  }
  // iOS 8+ changed UA
  if (device.ios && device.osVersion && ua.indexOf('Version/') >= 0) {
    if (device.osVersion.split('.')[0] === '10') {
      device.osVersion = ua.toLowerCase().split('version/')[1].split(' ')[0];
    }
  }

  // Webview
  device.webView = !!((iphone || ipad || ipod) && (ua.match(/.*AppleWebKit(?!.*Safari)/i) || window.navigator.standalone))
    || (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches);
  device.webview = device.webView;
  device.standalone = device.webView;

  // Desktop
  device.desktop = !(device.ios || device.android || device.windowsPhone) || electron;
  if (device.desktop) {
    device.electron = electron;
    device.macos = macos;
    device.windows = windows;
  }

  // Meta statusbar
  const metaStatusbar = document.querySelector('meta[name="apple-mobile-web-app-status-bar-style"]');

  // Check for status bar and fullscreen app mode
  device.needsStatusbarOverlay = function needsStatusbarOverlay() {
    if (device.desktop) return false;
    if (device.standalone && device.ios && metaStatusbar && metaStatusbar.content === 'black-translucent') {
      return true;
    }
    if ((device.webView || (device.android && device.cordova)) && (window.innerWidth * window.innerHeight === window.screen.width * window.screen.height)) {
      if (device.iphoneX && (window.orientation === 90 || window.orientation === -90)) {
        return false;
      }
      return true;
    }
    return false;
  };
  device.statusbar = device.needsStatusbarOverlay();

  // Pixel Ratio
  device.pixelRatio = window.devicePixelRatio || 1;

  // Export object
  return device;
}());

export default Device;
<|endoftext|>


<commit_before>var assert = chai.assert;

describe('LMLayer', function () {
  describe('[[constructor]]', function () {
      it('should construct with zero arguments', function () {
        let lmlayer = new LMLayer();
        assert.instanceOf(lmlayer, LMLayer);
      });
  });

  describe('#asBlobURI', function () {
    it('should take a function and convert it into a blob function', function () {
      let uri = LMLayer.asBlobURI(function dummyHandler() {
        var dummy;
      });
      assert.match(uri, /^blob:/);
    })
  })
});
<commit_msg>Make LMLayer.asBlobURI() test a bit more stringent.<commit_after>var assert = chai.assert;

describe('LMLayer', function () {
  describe('[[constructor]]', function () {
      it('should construct with zero arguments', function () {
        let lmlayer = new LMLayer();
        assert.instanceOf(lmlayer, LMLayer);
      });
  });

  describe('#asBlobURI', function () {
    it('should take a function and convert it into a blob function', function (done) {
      let uri = LMLayer.asBlobURI(function dummyHandler() {
        // Post something weird, so we can be reasonably certain it's not a fluke.
        // WARNING: Do NOT factor out this string as a variable.
        // It MUST remain a string in this function body, because it gets stringified!
        postMessage('fhqwhgads');
      });
      assert.match(uri, /^blob:/);

      let worker = new Worker(uri);
      worker.onmessage = function thisShouldBeCalled(event) {
        assert.propertyVal(event, 'data', 'fhqwhgads');
        done();
      };
    })
  })
});
<|endoftext|>


<commit_before>export default {
  /**
   * 检测json是否合法
   */
  validateJson(str) {
    try {
      return !!JSON.parse(str);
    } catch (e) {
      return false;
    }
  },

  /**
   * 检测xml和html是否合法
   */
  validateXml(str) {
    try {
      if (typeof DOMParser !== 'undefined') {
        let parserObj =
          new window.DOMParser()
            .parseFromString(str, 'application/xml')
            .getElementsByTagName('parsererror') || {};
        return parserObj.length === 0;
      } else if (typeof window.ActiveXObject !== 'undefined') {
        let xml = new window.ActiveXObject('Microsoft.XMLDOM');
        xml.async = 'false';
        xml.loadXML(str);
        return xml;
      }
    } catch (e) {
      return false;
    }
  },

  /**
   * 检测yaml是否合法
   */
  //   validateYaml(str) {
  //     try {
  //       console.log('yaml: ', yaml, yaml.safeLoadAll(str));
  //       return !!yaml.safeLoadAll(str);
  //     } catch (e) {
  //       console.log('e: ', e);
  //       return false;
  //     }
  //   },

  /**
   * 检测属性是否正确
   */
  validateProperties(str = '') {
    let reg = /^[A-Za-z\d-_]+=.+$/;
    return str
      .replace('\n\r', '\n')
      .split('\n')
      .every(_str => reg.test(_str));
  },

  /**
   * 根据类型验证类型
   */
  validate({ content, type }) {
    let validateObj = {
      json: this.validateJson,
      xml: this.validateXml,
      'text/html': this.validateXml,
      html: this.validateXml,
      properties: this.validateProperties,
    };

    if (!validateObj[type]) {
      return true;
    }

    return validateObj[type](content);
  },
};
<commit_msg>fix 445<commit_after>export default {
  /**
   * 检测json是否合法
   */
  validateJson(str) {
    try {
      return !!JSON.parse(str);
    } catch (e) {
      return false;
    }
  },

  /**
   * 检测xml和html是否合法
   */
  validateXml(str) {
    try {
      if (typeof DOMParser !== 'undefined') {
        let parserObj =
          new window.DOMParser()
            .parseFromString(str, 'application/xml')
            .getElementsByTagName('parsererror') || {};
        return parserObj.length === 0;
      } else if (typeof window.ActiveXObject !== 'undefined') {
        let xml = new window.ActiveXObject('Microsoft.XMLDOM');
        xml.async = 'false';
        xml.loadXML(str);
        return xml;
      }
    } catch (e) {
      return false;
    }
  },

  /**
   * 检测yaml是否合法
   */
  //   validateYaml(str) {
  //     try {
  //       console.log('yaml: ', yaml, yaml.safeLoadAll(str));
  //       return !!yaml.safeLoadAll(str);
  //     } catch (e) {
  //       console.log('e: ', e);
  //       return false;
  //     }
  //   },

  /**
   * 检测属性是否正确
   */
  validateProperties(str = '') {
    const reg = /^[^=]+=[^=]+$/;
    return str
      .replace('\n\r', '\n')
      .split('\n')
      .every(_str => reg.test(_str.trim()));
  },

  /**
   * 根据类型验证类型
   */
  validate({ content, type }) {
    let validateObj = {
      json: this.validateJson,
      xml: this.validateXml,
      'text/html': this.validateXml,
      html: this.validateXml,
      properties: this.validateProperties,
    };

    if (!validateObj[type]) {
      return true;
    }

    return validateObj[type](content);
  },
};
<|endoftext|>


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

/**
 * @fileoverview Controllers for the editor's state graph.
 *
 * @author sll@google.com (Sean Lip)
 */

function EditorGraph($scope, $filter, explorationData) {
  $scope.updateViz = function() {
    explorationData.getData().then(function(data) {
      $scope.graphData = $scope.reformatResponse(
          data.states, data.init_state_id);
    });
  };

  // At the start, update the graph.
  $scope.updateViz();

  // Also update the graph when an update event is broadcast.
  $scope.$on('updateViz', $scope.updateViz);

  // Reformat the model into a response that is processable by d3.js.
  $scope.reformatResponse = function(states, initStateId) {
    var SENTINEL_DEPTH = 3000;
    var VERT_OFFSET = 20;
    var HORIZ_SPACING = 150;
    var VERT_SPACING = 100;
    var HORIZ_OFFSET = 100;
    var nodes = {};
    nodes[END_DEST] = {name: END_DEST, depth: SENTINEL_DEPTH, reachable: false};
    for (var state in states) {
      nodes[state] = {name: states[state].name, depth: SENTINEL_DEPTH, reachable: false};
    }
    nodes[initStateId].depth = 0;

    var maxDepth = 0;
    var seenNodes = [initStateId];
    var queue = [initStateId];
    var maxXDistPerLevel = {0: HORIZ_OFFSET};
    nodes[initStateId].y0 = VERT_OFFSET;
    nodes[initStateId].x0 = HORIZ_OFFSET;

    while (queue.length > 0) {
      var currNode = queue[0];
      queue.shift();
      nodes[currNode].reachable = true;
      if (currNode in states) {
        for (var i = 0; i < states[currNode].widget.rules.submit.length; i++) {
          // Assign levels to nodes only when they are first encountered.
          if (seenNodes.indexOf(states[currNode].widget.rules.submit[i].dest) == -1) {
            seenNodes.push(states[currNode].widget.rules.submit[i].dest);
            nodes[states[currNode].widget.rules.submit[i].dest].depth = nodes[currNode].depth + 1;
            nodes[states[currNode].widget.rules.submit[i].dest].y0 = (nodes[currNode].depth + 1) * VERT_SPACING + VERT_OFFSET;
            if (nodes[currNode].depth + 1 in maxXDistPerLevel) {
              nodes[states[currNode].widget.rules.submit[i].dest].x0 = maxXDistPerLevel[nodes[currNode].depth + 1] + HORIZ_SPACING;
              maxXDistPerLevel[nodes[currNode].depth + 1] += HORIZ_SPACING;
            } else {
              nodes[states[currNode].widget.rules.submit[i].dest].x0 = HORIZ_OFFSET;
              maxXDistPerLevel[nodes[currNode].depth + 1] = HORIZ_OFFSET;
            }
            maxDepth = Math.max(maxDepth, nodes[currNode].depth + 1);
            queue.push(states[currNode].widget.rules.submit[i].dest);
          }
        }
      }
    }

    var horizPositionForLastRow = HORIZ_OFFSET;
    for (var node in nodes) {
      if (nodes[node].depth == SENTINEL_DEPTH) {
        nodes[node].depth = maxDepth + 1;
        nodes[node].y0 = VERT_OFFSET + nodes[node].depth * VERT_SPACING;
        nodes[node].x0 = horizPositionForLastRow;
        horizPositionForLastRow += HORIZ_SPACING;
      }
    }

    // Assign unique IDs to each node.
    var idCount = 0;
    var nodeList = [];
    for (var node in nodes) {
      var nodeMap = nodes[node];
      nodeMap['hashId'] = node;
      nodeMap['id'] = idCount;
      nodes[node]['id'] = idCount;
      idCount++;
      nodeList.push(nodeMap);
    }

    var links = [];
    for (var state in states) {
      for (var i = 0; i < states[state].widget.rules.submit.length; i++) {
        links.push({
            source: nodeList[nodes[state].id],
            target: nodeList[nodes[states[state].widget.rules.submit[i].dest].id],
            name: $filter('parameterizeRule')({
                rule: states[state].widget.rules.submit[i].rule,
                inputs: states[state].widget.rules.submit[i].inputs
            })
        });
      }
    }

    return {nodes: nodeList, links: links, initStateId: initStateId};
  };
}

oppia.directive('stateGraphViz', function(explorationData) {
  // constants
  var w = 960,
      h = 4000,
      i = 0;
  var DEFAULT_CIRCLE_RADIUS = 30;
  // The following variable must be at least 3.
  var MAX_CATEGORY_LENGTH = 20;

  var getTextWidth = function(text) {
    return 40 + Math.min(MAX_CATEGORY_LENGTH, text.length) * 5;
  };

  return {
    restrict: 'E',
    scope: {
      val: '=',
      grouped: '='
    },
    link: function(scope, element, attrs) {
      scope.truncate = function(text) {
        if (text.length > MAX_CATEGORY_LENGTH) {
          return text.substring(0, MAX_CATEGORY_LENGTH - 3) + '...';
        } else {
          return text;
        }
      };

      var vis = d3.select(element[0]).append('svg:svg')
          .attr('width', w)
          .attr('height', h)
          .attr('class', 'oppia-graph-viz')
        .append('svg:g')
          .attr('transform', 'translate(20,30)');

      scope.$watch('val', function (newVal, oldVal) {
        // clear the elements inside of the directive
        vis.selectAll('*').remove();

        // if 'val' is undefined, exit
        if (!newVal) {
          return;
        }

        var source = newVal;
        var nodes = source.nodes;
        var links = source.links;
        var initStateId = source.initStateId;

        // Update the links
        var link = vis.selectAll('path.link')
            .data(links);

        vis.append('svg:defs').selectAll('marker')
            .data(['arrowhead'])
          .enter().append('svg:marker')
            .attr('id', String)
            .attr('viewBox', '0 -5 10 10')
            .attr('refX', 25)
            .attr('refY', -1.5)
            .attr('markerWidth', 3)
            .attr('markerHeight', 3)
            .attr('orient', 'auto')
          .append('svg:path')
            .attr('d', 'M0,-5L10,0L0,5')
            .attr('fill', 'black');

        var linkEnter = link.enter().append('svg:g')
            .attr('class', 'link');

        linkEnter.insert('svg:path', 'g')
            .style('stroke-width', 3)
            .style('stroke', 'grey')
            .attr('opacity', 0.6)
            .attr('class', 'link')
            .attr('d', function(d) {
              var sourcex = d.source.x0 + (getTextWidth(d.source.name) / 2);
              var sourcey = d.source.y0 + 25;
              var targetx = d.target.x0 + (getTextWidth(d.target.name) / 2);
              var targety = d.target.y0 + 25;

              if (d.source == d.target) {
                return 'M' + sourcex + ' ' + sourcey;
              }

              var dx = targetx - sourcex;
              var dy = targety - sourcey;

              var dxperp = targety - sourcey;
              var dyperp = sourcex - targetx;
              var norm = Math.sqrt(dxperp*dxperp + dyperp*dyperp);
              dxperp /= norm;
              dyperp /= norm;

              var midx = sourcex + dx/2 + dxperp*20;
              var midy = sourcey + dy/2 + dyperp*20;

              // Draw a quadratic bezier curve.
              // TODO(sll): Add a point in the middle so that the arrowhead
              // can be moved there, using marker-mid. Alternatively, truncate
              // the links so that the arrows stay outside the boxes.
              return 'M' + sourcex + ' ' + sourcey + ' Q ' + midx + ' ' + midy +
                  ' ' + targetx + ' ' + targety;
            })
            .attr('marker-end', function(d) {
              if (d.source.x0 == d.target.x0 && d.source.y0 == d.target.y0) {
                return '';
              } else {
                return 'url(#arrowhead)';
              }
            });

        linkEnter.append('svg:text')
            .attr('dy', function(d) { return d.source.y0*0.5 + d.target.y0*0.5; })
            .attr('dx', function(d) {
              return d.source.x0*0.5 + d.target.x0*0.5 - DEFAULT_CIRCLE_RADIUS;
            })
            .attr('color', 'black')
            .text(function(d) {
              if (d.source.x0 == d.target.x0 && d.source.y0 == d.target.y0) {
                return '';
              }
              return '';
              // TODO(sll): display link text (return d.name) when we can do so
              // without all the text overlapping.
            });

        // Update the nodes
        // TODO(sll): Put a blue border around the current node.
        var node = vis.selectAll('g.node')
            .data(nodes, function(d) { return d.id; });

        var nodeEnter = node.enter().append('svg:g')
            .attr('class', 'node');

        // Add nodes to the canvas.
        nodeEnter.append('svg:rect')
            .attr('x', function(d) { return d.x0; })
            .attr('y', function(d) { return d.y0; })
            .attr('ry', function(d) { return 4; })
            .attr('rx', function(d) { return 4; })
            .attr('width', function(d) { return getTextWidth(d.name); })
            .attr('height', function(d) { return 40; })
            .attr('class', function(d) {
              if (d.hashId != END_DEST) {
                return 'clickable';
              }
            })
            .style('stroke', 'black')
            .style('stroke-width', function(d) {
              if (d.hashId == initStateId || d.hashId == END_DEST) {
                return '3';
              }
              return '2';
            })
            .style('fill', function(d) {
              if (d.hashId == initStateId) {
                return 'olive';
              } else if (d.hashId == END_DEST) {
                return 'green';
              } else if (d.reachable === false) {
                return 'pink';
              } else {
                return 'beige';
              }
            })
            .on('click', function (d) {
              if (d.hashId == END_DEST) {
                return;
              }
              explorationData.getStateData(d.hashId);
              scope.$parent.$parent.stateId = d.hashId;
              $('#editorViewTab a[href="#stateEditor"]').tab('show');
            })
            .append('svg:title')
            .text(function(d) { return d.name; });

        nodeEnter.append('svg:text')
            .attr('text-anchor', 'middle')
            .attr('dx', function(d) { return d.x0 + (getTextWidth(d.name) / 2); })
            .attr('dy', function(d) { return d.y0 + 25; })
            .text(function(d) { return scope.truncate(d.name); });

        // Add a 'delete node' handler.
        nodeEnter.append('svg:rect')
            .attr('y', function(d) { return d.y0; })
            .attr('x', function(d) { return d.x0; })
            .attr('height', 20)
            .attr('width', 20)
            .attr('opacity', 0) // comment out this line to see the delete target
            .attr('transform', function(d) {
              return 'translate(' + (getTextWidth(d.name) - 15) + ',' + (+0) + ')'; }
            )
            .attr('stroke-width', '0')
            .style('fill', 'pink')
            .on('click', function (d) {
              if (d.hashId == initStateId || d.hashId == END_DEST) {
                return;
              }
              scope.$parent.$parent.openDeleteStateModal(d.hashId);
            });

        nodeEnter.append('svg:text')
            .attr('text-anchor', 'right')
            .attr('dx', function(d) { return d.x0 + getTextWidth(d.name) -10; })
            .attr('dy', function(d) { return d.y0 + 10; })
            .text(function(d) {
              if (d.hashId == initStateId || d.hashId == END_DEST) {
                return;
              }
              return 'x';
            });
      });
    }
  };
});

/**
 * Injects dependencies in a way that is preserved by minification.
 */
EditorGraph.$inject = ['$scope', '$filter', 'explorationData'];
<commit_msg>Finish edits to exploration graph to make it look nicer.<commit_after>// Copyright 2012 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview Controllers for the editor's state graph.
 *
 * @author sll@google.com (Sean Lip)
 */

function EditorGraph($scope, $filter, explorationData) {
  $scope.updateViz = function() {
    explorationData.getData().then(function(data) {
      $scope.graphData = $scope.reformatResponse(
          data.states, data.init_state_id);
    });
  };

  // At the start, update the graph.
  $scope.updateViz();

  // Also update the graph when an update event is broadcast.
  $scope.$on('updateViz', $scope.updateViz);

  // Reformat the model into a response that is processable by d3.js.
  $scope.reformatResponse = function(states, initStateId) {
    var SENTINEL_DEPTH = 3000;
    var VERT_OFFSET = 20;
    var HORIZ_SPACING = 150;
    var VERT_SPACING = 100;
    var HORIZ_OFFSET = 100;
    var nodes = {};
    nodes[END_DEST] = {name: END_DEST, depth: SENTINEL_DEPTH, reachable: false};
    for (var state in states) {
      nodes[state] = {name: states[state].name, depth: SENTINEL_DEPTH, reachable: false};
    }
    nodes[initStateId].depth = 0;

    var maxDepth = 0;
    var seenNodes = [initStateId];
    var queue = [initStateId];
    var maxXDistPerLevel = {0: HORIZ_OFFSET};
    nodes[initStateId].y0 = VERT_OFFSET;
    nodes[initStateId].x0 = HORIZ_OFFSET;

    while (queue.length > 0) {
      var currNode = queue[0];
      queue.shift();
      nodes[currNode].reachable = true;
      if (currNode in states) {
        for (var i = 0; i < states[currNode].widget.rules.submit.length; i++) {
          // Assign levels to nodes only when they are first encountered.
          if (seenNodes.indexOf(states[currNode].widget.rules.submit[i].dest) == -1) {
            seenNodes.push(states[currNode].widget.rules.submit[i].dest);
            nodes[states[currNode].widget.rules.submit[i].dest].depth = nodes[currNode].depth + 1;
            nodes[states[currNode].widget.rules.submit[i].dest].y0 = (nodes[currNode].depth + 1) * VERT_SPACING + VERT_OFFSET;
            if (nodes[currNode].depth + 1 in maxXDistPerLevel) {
              nodes[states[currNode].widget.rules.submit[i].dest].x0 = maxXDistPerLevel[nodes[currNode].depth + 1] + HORIZ_SPACING;
              maxXDistPerLevel[nodes[currNode].depth + 1] += HORIZ_SPACING;
            } else {
              nodes[states[currNode].widget.rules.submit[i].dest].x0 = HORIZ_OFFSET;
              maxXDistPerLevel[nodes[currNode].depth + 1] = HORIZ_OFFSET;
            }
            maxDepth = Math.max(maxDepth, nodes[currNode].depth + 1);
            queue.push(states[currNode].widget.rules.submit[i].dest);
          }
        }
      }
    }

    var horizPositionForLastRow = HORIZ_OFFSET;
    for (var node in nodes) {
      if (nodes[node].depth == SENTINEL_DEPTH) {
        nodes[node].depth = maxDepth + 1;
        nodes[node].y0 = VERT_OFFSET + nodes[node].depth * VERT_SPACING;
        nodes[node].x0 = horizPositionForLastRow;
        horizPositionForLastRow += HORIZ_SPACING;
      }
    }

    // Assign unique IDs to each node.
    var idCount = 0;
    var nodeList = [];
    for (var node in nodes) {
      var nodeMap = nodes[node];
      nodeMap['hashId'] = node;
      nodeMap['id'] = idCount;
      nodes[node]['id'] = idCount;
      idCount++;
      nodeList.push(nodeMap);
    }

    var links = [];
    for (var state in states) {
      for (var i = 0; i < states[state].widget.rules.submit.length; i++) {
        links.push({
            source: nodeList[nodes[state].id],
            target: nodeList[nodes[states[state].widget.rules.submit[i].dest].id],
            name: $filter('parameterizeRule')({
                rule: states[state].widget.rules.submit[i].rule,
                inputs: states[state].widget.rules.submit[i].inputs
            })
        });
      }
    }

    return {nodes: nodeList, links: links, initStateId: initStateId};
  };
}

oppia.directive('stateGraphViz', function(explorationData) {
  // constants
  var w = 960,
      h = 4000,
      i = 0;
  var DEFAULT_CIRCLE_RADIUS = 30;
  // The following variable must be at least 3.
  var MAX_CATEGORY_LENGTH = 20;

  var getTextWidth = function(text) {
    return 40 + Math.min(MAX_CATEGORY_LENGTH, text.length) * 5;
  };

  return {
    restrict: 'E',
    scope: {
      val: '=',
      grouped: '='
    },
    link: function(scope, element, attrs) {
      scope.truncate = function(text) {
        if (text.length > MAX_CATEGORY_LENGTH) {
          return text.substring(0, MAX_CATEGORY_LENGTH - 3) + '...';
        } else {
          return text;
        }
      };

      var vis = d3.select(element[0]).append('svg:svg')
          .attr('width', w)
          .attr('height', h)
          .attr('class', 'oppia-graph-viz')
        .append('svg:g')
          .attr('transform', 'translate(20,30)');

      scope.$watch('val', function (newVal, oldVal) {
        // clear the elements inside of the directive
        vis.selectAll('*').remove();

        // if 'val' is undefined, exit
        if (!newVal) {
          return;
        }

        var source = newVal;
        var nodes = source.nodes;
        var links = source.links;
        var initStateId = source.initStateId;

        // Update the links
        var link = vis.selectAll('path.link')
            .data(links);

        vis.append('svg:defs').selectAll('marker')
            .data(['arrowhead'])
          .enter().append('svg:marker')
            .attr('id', String)
            .attr('viewBox', '0 -5 10 10')
            .attr('refX', 25)
            .attr('refY', -1.5)
            .attr('markerWidth', 3)
            .attr('markerHeight', 3)
            .attr('orient', 'auto')
          .append('svg:path')
            .attr('d', 'M0,-5L10,0L0,5')
            .attr('fill', 'black');

        var linkEnter = link.enter().append('svg:g')
            .attr('class', 'link');

        linkEnter.insert('svg:path', 'g')
            .style('stroke-width', 3)
            .style('stroke', 'grey')
            .attr('opacity', 0.6)
            .attr('class', 'link')
            .attr('d', function(d) {
              var sourceWidth = getTextWidth(d.source.name);
              var targetWidth = getTextWidth(d.target.name);

              var sourcex = d.source.x0 + sourceWidth/2;
              var sourcey = d.source.y0 + 20;
              var targetx = d.target.x0 + targetWidth/2;
              var targety = d.target.y0 + 20;

              if (d.source == d.target) {
                return 'M' + sourcex + ' ' + sourcey;
              }

              var dx = targetx - sourcex;
              var dy = targety - sourcey;

              /* Fractional amount of truncation to be applied to the end of
                 each link. */
              var startCutoff = (sourceWidth/2)/Math.abs(dx);
              var endCutoff = (targetWidth/2)/Math.abs(dx);
              if (dx === 0 || dy !== 0) {
                startCutoff = (dx === 0) ? 20.0/Math.abs(dy) : Math.min(
                    startCutoff, 20.0/Math.abs(dy));
                endCutoff = (dx === 0) ? 20.0/Math.abs(dy) : Math.min(
                    endCutoff, 20.0/Math.abs(dy));
              }

              var dxperp = targety - sourcey;
              var dyperp = sourcex - targetx;
              var norm = Math.sqrt(dxperp*dxperp + dyperp*dyperp);
              dxperp /= norm;
              dyperp /= norm;

              var midx = sourcex + dx/2 + dxperp*20;
              var midy = sourcey + dy/2 + dyperp*20;

              var startx = sourcex + startCutoff*dx;
              var starty = sourcey + startCutoff*dy;
              var endx = targetx - endCutoff*dx;
              var endy = targety - endCutoff*dy;

              // Draw a quadratic bezier curve.
              return 'M' + startx + ' ' + starty + ' Q ' + midx + ' ' + midy +
                  ' ' + endx + ' ' + endy;
            })
            .attr('marker-end', function(d) {
              if (d.source.x0 == d.target.x0 && d.source.y0 == d.target.y0) {
                return '';
              } else {
                return 'url(#arrowhead)';
              }
            });

        linkEnter.append('svg:text')
            .attr('dy', function(d) { return d.source.y0*0.5 + d.target.y0*0.5; })
            .attr('dx', function(d) {
              return d.source.x0*0.5 + d.target.x0*0.5 - DEFAULT_CIRCLE_RADIUS;
            })
            .attr('color', 'black')
            .text(function(d) {
              if (d.source.x0 == d.target.x0 && d.source.y0 == d.target.y0) {
                return '';
              }
              return '';
              // TODO(sll): display link text (return d.name) when we can do so
              // without all the text overlapping.
            });

        // Update the nodes
        // TODO(sll): Put a blue border around the current node.
        var node = vis.selectAll('g.node')
            .data(nodes, function(d) { return d.id; });

        var nodeEnter = node.enter().append('svg:g')
            .attr('class', 'node');

        // Add nodes to the canvas.
        nodeEnter.append('svg:rect')
            .attr('x', function(d) { return d.x0; })
            .attr('y', function(d) { return d.y0; })
            .attr('ry', function(d) { return 4; })
            .attr('rx', function(d) { return 4; })
            .attr('width', function(d) { return getTextWidth(d.name); })
            .attr('height', function(d) { return 40; })
            .attr('class', function(d) {
              if (d.hashId != END_DEST) {
                return 'clickable';
              }
            })
            .style('stroke', 'black')
            .style('stroke-width', function(d) {
              if (d.hashId == initStateId || d.hashId == END_DEST) {
                return '3';
              }
              return '2';
            })
            .style('fill', function(d) {
              if (d.hashId == initStateId) {
                return 'olive';
              } else if (d.hashId == END_DEST) {
                return 'green';
              } else if (d.reachable === false) {
                return 'pink';
              } else {
                return 'beige';
              }
            })
            .on('click', function (d) {
              if (d.hashId == END_DEST) {
                return;
              }
              explorationData.getStateData(d.hashId);
              scope.$parent.$parent.stateId = d.hashId;
              $('#editorViewTab a[href="#stateEditor"]').tab('show');
            })
            .append('svg:title')
            .text(function(d) { return d.name; });

        nodeEnter.append('svg:text')
            .attr('text-anchor', 'middle')
            .attr('dx', function(d) { return d.x0 + (getTextWidth(d.name) / 2); })
            .attr('dy', function(d) { return d.y0 + 25; })
            .text(function(d) { return scope.truncate(d.name); });

        // Add a 'delete node' handler.
        nodeEnter.append('svg:rect')
            .attr('y', function(d) { return d.y0; })
            .attr('x', function(d) { return d.x0; })
            .attr('height', 20)
            .attr('width', 20)
            .attr('opacity', 0) // comment out this line to see the delete target
            .attr('transform', function(d) {
              return 'translate(' + (getTextWidth(d.name) - 15) + ',' + (+0) + ')'; }
            )
            .attr('stroke-width', '0')
            .style('fill', 'pink')
            .on('click', function (d) {
              if (d.hashId == initStateId || d.hashId == END_DEST) {
                return;
              }
              scope.$parent.$parent.openDeleteStateModal(d.hashId);
            });

        nodeEnter.append('svg:text')
            .attr('text-anchor', 'right')
            .attr('dx', function(d) { return d.x0 + getTextWidth(d.name) -10; })
            .attr('dy', function(d) { return d.y0 + 10; })
            .text(function(d) {
              if (d.hashId == initStateId || d.hashId == END_DEST) {
                return;
              }
              return 'x';
            });
      });
    }
  };
});

/**
 * Injects dependencies in a way that is preserved by minification.
 */
EditorGraph.$inject = ['$scope', '$filter', 'explorationData'];
<|endoftext|>


<commit_before>import { expect } from "chai";
import { title } from "./";

describe(title, function() {

    describe("Part 1:", function() {

        it("should work", function() {

        });

    });

});
<commit_msg>Added specs for parse and getNeighbours.<commit_after>import { expect } from "chai";
import { title } from "./";
import _ from "lodash-fp";

describe(title, function() {

    describe("Part 1:", function() {

        describe("parse", function() {

            it("should return [['.', '#']] given '.#'", function() {
                const input = ".#";

                expect(parse(input)).to.eql([[".", "#"]]);
            });

            it("should return [['.', '.'], ['#', '.']] given '..\\n#.'", function() {
                const input = [
                    "..",
                    "#."
                ].join("\n");

                expect(parse(input)).to.eql([
                    [".", "."],
                    ["#", "."]
                ]);
            });

            it("should parse the initial state example correctly", function() {
                const input = [
                    ".#.#.#",
                    "...##.",
                    "#....#",
                    "..#...",
                    "#.#..#",
                    "####.."
                ].join("\n");

                expect(parse(input)).to.eql([
                    [".", "#", ".", "#", ".", "#"],
                    [".", ".", ".", "#", "#", "."],
                    ["#", ".", ".", ".", ".", "#"],
                    [".", ".", "#", ".", ".", "."],
                    ["#", ".", "#", ".", ".", "#"],
                    ["#", "#", "#", "#", ".", "."]
                ]);
            });

        });

        describe("getNeighbours", function() {

            beforeEach(function() {
                const input = [
                    "..##..",
                    ".#..#.",
                    "#....#"
                ].join("\n");

                this.grid = parse(input);
            });

            it("should return an array of all 8 strings, given the coordinate [1, 1]", function() {
                const coordinate = [1, 1];
                const neighbours = getNeighbours(this.grid, coordinate);
                const characters = _.groupBy(_.identity, neighbours);

                expect(characters["."].length).to.eq(6);
                expect(characters["#"].length).to.eq(2);
            });

            it("should return an array of 3 strings, given the coordinate [0, 0]", function() {
                const coordinate = [0, 0];
                const neighbours = getNeighbours(this.grid, coordinate);
                const characters = _.groupBy(_.identity, neighbours);

                expect(characters["."].length).to.eq(2);
                expect(characters["#"].length).to.eq(1);
            });

            it("should return an array of 5 strings, given the coordinate [0, 1]", function() {
                const coordinate = [0, 1];
                const neighbours = getNeighbours(this.grid, coordinate);
                const characters = _.groupBy(_.identity, neighbours);

                expect(characters["."].length).to.eq(3);
                expect(characters["#"].length).to.eq(2);
            });

        });

        xdescribe("animate", function() {

            it("should work", function() {

            });

        });

    });

});
<|endoftext|>


<commit_before>// CKEditor TeXZilla Plugin
// Copyright (C) 2014  Raniere Silva
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

CKEDITOR.dialog.add('texzillaDialog', function( editor ) {
    return {
        title: 'TeXZilla Edit Box',
        minWidth: 300,
        minHeight: 300,
        contents: [
            {
                id: 'basic',
                label: 'Basic Settings',
                elements: [
                    {
                        id: 'tex',
                        type: 'textarea',
                        label: '(La)TeX code (Click outside the textarea to update the preview)',
                        setup: function(insertMode, element) {
                            if (!insertMode)
                                this.setValue(TeXZilla.getTeXSource(element.$));
                        },
                        onChange: function() {
                            var preview = document.getElementById('Preview');
                            // Clean previous preview
                            var previous = preview.childNodes;
                            for(i = 0; i < previous.length; i++) {
                                preview.removeChild(previous[i]);
                            }
                            var mathElement = TeXZilla.toMathML(this.getValue());

                            // Check for error
                            var has_error = false;
                            for (var i = 0; i < mathElement.lastElementChild.childElementCount; i++) {
                                if (mathElement.lastElementChild.children[i].localName === 'merror') {
                                    has_error = true;
                                }
                            }

                            var dialog = CKEDITOR.dialog.getCurrent();
                            // Disable button if errer
                            if (has_error === true) {
                                dialog.disableButton("ok");
                            }
                            else {
                                dialog.enableButton("ok");
                            }
                            preview.appendChild(mathElement);
                        }
                    },
                    {
                        id: 'display',
                        type: 'checkbox',
                        label: 'Display',
                        setup: function(insertMode, element) {
                            var display = element.getAttribute("display");
                            if (display === 'block') {
                                this.setValue(true);
                            }
                            else {
                                this.setValue(false);
                            }
                        }
                    },
                    {
                        id: 'direction',
                        type: 'checkbox',
                        label: 'RTL',
                        setup: function(insertMode, element) {
                            var direction = element.getAttribute("dir");
                            if (direction === 'rtl') {
                                this.setValue(true);
                            }
                            else {
                                this.setValue(false);
                            }
                        }
                    },
                    {
                        id: 'preview',
                        type: 'html',
                        label: 'Preview',
                        // Width of the preview box is based on the minWidth of the dialog window.
                        html: '

Preview:

',
                        setup: function(insertMode, element) {
                            var preview = document.getElementById('Preview');
                            // Clean previous preview
                            var previous = preview.childNodes;
                            for(i = 0; i < previous.length; i++) {
                                preview.removeChild(previous[i]);
                            }
                            if (!insertMode)
                                preview.appendChild(element.$.cloneNode(true));
                        }
                    }
                ]
            }
        ],
        onShow: function() {
            var selection = editor.getSelection();
            var element = selection.getStartElement();
            // Try to locate a `math` or `body` tag.
            while (element.getName() != 'math' &&
                    element.getName() != 'body') {
                element = element.getParent();
            }
            if (!element || element.getName() != 'math') {
                this.insertMode = true;
            }
            else {
                this.insertMode = false;
                this.mathRoot = element;
            }
            // invoke the setup functions for the element
            this.setupContent( this.insertMode, element );
        },
        onOk: function() {
          var dialog = this;

          // This is the better way to insert the MathML, although we got
          //
          //     TypeError: element.getName is not a function
          //
          // when using it.
          //
          // var mathElement = TeXZilla.toMathML(dialog.getValueOf('basic', 'tex'));

          // This is a hack found at
          // http://stackoverflow.com/a/17339275/1802726.
          var math = TeXZilla.toMathMLString(
                  dialog.getValueOf('basic', 'tex'),
                  dialog.getValueOf('basic', 'display'),
                  dialog.getValueOf('basic', 'direction'));
          var mathElement = CKEDITOR.dom.element.createFromHtml(math, editor.document);

          if(!this.insertMode) {
              // Remove old equation
              this.mathRoot.$.parentNode.removeChild(this.mathRoot.$);
          }
          editor.insertElement(mathElement);
        }
    };
} );
<commit_msg>Fix preview<commit_after>// CKEditor TeXZilla Plugin
// Copyright (C) 2014  Raniere Silva
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

function update_preview() {
    var dialog = CKEDITOR.dialog.getCurrent();

    var preview = document.getElementById('Preview');
    // Clean previous preview
    var previous = preview.childNodes;
    for(i = 0; i < previous.length; i++) {
        preview.removeChild(previous[i]);
    }
    var mathElement = TeXZilla.toMathML(
            dialog.getValueOf('basic', 'tex'),
            dialog.getValueOf('basic', 'display'),
            dialog.getValueOf('basic', 'direction'));

    // Check for error
    var has_error = false;
    for (var i = 0; i < mathElement.lastElementChild.childElementCount; i++) {
        if (mathElement.lastElementChild.children[i].localName === 'merror') {
            has_error = true;
        }
    }

    // Disable button if errer
    if (has_error === true) {
        dialog.disableButton("ok");
    }
    else {
        dialog.enableButton("ok");
    }
    preview.appendChild(mathElement);
}

CKEDITOR.dialog.add('texzillaDialog', function( editor ) {
    return {
        title: 'TeXZilla Edit Box',
        minWidth: 300,
        minHeight: 300,
        contents: [
            {
                id: 'basic',
                label: 'Basic Settings',
                elements: [
                    {
                        id: 'tex',
                        type: 'textarea',
                        label: '(La)TeX code (Click outside the textarea to update the preview)',
                        setup: function(insertMode, element) {
                            if (!insertMode)
                                this.setValue(TeXZilla.getTeXSource(element.$));
                        },
                        onChange: function() {
                            update_preview()
                        }
                    },
                    {
                        id: 'display',
                        type: 'checkbox',
                        label: 'Display',
                        setup: function(insertMode, element) {
                            var display = element.getAttribute("display");
                            if (display === 'block') {
                                this.setValue(true);
                            }
                            else {
                                this.setValue(false);
                            }
                        },
                        onChange: function() {
                            update_preview()
                        }
                    },
                    {
                        id: 'direction',
                        type: 'checkbox',
                        label: 'RTL',
                        setup: function(insertMode, element) {
                            var direction = element.getAttribute("dir");
                            if (direction === 'rtl') {
                                this.setValue(true);
                            }
                            else {
                                this.setValue(false);
                            }
                        },
                        onChange: function() {
                            update_preview()
                        }
                    },
                    {
                        id: 'preview',
                        type: 'html',
                        label: 'Preview',
                        // Width of the preview box is based on the minWidth of the dialog window.
                        html: '

Preview:

',
                        setup: function(insertMode, element) {
                            var preview = document.getElementById('Preview');
                            // Clean previous preview
                            var previous = preview.childNodes;
                            for(i = 0; i < previous.length; i++) {
                                preview.removeChild(previous[i]);
                            }
                            if (!insertMode)
                                preview.appendChild(element.$.cloneNode(true));
                        }
                    }
                ]
            }
        ],
        onShow: function() {
            var selection = editor.getSelection();
            var element = selection.getStartElement();
            // Try to locate a `math` or `body` tag.
            while (element.getName() != 'math' &&
                    element.getName() != 'body') {
                element = element.getParent();
            }
            if (!element || element.getName() != 'math') {
                this.insertMode = true;
            }
            else {
                this.insertMode = false;
                this.mathRoot = element;
            }
            // invoke the setup functions for the element
            this.setupContent( this.insertMode, element );
        },
        onOk: function() {
          var dialog = this;

          // This is the better way to insert the MathML, although we got
          //
          //     TypeError: element.getName is not a function
          //
          // when using it.
          //
          // var mathElement = TeXZilla.toMathML(dialog.getValueOf('basic', 'tex'));

          // This is a hack found at
          // http://stackoverflow.com/a/17339275/1802726.
          var math = TeXZilla.toMathMLString(
                  dialog.getValueOf('basic', 'tex'),
                  dialog.getValueOf('basic', 'display'),
                  dialog.getValueOf('basic', 'direction'));
          var mathElement = CKEDITOR.dom.element.createFromHtml(math, editor.document);

          if(!this.insertMode) {
              // Remove old equation
              this.mathRoot.$.parentNode.removeChild(this.mathRoot.$);
          }
          editor.insertElement(mathElement);
        }
    };
} );
<|endoftext|>