<commit_before>import $ from 'jquery';
import { Lists } from '../../list/model/index';
import dialog from '../../../../node_modules/phraseanet-common/src/components/dialog';
import Selectable from '../../utils/selectable';
import pushAddUser from './addUser';
import * as _ from 'underscore';
const humane = require('humane-js');
require('phraseanet-common/src/components/tooltip');

const Feedback = function (services, options) {
    const { configService, localeService, appEvents } = services;
    const url = configService.get('baseUrl');
    let $container;
    let { containerId, context } = options;

    this.url = url;
    this.container = $container = $(containerId);
    this.userList = new Lists();

    this.Context = context;

    this.selection = new Selectable(services,
        $('.user_content .badges', this.container),
        {
            selector: '.badge'
        }
    );

    pushAddUser(services).initialize({$container: this.container});

    var $this = this;

    this.container.on('mouseenter', '.list-trash-btn', function (event) {
        var $el = $(event.currentTarget);
        $el.find('.image-normal').hide();
        $el.find('.image-hover').show();
    });

    this.container.on('mouseleave', '.list-trash-btn', function (event) {
        var $el = $(event.currentTarget);
        $el.find('.image-normal').show();
        $el.find('.image-hover').hide();
    });

    this.container.on('click', '.list-trash-btn', function (event) {
        var $el = $(event.currentTarget);
        var list_id = $el.parent().data('list-id');

        appEvents.emit('push.removeList', {
                list_id: list_id,
                container: containerId
            }
        );
    });

    this.container.on('click', '.push-refresh-list-action', function (event) {
        event.preventDefault();

        var callback = function callback(datas) {
            var context = $(datas);
            var dataList = $(context).find('.lists').prop('outerHTML');

            var refreshContent = $('.LeftColumn .content .lists', $container);
            refreshContent.removeClass('loading').append(dataList);
        };

        $('.LeftColumn .content .lists', $container).empty().addClass('loading');

        _this.userList.get(callback, 'html');
    });

    this.container.on('click', '.content .options .select-all', function (event) {
        $this.selection.selectAll();
    });

    this.container.on('click', '.content .options .unselect-all', function (event) {
        $this.selection.empty();
    });

    this.container.on('click', '.content .options .delete-selection', function (event) {
        _.each($('.badges.selectionnable').children(), function(item) {
            var $elem = $(item);
            if($elem.hasClass('selected')) {
                $elem.fadeOut(function () {
                    $elem.remove();
                });
            }
        });
        return false;
    });

    $('.UserTips', this.container).tooltip();

    /*this.container.on('click', '.user_adder', function (event) {
        event.preventDefault();
        const url = configService.get('baseUrl');
        var $this = $(this);

        $.ajax({
            type: 'GET',
            url: `${url}prod/push/add-user/`,
            dataType: 'html',
            beforeSend: function () {
                var options = {
                    size: 'Medium',
                    title: $this.html()
                };
                dialog.create(services, options, 2).getDomElement().addClass('loading');
            },
            success: function (data) {
                dialog.get(2).getDomElement().removeClass('loading').empty().append(data);
                return;
            },
            error: function () {
                dialog.get(2).close();
                return;
            },
            timeout: function () {
                dialog.get(2).close();
                return;
            }
        });

        return false;
    });*/

    this.container.on('click', '.recommended_users', function (event) {
        var usr_id = $('input[name="usr_id"]', $(this)).val();

        $this.loadUser(usr_id, $this.selectUser);

        return false;
    });

    this.container.on('click', '.recommended_users_list', function (event) {

        var content = $('#push_user_recommendations').html();

        var options = {
            size: 'Small',
            title: $(this).attr('title')
        };

        let $dialog = dialog.create(services, options, 2);
        $dialog.setContent(content);

        $dialog.getDomElement().find('a.adder').bind('click', function () {

            $(this).addClass('added');

            var usr_id = $(this).closest('tr').find('input[name="usr_id"]').val();

            $this.loadUser(usr_id, $this.selectUser);

            return false;
        });

        $dialog.getDomElement().find('a.adder').each(function (i, el) {

            var usr_id = $(this).closest('tr').find('input[name="usr_id"]').val();

            if ($('.badge_' + usr_id, $this.container).length > 0) {
                $(this).addClass('added');
            }

        });

        return false;
    });

    //this.container.on('submit', '#PushBox form[name="FeedBackForm"]', function (event) {
    $('#PushBox form[name="FeedBackForm"]').bind('submit', function () {

        var $this = $(this);

        $.ajax({
            type: $this.attr('method'),
            url: $this.attr('action'),
            dataType: 'json',
            data: $this.serializeArray(),
            beforeSend: function () {

            },
            success: function (data) {
                if (data.success) {
                    humane.info(data.message);
                    dialog.close(1);
                    appEvents.emit('workzone.refresh');
                } else {
                    humane.error(data.message);
                }
                return;
            },
            error: function () {

                return;
            },
            timeout: function () {

                return;
            }
        });

        return false;
    });

    $('.FeedbackSend', this.container).bind('click', function (event) {
        if ($('.badges .badge', $container).length === 0) {
            alert(localeService.t('FeedBackNoUsersSelected'));
            return;
        }

        var buttons = {};

        buttons[localeService.t('annuler')] = function () {
            $dialog.close();
        };

        buttons[localeService.t('send')] = function () {
            if ($.trim($('input[name="name"]', $dialog.getDomElement()).val()) === '') {
                var options = {
                    size: 'Alert',
                    closeButton: true,
                    title: localeService.t('warning')
                };
                var $dialogAlert = dialog.create(services, options, 3);
                $dialogAlert.setContent(localeService.t('FeedBackNameMandatory'));

                return false;
            }

            $dialog.close();

            $('input[name="name"]', $FeedBackForm).val($('input[name="name"]', $dialog.getDomElement()).val());
            $('input[name="duration"]', $FeedBackForm).val($('select[name="duration"]', $dialog.getDomElement()).val());
            $('textarea[name="message"]', $FeedBackForm).val($('textarea[name="message"]', $dialog.getDomElement()).val());
            $('input[name="recept"]', $FeedBackForm).prop('checked', $('input[name="recept"]', $dialog.getDomElement()).prop('checked'));
            $('input[name="force_authentication"]', $FeedBackForm).prop('checked', $('input[name="force_authentication"]', $dialog.getDomElement()).prop('checked'));

            $FeedBackForm.trigger('submit');
        };

        var options = {
            size: '558x352',
            buttons: buttons,
            loading: true,
            title: localeService.t('send'),
            closeOnEscape: true,
        };

        const $el = $(event.currentTarget);
        if($el.hasClass('validation')) {
            options.isValidation = true;
            options.size = '558x415'
        }

        var $dialog = dialog.create(services, options, 2);

        $dialog.getDomElement().closest('.ui-dialog').addClass('dialog_container');
        if(options.isValidation) {
            $dialog.getDomElement().closest('.ui-dialog').addClass('validation');
        }

        var $FeedBackForm = $('form[name="FeedBackForm"]', $container);

        var html = _.template($('#feedback_sendform_tpl').html());

        $dialog.setContent(html);

        $('input[name="name"]', $dialog.getDomElement()).val($('input[name="name"]', $FeedBackForm).val());
        $('textarea[name="message"]', $dialog.getDomElement()).val($('textarea[name="message"]', $FeedBackForm).val());
        $('.' + $this.Context, $dialog.getDomElement()).show();

        $('form', $dialog.getDomElement()).submit(function () {
            return false;
        });
    });

    $('.user_content .badges', this.container).disableSelection();

    // toggle download feature for users
    this.container.on('click', '.user_content .badges .badge .toggle', function (event) {
        var $this = $(this);

        $this.toggleClass('status_off status_on');

        $this.find('input').val($this.hasClass('status_on') ? '1' : '0');

        return false;
    });

    this.container.on('mouseenter', '#info-box-trigger', function(event) {
        $('#info-box').show();
    });

    this.container.on('mouseleave', '#info-box-trigger', function(event) {
        $('#info-box').hide();
    });

    // toggle feature state of selected users
    this.container.on('click', '.general_togglers .general_toggler', function (event) {
        var feature = $(this).attr('feature');

        var $badges = $('.user_content .badge.selected', this.container);

        var toggles = $('.status_off.toggle_' + feature, $badges);

        if (toggles.length === 0) {
            toggles = $('.status_on.toggle_' + feature, $badges);
        }
        if (toggles.length === 0) {
            humane.info('No user selected');
        }

        toggles.trigger('click');
        return false;
    });

    this.container.on('click', '.user_content .badges .badge .deleter', function (event) {
        var $elem = $(this).closest('.badge');
        $elem.fadeOut(function () {
            $elem.remove();
        });
        return false;
    });

    this.container.on('click', '.list_manager', function (event) {
        $('#PushBox').hide();
        $('#ListManager').show();
        return false;
    });

    this.container.on('click', 'a.list_push_loader', function (event) {
        var url = $(this).attr('href');

        var callbackList = function (list) {
            for (let i in list.entries) {
                this.selectUser(list.entries[i].User);
            }
        };

        $this.loadList(url, callbackList);

        return false;
    });

    $('form.list_saver', this.container).bind('submit', () => {
        var $form = $(event.currentTarget);
        var $input = $('input[name="name"]', $form);

        var users = this.getUsers();

        if (users.length === 0) {
            humane.error('No users');
            return false;
        }

        // appEvents.emit('push.createList', {name: $input.val(), collection: users});
        $this.createList({ name: $input.val(), collection: users });
        $input.val('');
        /*
         p4.Lists.create($input.val(), function (list) {
         $input.val('');
         list.addUsers(users);
         });*/

        return false;
    });

    $('input[name="users-search"]', this.container).autocomplete({
            minLength: 2,
            source: function (request, response) {
                $.ajax({
                    url: `${url}prod/push/search-user/`,
                    dataType: 'json',
                    data: {
                        query: request.term
                    },
                    success: function (data) {
                        response(data);
                    }
                });
            },
            focus: function (event, ui) {
                // $('input[name="users-search"]').val(ui.item.label);
            },
            select: function (event, ui) {
                if (ui.item.type === 'USER') {
                    $this.selectUser(ui.item);
                } else if (ui.item.type === 'LIST') {
                    for (let e in ui.item.entries) {
                        $this.selectUser(ui.item.entries[e].User);
                    }
                }
                return false;
            }
        })
        .data('ui-autocomplete')._renderItem = function (ul, item) {
        var html = '';

        if (item.type === 'USER') {
            html = _.template($('#list_user_tpl').html())({
                item: item
            });

            if (_this.Context === 'Push') {
                $('img[src="/assets/common/images/icons/user-orange.png"]')
                .attr('src', '/assets/common/images/icons/user-blue.png')
            }
            else if (_this.Context === 'Feedback') {
                $('img[src="/assets/common/images/icons/user-orange.png"]')
                .attr('src', '/assets/common/images/icons/user-green.png')
            }
        }
        else if (item.type === 'LIST') {
            html = _.template($('#list_list_tpl').html())({
                item: item
            });
        }

        return $(html).data('ui-autocomplete-item', item).appendTo(ul);
    };

    return this;
};

