<commit_before>"use strict";

// TODO : Events fireing or callback implementation
// TODO : Add support for sorting
// TODO : Tests

(function( $, window, document, undefined ){
    var Picker = function( elem, options ){
        this.elem = elem;
        this.$elem = $(elem);
        this.options = options;
        this.currentData = [];
    };

    Picker.prototype = {
        defaults: {
            multiple: undefined,
            containerClass: '',
            containerWidth: false,
            width: false,
            search: false,
            searchAutofocus: false,
            autofocusScrollOffset: 0,
            coloring: {},
            texts: {
                trigger : "Select value",
                noResult : "No results",
                search : "Search"
            }
        },

        config: {},

        init: function() {
            this.config = $.extend({}, this.defaults, this.options);

            if(this.config.multiple === undefined) {
                this.config.multiple = this.$elem.is("select[multiple='multiple']") || this.$elem.is("select[multiple]");
            }

            if(!this.$elem.is("select")){
                console.log("Picker - Element is not Selectbox");
                return;
            }

            if(this.config.width !== false
                && (Math.floor(this.config.width) != this.config.width || !$.isNumeric(this.config.width))){
                console.log("Picker - Width is not a integer.");
                return;
            }

            if(this.config.containerWidth !== false
                && (Math.floor(this.config.containerWidth) != this.config.containerWidth || !$.isNumeric(this.config.containerWidth))){
                console.log("Picker - Container width is not a integer.");
                return;
            }

            this._build();
            this.$elem.css('display', 'none');

            this._fillList();

            this.$container.find('.pc-trigger').click(function(){
                var list = this.$container.find('.pc-list');
                list.toggle();

                if(this.config.search && this.config.searchAutofocus){
                    if (list.is(':visible')) {
                        list.find('input').focus();
                        $('html, body').animate({
                            scrollTop: list.find('input').offset().top - this.config.autofocusScrollOffset
                        }, 800);
                    }
                }
            }.bind(this));

            $(document).mouseup(function (e)
                {
                    var pc_list = this.$container.find(".pc-list");
                    if (!pc_list.is(e.target) && pc_list.has(e.target).length === 0 && !this.$container.find(".pc-trigger").is(e.target))
                    {
                        pc_list.hide();

                        if(this.config.search){
                            this.$container.find(".pc-list input").val('');
                            this._updateList(this.currentData);
                        }
                    }

                }.bind(this));

            return this;
        },

        pc_selected: function(e){
            var $elem = $(e.target);
            var selectedId = $elem.data('id');
            this._selectElement(selectedId, $elem);

            this.$container.find(".pc-list").hide();

            if(this.config.search){
                this.$container.find(".pc-list input").val('');
                this._updateList(this.currentData);
            }
        },

        pc_remove: function(elem){
            var $elem = $(elem.target);
            var selectedId = $elem.parent().data('id');
            var order = $elem.parent().data('order');

            var li = $("
").html($elem.parent().text()).attr('data-id', selectedId).attr('data-order', order);
            li.click(this.pc_selected.bind(this));

            if(this.config.search) {
                this._insertIntoCurrentData(elem);
            }

            var currentList = this.$container.find('.pc-list li');
            if(!this.$container.find(".pc-trigger").is(':visible')) { // Empty list
                this.$container.find('.pc-list ul').html('').append(li);
                this.$container.find(".pc-trigger").show();
            }else if(currentList.size() == 1) { // Only one item in list
                if(order > currentList.data('order')){
                    li.insertAfter(currentList);
                }else{
                    li.insertBefore(currentList);
                }
            }else{
                currentList.each(function(i,e) {
                    e = $(e);
                    if(e.is(':first-child')){
                        if(order < e.data('order')){
                            li.insertBefore(e);
                            return false;
                        }else if(order > e.data('order')
                            && order < e.next().data('order')){
                            li.insertAfter(e);
                            return false;
                        }
                    }else if(e.is(':last-child')) {
                        if(order > e.data('order')){
                            li.insertAfter(e);
                            return false;
                        }
                    }else{
                        if(order > e.data('order')
                            && order < e.next().data('order')){
                            li.insertAfter(e);
                            return false;
                        }
                    }
                });
            }
            this.$elem.find(" option[value='" + selectedId + "']").removeAttr("selected");
            $elem.parent().remove();
        },

        pc_search: function(e){
            var searchedValue = $(e.target).val().toLowerCase();
            var filteredData = this._filterData(searchedValue);
            this._updateList(filteredData, searchedValue);
        },

        ////////////////////////////////////////////
        ////////////// Private methods//////////////
        ////////////////////////////////////////////

        _selectElement: function (id, $elem) {
            if($elem == undefined){
                $elem = this.$container.find('.pc-list li[data-id="' + id + '"]');

                if($elem.length == 0){
                    console.log('Picker - ID to select not found!');
                    return;
                }
            }

            if(this.config.multiple) {
                this.$container.prepend(this._createElement($elem));
                $elem.remove();

                if(this.config.search){
                    this.currentData = this.currentData.filter(function (value) {
                        return value.id != id;
                    });
                }

                if(this.$container.find(".pc-list li").size() == 0) {
                    this.$container.find(".pc-trigger").hide();
                }
            }else{
                this.$elem.find("option").removeAttr("selected");

                if (this.config.coloring[id]) {
                    this.$container.find(".pc-trigger").removeClass().addClass(this.config.coloring[selectedId] + " pc-trigger pc-element").contents().first().replaceWith($elem.text());
                } else {
                    this.$container.find(".pc-trigger").contents().first().replaceWith($elem.text());
                }
            }

            this.$elem.find("option[value='" + id + "']").attr("selected", "selected");

        },

        _insertIntoCurrentData: function (elem) {
            var $elem = $(elem.target);
            var selectedId = $elem.parent().data('id');
            var order = $elem.parent().data('order');

            if(this.currentData.length == 0){
                this.currentData = [{
                    'id': selectedId,
                    'text': $elem.parent().text(),
                    'order': order
                }];

                return;
            }

            var i;
            for (i = 0; i < this.currentData.length; i++) {
                if (i == 0) {
                    if(order < this.currentData[i].order || this.currentData.length == 1){
                        this.currentData.splice(0, 0, {
                            'id': selectedId,
                            'text': $elem.parent().text(),
                            'order': order
                        });
                        break;
                    }
                } else if (i == (this.currentData.length - 1)) {
                    if(order > this.currentData[i].order){
                        this.currentData.splice(i, 0, {
                            'id': selectedId,
                            'text': $elem.parent().text(),
                            'order': order
                        });
                        break;
                    }
                } else if (this.currentData[i - 1].order < order && order < this.currentData[i].order) {
                    this.currentData.splice(i, 0, {
                        'id': selectedId,
                        'text': $elem.parent().text(),
                        'order': order
                    });
                    break;
                }
            }
        },

        _createElement: function($elem){
            var tagClass = this.config.coloring[$elem.data('id')];
            var root = $("").addClass("pc-element " + (tagClass ? tagClass : "")).text($elem.text())
                .attr('data-id', $elem.data('id')).attr('data-order',$elem.data('order'));

            root.append($('').click(this.pc_remove.bind(this)));
            return root;
        },

        _build: function(){
            var triggerText;
            if(this.config.multiple){
                triggerText = this.config.texts.trigger;
            }else{
                triggerText = this.$elem.find('option').first().text();
            }

            this.$container = $("
" +
            "" +
            "" + triggerText + "" +
            "

" +
            "" +
            "
");

            if(this.config.containerWidth !== false){
                this.$container.width(this.config.containerWidth);
            }

            this.$container.insertAfter(this.$elem);

            if(this.config.search){
                this._buildSearch();
            }
        },

        _buildSearch : function () {
            var $searchField = $("<input type='search' placeholder='" + this.config.texts.search + "'>");
            $searchField.on('input', this.pc_search.bind(this));
            $searchField.on('keypress', function (e) {
                if(e.which == 13){
                    var searchedValue = $(e.target).val().toLowerCase();
                    var filteredData = this._filterData(searchedValue);

                    if(filteredData.length == 1){
                        this.$container.find('.pc-list li').first().click();
                        return false;
                    }
                }

                return true;
            }.bind(this));

            this.$container.find('.pc-list').prepend($searchField);
        },

        _fillList: function(){
            var listContainer = this.$container.find('.pc-list ul');
            var counter = 0;
            this.$elem.find('option:not([hidden])').each(function(index, elem){
                var li = $("
").html($(elem).text()).attr('data-id', $(elem).attr('value')).attr('data-order', counter);
                li.click(this.pc_selected.bind(this));
                listContainer.append(li);

                if(this.config.search){
                    this.currentData.push({
                        'id' : $(elem).attr('value'),
                        'text' : $(elem).text(),
                        'order' : counter
                    });
                }

                if($(elem).attr('selected') == 'selected'){
                    li.click();
                }

                counter++;
            }.bind(this));

            this.$container.find(".pc-trigger").show();
        },

        _filterData: function (searchedValue) {
            return this.currentData.filter(function (value) {
                return value.text.toLowerCase().indexOf(searchedValue) != -1;
            });
        },

        _updateList: function(filteredData, searchedValue){
            var listContainer = this.$container.find('.pc-list ul');
            if(filteredData.length == 0){
                listContainer.html('
' + this.config.texts.noResult + '
');
                return;
            }

            listContainer.html('');
            var i, liContent;
            for(i = 0; i < filteredData.length; i++){
                // Highlighting searched string
                if(searchedValue !== undefined){
                    var regex = new RegExp( '(' + searchedValue + ')', 'gi' );
                    liContent = filteredData[i].text.replace( regex, '$1' )
                }else{
                    liContent = filteredData[i].text;
                }

                var li = $("
").html(liContent).attr('data-id', filteredData[i].id).attr('data-order', filteredData[i].order);
                li.click(this.pc_selected.bind(this));
                listContainer.append(li);
            }
        },

        /////////////////////////////////
        ////////////// API //////////////
        /////////////////////////////////

        // API invocation
        api: function (args) {
            if(Picker.prototype['api_' + args[0]]){
                return this['api_' + args[0]](args.slice(1));
            }else{
                console.log('Picker - unknown command!');
            }
        },

        // API functions

        api_destroy : function () {
            this.$container.remove();
            this.$elem.show();
            this.$elem.removeData("plugin_picker");

            return this.$elem;
        },

        api_get : function () {
            return this.$elem.val();
        },

        api_set : function (args) {
            if(args.length != 1){
                console.log('Picker - unknown number of arguments');
                return;
            }

            this._selectElement(args[0]);

            return this.$elem;
        }
    };

    $.fn.picker = function(options) {
        var instance = $(this).data("plugin_picker");
        if (!instance) {
            $(this).data("plugin_picker", new Picker(this, options).init());
            return this;
        } else {
            return instance.api(Array.prototype.slice.call(arguments));
        }
    };

})( jQuery, window , document );<commit_msg>Adding support for selectors with multiple elements<commit_after>"use strict";

// TODO : Events fireing or callback implementation
// TODO : Add support for sorting
// TODO : Tests

(function( $, window, document, undefined ){
    var Picker = function( elem, options ){
        this.elem = elem;
        this.$elem = $(elem);
        this.options = options;
        this.currentData = [];
    };

    Picker.prototype = {
        defaults: {
            multiple: undefined,
            containerClass: '',
            containerWidth: false,
            width: false,
            search: false,
            searchAutofocus: false,
            autofocusScrollOffset: 0,
            coloring: {},
            texts: {
                trigger : "Select value",
                noResult : "No results",
                search : "Search"
            }
        },

        config: {},

        init: function() {
            this.config = $.extend({}, this.defaults, this.options);

            if(this.config.multiple === undefined) {
                this.config.multiple = this.$elem.is("select[multiple='multiple']") || this.$elem.is("select[multiple]");
            }

            if(!this.$elem.is("select")){
                console.log("Picker - Element is not Selectbox");
                return;
            }

            if(this.config.width !== false
                && (Math.floor(this.config.width) != this.config.width || !$.isNumeric(this.config.width))){
                console.log("Picker - Width is not a integer.");
                return;
            }

            if(this.config.containerWidth !== false
                && (Math.floor(this.config.containerWidth) != this.config.containerWidth || !$.isNumeric(this.config.containerWidth))){
                console.log("Picker - Container width is not a integer.");
                return;
            }

            this._build();
            this.$elem.css('display', 'none');

            this._fillList();

            this.$container.find('.pc-trigger').click(function(){
                var list = this.$container.find('.pc-list');
                list.toggle();

                if(this.config.search && this.config.searchAutofocus){
                    if (list.is(':visible')) {
                        list.find('input').focus();
                        $('html, body').animate({
                            scrollTop: list.find('input').offset().top - this.config.autofocusScrollOffset
                        }, 800);
                    }
                }
            }.bind(this));

            $(document).mouseup(function (e)
                {
                    var pc_list = this.$container.find(".pc-list");
                    if (!pc_list.is(e.target) && pc_list.has(e.target).length === 0 && !this.$container.find(".pc-trigger").is(e.target))
                    {
                        pc_list.hide();

                        if(this.config.search){
                            this.$container.find(".pc-list input").val('');
                            this._updateList(this.currentData);
                        }
                    }

                }.bind(this));

            return this;
        },

        pc_selected: function(e){
            var $elem = $(e.target);
            var selectedId = $elem.data('id');
            this._selectElement(selectedId, $elem);

            this.$container.find(".pc-list").hide();

            if(this.config.search){
                this.$container.find(".pc-list input").val('');
                this._updateList(this.currentData);
            }
        },

        pc_remove: function(elem){
            var $elem = $(elem.target);
            var selectedId = $elem.parent().data('id');
            var order = $elem.parent().data('order');

            var li = $("
").html($elem.parent().text()).attr('data-id', selectedId).attr('data-order', order);
            li.click(this.pc_selected.bind(this));

            if(this.config.search) {
                this._insertIntoCurrentData(elem);
            }

            var currentList = this.$container.find('.pc-list li');
            if(!this.$container.find(".pc-trigger").is(':visible')) { // Empty list
                this.$container.find('.pc-list ul').html('').append(li);
                this.$container.find(".pc-trigger").show();
            }else if(currentList.size() == 1) { // Only one item in list
                if(order > currentList.data('order')){
                    li.insertAfter(currentList);
                }else{
                    li.insertBefore(currentList);
                }
            }else{
                currentList.each(function(i,e) {
                    e = $(e);
                    if(e.is(':first-child')){
                        if(order < e.data('order')){
                            li.insertBefore(e);
                            return false;
                        }else if(order > e.data('order')
                            && order < e.next().data('order')){
                            li.insertAfter(e);
                            return false;
                        }
                    }else if(e.is(':last-child')) {
                        if(order > e.data('order')){
                            li.insertAfter(e);
                            return false;
                        }
                    }else{
                        if(order > e.data('order')
                            && order < e.next().data('order')){
                            li.insertAfter(e);
                            return false;
                        }
                    }
                });
            }
            this.$elem.find(" option[value='" + selectedId + "']").removeAttr("selected");
            $elem.parent().remove();
        },

        pc_search: function(e){
            var searchedValue = $(e.target).val().toLowerCase();
            var filteredData = this._filterData(searchedValue);
            this._updateList(filteredData, searchedValue);
        },

        ////////////////////////////////////////////
        ////////////// Private methods//////////////
        ////////////////////////////////////////////

        _selectElement: function (id, $elem) {
            if($elem == undefined){
                $elem = this.$container.find('.pc-list li[data-id="' + id + '"]');

                if($elem.length == 0){
                    console.log('Picker - ID to select not found!');
                    return;
                }
            }

            if(this.config.multiple) {
                this.$container.prepend(this._createElement($elem));
                $elem.remove();

                if(this.config.search){
                    this.currentData = this.currentData.filter(function (value) {
                        return value.id != id;
                    });
                }

                if(this.$container.find(".pc-list li").size() == 0) {
                    this.$container.find(".pc-trigger").hide();
                }
            }else{
                this.$elem.find("option").removeAttr("selected");

                if (this.config.coloring[id]) {
                    this.$container.find(".pc-trigger").removeClass().addClass(this.config.coloring[selectedId] + " pc-trigger pc-element").contents().first().replaceWith($elem.text());
                } else {
                    this.$container.find(".pc-trigger").contents().first().replaceWith($elem.text());
                }
            }

            this.$elem.find("option[value='" + id + "']").attr("selected", "selected");

        },

        _insertIntoCurrentData: function (elem) {
            var $elem = $(elem.target);
            var selectedId = $elem.parent().data('id');
            var order = $elem.parent().data('order');

            if(this.currentData.length == 0){
                this.currentData = [{
                    'id': selectedId,
                    'text': $elem.parent().text(),
                    'order': order
                }];

                return;
            }

            var i;
            for (i = 0; i < this.currentData.length; i++) {
                if (i == 0) {
                    if(order < this.currentData[i].order || this.currentData.length == 1){
                        this.currentData.splice(0, 0, {
                            'id': selectedId,
                            'text': $elem.parent().text(),
                            'order': order
                        });
                        break;
                    }
                } else if (i == (this.currentData.length - 1)) {
                    if(order > this.currentData[i].order){
                        this.currentData.splice(i, 0, {
                            'id': selectedId,
                            'text': $elem.parent().text(),
                            'order': order
                        });
                        break;
                    }
                } else if (this.currentData[i - 1].order < order && order < this.currentData[i].order) {
                    this.currentData.splice(i, 0, {
                        'id': selectedId,
                        'text': $elem.parent().text(),
                        'order': order
                    });
                    break;
                }
            }
        },

        _createElement: function($elem){
            var tagClass = this.config.coloring[$elem.data('id')];
            var root = $("").addClass("pc-element " + (tagClass ? tagClass : "")).text($elem.text())
                .attr('data-id', $elem.data('id')).attr('data-order',$elem.data('order'));

            root.append($('').click(this.pc_remove.bind(this)));
            return root;
        },

        _build: function(){
            var triggerText;
            if(this.config.multiple){
                triggerText = this.config.texts.trigger;
            }else{
                triggerText = this.$elem.find('option').first().text();
            }

            this.$container = $("
" +
            "" +
            "" + triggerText + "" +
            "

" +
            "" +
            "
");

            if(this.config.containerWidth !== false){
                this.$container.width(this.config.containerWidth);
            }

            this.$container.insertAfter(this.$elem);

            if(this.config.search){
                this._buildSearch();
            }
        },

        _buildSearch : function () {
            var $searchField = $("<input type='search' placeholder='" + this.config.texts.search + "'>");
            $searchField.on('input', this.pc_search.bind(this));
            $searchField.on('keypress', function (e) {
                if(e.which == 13){
                    var searchedValue = $(e.target).val().toLowerCase();
                    var filteredData = this._filterData(searchedValue);

                    if(filteredData.length == 1){
                        this.$container.find('.pc-list li').first().click();
                        return false;
                    }
                }

                return true;
            }.bind(this));

            this.$container.find('.pc-list').prepend($searchField);
        },

        _fillList: function(){
            var listContainer = this.$container.find('.pc-list ul');
            var counter = 0;
            this.$elem.find('option:not([hidden])').each(function(index, elem){
                var li = $("
").html($(elem).text()).attr('data-id', $(elem).attr('value')).attr('data-order', counter);
                li.click(this.pc_selected.bind(this));
                listContainer.append(li);

                if(this.config.search){
                    this.currentData.push({
                        'id' : $(elem).attr('value'),
                        'text' : $(elem).text(),
                        'order' : counter
                    });
                }

                if($(elem).attr('selected') == 'selected'){
                    li.click();
                }

                counter++;
            }.bind(this));

            this.$container.find(".pc-trigger").show();
        },

        _filterData: function (searchedValue) {
            return this.currentData.filter(function (value) {
                return value.text.toLowerCase().indexOf(searchedValue) != -1;
            });
        },

        _updateList: function(filteredData, searchedValue){
            var listContainer = this.$container.find('.pc-list ul');
            if(filteredData.length == 0){
                listContainer.html('
' + this.config.texts.noResult + '
');
                return;
            }

            listContainer.html('');
            var i, liContent;
            for(i = 0; i < filteredData.length; i++){
                // Highlighting searched string
                if(searchedValue !== undefined){
                    var regex = new RegExp( '(' + searchedValue + ')', 'gi' );
                    liContent = filteredData[i].text.replace( regex, '$1' )
                }else{
                    liContent = filteredData[i].text;
                }

                var li = $("
").html(liContent).attr('data-id', filteredData[i].id).attr('data-order', filteredData[i].order);
                li.click(this.pc_selected.bind(this));
                listContainer.append(li);
            }
        },

        /////////////////////////////////
        ////////////// API //////////////
        /////////////////////////////////

        // API invocation
        api: function (args) {
            if(Picker.prototype['api_' + args[0]]){
                return this['api_' + args[0]](args.slice(1));
            }else{
                console.log('Picker - unknown command!');
            }
        },

        // API functions

        api_destroy : function () {
            this.$container.remove();
            this.$elem.show();
            this.$elem.removeData("plugin_picker");

            return this.$elem;
        },

        api_get : function () {
            return this.$elem.val();
        },

        api_set : function (args) {
            if(args.length != 1){
                console.log('Picker - unknown number of arguments');
                return;
            }

            this._selectElement(args[0]);

            return this.$elem;
        }
    };

    $.fn.picker = function(options) {
        var pickerArguments = arguments;

        if($(this).length == 1){
            var instance = $(this).data("plugin_picker");
            if (!instance) {
                $(this).data("plugin_picker", new Picker(this, options).init());
                return this;
            } else {
                return instance.api(Array.prototype.slice.call(pickerArguments));
            }
        }else{
            return this.each(function() {
                var instance = $(this).data("plugin_picker");
                if (!instance) {
                    $(this).data("plugin_picker", new Picker(this, options).init());
                } else {
                    instance.api(Array.prototype.slice.call(pickerArguments));
                }
            });
        }
    };

})( jQuery, window , document );<|endoftext|>


<commit_before>// -------------------------------------------------------------------------- \\
// File: Mailbox.js                                                           \\
// Module: MailModel                                                          \\
// Requires: API                                                              \\
// Author: Neil Jenkins                                                       \\
// License: © 2010-2015 FastMail Pty Ltd. MIT Licensed.                       \\
// -------------------------------------------------------------------------- \\

/*global O, JMAP */

"use strict";

( function ( JMAP ) {

var Record = O.Record,
    attr = Record.attr;

var ValidationError = O.ValidationError;
var REQUIRED        = ValidationError.REQUIRED;
var TOO_LONG        = ValidationError.TOO_LONG;

var Mailbox = O.Class({

    Extends: Record,

    name: attr( String, {
        defaultValue: '',
        validate: function ( propValue/*, propKey, record*/ ) {
            if ( !propValue ) {
                return new ValidationError( REQUIRED,
                    O.loc( 'S_LABEL_REQUIRED' )
                );
            }
            if ( propValue.length > 256 ) {
                return new ValidationError( TOO_LONG,
                    O.loc( 'S_MAIL_ERROR_MAX_CHARS', 256 )
                );
            }
            return null;
        }
    }),

    parent: Record.toOne({
        Type: Mailbox,
        key: 'parentId',
        defaultValue: null
    }),

    role: attr( String, {
        defaultValue: null
    }),

    sortOrder: attr( Number, {
        defaultValue: 10
    }),

    // ---

    mustBeOnlyMailbox: attr( Boolean, {
        defaultValue: true,
        noSync: true
    }),
    mayReadItems: attr( Boolean, {
        defaultValue: true,
        noSync: true
    }),
    mayAddItems: attr( Boolean, {
        defaultValue: true,
        noSync: true
    }),
    mayRemoveItems: attr( Boolean, {
        defaultValue: true,
        noSync: true
    }),
    mayCreateChild: attr( Boolean, {
        defaultValue: true,
        noSync: true
    }),
    mayRename: attr( Boolean, {
        defaultValue: true,
        noSync: true
    }),
    mayDelete: attr( Boolean, {
        defaultValue: true,
        noSync: true
    }),

    // ---

    totalMessages: attr( Number, {
        defaultValue: 0,
        noSync: true
    }),
    unreadMessages: attr( Number, {
        defaultValue: 0,
        noSync: true
    }),
    totalThreads: attr( Number, {
        defaultValue: 0,
        noSync: true
    }),
    unreadThreads: attr( Number, {
        defaultValue: 0,
        noSync: true
    }),

    // ---

    displayName: function () {
        return this.get( 'name' );
    }.property( 'name' ),

    subfolders: function () {
        var storeKey = this.get( 'storeKey' ),
            store = this.get( 'store' );
        return storeKey ?
            store.getAll( Mailbox,
                function ( data ) {
                    return data.parentId === storeKey;
                },
                O.sortByProperties([ 'sortOrder', 'name' ])
            ) :
            new O.RecordArray( store, Mailbox, [] );
    }.property().nocache(),

    depth: function () {
        var parent = this.get( 'parent' );
        return parent ? parent.get( 'depth' ) + 1 : 0;
    }.property( 'parent' ),

    depthDidChange: function ( _, __, oldDepth ) {
        if ( oldDepth !== this.get( 'depth' ) ) {
            this.get( 'subfolders' ).forEach( function ( mailbox ) {
                mailbox.computedPropertyDidChange( 'depth' );
            });
        }
    }.observes( 'depth' ),

    // ---

    moveTo: function ( dest, where ) {
        var sub = ( where === 'sub' ),
            parent = sub ? dest : dest.get( 'parent' ),
            siblings = parent ?
                parent.get( 'subfolders' ) :
                this.get( 'store' ).getQuery( 'rootMailboxes', O.LiveQuery, {
                    Type: Mailbox,
                    filter: function ( data ) {
                        return !data.parentId;
                    },
                    sort: [ 'sortOrder', 'name' ]
                }),
            index = sub ? 0 :
                siblings.indexOf( dest ) + ( where === 'next' ? 1 : 0 ),
            prev = index ? siblings.getObjectAt( index - 1 ) : null,
            next = siblings.getObjectAt( index ),
            prevPrec = prev ? prev.get( 'sortOrder' ) : 0,
            nextPrec = next ? next.get( 'sortOrder' ) : ( index + 2 ) * 32,
            i, p, l, folder;

        if ( nextPrec - prevPrec < 2 ) {
            for ( i = 0, p = 32, l = siblings.get( 'length' );
                    i < l; i += 1, p += 32 ) {
                folder = siblings.getObjectAt( i );
                if ( folder !== this ) {
                    folder.set( 'sortOrder', p );
                    if ( folder === prev ) {
                        p += 32;
                    }
                }
            }
            if ( prev ) { prevPrec = prev.get( 'sortOrder' ); }
            if ( next ) { nextPrec = next.get( 'sortOrder' ); }
        }
        this.set( 'parent', parent || null )
            .set( 'sortOrder', ( nextPrec + prevPrec ) >> 1 );
    },

    // ---

    destroy: function () {
        // Check ACL
        if ( this.get( 'mayDelete' ) ) {
            // Destroy dependent records
            this.get( 'subfolders' ).forEach( function ( folder ) {
                folder.destroy();
            });
            Mailbox.parent.destroy.call( this );
        }
    }
});

Mailbox.prototype.parent.Type = Mailbox;

JMAP.mail.handle( Mailbox, {
    precedence: 0,
    fetch: function ( ids ) {
        this.callMethod( 'getMailboxes', {
            ids: ids || null,
            properties: null
        });
    },
    refresh: function ( ids, state ) {
        if ( ids ) {
            this.callMethod( 'getMailboxes', {
                ids: ids,
                properties: [
                    'totalMessages', 'unreadMessages',
                    'totalThreads', 'unreadThreads'
                ]
            });
        } else {
            this.callMethod( 'getMailboxUpdates', {
                sinceState: state,
                fetchRecords: true,
                fetchRecordProperties: null
            });
        }
    },
    commit: 'setMailboxes',

    // ---

    mailboxes: function ( args, reqMethod, reqArgs ) {
        this.didFetch( Mailbox, args,
            reqMethod === 'getMailboxes' && !reqArgs.ids );
    },

    mailboxUpdates: function ( args, _, reqArgs ) {
        this.didFetchUpdates( Mailbox, args, reqArgs );
    },
    error_getMailboxUpdates_cannotCalculateChanges: function () {
        // All our data may be wrong. Refetch everything.
        this.fetchAllRecords( Mailbox );
    },

    mailboxesSet: function ( args ) {
        this.didCommit( Mailbox, args );
    }
});

JMAP.Mailbox = Mailbox;

}( JMAP ) );
<commit_msg>Use sortOrder rather than precedence for var names.<commit_after>// -------------------------------------------------------------------------- \\
// File: Mailbox.js                                                           \\
// Module: MailModel                                                          \\
// Requires: API                                                              \\
// Author: Neil Jenkins                                                       \\
// License: © 2010-2015 FastMail Pty Ltd. MIT Licensed.                       \\
// -------------------------------------------------------------------------- \\

/*global O, JMAP */

"use strict";

( function ( JMAP ) {

var Record = O.Record;
var attr = Record.attr;

var ValidationError = O.ValidationError;
var REQUIRED        = ValidationError.REQUIRED;
var TOO_LONG        = ValidationError.TOO_LONG;

var Mailbox = O.Class({

    Extends: Record,

    name: attr( String, {
        defaultValue: '',
        validate: function ( propValue/*, propKey, record*/ ) {
            if ( !propValue ) {
                return new ValidationError( REQUIRED,
                    O.loc( 'S_LABEL_REQUIRED' )
                );
            }
            if ( propValue.length > 256 ) {
                return new ValidationError( TOO_LONG,
                    O.loc( 'S_MAIL_ERROR_MAX_CHARS', 256 )
                );
            }
            return null;
        }
    }),

    parent: Record.toOne({
        Type: Mailbox,
        key: 'parentId',
        defaultValue: null
    }),

    role: attr( String, {
        defaultValue: null
    }),

    sortOrder: attr( Number, {
        defaultValue: 10
    }),

    // ---

    mustBeOnlyMailbox: attr( Boolean, {
        defaultValue: true,
        noSync: true
    }),
    mayReadItems: attr( Boolean, {
        defaultValue: true,
        noSync: true
    }),
    mayAddItems: attr( Boolean, {
        defaultValue: true,
        noSync: true
    }),
    mayRemoveItems: attr( Boolean, {
        defaultValue: true,
        noSync: true
    }),
    mayCreateChild: attr( Boolean, {
        defaultValue: true,
        noSync: true
    }),
    mayRename: attr( Boolean, {
        defaultValue: true,
        noSync: true
    }),
    mayDelete: attr( Boolean, {
        defaultValue: true,
        noSync: true
    }),

    // ---

    totalMessages: attr( Number, {
        defaultValue: 0,
        noSync: true
    }),
    unreadMessages: attr( Number, {
        defaultValue: 0,
        noSync: true
    }),
    totalThreads: attr( Number, {
        defaultValue: 0,
        noSync: true
    }),
    unreadThreads: attr( Number, {
        defaultValue: 0,
        noSync: true
    }),

    // ---

    displayName: function () {
        return this.get( 'name' );
    }.property( 'name' ),

    subfolders: function () {
        var storeKey = this.get( 'storeKey' ),
            store = this.get( 'store' );
        return storeKey ?
            store.getAll( Mailbox,
                function ( data ) {
                    return data.parentId === storeKey;
                },
                O.sortByProperties([ 'sortOrder', 'name' ])
            ) :
            new O.RecordArray( store, Mailbox, [] );
    }.property().nocache(),

    depth: function () {
        var parent = this.get( 'parent' );
        return parent ? parent.get( 'depth' ) + 1 : 0;
    }.property( 'parent' ),

    depthDidChange: function ( _, __, oldDepth ) {
        if ( oldDepth !== this.get( 'depth' ) ) {
            this.get( 'subfolders' ).forEach( function ( mailbox ) {
                mailbox.computedPropertyDidChange( 'depth' );
            });
        }
    }.observes( 'depth' ),

    // ---

    moveTo: function ( dest, where ) {
        var sub = ( where === 'sub' ),
            parent = sub ? dest : dest.get( 'parent' ),
            siblings = parent ?
                parent.get( 'subfolders' ) :
                this.get( 'store' ).getQuery( 'rootMailboxes', O.LiveQuery, {
                    Type: Mailbox,
                    filter: function ( data ) {
                        return !data.parentId;
                    },
                    sort: [ 'sortOrder', 'name' ]
                }),
            index = sub ? 0 :
                siblings.indexOf( dest ) + ( where === 'next' ? 1 : 0 ),
            prev = index ? siblings.getObjectAt( index - 1 ) : null,
            next = siblings.getObjectAt( index ),
            prevSortOrder = prev ? prev.get( 'sortOrder' ) : 0,
            nextSortOrder = next ? next.get( 'sortOrder' ) : ( index + 2 ) * 32,
            i, p, l, folder;

        if ( nextSortOrder - prevSortOrder < 2 ) {
            for ( i = 0, p = 32, l = siblings.get( 'length' );
                    i < l; i += 1, p += 32 ) {
                folder = siblings.getObjectAt( i );
                if ( folder !== this ) {
                    folder.set( 'sortOrder', p );
                    if ( folder === prev ) {
                        p += 32;
                    }
                }
            }
            if ( prev ) { prevSortOrder = prev.get( 'sortOrder' ); }
            if ( next ) { nextSortOrder = next.get( 'sortOrder' ); }
        }
        this.set( 'parent', parent || null )
            .set( 'sortOrder', ( nextSortOrder + prevSortOrder ) >> 1 );
    },

    // ---

    destroy: function () {
        // Check ACL
        if ( this.get( 'mayDelete' ) ) {
            // Destroy dependent records
            this.get( 'subfolders' ).forEach( function ( folder ) {
                folder.destroy();
            });
            Mailbox.parent.destroy.call( this );
        }
    }
});

Mailbox.prototype.parent.Type = Mailbox;

JMAP.mail.handle( Mailbox, {
    precedence: 0,
    fetch: function ( ids ) {
        this.callMethod( 'getMailboxes', {
            ids: ids || null,
            properties: null
        });
    },
    refresh: function ( ids, state ) {
        if ( ids ) {
            this.callMethod( 'getMailboxes', {
                ids: ids,
                properties: [
                    'totalMessages', 'unreadMessages',
                    'totalThreads', 'unreadThreads'
                ]
            });
        } else {
            this.callMethod( 'getMailboxUpdates', {
                sinceState: state,
                fetchRecords: true,
                fetchRecordProperties: null
            });
        }
    },
    commit: 'setMailboxes',

    // ---

    mailboxes: function ( args, reqMethod, reqArgs ) {
        this.didFetch( Mailbox, args,
            reqMethod === 'getMailboxes' && !reqArgs.ids );
    },

    mailboxUpdates: function ( args, _, reqArgs ) {
        this.didFetchUpdates( Mailbox, args, reqArgs );
    },
    error_getMailboxUpdates_cannotCalculateChanges: function () {
        // All our data may be wrong. Refetch everything.
        this.fetchAllRecords( Mailbox );
    },

    mailboxesSet: function ( args ) {
        this.didCommit( Mailbox, args );
    }
});

JMAP.Mailbox = Mailbox;

}( JMAP ) );
<|endoftext|>


<commit_before><commit_msg>night-first fixes<commit_after><|endoftext|>


<commit_before>import getClientRect from '../utils/getClientRect';
import getOppositePlacement from '../utils/getOppositePlacement';
import getOffsetParent from '../utils/getOffsetParent';
import getBoundaries from '../utils/getBoundaries';

/**
 * Modifier used to prevent the popper from being positioned outside the boundary.
 *
 * An scenario exists where the reference itself is not within the boundaries. We can
 * say it has "escaped the boundaries" — or just "escaped". In this case we need to
 * decide whether the popper should either:
 *
 * - detach from the reference and remain "trapped" in the boundaries, or
 * - if it should be ignore the boundary and "escape with the reference"
 *
 * When `escapeWithReference` is `true`, and reference is completely outside the
 * boundaries, the popper will overflow (or completely leave) the boundaries in order
 * to remain attached to the edge of the reference.
 *
 * @method
 * @memberof Modifiers
 * @argument {Object} data - The data object generated by `update` method
 * @argument {Object} options - Modifiers configuration and options
 * @returns {Object} The data object, properly modified
 */
export default function preventOverflow(data, options) {
    const boundariesElement = options.boundariesElement || getOffsetParent(data.instance.popper);
    const boundaries = getBoundaries(data.instance.popper, options.padding, boundariesElement);
    options.boundaries = boundaries;

    const order = options.priority;
    let popper = getClientRect(data.offsets.popper);

    const check = {
        left() {
            let left = popper.left;
            if (popper.left < boundaries.left && !shouldOverflowBoundary(data, options, 'left')) {
                left = Math.max(popper.left, boundaries.left);
            }
            return { left };
        },
        right() {
            let left = popper.left;
            if (popper.right > boundaries.right && !shouldOverflowBoundary(data, options, 'right')) {
                left = Math.min(popper.left, boundaries.right - popper.width);
            }
            return { left };
        },
        top() {
            let top = popper.top;
            if (popper.top < boundaries.top && !shouldOverflowBoundary(data, options, 'top')) {
                top = Math.max(popper.top, boundaries.top);
            }
            return { top };
        },
        bottom() {
            let top = popper.top;
            if (popper.bottom > boundaries.bottom && !shouldOverflowBoundary(data, options, 'bottom')) {
                top = Math.min(popper.top, boundaries.bottom - popper.height);
            }
            return { top };
        }
    };

    order.forEach((direction) => {
        popper = {...popper, ...check[direction]()};
    });

    data.offsets.popper = popper;

    return data;
}

/**
 * Determine if the popper should overflow a boundary edge to stay together with the reference.
 */
function shouldOverflowBoundary(data, options, overflowDirection) {
    if (!options.escapeWithReference) {
        return false;
    }

    if (data.flipped && isSameAxis(data.originalPlacement, overflowDirection)) {
        return true;
    }

    if (!isSameAxis(data.originalPlacement, overflowDirection)) {
        return true;
    }

    return true;
}

/**
 * Determine if two placement values are on the same axis.
 */
function isSameAxis(a, b) {
    // placement syntax:
    //
    //     ( "top" | "right" | "bottom" | "left" ) ( "-start" | "" | "-end" )
    //     |------------- Direction -------------|
    //
    const aDirection = a.split('-')[0];
    const bDirection = b.split('-')[0];

    return aDirection === bDirection || aDirection === getOppositePlacement(b);
}
<commit_msg>[refactor] code cleanup<commit_after>import getClientRect from '../utils/getClientRect';
import getOppositePlacement from '../utils/getOppositePlacement';
import getOffsetParent from '../utils/getOffsetParent';
import getBoundaries from '../utils/getBoundaries';

/**
 * Modifier used to prevent the popper from being positioned outside the boundary.
 *
 * An scenario exists where the reference itself is not within the boundaries. We can
 * say it has "escaped the boundaries" — or just "escaped". In this case we need to
 * decide whether the popper should either:
 *
 * - detach from the reference and remain "trapped" in the boundaries, or
 * - if it should be ignore the boundary and "escape with the reference"
 *
 * When `escapeWithReference` is `true`, and reference is completely outside the
 * boundaries, the popper will overflow (or completely leave) the boundaries in order
 * to remain attached to the edge of the reference.
 *
 * @method
 * @memberof Modifiers
 * @argument {Object} data - The data object generated by `update` method
 * @argument {Object} options - Modifiers configuration and options
 * @returns {Object} The data object, properly modified
 */
export default function preventOverflow(data, options) {
    const boundariesElement = options.boundariesElement || getOffsetParent(data.instance.popper);
    const boundaries = getBoundaries(data.instance.popper, options.padding, boundariesElement);
    options.boundaries = boundaries;

    const order = options.priority;
    let popper = getClientRect(data.offsets.popper);

    const check = {
        primary(placement) {
            let value = popper[placement];
            if (popper[placement] < boundaries[placement] && !shouldOverflowBoundary(data, options, placement)) {
                value = Math.max(popper[placement], boundaries[placement]);
            }
            return { [placement]: value };
        },
        secondary(placement) {
            const mainSide = placement === 'right' ? 'left' : 'top';
            let value = popper[mainSide];
            if (popper[placement] > boundaries[placement] && !shouldOverflowBoundary(data, options, placement)) {
                value = Math.min(popper[mainSide], boundaries[placement] - (placement === 'right' ? popper.width : popper.height));
            }
            return { [mainSide]: value };
        }
    }

    order.forEach((placement) => {
        const side = ['left', 'top'].indexOf(placement) !== -1 ? 'primary' : 'secondary';
        popper = {...popper, ...check[side](placement)};
    });

    data.offsets.popper = popper;

    return data;
}

/**
 * Determine if the popper should overflow a boundary edge to stay together with the reference.
 */
function shouldOverflowBoundary(data, options, overflowDirection) {
    if (!options.escapeWithReference) {
        return false;
    }

    if (data.flipped && isSameAxis(data.originalPlacement, overflowDirection)) {
        return true;
    }

    if (!isSameAxis(data.originalPlacement, overflowDirection)) {
        return true;
    }

    return true;
}

/**
 * Determine if two placement values are on the same axis.
 */
function isSameAxis(a, b) {
    // placement syntax:
    //
    //     ( "top" | "right" | "bottom" | "left" ) ( "-start" | "" | "-end" )
    //     |------------- Direction -------------|
    //
    const aDirection = a.split('-')[0];
    const bDirection = b.split('-')[0];

    return aDirection === bDirection || aDirection === getOppositePlacement(b);
}
<|endoftext|>


<commit_before>/**
 * This module contains the basic function of managing & storing rooms, groups and socket clients information
 * @type {*|exports|module.exports}
 */
var express = require('express');
var socket = require('socket.io');
var lobby = new Lobby();

var getLobby = function () {
	return lobby;
}

/**
 * Lobby contruct an object that stores all the rooms into a hash map
 * @constructor
 */
function Lobby() {
	this.count = 0;
	this.rooms = {};
}

Lobby.prototype.size = function () {
	return this.count;
}

Lobby.prototype.addRoom = function (roomId, room) {
	if (room instanceof Room) {
		if (this.rooms[roomId]) {
			return true;
		}
		this.rooms[roomId] = room;
		this.count++;
		return true;
	} else return false;
}

Lobby.prototype.removeRoom = function (roomId) {
	if (this.rooms[roomId]) {
		delete this.rooms[roomId];
		this.count--;
		return true;
	} else return false;
}

Lobby.prototype.removeAllRooms = function(){
	this.rooms = {};
	this.count = 0;
	return true;
}

Lobby.prototype.get = function (roomId) {
	var room = this.rooms[roomId];
	if (room) {
		return room;
	} else return null;
}

Lobby.prototype.getRoomsMap = function () {
	return this.rooms;
}

Lobby.prototype.getUser = function(userId){
	for(var roomID in this.rooms) {
		var group = this.rooms[roomID].get('default');
		for (var user in group.socketClientMap) {
			if (user == userId) {
				return group.socketClientMap[user];
			}
		}
	}

	return null;
}

/**
 * Room construct an object that stores all the groups into a hash map
 * @constructor
 */
function Room() {
	var defaultGroup = new Group('default');
	this.count = 1;
	this.groups = {};
	this.groups[defaultGroup.groupId] = defaultGroup;
}

/**
 * Returns size of current Room
 * @returns {number}
 */
Room.prototype.size = function () {
	return this.count;
}

/**
 * Add group into current room
 * @param group
 * @returns {boolean}
 */
Room.prototype.addGroup = function (group) {
	if (group instanceof Group) {
		if (this.groups[group.groupId]) {
			return false;
		}
		this.groups[group.groupId] = group;
		this.count++;
		return true;
	} else return false;
}

/**
 * Remove group according to its groupId
 * @param groupId
 * @returns {boolean}
 */
Room.prototype.removeGroup = function (groupId) {
	if (groupId === 'default'){
		return false;
	}
	if (this.groups[groupId]) {
		delete this.groups[groupId];
		this.count--;
		return true;
	} else return false;
}

/**
 * Retrieve the group according to its groupId
 * @param groupId
 * @returns {*}
 */
Room.prototype.get = function (groupId) {
	var group = this.groups[groupId];
	if (group) {
		return group;
	} else return null;
}

/**
 * Retrieve the Room
 * @returns {{}|*}
 */
Room.prototype.getGroupsMap = function () {
	return this.groups;
}

/**
 * Group stores socket clients into a hash map
 * @param groupId
 * @constructor
 */
function Group(groupId) {
	this.groupId = groupId;
	this.count = 0;
	this.socketClientMap = {};
}

/**
 * Return size of current Group
 * @returns {number}
 */
Group.prototype.size = function () {
	return this.count;
}

/**
 * Add user socket client into Group
 * @param socketClient
 * @returns {boolean}
 */
Group.prototype.addClient = function (socketClient) {
	if (socketClient instanceof SocketClient) {
		if (this.socketClientMap[socketClient.userID]) {
			return false;
		}
		this.socketClientMap[socketClient.userID] = socketClient;
		this.count++;
		return true;
	} else return false;
}

/**
 * Remove user socket client from Group according to the input user id
 * @param userId
 */
Group.prototype.removeClient = function (userId) {
	if (this.socketClientMap[userId]) {
		delete this.socketClientMap[userId];
		this.count--;
	}
}

/**
 * Retrieve socket client according to its user id
 * @param userId
 * @returns {*}
 */
Group.prototype.get = function (userId) {
	var socketClient = this.socketClientMap[userId];
	if (socketClient) {
		return socketClient;
	} else return null;
}

/**
 * Retrieve map of all socket clients
 * @returns {{}|*}
 */
Group.prototype.getClientsMap = function () {
	return this.socketClientMap;
}

/*
* Retrieve the list of connected clients
* @returns [{}|*]
* */
Group.prototype.getConnectedClientsList = function () {
	var dataArray = [];
	for(var element in this.socketClientMap) {
        if (this.socketClientMap[element].connected) {
            dataArray.push(this.socketClientMap[element]);
        }
	}
	return dataArray;
}

/**
 * Socket Client Wrapper for storing user ID and user socket
 * @param userId
 * @param socket
 * @constructor
 */
function SocketClient(userId, socket) {
	this.userID = userId;
	this.socket = socket;
	this.socketID = socket.id;
	this.header = socket.request.headers;
	this.currentGroupID = null;
	this.currentRoomID = null;
	this.connected = true;
}

/*
* Set value of this.connected to false
* */
SocketClient.prototype.setDisconnect = function() {
    this.connected = false;
}

/**
 * Notify All user in current room on user leave
 */
SocketClient.prototype.notifyGroupUsersOnUserLeave = function() {
    this.roomBroadcast('User Leave',  {'userID': this.socketID});
}

/**
 * Socket listener wrapper
 * @param evt
 * @param callback
 */
SocketClient.prototype.on = function (evt, callback) {
	this.socket.on(evt, callback);
}

SocketClient.prototype.getRoom = function () {
	if (this.currentRoomID != null) {
		return getLobby().get(this.currentRoomID);
	} else return null;
}

/**
 * Add Socket Client to a room by its room id
 * @param roomId
 */
SocketClient.prototype.joinRoom = function (roomId) {
	var defaultGroup = getLobby().get(roomId).get('default');
	defaultGroup.addClient(this);
	this.currentGroupID = 'default';
	this.currentRoomID = roomId;
}

SocketClient.prototype.inRoom = function (roomId) {
	return (roomId === this.currentRoomID);
}

SocketClient.prototype.leaveRoom = function () {
	if (this.currentRoomID == null) {
		return false;
	} else {
		var currentGroup = getLobby().get(this.currentRoomID).get(this.currentGroupID);
		currentGroup.removeClient(this.userID);
		this.currentGroupID = null;
		this.currentRoomID = null;
	}
}

SocketClient.prototype.joinGroup = function (roomId, groupId) {
	if (this.inRoom(roomId)) {
		var group = getLobby().get(roomId).get(groupId);
		if (group && (group instanceof Group)) {
			group.addClient(this);
			this.currentGroupID = groupId;
		}
	}
}

SocketClient.prototype.inGroup = function (roomId, groupId) {
	return (this.inRoom(roomId) && groupId === this.currentGroupID);
}

SocketClient.prototype.leaveGroup = function (roomId, groupId) {
	if (this.inGroup(roomId, groupId) && (this.currentGroupID != 'default')) {
		var group = getLobby().get(roomId).get(groupId);
		if (group && (group instanceof Group)) {
			group.removeClient(this.userID);
			this.currentGroupID = 'default';
		}
	}
}

SocketClient.prototype.getCurrentGroup = function() {
    return getLobby().get(this.currentRoomID).get(this.currentGroupID);
}

SocketClient.prototype.getCurrentGroupUserList = function () {
	var curGroup = this.getCurrentGroup();
	return curGroup.getConnectedClientsList();
}

/**
 * Socket Emit wrapper
 * @param key
 * @param value
 */
SocketClient.prototype.emit = function (key, value) {
	this.socket.emit(key, value);
}

/**
 * Room Broadcaster
 * @param key
 * @param value
 */
SocketClient.prototype.roomBroadcast = function (key, value) {
	var clients = getLobby().get(this.currentRoomID).get('default').getClientsMap();
	//null check not implemented!
	for (var client in clients) {
		if (clients[client] == this) {
			continue;
		}
		clients[client].emit(key, value);
	}
}

SocketClient.prototype.personalMessage = function (key, value, receiverId) {
	var clients = getLobby().get(this.currentRoomID).get(this.currentGroupID).getConnectedClientsList();
	//null check not implemented!
	for (var client in clients) {
		if (client.userId == receiverId) {
			client.emit(key, value);
		}
	}
}

/**
 * Group Broadcaster
 * @param name
 * @param key
 * @param value
 */
SocketClient.prototype.groupBroadcast = function (key, value) {
	var clients = getLobby().get(this.currentRoomID).get(this.currentGroupID).getClientsMap();
	//null check not implemented!
	for (var client in clients) {
		if (clients[client] == this) {
			continue;
		}
		clients[client].emit(key, value);
	}
}

/**
 * Stringify Socket Client JSON object
 * @returns {SocketClient}
 */
SocketClient.prototype.toJSON = function () {
	var tojson = {};
	tojson.userID = this.userID;
	tojson.socketID = this.socketID;
	tojson.currentGroupID = this.currentGroupID;
	tojson.currentRoomID = this.currentRoomID;
	tojson.connected = this.connected;
	return tojson;
}

module.exports.getLobby = getLobby;
module.exports.SocketClient = SocketClient;
module.exports.Lobby = Lobby;
module.exports.Room = Room;
module.exports.Group = Group;<commit_msg>Allow reconnection of clients when disconnected from socket. However, opening a new tab to connect to the socket overwrites the old tab. Broadcasting of messages now will be received by sender too<commit_after>/**
 * This module contains the basic function of managing & storing rooms, groups and socket clients information
 * @type {*|exports|module.exports}
 */
var express = require('express');
var socket = require('socket.io');
var lobby = new Lobby();

var getLobby = function () {
	return lobby;
}

/**
 * Lobby contruct an object that stores all the rooms into a hash map
 * @constructor
 */
function Lobby() {
	this.count = 0;
	this.rooms = {};
}

Lobby.prototype.size = function () {
	return this.count;
}

Lobby.prototype.addRoom = function (roomId, room) {
	if (room instanceof Room) {
		if (this.rooms[roomId]) {
			return true;
		}
		this.rooms[roomId] = room;
		this.count++;
		return true;
	} else return false;
}

Lobby.prototype.removeRoom = function (roomId) {
	if (this.rooms[roomId]) {
		delete this.rooms[roomId];
		this.count--;
		return true;
	} else return false;
}

Lobby.prototype.removeAllRooms = function(){
	this.rooms = {};
	this.count = 0;
	return true;
}

Lobby.prototype.get = function (roomId) {
	var room = this.rooms[roomId];
	if (room) {
		return room;
	} else return null;
}

Lobby.prototype.getRoomsMap = function () {
	return this.rooms;
}

Lobby.prototype.getUser = function(userId){
	for(var roomID in this.rooms) {
		var group = this.rooms[roomID].get('default');
		for (var user in group.socketClientMap) {
			if (user == userId) {
				return group.socketClientMap[user];
			}
		}
	}

	return null;
}

/**
 * Room construct an object that stores all the groups into a hash map
 * @constructor
 */
function Room() {
	var defaultGroup = new Group('default');
	this.count = 1;
	this.groups = {};
	this.groups[defaultGroup.groupId] = defaultGroup;
}

/**
 * Returns size of current Room
 * @returns {number}
 */
Room.prototype.size = function () {
	return this.count;
}

/**
 * Add group into current room
 * @param group
 * @returns {boolean}
 */
Room.prototype.addGroup = function (group) {
	if (group instanceof Group) {
		if (this.groups[group.groupId]) {
			return false;
		}
		this.groups[group.groupId] = group;
		this.count++;
		return true;
	} else return false;
}

/**
 * Remove group according to its groupId
 * @param groupId
 * @returns {boolean}
 */
Room.prototype.removeGroup = function (groupId) {
	if (groupId === 'default'){
		return false;
	}
	if (this.groups[groupId]) {
		delete this.groups[groupId];
		this.count--;
		return true;
	} else return false;
}

/**
 * Retrieve the group according to its groupId
 * @param groupId
 * @returns {*}
 */
Room.prototype.get = function (groupId) {
	var group = this.groups[groupId];
	if (group) {
		return group;
	} else return null;
}

/**
 * Retrieve the Room
 * @returns {{}|*}
 */
Room.prototype.getGroupsMap = function () {
	return this.groups;
}

/**
 * Group stores socket clients into a hash map
 * @param groupId
 * @constructor
 */
function Group(groupId) {
	this.groupId = groupId;
	this.count = 0;
	this.socketClientMap = {};
}

/**
 * Return size of current Group
 * @returns {number}
 */
Group.prototype.size = function () {
	return this.count;
}

/**
 * Add user socket client into Group
 * @param socketClient
 * @returns {boolean}
 */
Group.prototype.addClient = function (socketClient) {
	if (socketClient instanceof SocketClient) {
		// Uncommenting this results in inability for client to reconnect
		//if (this.socketClientMap[socketClient.userID]) {
		//	return false;
		//}
		this.socketClientMap[socketClient.userID] = socketClient;
		this.count++;
		return true;
	} else return false;
}

/**
 * Remove user socket client from Group according to the input user id
 * @param userId
 */
Group.prototype.removeClient = function (userId) {
	if (this.socketClientMap[userId]) {
		delete this.socketClientMap[userId];
		this.count--;
	}
}

/**
 * Retrieve socket client according to its user id
 * @param userId
 * @returns {*}
 */
Group.prototype.get = function (userId) {
	var socketClient = this.socketClientMap[userId];
	if (socketClient) {
		return socketClient;
	} else return null;
}

/**
 * Retrieve map of all socket clients
 * @returns {{}|*}
 */
Group.prototype.getClientsMap = function () {
	return this.socketClientMap;
}

/*
* Retrieve the list of connected clients
* @returns [{}|*]
* */
Group.prototype.getConnectedClientsList = function () {
	var dataArray = [];
	for(var element in this.socketClientMap) {
        if (this.socketClientMap[element].connected) {
            dataArray.push(this.socketClientMap[element]);
        }
	}
	return dataArray;
}

/**
 * Socket Client Wrapper for storing user ID and user socket
 * @param userId
 * @param socket
 * @constructor
 */
function SocketClient(userId, socket) {
	this.userID = userId;
	this.socket = socket;
	this.socketID = socket.id;
	this.header = socket.request.headers;
	this.currentGroupID = null;
	this.currentRoomID = null;
	this.connected = true;
}

/*
* Set value of this.connected to false
* */
SocketClient.prototype.setDisconnect = function() {
    this.connected = false;
}

/**
 * Notify All user in current room on user leave
 */
SocketClient.prototype.notifyGroupUsersOnUserLeave = function() {
    this.roomBroadcast('User Leave',  {'userID': this.socketID});
}

/**
 * Socket listener wrapper
 * @param evt
 * @param callback
 */
SocketClient.prototype.on = function (evt, callback) {
	this.socket.on(evt, callback);
}

SocketClient.prototype.getRoom = function () {
	if (this.currentRoomID != null) {
		return getLobby().get(this.currentRoomID);
	} else return null;
}

/**
 * Add Socket Client to a room by its room id
 * @param roomId
 */
SocketClient.prototype.joinRoom = function (roomId) {
	var defaultGroup = getLobby().get(roomId).get('default');

	defaultGroup.addClient(this);

	this.currentGroupID = 'default';
	this.currentRoomID = roomId;
}

SocketClient.prototype.inRoom = function (roomId) {
	return (roomId === this.currentRoomID);
}

SocketClient.prototype.leaveRoom = function () {
	if (this.currentRoomID == null) {
		return false;
	} else {
		var currentGroup = getLobby().get(this.currentRoomID).get(this.currentGroupID);
		currentGroup.removeClient(this.userID);
		this.currentGroupID = null;
		this.currentRoomID = null;
	}
}

SocketClient.prototype.joinGroup = function (roomId, groupId) {
	if (this.inRoom(roomId)) {
		var group = getLobby().get(roomId).get(groupId);
		if (group && (group instanceof Group)) {
			group.addClient(this);
			this.currentGroupID = groupId;
		}
	}
}

SocketClient.prototype.inGroup = function (roomId, groupId) {
	return (this.inRoom(roomId) && groupId === this.currentGroupID);
}

SocketClient.prototype.leaveGroup = function (roomId, groupId) {
	if (this.inGroup(roomId, groupId) && (this.currentGroupID != 'default')) {
		var group = getLobby().get(roomId).get(groupId);
		if (group && (group instanceof Group)) {
			group.removeClient(this.userID);
			this.currentGroupID = 'default';
		}
	}
}

SocketClient.prototype.getCurrentGroup = function() {
    return getLobby().get(this.currentRoomID).get(this.currentGroupID);
}

SocketClient.prototype.getCurrentGroupUserList = function () {
	var curGroup = this.getCurrentGroup();
	return curGroup.getConnectedClientsList();
}

/**
 * Socket Emit wrapper
 * @param key
 * @param value
 */
SocketClient.prototype.emit = function (key, value) {
	this.socket.emit(key, value);
}

/**
 * Room Broadcaster
 * @param key
 * @param value
 */
SocketClient.prototype.roomBroadcast = function (key, value) {
	var clients = getLobby().get(this.currentRoomID).get('default').getClientsMap();
	//null check not implemented!
	for (var client in clients) {
			if (clients[client] == this) {
				clients[client].emit(key, value);
			}
	}
}

SocketClient.prototype.personalMessage = function (key, value, receiverId) {
	var clients = getLobby().get(this.currentRoomID).get(this.currentGroupID).getConnectedClientsList();
	//null check not implemented!
	for (var client in clients) {
		if (client.userId == receiverId) {
			client.emit(key, value);
		}
	}
}

/**
 * Group Broadcaster
 * @param name
 * @param key
 * @param value
 */
SocketClient.prototype.groupBroadcast = function (key, value) {
	var clients = getLobby().get(this.currentRoomID).get(this.currentGroupID).getClientsMap();
	//null check not implemented!
	for (var client in clients) {
		if (clients[client] == this) {
			continue;
		}
		clients[client].emit(key, value);
	}
}

/**
 * Stringify Socket Client JSON object
 * @returns {SocketClient}
 */
SocketClient.prototype.toJSON = function () {
	var tojson = {};
	tojson.userID = this.userID;
	tojson.socketID = this.socketID;
	tojson.currentGroupID = this.currentGroupID;
	tojson.currentRoomID = this.currentRoomID;
	tojson.connected = this.connected;
	return tojson;
}

module.exports.getLobby = getLobby;
module.exports.SocketClient = SocketClient;
module.exports.Lobby = Lobby;
module.exports.Room = Room;
module.exports.Group = Group;<|endoftext|>


<commit_before>// articles.js

var
    async = require('async'),
    api = require('../api'),
    db = require('../db'),
    utils = require('./_utils'),
    images = require('./_images'),
    constants = require('../constants');

var
    attachmentsApi = require('./attachmentApi'),
    checkAttachment = attachmentsApi.checkAttachment,
    createAttachmentTaskInTx = attachmentsApi.createAttachmentTaskInTx;

var
    User = db.user,
    Article = db.article,
    Category = db.category,
    Text = db.text,
    warp = db.warp,
    next_id = db.next_id;

function getArticles(page, allArticles, callback) {
    var now = Date.now();
    var countOptions = allArticles ? 'count(*)' : {
        select: 'count(*)',
        where: 'publish_time<?',
        params: [now]
    };
    Article.findNumber(countOptions, function(err, num) {
        if (err) {
            return callback(err);
        }
        page.totalItems = num;
        if (page.isEmpty) {
            return callback(null, { page: page, articles: [] });
        }
        var findOptions = {
            offset: page.offset,
            limit: page.limit,
            order: 'publish_time desc'
        };
        if (! allArticles) {
            findOptions.where = 'publish_time<?';
            findOptions.params = [now];
        }
        Article.findAll(findOptions, function(err, entities) {
            if (err) {
                return callback(err);
            }
            return callback(null, {
                page: page,
                articles: entities
            });
        });
    });
}

exports = module.exports = {

    getArticles: getArticles,

    'GET /api/articles/:id': function(req, res, next) {
        Article.find(req.params.id, function(err, article) {
            if (err) {
                return next(err);
            }
            if (article===null) {
                return next(api.not_found('Article'));
            }
            Text.find(article.content_id, function(err, text) {
                if (err) {
                    return next(err);
                }
                if (text===null) {
                    return next(api.not_found('Text'));
                }
                article.content = text.value;
                return res.send(article);
            });
        });
    },

    'GET /api/articles': function(req, res, next) {
        var page = utils.getPage(req);
        getArticles(page, false, function(err, articles) {
            if (err) {
                return next(err);
            }
            return res.send(articles);
        });
    },

    'POST /api/articles': function(req, res, next) {
        /**
         * Create a new article.
         *
         * @return {object} The created article object.
         */
        if (utils.isForbidden(req, constants.ROLE_EDITOR)) {
            return next(api.not_allowed('Permission denied.'));
        }
        try {
            var name = utils.getRequiredParam('name', req),
                category_id = utils.getRequiredParam('category_id', req),
                content = utils.getRequiredParam('content', req);
        }
        catch (e) {
            return next(e);
        }
        var description = utils.getParam('description', '', req),
            tags = utils.formatTags(utils.getParam('tags', '', req));

        var file = req.files && req.files.file;

        //var spt = utils.getParam('publish_time', '', req);
        //parse datetime
        var publish_time = Date.now();

        var content_id = next_id();
        var article_id = next_id();

        var fnCreate = function(fileObject) {
            warp.transaction(function(err, tx) {
                if (err) {
                    return next(err);
                }
                async.waterfall([
                    // check category:
                    function(callback) {
                        Category.find(category_id, tx, callback);
                    },
                    // create text:
                    function(category, callback) {
                        if (category===null) {
                            return callback(api.invalid_param('category_id'));
                        }
                        Text.create({
                            id: content_id,
                            ref_id: article_id,
                            value: content
                        }, tx, callback);
                    },
                    // create attachment:
                    function(text, callback) {
                        if (fileObject) {
                            var fn = createAttachmentTaskInTx(fileObject, tx, req.user.id);
                            return fn(callback);
                        }
                        callback(null, null);
                    },
                    // create article:
                    function(atta, callback) {
                        Article.create({
                            id: article_id,
                            user_id: req.user.id,
                            user_name: req.user.name,
                            category_id: category_id,
                            cover_id: atta===null ? '' : atta.id,
                            content_id: content_id,
                            name: name,
                            tags: tags,
                            description: description,
                            publish_time: publish_time
                        }, tx, callback);
                    }
                ], function(err, result) {
                    tx.done(err, function(err) {
                        if (err) {
                            return next(err);
                        }
                        result.content = content;
                        return res.send(result);
                    });
                });
            });
        };

        if (file) {
            return checkAttachment(file, function(err, attachFileObject) {
                if (err) {
                    return next(err);
                }
                if (! attachFileObject.isImage) {
                    return next(api.invalid_param('file', 'Invalid image file.'));
                }
                // override name:
                attachFileObject.name = name;
                fnCreate(attachFileObject);
            });
        }
        return fnCreate(null);
    },

    'POST /api/articles/:id': function(req, res, next) {
        /**
         * Update an article.
         *
         * @return {object} The updated article object.
         */
        if (utils.isForbidden(req, constants.ROLE_EDITOR)) {
            return next(api.not_allowed('Permission denied.'));
        }
        var name = utils.getParam('name', req),
            category_id = utils.getParam('category_id', req),
            description = utils.getParam('description', req),
            tags = utils.getParam('tags', req),
            content = utils.getParam('content', req);

        if (name!==null && name==='') {
            return next(api.invalid_param('name'));
        }
        if (category_id!==null && category_id==='') {
            return next(api.invalid_param('category_id'));
        }
        if (content!==null && content==='') {
            return next(api.invalid_param('content'));
        }
        if (tags!==null) {
            tags = utils.formatTags(tags);
        }

        var file = req.files && req.files.file;

        //var spt = utils.getParam('publish_time', '', req);
        //parse datetime
        var publish_time = null; //Date.now();

        var fnUpdate = function(fileObject) {
            warp.transaction(function(err, tx) {
                if (err) {
                    return next(err);
                }
                async.waterfall([
                    // query article:
                    function(callback) {
                        Article.find(req.params.id, tx, callback);
                    },
                    // update category?
                    function(article, callback) {
                        if (article===null) {
                            return callback(api.not_found('Article'));
                        }
                        if (req.user.role!==constants.ROLE_ADMIN && req.user.id!==article.user_id) {
                            return next(api.not_allowed('Permission denied.'));
                        }
                        if (category_id===null || category_id===article.category_id) {
                            return callback(null, article);
                        }
                        Category.find(category_id, tx, function(err, category) {
                            if (err) {
                                return callback(err);
                            }
                            if (category===null) {
                                return callback(api.invalid_param('category_id'));
                            }
                            article.category_id = category_id;
                            callback(null, article);
                        });
                    },
                    // update text?
                    function(article, callback) {
                        if (content===null) {
                            return callback(null, article);
                        }
                        var content_id = next_id();
                        Text.create({
                            id: content_id,
                            ref_id: article.id,
                            value: content
                        }, tx, function(err, text) {
                            if (err) {
                                return callback(err);
                            }
                            article.content_id = content_id;
                            callback(null, article);
                        });
                    },
                    // update cover?
                    function(article, callback) {
                        if (fileObject) {
                            var fn = createAttachmentTaskInTx(fileObject, tx, req.user.id);
                            return fn(function(err, atta) {
                                if (err) {
                                    return callback(err);
                                }
                                article.cover_id = atta.id;
                                callback(null, article);
                            });
                        }
                        callback(null, article);
                    },
                    // update article:
                    function(article, callback) {
                        if (name!==null) {
                            article.name = name;
                        }
                        if (description!==null) {
                            article.description = description;
                        }
                        if (tags!==null) {
                            article.tags = tags;
                        }
                        if (publish_time!==null) {
                            article.publish_time = publish_time;
                        }
                        article.update(tx, callback);
                    }
                ], function(err, result) {
                    tx.done(err, function(err) {
                        if (err) {
                            return next(err);
                        }
                        if (content!==null) {
                            result.content = content;
                            return res.send(result);
                        }
                        Text.find(result.content_id, function(err, text) {
                            if (err) {
                                return next(err);
                            }
                            result.content = text.value;
                            return res.send(result);
                        });
                    });
                });
            });
        };

        if (file) {
            return checkAttachment(file, function(err, attachFileObject) {
                if (err) {
                    return next(err);
                }
                if (! attachFileObject.isImage) {
                    return next(api.invalid_param('file', 'Invalid image file.'));
                }
                // override name:
                attachFileObject.name = name;
                fnUpdate(attachFileObject);
            });
        }
        return fnUpdate(null);
    },

    'POST /api/articles/:id/delete': function(req, res, next) {
        /**
         * Delete an article by its id.
         *
         * @param {string} :id - The id of the article.
         * @return {object} Results contains deleted id. e.g. {"id": "12345"}
         */
        if (utils.isForbidden(req, constants.ROLE_EDITOR)) {
            return next(api.not_allowed('Permission denied.'));
        }
        warp.transaction(function(err, tx) {
            if (err) {
                return next(err);
            }
            async.waterfall([
                function(callback) {
                    Article.find(req.params.id, tx, callback);
                },
                function(article, callback) {
                    if (article===null) {
                        return callback(api.not_found('Article'));
                    }
                    if (req.user.role!==constants.ROLE_ADMIN && req.user.id!==article.user_id) {
                        return next(api.not_allowed('Permission denied.'));
                    }
                    article.destroy(tx, callback);
                },
                function(r, callback) {
                    // delete all texts:
                    warp.update('delete from texts where ref_id=?', [req.params.id], tx, callback);
                }
            ], function(err, result) {
                tx.done(err, function(err) {
                    if (err) {
                        return next(err);
                    }
                    res.send({ id: req.params.id });
                });
            });
        });
    }
}
<commit_msg>add getArticle api, fix publish_at<commit_after>// articles.js

var
    async = require('async'),
    api = require('../api'),
    db = require('../db'),
    utils = require('./_utils'),
    images = require('./_images'),
    constants = require('../constants');

var
    attachmentsApi = require('./attachmentApi'),
    checkAttachment = attachmentsApi.checkAttachment,
    createAttachmentTaskInTx = attachmentsApi.createAttachmentTaskInTx;

var
    User = db.user,
    Article = db.article,
    Category = db.category,
    Text = db.text,
    warp = db.warp,
    next_id = db.next_id;

function getArticles(page, allArticles, callback) {
    var now = Date.now();
    var countOptions = allArticles ? 'count(*)' : {
        select: 'count(*)',
        where: 'publish_at<?',
        params: [now]
    };
    Article.findNumber(countOptions, function(err, num) {
        if (err) {
            return callback(err);
        }
        page.totalItems = num;
        if (page.isEmpty) {
            return callback(null, { page: page, articles: [] });
        }
        var findOptions = {
            offset: page.offset,
            limit: page.limit,
            order: 'publish_at desc'
        };
        if (! allArticles) {
            findOptions.where = 'publish_at<?';
            findOptions.params = [now];
        }
        Article.findAll(findOptions, function(err, entities) {
            if (err) {
                return callback(err);
            }
            return callback(null, {
                page: page,
                articles: entities
            });
        });
    });
}

function getArticle(id, callback) {
    Article.find(id, function(err, article) {
        if (err) {
            return callback(err);
        }
        if (article===null) {
            return callback(api.not_found('Article'));
        }
        Text.find(article.content_id, function(err, text) {
            if (err) {
                return callback(err);
            }
            if (text===null) {
                return callback(api.not_found('Text'));
            }
            article.content = text.value;
            callback(null, article);
        });
    });
}

exports = module.exports = {

    getArticles: getArticles,

    getArticle: getArticle,

    'GET /api/articles/:id': function(req, res, next) {
        getArticle(req, params.id, function(err, article) {
            if (err) {
                return next(err);
            }
            return res.send(article);
        });
    },

    'GET /api/articles': function(req, res, next) {
        var page = utils.getPage(req);
        getArticles(page, false, function(err, articles) {
            if (err) {
                return next(err);
            }
            return res.send(articles);
        });
    },

    'POST /api/articles': function(req, res, next) {
        /**
         * Create a new article.
         *
         * @return {object} The created article object.
         */
        if (utils.isForbidden(req, constants.ROLE_EDITOR)) {
            return next(api.not_allowed('Permission denied.'));
        }
        try {
            var name = utils.getRequiredParam('name', req),
                category_id = utils.getRequiredParam('category_id', req),
                content = utils.getRequiredParam('content', req);
        }
        catch (e) {
            return next(e);
        }
        var description = utils.getParam('description', '', req),
            tags = utils.formatTags(utils.getParam('tags', '', req));

        var file = req.files && req.files.file;

        //var spt = utils.getParam('publish_at', '', req);
        //parse datetime
        var publish_at = Date.now();

        var content_id = next_id();
        var article_id = next_id();

        var fnCreate = function(fileObject) {
            warp.transaction(function(err, tx) {
                if (err) {
                    return next(err);
                }
                async.waterfall([
                    // check category:
                    function(callback) {
                        Category.find(category_id, tx, callback);
                    },
                    // create text:
                    function(category, callback) {
                        if (category===null) {
                            return callback(api.invalid_param('category_id'));
                        }
                        Text.create({
                            id: content_id,
                            ref_id: article_id,
                            value: content
                        }, tx, callback);
                    },
                    // create attachment:
                    function(text, callback) {
                        if (fileObject) {
                            var fn = createAttachmentTaskInTx(fileObject, tx, req.user.id);
                            return fn(callback);
                        }
                        callback(null, null);
                    },
                    // create article:
                    function(atta, callback) {
                        Article.create({
                            id: article_id,
                            user_id: req.user.id,
                            user_name: req.user.name,
                            category_id: category_id,
                            cover_id: atta===null ? '' : atta.id,
                            content_id: content_id,
                            name: name,
                            tags: tags,
                            description: description,
                            publish_at: publish_at
                        }, tx, callback);
                    }
                ], function(err, result) {
                    tx.done(err, function(err) {
                        if (err) {
                            return next(err);
                        }
                        result.content = content;
                        return res.send(result);
                    });
                });
            });
        };

        if (file) {
            return checkAttachment(file, function(err, attachFileObject) {
                if (err) {
                    return next(err);
                }
                if (! attachFileObject.isImage) {
                    return next(api.invalid_param('file', 'Invalid image file.'));
                }
                // override name:
                attachFileObject.name = name;
                fnCreate(attachFileObject);
            });
        }
        return fnCreate(null);
    },

    'POST /api/articles/:id': function(req, res, next) {
        /**
         * Update an article.
         *
         * @return {object} The updated article object.
         */
        if (utils.isForbidden(req, constants.ROLE_EDITOR)) {
            return next(api.not_allowed('Permission denied.'));
        }
        var name = utils.getParam('name', req),
            category_id = utils.getParam('category_id', req),
            description = utils.getParam('description', req),
            tags = utils.getParam('tags', req),
            content = utils.getParam('content', req);

        if (name!==null && name==='') {
            return next(api.invalid_param('name'));
        }
        if (category_id!==null && category_id==='') {
            return next(api.invalid_param('category_id'));
        }
        if (content!==null && content==='') {
            return next(api.invalid_param('content'));
        }
        if (tags!==null) {
            tags = utils.formatTags(tags);
        }

        var file = req.files && req.files.file;

        //var spt = utils.getParam('publish_at', '', req);
        //parse datetime
        var publish_at = null; //Date.now();

        var fnUpdate = function(fileObject) {
            warp.transaction(function(err, tx) {
                if (err) {
                    return next(err);
                }
                async.waterfall([
                    // query article:
                    function(callback) {
                        Article.find(req.params.id, tx, callback);
                    },
                    // update category?
                    function(article, callback) {
                        if (article===null) {
                            return callback(api.not_found('Article'));
                        }
                        if (req.user.role!==constants.ROLE_ADMIN && req.user.id!==article.user_id) {
                            return next(api.not_allowed('Permission denied.'));
                        }
                        if (category_id===null || category_id===article.category_id) {
                            return callback(null, article);
                        }
                        Category.find(category_id, tx, function(err, category) {
                            if (err) {
                                return callback(err);
                            }
                            if (category===null) {
                                return callback(api.invalid_param('category_id'));
                            }
                            article.category_id = category_id;
                            callback(null, article);
                        });
                    },
                    // update text?
                    function(article, callback) {
                        if (content===null) {
                            return callback(null, article);
                        }
                        var content_id = next_id();
                        Text.create({
                            id: content_id,
                            ref_id: article.id,
                            value: content
                        }, tx, function(err, text) {
                            if (err) {
                                return callback(err);
                            }
                            article.content_id = content_id;
                            callback(null, article);
                        });
                    },
                    // update cover?
                    function(article, callback) {
                        if (fileObject) {
                            var fn = createAttachmentTaskInTx(fileObject, tx, req.user.id);
                            return fn(function(err, atta) {
                                if (err) {
                                    return callback(err);
                                }
                                article.cover_id = atta.id;
                                callback(null, article);
                            });
                        }
                        callback(null, article);
                    },
                    // update article:
                    function(article, callback) {
                        if (name!==null) {
                            article.name = name;
                        }
                        if (description!==null) {
                            article.description = description;
                        }
                        if (tags!==null) {
                            article.tags = tags;
                        }
                        if (publish_at!==null) {
                            article.publish_at = publish_at;
                        }
                        article.update(tx, callback);
                    }
                ], function(err, result) {
                    tx.done(err, function(err) {
                        if (err) {
                            return next(err);
                        }
                        if (content!==null) {
                            result.content = content;
                            return res.send(result);
                        }
                        Text.find(result.content_id, function(err, text) {
                            if (err) {
                                return next(err);
                            }
                            result.content = text.value;
                            return res.send(result);
                        });
                    });
                });
            });
        };

        if (file) {
            return checkAttachment(file, function(err, attachFileObject) {
                if (err) {
                    return next(err);
                }
                if (! attachFileObject.isImage) {
                    return next(api.invalid_param('file', 'Invalid image file.'));
                }
                // override name:
                attachFileObject.name = name;
                fnUpdate(attachFileObject);
            });
        }
        return fnUpdate(null);
    },

    'POST /api/articles/:id/delete': function(req, res, next) {
        /**
         * Delete an article by its id.
         *
         * @param {string} :id - The id of the article.
         * @return {object} Results contains deleted id. e.g. {"id": "12345"}
         */
        if (utils.isForbidden(req, constants.ROLE_EDITOR)) {
            return next(api.not_allowed('Permission denied.'));
        }
        warp.transaction(function(err, tx) {
            if (err) {
                return next(err);
            }
            async.waterfall([
                function(callback) {
                    Article.find(req.params.id, tx, callback);
                },
                function(article, callback) {
                    if (article===null) {
                        return callback(api.not_found('Article'));
                    }
                    if (req.user.role!==constants.ROLE_ADMIN && req.user.id!==article.user_id) {
                        return next(api.not_allowed('Permission denied.'));
                    }
                    article.destroy(tx, callback);
                },
                function(r, callback) {
                    // delete all texts:
                    warp.update('delete from texts where ref_id=?', [req.params.id], tx, callback);
                }
            ], function(err, result) {
                tx.done(err, function(err) {
                    if (err) {
                        return next(err);
                    }
                    res.send({ id: req.params.id });
                });
            });
        });
    }
}
<|endoftext|>