<commit_before>/*global $, app, me, client*/
"use strict";

var _ = require('underscore');
var StayDown = require('staydown');
var BasePage = require('./base');
var templates = require('../templates');
var MUCRosterItem = require('../views/mucRosterItem');
var Message = require('../views/mucMessage');
var MessageModel = require('../models/message');
var embedIt = require('../helpers/embedIt');
var htmlify = require('../helpers/htmlify');
var tempSubject = '';

var groupchat = module.exports = BasePage.extend({
    template: templates.pages.groupchat,
    initialize: function (spec) {
        this.editMode = false;

        this.listenTo(this, 'pageloaded', this.handlePageLoaded);
        this.listenTo(this, 'pageunloaded', this.handlePageUnloaded);

        this.listenTo(this.model.messages, 'change', this.refreshModel);
        this.listenTo(this.model.messages, 'reset', this.renderMessages);
        this.listenTo(this.model, 'refresh', this.renderMessages);

        app.state.bind('change:connected', this.connectionChange, this);

        this.render();
    },
    events: {
        'keydown textarea': 'handleKeyDown',
        'keyup textarea': 'handleKeyUp',
        'click .status': 'clickStatusChange',
        'blur .status': 'blurStatusChange',
        'keydown .status': 'keyDownStatusChange',
        'click #members_toggle': 'clickMembersToggle'
    },
    classBindings: {
        joined: '.controls'
    },
    textBindings: {
        displayName: 'header .name',
        subject: 'header .status',
        membersCount: '#members_toggle_count'
    },
    props: {
        name: 'string'
    },
    derived: {
        namec: {
            deps: ['displayName'],
            fn: function () {
                return this.displayName;
            }
        }
    },
    show: function (animation) {
        BasePage.prototype.show.apply(this, [animation]);
        client.sendMessage({
            type: 'groupchat',
            to: this.model.jid,
            chatState: 'active'
        });

        this.firstChanged = true;
        var self = this;
        $('.messages').scroll(function() {
            if (self.firstChanged && $(".messages li:first-child").offset().top > 0) {
                self.firstChanged = false;
                self.model.fetchHistory();
            }
        });

        this.$chatInput.focus();
    },
    hide: function () {
        BasePage.prototype.hide.apply(this);
        client.sendMessage({
            type: 'groupchat',
            to: this.model.jid,
            chatState: 'inactive'
        });
    },
    render: function () {
        if (this.rendered) return this;
        this.rendered = true;

        this.renderAndBind();
        this.$chatInput = this.$('.chatBox textarea');
        this.$chatInput.val(app.composing[this.model.jid] || '');
        this.$chatBox = this.$('.chatBox');
        this.$messageList = this.$('.messages');
        this.$autoComplete = this.$('.autoComplete');

        this.staydown = new StayDown(this.$messageList[0], 500);

        this.renderMessages();

        this.renderCollection(this.model.resources, MUCRosterItem, this.$('.groupRoster'));

        this.listenTo(this, 'rosterItemClicked', this.rosterItemSelected);
        this.listenTo(this.model.messages, 'add', this.handleChatAdded);
        this.listenTo(this.model.resources, 'add', this.handleResourceAdded);

        $(window).on('resize', _.bind(this.resizeInput, this));

        this.registerBindings();

        return this;
    },
    renderMessages: function () {
        var self = this;

        this.$messageList.empty();
        delete this.firstModel;
        delete this.firstDate;
        delete this.lastModel;
        delete this.lastDate;

        this.model.messages.each(function (model, i) {
            self.appendModel(model);
        });
        this.staydown.checkdown();
    },
    handleChatAdded: function (model) {
        this.appendModel(model, true);
    },
    handleResourceAdded: function (model) {
        var xmppContact = me.getContact(model.id.split('/')[1]);
        if (xmppContact) {
            xmppContact.bind('change:avatar', this.handleAvatarChanged, this);
        }
    },
    handleAvatarChanged: function(contact, uri) {
        if (!me.isMe(contact.jid)) {
            $('.' + contact.jid.substr(0, contact.jid.indexOf('@')) + ' .messageAvatar img').attr('src', uri);
        }
    },
    handlePageLoaded: function () {
        this.staydown.checkdown();
        this.resizeInput();
    },
    handleKeyDown: function (e) {
        if ((e.which === 13 || e.which === 9) && !e.shiftKey) { // Enter or Tab
            if (this.$autoComplete.css('display') != 'none') {
                var nickname = this.$autoComplete.find(">:nth-child(" + this.autoCompletePos + ")>:first-child").text();
                this.rosterItemSelected(nickname);
            } else {
                app.composing[this.model.jid] = '';
                this.sendChat();
            }
            e.preventDefault();
            return false;
        } else if (e.which === 38) { // Up arrow

            if (this.$autoComplete.css('display') != 'none') {
                var count = this.$autoComplete.find(">li").length;
                var oldPos = this.autoCompletePos;
                this.autoCompletePos = (oldPos - 1) < 1 ? count : oldPos - 1;

                this.$autoComplete.find(">:nth-child(" + oldPos + ")").removeClass('selected');
                this.$autoComplete.find(">:nth-child(" + this.autoCompletePos + ")").addClass('selected');

            }
            else if (this.$chatInput.val() === '' && this.model.lastSentMessage) {
                this.editMode = true;
                this.$chatInput.addClass('editing');
                this.$chatInput.val(this.model.lastSentMessage.body);
            }
            e.preventDefault();
            return false;
        } else if (e.which === 40) { // Down arrow

            if (this.$autoComplete.css('display') != 'none') {
                var count = this.$autoComplete.find(">li").length;
                var oldPos = this.autoCompletePos;
                this.autoCompletePos = (oldPos + 1) > count ? 1 : oldPos + 1;

                this.$autoComplete.find(">:nth-child(" + oldPos + ")").removeClass('selected');
                this.$autoComplete.find(">:nth-child(" + this.autoCompletePos + ")").addClass('selected');
            }
            else if (this.editMode) {
                this.editMode = false;
                this.$chatInput.removeClass('editing');
            }
            e.preventDefault();
            return false;
        } else if (!e.ctrlKey && !e.metaKey) {
            if (!this.typing || this.paused) {
                this.typing = true;
                this.paused = false;
                client.sendMessage({
                    type: 'groupchat',
                    to: this.model.jid,
                    chatState: 'composing'
                });
            }
        }
    },
    handleKeyUp: function (e) {
        this.resizeInput();
        app.composing[this.model.jid] = this.$chatInput.val();
        if (this.typing && this.$chatInput.val().length === 0) {
            this.typing = false;
            this.paused = false;
            client.sendMessage({
                type: 'groupchat',
                to: this.model.jid,
                chatState: 'active'
            });
        } else if (this.typing) {
            this.pausedTyping();
        }

        if (([38, 40, 13]).indexOf(e.which) === -1) {
            var lastWord = this.$chatInput.val().split(' ').pop();
            if (lastWord.charAt(0) === '@') {
              var models = this.model.resources.search(lastWord.substr(1) || '', true, true);
              if (models.length) {
                this.renderCollection(models, MUCRosterItem, this.$autoComplete);
                this.autoCompletePos = 1;
                this.$autoComplete.find(">:nth-child(" + this.autoCompletePos + ")").addClass('selected');
                this.$autoComplete.show();
              }
              else
                this.$autoComplete.hide();
            }

            if (this.$autoComplete.css('display') != 'none') {
                if( lastWord ===  '') {
                    this.$autoComplete.hide();
                    return;
                }
            }
        }
    },
    rosterItemSelected: function (nickName) {
        if (nickName == me.nick)
            nickName = 'me';
        var val = this.$chatInput.val();
        var splited = val.split(' ');
        var length = splited.length-1;
        var lastWord = splited.pop();
        if (('@' + nickName).indexOf(lastWord) > -1)
            splited[length] = '@' + nickName + ' ';
        else
            splited.push('@' + nickName + ' ');
        this.$chatInput.val(splited.join(' '));
        this.$autoComplete.hide();
        this.$chatInput.focus();
    },
    resizeInput: _.throttle(function () {
        var height;
        var scrollHeight;
        var heightDiff;
        var newHeight;
        var newMargin;
        var marginDelta;
        var maxHeight = parseInt(this.$chatInput.css('max-height'), 10);

        this.$chatInput.removeAttr('style');
        height = this.$chatInput.outerHeight(),
        scrollHeight = this.$chatInput.get(0).scrollHeight,
        newHeight = Math.max(height, scrollHeight);
        heightDiff = height - this.$chatInput.innerHeight();

        if (newHeight > maxHeight) newHeight = maxHeight;
        if (newHeight > height) {
            this.$chatInput.css('height', newHeight+heightDiff);
            this.$chatInput.scrollTop(this.$chatInput[0].scrollHeight - this.$chatInput.height());
            newMargin = newHeight - height + heightDiff;
            marginDelta = newMargin - parseInt(this.$messageList.css('marginBottom'), 10);
            if (!!marginDelta) {
                this.$messageList.css('marginBottom', newMargin);
            }
        } else {
            this.$messageList.css('marginBottom', 0);
        }
    }, 300),
    pausedTyping: _.debounce(function () {
        if (this.typing && !this.paused) {
            this.paused = true;
            client.sendMessage({
                type: 'groupchat',
                to: this.model.jid,
                chatState: 'paused'
            });
        }
    }, 3000),
    sendChat: function () {
        var message;
        var val = this.$chatInput.val();

        if (val) {
            this.staydown.intend_down = true;

            var links = _.map(htmlify.collectLinks(val), function (link) {
                return {url: link};
            });

            message = {
                to: this.model.jid,
                type: 'groupchat',
                body: val,
                chatState: 'active',
                oobURIs: links
            };
            if (this.editMode) {
                message.replace = this.model.lastSentMessage.mid || this.model.lastSentMessage.cid;
            }

            var id = client.sendMessage(message);
            message.mid = id;
            message.from = client.JID(this.model.jid.bare + '/' + this.model.nick);

            if (this.editMode) {
                this.model.lastSentMessage.correct(message);
            } else {
                var msgModel = new MessageModel(message);
                this.model.addMessage(msgModel, false);
                this.model.lastSentMessage = msgModel;
            }
        }
        this.editMode = false;
        this.typing = false;
        this.paused = false;
        this.$chatInput.removeClass('editing');
        this.$chatInput.val('');
    },
    clickStatusChange: function (e) {
        tempSubject = e.target.textContent;
    },
    blurStatusChange: function (e) {
        var subject = e.target.textContent;
        if (subject == '')
            subject = true;
        client.setSubject(this.model.jid, subject);
        e.target.textContent = tempSubject;
    },
    keyDownStatusChange: function (e) {
        if (e.which === 13 && !e.shiftKey) {
            e.target.blur();
            return false;
        }
    },
    clickMembersToggle: function (e) {
        var roster = $('.groupRoster');
        if (roster.css('visibility') == 'hidden')
            roster.css('visibility', 'visible');
        else
            roster.css('visibility', 'hidden');
    },
    appendModel: function (model, preload) {
        var newEl, first, last;
        var msgDate = Date.create(model.timestamp);
        var messageDay = msgDate.format('{month} {ord}, {yyyy}');

        if (this.firstModel === undefined || msgDate > Date.create(this.firstModel.timestamp)) {
            if (this.firstModel === undefined) {
                this.firstModel = model;
                this.firstDate = messageDay;
            }

            if (messageDay !== this.lastDate) {
                var dayDivider = $(templates.includes.dayDivider({day_name: messageDay}));
                this.staydown.append(dayDivider[0]);
                this.lastDate = messageDay;
            }

            var isGrouped = model.shouldGroupWith(this.lastModel);
            if (isGrouped) {
                newEl = $(model.partialTemplateHtml);
                last = this.$messageList.find('li').last();
                last.find('.messageWrapper').append(newEl);
                last.addClass('chatGroup');
                this.staydown.checkdown();
            } else {
                newEl = $(model.templateHtml);
                newEl.addClass(model.sender.getNickname(model.from.full));
                this.staydown.append(newEl[0]);
                this.lastModel = model;
            }
            if (!model.pending) embedIt(newEl);
        }
        else {
            var scrollDown = this.$messageList.prop('scrollHeight') - this.$messageList.scrollTop();
            var firstEl = this.$messageList.find('li').first();

            if (messageDay !== this.firstDate) {
                var dayDivider = $(templates.includes.dayDivider({day_name: messageDay}));
                firstEl.before(dayDivider[0]);
                var firstEl = this.$messageList.find('li').first();
                this.firstDate = messageDay;
            }

            var isGrouped = model.shouldGroupWith(this.firstModel);
            if (isGrouped) {
                newEl = $(model.partialTemplateHtml);
                first = this.$messageList.find('li').first().next();
                first.find('.messageWrapper div:first').after(newEl);
                first.addClass('chatGroup');
            } else {
                newEl = $(model.templateHtml);
                newEl.addClass(model.sender.getNickname(model.from.full));
                firstEl.after(newEl[0]);
                this.firstModel = model;
            }
            if (!model.pending) embedIt(newEl);

            this.$messageList.scrollTop(this.$messageList.prop('scrollHeight') - scrollDown);
            this.firstChanged = true;
        }
    },
    refreshModel: function (model) {
        var existing = this.$('#chat' + model.cid);
        existing.replaceWith(model.bareMessageTemplate(existing.prev().hasClass('message_header')));
    },
    connectionChange: function () {
        if (app.state.connected) {
            this.$chatInput.attr("disabled", false);
        } else {
            this.$chatInput.attr("disabled", "disabled");
        }
    }
});
<commit_msg>Update groupchat.js<commit_after>/*global $, app, me, client*/
"use strict";