Feedback.prototype = {
    selectUser: function (user) {
        if (typeof user !== 'object') {
            if (window.console) {
                console.log('trying to select a user with wrong datas');
            }
        }
        if ($('.badge_' + user.usr_id, this.container).length > 0) {
            humane.info('User already selected');
            return;
        }

        var html = _.template($('#' + this.Context.toLowerCase() + '_badge_tpl').html())({
            user: user
        });

        // p4.Feedback.appendBadge(html);
        this.appendBadge(html);
    },
    loadUser: function (usr_id, callback) {
        var $this = this;
        $.ajax({
            type: 'GET',
            url: `${this.url}prod/push/user/${usr_id}/`,
            dataType: 'json',
            data: {
                usr_id: usr_id
            },
            success: function (data) {
                if (typeof callback === 'function') {
                    callback.call($this, data);
                }
            }
        });
    },
    createList: function (options) {
        let { name, collection } = options;

        this.userList.create(name, function (list) {
            list.addUsers(collection);
        });
    },
    loadList: function (url, callback) {
        var $this = this;

        $.ajax({
            type: 'GET',
            url: url,
            dataType: 'json',
            success: function (data) {
                if (typeof callback === 'function') {
                    callback.call($this, data);
                }
            }
        });
    },
    appendBadge: function (badge) {
        $('.user_content .badges', this.container).append(badge);
    },
    addUser: function (options) {
        let {$userForm, callback} = options;
        var $this = this;
        $.ajax({
            type: 'POST',
            url: `${this.url}prod/push/add-user/`,
            dataType: 'json',
            data: $userForm.serializeArray(),
            success: function (data) {
                if (data.success) {
                    humane.info(data.message);
                    $this.selectUser(data.user);
                    callback();
                } else {
                    humane.error(data.message);
                }
            }
        });
    },
    getSelection: function () {
        return this.selection;
    },
    getUsers: function () {
        return $('.user_content .badge', this.container).map(function () {
            return $('input[name="id"]', $(this)).val();
        });
    }
};

export default Feedback;
<commit_msg>PHRAS-2220_feedback_window_UI__correction fix<commit_after>import $ from 'jquery';
import { Lists } from '../../list/model/index';
import dialog from '../../../../node_modules/phraseanet-common/src/components/dialog';
import Selectable from '../../utils/selectable';
import pushAddUser from './addUser';
import * as _ from 'underscore';
const humane = require('humane-js');
require('phraseanet-common/src/components/tooltip');

