<commit_before>function add_chat_item(name, msg, time){

    item = '
';
    item = item + ' ';
    item = item + '
';
    item = item + '    ';
    item = item + '       ' + time +' UTC';
    item = item + name;
    item = item + '    ';
    item = item + msg;
    item = item + '
';
    item = item + '
';

    $('#bot_chat').prepend(item);
}

function WebSocketTest(server) {
    if ("WebSocket" in window) {
        var ws = new WebSocket("ws://" + server + "/ws/");
        ws.onopen = function() {
            // toggle_status('on');
            // ws.send("Connection established");
        };
        ws.onmessage = function (evt) {
            var type = evt.data.split(' ', 1)[0];
            var received_msg = evt.data.substring(evt.data.indexOf(' ') + 1)

            var msg = jQuery.parseJSON(received_msg);

            if (type == 'PAN001'){
                add_chat_item(type, msg.message, msg.timestamp);
            }
            if (type == 'STATUS'){
                update_info(msg['observatory']);
                change_state(msg['state']);
                refresh_images();
            }
            if (type == 'WEATHER'){
                update_weather(msg);
            }
        };
        ws.onclose = function() {
            toggle_status('off');
        };
    } else {
        toggle_status('error');
    }
}

function update_weather(info){
    var box = $('#weather_info');

    box.html();
    $.each(info['data'], function(name, val){
        var item = '
' + name + '
';
        item = item + '
' + val + '
';

        box.append(item);
    });
}

function toggle_status(status){
    var icon = $('.current_state i');
    var text = $('.current_state span');

    icon.removeClass().addClass('fa');
    if (status == 'on'){
        icon.addClass('fa-circle');
        text.html('Online');
    } else if (status == 'off'){
        icon.addClass('fa-bolt').addClass('text-danger');
        text.html('Offline').addClass('text-danger');
    } else {
        icon.addClass('fa-exclamation-triangle', 'text-danger').addClass('text-danger');
        text.html('Error').addClass('text-danger');
    }
}

function change_state(state){
    var icon = $('.current_state i');
    var text = $('.current_state span');

    icon.removeClass().addClass('fa');
    text.html(state);
    switch(state) {
        case 'Analyzing':
            icon.addClass('fa-calculator');
            break;
        case 'Tracking':
            icon.addClass('fa-binoculars');
            break;
        case 'Observing':
            icon.addClass('fa-camera');
            break;
        case 'Pointing':
            icon.addClass('fa-dot-circle-o');
            break;
        case 'Slewing':
            icon.addClass('fa-cog fa-spin');
            break;
        case 'Scheduling':
            icon.addClass('fa-tasks');
            break;
        case 'Ready':
            icon.addClass('fa-thumbs-o-up');
            break;
        case 'Parking':
        case 'Parked':
            icon.addClass('fa-car');
            break;
        case 'Sleeping':
            icon.addClass('fa-check-circle');
            break;
        default:
            icon.addClass('fa-circle');
    }
}

// Find all the elements with the class that matches a return value
// and update their html
function update_info(status){
    $.each(status, function(key, val){
        $('.' + key).each(function(idx, elem){
            $(elem).html(val);
        })
    });
}

// Refresh all images with `img_refresh` container class
function refresh_images(){
    console.log("Refreshing images")
    $.each($('.img_refresh img'), function(idx, img){
        reload_img(img);
    });
}

// Reload individual image
function reload_img(img){
    base = $(img).attr('src').split('?')[0];

    // Hack for others
    if(base.startsWith('http')){
        new_src = $(img).attr('src');
    } else {
        new_src = base + '?' + Math.random()
    }

    $(img).attr('src', new_src);
}

// Startup
$( document ).ready(function() {
    // Image refresh timer
    second = 1000;

    WebSocketTest(window.location.host);

    // Refresh images
    // setInterval(refresh_images, 15 * second);
})
<commit_msg>Clear box properly<commit_after>function add_chat_item(name, msg, time){

    item = '
';
    item = item + ' ';
    item = item + '
';
    item = item + '    ';
    item = item + '       ' + time +' UTC';
    item = item + name;
    item = item + '    ';
    item = item + msg;
    item = item + '
';
    item = item + '
';

    $('#bot_chat').prepend(item);
}

function WebSocketTest(server) {
    if ("WebSocket" in window) {
        var ws = new WebSocket("ws://" + server + "/ws/");
        ws.onopen = function() {
            // toggle_status('on');
            // ws.send("Connection established");
        };
        ws.onmessage = function (evt) {
            var type = evt.data.split(' ', 1)[0];
            var received_msg = evt.data.substring(evt.data.indexOf(' ') + 1)

            var msg = jQuery.parseJSON(received_msg);

            if (type == 'PAN001'){
                add_chat_item(type, msg.message, msg.timestamp);
            }
            if (type == 'STATUS'){
                update_info(msg['observatory']);
                change_state(msg['state']);
                refresh_images();
            }
            if (type == 'WEATHER'){
                update_weather(msg);
            }
        };
        ws.onclose = function() {
            toggle_status('off');
        };
    } else {
        toggle_status('error');
    }
}

function update_weather(info){
    var box = $('#weather_info');

    box.html('');
    $.each(info['data'], function(name, val){
        var item = '
' + name + '
';
        item = item + '
' + val + '
';

        box.append(item);
    });
}

function toggle_status(status){
    var icon = $('.current_state i');
    var text = $('.current_state span');

    icon.removeClass().addClass('fa');
    if (status == 'on'){
        icon.addClass('fa-circle');
        text.html('Online');
    } else if (status == 'off'){
        icon.addClass('fa-bolt').addClass('text-danger');
        text.html('Offline').addClass('text-danger');
    } else {
        icon.addClass('fa-exclamation-triangle', 'text-danger').addClass('text-danger');
        text.html('Error').addClass('text-danger');
    }
}

function change_state(state){
    var icon = $('.current_state i');
    var text = $('.current_state span');

    icon.removeClass().addClass('fa');
    text.html(state);
    switch(state) {
        case 'Analyzing':
            icon.addClass('fa-calculator');
            break;
        case 'Tracking':
            icon.addClass('fa-binoculars');
            break;
        case 'Observing':
            icon.addClass('fa-camera');
            break;
        case 'Pointing':
            icon.addClass('fa-dot-circle-o');
            break;
        case 'Slewing':
            icon.addClass('fa-cog fa-spin');
            break;
        case 'Scheduling':
            icon.addClass('fa-tasks');
            break;
        case 'Ready':
            icon.addClass('fa-thumbs-o-up');
            break;
        case 'Parking':
        case 'Parked':
            icon.addClass('fa-car');
            break;
        case 'Sleeping':
            icon.addClass('fa-check-circle');
            break;
        default:
            icon.addClass('fa-circle');
    }
}

// Find all the elements with the class that matches a return value
// and update their html
function update_info(status){
    $.each(status, function(key, val){
        $('.' + key).each(function(idx, elem){
            $(elem).html(val);
        })
    });
}

// Refresh all images with `img_refresh` container class
function refresh_images(){
    console.log("Refreshing images")
    $.each($('.img_refresh img'), function(idx, img){
        reload_img(img);
    });
}

// Reload individual image
function reload_img(img){
    base = $(img).attr('src').split('?')[0];

    // Hack for others
    if(base.startsWith('http')){
        new_src = $(img).attr('src');
    } else {
        new_src = base + '?' + Math.random()
    }

    $(img).attr('src', new_src);
}

// Startup
$( document ).ready(function() {
    // Image refresh timer
    second = 1000;

    WebSocketTest(window.location.host);

    // Refresh images
    // setInterval(refresh_images, 15 * second);
})
<|endoftext|>


<commit_before>/*
Case Conductor is a Test Case Management system.
Copyright (C) 2011-2012 Mozilla

This file is part of Case Conductor.

Case Conductor 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.

Case Conductor 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 Case Conductor.  If not, see <http://www.gnu.org/licenses/>.
*/
/*jslint    browser:    true,
            indent:     4 */
/*global    ich, jQuery, VALID_ENVIRONMENTS */