var _ = require('underscore');
var StayDown = require('staydown');
var BasePage = require('./base');
var templates = require('../templates');
var MUCRosterItem = require('../views/mucRosterItem');
var Message = require('../views/mucMessage');
var MessageModel = require('../models/message');
var embedIt = require('../helpers/embedIt');
var htmlify = require('../helpers/htmlify');
var tempSubject = '';

var plzchat = module.exports = BasePage.extend({
    template: templates.pages.groupchat,
    initialize: function (spec) {
        this.editMode = false;

        this.listenTo(this, 'pageloaded', this.handlePageLoaded);
        this.listenTo(this, 'pageunloaded', this.handlePageUnloaded);

        this.listenTo(this.model.messages, 'change', this.refreshModel);
        this.listenTo(this.model.messages, 'reset', this.renderMessages);
        this.listenTo(this.model, 'refresh', this.renderMessages);

        app.state.bind('change:connected', this.connectionChange, this);

        this.render();
    },
    events: {
        'keydown textarea': 'handleKeyDown',
        'keyup textarea': 'handleKeyUp',
        'click .status': 'clickStatusChange',
        'blur .status': 'blurStatusChange',
        'keydown .status': 'keyDownStatusChange',
        'click #members_toggle': 'clickMembersToggle'
    },
    classBindings: {
        joined: '.controls'
    },
    textBindings: {
        displayName: 'header .name',
        subject: 'header .status',
        membersCount: '#members_toggle_count'
    },
    props: {
        name: 'string'
    },
    derived: {
        namec: {
            deps: ['displayName'],
            fn: function () {
                return this.displayName;
            }
        }
    },
    show: function (animation) {
        BasePage.prototype.show.apply(this, [animation]);
        client.sendMessage({
            type: 'groupchat',
            to: this.model.jid,
            chatState: 'active'
        });

        this.firstChanged = true;
        var self = this;
        $('.messages').scroll(function() {
            if (self.firstChanged && $(".messages li:first-child").offset().top > 0) {
                self.firstChanged = false;
                self.model.fetchHistory();
            }
        });

        this.$chatInput.focus();
    },
    hide: function () {
        BasePage.prototype.hide.apply(this);
        client.sendMessage({
            type: 'groupchat',
            to: this.model.jid,
            chatState: 'inactive'
        });
    },
    render: function () {
        if (this.rendered) return this;
        this.rendered = true;

        this.renderAndBind();
        this.$chatInput = this.$('.chatBox textarea');
        this.$chatInput.val(app.composing[this.model.jid] || '');
        this.$chatBox = this.$('.chatBox');
        this.$messageList = this.$('.messages');
        this.$autoComplete = this.$('.autoComplete');

        this.staydown = new StayDown(this.$messageList[0], 500);

        this.renderMessages();

        this.renderCollection(this.model.resources, MUCRosterItem, this.$('.groupRoster'));

        this.listenTo(this, 'rosterItemClicked', this.rosterItemSelected);
        this.listenTo(this.model.messages, 'add', this.handleChatAdded);
        this.listenTo(this.model.resources, 'add', this.handleResourceAdded);

        $(window).on('resize', _.bind(this.resizeInput, this));

        this.registerBindings();

        return this;
    },
    renderMessages: function () {
        var self = this;

        this.$messageList.empty();
        delete this.firstModel;
        delete this.firstDate;
        delete this.lastModel;
        delete this.lastDate;

        this.model.messages.each(function (model, i) {
            self.appendModel(model);
        });
        this.staydown.checkdown();
    },
    handleChatAdded: function (model) {
        this.appendModel(model, true);
    },
    handleResourceAdded: function (model) {
        var xmppContact = me.getContact(model.id.split('/')[1]);
        if (xmppContact) {
            xmppContact.bind('change:avatar', this.handleAvatarChanged, this);
        }
    },
    handleAvatarChanged: function(contact, uri) {
        if (!me.isMe(contact.jid)) {
            $('.' + contact.jid.substr(0, contact.jid.indexOf('@')) + ' .messageAvatar img').attr('src', uri);
        }
    },
    handlePageLoaded: function () {
        this.staydown.checkdown();
        this.resizeInput();
    },
    handleKeyDown: function (e) {
        if ((e.which === 13 || e.which === 9) && !e.shiftKey) { // Enter or Tab
            if (this.$autoComplete.css('display') != 'none') {
                var nickname = this.$autoComplete.find(">:nth-child(" + this.autoCompletePos + ")>:first-child").text();
                this.rosterItemSelected(nickname);
            } else {
                app.composing[this.model.jid] = '';
                this.sendChat();
            }
            e.preventDefault();
            return false;
        } else if (e.which === 38) { // Up arrow

            if (this.$autoComplete.css('display') != 'none') {
                var count = this.$autoComplete.find(">li").length;
                var oldPos = this.autoCompletePos;
                this.autoCompletePos = (oldPos - 1) < 1 ? count : oldPos - 1;

                this.$autoComplete.find(">:nth-child(" + oldPos + ")").removeClass('selected');
                this.$autoComplete.find(">:nth-child(" + this.autoCompletePos + ")").addClass('selected');

            }
            else if (this.$chatInput.val() === '' && this.model.lastSentMessage) {
                this.editMode = true;
                this.$chatInput.addClass('editing');
                this.$chatInput.val(this.model.lastSentMessage.body);
            }
            e.preventDefault();
            return false;
        } else if (e.which === 40) { // Down arrow

            if (this.$autoComplete.css('display') != 'none') {
                var count = this.$autoComplete.find(">li").length;
                var oldPos = this.autoCompletePos;
                this.autoCompletePos = (oldPos + 1) > count ? 1 : oldPos + 1;

                this.$autoComplete.find(">:nth-child(" + oldPos + ")").removeClass('selected');
                this.$autoComplete.find(">:nth-child(" + this.autoCompletePos + ")").addClass('selected');
            }
            else if (this.editMode) {
                this.editMode = false;
                this.$chatInput.removeClass('editing');
            }
            e.preventDefault();
            return false;
        } else if (!e.ctrlKey && !e.metaKey) {
            if (!this.typing || this.paused) {
                this.typing = true;
                this.paused = false;
                client.sendMessage({
                    type: 'groupchat',
                    to: this.model.jid,
                    chatState: 'composing'
                });
            }
        }
    },
    handleKeyUp: function (e) {
        this.resizeInput();
        app.composing[this.model.jid] = this.$chatInput.val();
        if (this.typing && this.$chatInput.val().length === 0) {
            this.typing = false;
            this.paused = false;
            client.sendMessage({
                type: 'groupchat',
                to: this.model.jid,
                chatState: 'active'
            });
        } else if (this.typing) {
            this.pausedTyping();
        }

        if (([38, 40, 13]).indexOf(e.which) === -1) {
            var lastWord = this.$chatInput.val().split(' ').pop();
            if (lastWord.charAt(0) === '@') {
              var models = this.model.resources.search(lastWord.substr(1) || '', true, true);
              if (models.length) {
                this.renderCollection(models, MUCRosterItem, this.$autoComplete);
                this.autoCompletePos = 1;
                this.$autoComplete.find(">:nth-child(" + this.autoCompletePos + ")").addClass('selected');
                this.$autoComplete.show();
              }
              else
                this.$autoComplete.hide();
            }

            if (this.$autoComplete.css('display') != 'none') {
                if( lastWord ===  '') {
                    this.$autoComplete.hide();
                    return;
                }
            }
        }
    },
    rosterItemSelected: function (nickName) {
        if (nickName == me.nick)
            nickName = 'me';
        var val = this.$chatInput.val();
        var splited = val.split(' ');
        var length = splited.length-1;
        var lastWord = splited.pop();
        if (('@' + nickName).indexOf(lastWord) > -1)
            splited[length] = '@' + nickName + ' ';
        else
            splited.push('@' + nickName + ' ');
        this.$chatInput.val(splited.join(' '));
        this.$autoComplete.hide();
        this.$chatInput.focus();
    },
    resizeInput: _.throttle(function () {
        var height;
        var scrollHeight;
        var heightDiff;
        var newHeight;
        var newMargin;
        var marginDelta;
        var maxHeight = parseInt(this.$chatInput.css('max-height'), 10);

        this.$chatInput.removeAttr('style');
        height = this.$chatInput.outerHeight(),
        scrollHeight = this.$chatInput.get(0).scrollHeight,
        newHeight = Math.max(height, scrollHeight);
        heightDiff = height - this.$chatInput.innerHeight();

        if (newHeight > maxHeight) newHeight = maxHeight;
        if (newHeight > height) {
            this.$chatInput.css('height', newHeight+heightDiff);
            this.$chatInput.scrollTop(this.$chatInput[0].scrollHeight - this.$chatInput.height());
            newMargin = newHeight - height + heightDiff;
            marginDelta = newMargin - parseInt(this.$messageList.css('marginBottom'), 10);
            if (!!marginDelta) {
                this.$messageList.css('marginBottom', newMargin);
            }
        } else {
            this.$messageList.css('marginBottom', 0);
        }
    }, 300),
    pausedTyping: _.debounce(function () {
        if (this.typing && !this.paused) {
            this.paused = true;
            client.sendMessage({
                type: 'groupchat',
                to: this.model.jid,
                chatState: 'paused'
            });
        }
    }, 3000),
    sendChat: function () {
        var message;
        var val = this.$chatInput.val();

        if (val) {
            this.staydown.intend_down = true;

            var links = _.map(htmlify.collectLinks(val), function (link) {
                return {url: link};
            });

            message = {
                to: this.model.jid,
                type: 'groupchat',
                body: val,
                chatState: 'active',
                oobURIs: links
            };
            if (this.editMode) {
                message.replace = this.model.lastSentMessage.mid || this.model.lastSentMessage.cid;
            }

            var id = client.sendMessage(message);
            message.mid = id;
            message.from = client.JID(this.model.jid.bare + '/' + this.model.nick);

            if (this.editMode) {
                this.model.lastSentMessage.correct(message);
            } else {
                var msgModel = new MessageModel(message);
                this.model.addMessage(msgModel, false);
                this.model.lastSentMessage = msgModel;
            }
        }
        this.editMode = false;
        this.typing = false;
        this.paused = false;
        this.$chatInput.removeClass('editing');
        this.$chatInput.val('');
    },
    clickStatusChange: function (e) {
        tempSubject = e.target.textContent;
    },
    blurStatusChange: function (e) {
        var subject = e.target.textContent;
        if (subject == '')
            subject = true;
        client.setSubject(this.model.jid, subject);
        e.target.textContent = tempSubject;
    },
    keyDownStatusChange: function (e) {
        if (e.which === 13 && !e.shiftKey) {
            e.target.blur();
            return false;
        }
    },
    clickMembersToggle: function (e) {
        var roster = $('.groupRoster');
        if (roster.css('visibility') == 'hidden')
            roster.css('visibility', 'visible');
        else
            roster.css('visibility', 'hidden');
    },
    appendModel: function (model, preload) {
        var newEl, first, last;
        var msgDate = Date.create(model.timestamp);
        var messageDay = msgDate.format('{month} {ord}, {yyyy}');

        if (this.firstModel === undefined || msgDate > Date.create(this.firstModel.timestamp)) {
            if (this.firstModel === undefined) {
                this.firstModel = model;
                this.firstDate = messageDay;
            }

            if (messageDay !== this.lastDate) {
                var dayDivider = $(templates.includes.dayDivider({day_name: messageDay}));
                this.staydown.append(dayDivider[0]);
                this.lastDate = messageDay;
            }

            var isGrouped = model.shouldGroupWith(this.lastModel);
            if (isGrouped) {
                newEl = $(model.partialTemplateHtml);
                last = this.$messageList.find('li').last();
                last.find('.messageWrapper').append(newEl);
                last.addClass('chatGroup');
                this.staydown.checkdown();
            } else {
                newEl = $(model.templateHtml);
                newEl.addClass(model.sender.getNickname(model.from.full));
                this.staydown.append(newEl[0]);
                this.lastModel = model;
            }
            if (!model.pending) embedIt(newEl);
        }
        else {
            var scrollDown = this.$messageList.prop('scrollHeight') - this.$messageList.scrollTop();
            var firstEl = this.$messageList.find('li').first();

            if (messageDay !== this.firstDate) {
                var dayDivider = $(templates.includes.dayDivider({day_name: messageDay}));
                firstEl.before(dayDivider[0]);
                var firstEl = this.$messageList.find('li').first();
                this.firstDate = messageDay;
            }

            var isGrouped = model.shouldGroupWith(this.firstModel);
            if (isGrouped) {
                newEl = $(model.partialTemplateHtml);
                first = this.$messageList.find('li').first().next();
                first.find('.messageWrapper div:first').after(newEl);
                first.addClass('chatGroup');
            } else {
                newEl = $(model.templateHtml);
                newEl.addClass(model.sender.getNickname(model.from.full));
                firstEl.after(newEl[0]);
                this.firstModel = model;
            }
            if (!model.pending) embedIt(newEl);

            this.$messageList.scrollTop(this.$messageList.prop('scrollHeight') - scrollDown);
            this.firstChanged = true;
        }
    },
    refreshModel: function (model) {
        var existing = this.$('#chat' + model.cid);
        existing.replaceWith(model.bareMessageTemplate(existing.prev().hasClass('message_header')));
    },
    connectionChange: function () {
        if (app.state.connected) {
            this.$chatInput.attr("disabled", false);
        } else {
            this.$chatInput.attr("disabled", "disabled");
        }
    }
});
<|endoftext|>