const Feedback = function (services, options) {
    const { configService, localeService, appEvents } = services;
    const url = configService.get('baseUrl');
    let $container;
    let { containerId, context } = options;

    this.url = url;
    this.container = $container = $(containerId);
    this.userList = new Lists();

    this.Context = context;

    this.selection = new Selectable(services,
        $('.user_content .badges', this.container),
        {
            selector: '.badge'
        }
    );

    pushAddUser(services).initialize({$container: this.container});

    var $this = this;

    this.container.on('mouseenter', '.list-trash-btn', function (event) {
        var $el = $(event.currentTarget);
        $el.find('.image-normal').hide();
        $el.find('.image-hover').show();
    });

    this.container.on('mouseleave', '.list-trash-btn', function (event) {
        var $el = $(event.currentTarget);
        $el.find('.image-normal').show();
        $el.find('.image-hover').hide();
    });

    this.container.on('click', '.list-trash-btn', function (event) {
        var $el = $(event.currentTarget);
        var list_id = $el.parent().data('list-id');

        appEvents.emit('push.removeList', {
                list_id: list_id,
                container: containerId
            }
        );
    });

    this.container.on('click', '.push-refresh-list-action', function (event) {
        event.preventDefault();

        var callback = function callback(datas) {
            var context = $(datas);
            var dataList = $(context).find('.lists').prop('outerHTML');

            var refreshContent = $('.LeftColumn .content .lists', $container);
            refreshContent.removeClass('loading').append(dataList);
        };

        $('.LeftColumn .content .lists', $container).empty().addClass('loading');

        $this.userList.get(callback, 'html');
    });

    this.container.on('click', '.content .options .select-all', function (event) {
        $this.selection.selectAll();
    });

    this.container.on('click', '.content .options .unselect-all', function (event) {
        $this.selection.empty();
    });

    this.container.on('click', '.content .options .delete-selection', function (event) {
        _.each($('.badges.selectionnable').children(), function(item) {
            var $elem = $(item);
            if($elem.hasClass('selected')) {
                $elem.fadeOut(function () {
                    $elem.remove();
                });
            }
        });
        return false;
    });

    $('.UserTips', this.container).tooltip();

    /*this.container.on('click', '.user_adder', function (event) {
        event.preventDefault();
        const url = configService.get('baseUrl');
        var $this = $(this);

        $.ajax({
            type: 'GET',
            url: `${url}prod/push/add-user/`,
            dataType: 'html',
            beforeSend: function () {
                var options = {
                    size: 'Medium',
                    title: $this.html()
                };
                dialog.create(services, options, 2).getDomElement().addClass('loading');
            },
            success: function (data) {
                dialog.get(2).getDomElement().removeClass('loading').empty().append(data);
                return;
            },
            error: function () {
                dialog.get(2).close();
                return;
            },
            timeout: function () {
                dialog.get(2).close();
                return;
            }
        });

        return false;
    });*/

    this.container.on('click', '.recommended_users', function (event) {
        var usr_id = $('input[name="usr_id"]', $(this)).val();

        $this.loadUser(usr_id, $this.selectUser);

        return false;
    });

    this.container.on('click', '.recommended_users_list', function (event) {

        var content = $('#push_user_recommendations').html();

        var options = {
            size: 'Small',
            title: $(this).attr('title')
        };

        let $dialog = dialog.create(services, options, 2);
        $dialog.setContent(content);

        $dialog.getDomElement().find('a.adder').bind('click', function () {

            $(this).addClass('added');

            var usr_id = $(this).closest('tr').find('input[name="usr_id"]').val();

            $this.loadUser(usr_id, $this.selectUser);

            return false;
        });

        $dialog.getDomElement().find('a.adder').each(function (i, el) {

            var usr_id = $(this).closest('tr').find('input[name="usr_id"]').val();

            if ($('.badge_' + usr_id, $this.container).length > 0) {
                $(this).addClass('added');
            }

        });

        return false;
    });

    //this.container.on('submit', '#PushBox form[name="FeedBackForm"]', function (event) {
    $('#PushBox form[name="FeedBackForm"]').bind('submit', function () {

        var $this = $(this);

        $.ajax({
            type: $this.attr('method'),
            url: $this.attr('action'),
            dataType: 'json',
            data: $this.serializeArray(),
            beforeSend: function () {

            },
            success: function (data) {
                if (data.success) {
                    humane.info(data.message);
                    dialog.close(1);
                    appEvents.emit('workzone.refresh');
                } else {
                    humane.error(data.message);
                }
                return;
            },
            error: function () {

                return;
            },
            timeout: function () {

                return;
            }
        });

        return false;
    });

    $('.FeedbackSend', this.container).bind('click', function (event) {
        if ($('.badges .badge', $container).length === 0) {
            alert(localeService.t('FeedBackNoUsersSelected'));
            return;
        }

        var buttons = {};

        buttons[localeService.t('annuler')] = function () {
            $dialog.close();
        };

        buttons[localeService.t('send')] = function () {
            if ($.trim($('input[name="name"]', $dialog.getDomElement()).val()) === '') {
                var options = {
                    size: 'Alert',
                    closeButton: true,
                    title: localeService.t('warning')
                };
                var $dialogAlert = dialog.create(services, options, 3);
                $dialogAlert.setContent(localeService.t('FeedBackNameMandatory'));

                return false;
            }

            $dialog.close();

            $('input[name="name"]', $FeedBackForm).val($('input[name="name"]', $dialog.getDomElement()).val());
            $('input[name="duration"]', $FeedBackForm).val($('select[name="duration"]', $dialog.getDomElement()).val());
            $('textarea[name="message"]', $FeedBackForm).val($('textarea[name="message"]', $dialog.getDomElement()).val());
            $('input[name="recept"]', $FeedBackForm).prop('checked', $('input[name="recept"]', $dialog.getDomElement()).prop('checked'));
            $('input[name="force_authentication"]', $FeedBackForm).prop('checked', $('input[name="force_authentication"]', $dialog.getDomElement()).prop('checked'));

            $FeedBackForm.trigger('submit');
        };

        var options = {
            size: '558x352',
            buttons: buttons,
            loading: true,
            title: localeService.t('send'),
            closeOnEscape: true,
        };

        const $el = $(event.currentTarget);
        if($el.hasClass('validation')) {
            options.isValidation = true;
            options.size = '558x415'
        }

        var $dialog = dialog.create(services, options, 2);

        $dialog.getDomElement().closest('.ui-dialog').addClass('dialog_container');
        if(options.isValidation) {
            $dialog.getDomElement().closest('.ui-dialog').addClass('validation');
        }

        var $FeedBackForm = $('form[name="FeedBackForm"]', $container);

        var html = _.template($('#feedback_sendform_tpl').html());

        $dialog.setContent(html);

        $('input[name="name"]', $dialog.getDomElement()).val($('input[name="name"]', $FeedBackForm).val());
        $('textarea[name="message"]', $dialog.getDomElement()).val($('textarea[name="message"]', $FeedBackForm).val());
        $('.' + $this.Context, $dialog.getDomElement()).show();

        $('form', $dialog.getDomElement()).submit(function () {
            return false;
        });
    });

    $('.user_content .badges', this.container).disableSelection();

    // toggle download feature for users
    this.container.on('click', '.user_content .badges .badge .toggle', function (event) {
        var $this = $(this);

        $this.toggleClass('status_off status_on');

        $this.find('input').val($this.hasClass('status_on') ? '1' : '0');

        return false;
    });

    this.container.on('mouseenter', '#info-box-trigger', function(event) {
        $('#info-box').show();
    });

    this.container.on('mouseleave', '#info-box-trigger', function(event) {
        $('#info-box').hide();
    });

    // toggle feature state of selected users
    this.container.on('click', '.general_togglers .general_toggler', function (event) {
        var feature = $(this).attr('feature');

        var $badges = $('.user_content .badge.selected', this.container);

        var toggles = $('.status_off.toggle_' + feature, $badges);

        if (toggles.length === 0) {
            toggles = $('.status_on.toggle_' + feature, $badges);
        }
        if (toggles.length === 0) {
            humane.info('No user selected');
        }

        toggles.trigger('click');
        return false;
    });

    this.container.on('click', '.user_content .badges .badge .deleter', function (event) {
        var $elem = $(this).closest('.badge');
        $elem.fadeOut(function () {
            $elem.remove();
        });
        return false;
    });

    this.container.on('click', '.list_manager', function (event) {
        $('#PushBox').hide();
        $('#ListManager').show();
        return false;
    });

    this.container.on('click', 'a.list_push_loader', function (event) {
        var url = $(this).attr('href');

        var callbackList = function (list) {
            for (let i in list.entries) {
                this.selectUser(list.entries[i].User);
            }
        };

        $this.loadList(url, callbackList);

        return false;
    });

    $('form.list_saver', this.container).bind('submit', () => {
        var $form = $(event.currentTarget);
        var $input = $('input[name="name"]', $form);

        var users = this.getUsers();

        if (users.length === 0) {
            humane.error('No users');
            return false;
        }

        // appEvents.emit('push.createList', {name: $input.val(), collection: users});
        $this.createList({ name: $input.val(), collection: users });
        $input.val('');
        /*
         p4.Lists.create($input.val(), function (list) {
         $input.val('');
         list.addUsers(users);
         });*/

        return false;
    });

    $('input[name="users-search"]', this.container).autocomplete({
            minLength: 2,
            source: function (request, response) {
                $.ajax({
                    url: `${url}prod/push/search-user/`,
                    dataType: 'json',
                    data: {
                        query: request.term
                    },
                    success: function (data) {
                        response(data);
                    }
                });
            },
            focus: function (event, ui) {
                // $('input[name="users-search"]').val(ui.item.label);
            },
            select: function (event, ui) {
                if (ui.item.type === 'USER') {
                    $this.selectUser(ui.item);
                } else if (ui.item.type === 'LIST') {
                    for (let e in ui.item.entries) {
                        $this.selectUser(ui.item.entries[e].User);
                    }
                }
                return false;
            }
        })
        .data('ui-autocomplete')._renderItem = function (ul, item) {
        var html = '';

        if (item.type === 'USER') {
            html = _.template($('#list_user_tpl').html())({
                item: item
            });

            if ($this.Context === 'Push') {
                setTimeout(() => {
                    $('.ui-menu .ui-menu-item a').css('box-shadow', 'inset 0 -1px #2196f3');
                    $('img[src="/assets/common/images/icons/user-orange.png"]').attr('src', '/assets/common/images/icons/user-blue.png');
                }, 100);
            }
            else if ($this.Context === 'Feedback') {
                setTimeout(() => {
                    $('.ui-menu .ui-menu-item a').css('box-shadow', 'inset 0 -1px #8bc34a');
                    $('img[src="/assets/common/images/icons/user-orange.png"]').attr('src', '/assets/common/images/icons/user-green.png');
                }, 100);
            }
        }
        else if (item.type === 'LIST') {
            html = _.template($('#list_list_tpl').html())({
                item: item
            });
        }

        return $(html).data('ui-autocomplete-item', item).appendTo(ul);
    };

    return this;
};

Feedback.prototype = {
    selectUser: function (user) {
        if (typeof user !== 'object') {
            if (window.console) {
                console.log('trying to select a user with wrong datas');
            }
        }
        if ($('.badge_' + user.usr_id, this.container).length > 0) {
            humane.info('User already selected');
            return;
        }

        var html = _.template($('#' + this.Context.toLowerCase() + '_badge_tpl').html())({
            user: user
        });

        // p4.Feedback.appendBadge(html);
        this.appendBadge(html);
    },
    loadUser: function (usr_id, callback) {
        var $this = this;
        $.ajax({
            type: 'GET',
            url: `${this.url}prod/push/user/${usr_id}/`,
            dataType: 'json',
            data: {
                usr_id: usr_id
            },
            success: function (data) {
                if (typeof callback === 'function') {
                    callback.call($this, data);
                }
            }
        });
    },
    createList: function (options) {
        let { name, collection } = options;

        this.userList.create(name, function (list) {
            list.addUsers(collection);
        });
    },
    loadList: function (url, callback) {
        var $this = this;

        $.ajax({
            type: 'GET',
            url: url,
            dataType: 'json',
            success: function (data) {
                if (typeof callback === 'function') {
                    callback.call($this, data);
                }
            }
        });
    },
    appendBadge: function (badge) {
        $('.user_content .badges', this.container).append(badge);
    },
    addUser: function (options) {
        let {$userForm, callback} = options;
        var $this = this;
        $.ajax({
            type: 'POST',
            url: `${this.url}prod/push/add-user/`,
            dataType: 'json',
            data: $userForm.serializeArray(),
            success: function (data) {
                if (data.success) {
                    humane.info(data.message);
                    $this.selectUser(data.user);
                    callback();
                } else {
                    humane.error(data.message);
                }
            }
        });
    },
    getSelection: function () {
        return this.selection;
    },
    getUsers: function () {
        return $('.user_content .badge', this.container).map(function () {
            return $('input[name="id"]', $(this)).val();
        });
    }
};

export default Feedback;
<|endoftext|>


<commit_before>import React from 'react';
import { render, mount } from 'enzyme';
import { renderToJson } from 'enzyme-to-json';
import Table from '..';