var CC = (function (CC, $) {

    'use strict';

    // hide empty run-tests environments form on initial load
    CC.hideEmptyRuntestsEnv = function () {
        $('.runenvselect.empty').hide();
    };

    // Add focus to ``invalid`` and ``fail`` textboxes when expanded
    CC.autoFocus = function (trigger, context) {
        $(context).on('click', trigger, function () {
            if ($(this).parent().hasClass('open')) {
                $(this).parent().find('textarea').focus();
            }
        });
    };

    // Open hierarchical navigation directly to clicked breadcrumb link
    CC.breadcrumb = function (context) {
        var finder = $(context);
        finder.find('.runsdrill .colcontent').each(function () {
            $(this).data('originalHTML', $(this).html());
        });
        $('.runtests-nav .secondary .breadcrumb').click(function (e) {
            if (!finder.hasClass('open')) {
                finder.children('.summary').click();
            }
            finder.find('.runsdrill .colcontent').each(function () {
                $(this).html($(this).data('originalHTML'));
            });
            finder.find('#' + $(this).data('id')).click();
            e.preventDefault();
        });
    };

    // Expand all tests on bulk-open
    CC.expandAllTests = function (container) {
        var context = $(container),
            trigger = context.find('.itemlist .listordering .bybulk-open > a'),
            target,
            updateTrigger = function () {
                if (context.find('.itemlist .listitem .itembody.details').length === context.find('.itemlist .listitem .itembody.details.open').length) {
                    trigger.addClass('open');
                } else {
                    trigger.removeClass('open');
                }
            };

        trigger.click(function (e) {
            target = context.find('.itemlist .listitem .itembody.details');
            trigger.toggleClass('open');
            if (trigger.hasClass('open')) {
                target.each(function () {
                    if (!($(this).hasClass('open'))) {
                        $(this).children('.item-summary').click();
                    }
                });
            } else {
                target.each(function () {
                    if ($(this).hasClass('open')) {
                        $(this).children('.item-summary').click();
                    }
                });
            }
            e.preventDefault();
        });

        context.on('click', '.itemlist .listitem .itembody.details > .item-summary.summary', function () {
            updateTrigger();
        });

        updateTrigger();
    };

    // Ajax submit runtest forms
    CC.runTests = function (container) {
        var context = $(container),
            tests = context.find('.listitem'),
            ajaxFormsInit = function (test) {
                var forms = test.find('form');

                forms.ajaxForm({
                    beforeSubmit: function (arr, form, options) {
                        test.loadingOverlay();
                    },
                    success: function (response) {
                        var newTest = $(response.html);
                        test.loadingOverlay('remove');
                        if (response.html) {
                            test.replaceWith(newTest);
                            ajaxFormsInit(newTest);
                            newTest.find('.details').html5accordion();
                        }
                    }
                });
            },
            ajaxifyTests = function () {
                tests.each(function () {
                    var thisTest = $(this);
                    ajaxFormsInit(thisTest);
                });
            };

        ajaxifyTests();

        // Re-attach ajax-form handlers after list is ajax-replaced (sorting/filtering called in listpages.js)
        context.on('after-replace', '.itemlist.action-ajax-replace', function (event, replacement) {
            tests = context.find('.listitem');
            ajaxifyTests();
        });
    };

    // Enable/disable failed test bug URL input on-select
    CC.failedTestBug = function (container) {
        var context = $(container);

        context.on('change', '.listitem .stepfail input[type="radio"][name="bug"]', function () {
            var thisList = $(this).closest('.stepfail'),
                newBug = thisList.find('input[type="radio"].newbug-radio'),
                newBugInput = thisList.find('input[type="url"].newbug-input');

            if (newBug.is(':checked')) {
                newBugInput.removeClass('disabled').attr('name', 'bug').focus();
            } else {
                newBugInput.addClass('disabled').attr('name', 'disabled-bug');
            }
        });

        context.on('click', '.listitem .stepfail input[type="url"].newbug-input.disabled', function () {
            var newBugRadio = $(this).siblings('.newbug-radio');
            newBugRadio.click();
        });
    };

    // Clicking anywhere on test header expands test
    CC.expandTestDetails = function (container) {
        var context = $(container);

        context.on('click', '.itemlist .listitem .itemhead', function (e) {
            if (!($(e.target).is('button'))) {
                $(this).closest('.listitem').find('.itembody .item-summary').click();
            }
        });
    };

    // Filter environment form options
    CC.filterEnvironments = function (container) {
        var context = $(container),
            triggers = context.find('select'),
            doFilter;

        if (context.length) {
            triggers.each(function () {
                var allopts = $(this).find('option').clone().removeAttr('selected');
                $(this).data('allopts', allopts);
            });

            doFilter = function () {
                var i,
                    key = triggers.map(function () {
                        if ($(this).find('option:selected').val()) {
                            return parseInt($(this).find('option:selected').val(), 10);
                        } else {
                            return '';
                        }
                    }),
                    filterCombos = function (elementOfArray, indexInArray) {
                        if (elementOfArray[i] === null) {
                            return true;
                        } else {
                            return elementOfArray[i] === key[i];
                        }
                    };

                for (i = 0; i < key.length; i = i + 1) {
                    if (key[i] === '') {
                        key[i] = null;
                    }
                }

                if (VALID_ENVIRONMENTS) {
                    triggers.each(function () {
                        var thisIndex = triggers.index($(this)),
                            validOpts = [],
                            acceptAll = false,
                            allopts = $(this).data('allopts'),
                            filteredValidCombos = VALID_ENVIRONMENTS,
                            filteredOpts,
                            selectedVal;

                        for (i = 0; i < key.length; i = i + 1) {
                            if (key[i] !== null && i !== thisIndex) {
                                filteredValidCombos = $.grep(filteredValidCombos, filterCombos);
                            }
                        }

                        validOpts = $.map(filteredValidCombos, function (elementOfArray, indexInArray) {
                            if (elementOfArray[thisIndex] === null) {
                                acceptAll = true;
                            }
                            return elementOfArray[thisIndex];
                        });

                        if ($(this).find('option:selected').val()) {
                            selectedVal = $(this).find('option:selected').val();
                        }

                        if (acceptAll) {
                            $(this).html(allopts);
                        } else {
                            filteredOpts = allopts.filter(function (index) {
                                if ($(this).val() === '') {
                                    return true;
                                } else {
                                    return $.inArray(parseInt($(this).val(), 10), validOpts) !== -1;
                                }
                            });
                            $(this).html(filteredOpts);
                        }

                        if (selectedVal) {
                            $(this).find('option').filter(function () {
                                return $(this).val() === selectedVal;
                            }).prop('selected', true);
                        } else {
                            $(this).find('option').first().prop('selected', true);
                        }
                    });
                }
            };

            triggers.change(doFilter);
        }
    };

    return CC;

}(CC || {}, jQuery));<commit_msg>clicking runtest .filter-link does not expand/collapse test details<commit_after>/*
Case Conductor is a Test Case Management system.
Copyright (C) 2011-2012 Mozilla

This file is part of Case Conductor.

Case Conductor 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.

Case Conductor 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 Case Conductor.  If not, see <http://www.gnu.org/licenses/>.
*/
/*jslint    browser:    true,
            indent:     4 */
/*global    ich, jQuery, VALID_ENVIRONMENTS */

var CC = (function (CC, $) {

    'use strict';

    // hide empty run-tests environments form on initial load
    CC.hideEmptyRuntestsEnv = function () {
        $('.runenvselect.empty').hide();
    };

    // Add focus to ``invalid`` and ``fail`` textboxes when expanded
    CC.autoFocus = function (trigger, context) {
        $(context).on('click', trigger, function () {
            if ($(this).parent().hasClass('open')) {
                $(this).parent().find('textarea').focus();
            }
        });
    };

    // Open hierarchical navigation directly to clicked breadcrumb link
    CC.breadcrumb = function (context) {
        var finder = $(context);
        finder.find('.runsdrill .colcontent').each(function () {
            $(this).data('originalHTML', $(this).html());
        });
        $('.runtests-nav .secondary .breadcrumb').click(function (e) {
            if (!finder.hasClass('open')) {
                finder.children('.summary').click();
            }
            finder.find('.runsdrill .colcontent').each(function () {
                $(this).html($(this).data('originalHTML'));
            });
            finder.find('#' + $(this).data('id')).click();
            e.preventDefault();
        });
    };

    // Expand all tests on bulk-open
    CC.expandAllTests = function (container) {
        var context = $(container),
            trigger = context.find('.itemlist .listordering .bybulk-open > a'),
            target,
            updateTrigger = function () {
                if (context.find('.itemlist .listitem .itembody.details').length === context.find('.itemlist .listitem .itembody.details.open').length) {
                    trigger.addClass('open');
                } else {
                    trigger.removeClass('open');
                }
            };

        trigger.click(function (e) {
            target = context.find('.itemlist .listitem .itembody.details');
            trigger.toggleClass('open');
            if (trigger.hasClass('open')) {
                target.each(function () {
                    if (!($(this).hasClass('open'))) {
                        $(this).children('.item-summary').click();
                    }
                });
            } else {
                target.each(function () {
                    if ($(this).hasClass('open')) {
                        $(this).children('.item-summary').click();
                    }
                });
            }
            e.preventDefault();
        });

        context.on('click', '.itemlist .listitem .itembody.details > .item-summary.summary', function () {
            updateTrigger();
        });

        updateTrigger();
    };

    // Ajax submit runtest forms
    CC.runTests = function (container) {
        var context = $(container),
            tests = context.find('.listitem'),
            ajaxFormsInit = function (test) {
                var forms = test.find('form');

                forms.ajaxForm({
                    beforeSubmit: function (arr, form, options) {
                        test.loadingOverlay();
                    },
                    success: function (response) {
                        var newTest = $(response.html);
                        test.loadingOverlay('remove');
                        if (response.html) {
                            test.replaceWith(newTest);
                            ajaxFormsInit(newTest);
                            newTest.find('.details').html5accordion();
                        }
                    }
                });
            },
            ajaxifyTests = function () {
                tests.each(function () {
                    var thisTest = $(this);
                    ajaxFormsInit(thisTest);
                });
            };

        ajaxifyTests();

        // Re-attach ajax-form handlers after list is ajax-replaced (sorting/filtering called in listpages.js)
        context.on('after-replace', '.itemlist.action-ajax-replace', function (event, replacement) {
            tests = context.find('.listitem');
            ajaxifyTests();
        });
    };

    // Enable/disable failed test bug URL input on-select
    CC.failedTestBug = function (container) {
        var context = $(container);

        context.on('change', '.listitem .stepfail input[type="radio"][name="bug"]', function () {
            var thisList = $(this).closest('.stepfail'),
                newBug = thisList.find('input[type="radio"].newbug-radio'),
                newBugInput = thisList.find('input[type="url"].newbug-input');

            if (newBug.is(':checked')) {
                newBugInput.removeClass('disabled').attr('name', 'bug').focus();
            } else {
                newBugInput.addClass('disabled').attr('name', 'disabled-bug');
            }
        });

        context.on('click', '.listitem .stepfail input[type="url"].newbug-input.disabled', function () {
            var newBugRadio = $(this).siblings('.newbug-radio');
            newBugRadio.click();
        });
    };

    // Clicking anywhere on test header expands test
    CC.expandTestDetails = function (container) {
        var context = $(container);

        context.on('click', '.itemlist .listitem .itemhead', function (e) {
            if (!($(e.target).is('button') || $(e.target).is('.filter-link'))) {
                $(this).closest('.listitem').find('.itembody .item-summary').click();
            }
        });
    };

    // Filter environment form options
    CC.filterEnvironments = function (container) {
        var context = $(container),
            triggers = context.find('select'),
            doFilter;

        if (context.length) {
            triggers.each(function () {
                var allopts = $(this).find('option').clone().removeAttr('selected');
                $(this).data('allopts', allopts);
            });

            doFilter = function () {
                var i,
                    key = triggers.map(function () {
                        if ($(this).find('option:selected').val()) {
                            return parseInt($(this).find('option:selected').val(), 10);
                        } else {
                            return '';
                        }
                    }),
                    filterCombos = function (elementOfArray, indexInArray) {
                        if (elementOfArray[i] === null) {
                            return true;
                        } else {
                            return elementOfArray[i] === key[i];
                        }
                    };

                for (i = 0; i < key.length; i = i + 1) {
                    if (key[i] === '') {
                        key[i] = null;
                    }
                }

                if (VALID_ENVIRONMENTS) {
                    triggers.each(function () {
                        var thisIndex = triggers.index($(this)),
                            validOpts = [],
                            acceptAll = false,
                            allopts = $(this).data('allopts'),
                            filteredValidCombos = VALID_ENVIRONMENTS,
                            filteredOpts,
                            selectedVal;

                        for (i = 0; i < key.length; i = i + 1) {
                            if (key[i] !== null && i !== thisIndex) {
                                filteredValidCombos = $.grep(filteredValidCombos, filterCombos);
                            }
                        }

                        validOpts = $.map(filteredValidCombos, function (elementOfArray, indexInArray) {
                            if (elementOfArray[thisIndex] === null) {
                                acceptAll = true;
                            }
                            return elementOfArray[thisIndex];
                        });

                        if ($(this).find('option:selected').val()) {
                            selectedVal = $(this).find('option:selected').val();
                        }

                        if (acceptAll) {
                            $(this).html(allopts);
                        } else {
                            filteredOpts = allopts.filter(function (index) {
                                if ($(this).val() === '') {
                                    return true;
                                } else {
                                    return $.inArray(parseInt($(this).val(), 10), validOpts) !== -1;
                                }
                            });
                            $(this).html(filteredOpts);
                        }

                        if (selectedVal) {
                            $(this).find('option').filter(function () {
                                return $(this).val() === selectedVal;
                            }).prop('selected', true);
                        } else {
                            $(this).find('option').first().prop('selected', true);
                        }
                    });
                }
            };

            triggers.change(doFilter);
        }
    };

    return CC;

}(CC || {}, jQuery));<|endoftext|>