<commit_before>(function() {

    "use strict";

    /** This behavior provides "fly" mode camera control on the given
     *  camera transformable.
     *  This behavior does have no notion of the interaction device. All it needs
     *  are deltaX and deltaY values, from which it computes the camera pose.
     *
     *  Usage:
     *  o instantiate this class
     *  o call rotate(), move{Forward,Backward}(), step{Left,Right}()
     *
     *  @constructor
     */
    XMOT.FlyControllerBehavior = new XMOT.Class({

        /**
         *  @this {XMOT.FlyControllerBehavior}
         *  @param {Element|Transformable} targetViewGroup
         *  @param {Object} options
         *
         *  options:
         *  o rotateSpeed
         *  o moveSpeed
         */
        initialize: function(targetViewGroup, options) {

			this.target = XMOT.util.getOrCreateTransformable(targetViewGroup);

            var options = options || {};

            this._rotateSpeed = options.rotateSpeed || 1;
            this._moveSpeed = options.moveSpeed || 1;

            this._initialRotation = new XML3DRotation();
            this._yAxisAngle = 0;
            this._xAxisAngle = 0;
            this._maxXAxisAngle = Math.PI/2 - 0.2;

            this._setInitialRotation(this.target.getOrientation());
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         */
        rotate: function(deltaX, deltaY) {

            var deltaXAxis = -this._rotateSpeed * deltaX * 2.0 * Math.PI;
            var deltaYAxis = -this._rotateSpeed * deltaY * 2.0 * Math.PI;

            this._xAxisAngle += deltaXAxis;
            this._yAxisAngle += deltaYAxis;

            if(this._xAxisAngle >= this._maxXAxisAngle)
                this._xAxisAngle = this._maxXAxisAngle;
            if(this._xAxisAngle <= -this._maxXAxisAngle)
                this._xAxisAngle = -this._maxXAxisAngle;

            var mx = new window.XML3DRotation(new window.XML3DVec3(1, 0, 0), this._xAxisAngle);
            var my = new window.XML3DRotation(new window.XML3DVec3(0, 1, 0), this._yAxisAngle);
            var rot = my.multiply(mx);

            var newRot = rot.multiply(this._initialRotation);

            this.target.setOrientation(newRot);
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         */
        moveForward: function() {
            this._moveInCamDirection();
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         */
        moveBackward: function() {
            this._moveInCamDirection(true);
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         */
        stepRight: function() {
            this._stepRight();
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         */
        stepLeft: function() {
            this._stepRight(true);
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         */
        lookAt: function(point) {

            var initCamDirection = new XML3DVec3(0, 0, -1);

            // calculate new direction
            var position = this.getPosition();
            var direction = point.subtract(position);
            direction = direction.normalize();

            // create rotation from angle b/w initial and new direction
            var dirRot = new XML3DRotation();
            dirRot.setRotation(initCamDirection, direction);

            this._setInitialRotation(dirRot);
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         */
        setPosition: function(position) {
            this.target.setPosition(position);
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         */
        setOrientation: function(orientation) {
            this._setInitialRotation(orientation);
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         */
        getPosition: function() {
            return this.target.getPosition();
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         */
        getOrientation: function() {
            return this.target.getOrientation();
        },

        getMoveSpeed: function() {
            return this._moveSpeed;
        },

        setMoveSpeed: function(speed) {
            this._moveSpeed = speed;
        },

        getRotationSpeed: function() {
            return this._rotateSpeed;
        },

        setRotationSpeed: function(speed) {
            this._rotateSpeed = speed;
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         *  @private
         */
        _moveInCamDirection: function(doInvertDirection) {

            var moveDirection = this._getLookDirection();

            if(doInvertDirection) {
                moveDirection = moveDirection.scale(-1);
            }

            this._translateCamera(moveDirection);
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         *  @private
         */
        _stepRight: function(doInvertDirection) {

            var lookDirection = this._getLookDirection();

            var stepDirection = lookDirection.cross(new XML3DVec3(0,1,0));

            if(doInvertDirection) {
                stepDirection = stepDirection.scale(-1);
            }

            this._translateCamera(stepDirection);
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         *  @private
         */
        _getLookDirection: function() {

            var curRot = this.target.getOrientation();

            var defaultDirection = new window.XML3DVec3(0, 0, -1);
            var lookDirection = curRot.rotateVec3(defaultDirection).normalize();

            return lookDirection;
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         *  @private
         */
        _translateCamera: function(direction) {

            var transl = direction.scale(this._moveSpeed);

            var newPos = this.target.getPosition().add(transl);
            this.target.setPosition(newPos);
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         *  @private
         */
        _setInitialRotation: function(rot) {

            var euler = this._toEuler(rot);

            this._xAxisAngle = euler.x;
            this._yAxisAngle = euler.y;
            this._initialRotation.set(new XML3DRotation());

            // align the target's orientation to compensate for precision errors
            this.rotate(0, 0);
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         *  @private
         */
        _toEuler: function(rot) {

            // from: http://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToEuler/
            //  - disregard for qxy + qzw case, since we never change z
            // and https://truesculpt.googlecode.com/hg-history/38000e9dfece971460473d5788c235fbbe82f31b/Doc/rotation_matrix_to_euler.pdf
            //  - disregard for two-fold solution of y, works up to now

            var q = rot.getQuaternion();

            var qxx = q[0]*q[0];
            var qxz = q[0]*q[2];
            var qxw = q[0]*q[3];
            var qyy = q[1]*q[1];
            var qyz = q[1]*q[2];
            var qyw = q[1]*q[3];
            var qzz = q[2]*q[2];

            var y = Math.atan2(2*(qyw + qxz), 1 - 2*(qyy + qzz));
            var x = Math.atan2(2*(qxw + qyz), 1 - 2*(qxx + qzz));
            if(Math.cos(y) < -0.01)
                x = -x;

            return {x: x, y: y};
        }
    });
}());
<commit_msg>FlyControllerBehavior: added usage of new rotationToEulerXY() utility<commit_after>(function() {

    "use strict";

    /** This behavior provides "fly" mode camera control on the given
     *  camera transformable.
     *  This behavior does have no notion of the interaction device. All it needs
     *  are deltaX and deltaY values, from which it computes the camera pose.
     *
     *  Usage:
     *  o instantiate this class
     *  o call rotate(), move{Forward,Backward}(), step{Left,Right}()
     *
     *  @constructor
     */
    XMOT.FlyControllerBehavior = new XMOT.Class({

        /**
         *  @this {XMOT.FlyControllerBehavior}
         *  @param {Element|Transformable} targetViewGroup
         *  @param {Object} options
         *
         *  options:
         *  o rotateSpeed
         *  o moveSpeed
         */
        initialize: function(targetViewGroup, options) {

			this.target = XMOT.util.getOrCreateTransformable(targetViewGroup);

            var options = options || {};

            this._rotateSpeed = options.rotateSpeed || 1;
            this._moveSpeed = options.moveSpeed || 1;

            this._initialRotation = new XML3DRotation();
            this._yAxisAngle = 0;
            this._xAxisAngle = 0;
            this._maxXAxisAngle = Math.PI/2 - 0.2;

            this._setInitialRotation(this.target.getOrientation());
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         */
        rotate: function(deltaX, deltaY) {

            var deltaXAxis = -this._rotateSpeed * deltaX * 2.0 * Math.PI;
            var deltaYAxis = -this._rotateSpeed * deltaY * 2.0 * Math.PI;

            this._xAxisAngle += deltaXAxis;
            this._yAxisAngle += deltaYAxis;

            if(this._xAxisAngle >= this._maxXAxisAngle)
                this._xAxisAngle = this._maxXAxisAngle;
            if(this._xAxisAngle <= -this._maxXAxisAngle)
                this._xAxisAngle = -this._maxXAxisAngle;

            var mx = new window.XML3DRotation(new window.XML3DVec3(1, 0, 0), this._xAxisAngle);
            var my = new window.XML3DRotation(new window.XML3DVec3(0, 1, 0), this._yAxisAngle);
            var rot = my.multiply(mx);

            var newRot = rot.multiply(this._initialRotation);

            this.target.setOrientation(newRot);
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         */
        moveForward: function() {
            this._moveInCamDirection();
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         */
        moveBackward: function() {
            this._moveInCamDirection(true);
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         */
        stepRight: function() {
            this._stepRight();
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         */
        stepLeft: function() {
            this._stepRight(true);
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         */
        lookAt: function(point) {

            var initCamDirection = new XML3DVec3(0, 0, -1);

            // calculate new direction
            var position = this.getPosition();
            var direction = point.subtract(position);
            direction = direction.normalize();

            // create rotation from angle b/w initial and new direction
            var dirRot = new XML3DRotation();
            dirRot.setRotation(initCamDirection, direction);

            this._setInitialRotation(dirRot);
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         */
        setPosition: function(position) {
            this.target.setPosition(position);
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         */
        setOrientation: function(orientation) {
            this._setInitialRotation(orientation);
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         */
        getPosition: function() {
            return this.target.getPosition();
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         */
        getOrientation: function() {
            return this.target.getOrientation();
        },

        getMoveSpeed: function() {
            return this._moveSpeed;
        },

        setMoveSpeed: function(speed) {
            this._moveSpeed = speed;
        },

        getRotationSpeed: function() {
            return this._rotateSpeed;
        },

        setRotationSpeed: function(speed) {
            this._rotateSpeed = speed;
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         *  @private
         */
        _moveInCamDirection: function(doInvertDirection) {

            var moveDirection = this._getLookDirection();

            if(doInvertDirection) {
                moveDirection = moveDirection.scale(-1);
            }

            this._translateCamera(moveDirection);
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         *  @private
         */
        _stepRight: function(doInvertDirection) {

            var lookDirection = this._getLookDirection();

            var stepDirection = lookDirection.cross(new XML3DVec3(0,1,0));

            if(doInvertDirection) {
                stepDirection = stepDirection.scale(-1);
            }

            this._translateCamera(stepDirection);
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         *  @private
         */
        _getLookDirection: function() {

            var curRot = this.target.getOrientation();

            var defaultDirection = new window.XML3DVec3(0, 0, -1);
            var lookDirection = curRot.rotateVec3(defaultDirection).normalize();

            return lookDirection;
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         *  @private
         */
        _translateCamera: function(direction) {

            var transl = direction.scale(this._moveSpeed);

            var newPos = this.target.getPosition().add(transl);
            this.target.setPosition(newPos);
        },

        /**
         *  @this {XMOT.FlyControllerBehavior}
         *  @private
         */
        _setInitialRotation: function(rot) {

            var euler = XMOT.math.rotationToEulerXY(rot);

            this._xAxisAngle = euler.x;
            this._yAxisAngle = euler.y;
            this._initialRotation.set(new XML3DRotation());

            // align the target's orientation to compensate for precision errors
            this.rotate(0, 0);
        }
    });
}());
<|endoftext|>


<commit_before>/*global $, app, me, client*/
"use strict";

var _ = require('underscore');
var StayDown = require('staydown');
var BasePage = require('./base');
var templates = require('../templates');
var MUCRosterItem = require('../views/mucRosterItem');
var Message = require('../views/mucMessage');
var MessageModel = require('../models/message');
var embedIt = require('../helpers/embedIt');
var htmlify = require('../helpers/htmlify');
var tempSubject = '';

module.exports = BasePage.extend({
    template: templates.pages.groupchat,
    initialize: function (spec) {
        this.editMode = false;

        this.listenTo(this, 'pageloaded', this.handlePageLoaded);
        this.listenTo(this, 'pageunloaded', this.handlePageUnloaded);

        this.listenTo(this.model.messages, 'change', this.refreshModel);
        this.listenTo(this.model.messages, 'reset', this.renderMessages);
        this.listenTo(this.model, 'refresh', this.renderMessages);

        app.state.bind('change:connected', this.connectionChange, this);

        this.render();
    },
    events: {
        'keydown textarea': 'handleKeyDown',
        'keyup textarea': 'handleKeyUp',
        'click .status': 'clickStatusChange',
        'blur .status': 'blurStatusChange',
        'keydown .status': 'keyDownStatusChange',
        'click #members_toggle': 'clickMembersToggle'
    },
    classBindings: {
        joined: '.controls'
    },
    textBindings: {
        displayName: 'header .name',
        subject: 'header .status',
        membersCount: '#members_toggle_count'
    },
    party: function() {
        return this.displayName;
    },
    show: function (animation) {
        BasePage.prototype.show.apply(this, [animation]);
        client.sendMessage({
            type: 'groupchat',
            to: this.model.jid,
            chatState: 'active'
        });

        this.firstChanged = true;
        var self = this;
        $('.messages').scroll(function() {
            if (self.firstChanged && $(".messages li:first-child").offset().top > 0) {
                self.firstChanged = false;
                self.model.fetchHistory();
            }
        });

        this.$chatInput.focus();
    },
    hide: function () {
        BasePage.prototype.hide.apply(this);
        client.sendMessage({
            type: 'groupchat',
            to: this.model.jid,
            chatState: 'inactive'
        });
    },
    render: function () {
        if (this.rendered) return this;
        this.rendered = true;

        this.renderAndBind();
        this.$chatInput = this.$('.chatBox textarea');
        this.$chatInput.val(app.composing[this.model.jid] || '');
        this.$chatBox = this.$('.chatBox');
        this.$messageList = this.$('.messages');
        this.$autoComplete = this.$('.autoComplete');

        this.staydown = new StayDown(this.$messageList[0], 500);

        this.renderMessages();

        this.renderCollection(this.model.resources, MUCRosterItem, this.$('.groupRoster'));

        this.listenTo(this, 'rosterItemClicked', this.rosterItemSelected);
        this.listenTo(this.model.messages, 'add', this.handleChatAdded);
        this.listenTo(this.model.resources, 'add', this.handleResourceAdded);

        $(window).on('resize', _.bind(this.resizeInput, this));

        this.registerBindings();

        return this;
    },
    renderMessages: function () {
        var self = this;

        this.$messageList.empty();
        delete this.firstModel;
        delete this.firstDate;
        delete this.lastModel;
        delete this.lastDate;

        this.model.messages.each(function (model, i) {
            self.appendModel(model);
        });
        this.staydown.checkdown();
    },
    handleChatAdded: function (model) {
        this.appendModel(model, true);
    },
    handleResourceAdded: function (model) {
        var xmppContact = me.getContact(model.id.split('/')[1]);
        if (xmppContact) {
            xmppContact.bind('change:avatar', this.handleAvatarChanged, this);
        }
    },
    handleAvatarChanged: function(contact, uri) {
        if (!me.isMe(contact.jid)) {
            $('.' + contact.jid.substr(0, contact.jid.indexOf('@')) + ' .messageAvatar img').attr('src', uri);
        }
    },
    handlePageLoaded: function () {
        this.staydown.checkdown();
        this.resizeInput();
    },
    handleKeyDown: function (e) {
        if ((e.which === 13 || e.which === 9) && !e.shiftKey) { // Enter or Tab
            if (this.$autoComplete.css('display') != 'none') {
                var nickname = this.$autoComplete.find(">:nth-child(" + this.autoCompletePos + ")>:first-child").text();
                this.rosterItemSelected(nickname);
            } else {
                app.composing[this.model.jid] = '';
                this.sendChat();
            }
            e.preventDefault();
            return false;
        } else if (e.which === 38) { // Up arrow

            if (this.$autoComplete.css('display') != 'none') {
                var count = this.$autoComplete.find(">li").length;
                var oldPos = this.autoCompletePos;
                this.autoCompletePos = (oldPos - 1) < 1 ? count : oldPos - 1;

                this.$autoComplete.find(">:nth-child(" + oldPos + ")").removeClass('selected');
                this.$autoComplete.find(">:nth-child(" + this.autoCompletePos + ")").addClass('selected');

            }
            else if (this.$chatInput.val() === '' && this.model.lastSentMessage) {
                this.editMode = true;
                this.$chatInput.addClass('editing');
                this.$chatInput.val(this.model.lastSentMessage.body);
            }
            e.preventDefault();
            return false;
        } else if (e.which === 40) { // Down arrow

            if (this.$autoComplete.css('display') != 'none') {
                var count = this.$autoComplete.find(">li").length;
                var oldPos = this.autoCompletePos;
                this.autoCompletePos = (oldPos + 1) > count ? 1 : oldPos + 1;

                this.$autoComplete.find(">:nth-child(" + oldPos + ")").removeClass('selected');
                this.$autoComplete.find(">:nth-child(" + this.autoCompletePos + ")").addClass('selected');
            }
            else if (this.editMode) {
                this.editMode = false;
                this.$chatInput.removeClass('editing');
            }
            e.preventDefault();
            return false;
        } else if (!e.ctrlKey && !e.metaKey) {
            if (!this.typing || this.paused) {
                this.typing = true;
                this.paused = false;
                client.sendMessage({
                    type: 'groupchat',
                    to: this.model.jid,
                    chatState: 'composing'
                });
            }
        }
    },
    handleKeyUp: function (e) {
        this.resizeInput();
        app.composing[this.model.jid] = this.$chatInput.val();
        if (this.typing && this.$chatInput.val().length === 0) {
            this.typing = false;
            this.paused = false;
            client.sendMessage({
                type: 'groupchat',
                to: this.model.jid,
                chatState: 'active'
            });
        } else if (this.typing) {
            this.pausedTyping();
        }

        if (([38, 40, 13]).indexOf(e.which) === -1) {
            var lastWord = this.$chatInput.val().split(' ').pop();
            if (lastWord.charAt(0) === '@') {
              var models = this.model.resources.search(lastWord.substr(1) || '', true, true);
              if (models.length) {
                this.renderCollection(models, MUCRosterItem, this.$autoComplete);
                this.autoCompletePos = 1;
                this.$autoComplete.find(">:nth-child(" + this.autoCompletePos + ")").addClass('selected');
                this.$autoComplete.show();
              }
              else
                this.$autoComplete.hide();
            }

            if (this.$autoComplete.css('display') != 'none') {
                if( lastWord ===  '') {
                    this.$autoComplete.hide();
                    return;
                }
            }
        }
    },
    rosterItemSelected: function (nickName) {
        if (nickName == me.nick)
            nickName = 'me';
        var val = this.$chatInput.val();
        var splited = val.split(' ');
        var length = splited.length-1;
        var lastWord = splited.pop();
        if (('@' + nickName).indexOf(lastWord) > -1)
            splited[length] = '@' + nickName + ' ';
        else
            splited.push('@' + nickName + ' ');
        this.$chatInput.val(splited.join(' '));
        this.$autoComplete.hide();
        this.$chatInput.focus();
    },
    resizeInput: _.throttle(function () {
        var height;
        var scrollHeight;
        var heightDiff;
        var newHeight;
        var newMargin;
        var marginDelta;
        var maxHeight = parseInt(this.$chatInput.css('max-height'), 10);

        this.$chatInput.removeAttr('style');
        height = this.$chatInput.outerHeight(),
        scrollHeight = this.$chatInput.get(0).scrollHeight,
        newHeight = Math.max(height, scrollHeight);
        heightDiff = height - this.$chatInput.innerHeight();

        if (newHeight > maxHeight) newHeight = maxHeight;
        if (newHeight > height) {
            this.$chatInput.css('height', newHeight+heightDiff);
            this.$chatInput.scrollTop(this.$chatInput[0].scrollHeight - this.$chatInput.height());
            newMargin = newHeight - height + heightDiff;
            marginDelta = newMargin - parseInt(this.$messageList.css('marginBottom'), 10);
            if (!!marginDelta) {
                this.$messageList.css('marginBottom', newMargin);
            }
        } else {
            this.$messageList.css('marginBottom', 0);
        }
    }, 300),
    pausedTyping: _.debounce(function () {
        if (this.typing && !this.paused) {
            this.paused = true;
            client.sendMessage({
                type: 'groupchat',
                to: this.model.jid,
                chatState: 'paused'
            });
        }
    }, 3000),
    sendChat: function () {
        var message;
        var val = this.$chatInput.val();

        if (val) {

            if(val[0] == '/') {

                var commands = [
                'clear',
                'topic',
                'password',
                'invite'
                ];

                var cmdfunctions = {};

                var cmdclear = function() {
                    this.$messageList.clear();
                };

                var cmdtopic = function(data) {
                    var subject = $('section.conversation').find('span.status');
                    client.setSubject(this.model.jid, data);
                    subject.text(data);
                };

                var match = val.match(/^\/([^\s]+)(?:\s+(.*))?$/m);
                var command = match[1];
                var data = match[2];

                if ($.inArray(command, commands) != -1) {
                    switch(command)
                    {
                        case 'clear':
                            cmdclear();
                            break;
                        case 'topic':
                            cmdtopic(data);
                            break;
                        default:
                            break;
                    }
                } else {
                    // TODO: Better way to notify the user of the invalid command
                    alert("Invalid command: " + command);
                }

                this.editMode = false;
                this.typing = false;
                this.paused = false;
                this.$chatInput.removeClass('editing');
                this.$chatInput.val('');
                return;
            }

            this.staydown.intend_down = true;

            var links = _.map(htmlify.collectLinks(val), function (link) {
                return {url: link};
            });

            message = {
                to: this.model.jid,
                type: 'groupchat',
                body: val,
                chatState: 'active',
                oobURIs: links
            };
            if (this.editMode) {
                message.replace = this.model.lastSentMessage.mid || this.model.lastSentMessage.cid;
            }

            var id = client.sendMessage(message);
            message.mid = id;
            message.from = client.JID(this.model.jid.bare + '/' + this.model.nick);

            if (this.editMode) {
                this.model.lastSentMessage.correct(message);
            } else {
                var msgModel = new MessageModel(message);
                this.model.addMessage(msgModel, false);
                this.model.lastSentMessage = msgModel;
            }
        }
        this.editMode = false;
        this.typing = false;
        this.paused = false;
        this.$chatInput.removeClass('editing');
        this.$chatInput.val('');
    },
    clickStatusChange: function (e) {
        tempSubject = e.target.textContent;
    },
    blurStatusChange: function (e) {
        var subject = e.target.textContent;
        if (subject == '')
            subject = true;
        client.setSubject(this.model.jid, subject);
        e.target.textContent = tempSubject;
    },
    keyDownStatusChange: function (e) {
        if (e.which === 13 && !e.shiftKey) {
            e.target.blur();
            return false;
        }
    },
    clickMembersToggle: function (e) {
        var roster = $('.groupRoster');
        if (roster.css('visibility') == 'hidden')
            roster.css('visibility', 'visible');
        else
            roster.css('visibility', 'hidden');
    },
    appendModel: function (model, preload) {
        var newEl, first, last;
        var msgDate = Date.create(model.timestamp);
        var messageDay = msgDate.format('{month} {ord}, {yyyy}');

        if (this.firstModel === undefined || msgDate > Date.create(this.firstModel.timestamp)) {
            if (this.firstModel === undefined) {
                this.firstModel = model;
                this.firstDate = messageDay;
            }

            if (messageDay !== this.lastDate) {
                var dayDivider = $(templates.includes.dayDivider({day_name: messageDay}));
                this.staydown.append(dayDivider[0]);
                this.lastDate = messageDay;
            }

            var isGrouped = model.shouldGroupWith(this.lastModel);
            if (isGrouped) {
                newEl = $(model.partialTemplateHtml);
                last = this.$messageList.find('li').last();
                last.find('.messageWrapper').append(newEl);
                last.addClass('chatGroup');
                this.staydown.checkdown();
            } else {
                newEl = $(model.templateHtml);
                newEl.addClass(model.sender.getNickname(model.from.full));
                this.staydown.append(newEl[0]);
                this.lastModel = model;
            }

            if(this.$messageList.length > 300) {
                var elementToRemove = this.$messageList.find('li').first();
                elementToRemove.remove();
            }

            if (!model.pending) embedIt(newEl);
        }
        else {
            var scrollDown = this.$messageList.prop('scrollHeight') - this.$messageList.scrollTop();
            var firstEl = this.$messageList.find('li').first();

            if (messageDay !== this.firstDate) {
                var dayDivider = $(templates.includes.dayDivider({day_name: messageDay}));
                firstEl.before(dayDivider[0]);
                var firstEl = this.$messageList.find('li').first();
                this.firstDate = messageDay;
            }

            var isGrouped = model.shouldGroupWith(this.firstModel);
            if (isGrouped) {
                newEl = $(model.partialTemplateHtml);
                first = this.$messageList.find('li').first().next();
                first.find('.messageWrapper div:first').after(newEl);
                first.addClass('chatGroup');
            } else {
                newEl = $(model.templateHtml);
                newEl.addClass(model.sender.getNickname(model.from.full));
                firstEl.after(newEl[0]);
                this.firstModel = model;
            }

            if(this.$messageList.length > 300) {
                var elementToRemove = this.$messageList.find('li').first();
                elementToRemove.remove();
            }

            if (!model.pending) embedIt(newEl);

            this.$messageList.scrollTop(this.$messageList.prop('scrollHeight') - scrollDown);
            this.firstChanged = true;
        }
    },
    refreshModel: function (model) {
        var existing = this.$('#chat' + model.cid);
        existing.replaceWith(model.bareMessageTemplate(existing.prev().hasClass('message_header')));
    },
    connectionChange: function () {
        if (app.state.connected) {
            this.$chatInput.attr("disabled", false);
        } else {
            this.$chatInput.attr("disabled", "disabled");
        }
    }
});
<commit_msg>Update groupchat.js<commit_after>/*global $, app, me, client*/
"use strict";

var _ = require('underscore');
var StayDown = require('staydown');
var BasePage = require('./base');
var templates = require('../templates');
var MUCRosterItem = require('../views/mucRosterItem');
var Message = require('../views/mucMessage');
var MessageModel = require('../models/message');
var embedIt = require('../helpers/embedIt');
var htmlify = require('../helpers/htmlify');
var tempSubject = '';

module.exports = BasePage.extend({
    template: templates.pages.groupchat,
    initialize: function (spec) {
        this.editMode = false;

        this.listenTo(this, 'pageloaded', this.handlePageLoaded);
        this.listenTo(this, 'pageunloaded', this.handlePageUnloaded);

        this.listenTo(this.model.messages, 'change', this.refreshModel);
        this.listenTo(this.model.messages, 'reset', this.renderMessages);
        this.listenTo(this.model, 'refresh', this.renderMessages);

        app.state.bind('change:connected', this.connectionChange, this);

        this.render();
    },
    events: {
        'keydown textarea': 'handleKeyDown',
        'keyup textarea': 'handleKeyUp',
        'click .status': 'clickStatusChange',
        'blur .status': 'blurStatusChange',
        'keydown .status': 'keyDownStatusChange',
        'click #members_toggle': 'clickMembersToggle'
    },
    classBindings: {
        joined: '.controls'
    },
    textBindings: {
        displayName: 'header .name',
        subject: 'header .status',
        membersCount: '#members_toggle_count'
    },
    party: function() {
        return this.displayName;
    },
    show: function (animation) {
        BasePage.prototype.show.apply(this, [animation]);
        client.sendMessage({
            type: 'groupchat',
            to: this.model.jid,
            chatState: 'active'
        });

        this.firstChanged = true;
        var self = this;
        $('.messages').scroll(function() {
            if (self.firstChanged && $(".messages li:first-child").offset().top > 0) {
                self.firstChanged = false;
                self.model.fetchHistory();
            }
        });

        this.$chatInput.focus();
    },
    hide: function () {
        BasePage.prototype.hide.apply(this);
        client.sendMessage({
            type: 'groupchat',
            to: this.model.jid,
            chatState: 'inactive'
        });
    },
    render: function () {
        if (this.rendered) return this;
        this.rendered = true;

        this.renderAndBind();
        this.$chatInput = this.$('.chatBox textarea');
        this.$chatInput.val(app.composing[this.model.jid] || '');
        this.$chatBox = this.$('.chatBox');
        this.$messageList = this.$('.messages');
        this.$autoComplete = this.$('.autoComplete');

        this.staydown = new StayDown(this.$messageList[0], 500);

        this.renderMessages();

        this.renderCollection(this.model.resources, MUCRosterItem, this.$('.groupRoster'));

        this.listenTo(this, 'rosterItemClicked', this.rosterItemSelected);
        this.listenTo(this.model.messages, 'add', this.handleChatAdded);
        this.listenTo(this.model.resources, 'add', this.handleResourceAdded);

        $(window).on('resize', _.bind(this.resizeInput, this));

        this.registerBindings();

        return this;
    },
    renderMessages: function () {
        var self = this;

        this.$messageList.empty();
        delete this.firstModel;
        delete this.firstDate;
        delete this.lastModel;
        delete this.lastDate;

        this.model.messages.each(function (model, i) {
            self.appendModel(model);
        });
        this.staydown.checkdown();
    },
    handleChatAdded: function (model) {
        this.appendModel(model, true);
    },
    handleResourceAdded: function (model) {
        var xmppContact = me.getContact(model.id.split('/')[1]);
        if (xmppContact) {
            xmppContact.bind('change:avatar', this.handleAvatarChanged, this);
        }
    },
    handleAvatarChanged: function(contact, uri) {
        if (!me.isMe(contact.jid)) {
            $('.' + contact.jid.substr(0, contact.jid.indexOf('@')) + ' .messageAvatar img').attr('src', uri);
        }
    },
    handlePageLoaded: function () {
        this.staydown.checkdown();
        this.resizeInput();
    },
    handleKeyDown: function (e) {
        if ((e.which === 13 || e.which === 9) && !e.shiftKey) { // Enter or Tab
            if (this.$autoComplete.css('display') != 'none') {
                var nickname = this.$autoComplete.find(">:nth-child(" + this.autoCompletePos + ")>:first-child").text();
                this.rosterItemSelected(nickname);
            } else {
                app.composing[this.model.jid] = '';
                this.sendChat();
            }
            e.preventDefault();
            return false;
        } else if (e.which === 38) { // Up arrow

            if (this.$autoComplete.css('display') != 'none') {
                var count = this.$autoComplete.find(">li").length;
                var oldPos = this.autoCompletePos;
                this.autoCompletePos = (oldPos - 1) < 1 ? count : oldPos - 1;

                this.$autoComplete.find(">:nth-child(" + oldPos + ")").removeClass('selected');
                this.$autoComplete.find(">:nth-child(" + this.autoCompletePos + ")").addClass('selected');

            }
            else if (this.$chatInput.val() === '' && this.model.lastSentMessage) {
                this.editMode = true;
                this.$chatInput.addClass('editing');
                this.$chatInput.val(this.model.lastSentMessage.body);
            }
            e.preventDefault();
            return false;
        } else if (e.which === 40) { // Down arrow

            if (this.$autoComplete.css('display') != 'none') {
                var count = this.$autoComplete.find(">li").length;
                var oldPos = this.autoCompletePos;
                this.autoCompletePos = (oldPos + 1) > count ? 1 : oldPos + 1;

                this.$autoComplete.find(">:nth-child(" + oldPos + ")").removeClass('selected');
                this.$autoComplete.find(">:nth-child(" + this.autoCompletePos + ")").addClass('selected');
            }
            else if (this.editMode) {
                this.editMode = false;
                this.$chatInput.removeClass('editing');
            }
            e.preventDefault();
            return false;
        } else if (!e.ctrlKey && !e.metaKey) {
            if (!this.typing || this.paused) {
                this.typing = true;
                this.paused = false;
                client.sendMessage({
                    type: 'groupchat',
                    to: this.model.jid,
                    chatState: 'composing'
                });
            }
        }
    },
    handleKeyUp: function (e) {
        this.resizeInput();
        app.composing[this.model.jid] = this.$chatInput.val();
        if (this.typing && this.$chatInput.val().length === 0) {
            this.typing = false;
            this.paused = false;
            client.sendMessage({
                type: 'groupchat',
                to: this.model.jid,
                chatState: 'active'
            });
        } else if (this.typing) {
            this.pausedTyping();
        }

        if (([38, 40, 13]).indexOf(e.which) === -1) {
            var lastWord = this.$chatInput.val().split(' ').pop();
            if (lastWord.charAt(0) === '@') {
              var models = this.model.resources.search(lastWord.substr(1) || '', true, true);
              if (models.length) {
                this.renderCollection(models, MUCRosterItem, this.$autoComplete);
                this.autoCompletePos = 1;
                this.$autoComplete.find(">:nth-child(" + this.autoCompletePos + ")").addClass('selected');
                this.$autoComplete.show();
              }
              else
                this.$autoComplete.hide();
            }

            if (this.$autoComplete.css('display') != 'none') {
                if( lastWord ===  '') {
                    this.$autoComplete.hide();
                    return;
                }
            }
        }
    },
    rosterItemSelected: function (nickName) {
        if (nickName == me.nick)
            nickName = 'me';
        var val = this.$chatInput.val();
        var splited = val.split(' ');
        var length = splited.length-1;
        var lastWord = splited.pop();
        if (('@' + nickName).indexOf(lastWord) > -1)
            splited[length] = '@' + nickName + ' ';
        else
            splited.push('@' + nickName + ' ');
        this.$chatInput.val(splited.join(' '));
        this.$autoComplete.hide();
        this.$chatInput.focus();
    },
    resizeInput: _.throttle(function () {
        var height;
        var scrollHeight;
        var heightDiff;
        var newHeight;
        var newMargin;
        var marginDelta;
        var maxHeight = parseInt(this.$chatInput.css('max-height'), 10);

        this.$chatInput.removeAttr('style');
        height = this.$chatInput.outerHeight(),
        scrollHeight = this.$chatInput.get(0).scrollHeight,
        newHeight = Math.max(height, scrollHeight);
        heightDiff = height - this.$chatInput.innerHeight();

        if (newHeight > maxHeight) newHeight = maxHeight;
        if (newHeight > height) {
            this.$chatInput.css('height', newHeight+heightDiff);
            this.$chatInput.scrollTop(this.$chatInput[0].scrollHeight - this.$chatInput.height());
            newMargin = newHeight - height + heightDiff;
            marginDelta = newMargin - parseInt(this.$messageList.css('marginBottom'), 10);
            if (!!marginDelta) {
                this.$messageList.css('marginBottom', newMargin);
            }
        } else {
            this.$messageList.css('marginBottom', 0);
        }
    }, 300),
    pausedTyping: _.debounce(function () {
        if (this.typing && !this.paused) {
            this.paused = true;
            client.sendMessage({
                type: 'groupchat',
                to: this.model.jid,
                chatState: 'paused'
            });
        }
    }, 3000),
    sendChat: function () {
        var message;
        var val = this.$chatInput.val();

        if (val) {

            if(val[0] == '/') {

                var commands = [
                'clear',
                'topic',
                'password',
                'invite'
                ];

                var cmdclear = function() {
                    this.$messageList.find('li').remove();
                };

                var cmdtopic = function(data) {
                    var subject = $('section.conversation').find('span.status');
                    client.setSubject(model.jid, data);
                    subject.text(data);
                };

                var match = val.match(/^\/([^\s]+)(?:\s+(.*))?$/m);
                var command = match[1];
                var data = match[2];

                if ($.inArray(command, commands) != -1) {
                    switch(command)
                    {
                        case 'clear':
                            cmdclear();
                            break;
                        case 'topic':
                            cmdtopic(data);
                            break;
                        default:
                            break;
                    }
                } else {
                    // TODO: Better way to notify the user of the invalid command
                    alert("Invalid command: " + command);
                }

                this.editMode = false;
                this.typing = false;
                this.paused = false;
                this.$chatInput.removeClass('editing');
                this.$chatInput.val('');
                return;
            }

            this.staydown.intend_down = true;

            var links = _.map(htmlify.collectLinks(val), function (link) {
                return {url: link};
            });

            message = {
                to: this.model.jid,
                type: 'groupchat',
                body: val,
                chatState: 'active',
                oobURIs: links
            };
            if (this.editMode) {
                message.replace = this.model.lastSentMessage.mid || this.model.lastSentMessage.cid;
            }

            var id = client.sendMessage(message);
            message.mid = id;
            message.from = client.JID(this.model.jid.bare + '/' + this.model.nick);

            if (this.editMode) {
                this.model.lastSentMessage.correct(message);
            } else {
                var msgModel = new MessageModel(message);
                this.model.addMessage(msgModel, false);
                this.model.lastSentMessage = msgModel;
            }
        }
        this.editMode = false;
        this.typing = false;
        this.paused = false;
        this.$chatInput.removeClass('editing');
        this.$chatInput.val('');
    },
    clickStatusChange: function (e) {
        tempSubject = e.target.textContent;
    },
    blurStatusChange: function (e) {
        var subject = e.target.textContent;
        if (subject == '')
            subject = true;
        client.setSubject(this.model.jid, subject);
        e.target.textContent = tempSubject;
    },
    keyDownStatusChange: function (e) {
        if (e.which === 13 && !e.shiftKey) {
            e.target.blur();
            return false;
        }
    },
    clickMembersToggle: function (e) {
        var roster = $('.groupRoster');
        if (roster.css('visibility') == 'hidden')
            roster.css('visibility', 'visible');
        else
            roster.css('visibility', 'hidden');
    },
    appendModel: function (model, preload) {
        var newEl, first, last;
        var msgDate = Date.create(model.timestamp);
        var messageDay = msgDate.format('{month} {ord}, {yyyy}');

        if (this.firstModel === undefined || msgDate > Date.create(this.firstModel.timestamp)) {
            if (this.firstModel === undefined) {
                this.firstModel = model;
                this.firstDate = messageDay;
            }

            if (messageDay !== this.lastDate) {
                var dayDivider = $(templates.includes.dayDivider({day_name: messageDay}));
                this.staydown.append(dayDivider[0]);
                this.lastDate = messageDay;
            }

            var isGrouped = model.shouldGroupWith(this.lastModel);
            if (isGrouped) {
                newEl = $(model.partialTemplateHtml);
                last = this.$messageList.find('li').last();
                last.find('.messageWrapper').append(newEl);
                last.addClass('chatGroup');
                this.staydown.checkdown();
            } else {
                newEl = $(model.templateHtml);
                newEl.addClass(model.sender.getNickname(model.from.full));
                this.staydown.append(newEl[0]);
                this.lastModel = model;
            }

            if(this.$messageList.length > 300) {
                var elementToRemove = this.$messageList.find('li').first();
                elementToRemove.remove();
            }

            if (!model.pending) embedIt(newEl);
        }
        else {
            var scrollDown = this.$messageList.prop('scrollHeight') - this.$messageList.scrollTop();
            var firstEl = this.$messageList.find('li').first();

            if (messageDay !== this.firstDate) {
                var dayDivider = $(templates.includes.dayDivider({day_name: messageDay}));
                firstEl.before(dayDivider[0]);
                var firstEl = this.$messageList.find('li').first();
                this.firstDate = messageDay;
            }

            var isGrouped = model.shouldGroupWith(this.firstModel);
            if (isGrouped) {
                newEl = $(model.partialTemplateHtml);
                first = this.$messageList.find('li').first().next();
                first.find('.messageWrapper div:first').after(newEl);
                first.addClass('chatGroup');
            } else {
                newEl = $(model.templateHtml);
                newEl.addClass(model.sender.getNickname(model.from.full));
                firstEl.after(newEl[0]);
                this.firstModel = model;
            }

            if(this.$messageList.length > 300) {
                var elementToRemove = this.$messageList.find('li').first();
                elementToRemove.remove();
            }

            if (!model.pending) embedIt(newEl);

            this.$messageList.scrollTop(this.$messageList.prop('scrollHeight') - scrollDown);
            this.firstChanged = true;
        }
    },
    refreshModel: function (model) {
        var existing = this.$('#chat' + model.cid);
        existing.replaceWith(model.bareMessageTemplate(existing.prev().hasClass('message_header')));
    },
    connectionChange: function () {
        if (app.state.connected) {
            this.$chatInput.attr("disabled", false);
        } else {
            this.$chatInput.attr("disabled", "disabled");
        }
    }
});
<|endoftext|>


<commit_before>
barchart = {}

barchart.draw = function(data, at) {

	getv = function(d, metric) {
		var mi = metric.split("/")
		if(mi.length == 1) {
			return d[metric]
		} else {
			return (d[mi[1]] != 0) ? (d[mi[0]] / d[mi[1]]) : 0
		}
	}

	var CHEIGHT = 600;
	var BWIDTH = 5;
	var BGAP = 2;
	var LEFTSPACE = 40;

	data.sort(function(da, db) { return getv(db, at.sort) - getv(da, at.sort)} )

	d3.selectAll("svg").remove();
	var chart = d3.select("body").append("svg")
		.attr("class", "chart")
		.attr("width", (BWIDTH + BGAP) * data.length + LEFTSPACE)
		.attr("height", CHEIGHT + 5); /* to accomodate bottom label */

	var xscale = d3.scale.linear()
		.domain([0, data.length])
		.rangeRound([LEFTSPACE, (BWIDTH + BGAP) * data.length + LEFTSPACE])

	var yscale = d3.scale.linear()
		.domain([0, d3.max(data, function(d) { return getv(d, at.height) })])
		.rangeRound([CHEIGHT, 1]);

	var fscale = d3.scale.linear()
		.domain([0, d3.max(data, function(d) { return getv(d, at.shade) })])
		.range([100, 0]);

	var yaxis = d3.svg.axis()
		.scale(yscale)
		.orient("left")
		.ticks(10);

	chart.selectAll("line")
		.data(yscale.ticks(10))
		.enter().append("line")
		.attr("x1", function(td) { return xscale(0) })
		.attr("x2", function(td) { return xscale(data.length) })
		.attr("y1", yscale)
		.attr("y2", yscale)
		.style("stroke", "#ccc");

	chart.selectAll("rect")
		.data(data)
		.enter().append("rect")
		.attr("x", function(d, i) { return xscale(i) })
		.attr("y", function(d) { return yscale(getv(d, at.height)) })
		.attr("height", function(d) { return CHEIGHT -yscale(getv(d, at.height)) })
		.attr("width", BWIDTH)
		.attr("shape-rendering", "crispEdges")
		.style("fill", function(d) { return "hsl(200, 80%, " + fscale(getv(d, at.shade)) + "%)" })
		.call(tooltip());

	chart.append("g")
		.attr("class", "axis")
		.attr("transform", "translate(" + LEFTSPACE + ", 0)")
		.call(yaxis);
}

<commit_msg>Showing bars with zero height.<commit_after>
barchart = {}

barchart.draw = function(data, at) {

	getv = function(d, metric) {
		var mi = metric.split("/")
		if(mi.length == 1) {
			return d[metric]
		} else {
			return (d[mi[1]] != 0) ? (d[mi[0]] / d[mi[1]]) : 0
		}
	}

	var CHEIGHT = 600;
	var BWIDTH = 5;
	var BGAP = 2;
	var LEFTSPACE = 40;

	data.sort(function(da, db) { return getv(db, at.sort) - getv(da, at.sort)} )

	d3.selectAll("svg").remove();
	var chart = d3.select("body").append("svg")
		.attr("class", "chart")
		.attr("width", (BWIDTH + BGAP) * data.length + LEFTSPACE)
		.attr("height", CHEIGHT + 5); /* to accomodate bottom label */

	var xscale = d3.scale.linear()
		.domain([0, data.length])
		.rangeRound([LEFTSPACE, (BWIDTH + BGAP) * data.length + LEFTSPACE])

	var yscale = d3.scale.linear()
		.domain([0, d3.max(data, function(d) { return getv(d, at.height) })])
		.rangeRound([CHEIGHT-1, 1]);

	var fscale = d3.scale.linear()
		.domain([0, d3.max(data, function(d) { return getv(d, at.shade) })])
		.range([100, 0]);

	var yaxis = d3.svg.axis()
		.scale(yscale)
		.orient("left")
		.ticks(10);

	chart.selectAll("line")
		.data(yscale.ticks(10))
		.enter().append("line")
		.attr("x1", function(td) { return xscale(0) })
		.attr("x2", function(td) { return xscale(data.length) })
		.attr("y1", yscale)
		.attr("y2", yscale)
		.style("stroke", "#ccc");

	chart.selectAll("rect")
		.data(data)
		.enter().append("rect")
		.attr("x", function(d, i) { return xscale(i) })
		.attr("y", function(d) { return yscale(getv(d, at.height)) })
		.attr("height", function(d) { return CHEIGHT -yscale(getv(d, at.height)) })
		.attr("width", BWIDTH)
		.attr("shape-rendering", "crispEdges")
		.style("fill", function(d) { return "hsl(200, 80%, " + fscale(getv(d, at.shade)) + "%)" })
		.call(tooltip());

	chart.append("g")
		.attr("class", "axis")
		.attr("transform", "translate(" + LEFTSPACE + ", 0)")
		.call(yaxis);
}

<|endoftext|>


<commit_before>/* @flow */
/*
---
id: getting-started
title: Getting started with Flow
permalink: /docs/getting-started.html
prev: about-flow.html
next: five-simple-examples.html
---
*/

/*
  To demonstrate how to add Flow to a project, let's set up an example npm project.

  First, we'll make an npm package for our example project called "get_started":

  ```bash
  $> mkdir -p get_started
  $> cd get_started
  $> echo '{"name": "get_started", "scripts": {"flow": "flow; [[ $?=0||$?=2 ]]"}}' > package.json
  ```

  Next we'll add Flow to our project:

  ```bash
  $> touch .flowconfig
  $> npm install --save-dev flow-bin
  ```

  And now we can start writing some code!

  **index.js**
*/

// @flow

// $DocIssue
var str = 'hello world!';
console.log(str);

/*
  Note that we've added `// @flow` to the top of our file. This indicates to Flow
  that we want this file to be checked. **If we don't add this flag to the top of
  the file, Flow will assume that the file isn't ready to be checked yet and
  Flow will not attempt to type check the file.**

  Ok, now let's run Flow and see what it has to say about the code we just wrote:

  ```bash
  $> npm run-script flow

  > test@ flow /get_started
  > flow

  No errors!
  ```

  Woohoo! No errors! Ok, now let's add a trivial type error just to see what
  happens:

  **index.js**
*/

// @flow

// $ExpectError
var str: number = 'hello world!';
console.log(str);

/*
  ```bash
  $> npm run-script flow

  > test@ flow /get_started
  > flow 2> /dev/null

  index.js:3
    3: let str: number = 'hello world!';
                         ^^^^^^^^^^^^^^ string. This type is incompatible with
    3: let str: number = 'hello world!';
                ^^^^^^ number

  Found 1 error
  ```

  Cool -- Flow noticed that we're assigning a string to a number and gave an
  error.

  Before we fix this type error, let's add Babel to our project so we can
  try running our code:

  ```bash
  $> npm install -g babel-cli
  $> npm install --save-dev babel-plugin-transform-flow-strip-types
  $> echo '{"plugins": ["transform-flow-strip-types"]}' > .babelrc
  ```

  Now we can try running our code *in spite* of the type error and see what
  happens:

  ```bash
  $> babel-node index.js
  hello world!
  ```

  It works! As you can see Flow does not prevent you from running the code you
  have written even if there are type errors. It is considered best practice to
  never publish your project when it has type errors, but often at development
  time it's useful to try running code even before it fully typechecks (usually
  for debugging or ad-hoc testing). This is one of the benefits of gradual typing
  and Flow is designed to support this and stay out of your way as much as you
  need it to during development.

  Note that we ran our code using `babel-node` rather than just vanilla `node` to
  run index.js. [`babel-node`](https://babeljs.io/docs/usage/cli/#babel-node)
  comes with `babel-cli` and is just a thin wrapper around vanilla `node` that
  first intercepts and transpiles JS code before running it with `node`.

  Check out the [Running Flow Code](/docs/running.html) section for more details
  on how we recommend compiling and publishing Flow code to npm and into
  production.

### Next steps

  Now that we know how to set Flow up, let's take a quick look at
  [a few examples](/five-simple-examples.html) that are
  [included in the Flow repo](https://github.com/facebook/flow/tree/master/examples):

  1. [Hello Flow!](/docs/five-simple-examples.html#1-hello-flow)
  2. [Adding type annotations](/docs/five-simple-examples.html#2-adding-type-annotations)
  3. [Nullable types](/docs/five-simple-examples.html#3-nullable-types)
  4. [Arrays](/docs/five-simple-examples.html#4-arrays)
  5. [Dynamic code](/docs/five-simple-examples.html#5-dynamic-code)
*/
<commit_msg>Update getting-started.doc.js to fix five-simple-examples link<commit_after>/* @flow */
/*
---
id: getting-started
title: Getting started with Flow
permalink: /docs/getting-started.html
prev: about-flow.html
next: five-simple-examples.html
---
*/

/*
  To demonstrate how to add Flow to a project, let's set up an example npm project.

  First, we'll make an npm package for our example project called "get_started":

  ```bash
  $> mkdir -p get_started
  $> cd get_started
  $> echo '{"name": "get_started", "scripts": {"flow": "flow; [[ $?=0||$?=2 ]]"}}' > package.json
  ```

  Next we'll add Flow to our project:

  ```bash
  $> touch .flowconfig
  $> npm install --save-dev flow-bin
  ```

  And now we can start writing some code!

  **index.js**
*/

// @flow

// $DocIssue
var str = 'hello world!';
console.log(str);

/*
  Note that we've added `// @flow` to the top of our file. This indicates to Flow
  that we want this file to be checked. **If we don't add this flag to the top of
  the file, Flow will assume that the file isn't ready to be checked yet and
  Flow will not attempt to type check the file.**

  Ok, now let's run Flow and see what it has to say about the code we just wrote:

  ```bash
  $> npm run-script flow

  > test@ flow /get_started
  > flow

  No errors!
  ```

  Woohoo! No errors! Ok, now let's add a trivial type error just to see what
  happens:

  **index.js**
*/

// @flow

// $ExpectError
var str: number = 'hello world!';
console.log(str);

/*
  ```bash
  $> npm run-script flow

  > test@ flow /get_started
  > flow 2> /dev/null

  index.js:3
    3: let str: number = 'hello world!';
                         ^^^^^^^^^^^^^^ string. This type is incompatible with
    3: let str: number = 'hello world!';
                ^^^^^^ number

  Found 1 error
  ```

  Cool -- Flow noticed that we're assigning a string to a number and gave an
  error.

  Before we fix this type error, let's add Babel to our project so we can
  try running our code:

  ```bash
  $> npm install -g babel-cli
  $> npm install --save-dev babel-plugin-transform-flow-strip-types
  $> echo '{"plugins": ["transform-flow-strip-types"]}' > .babelrc
  ```

  Now we can try running our code *in spite* of the type error and see what
  happens:

  ```bash
  $> babel-node index.js
  hello world!
  ```

  It works! As you can see Flow does not prevent you from running the code you
  have written even if there are type errors. It is considered best practice to
  never publish your project when it has type errors, but often at development
  time it's useful to try running code even before it fully typechecks (usually
  for debugging or ad-hoc testing). This is one of the benefits of gradual typing
  and Flow is designed to support this and stay out of your way as much as you
  need it to during development.

  Note that we ran our code using `babel-node` rather than just vanilla `node` to
  run index.js. [`babel-node`](https://babeljs.io/docs/usage/cli/#babel-node)
  comes with `babel-cli` and is just a thin wrapper around vanilla `node` that
  first intercepts and transpiles JS code before running it with `node`.

  Check out the [Running Flow Code](/docs/running.html) section for more details
  on how we recommend compiling and publishing Flow code to npm and into
  production.

### Next steps

  Now that we know how to set Flow up, let's take a quick look at
  [a few examples](/docs/five-simple-examples.html) that are
  [included in the Flow repo](https://github.com/facebook/flow/tree/master/examples):

  1. [Hello Flow!](/docs/five-simple-examples.html#1-hello-flow)
  2. [Adding type annotations](/docs/five-simple-examples.html#2-adding-type-annotations)
  3. [Nullable types](/docs/five-simple-examples.html#3-nullable-types)
  4. [Arrays](/docs/five-simple-examples.html#4-arrays)
  5. [Dynamic code](/docs/five-simple-examples.html#5-dynamic-code)
*/
<|endoftext|>