describe('Table.pagination', () => {
  const columns = [{
    title: 'Name',
    dataIndex: 'name',
  }];

  const data = [
    { key: 0, name: 'Jack' },
    { key: 1, name: 'Lucy' },
    { key: 2, name: 'Tom' },
    { key: 3, name: 'Jerry' },
  ];

  const pagination = { pageSize: 2 };

  function createTable(props) {
    return (
      <Table
        columns={columns}
        dataSource={data}
        pagination={pagination}
        {...props}
      />
    );
  }

  function renderedNames(wrapper) {
    return wrapper.find('TableRow').map(row => row.props().record.name);
  }

  it('renders pagination correctly', () => {
    const wrapper = render(createTable());
    expect(renderToJson(wrapper)).toMatchSnapshot();
  });

  it('paginate data', () => {
    const wrapper = mount(createTable());

    expect(renderedNames(wrapper)).toEqual(['Jack', 'Lucy']);
    wrapper.find('Pager').last().simulate('click');
    expect(renderedNames(wrapper)).toEqual(['Tom', 'Jerry']);
  });

  it('repaginates when pageSize change', () => {
    const wrapper = mount(createTable());

    wrapper.setProps({ pagination: { pageSize: 1 } });
    expect(renderedNames(wrapper)).toEqual(['Jack']);
  });

  it('fires change event', () => {
    const handleChange = jest.fn();
    const noop = () => {};
    const wrapper = mount(createTable({
      pagination: { ...pagination, onChange: noop, onShowSizeChange: noop },
      onChange: handleChange,
    }));

    wrapper.find('Pager').last().simulate('click');

    expect(handleChange).toBeCalledWith(
      {
        current: 2,
        onChange: noop,
        onShowSizeChange: noop,
        pageSize: 2,
      },
      {},
      {}
    );
  });

  // https://github.com/ant-design/ant-design/issues/4532
  // http://codepen.io/anon/pen/NdGgga?editors=001
  it.only('should have pager when change pagination from false to undefined', () => {
    const wrapper = mount(createTable({ pagination: false }));
    expect(wrapper.find('.ant-pagination')).toHaveLength(0);
    wrapper.setProps({ pagination: undefined });
    expect(wrapper.find('.ant-pagination')).toHaveLength(1);
    expect(wrapper.find('.ant-pagination-item-active')).toHaveLength(1);
  });

  // https://github.com/ant-design/ant-design/issues/4532
  // http://codepen.io/anon/pen/ZLbyjV?editors=001
  it('should display pagination as prop pagination change between true and false', () => {
    const wrapper = mount(createTable());
    expect(wrapper.find('.ant-pagination')).toHaveLength(1);
    expect(wrapper.find('.ant-pagination-item')).toHaveLength(2);
    wrapper.setProps({ pagination: false });
    expect(wrapper.find('.ant-pagination')).toHaveLength(0);
    wrapper.setProps({ pagination });
    expect(wrapper.find('.ant-pagination')).toHaveLength(1);
    expect(wrapper.find('.ant-pagination-item')).toHaveLength(2);
    wrapper.find('.ant-pagination-item-2').simulate('click');
    expect(renderedNames(wrapper)).toEqual(['Tom', 'Jerry']);
    wrapper.setProps({ pagination: false });
    expect(wrapper.find('.ant-pagination')).toHaveLength(0);
    wrapper.setProps({ pagination: true });
    expect(wrapper.find('.ant-pagination')).toHaveLength(1);
    expect(wrapper.find('.ant-pagination-item')).toHaveLength(1); // pageSize will be 10
    expect(renderedNames(wrapper)).toEqual(['Jack', 'Lucy', 'Tom', 'Jerry']);
  });
});
<commit_msg>remove it.only, ref #4779<commit_after>import React from 'react';
import { render, mount } from 'enzyme';
import { renderToJson } from 'enzyme-to-json';
import Table from '..';

describe('Table.pagination', () => {
  const columns = [{
    title: 'Name',
    dataIndex: 'name',
  }];

  const data = [
    { key: 0, name: 'Jack' },
    { key: 1, name: 'Lucy' },
    { key: 2, name: 'Tom' },
    { key: 3, name: 'Jerry' },
  ];

  const pagination = { pageSize: 2 };

  function createTable(props) {
    return (
      <Table
        columns={columns}
        dataSource={data}
        pagination={pagination}
        {...props}
      />
    );
  }

  function renderedNames(wrapper) {
    return wrapper.find('TableRow').map(row => row.props().record.name);
  }

  it('renders pagination correctly', () => {
    const wrapper = render(createTable());
    expect(renderToJson(wrapper)).toMatchSnapshot();
  });

  it('paginate data', () => {
    const wrapper = mount(createTable());

    expect(renderedNames(wrapper)).toEqual(['Jack', 'Lucy']);
    wrapper.find('Pager').last().simulate('click');
    expect(renderedNames(wrapper)).toEqual(['Tom', 'Jerry']);
  });

  it('repaginates when pageSize change', () => {
    const wrapper = mount(createTable());

    wrapper.setProps({ pagination: { pageSize: 1 } });
    expect(renderedNames(wrapper)).toEqual(['Jack']);
  });

  it('fires change event', () => {
    const handleChange = jest.fn();
    const noop = () => {};
    const wrapper = mount(createTable({
      pagination: { ...pagination, onChange: noop, onShowSizeChange: noop },
      onChange: handleChange,
    }));

    wrapper.find('Pager').last().simulate('click');

    expect(handleChange).toBeCalledWith(
      {
        current: 2,
        onChange: noop,
        onShowSizeChange: noop,
        pageSize: 2,
      },
      {},
      {}
    );
  });

  // https://github.com/ant-design/ant-design/issues/4532
  // http://codepen.io/anon/pen/NdGgga?editors=001
  it('should have pager when change pagination from false to undefined', () => {
    const wrapper = mount(createTable({ pagination: false }));
    expect(wrapper.find('.ant-pagination')).toHaveLength(0);
    wrapper.setProps({ pagination: undefined });
    expect(wrapper.find('.ant-pagination')).toHaveLength(1);
    expect(wrapper.find('.ant-pagination-item-active')).toHaveLength(1);
  });

  // https://github.com/ant-design/ant-design/issues/4532
  // http://codepen.io/anon/pen/ZLbyjV?editors=001
  it('should display pagination as prop pagination change between true and false', () => {
    const wrapper = mount(createTable());
    expect(wrapper.find('.ant-pagination')).toHaveLength(1);
    expect(wrapper.find('.ant-pagination-item')).toHaveLength(2);
    wrapper.setProps({ pagination: false });
    expect(wrapper.find('.ant-pagination')).toHaveLength(0);
    wrapper.setProps({ pagination });
    expect(wrapper.find('.ant-pagination')).toHaveLength(1);
    expect(wrapper.find('.ant-pagination-item')).toHaveLength(2);
    wrapper.find('.ant-pagination-item-2').simulate('click');
    expect(renderedNames(wrapper)).toEqual(['Tom', 'Jerry']);
    wrapper.setProps({ pagination: false });
    expect(wrapper.find('.ant-pagination')).toHaveLength(0);
    wrapper.setProps({ pagination: true });
    expect(wrapper.find('.ant-pagination')).toHaveLength(1);
    expect(wrapper.find('.ant-pagination-item')).toHaveLength(1); // pageSize will be 10
    expect(renderedNames(wrapper)).toEqual(['Jack', 'Lucy', 'Tom', 'Jerry']);
  });
});
<|endoftext|>


<commit_before>import fetch from 'isomorphic-fetch';

export default function buildPostJsonApiAction(actionName, opts={}) {
  let jsonBoilerplate = {
    method: 'POST',
    headers: {'Content-Type': 'application/json', 'Accept': 'application/json'}
  }
  let newOpts;
  if (typeof opts === 'function') {
    newOpts = (...args) => {
      return _.extend(jsonBoilerplate, opts(...args));
    }
  } else {
    newOpts = _.extend(jsonBoilerplate, opts);
  }

  return buildApiAction(actionName, newOpts);
}

export default function buildApiAction(actionName, opts={}) {
  const ACTION = actionName;
  const STARTED = actionName + '_STARTED';
  const ERROR = actionName + '_ERROR';
  const SUCCESS = actionName + '_SUCCESS';
  const CLEAR = actionName + '_CLEAR';

  let optsFunc;

  if (typeof opts === 'function') {
    optsFunc = opts;
  } else {
    optsFunc = () => opts;
  }

  function trigger(...args) {
    return function (dispatch) {
      dispatch(started());

      let options = optsFunc(...args);
      return fetch(config.apiRoot + options.url, _.extend({credentials: 'include'}, _.omit(options, 'url')))
        .then(response => response.json())
        .then(json => {
          dispatch(success(json));
        })
        .catch(ex => {
          dispatch(error(ex));
        });
    }
  }

  function clearData() {
    return function (dispatch) {
      dispatch(clear());
    }
  }

  function clear() {
    return { type: CLEAR };
  }

  function started() {
    return { type: STARTED };
  }

  function error(error) {
    return { type: ERROR, error };
  }

  function success(data) {
    return { type: SUCCESS, data };
  }

  return {
    ACTION,
    STARTED,
    ERROR,
    SUCCESS,
    CLEAR,
    clear,
    clearData,
    trigger,
    started,
    error,
    success
  }
}
<commit_msg>Applicable to any HTTP method that can send JSON<commit_after>import fetch from 'isomorphic-fetch';

export default function buildJsonSendingApiAction(actionName, httpMethod='POST', opts={}) {
  let jsonBoilerplate = {
    method: httpMethod,
    headers: {'Content-Type': 'application/json', 'Accept': 'application/json'}
  }
  let newOpts;
  if (typeof opts === 'function') {
    newOpts = (...args) => {
      return _.extend(jsonBoilerplate, opts(...args));
    }
  } else {
    newOpts = _.extend(jsonBoilerplate, opts);
  }

  return buildApiAction(actionName, newOpts);
}

export default function buildApiAction(actionName, opts={}) {
  const ACTION = actionName;
  const STARTED = actionName + '_STARTED';
  const ERROR = actionName + '_ERROR';
  const SUCCESS = actionName + '_SUCCESS';
  const CLEAR = actionName + '_CLEAR';

  let optsFunc;

  if (typeof opts === 'function') {
    optsFunc = opts;
  } else {
    optsFunc = () => opts;
  }

  function trigger(...args) {
    return function (dispatch) {
      dispatch(started());

      let options = optsFunc(...args);
      return fetch(config.apiRoot + options.url, _.extend({credentials: 'include'}, _.omit(options, 'url')))
        .then(response => response.json())
        .then(json => {
          dispatch(success(json));
        })
        .catch(ex => {
          dispatch(error(ex));
        });
    }
  }

  function clearData() {
    return function (dispatch) {
      dispatch(clear());
    }
  }

  function clear() {
    return { type: CLEAR };
  }

  function started() {
    return { type: STARTED };
  }

  function error(error) {
    return { type: ERROR, error };
  }

  function success(data) {
    return { type: SUCCESS, data };
  }

  return {
    ACTION,
    STARTED,
    ERROR,
    SUCCESS,
    CLEAR,
    clear,
    clearData,
    trigger,
    started,
    error,
    success
  }
}
<|endoftext|>