<commit_before>var viewport = (function () {
var exports = {};

var jwindow;
var dimensions = {};
var in_stoppable_autoscroll = false;

exports.at_top = function () {
    return (exports.scrollTop() <= 0);
};

exports.message_viewport_info = function () {
    // Return a structure that tells us details of the viewport
    // accounting for fixed elements like the top navbar.
    //
    // The message_header is NOT considered to be part of the visible
    // message pane, which should make sense for callers, who will
    // generally be concerned about whether actual message content is
    // visible.

    var res = {};

    var element_just_above_us = $(".floating_recipient");

    res.visible_top = element_just_above_us.offset().top
        + element_just_above_us.outerHeight();

    var element_just_below_us = $("#compose");

    res.visible_height =
        element_just_below_us.position().top
        - res.visible_top;

    return res;
};

exports.at_bottom = function () {
    var bottom = exports.scrollTop() + exports.height();
    var full_height = exports.message_pane.prop('scrollHeight');

    // We only know within a pixel or two if we're
    // exactly at the bottom, due to browser quirkiness,
    // and we err on the side of saying that we are at
    // the bottom.
    return bottom + 2 >= full_height;
};

// This differs from at_bottom in that it only requires the bottom message to
// be visible, but you may be able to scroll down further.
exports.bottom_message_visible = function () {
    var last_row = rows.last_visible();
    if (last_row.length) {
        var message_bottom = last_row[0].getBoundingClientRect().bottom;
        var bottom_of_feed = $("#compose")[0].getBoundingClientRect().top;
        return bottom_of_feed > message_bottom;
    } else {
        return false;
    }
};

exports.is_below_visible_bottom = function (offset) {
    return offset > exports.scrollTop() + exports.height() - $("#compose").height();
};

exports.set_message_position = function (message_top, message_height, viewport_info, ratio) {
    // message_top = offset of the top of a message that you are positioning
    // message_height = height of the message that you are positioning
    // viewport_info = result of calling viewport.message_viewport_info
    // ratio = fraction indicating how far down the screen the msg should be

    var how_far_down_in_visible_page = viewport_info.visible_height * ratio;

    // special case: keep large messages fully on the screen
    if (how_far_down_in_visible_page + message_height > viewport_info.visible_height) {
        how_far_down_in_visible_page = viewport_info.visible_height - message_height;

        // Next handle truly gigantic messages.  We just say that the top of the
        // message goes to the top of the viewing area.  Realistically, gigantic
        // messages should either be condensed, socially frowned upon, or scrolled
        // with the mouse.
        if (how_far_down_in_visible_page < 0) {
            how_far_down_in_visible_page = 0;
        }
    }

    var hidden_top =
        viewport_info.visible_top
        - exports.scrollTop();

    var message_offset =
        how_far_down_in_visible_page
        + hidden_top;

    var new_scroll_top =
        message_top
        - message_offset;

    suppress_scroll_pointer_update = true; // Gets set to false in the scroll handler.
    exports.scrollTop(new_scroll_top);
};

function in_viewport_or_tall(rect, top_of_feed, bottom_of_feed,
                             require_fully_visible) {
    if (require_fully_visible) {
        return ((rect.top > top_of_feed) && // Message top is in view and
                ((rect.bottom < bottom_of_feed) || // message is fully in view or
                 ((rect.height > bottom_of_feed - top_of_feed) &&
                  (rect.top < bottom_of_feed)))); // message is tall.
    } else {
        return (rect.bottom > top_of_feed && rect.top < bottom_of_feed);
    }
}

function add_to_visible(candidates, visible,
                                 top_of_feed, bottom_of_feed,
                                 require_fully_visible,
                                 row_to_id) {
    _.every(candidates, function (row) {
        var row_rect = row.getBoundingClientRect();
        // Mark very tall messages as read once we've gotten past them
        if (in_viewport_or_tall(row_rect, top_of_feed, bottom_of_feed,
                                require_fully_visible)) {
            visible.push(row_to_id(row));
            return true;
        } else {
            return false;
        }
    });
}

var top_of_feed = new util.CachedValue({
    compute_value: function () {
        return $(".floating_recipient").offset().top + $(".floating_recipient").outerHeight();
    }
});

var bottom_of_feed = new util.CachedValue({
    compute_value: function () {
        return $("#compose")[0].getBoundingClientRect().top;
    }
});

function _visible_divs(selected_row, row_min_height, row_to_output, div_class, require_fully_visible) {
    // Note that when using getBoundingClientRect() we are getting offsets
    // relative to the visible window, but when using jQuery's offset() we are
    // getting offsets relative to the full scrollable window. You can't try to
    // compare heights from these two methods.
    var height = bottom_of_feed.get() - top_of_feed.get();
    var num_neighbors = Math.floor(height / row_min_height);

    // We do this explicitly without merges and without recalculating
    // the feed bounds to keep this computation as cheap as possible.
    var visible = [];
    var above_pointer = selected_row.prevAll("div." + div_class + ":lt(" + num_neighbors + ")");
    var below_pointer = selected_row.nextAll("div." + div_class + ":lt(" + num_neighbors + ")");
    add_to_visible(selected_row, visible,
                            top_of_feed.get(), bottom_of_feed.get(), require_fully_visible, row_to_output);
    add_to_visible(above_pointer, visible,
                            top_of_feed.get(), bottom_of_feed.get(), require_fully_visible, row_to_output);
    add_to_visible(below_pointer, visible,
                            top_of_feed.get(), bottom_of_feed.get(), require_fully_visible, row_to_output);

    return visible;
}

exports.visible_groups = function (require_fully_visible) {
    var selected_row = current_msg_list.selected_row();
    if (selected_row === undefined || selected_row.length === 0) {
        return [];
    }

    var selected_group = rows.get_message_recipient_row(selected_row);

    function get_row(row) {
        return row;
    }

    // Being simplistic about this, the smallest group is about 75 px high.
    return _visible_divs(selected_group, 75, get_row, "recipient_row", require_fully_visible);
};

exports.visible_messages = function (require_fully_visible) {
    var selected_row = current_msg_list.selected_row();

    function row_to_id(row) {
        return current_msg_list.get(rows.id($(row)));
    }

    // Being simplistic about this, the smallest message is 25 px high.
    return _visible_divs(selected_row, 25, row_to_id, "message_row", require_fully_visible);
};

exports.scrollTop = function viewport_scrollTop (target_scrollTop) {
    var orig_scrollTop = exports.message_pane.scrollTop();
    if (target_scrollTop === undefined) {
        return orig_scrollTop;
    }
    var ret = exports.message_pane.scrollTop(target_scrollTop);
    var new_scrollTop = exports.message_pane.scrollTop();
    var space_to_scroll = $("#bottom_whitespace").offset().top - viewport.height();

    // Check whether our scrollTop didn't move even though one could have scrolled down
    if (space_to_scroll > 0 && target_scrollTop > 0 &&
        orig_scrollTop === 0 && new_scrollTop === 0) {
        // Chrome has a bug where sometimes calling
        // window.scrollTop(x) has no effect, resulting in the browser
        // staying at 0 -- and afterwards if you call
        // window.scrollTop(x) again, it will still do nothing.  To
        // fix this, we need to first scroll to some other place.
        blueslip.info("ScrollTop did nothing when scrolling to " + target_scrollTop + ", fixing...");
        // First scroll to 1 in order to clear the stuck state
        exports.message_pane.scrollTop(1);
        // And then scroll where we intended to scroll to
        ret = exports.message_pane.scrollTop(target_scrollTop);
        if (exports.message_pane.scrollTop() === 0) {
            blueslip.info("ScrollTop fix did not work when scrolling to " + target_scrollTop +
                          "!  space_to_scroll was " + space_to_scroll);
        }
    }
    return ret;
};

exports.set_message_offset = function viewport_set_message_offset(offset) {
    var msg = current_msg_list.selected_row();
    exports.scrollTop(exports.scrollTop() + msg.offset().top - offset);
};

function make_dimen_wrapper(dimen_name, dimen_func) {
    dimensions[dimen_name] = new util.CachedValue({
        compute_value: function () {
            return dimen_func.call(exports.message_pane);
        }
    });
    return function viewport_dimension_wrapper() {
        if (arguments.length !== 0) {
            dimensions[dimen_name].reset();
            return dimen_func.apply(exports.message_pane, arguments);
        }
        return dimensions[dimen_name].get();
    };
}

exports.height = make_dimen_wrapper('height', $(exports.message_pane).height);
exports.width  = make_dimen_wrapper('width',  $(exports.message_pane).width);

exports.stop_auto_scrolling = function () {
    if (in_stoppable_autoscroll) {
        exports.message_pane.stop();
    }
};

exports.is_narrow = function () {
    // This basically returns true when we hide the right sidebar for
    // skinny mode.  It would be nice to have a less brittle
    // test for this.  See the "@media (max-width: 975px)" section in
    // zulip.css.
    return window.innerWidth <= 975;
};

exports.system_initiated_animate_scroll = function (scroll_amount) {
    suppress_scroll_pointer_update = true; // Gets set to false in the scroll handler.
    var viewport_offset = exports.scrollTop();
    in_stoppable_autoscroll = true;
    exports.message_pane.animate({
        scrollTop: viewport_offset + scroll_amount,
        always: function () {
            in_stoppable_autoscroll = false;
        }
    });
};

exports.user_initiated_animate_scroll = function (scroll_amount) {
    suppress_scroll_pointer_update = true; // Gets set to false in the scroll handler.
    in_stoppable_autoscroll = false; // defensive

    var viewport_offset = exports.scrollTop();

    exports.message_pane.animate({
        scrollTop: viewport_offset + scroll_amount
    });
};

$(function () {
    jwindow = $(window);
    exports.message_pane = $(".app");
    // This handler must be placed before all resize handlers in our application
    jwindow.resize(function () {
        dimensions.height.reset();
        dimensions.width.reset();
        top_of_feed.reset();
        bottom_of_feed.reset();
    });

    $(document).on('compose_started compose_canceled compose_finished', function () {
        bottom_of_feed.reset();
    });
});

return exports;
}());
<commit_msg>Clarify the left_side_userlist comment.<commit_after>var viewport = (function () {
var exports = {};

var jwindow;
var dimensions = {};
var in_stoppable_autoscroll = false;

exports.at_top = function () {
    return (exports.scrollTop() <= 0);
};

exports.message_viewport_info = function () {
    // Return a structure that tells us details of the viewport
    // accounting for fixed elements like the top navbar.
    //
    // The message_header is NOT considered to be part of the visible
    // message pane, which should make sense for callers, who will
    // generally be concerned about whether actual message content is
    // visible.

    var res = {};

    var element_just_above_us = $(".floating_recipient");

    res.visible_top = element_just_above_us.offset().top
        + element_just_above_us.outerHeight();

    var element_just_below_us = $("#compose");

    res.visible_height =
        element_just_below_us.position().top
        - res.visible_top;

    return res;
};

exports.at_bottom = function () {
    var bottom = exports.scrollTop() + exports.height();
    var full_height = exports.message_pane.prop('scrollHeight');

    // We only know within a pixel or two if we're
    // exactly at the bottom, due to browser quirkiness,
    // and we err on the side of saying that we are at
    // the bottom.
    return bottom + 2 >= full_height;
};

// This differs from at_bottom in that it only requires the bottom message to
// be visible, but you may be able to scroll down further.
exports.bottom_message_visible = function () {
    var last_row = rows.last_visible();
    if (last_row.length) {
        var message_bottom = last_row[0].getBoundingClientRect().bottom;
        var bottom_of_feed = $("#compose")[0].getBoundingClientRect().top;
        return bottom_of_feed > message_bottom;
    } else {
        return false;
    }
};

exports.is_below_visible_bottom = function (offset) {
    return offset > exports.scrollTop() + exports.height() - $("#compose").height();
};

exports.set_message_position = function (message_top, message_height, viewport_info, ratio) {
    // message_top = offset of the top of a message that you are positioning
    // message_height = height of the message that you are positioning
    // viewport_info = result of calling viewport.message_viewport_info
    // ratio = fraction indicating how far down the screen the msg should be

    var how_far_down_in_visible_page = viewport_info.visible_height * ratio;

    // special case: keep large messages fully on the screen
    if (how_far_down_in_visible_page + message_height > viewport_info.visible_height) {
        how_far_down_in_visible_page = viewport_info.visible_height - message_height;

        // Next handle truly gigantic messages.  We just say that the top of the
        // message goes to the top of the viewing area.  Realistically, gigantic
        // messages should either be condensed, socially frowned upon, or scrolled
        // with the mouse.
        if (how_far_down_in_visible_page < 0) {
            how_far_down_in_visible_page = 0;
        }
    }

    var hidden_top =
        viewport_info.visible_top
        - exports.scrollTop();

    var message_offset =
        how_far_down_in_visible_page
        + hidden_top;

    var new_scroll_top =
        message_top
        - message_offset;

    suppress_scroll_pointer_update = true; // Gets set to false in the scroll handler.
    exports.scrollTop(new_scroll_top);
};

function in_viewport_or_tall(rect, top_of_feed, bottom_of_feed,
                             require_fully_visible) {
    if (require_fully_visible) {
        return ((rect.top > top_of_feed) && // Message top is in view and
                ((rect.bottom < bottom_of_feed) || // message is fully in view or
                 ((rect.height > bottom_of_feed - top_of_feed) &&
                  (rect.top < bottom_of_feed)))); // message is tall.
    } else {
        return (rect.bottom > top_of_feed && rect.top < bottom_of_feed);
    }
}

function add_to_visible(candidates, visible,
                                 top_of_feed, bottom_of_feed,
                                 require_fully_visible,
                                 row_to_id) {
    _.every(candidates, function (row) {
        var row_rect = row.getBoundingClientRect();
        // Mark very tall messages as read once we've gotten past them
        if (in_viewport_or_tall(row_rect, top_of_feed, bottom_of_feed,
                                require_fully_visible)) {
            visible.push(row_to_id(row));
            return true;
        } else {
            return false;
        }
    });
}

var top_of_feed = new util.CachedValue({
    compute_value: function () {
        return $(".floating_recipient").offset().top + $(".floating_recipient").outerHeight();
    }
});

var bottom_of_feed = new util.CachedValue({
    compute_value: function () {
        return $("#compose")[0].getBoundingClientRect().top;
    }
});

function _visible_divs(selected_row, row_min_height, row_to_output, div_class, require_fully_visible) {
    // Note that when using getBoundingClientRect() we are getting offsets
    // relative to the visible window, but when using jQuery's offset() we are
    // getting offsets relative to the full scrollable window. You can't try to
    // compare heights from these two methods.
    var height = bottom_of_feed.get() - top_of_feed.get();
    var num_neighbors = Math.floor(height / row_min_height);

    // We do this explicitly without merges and without recalculating
    // the feed bounds to keep this computation as cheap as possible.
    var visible = [];
    var above_pointer = selected_row.prevAll("div." + div_class + ":lt(" + num_neighbors + ")");
    var below_pointer = selected_row.nextAll("div." + div_class + ":lt(" + num_neighbors + ")");
    add_to_visible(selected_row, visible,
                            top_of_feed.get(), bottom_of_feed.get(), require_fully_visible, row_to_output);
    add_to_visible(above_pointer, visible,
                            top_of_feed.get(), bottom_of_feed.get(), require_fully_visible, row_to_output);
    add_to_visible(below_pointer, visible,
                            top_of_feed.get(), bottom_of_feed.get(), require_fully_visible, row_to_output);

    return visible;
}

exports.visible_groups = function (require_fully_visible) {
    var selected_row = current_msg_list.selected_row();
    if (selected_row === undefined || selected_row.length === 0) {
        return [];
    }

    var selected_group = rows.get_message_recipient_row(selected_row);

    function get_row(row) {
        return row;
    }

    // Being simplistic about this, the smallest group is about 75 px high.
    return _visible_divs(selected_group, 75, get_row, "recipient_row", require_fully_visible);
};

exports.visible_messages = function (require_fully_visible) {
    var selected_row = current_msg_list.selected_row();

    function row_to_id(row) {
        return current_msg_list.get(rows.id($(row)));
    }

    // Being simplistic about this, the smallest message is 25 px high.
    return _visible_divs(selected_row, 25, row_to_id, "message_row", require_fully_visible);
};

exports.scrollTop = function viewport_scrollTop (target_scrollTop) {
    var orig_scrollTop = exports.message_pane.scrollTop();
    if (target_scrollTop === undefined) {
        return orig_scrollTop;
    }
    var ret = exports.message_pane.scrollTop(target_scrollTop);
    var new_scrollTop = exports.message_pane.scrollTop();
    var space_to_scroll = $("#bottom_whitespace").offset().top - viewport.height();

    // Check whether our scrollTop didn't move even though one could have scrolled down
    if (space_to_scroll > 0 && target_scrollTop > 0 &&
        orig_scrollTop === 0 && new_scrollTop === 0) {
        // Chrome has a bug where sometimes calling
        // window.scrollTop(x) has no effect, resulting in the browser
        // staying at 0 -- and afterwards if you call
        // window.scrollTop(x) again, it will still do nothing.  To
        // fix this, we need to first scroll to some other place.
        blueslip.info("ScrollTop did nothing when scrolling to " + target_scrollTop + ", fixing...");
        // First scroll to 1 in order to clear the stuck state
        exports.message_pane.scrollTop(1);
        // And then scroll where we intended to scroll to
        ret = exports.message_pane.scrollTop(target_scrollTop);
        if (exports.message_pane.scrollTop() === 0) {
            blueslip.info("ScrollTop fix did not work when scrolling to " + target_scrollTop +
                          "!  space_to_scroll was " + space_to_scroll);
        }
    }
    return ret;
};

exports.set_message_offset = function viewport_set_message_offset(offset) {
    var msg = current_msg_list.selected_row();
    exports.scrollTop(exports.scrollTop() + msg.offset().top - offset);
};

function make_dimen_wrapper(dimen_name, dimen_func) {
    dimensions[dimen_name] = new util.CachedValue({
        compute_value: function () {
            return dimen_func.call(exports.message_pane);
        }
    });
    return function viewport_dimension_wrapper() {
        if (arguments.length !== 0) {
            dimensions[dimen_name].reset();
            return dimen_func.apply(exports.message_pane, arguments);
        }
        return dimensions[dimen_name].get();
    };
}

exports.height = make_dimen_wrapper('height', $(exports.message_pane).height);
exports.width  = make_dimen_wrapper('width',  $(exports.message_pane).width);

exports.stop_auto_scrolling = function () {
    if (in_stoppable_autoscroll) {
        exports.message_pane.stop();
    }
};

exports.is_narrow = function () {
    // This basically returns true when we hide the right sidebar for
    // the left_side_userlist skinny mode.  It would be nice to have a less brittle
    // test for this.  See the "@media (max-width: 975px)" section in
    // zulip.css.
    return window.innerWidth <= 975;
};

exports.system_initiated_animate_scroll = function (scroll_amount) {
    suppress_scroll_pointer_update = true; // Gets set to false in the scroll handler.
    var viewport_offset = exports.scrollTop();
    in_stoppable_autoscroll = true;
    exports.message_pane.animate({
        scrollTop: viewport_offset + scroll_amount,
        always: function () {
            in_stoppable_autoscroll = false;
        }
    });
};

exports.user_initiated_animate_scroll = function (scroll_amount) {
    suppress_scroll_pointer_update = true; // Gets set to false in the scroll handler.
    in_stoppable_autoscroll = false; // defensive

    var viewport_offset = exports.scrollTop();

    exports.message_pane.animate({
        scrollTop: viewport_offset + scroll_amount
    });
};

$(function () {
    jwindow = $(window);
    exports.message_pane = $(".app");
    // This handler must be placed before all resize handlers in our application
    jwindow.resize(function () {
        dimensions.height.reset();
        dimensions.width.reset();
        top_of_feed.reset();
        bottom_of_feed.reset();
    });

    $(document).on('compose_started compose_canceled compose_finished', function () {
        bottom_of_feed.reset();
    });
});

return exports;
}());
<|endoftext|>