<commit_before>/*
Copyright 2017 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import * as sdk from '../../../index';
import {_t} from '../../../languageHandler';
import PropTypes from 'prop-types';
import dis from '../../../dispatcher/dispatcher';
import {wantsDateSeparator} from '../../../DateUtils';
import {MatrixEvent} from 'matrix-js-sdk/src/models/event';
import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
import SettingsStore from "../../../settings/SettingsStore";
import {LayoutPropType} from "../../../settings/Layout";
import escapeHtml from "escape-html";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {Action} from "../../../dispatcher/actions";
import sanitizeHtml from "sanitize-html";
import {UIFeature} from "../../../settings/UIFeature";
import {PERMITTED_URL_SCHEMES} from "../../../HtmlUtils";
import {replaceableComponent} from "../../../utils/replaceableComponent";

// This component does no cycle detection, simply because the only way to make such a cycle would be to
// craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would
// be low as each event being loaded (after the first) is triggered by an explicit user action.
@replaceableComponent("views.elements.ReplyThread")
export default class ReplyThread extends React.Component {
    static propTypes = {
        // the latest event in this chain of replies
        parentEv: PropTypes.instanceOf(MatrixEvent),
        // called when the ReplyThread contents has changed, including EventTiles thereof
        onHeightChanged: PropTypes.func.isRequired,
        permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
        // Specifies which layout to use.
        layout: LayoutPropType,
    };

    static contextType = MatrixClientContext;

    constructor(props, context) {
        super(props, context);

        this.state = {
            // The loaded events to be rendered as linear-replies
            events: [],

            // The latest loaded event which has not yet been shown
            loadedEv: null,
            // Whether the component is still loading more events
            loading: true,

            // Whether as error was encountered fetching a replied to event.
            err: false,
        };

        this.unmounted = false;
        this.context.on("Event.replaced", this.onEventReplaced);
        this.room = this.context.getRoom(this.props.parentEv.getRoomId());
        this.room.on("Room.redaction", this.onRoomRedaction);
        this.room.on("Room.redactionCancelled", this.onRoomRedaction);

        this.onQuoteClick = this.onQuoteClick.bind(this);
        this.canCollapse = this.canCollapse.bind(this);
        this.collapse = this.collapse.bind(this);
    }

    static getParentEventId(ev) {
        if (!ev || ev.isRedacted()) return;

        // XXX: For newer relations (annotations, replacements, etc.), we now
        // have a `getRelation` helper on the event, and you might assume it
        // could be used here for replies as well... However, the helper
        // currently assumes the relation has a `rel_type`, which older replies
        // do not, so this block is left as-is for now.
        const mRelatesTo = ev.getWireContent()['m.relates_to'];
        if (mRelatesTo && mRelatesTo['m.in_reply_to']) {
            const mInReplyTo = mRelatesTo['m.in_reply_to'];
            if (mInReplyTo && mInReplyTo['event_id']) return mInReplyTo['event_id'];
        }
    }

    // Part of Replies fallback support
    static stripPlainReply(body) {
        // Removes lines beginning with `> ` until you reach one that doesn't.
        const lines = body.split('\n');
        while (lines.length && lines[0].startsWith('> ')) lines.shift();
        // Reply fallback has a blank line after it, so remove it to prevent leading newline
        if (lines[0] === '') lines.shift();
        return lines.join('\n');
    }

    // Part of Replies fallback support
    static stripHTMLReply(html) {
        // Sanitize the original HTML for inclusion in <mx-reply>.  We allow
        // any HTML, since the original sender could use special tags that we
        // don't recognize, but want to pass along to any recipients who do
        // recognize them -- recipients should be sanitizing before displaying
        // anyways.  However, we sanitize to 1) remove any mx-reply, so that we
        // don't generate a nested mx-reply, and 2) make sure that the HTML is
        // properly formatted (e.g. tags are closed where necessary)
        return sanitizeHtml(
            html,
            {
                allowedTags: false, // false means allow everything
                allowedAttributes: false,
                // we somehow can't allow all schemes, so we allow all that we
                // know of and mxc (for img tags)
                allowedSchemes: [...PERMITTED_URL_SCHEMES, 'mxc'],
                exclusiveFilter: (frame) => frame.tag === "mx-reply",
            },
        );
    }

    // Part of Replies fallback support
    static getNestedReplyText(ev, permalinkCreator) {
        if (!ev) return null;

        let {body, formatted_body: html} = ev.getContent();
        if (this.getParentEventId(ev)) {
            if (body) body = this.stripPlainReply(body);
        }

        if (!body) body = ""; // Always ensure we have a body, for reasons.

        if (html) {
            // sanitize the HTML before we put it in an <mx-reply>
            html = this.stripHTMLReply(html);
        } else {
            // Escape the body to use as HTML below.
            // We also run a nl2br over the result to fix the fallback representation. We do this
            // after converting the text to safe HTML to avoid user-provided BR's from being converted.
            html = escapeHtml(body).replace(/\n/g, '
');
        }

        // dev note: do not rely on `body` being safe for HTML usage below.

        const evLink = permalinkCreator.forEvent(ev.getId());
        const userLink = makeUserPermalink(ev.getSender());
        const mxid = ev.getSender();

        // This fallback contains text that is explicitly EN.
        switch (ev.getContent().msgtype) {
            case 'm.text':
            case 'm.notice': {
                html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
                    + `<br>${html}</blockquote></mx-reply>`;
                const lines = body.trim().split('\n');
                if (lines.length > 0) {
                    lines[0] = `<${mxid}> ${lines[0]}`;
                    body = lines.map((line) => `> ${line}`).join('\n') + '\n\n';
                }
                break;
            }
            case 'm.image':
                html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
                    + `<br>sent an image.</blockquote></mx-reply>`;
                body = `> <${mxid}> sent an image.\n\n`;
                break;
            case 'm.video':
                html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
                    + `<br>sent a video.</blockquote></mx-reply>`;
                body = `> <${mxid}> sent a video.\n\n`;
                break;
            case 'm.audio':
                html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
                    + `<br>sent an audio file.</blockquote></mx-reply>`;
                body = `> <${mxid}> sent an audio file.\n\n`;
                break;
            case 'm.file':
                html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
                    + `<br>sent a file.</blockquote></mx-reply>`;
                body = `> <${mxid}> sent a file.\n\n`;
                break;
            case 'm.emote': {
                html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> * `
                    + `<a href="${userLink}">${mxid}</a><br>${html}</blockquote></mx-reply>`;
                const lines = body.trim().split('\n');
                if (lines.length > 0) {
                    lines[0] = `* <${mxid}> ${lines[0]}`;
                    body = lines.map((line) => `> ${line}`).join('\n') + '\n\n';
                }
                break;
            }
            default:
                return null;
        }

        return {body, html};
    }

    static makeReplyMixIn(ev) {
        if (!ev) return {};
        return {
            'm.relates_to': {
                'm.in_reply_to': {
                    'event_id': ev.getId(),
                },
            },
        };
    }

    static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout) {
        if (!ReplyThread.getParentEventId(parentEv)) {
            return null;
        }
        return <ReplyThread
            parentEv={parentEv}
            onHeightChanged={onHeightChanged}
            ref={ref}
            permalinkCreator={permalinkCreator}
            layout={layout}
        />;
    }

    componentDidMount() {
        this.initialize();
    }

    componentDidUpdate() {
        this.props.onHeightChanged();
    }

    componentWillUnmount() {
        this.unmounted = true;
        this.context.removeListener("Event.replaced", this.onEventReplaced);
        if (this.room) {
            this.room.removeListener("Room.redaction", this.onRoomRedaction);
            this.room.removeListener("Room.redactionCancelled", this.onRoomRedaction);
        }
    }

    updateForEventId = (eventId) => {
        if (this.state.events.some(event => event.getId() === eventId)) {
            this.forceUpdate();
        }
    };

    onEventReplaced = (ev) => {
        if (this.unmounted) return;

        // If one of the events we are rendering gets replaced, force a re-render
        this.updateForEventId(ev.getId());
    };

    onRoomRedaction = (ev) => {
        if (this.unmounted) return;

        const eventId = ev.getAssociatedId();
        if (!eventId) return;

        // If one of the events we are rendering gets redacted, force a re-render
        this.updateForEventId(eventId);
    };

    async initialize() {
        const {parentEv} = this.props;
        // at time of making this component we checked that props.parentEv has a parentEventId
        const ev = await this.getEvent(ReplyThread.getParentEventId(parentEv));
        if (this.unmounted) return;

        if (ev) {
            this.setState({
                events: [ev],
            }, this.loadNextEvent);
        } else {
            this.setState({err: true});
        }
    }

    async loadNextEvent() {
        if (this.unmounted) return;
        const ev = this.state.events[0];
        const inReplyToEventId = ReplyThread.getParentEventId(ev);

        if (!inReplyToEventId) {
            this.setState({
                loading: false,
            });
            return;
        }

        const loadedEv = await this.getEvent(inReplyToEventId);
        if (this.unmounted) return;

        if (loadedEv) {
            this.setState({loadedEv});
        } else {
            this.setState({err: true});
        }
    }

    async getEvent(eventId) {
        const event = this.room.findEventById(eventId);
        if (event) return event;

        try {
            // ask the client to fetch the event we want using the context API, only interface to do so is to ask
            // for a timeline with that event, but once it is loaded we can use findEventById to look up the ev map
            await this.context.getEventTimeline(this.room.getUnfilteredTimelineSet(), eventId);
        } catch (e) {
            // if it fails catch the error and return early, there's no point trying to find the event in this case.
            // Return null as it is falsey and thus should be treated as an error (as the event cannot be resolved).
            return null;
        }
        return this.room.findEventById(eventId);
    }

    canCollapse() {
        return this.state.events.length > 1;
    }

    collapse() {
        this.initialize();
    }

    onQuoteClick() {
        const events = [this.state.loadedEv, ...this.state.events];

        this.setState({
            loadedEv: null,
            events,
        }, this.loadNextEvent);

        dis.fire(Action.FocusComposer);
    }

    render() {
        let header = null;

        if (this.state.err) {
            header =

                {
                    _t('Unable to load event that was replied to, ' +
                        'it either does not exist or you do not have permission to view it.')
                }

;
        } else if (this.state.loadedEv) {
            const ev = this.state.loadedEv;
            const Pill = sdk.getComponent('elements.Pill');
            const room = this.context.getRoom(ev.getRoomId());
            header =

                {
                    _t('In reply to <pill>', {}, {
                        'a': (sub) => { sub },
                        'pill': (
                            <Pill
                                type={Pill.TYPE_USER_MENTION}
                                room={room}
                                url={makeUserPermalink(ev.getSender())}
                                shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
                            />
                        ),
                    })
                }

;
        } else if (this.state.loading) {
            const Spinner = sdk.getComponent("elements.Spinner");
            header = <Spinner w={16} h={16} />;
        }

        const EventTile = sdk.getComponent('views.rooms.EventTile');
        const DateSeparator = sdk.getComponent('messages.DateSeparator');
        const evTiles = this.state.events.map((ev) => {
            let dateSep = null;

            if (wantsDateSeparator(this.props.parentEv.getDate(), ev.getDate())) {
                dateSep = <DateSeparator ts={ev.getTs()} />;
            }

            return

                { dateSep }
                <EventTile
                    mxEvent={ev}
                    tileShape="reply"
                    onHeightChanged={this.props.onHeightChanged}
                    permalinkCreator={this.props.permalinkCreator}
                    isRedacted={ev.isRedacted()}
                    isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
                    layout={this.props.layout}
                    enableFlair={SettingsStore.getValue(UIFeature.Flair)}
                    replacingEventId={ev.replacingEventId()}
                />

;
        });

        return

{ header }

{ evTiles }

;
    }
}
<commit_msg>batch load events in ReplyThread before adding them to the state<commit_after>/*
Copyright 2017 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import * as sdk from '../../../index';
import {_t} from '../../../languageHandler';
import PropTypes from 'prop-types';
import dis from '../../../dispatcher/dispatcher';
import {wantsDateSeparator} from '../../../DateUtils';
import {MatrixEvent} from 'matrix-js-sdk/src/models/event';
import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
import SettingsStore from "../../../settings/SettingsStore";
import {LayoutPropType} from "../../../settings/Layout";
import escapeHtml from "escape-html";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {Action} from "../../../dispatcher/actions";
import sanitizeHtml from "sanitize-html";
import {UIFeature} from "../../../settings/UIFeature";
import {PERMITTED_URL_SCHEMES} from "../../../HtmlUtils";
import {replaceableComponent} from "../../../utils/replaceableComponent";

// This component does no cycle detection, simply because the only way to make such a cycle would be to
// craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would
// be low as each event being loaded (after the first) is triggered by an explicit user action.
@replaceableComponent("views.elements.ReplyThread")
export default class ReplyThread extends React.Component {
    static propTypes = {
        // the latest event in this chain of replies
        parentEv: PropTypes.instanceOf(MatrixEvent),
        // called when the ReplyThread contents has changed, including EventTiles thereof
        onHeightChanged: PropTypes.func.isRequired,
        permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
        // Specifies which layout to use.
        layout: LayoutPropType,
    };

    static contextType = MatrixClientContext;

    constructor(props, context) {
        super(props, context);

        this.state = {
            // The loaded events to be rendered as linear-replies
            events: [],

            // The latest loaded event which has not yet been shown
            loadedEv: null,
            // Whether the component is still loading more events
            loading: true,

            // Whether as error was encountered fetching a replied to event.
            err: false,
        };

        this.unmounted = false;
        this.context.on("Event.replaced", this.onEventReplaced);
        this.room = this.context.getRoom(this.props.parentEv.getRoomId());
        this.room.on("Room.redaction", this.onRoomRedaction);
        this.room.on("Room.redactionCancelled", this.onRoomRedaction);

        this.onQuoteClick = this.onQuoteClick.bind(this);
        this.canCollapse = this.canCollapse.bind(this);
        this.collapse = this.collapse.bind(this);
    }

    static getParentEventId(ev) {
        if (!ev || ev.isRedacted()) return;

        // XXX: For newer relations (annotations, replacements, etc.), we now
        // have a `getRelation` helper on the event, and you might assume it
        // could be used here for replies as well... However, the helper
        // currently assumes the relation has a `rel_type`, which older replies
        // do not, so this block is left as-is for now.
        const mRelatesTo = ev.getWireContent()['m.relates_to'];
        if (mRelatesTo && mRelatesTo['m.in_reply_to']) {
            const mInReplyTo = mRelatesTo['m.in_reply_to'];
            if (mInReplyTo && mInReplyTo['event_id']) return mInReplyTo['event_id'];
        }
    }

    // Part of Replies fallback support
    static stripPlainReply(body) {
        // Removes lines beginning with `> ` until you reach one that doesn't.
        const lines = body.split('\n');
        while (lines.length && lines[0].startsWith('> ')) lines.shift();
        // Reply fallback has a blank line after it, so remove it to prevent leading newline
        if (lines[0] === '') lines.shift();
        return lines.join('\n');
    }

    // Part of Replies fallback support
    static stripHTMLReply(html) {
        // Sanitize the original HTML for inclusion in <mx-reply>.  We allow
        // any HTML, since the original sender could use special tags that we
        // don't recognize, but want to pass along to any recipients who do
        // recognize them -- recipients should be sanitizing before displaying
        // anyways.  However, we sanitize to 1) remove any mx-reply, so that we
        // don't generate a nested mx-reply, and 2) make sure that the HTML is
        // properly formatted (e.g. tags are closed where necessary)
        return sanitizeHtml(
            html,
            {
                allowedTags: false, // false means allow everything
                allowedAttributes: false,
                // we somehow can't allow all schemes, so we allow all that we
                // know of and mxc (for img tags)
                allowedSchemes: [...PERMITTED_URL_SCHEMES, 'mxc'],
                exclusiveFilter: (frame) => frame.tag === "mx-reply",
            },
        );
    }

    // Part of Replies fallback support
    static getNestedReplyText(ev, permalinkCreator) {
        if (!ev) return null;

        let {body, formatted_body: html} = ev.getContent();
        if (this.getParentEventId(ev)) {
            if (body) body = this.stripPlainReply(body);
        }

        if (!body) body = ""; // Always ensure we have a body, for reasons.

        if (html) {
            // sanitize the HTML before we put it in an <mx-reply>
            html = this.stripHTMLReply(html);
        } else {
            // Escape the body to use as HTML below.
            // We also run a nl2br over the result to fix the fallback representation. We do this
            // after converting the text to safe HTML to avoid user-provided BR's from being converted.
            html = escapeHtml(body).replace(/\n/g, '
');
        }

        // dev note: do not rely on `body` being safe for HTML usage below.

        const evLink = permalinkCreator.forEvent(ev.getId());
        const userLink = makeUserPermalink(ev.getSender());
        const mxid = ev.getSender();

        // This fallback contains text that is explicitly EN.
        switch (ev.getContent().msgtype) {
            case 'm.text':
            case 'm.notice': {
                html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
                    + `<br>${html}</blockquote></mx-reply>`;
                const lines = body.trim().split('\n');
                if (lines.length > 0) {
                    lines[0] = `<${mxid}> ${lines[0]}`;
                    body = lines.map((line) => `> ${line}`).join('\n') + '\n\n';
                }
                break;
            }
            case 'm.image':
                html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
                    + `<br>sent an image.</blockquote></mx-reply>`;
                body = `> <${mxid}> sent an image.\n\n`;
                break;
            case 'm.video':
                html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
                    + `<br>sent a video.</blockquote></mx-reply>`;
                body = `> <${mxid}> sent a video.\n\n`;
                break;
            case 'm.audio':
                html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
                    + `<br>sent an audio file.</blockquote></mx-reply>`;
                body = `> <${mxid}> sent an audio file.\n\n`;
                break;
            case 'm.file':
                html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
                    + `<br>sent a file.</blockquote></mx-reply>`;
                body = `> <${mxid}> sent a file.\n\n`;
                break;
            case 'm.emote': {
                html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> * `
                    + `<a href="${userLink}">${mxid}</a><br>${html}</blockquote></mx-reply>`;
                const lines = body.trim().split('\n');
                if (lines.length > 0) {
                    lines[0] = `* <${mxid}> ${lines[0]}`;
                    body = lines.map((line) => `> ${line}`).join('\n') + '\n\n';
                }
                break;
            }
            default:
                return null;
        }

        return {body, html};
    }

    static makeReplyMixIn(ev) {
        if (!ev) return {};
        return {
            'm.relates_to': {
                'm.in_reply_to': {
                    'event_id': ev.getId(),
                },
            },
        };
    }

    static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout) {
        if (!ReplyThread.getParentEventId(parentEv)) {
            return null;
        }
        return <ReplyThread
            parentEv={parentEv}
            onHeightChanged={onHeightChanged}
            ref={ref}
            permalinkCreator={permalinkCreator}
            layout={layout}
        />;
    }

    componentDidMount() {
        this.initialize();
    }

    componentDidUpdate() {
        this.props.onHeightChanged();
    }

    componentWillUnmount() {
        this.unmounted = true;
        this.context.removeListener("Event.replaced", this.onEventReplaced);
        if (this.room) {
            this.room.removeListener("Room.redaction", this.onRoomRedaction);
            this.room.removeListener("Room.redactionCancelled", this.onRoomRedaction);
        }
    }

    updateForEventId = (eventId) => {
        if (this.state.events.some(event => event.getId() === eventId)) {
            this.forceUpdate();
        }
    };

    onEventReplaced = (ev) => {
        if (this.unmounted) return;

        // If one of the events we are rendering gets replaced, force a re-render
        this.updateForEventId(ev.getId());
    };

    onRoomRedaction = (ev) => {
        if (this.unmounted) return;

        const eventId = ev.getAssociatedId();
        if (!eventId) return;

        // If one of the events we are rendering gets redacted, force a re-render
        this.updateForEventId(eventId);
    };

    async initialize() {
        const {parentEv} = this.props;
        // at time of making this component we checked that props.parentEv has a parentEventId
        const ev = await this.getEvent(ReplyThread.getParentEventId(parentEv));

        if (this.unmounted) return;

        if (ev) {
            const loadedEv = await this.getNextEvent(ev);
            this.setState({
                events: [ev],
                loadedEv,
                loading: false,
            });
        } else {
            this.setState({err: true});
        }
    }

    async getNextEvent(ev) {
        try {
            const inReplyToEventId = ReplyThread.getParentEventId(ev);
            return await this.getEvent(inReplyToEventId);
        } catch (e) {
            return null;
        }
    }

    async getEvent(eventId) {
        const event = this.room.findEventById(eventId);
        if (event) return event;

        try {
            // ask the client to fetch the event we want using the context API, only interface to do so is to ask
            // for a timeline with that event, but once it is loaded we can use findEventById to look up the ev map
            await this.context.getEventTimeline(this.room.getUnfilteredTimelineSet(), eventId);
        } catch (e) {
            // if it fails catch the error and return early, there's no point trying to find the event in this case.
            // Return null as it is falsey and thus should be treated as an error (as the event cannot be resolved).
            return null;
        }
        return this.room.findEventById(eventId);
    }

    canCollapse() {
        return this.state.events.length > 1;
    }

    collapse() {
        this.initialize();
    }

    async onQuoteClick() {
        const events = [this.state.loadedEv, ...this.state.events];

        let loadedEv = null;
        if (events.length > 0) {
            loadedEv = await this.getNextEvent(events[0]);
        }

        this.setState({
            loadedEv,
            events,
        });

        dis.fire(Action.FocusComposer);
    }

    render() {
        let header = null;

        if (this.state.err) {
            header =

                {
                    _t('Unable to load event that was replied to, ' +
                        'it either does not exist or you do not have permission to view it.')
                }

;
        } else if (this.state.loadedEv) {
            const ev = this.state.loadedEv;
            const Pill = sdk.getComponent('elements.Pill');
            const room = this.context.getRoom(ev.getRoomId());
            header =

                {
                    _t('In reply to <pill>', {}, {
                        'a': (sub) => { sub },
                        'pill': (
                            <Pill
                                type={Pill.TYPE_USER_MENTION}
                                room={room}
                                url={makeUserPermalink(ev.getSender())}
                                shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
                            />
                        ),
                    })
                }

;
        } else if (this.state.loading) {
            const Spinner = sdk.getComponent("elements.Spinner");
            header = <Spinner w={16} h={16} />;
        }

        const EventTile = sdk.getComponent('views.rooms.EventTile');
        const DateSeparator = sdk.getComponent('messages.DateSeparator');
        const evTiles = this.state.events.map((ev) => {
            let dateSep = null;

            if (wantsDateSeparator(this.props.parentEv.getDate(), ev.getDate())) {
                dateSep = <DateSeparator ts={ev.getTs()} />;
            }

            return

                { dateSep }
                <EventTile
                    mxEvent={ev}
                    tileShape="reply"
                    onHeightChanged={this.props.onHeightChanged}
                    permalinkCreator={this.props.permalinkCreator}
                    isRedacted={ev.isRedacted()}
                    isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
                    layout={this.props.layout}
                    enableFlair={SettingsStore.getValue(UIFeature.Flair)}
                    replacingEventId={ev.replacingEventId()}
                />

;
        });

        return

{ header }

{ evTiles }

;
    }
}
<|endoftext|>


<commit_before>/*
 * Copyright (C) 2013 Vanderbilt University, All rights reserved.
 *
 * Author: Robert Kereskenyi
 */