<commit_before>var s;
var CANVAS_WIDTH = 900,
    CANVAS_HEIGHT = 600,
    MAX_NODES = 2000;

var filterKeywords = {
    substance: {
        'Protein': ['actin', 'myosin', 'myoglobin', 'hemoglobin', 'melanopsin', 'photoisomerase', 'opsin', 'collagen', 'adiponectin', 'tropoelastin', 'elastin'],
        'Hormone:': {
            'Amine': ['insulin', 'oxytocin', 'cortisol', 'glucagon', 'neuroestrogen'],
            'Peptide': ['progesterone', 'estrogen', 'testosterone', 'growth hormone', 'growth factor']
        },
        'Neurotransmitter': ['dopamine', 'serotonin', 'norepinephrine', 'epinephrine', 'acetylcholine', 'GABA', 'catechcholamine']
    },
    structure: {
        'Biological System': ['immune system', 'limbic system', 'circulatory system', 'digestive system', 'integumentary system', 'lymphatic system'],
        'Tissue | Organ': ['amygdala', 'hypothalamus', 'thalamus', 'hippocampus', 'pituitary gland', 'adrenal gland', 'pancreas', 'acruate nucleus', 'kidney', 'blood vessels', 'esophagus', 'tumor'],
        'Cell Type': ['lymphocyte', 'native T cell', 'helper T cell', 'cytotoxic T cell', 'activated T cell', 'rested T cells', 'spermatocyte', 'leukocyte', 'adipocyte', 'neuron', 'astrocyte', 'stem cell'],
        'Receptor': ['G-protein coupled receptor', 'calcium ion channel', 'transport channel', 'serotonin receptor']
    },
    process: {
        'Event Type:': {
            'Molecular': ['DNA synthesis', 'transcription', 'translation', 'DNA damage', 'DNA repair'],
            'Pathway': ['[X]-reuptake', 'MAPKKK cascade', 'signal transduction', '[X]-binding', '[X]-transport'],
            'Environmental': ['drug administration', 'X injection', 'drug use', 'food intake', 'sun exposure', 'smoke inhalation'],
            'Biological Phenomena': ['necrosis', 'apoptosis', 'glycolysis', 'lipolysis', 'embryogenesis', 'spermatogenesis']
        },
        'Physiological': ['digestion', 'metabolism', 'heart rate', 'vasodilation', 'blood pressure'],
        'Cognitive': ['memory', 'memory loss', 'attention', 'intelligence', 'executive function', 'perception'],
        'Disease/Disorder': ['ebola', 'cancer', 'diabetes', 'ADHD', 'Parkinson\'s Disease', 'Huntington\'s Disease', 'obesity', 'anorexia', 'drug addiction', 'alcoholism'],
        'Other': ['light', 'stress', 'sleepiness', 'arousal', 'hunger', 'bone-formation', 'death', 'aging', 'skin degeneration']
    },
}

var Node = function(id, name) {
    this.id = id;
    this.name = name;
};

var Edge = function(id, source, target, name) {
    this.id = id;
    this.source = source;
    this.target = target;
    this.name = name;
    this.links = [];
};

_.extend(Edge.prototype, {
    addLink: function(link) {
        if (!_.find(this.links, function(existingLink) {
            return existingLink === link;
        })) {
            this.links.push(link);
        }
    }
});

var nodes = [];
var edges = [];
var g = {
    nodes: [],
    edges: []
};