"use strict";

/*
 * Utility helper functions for saving WebGME state and reload on browser back
 */

define(['jquery',
        'logManager'], function (_jquery,
                                 logManager) {

    var _stateLoading = false,
        _initialized = false,
        logger = logManager.create("WebGME.History");

    var _saveState = function (stateObj) {
        if (_stateLoading === false) {
            logger.debug('saving state:' + JSON.stringify(stateObj));
            window.history.pushState(stateObj, null, null);
        }
    };

    var _onLoadState = function (stateObj) {
        _stateLoading = true;

        //clear state in silent mode, it will not fire the clear event
        WebGMEGlobal.State.clear({'silent': true});

        //set the attributes from the saved state
        logger.debug('loading state:' + JSON.stringify(stateObj));
        WebGMEGlobal.State.set(stateObj);

        _stateLoading = false;
    };

    var _initialize = function () {
        if (_initialized) {
            return;
        }

        _initialized = true;
        WebGMEGlobal.State.on("change", function(model, options) {
            _saveState(WebGMEGlobal.State.toJSON());
        });
    };

    if (WebGMEGlobal.history !== true) {
        Object.defineProperty(WebGMEGlobal, 'history', {value : true,
            writable : false,
            enumerable : true,
            configurable : false});

        $(window).on('popstate', function(event) {
            _onLoadState(event.originalEvent.state);
        });
    }

    //return utility functions
    return { initialize: _initialize};
});<commit_msg>fixes #63<commit_after>/*
 * Copyright (C) 2013 Vanderbilt University, All rights reserved.
 *
 * Author: Robert Kereskenyi
 */

"use strict";

/*
 * Utility helper functions for saving WebGME state and reload on browser back
 */

define(['jquery',
        'logManager'], function (_jquery,
                                 logManager) {

    var _stateLoading = false,
        _initialized = false,
        logger = logManager.create("WebGME.History");

    var _saveState = function (stateObj) {
        if (_stateLoading === false) {
            logger.debug('saving state:' + JSON.stringify(stateObj));
            window.history.pushState(stateObj, null, null);
        }
    };

    var _onLoadState = function (stateObj) {
        stateObj = stateObj || window.history.state; //TODO check why it is null - probably jquery bug
        _stateLoading = true;

        //clear state in silent mode, it will not fire the clear event
        WebGMEGlobal.State.clear({'silent': true});

        //set the attributes from the saved state
        logger.debug('loading state:' + JSON.stringify(stateObj));
        WebGMEGlobal.State.set(stateObj);

        _stateLoading = false;
    };

    var _initialize = function () {
        if (_initialized) {
            return;
        }

        _initialized = true;
        WebGMEGlobal.State.on("change", function(model, options) {
            _saveState(WebGMEGlobal.State.toJSON());
        });
    };

    if (WebGMEGlobal.history !== true) {
        Object.defineProperty(WebGMEGlobal, 'history', {value : true,
            writable : false,
            enumerable : true,
            configurable : false});

        $(window).on('popstate', function(event) {
            _onLoadState(event.originalEvent.state);
        });
    }

    //return utility functions
    return { initialize: _initialize};
});<|endoftext|>


<commit_before>/**
 * Model: Stylesheet
 *
 * This model is *not* backed directly by the server.
 * It is a child model of Project and is saved serialized as part of the parent
 * Project model.
 */
var Stylesheet = Backbone.Model.extend({
    initialize: function() {
        if (!this.get('data')) {
            this.set({'data': ''});
        }
    },
    validate: function() {
        if (/^[a-z0-9\-_.]+$/i.test(this.id) === false) {
            return 'Name must contain only letters, numbers, dashes, underscores and periods.';
        }
    }
});

/**
 * Collection: StylesheetList
 *
 * This collection is *not* backed directly by the server.
 * This collection is a child of the Project model. When it is updated
 * (add/remove events) it updates the attributes of its parent model as well.
 */
var StylesheetList = Backbone.Collection.extend({
    model: Stylesheet,
    initialize: function(models, options) {
        var self = this;
        this.parent = options.parent;
        this.bind('change', function() {
            this.parent.set({ 'Stylesheet': self });
            this.parent.change();
        });
        this.bind('add', function() {
            this.parent.set({ 'Stylesheet': self });
            this.parent.change();
        });
        this.bind('remove', function() {
            this.parent.set({ 'Stylesheet': self });
            this.parent.change();
        });
    }
});

/**
 * View: StylesheetListView
 *
 * Display a StylesheetList collection as a set of tabs.
 */