window.addEventListener('load', function() {
    populateNodeFilterer();

    s = new sigma({
        graph: g,
        renderer: {
            container: document.getElementById('graph-container'),
            type: 'canvas'
        },
        settings: {
            minEdgeSize: 0.5,
            maxEdgeSize: 4,
            enableEdgeHovering: true,
            edgeHoverSizeRatio: 1,
            edgeHoverExtremities: true,
            batchEdgesDrawing: true
        }
    });

    // s.graph.addNode({
    //     id: 'newnode1',
    //     label: 'newnode1',
    //     x: 1,
    //     y: 1,
    //     size: 1
    // }).addNode({
    //     id: 'newnode2',
    //     label: 'newnode2',
    //     x: 500,
    //     y: 500,
    //     size: 1
    // }).addEdge({
    //     id: 'newedge1',
    //     source: 'newnode1',
    //     target: 'newnode2',
    //     label: 'newedge1',
    //     hover_color: '#333333',
    //     size: 13
    //     // type: 't'
    // });

    // s.refresh();

    s.bind('overEdge outEdge clickEdge doubleClickEdge rightClickEdge', function(e) {
        console.log(e.type, e.data.edge, e.data.captor);
    });

    s.bind('clickNode', function(e) {
        handleClickNode(e.data.node);
    });

    // s.bind('overEdge outEdge clickEdge', function(e) {
    //     console.log('event', e);
    //     handleClickEdge(e.data.edge);
    // });

    s.bind('clickStage', function(e) {
        handleClickStage();
    });

    fetchRelations();
});

function populateNodeFilterer() {
    var html = '';

    var generateCheckbox = function(label) {
        var checkbox = '';
        checkbox += '
';
        checkbox += '<input type="checkbox"> ' + label;
        checkbox += '
';
        return checkbox;
    };

    _.each(filterKeywords, function(categoryItems, categoryKey) {
        html += '

';
        html += '
' + categoryKey + '
';
        if (_.isArray(categoryItems)) {
            _.each(categoryItems, function(item) {
                html += generateCheckbox(item);
            });
        } else {
            html += '
';
            _.each(categoryItems, function(subcategoryItems, subcategoryKey) {
                html += '
';
                html += '
' + subcategoryKey + '
';
                if (_.isArray(subcategoryItems)) {
                    _.each(subcategoryItems, function(item) {
                        html += generateCheckbox(item);
                    });
                } else {
                    html += '
';
                    _.each(subcategoryItems, function(items, key) {
                        html += '
';
                        html += '<h7>' + key + '</h7>';
                        _.each(items, function(item) {
                            html += generateCheckbox(item);
                        });
                        html += '
'
                    });
                    html += '
'
                }
                html += '
';
            });
            html += '
';
        }
        html += '

';
    });

    var newHtml = $('#filter-terms-anchor').html() + html;
    $('#filter-terms-anchor').html(newHtml);

}

function fetchRelations() {
    websocketClient();
}

function getRandomInt(min, max) {
    return Math.floor(Math.random() * (max - min) + min);
}

function handleOnMessage(relation) {
    if (nodes.length > MAX_NODES) return;
    var subjectNode = _.find(nodes, function(node) {
        return node.name === relation.subject;
    });

    if (!subjectNode) {
        subjectNode = new Node(nodes.length, relation.subject);
        nodes.push(subjectNode);
        s.graph.addNode({
          id: subjectNode.id.toString(),
          label: subjectNode.name,
          x: getRandomInt(0, CANVAS_WIDTH),
          y: getRandomInt(0, CANVAS_HEIGHT),
          size: 1,
        });
    }

    var objectNode = _.find(nodes, function(node) {
        return node.name === relation.object;
    });

    if (!objectNode) {
        objectNode = new Node(nodes.length, relation.object);
        nodes.push(objectNode);
        s.graph.addNode({
          id: objectNode.id.toString(),
          label: objectNode.name,
          x: getRandomInt(0, CANVAS_WIDTH),
          y: getRandomInt(0, CANVAS_HEIGHT),
          size: 1,
        });
    }

    var edge = _.find(edges, function(edge) {
        return edge.source === subjectNode.id
                && edge.target === objectNode.id
                && edge.name === relation.predicate;
    });

    if (!edge) {
        edge = new Edge(edges.length, subjectNode.id, objectNode.id, relation.predicate);
        edges.push(edge);
        s.graph.addEdge({
          id: edge.id.toString(),
          source: edge.source.toString(),
          target: edge.target.toString(),
          label: edge.name,
          type: "arrow"
        });
    }

    edge.addLink(relation.link);

    // s.refresh();
}

function handleConnectionClose() {
    console.log("Connection closed");
}

function handleClickEdge(edge) {
    console.log(edge);
}

function handleClickStage() {

}

function applyLayout() {
    s.startForceAtlas2({
        linLogMode: false,
        scalingRatio: 100,
        iterationsPerRender: 10000,
        outboundAttractionDistribution: true,
        // adjustSizes: true,
        edgeWeightInfluence: 0.2
    });
}

function stopLayout() {
    s.stopForceAtlas2();
}
<commit_msg>Update filterKeywords in app.js<commit_after>var s;
var CANVAS_WIDTH = 900,
    CANVAS_HEIGHT = 600,
    MAX_NODES = 2000;

var filterKeywords = {
    substance: {
        'Protein': ['AANAT', 'melatonin', 'elastin', 'collagen', 'cytochrome c', 'adiponectin', 'opsin', 'collagen', 'adiponectin'],
        'Hormone:': ['melatonin', 'cortisol', 'insulin', 'glucagon', 'adrenocorticotropic hormone', 'ACTH', 'oxytocin', 'prolactin', 'leptin', 'vasopressin', 'grelin', 'food intake leptin', 'somatostatin', 'globulin'],
        'Neurotransmitter': ['dopamine', 'serotonin', 'norepinephrine', 'epinephrine', 'adrenaline', 'vasopressin', 'GABA', 'catechcholamine', 'retina dopamine'],
        'Other': ['glucose', 'gastric acid', 'receptor', 'elastase', 'calpain', 'alpha-1-antitrypsin', 'alapha-2-macroglobulin', 'adrenal cortex cortisol']
    },
    structure: ['collagen structure', 'collagen structure', 'hypothalamus', 'pituitary gland', 'adrenal cortex']
    },
    process: ['leptin hunger response', 'aging', 'reuptake', 'uptake']
}

// var filterKeywords = {
//     substance: {
//         'Protein': ['actin', 'myosin', 'myoglobin', 'hemoglobin', 'melanopsin', 'photoisomerase', 'opsin', 'collagen', 'adiponectin', 'tropoelastin', 'elastin'],
//         'Hormone:': {
//             'Amine': ['insulin', 'oxytocin', 'cortisol', 'glucagon', 'neuroestrogen'],
//             'Peptide': ['progesterone', 'estrogen', 'testosterone', 'growth hormone', 'growth factor']
//         },
//         'Neurotransmitter': ['dopamine', 'serotonin', 'norepinephrine', 'epinephrine', 'acetylcholine', 'GABA', 'catechcholamine']
//     },
//     structure: {
//         'Biological System': ['immune system', 'limbic system', 'circulatory system', 'digestive system', 'integumentary system', 'lymphatic system'],
//         'Tissue | Organ': ['amygdala', 'hypothalamus', 'thalamus', 'hippocampus', 'pituitary gland', 'adrenal gland', 'pancreas', 'acruate nucleus', 'kidney', 'blood vessels', 'esophagus', 'tumor'],
//         'Cell Type': ['lymphocyte', 'native T cell', 'helper T cell', 'cytotoxic T cell', 'activated T cell', 'rested T cells', 'spermatocyte', 'leukocyte', 'adipocyte', 'neuron', 'astrocyte', 'stem cell'],
//         'Receptor': ['G-protein coupled receptor', 'calcium ion channel', 'transport channel', 'serotonin receptor']
//     },
//     process: {
//         'Event Type:': {
//             'Molecular': ['DNA synthesis', 'transcription', 'translation', 'DNA damage', 'DNA repair'],
//             'Pathway': ['[X]-reuptake', 'MAPKKK cascade', 'signal transduction', '[X]-binding', '[X]-transport'],
//             'Environmental': ['drug administration', 'X injection', 'drug use', 'food intake', 'sun exposure', 'smoke inhalation'],
//             'Biological Phenomena': ['necrosis', 'apoptosis', 'glycolysis', 'lipolysis', 'embryogenesis', 'spermatogenesis']
//         },
//         'Physiological': ['digestion', 'metabolism', 'heart rate', 'vasodilation', 'blood pressure'],
//         'Cognitive': ['memory', 'memory loss', 'attention', 'intelligence', 'executive function', 'perception'],
//         'Disease/Disorder': ['ebola', 'cancer', 'diabetes', 'ADHD', 'Parkinson\'s Disease', 'Huntington\'s Disease', 'obesity', 'anorexia', 'drug addiction', 'alcoholism'],
//         'Other': ['light', 'stress', 'sleepiness', 'arousal', 'hunger', 'bone-formation', 'death', 'aging', 'skin degeneration']
//     },
// }

var Node = function(id, name) {
    this.id = id;
    this.name = name;
};

var Edge = function(id, source, target, name) {
    this.id = id;
    this.source = source;
    this.target = target;
    this.name = name;
    this.links = [];
};

_.extend(Edge.prototype, {
    addLink: function(link) {
        if (!_.find(this.links, function(existingLink) {
            return existingLink === link;
        })) {
            this.links.push(link);
        }
    }
});

var nodes = [];
var edges = [];
var g = {
    nodes: [],
    edges: []
};

window.addEventListener('load', function() {
    populateNodeFilterer();

    s = new sigma({
        graph: g,
        renderer: {
            container: document.getElementById('graph-container'),
            type: 'canvas'
        },
        settings: {
            minEdgeSize: 0.5,
            maxEdgeSize: 4,
            enableEdgeHovering: true,
            edgeHoverSizeRatio: 1,
            edgeHoverExtremities: true,
            batchEdgesDrawing: true
        }
    });

    // s.graph.addNode({
    //     id: 'newnode1',
    //     label: 'newnode1',
    //     x: 1,
    //     y: 1,
    //     size: 1
    // }).addNode({
    //     id: 'newnode2',
    //     label: 'newnode2',
    //     x: 500,
    //     y: 500,
    //     size: 1
    // }).addEdge({
    //     id: 'newedge1',
    //     source: 'newnode1',
    //     target: 'newnode2',
    //     label: 'newedge1',
    //     hover_color: '#333333',
    //     size: 13
    //     // type: 't'
    // });

    // s.refresh();

    s.bind('overEdge outEdge clickEdge doubleClickEdge rightClickEdge', function(e) {
        console.log(e.type, e.data.edge, e.data.captor);
    });

    s.bind('clickNode', function(e) {
        handleClickNode(e.data.node);
    });

    // s.bind('overEdge outEdge clickEdge', function(e) {
    //     console.log('event', e);
    //     handleClickEdge(e.data.edge);
    // });

    s.bind('clickStage', function(e) {
        handleClickStage();
    });

    fetchRelations();
});