var StylesheetListView = Backbone.View.extend({
    initialize: function(options) {
        _.bindAll(this, 'render', 'add', 'activate');
        var self = this;
        this.collection.bind('add', this.render);
        this.collection.bind('add', this.activate);
        this.collection.bind('remove', this.render);
        this.collection.bind('remove', this.activate);
        window.app.bind('ready', this.activate);
        options.project.view.stylesheets = this;
        this.render();
        /*
        @TODO: bind re-render to project events.
        */
    },
    render: function() {
        // Render the stylesheets wrapper if not present.
        if ($(this.el).has('.stylesheets').length === 0) {
            $(this.el).html(ich.StylesheetListView());
            $('.stylesheets', this.el).sortable({
                axis: 'x',
                revert: true,
                containment: 'parent'
                // @TODO: proper event.
                // change: TileMill.project.changed
            });
        }

        // Add a tab view for each stylesheet.
        var self = this;
        this.collection.each(function(stylesheet) {
            if (!stylesheet.view) {
                stylesheet.view = new StylesheetTabView({
                    model: stylesheet,
                    list: self
                });
                $('.stylesheets', self.el).append(stylesheet.view.el);
                self.activeTab = self.activeTab || stylesheet.view;
            }
        });
        return this;
    },
    activate: function() {
        if (this.activeTab) {
            this.activeTab.activate();
        }
    },
    events: {
        'click .add': 'add'
    },
    add: function() {
        new StylesheetPopupView({collection: this.collection});
        return false;
    }
});

/**
 * View: StylesheetTabView
 *
 * Display a Stylesheet as a tab within a StylesheetListView.
 */
var StylesheetTabView = Backbone.View.extend({
    tagName: 'a',
    className: 'tab',
    initialize: function(params) {
        _.bindAll(this, 'render', 'update', 'del', 'activate', 'remove');
        this.list = params.list;
        this.input = $(ich.StylesheetTabEditor());
        this.tools = $(ich.StylesheetTools());
        this.codemirror = false;
        this.render();
    },
    render: function() {
        $(this.el).html(ich.StylesheetTabView({ id: this.model.get('id') }));
        $('#editor', this.list.el).append(this.input);

        return this;
    },
    events: {
        'click .name': 'activate',
        'click .delete': 'del'
    },
    activate: function() {
        var self = this;

        $('#tabs .tab, #editor .editor', this.list.el).removeClass('active');
        $(this.el).addClass('active');
        $(this.input).addClass('active');
        $(this.tools).addClass('active');
        this.list.activeTab = this;
        if (!this.codemirror) {
            this.codemirror = CodeMirror.fromTextArea($('textarea', this.input).get(0), {
                content: this.model.get('data'),
                height: '100%',
                stylesheet: 'css/code.css',
                path: 'js/codemirror/js/',
                parserfile: 'parsemss.js',
                parserConfig: window.data.reference,
                saveFunction: function() {
                    self.model.collection.parent.view.saveProject();
                },
                onCursorActivity: function() {
                    self.model.set({'data': self.codemirror.getCode()});
                },
                onChange: function() {
                    // Trigger event on the project
                    self.model.collection.parent.trigger('codeMirrorChange');
                },
                initCallback: function(cm) {
                    self.model.collection.parent.trigger('ready');
                }
            });
        }
    },
    del: function() {
        window.app.loading();
        if (confirm('Are you sure you want to delete this stylesheet?')) {
            this.list.collection.remove(this.model);
            this.remove();
            window.app.done();
        }
        else {
            window.app.done();
        }
        return false;
    },
    /**
     * Override of .remove(). Removes the input editor element as well.
     */
    remove: function() {
        $(this.el).remove();
        $(this.input).remove();
        return this;
    }
});

/**
 * View: StylesheetPopupView
 *
 * Popup form for adding a new stylesheet.
 */
var StylesheetPopupView = PopupView.extend({
    events: _.extend(PopupView.prototype.events, {
        'click input.submit': 'submit'
    }),
    initialize: function(params) {
        this.options.title = 'Add stylesheet';
        this.options.content = ich.StylesheetPopupView({}, true);
        this.render();
    },
    submit: function() {
        var id = $('input.text', this.el).val();
        var stylesheet = new Stylesheet({id: id});
        var error = stylesheet.validate();
        if (error) {
            window.app.message('Error', error);
        }
        else {
            this.collection.add(stylesheet);
            this.remove();
        }
        return false;
    }
});

<commit_msg>Allow click anywhere on stylesheet tab.<commit_after>/**
 * Model: Stylesheet
 *
 * This model is *not* backed directly by the server.
 * It is a child model of Project and is saved serialized as part of the parent
 * Project model.
 */
var Stylesheet = Backbone.Model.extend({
    initialize: function() {
        if (!this.get('data')) {
            this.set({'data': ''});
        }
    },
    validate: function() {
        if (/^[a-z0-9\-_.]+$/i.test(this.id) === false) {
            return 'Name must contain only letters, numbers, dashes, underscores and periods.';
        }
    }
});

/**
 * Collection: StylesheetList
 *
 * This collection is *not* backed directly by the server.
 * This collection is a child of the Project model. When it is updated
 * (add/remove events) it updates the attributes of its parent model as well.
 */
var StylesheetList = Backbone.Collection.extend({
    model: Stylesheet,
    initialize: function(models, options) {
        var self = this;
        this.parent = options.parent;
        this.bind('change', function() {
            this.parent.set({ 'Stylesheet': self });
            this.parent.change();
        });
        this.bind('add', function() {
            this.parent.set({ 'Stylesheet': self });
            this.parent.change();
        });
        this.bind('remove', function() {
            this.parent.set({ 'Stylesheet': self });
            this.parent.change();
        });
    }
});

/**
 * View: StylesheetListView
 *
 * Display a StylesheetList collection as a set of tabs.
 */
var StylesheetListView = Backbone.View.extend({
    initialize: function(options) {
        _.bindAll(this, 'render', 'add', 'activate');
        var self = this;
        this.collection.bind('add', this.render);
        this.collection.bind('add', this.activate);
        this.collection.bind('remove', this.render);
        this.collection.bind('remove', this.activate);
        window.app.bind('ready', this.activate);
        options.project.view.stylesheets = this;
        this.render();
        /*
        @TODO: bind re-render to project events.
        */
    },
    render: function() {
        // Render the stylesheets wrapper if not present.
        if ($(this.el).has('.stylesheets').length === 0) {
            $(this.el).html(ich.StylesheetListView());
            $('.stylesheets', this.el).sortable({
                axis: 'x',
                revert: true,
                containment: 'parent'
                // @TODO: proper event.
                // change: TileMill.project.changed
            });
        }

        // Add a tab view for each stylesheet.
        var self = this;
        this.collection.each(function(stylesheet) {
            if (!stylesheet.view) {
                stylesheet.view = new StylesheetTabView({
                    model: stylesheet,
                    list: self
                });
                $('.stylesheets', self.el).append(stylesheet.view.el);
                self.activeTab = self.activeTab || stylesheet.view;
            }
        });
        return this;
    },
    activate: function() {
        if (this.activeTab) {
            this.activeTab.activate();
        }
    },
    events: {
        'click .add': 'add'
    },
    add: function() {
        new StylesheetPopupView({collection: this.collection});
        return false;
    }
});

/**
 * View: StylesheetTabView
 *
 * Display a Stylesheet as a tab within a StylesheetListView.
 */
var StylesheetTabView = Backbone.View.extend({
    tagName: 'a',
    className: 'tab',
    initialize: function(params) {
        _.bindAll(this, 'render', 'update', 'del', 'activate', 'remove');
        this.list = params.list;
        this.input = $(ich.StylesheetTabEditor());
        this.tools = $(ich.StylesheetTools());
        this.codemirror = false;
        this.render();
    },
    render: function() {
        $(this.el).html(ich.StylesheetTabView({ id: this.model.get('id') }));
        $('#editor', this.list.el).append(this.input);

        return this;
    },
    events: {
        'click': 'activate',
        'click .delete': 'del'
    },
    activate: function() {
        var self = this;

        $('#tabs .tab, #editor .editor', this.list.el).removeClass('active');
        $(this.el).addClass('active');
        $(this.input).addClass('active');
        $(this.tools).addClass('active');
        this.list.activeTab = this;
        if (!this.codemirror) {
            this.codemirror = CodeMirror.fromTextArea($('textarea', this.input).get(0), {
                content: this.model.get('data'),
                height: '100%',
                stylesheet: 'css/code.css',
                path: 'js/codemirror/js/',
                parserfile: 'parsemss.js',
                parserConfig: window.data.reference,
                saveFunction: function() {
                    self.model.collection.parent.view.saveProject();
                },
                onCursorActivity: function() {
                    self.model.set({'data': self.codemirror.getCode()});
                },
                onChange: function() {
                    // Trigger event on the project
                    self.model.collection.parent.trigger('codeMirrorChange');
                },
                initCallback: function(cm) {
                    self.model.collection.parent.trigger('ready');
                }
            });
        }
    },
    del: function() {
        window.app.loading();
        if (confirm('Are you sure you want to delete this stylesheet?')) {
            this.list.collection.remove(this.model);
            this.remove();
            window.app.done();
        }
        else {
            window.app.done();
        }
        return false;
    },
    /**
     * Override of .remove(). Removes the input editor element as well.
     */
    remove: function() {
        $(this.el).remove();
        $(this.input).remove();
        return this;
    }
});

/**
 * View: StylesheetPopupView
 *
 * Popup form for adding a new stylesheet.
 */
var StylesheetPopupView = PopupView.extend({
    events: _.extend(PopupView.prototype.events, {
        'click input.submit': 'submit'
    }),
    initialize: function(params) {
        this.options.title = 'Add stylesheet';
        this.options.content = ich.StylesheetPopupView({}, true);
        this.render();
    },
    submit: function() {
        var id = $('input.text', this.el).val();
        var stylesheet = new Stylesheet({id: id});
        var error = stylesheet.validate();
        if (error) {
            window.app.message('Error', error);
        }
        else {
            this.collection.add(stylesheet);
            this.remove();
        }
        return false;
    }
});

<|endoftext|>