function populateNodeFilterer() {
    var html = '';

    var generateCheckbox = function(label) {
        var checkbox = '';
        checkbox += '
';
        checkbox += '<input type="checkbox"> ' + label;
        checkbox += '
';
        return checkbox;
    };

    _.each(filterKeywords, function(categoryItems, categoryKey) {
        html += '

';
        html += '
' + categoryKey + '
';
        if (_.isArray(categoryItems)) {
            _.each(categoryItems, function(item) {
                html += generateCheckbox(item);
            });
        } else {
            html += '
';
            _.each(categoryItems, function(subcategoryItems, subcategoryKey) {
                html += '
';
                html += '
' + subcategoryKey + '
';
                if (_.isArray(subcategoryItems)) {
                    _.each(subcategoryItems, function(item) {
                        html += generateCheckbox(item);
                    });
                } else {
                    html += '
';
                    _.each(subcategoryItems, function(items, key) {
                        html += '
';
                        html += '<h7>' + key + '</h7>';
                        _.each(items, function(item) {
                            html += generateCheckbox(item);
                        });
                        html += '
'
                    });
                    html += '
'
                }
                html += '
';
            });
            html += '
';
        }
        html += '

';
    });

    var newHtml = $('#filter-terms-anchor').html() + html;
    $('#filter-terms-anchor').html(newHtml);

}

function fetchRelations() {
    websocketClient();
}

function getRandomInt(min, max) {
    return Math.floor(Math.random() * (max - min) + min);
}

function handleOnMessage(relation) {
    if (nodes.length > MAX_NODES) return;
    var subjectNode = _.find(nodes, function(node) {
        return node.name === relation.subject;
    });

    if (!subjectNode) {
        subjectNode = new Node(nodes.length, relation.subject);
        nodes.push(subjectNode);
        s.graph.addNode({
          id: subjectNode.id.toString(),
          label: subjectNode.name,
          x: getRandomInt(0, CANVAS_WIDTH),
          y: getRandomInt(0, CANVAS_HEIGHT),
          size: 1,
        });
    }

    var objectNode = _.find(nodes, function(node) {
        return node.name === relation.object;
    });

    if (!objectNode) {
        objectNode = new Node(nodes.length, relation.object);
        nodes.push(objectNode);
        s.graph.addNode({
          id: objectNode.id.toString(),
          label: objectNode.name,
          x: getRandomInt(0, CANVAS_WIDTH),
          y: getRandomInt(0, CANVAS_HEIGHT),
          size: 1,
        });
    }

    var edge = _.find(edges, function(edge) {
        return edge.source === subjectNode.id
                && edge.target === objectNode.id
                && edge.name === relation.predicate;
    });

    if (!edge) {
        edge = new Edge(edges.length, subjectNode.id, objectNode.id, relation.predicate);
        edges.push(edge);
        s.graph.addEdge({
          id: edge.id.toString(),
          source: edge.source.toString(),
          target: edge.target.toString(),
          label: edge.name,
          type: "arrow"
        });
    }

    edge.addLink(relation.link);

    // s.refresh();
}

function handleConnectionClose() {
    console.log("Connection closed");
}

function handleClickEdge(edge) {
    console.log(edge);
}

function handleClickStage() {

}

function applyLayout() {
    s.startForceAtlas2({
        linLogMode: false,
        scalingRatio: 100,
        iterationsPerRender: 10000,
        outboundAttractionDistribution: true,
        // adjustSizes: true,
        edgeWeightInfluence: 0.2
    });
}

function stopLayout() {
    s.stopForceAtlas2();
}
<|endoftext|>


<commit_before>"use strict";

function mockMessageId(idx) {
  return btoa(String(idx));
}
function resolveMockId(ref, offset) {
  return Number(atob(ref));
}

function MockMessageModel(count) {
  this.count_ = count;
}
MockMessageModel.prototype.getMessage_ = function(number) {
  if (number < 0 || number >= this.count_)
    throw "Bad message number";

  var idx = number % SAMPLE_MSGS.length;
  if (idx < 0)
    idx += SAMPLE_MSGS.length;

  var msg = { };
  for (var key in SAMPLE_MSGS[idx]) {
    msg[key] = SAMPLE_MSGS[idx][key];
  }
  msg.number = number;
  msg.id = mockMessageId(number);
  return msg;
};
// TODO(davidben): Just make the API take the options dict along with
// an initial count parameter?
MockMessageModel.prototype.newTailInclusive = function(start, cb) {
  start = resolveMockId(start);
  if (start < 0 || start >= this.count_)
    throw "Bad message id";
  return new MockMessageTail(this, start, cb, {
    inclusive: true
  });
};
MockMessageModel.prototype.newTail = function(start, cb) {
  start = resolveMockId(start);
  if (start < 0 || start >= this.count_)
    throw "Bad message id";
  return new MockMessageTail(this, start, cb, {});
};
MockMessageModel.prototype.newReverseTail = function(start, cb) {
  start = resolveMockId(start);
  if (start < 0 || start >= this.count_)
    throw "Bad message id";
  return new MockMessageTail(this, start, cb, {
    reverse: true
  });
};

function MockMessageTail(model, start, cb, opts) {
  this.model_ = model;
  this.start_ = start;
  this.reverse_ = opts.reverse;
  this.cb_ = cb;

  // Simulate doing only one XHR at a time.
  this.requestPending_ = false;
  this.lastRequested_ = opts.inclusive ? -1 : 0;
  this.count_ = this.lastRequested_;
}
MockMessageTail.prototype.expandTo = function(count) {
  this.count_ = Math.max(this.count_, count);
  this.fireRequest_();
};
MockMessageTail.prototype.close = function() {
  this.cb_ = null;
};
MockMessageTail.prototype.fireRequest_ = function() {
  if (this.requestPending_)
    return;
  if (this.lastRequested_ >= this.count_)
    return;

  this.requestPending_ = true;

  window.setTimeout(function() {
    var requestStart, requestEnd;
    if (this.reverse_) {
      requestStart = Math.max(0, this.start_ - this.count_);
      requestEnd = this.start_ - this.lastRequested_;
    } else {
      requestStart = this.start_ + this.lastRequested_ + 1;
      requestEnd = Math.min(this.start_ + this.count_ + 1, this.model_.count_);
    }
    // TODO(davidben): Figure out the right way to mock this stuff
    // when the size of the model changes.
    this.lastRequested_ = this.count_;

    // Fake some messages.
    var msgs = [];
    for (var i = requestStart; i < requestEnd; i++) {
      msgs.push(this.model_.getMessage_(i));
    }
    var atEnd = this.reverse_ ?
      (requestStart == 0) :
      (requestEnd == this.model_.count_);
    console.log(this.reverse_, atEnd);
    if (this.cb_)
      this.cb_(msgs, atEnd);

    this.requestPending_ = false;
    this.fireRequest_();
  }.bind(this), 500);
};

// If the number of messages outside the scroll goes outside
// [MIN_BUFFER, MAX_BUFFER], request/retire messages to reach
// TARGET_BUFFER. That there is a buffer between TARGET_BUFFER and our
// endpoints ensures that spurious scrolling will not repeatedly
// request and retire the same message. Also that we do our requests
// in large-ish batches.
var MIN_BUFFER = 50;
var TARGET_BUFFER = MIN_BUFFER * 2;
var MAX_BUFFER = MIN_BUFFER * 3;

function MessageView(model, container) {
  this.model_ = model;
  this.container_ = container;
  // Make this element focusable. This is needed so that you can
  // direct pageup/down and friends to it. Clicking in it mostly works
  // but we cannot control that programmatically. Moreover, it exposes
  // a quirk in WebKit and Blink; they track the most recently clicked
  // DOM node and use it to determine scroll when there is no focus
  // node. This breaks when we delete that node.
  this.container_.tabIndex = 0;

  this.loadingAbove_ = document.createElement("div");
  this.loadingAbove_.className = "loading";
  this.loadingAbove_.textContent = "Loading...";
  this.atTop_ = true;

  this.loadingBelow_ = document.createElement("div");
  this.loadingBelow_.className = "loading";
  this.loadingBelow_.textContent = "Loading...";
  this.atBottom_ = true;

  this.messagesDiv_ = document.createElement("div");

  this.container_.appendChild(this.loadingAbove_);
  this.container_.appendChild(this.messagesDiv_);
  this.container_.appendChild(this.loadingBelow_);

  this.active_ = false;

  this.tailBelow_ = null;
  this.tailBelowOffset_ = 0;  // The global index of the tail reference.

  this.tailAbove_ = null;
  this.tailAboveOffset_ = 0;  // The global index of the tail reference.

  this.listOffset_ = 0;  // The global index of the top of the list.
  this.messages_ = [];
  this.nodes_ = [];

  this.messageToIndex_ = { };  // Map id to global index.

  this.container_.addEventListener("scroll", this.checkBuffers_.bind(this));
}

MessageView.prototype.reset_ = function() {
  // It's not visible. Blow everything away and start from
  // there. (This is mildly annoying. Can we refactor some of this
  // code to not need this?)
  if (this.tailAbove_) {
    this.tailAbove_.close();
    this.tailAbove_ = null;
  }
  if (this.tailBelow_) {
    this.tailBelow_.close();
    this.tailBelow_ = null;
  }
  this.active_ = false;
  this.listOffset_ = 0;
  this.messages_ = [];
  this.nodes_ = [];
  this.messagesDiv_.textContent = "";
  this.messageToIndex_ = {};
};

MessageView.prototype.scrollToMessage = function(id) {
  if (id in this.messageToIndex_) {
    // Easy case: if the message is in our current view, we just jump
    // to it.
    this.nodes_[this.messageToIndex_[id] - this.listOffset_].scrollIntoView();
    return;
  }

  // Otherwise, we reset the universe and use |id| as our new point of
  // reference.
  this.reset_();
  this.active_ = true;

  this.tailBelow_ = this.model_.newTailInclusive(
    id, this.appendMessages_.bind(this));
  this.tailBelowOffset_ = 0;

  this.tailAbove_ = this.model_.newReverseTail(
    id, this.prependMessages_.bind(this));
  this.tailAboveOffset_ = 0;  // The global index of the tail reference.

  this.checkBuffers_();
};

MessageView.prototype.setAtTop = function(atTop) {
  this.atTop_ = atTop;
  this.loadingAbove_.style.display = atTop ? "none" : "block";
};

MessageView.prototype.setAtBottom = function(atBottom) {
  this.atBottom_ = atBottom;
  this.loadingBelow_.style.display = atBottom ? "none" : "block";
};

MessageView.prototype.appendMessages_ = function(msgs, isDone) {
  for (var i = 0; i < msgs.length; i++) {
    this.messageToIndex_[msgs[i].id] =
      this.messages_.length + this.listOffset_;

    var node = this.formatMessage_(msgs[i]);
    this.nodes_.push(node);
    this.messages_.push(msgs[i]);

    this.messagesDiv_.appendChild(node);
  }

  this.setAtBottom(isDone);
};

MessageView.prototype.prependMessages_ = function(msgs, isDone) {
  // TODO(davidben): This triggers layout a bunch. Optimize this if needbe.
  var nodes = [];
  var insertReference = this.messagesDiv_.firstChild;
  var oldHeight = this.container_.scrollHeight;
  for (var i = 0; i < msgs.length; i++) {
    this.messageToIndex_[msgs[i].id] =
      this.listOffset_ - msgs.length + i;

    var node = this.formatMessage_(msgs[i]);
    nodes.push(node);

    this.messagesDiv_.insertBefore(node, insertReference);
  }
  this.container_.scrollTop += (this.container_.scrollHeight - oldHeight);

  this.messages_.unshift.apply(this.messages_, msgs);
  this.nodes_.unshift.apply(this.nodes_, nodes);
  this.listOffset_ -= msgs.length;

  this.setAtTop(isDone);
};

var COLORS = ["black", "silver", "gray", "white", "maroon", "red",
              "purple", "fuchsia", "green", "lime"];
MessageView.prototype.formatMessage_ = function(msg) {
  var pre = document.createElement("pre");
  var indented = "   " +
    msg.message.replace(/\s+$/, '').split("\n").join("\n   ");
  pre.textContent =
    msg.number + ": " +
    msg.class + " / " + msg.instance + " / " + msg.sender + "  " +
      new Date(msg.time).toString() + "\n" +
    indented;
  pre.className = "message";
  pre.style.color = COLORS[((msg.number % COLORS.length) + COLORS.length) % COLORS.length];
  return pre;
};

// Return 1 if we need to expand below, -1 if we need to contract, and
// 0 if neither.
MessageView.prototype.checkBelow_ = function(bounds) {
  // Do we need to expand?
  if (this.nodes_.length < MIN_BUFFER)
    return 1;
  var b = this.nodes_[this.nodes_.length - MIN_BUFFER].getBoundingClientRect();
  if (bounds.bottom > b.top)
    return 1;

  // Do we need to contract?
  if (this.nodes_.length < MAX_BUFFER)
    return 0;
  b = this.nodes_[this.nodes_.length - MAX_BUFFER].getBoundingClientRect();
  if (bounds.bottom < b.top)
    return -1;

  return 0;
};

MessageView.prototype.checkAbove_ = function(bounds) {
  // Do we need to expand?
  if (this.nodes_.length < MIN_BUFFER)
    return 1;
  var b = this.nodes_[MIN_BUFFER - 1].getBoundingClientRect();
  if (bounds.top < b.bottom)
    return 1;

  // Do we need to contract?
  if (this.nodes_.length < MAX_BUFFER)
    return 0;
  b = this.nodes_[MAX_BUFFER - 1].getBoundingClientRect();
  if (bounds.top > b.bottom)
    return -1;

  return 0;
};

MessageView.prototype.checkBuffers_ = function() {
  if (!this.active_)
    return;

  var bounds = this.container_.getBoundingClientRect();

  // TODO(davidben): Instead of only ever working 50 messages at a
  // time, it's possibly better to just pay a binary search and figure
  // out exactly how many we need to reach TARGET_BUFFER?
  //
  // TODO(davidben): Trigger removal by receiving messages?
  var below = this.checkBelow_(bounds);
  if (below > 0) {
    if (!this.tailBelow_) {
      this.tailBelow_ = this.model_.newTail(
        this.messages_[this.messages_.length - 1].id,
        this.appendMessages_.bind(this));
      this.tailBelowOffset_ = this.listOffset_ + this.messages_.length - 1;
    }
    this.tailBelow_.expandTo(
      this.listOffset_ + this.nodes_.length - 1 + (TARGET_BUFFER - MIN_BUFFER)
        - this.tailBelowOffset_);
  } else if (below < 0) {
    // Close the current tail.
    if (this.tailBelow_) {
      this.tailBelow_.close();
      this.tailBelow_ = null;
    }

    var num = MAX_BUFFER - TARGET_BUFFER;
    for (var i = 0; i < num; i++) {
      var idx = this.nodes_.length - i - 1;
      this.messagesDiv_.removeChild(this.nodes_[idx]);
      delete this.messageToIndex_[this.messages_[idx].id];
    }
    this.nodes_.splice(this.nodes_.length - num, num);
    this.messages_.splice(this.messages_.length - num, num);

    this.setAtBottom(false);
  }

  var above = this.checkAbove_(bounds);
  if (above > 0) {
    if (!this.tailAbove_) {
      this.tailAbove_ = this.model_.newReverseTail(
        this.messages_[0].id,
        this.prependMessages_.bind(this));
      this.tailAboveOffset_ = this.listOffset_;
    }
    this.tailAbove_.expandTo(
      this.tailAboveOffset_ -
        (this.listOffset_ - (TARGET_BUFFER - MIN_BUFFER)));
  } else if (above < 0) {
    // Close the current tail.
    if (this.tailAbove_) {
      this.tailAbove_.close();
      this.tailAbove_ = null;
    }

    var maxRemoved = MAX_BUFFER - TARGET_BUFFER;
    var oldHeight = this.container_.scrollHeight;

    // Limit the nodes removed; if we remove enough that scrollTop has
    // to change, the scroll gets off. Do this in two passes so as not
    // to keep re-triggering layout with
    // getBoundingClientRect. Unfortunately, this isn't the cause of
    // us getting stuck.
    var heightLost = 0;
    var maxHeight = oldHeight - this.container_.scrollTop - bounds.height;
    for (var num = 0; num < maxRemoved; num++) {
      // FIXME: What if there are margins?
      var b = this.nodes_[num].getBoundingClientRect();
      if (heightLost + b.height >= maxHeight)
        break;
      heightLost += b.height;
    }

    for (var i = 0; i < num; i++) {
      this.messagesDiv_.removeChild(this.nodes_[i]);
      delete this.messageToIndex_[this.messages_[i].id];
    }
    this.container_.scrollTop += (this.container_.scrollHeight - oldHeight);
    this.nodes_.splice(0, num);
    this.messages_.splice(0, num);
    this.listOffset_ += num;

    this.setAtTop(false);
  }
};

var messageView;  // For debugging.
$(function() {
  messageView = new MessageView(new MockMessageModel(1000),
                                document.getElementById("messagelist"));
  document.getElementById("messagelist").focus();

  messageView.scrollToMessage(mockMessageId(0));
});
<commit_msg>Mock out getting new messages too<commit_after>"use strict";

function mockMessageId(idx) {
  return btoa(String(idx));
}
function resolveMockId(ref, offset) {
  return Number(atob(ref));
}

function MockMessageModel(count) {
  this.count_ = count;
  this.listeners_ = [];
  this.interval_ = window.setInterval(function() {
    this.count_ += 1;
    var listeners = this.listeners_;
    this.listeners_ = [];
    listeners.forEach(function(tail) {
      tail.wakeUp_();
    })
  }.bind(this), 1000);
}
MockMessageModel.prototype.getMessage_ = function(number) {
  if (number < 0 || number >= this.count_)
    throw "Bad message number";

  var idx = number % SAMPLE_MSGS.length;
  if (idx < 0)
    idx += SAMPLE_MSGS.length;

  var msg = { };
  for (var key in SAMPLE_MSGS[idx]) {
    msg[key] = SAMPLE_MSGS[idx][key];
  }
  msg.number = number;
  msg.id = mockMessageId(number);
  return msg;
};
// TODO(davidben): Just make the API take the options dict along with
// an initial count parameter?
MockMessageModel.prototype.newTailInclusive = function(start, cb) {
  start = resolveMockId(start);
  if (start < 0 || start >= this.count_)
    throw "Bad message id";
  return new MockMessageTail(this, start, cb, {
    inclusive: true
  });
};
MockMessageModel.prototype.newTail = function(start, cb) {
  start = resolveMockId(start);
  if (start < 0 || start >= this.count_)
    throw "Bad message id";
  return new MockMessageTail(this, start, cb, {});
};
MockMessageModel.prototype.newReverseTail = function(start, cb) {
  start = resolveMockId(start);
  if (start < 0 || start >= this.count_)
    throw "Bad message id";
  return new MockMessageTail(this, start, cb, {
    reverse: true
  });
};

function MockMessageTail(model, start, cb, opts) {
  this.model_ = model;
  this.start_ = start;
  this.reverse_ = opts.reverse;
  this.cb_ = cb;

  // Simulate doing only one XHR at a time.
  this.requestPending_ = false;
  this.lastRequested_ = opts.inclusive ? -1 : 0;
  this.count_ = this.lastRequested_;
  this.atEnd_ = false;
}
MockMessageTail.prototype.expandTo = function(count) {
  this.count_ = Math.max(this.count_, count);
  this.fireRequest_();
};
MockMessageTail.prototype.close = function() {
  this.cb_ = null;
};
MockMessageTail.prototype.wakeUp_ = function() {
  this.atEnd_ = false;
  this.fireRequest_(true);
};
MockMessageTail.prototype.fireRequest_ = function(immediate) {
  if (this.requestPending_)
    return;
  if (this.lastRequested_ >= this.count_)
    return;
  if (this.atEnd_)
    return;

  this.requestPending_ = true;

  window.setTimeout(function() {
    var requestStart, requestEnd;
    if (this.reverse_) {
      requestStart = Math.max(0, this.start_ - this.count_);
      requestEnd = this.start_ - this.lastRequested_;
      this.lastRequested_ = this.start_ - requestStart;
    } else {
      requestStart = this.start_ + this.lastRequested_ + 1;
      requestEnd = Math.min(this.start_ + this.count_ + 1, this.model_.count_);
      this.lastRequested_ = requestEnd - this.start_ - 1;
    }

    // Fake some messages.
    var msgs = [];
    for (var i = requestStart; i < requestEnd; i++) {
      msgs.push(this.model_.getMessage_(i));
    }
    this.atEnd_ = this.reverse_ ?
      (requestStart == 0) :
      (requestEnd == this.model_.count_);
    if (this.cb_) {
      this.cb_(msgs, this.atEnd_);
      if (this.atEnd_ && !this.reverse_)
        this.model_.listeners_.push(this);
    }

    this.requestPending_ = false;
    this.fireRequest_();
  }.bind(this), immediate ? 0 : 500);
};

// If the number of messages outside the scroll goes outside
// [MIN_BUFFER, MAX_BUFFER], request/retire messages to reach
// TARGET_BUFFER. That there is a buffer between TARGET_BUFFER and our
// endpoints ensures that spurious scrolling will not repeatedly
// request and retire the same message. Also that we do our requests
// in large-ish batches.
var MIN_BUFFER = 50;
var TARGET_BUFFER = MIN_BUFFER * 2;
var MAX_BUFFER = MIN_BUFFER * 3;

function MessageView(model, container) {
  this.model_ = model;
  this.container_ = container;
  // Make this element focusable. This is needed so that you can
  // direct pageup/down and friends to it. Clicking in it mostly works
  // but we cannot control that programmatically. Moreover, it exposes
  // a quirk in WebKit and Blink; they track the most recently clicked
  // DOM node and use it to determine scroll when there is no focus
  // node. This breaks when we delete that node.
  this.container_.tabIndex = 0;

  this.loadingAbove_ = document.createElement("div");
  this.loadingAbove_.className = "loading";
  this.loadingAbove_.textContent = "Loading...";
  this.atTop_ = true;

  this.loadingBelow_ = document.createElement("div");
  this.loadingBelow_.className = "loading";
  this.loadingBelow_.textContent = "Loading...";
  this.atBottom_ = true;

  this.messagesDiv_ = document.createElement("div");

  this.container_.appendChild(this.loadingAbove_);
  this.container_.appendChild(this.messagesDiv_);
  this.container_.appendChild(this.loadingBelow_);

  this.active_ = false;

  this.tailBelow_ = null;
  this.tailBelowOffset_ = 0;  // The global index of the tail reference.

  this.tailAbove_ = null;
  this.tailAboveOffset_ = 0;  // The global index of the tail reference.

  this.listOffset_ = 0;  // The global index of the top of the list.
  this.messages_ = [];
  this.nodes_ = [];

  this.messageToIndex_ = { };  // Map id to global index.

  this.container_.addEventListener("scroll", this.checkBuffers_.bind(this));
}

MessageView.prototype.reset_ = function() {
  // It's not visible. Blow everything away and start from
  // there. (This is mildly annoying. Can we refactor some of this
  // code to not need this?)
  if (this.tailAbove_) {
    this.tailAbove_.close();
    this.tailAbove_ = null;
  }
  if (this.tailBelow_) {
    this.tailBelow_.close();
    this.tailBelow_ = null;
  }
  this.active_ = false;
  this.listOffset_ = 0;
  this.messages_ = [];
  this.nodes_ = [];
  this.messagesDiv_.textContent = "";
  this.messageToIndex_ = {};
};

MessageView.prototype.scrollToMessage = function(id) {
  if (id in this.messageToIndex_) {
    // Easy case: if the message is in our current view, we just jump
    // to it.
    this.nodes_[this.messageToIndex_[id] - this.listOffset_].scrollIntoView();
    return;
  }

  // Otherwise, we reset the universe and use |id| as our new point of
  // reference.
  this.reset_();
  this.active_ = true;

  this.tailBelow_ = this.model_.newTailInclusive(
    id, this.appendMessages_.bind(this));
  this.tailBelowOffset_ = 0;

  this.tailAbove_ = this.model_.newReverseTail(
    id, this.prependMessages_.bind(this));
  this.tailAboveOffset_ = 0;  // The global index of the tail reference.

  this.checkBuffers_();
};

MessageView.prototype.setAtTop = function(atTop) {
  this.atTop_ = atTop;
  this.loadingAbove_.style.display = atTop ? "none" : "block";
};

MessageView.prototype.setAtBottom = function(atBottom) {
  this.atBottom_ = atBottom;
  this.loadingBelow_.style.display = atBottom ? "none" : "block";
};

MessageView.prototype.appendMessages_ = function(msgs, isDone) {
  for (var i = 0; i < msgs.length; i++) {
    this.messageToIndex_[msgs[i].id] =
      this.messages_.length + this.listOffset_;

    var node = this.formatMessage_(msgs[i]);
    this.nodes_.push(node);
    this.messages_.push(msgs[i]);

    this.messagesDiv_.appendChild(node);
  }

  this.setAtBottom(isDone);
};

MessageView.prototype.prependMessages_ = function(msgs, isDone) {
  // TODO(davidben): This triggers layout a bunch. Optimize this if needbe.
  var nodes = [];
  var insertReference = this.messagesDiv_.firstChild;
  var oldHeight = this.container_.scrollHeight;
  for (var i = 0; i < msgs.length; i++) {
    this.messageToIndex_[msgs[i].id] =
      this.listOffset_ - msgs.length + i;

    var node = this.formatMessage_(msgs[i]);
    nodes.push(node);

    this.messagesDiv_.insertBefore(node, insertReference);
  }
  this.container_.scrollTop += (this.container_.scrollHeight - oldHeight);

  this.messages_.unshift.apply(this.messages_, msgs);
  this.nodes_.unshift.apply(this.nodes_, nodes);
  this.listOffset_ -= msgs.length;

  this.setAtTop(isDone);
};

var COLORS = ["black", "silver", "gray", "white", "maroon", "red",
              "purple", "fuchsia", "green", "lime"];
MessageView.prototype.formatMessage_ = function(msg) {
  var pre = document.createElement("pre");
  var indented = "   " +
    msg.message.replace(/\s+$/, '').split("\n").join("\n   ");
  pre.textContent =
    msg.number + ": " +
    msg.class + " / " + msg.instance + " / " + msg.sender + "  " +
      new Date(msg.time).toString() + "\n" +
    indented;
  pre.className = "message";
  pre.style.color = COLORS[((msg.number % COLORS.length) + COLORS.length) % COLORS.length];
  return pre;
};

// Return 1 if we need to expand below, -1 if we need to contract, and
// 0 if neither.
MessageView.prototype.checkBelow_ = function(bounds) {
  // Do we need to expand?
  if (this.nodes_.length < MIN_BUFFER)
    return 1;
  var b = this.nodes_[this.nodes_.length - MIN_BUFFER].getBoundingClientRect();
  if (bounds.bottom > b.top)
    return 1;

  // Do we need to contract?
  if (this.nodes_.length < MAX_BUFFER)
    return 0;
  b = this.nodes_[this.nodes_.length - MAX_BUFFER].getBoundingClientRect();
  if (bounds.bottom < b.top)
    return -1;

  return 0;
};

MessageView.prototype.checkAbove_ = function(bounds) {
  // Do we need to expand?
  if (this.nodes_.length < MIN_BUFFER)
    return 1;
  var b = this.nodes_[MIN_BUFFER - 1].getBoundingClientRect();
  if (bounds.top < b.bottom)
    return 1;

  // Do we need to contract?
  if (this.nodes_.length < MAX_BUFFER)
    return 0;
  b = this.nodes_[MAX_BUFFER - 1].getBoundingClientRect();
  if (bounds.top > b.bottom)
    return -1;

  return 0;
};

MessageView.prototype.checkBuffers_ = function() {
  if (!this.active_)
    return;

  var bounds = this.container_.getBoundingClientRect();

  // TODO(davidben): Instead of only ever working 50 messages at a
  // time, it's possibly better to just pay a binary search and figure
  // out exactly how many we need to reach TARGET_BUFFER?
  //
  // TODO(davidben): Trigger removal by receiving messages?
  var below = this.checkBelow_(bounds);
  if (below > 0) {
    if (!this.tailBelow_) {
      this.tailBelow_ = this.model_.newTail(
        this.messages_[this.messages_.length - 1].id,
        this.appendMessages_.bind(this));
      this.tailBelowOffset_ = this.listOffset_ + this.messages_.length - 1;
    }
    this.tailBelow_.expandTo(
      this.listOffset_ + this.nodes_.length - 1 + (TARGET_BUFFER - MIN_BUFFER)
        - this.tailBelowOffset_);
  } else if (below < 0) {
    // Close the current tail.
    if (this.tailBelow_) {
      this.tailBelow_.close();
      this.tailBelow_ = null;
    }

    var num = MAX_BUFFER - TARGET_BUFFER;
    for (var i = 0; i < num; i++) {
      var idx = this.nodes_.length - i - 1;
      this.messagesDiv_.removeChild(this.nodes_[idx]);
      delete this.messageToIndex_[this.messages_[idx].id];
    }
    this.nodes_.splice(this.nodes_.length - num, num);
    this.messages_.splice(this.messages_.length - num, num);

    this.setAtBottom(false);
  }

  var above = this.checkAbove_(bounds);
  if (above > 0) {
    if (!this.tailAbove_) {
      this.tailAbove_ = this.model_.newReverseTail(
        this.messages_[0].id,
        this.prependMessages_.bind(this));
      this.tailAboveOffset_ = this.listOffset_;
    }
    this.tailAbove_.expandTo(
      this.tailAboveOffset_ -
        (this.listOffset_ - (TARGET_BUFFER - MIN_BUFFER)));
  } else if (above < 0) {
    // Close the current tail.
    if (this.tailAbove_) {
      this.tailAbove_.close();
      this.tailAbove_ = null;
    }

    var maxRemoved = MAX_BUFFER - TARGET_BUFFER;
    var oldHeight = this.container_.scrollHeight;

    // Limit the nodes removed; if we remove enough that scrollTop has
    // to change, the scroll gets off. Do this in two passes so as not
    // to keep re-triggering layout with
    // getBoundingClientRect. Unfortunately, this isn't the cause of
    // us getting stuck.
    var heightLost = 0;
    var maxHeight = oldHeight - this.container_.scrollTop - bounds.height;
    for (var num = 0; num < maxRemoved; num++) {
      // FIXME: What if there are margins?
      var b = this.nodes_[num].getBoundingClientRect();
      if (heightLost + b.height >= maxHeight)
        break;
      heightLost += b.height;
    }

    for (var i = 0; i < num; i++) {
      this.messagesDiv_.removeChild(this.nodes_[i]);
      delete this.messageToIndex_[this.messages_[i].id];
    }
    this.container_.scrollTop += (this.container_.scrollHeight - oldHeight);
    this.nodes_.splice(0, num);
    this.messages_.splice(0, num);
    this.listOffset_ += num;

    this.setAtTop(false);
  }
};

var messageView;  // For debugging.
$(function() {
  messageView = new MessageView(new MockMessageModel(1000),
                                document.getElementById("messagelist"));
  document.getElementById("messagelist").focus();

  messageView.scrollToMessage(mockMessageId(950));
});
<|endoftext|>


<commit_before>import { ease } from '../util/ease'
import { DIRECTION_DOWN, PROBE_REALTIME } from '../util/const'

export function pullDownMixin(BScroll) {
  BScroll.prototype._initPullDown = function () {
    // must watch scroll in real time
    this.options.probeType = PROBE_REALTIME
  }

  BScroll.prototype._checkPullDown = function () {
    const {threshold = 90, stop = 40} = this.options.pullDownRefresh

    // check if a real pull down action
    if (this.directionY !== DIRECTION_DOWN || this.y < threshold) {
      return false
    }

    if (!this.pulling) {
      this.pulling = true
      this.trigger('pullingDown')
    }
    this.scrollTo(this.x, stop, this.options.bounceTime, ease.bounce)

    return this.pulling
  }

  BScroll.prototype.finishPullDown = function () {
    this.pulling = false
    this.resetPosition(this.options.bounceTime, ease.bounce)
  }

  BScroll.prototype.openPullDown = function (config = true) {
    this.options.pullDownRefresh = config
    this._initPullDown()
  }

  BScroll.prototype.closePullDown = function () {
    this.options.pullDownRefresh = false
  }

  BScroll.prototype.autoPullDownRefresh = function () {
    const {threshold = 90, stop = 40} = this.options.pullDownRefresh

    this.scrollTo(this.x, threshold)

    if (!this.pulling) {
      this.pulling = true
      this.trigger('pullingDown')
    }

    this.scrollTo(this.x, stop, this.options.bounceTime, ease.bounce)
  }
}
<commit_msg>optimize autoPullDownRefresh<commit_after>import { ease } from '../util/ease'
import { DIRECTION_DOWN, PROBE_REALTIME } from '../util/const'

export function pullDownMixin (BScroll) {
  BScroll.prototype._initPullDown = function () {
    // must watch scroll in real time
    this.options.probeType = PROBE_REALTIME
  }

  BScroll.prototype._checkPullDown = function () {
    const { threshold = 90, stop = 40 } = this.options.pullDownRefresh

    // check if a real pull down action
    if (this.directionY !== DIRECTION_DOWN || this.y < threshold) {
      return false
    }

    if (!this.pulling) {
      this.pulling = true
      this.trigger('pullingDown')
    }
    this.scrollTo(this.x, stop, this.options.bounceTime, ease.bounce)

    return this.pulling
  }

  BScroll.prototype.finishPullDown = function () {
    this.pulling = false
    this.resetPosition(this.options.bounceTime, ease.bounce)
  }

  BScroll.prototype.openPullDown = function (config = true) {
    this.options.pullDownRefresh = config
    this._initPullDown()
  }

  BScroll.prototype.closePullDown = function () {
    this.options.pullDownRefresh = false
  }

  BScroll.prototype.autoPullDownRefresh = function () {
    const { threshold = 90, stop = 40 } = this.options.pullDownRefresh

    if (this.pulling) {
      return
    }
    this.pulling = true

    this.scrollTo(this.x, threshold)
    this.trigger('pullingDown')
    this.scrollTo(this.x, stop, this.options.bounceTime, ease.bounce)
  }
}
<|endoftext|>