<commit_before>Ext.define('SlateAdmin.controller.people.Progress', {
    extend: 'Ext.app.Controller',
    requires: [
        'Ext.window.MessageBox',

        /* global SlateAdmin */
        'SlateAdmin.API'
    ],

    views: [
        'people.details.Progress',
        'people.details.progress.Previewer',

        'people.details.progress.note.EditWindow'
    ],

    stores: [
        'people.ProgressReports',
        'people.progress.NoteRecipients'
    ],

    models: [
        'person.progress.ProgressNote'
    ],

    refs: {
        peopleManager: 'people-manager',

        progressPanel: {
            selector: 'people-details-progress',
            autoCreate: true,

            xtype: 'people-details-progress'
        },
        progressList: 'people-details-progress dataview',
        classesSelector: 'people-details-progress #classesSelector',
        termSelector: 'people-details-progress #termSelector',

        reportPreviewer: {
            selector: 'people-details-progress-previewer',
            autoCreate: true,

            xtype: 'people-details-progress-previewer'
        },

        progressNoteEditorWindow: {
            selector: 'people-details-progress-note-editwindow',
            autoCreate: true,

            xtype: 'people-details-progress-note-editwindow'
        },
        noteEditorCt: 'people-details-progress-note-editwindow #progressNoteCt',
        progressNoteForm: 'people-details-progress-note-editwindow people-details-progress-note-form',
        progressNoteViewer: 'people-details-progress-note-editwindow people-details-progress-note-viewer',
        progressNoteRecipientGrid: 'people-details-progress-note-editwindow people-details-progress-note-recipientgrid'
    },

    control: {
        'people-manager #detailTabs': {
            beforerender: 'onBeforeTabsRender'
        },
        'people-details-progress': {
            personloaded: 'onPersonLoaded'
        },
        'people-details-progress #classesSelector menucheckitem': {
            checkchange: 'onProgressClassesChange'
        },
        termSelector: {
            change: 'onProgressTermChange'
        },
        'people-details-progress button[action=launch-browser]': {
            click: 'onListLaunchBrowserClick'
        },
        'people-details-progress #progressReportsList': {
            itemclick: 'onProgressRecordClick'
        },
        'people-details-progress-previewer button[action=launch-browser]': {
            click: 'onPreviewerLaunchBrowserClick'
        },
        'people-details-progress button[action=export-reports]': {
            click: 'onExportProgressClick'
        },
        'people-details-progress button[action=composeNote]': {
            click: 'onComposeProgressNoteClick'
        },
        'people-details-progress-note-recipientgrid': {
            select: 'onRecipientsGridSelect',
            deselect: 'onRecipientsGridDeselect'
        },
        'people-details-progress-note-recipientgrid #customRecipientPersonCombo': {
            select: 'onCustomRecipientPersonSelect'
        },
        'people-details-progress-note-recipientgrid button[action=addRecepient]': {
            click: 'onAddProgressNoteRecipient'
        },
        'people-details-progress-note-editwindow button[action=discardProgressNote]': {
            click: 'onDiscardProgressNote'
        },
        'people-details-progress-note-editwindow button[action=sendProgressNote]': {
            click: 'onSendProgressNote'
        }
    },

    init: function () {
        this.bufferedDoFilter = Ext.Function.createBuffered(this.doFilter, 1000, this);
    },

    // event handlers
    onBeforeTabsRender: function (detailTabs) {
        detailTabs.add(this.getProgressPanel());
    },

    onPersonLoaded: function (progressPanel, person) {
        var me = this,
            progressProxy = me.getPeopleProgressReportsStore().getProxy();

        progressProxy.setExtraParam('student', person.get('Username') || person.getId());
        progressProxy.setExtraParam('classes[]', Ext.Array.map(me.getClassesSelector().query('menucheckitem'), function(checkItem) {
            return checkItem.getValue();
        }));
        progressProxy.setExtraParam('term', me.getTermSelector().getValue() || '');

        me.doFilter(true);
    },

    onComposeProgressNoteClick: function () {
        var me = this,
            editor = me.getProgressNoteEditorWindow(),
            store = me.getPeopleProgressNoteRecipientsStore(),
            person = me.getPeopleManager().getSelectedPerson(),
            personId = person.getId(),
            phantomRecord = new (me.getPersonProgressProgressNoteModel())({
                ContextClass: 'Emergence\\People\\Person',
                ContextID: personId
            });

        editor.setProgressNote(phantomRecord);

        editor.show();

        store.load({
            params: {
                personID: personId
            },
            callback: function () {
                var advisorRecipient = store.findExact('PersonID', person.get('AdvisorID'));

                if (advisorRecipient !== -1) {
                    me.getProgressNoteRecipientGrid().getSelectionModel().select(advisorRecipient);
                }
            }
        });
    },

    onRecipientsGridSelect: function(selModel, selectedRecord) {
        var personId = selectedRecord.get('PersonID');

        selModel.select(
            this.getPeopleProgressNoteRecipientsStore().queryBy(function(record) {
                return record !== selectedRecord && record.get('PersonID') === personId;
            }).getRange(),
            true // true to keep existing selection
        );
    },

    onRecipientsGridDeselect: function(selModel, deselectedRecord) {
        var personId = deselectedRecord.get('PersonID');

        selModel.deselect(
            this.getPeopleProgressNoteRecipientsStore().queryBy(function(record) {
                return record !== deselectedRecord && record.get('PersonID') === personId;
            }).getRange()
        );
    },

    onCustomRecipientPersonSelect: function (combo, record) {
        combo.nextSibling('textfield[name=Email]').setValue(record.get('Email'));
    },

    onAddProgressNoteRecipient: function (btn) {
        var me = this,
            menu = btn.up('menu'),
            personField = menu.down('combo[name="Person"]'),
            emailField = menu.down('textfield[name="Email"]'),
            relationshipField = menu.down('textfield[name="Label"]'),
            person = me.getPeopleManager().getSelectedPerson(),
            values = {
                Person: personField.getValue(),
                Label: relationshipField.getValue(),
                Email: emailField.getValue(),
                StudentID: person.getId()
            },
            recipientGrid = me.getProgressNoteRecipientGrid(),
            recipientsStore = me.getPeopleProgressNoteRecipientsStore();

        if (personField.isValid() && emailField.isValid()) {
            recipientGrid.setLoading('Attempting to add custom recipient …');

            SlateAdmin.API.request({
                url: '/notes/addCustomRecipient',
                params: values,
                success: function (res) {
                    var r = Ext.decode(res.responseText);

                    if (r.success) {
                        recipientGrid.getSelectionModel().select(recipientsStore.add(r.data), true);

                        menu.hide();
                        personField.reset();
                        emailField.reset();
                        relationshipField.reset();
                    } else {
                        Ext.Msg.alert('Failure adding recipient', r.message);
                    }

                    recipientGrid.setLoading(false);
                },
                failure: function () {
                    recipientGrid.setLoading(false);
                }
            });
        }
    },

    onDiscardProgressNote: function () {
        Ext.Msg.confirm('Discarding Progress Note', 'Are you sure you want to discard this progress note?', function(btn) {
            if (btn == 'yes') {
                this.getProgressNoteEditorWindow().close();
            }
        }, this);
    },

    onSendProgressNote: function() {
        var me = this,
            recipients = me.getProgressNoteRecipientGrid().getSelectionModel().getSelection();

        if (recipients.length) {
            Ext.Msg.confirm('Sending', 'Are you sure you want to send this message?', function (btn) {
                if (btn == 'yes') {
                    me.doSaveProgressNote(me.getProgressNoteEditorWindow().syncProgressNote(), recipients);
                }
            });
        } else {
            Ext.Msg.alert('Cannot send email', 'Please select recipients before sending.');
        }

    },

    onProgressClassesChange: function () {
        this.getPeopleProgressReportsStore().getProxy().setExtraParam(
            'classes[]',
            Ext.Array.map(this.getClassesSelector().query('menucheckitem[checked]'), function(checkItem) {
                return checkItem.getValue();
            })
        );

        this.bufferedDoFilter();
    },

    onProgressTermChange: function (field, newValue) {
        var reportsStore = this.getPeopleProgressReportsStore(),
            reportsProxy = reportsStore.getProxy();

        reportsProxy.setExtraParam('term', newValue);

        this.bufferedDoFilter();
    },

    onExportProgressClick: function () {
        var reportsPanel = this.getProgressPanel(),
            params = Ext.apply(
                {
                    format: 'pdf'
                },
                this.getPeopleProgressReportsStore().getProxy().getExtraParams()
            );

        Ext.Msg.confirm('Exporting Reports', 'Are you sure want to export the currently loaded reports', function(btn) {
            if (btn != 'yes') {
                return;
            }

            reportsPanel.setLoading({
                msg: 'Preparing PDF, please wait, this may take a minute…'
            });

            SlateAdmin.API.downloadFile('/progress?' + Ext.Object.toQueryString(params), function () {
                reportsPanel.setLoading(false);
            });
        });
    },

    onListLaunchBrowserClick: function() {
        var params = this.getPeopleProgressReportsStore().getProxy().getExtraParams();

        window.open(SlateAdmin.API.buildUrl('/progress?' + Ext.Object.toQueryString(params)));
    },

    onProgressRecordClick: function (view, record) {
        var me = this;

        switch (record.get('Class')) {
            case 'Slate\\Progress\\Note':
                me.onProgressNoteClick(record);
                break;

            case 'Slate\\Progress\\SectionTermReport':
                me.onTermReportClick(record);
                break;

            case 'Slate\\Progress\\SectionInterimReport':
                me.onInterimReportClick(record);
                break;

            default:
                break;
        }
    },

    onPreviewerLaunchBrowserClick: function() {
        window.open(this.getReportPreviewer().getUrl());
    },

    onProgressNoteClick: function (progressRecord) {
        var me = this,
            editor = me.getProgressNoteEditorWindow(),
            recipientsStore = me.getPeopleProgressNoteRecipientsStore();

        me.getNoteEditorCt().getLayout().setActiveItem(me.getProgressNoteViewer());
        recipientsStore.removeAll();
        editor.setProgressNote(null);

        editor.show();
        editor.setLoading({
            msg: 'Loading progress note…'
        });

        me.getPersonProgressProgressNoteModel().load(progressRecord.get('ID'), {
            success: function(noteRecord) {
                editor.setProgressNote(noteRecord);
                editor.setLoading(false);
            }
        });

        recipientsStore.load({
            params: {
                messageID: progressRecord.get('ID')
            },
            callback: function (records) {
                me.getProgressNoteRecipientGrid().getSelectionModel().select(Ext.Array.filter(records, function(record) {
                    return record.get('selected');
                }));
            }
        });
    },

    onTermReportClick: function (record) {
        var reportPreviewer = this.getReportPreviewer();

        reportPreviewer.show();
        reportPreviewer.setReport(record);
    },

    onInterimReportClick: function (record) {
        var reportPreviewer = this.getReportPreviewer();

        reportPreviewer.show();
        reportPreviewer.setReport(record);
    },

    doFilter: function (forceReload, callback) {
        var store = this.getPeopleProgressReportsStore(),
            proxy = store.getProxy();

        if (forceReload || proxy.isExtraParamsDirty()) {
            store.load({
                callback: callback,
                scope: this
            });
        }
    },

    doSaveProgressNote: function (record, recipients) {
        var me = this,
            editorWindow = me.getProgressNoteEditorWindow(),
            reportsProxy = me.getPeopleProgressReportsStore().getProxy(),
            includeFields = reportsProxy.getInclude();

        editorWindow.setLoading('Sending…');

        if (record.phantom) {
            record.save({
                params: {
                    summary: reportsProxy.getSummary(),
                    include: Ext.isArray(includeFields) ? includeFields.join(',') : includeFields
                },
                success: function (savedRecord) {
                    me.getPeopleProgressReportsStore().insert(0, savedRecord);
                    me.doSaveRecipients(savedRecord, recipients);
                },
                failure: function(failedRecord, operation) {
                    editorWindow.setLoading(false);
                    Ext.Msg.alert('Failed to save note', operation.getError() || 'An unknown problem occurred, please try again later or contact support');
                }
            });
        } else {
            me.doSaveRecipients(record, recipients);
        }
    },

    doSaveRecipients: function (record, recipients) {
        var me = this,
            editorWindow = me.getProgressNoteEditorWindow();

        SlateAdmin.API.request({
            url: '/notes/' + record.get('ID') + '/recipients',
            method: 'POST',
            jsonData: {
                data: recipients.map(function(r) {
                    return {
                        PersonID: r.get('PersonID'),
                        Email: r.get('Email')
                    };
                })
            },
            success: function (response) {
                editorWindow.setLoading(false);

                if (response.data.success) {
                    editorWindow.hide();
                } else {
                    Ext.Msg.alert('Failed to add recipients to saved note', response.data.error || 'An unknown problem occurred, please try again later or contact support');
                }
            },
            failure: function() {
                editorWindow.setLoading(false);
                Ext.Msg.alert('Failed to add recipients to saved note', 'An unknown problem occurred, please try again later or contact support');
            }
        });
    }
});<commit_msg>Prevent selection of recipients with no email and show warning<commit_after>Ext.define('SlateAdmin.controller.people.Progress', {
    extend: 'Ext.app.Controller',
    requires: [
        'Ext.window.MessageBox',

        /* global SlateAdmin */
        'SlateAdmin.API'
    ],

    views: [
        'people.details.Progress',
        'people.details.progress.Previewer',

        'people.details.progress.note.EditWindow'
    ],

    stores: [
        'people.ProgressReports',
        'people.progress.NoteRecipients'
    ],

    models: [
        'person.progress.ProgressNote'
    ],

    refs: {
        peopleManager: 'people-manager',

        progressPanel: {
            selector: 'people-details-progress',
            autoCreate: true,

            xtype: 'people-details-progress'
        },
        progressList: 'people-details-progress dataview',
        classesSelector: 'people-details-progress #classesSelector',
        termSelector: 'people-details-progress #termSelector',

        reportPreviewer: {
            selector: 'people-details-progress-previewer',
            autoCreate: true,

            xtype: 'people-details-progress-previewer'
        },

        progressNoteEditorWindow: {
            selector: 'people-details-progress-note-editwindow',
            autoCreate: true,

            xtype: 'people-details-progress-note-editwindow'
        },
        noteEditorCt: 'people-details-progress-note-editwindow #progressNoteCt',
        progressNoteForm: 'people-details-progress-note-editwindow people-details-progress-note-form',
        progressNoteViewer: 'people-details-progress-note-editwindow people-details-progress-note-viewer',
        progressNoteRecipientGrid: 'people-details-progress-note-editwindow people-details-progress-note-recipientgrid'
    },

    control: {
        'people-manager #detailTabs': {
            beforerender: 'onBeforeTabsRender'
        },
        'people-details-progress': {
            personloaded: 'onPersonLoaded'
        },
        'people-details-progress #classesSelector menucheckitem': {
            checkchange: 'onProgressClassesChange'
        },
        termSelector: {
            change: 'onProgressTermChange'
        },
        'people-details-progress button[action=launch-browser]': {
            click: 'onListLaunchBrowserClick'
        },
        'people-details-progress #progressReportsList': {
            itemclick: 'onProgressRecordClick'
        },
        'people-details-progress-previewer button[action=launch-browser]': {
            click: 'onPreviewerLaunchBrowserClick'
        },
        'people-details-progress button[action=export-reports]': {
            click: 'onExportProgressClick'
        },
        'people-details-progress button[action=composeNote]': {
            click: 'onComposeProgressNoteClick'
        },
        'people-details-progress-note-recipientgrid': {
            beforeselect: 'onRecipientsGridBeforeSelect',
            select: 'onRecipientsGridSelect',
            deselect: 'onRecipientsGridDeselect'
        },
        'people-details-progress-note-recipientgrid #customRecipientPersonCombo': {
            select: 'onCustomRecipientPersonSelect'
        },
        'people-details-progress-note-recipientgrid button[action=addRecepient]': {
            click: 'onAddProgressNoteRecipient'
        },
        'people-details-progress-note-editwindow button[action=discardProgressNote]': {
            click: 'onDiscardProgressNote'
        },
        'people-details-progress-note-editwindow button[action=sendProgressNote]': {
            click: 'onSendProgressNote'
        }
    },

    init: function () {
        this.bufferedDoFilter = Ext.Function.createBuffered(this.doFilter, 1000, this);
    },

    // event handlers
    onBeforeTabsRender: function (detailTabs) {
        detailTabs.add(this.getProgressPanel());
    },

    onPersonLoaded: function (progressPanel, person) {
        var me = this,
            progressProxy = me.getPeopleProgressReportsStore().getProxy();

        progressProxy.setExtraParam('student', person.get('Username') || person.getId());
        progressProxy.setExtraParam('classes[]', Ext.Array.map(me.getClassesSelector().query('menucheckitem'), function(checkItem) {
            return checkItem.getValue();
        }));
        progressProxy.setExtraParam('term', me.getTermSelector().getValue() || '');

        me.doFilter(true);
    },

    onComposeProgressNoteClick: function () {
        var me = this,
            editor = me.getProgressNoteEditorWindow(),
            store = me.getPeopleProgressNoteRecipientsStore(),
            person = me.getPeopleManager().getSelectedPerson(),
            personId = person.getId(),
            phantomRecord = new (me.getPersonProgressProgressNoteModel())({
                ContextClass: 'Emergence\\People\\Person',
                ContextID: personId
            });

        editor.setProgressNote(phantomRecord);

        editor.show();

        store.load({
            params: {
                personID: personId
            },
            callback: function () {
                var advisorRecipient = store.findExact('PersonID', person.get('AdvisorID'));

                if (advisorRecipient !== -1) {
                    me.getProgressNoteRecipientGrid().getSelectionModel().select(advisorRecipient);
                }
            }
        });
    },

    onRecipientsGridBeforeSelect: function(selModel, selectedRecord) {
        var personId = selectedRecord.get('PersonID'),
            fullName = selectedRecord.get('FullName');

        if (selectedRecord.get('Email')) {
            return true;
        }

        if (this.lastRecipientWarning != personId) {
            this.lastRecipientWarning = personId;
            Ext.Msg.alert(
                'Cannot select '+fullName,
                fullName+' cannot be selected as a recipient because they do not have any email address on file. Update their contact details before adding them as a recipient.'
            );
        }

        return false;
    },

    onRecipientsGridSelect: function(selModel, selectedRecord) {
        var personId = selectedRecord.get('PersonID');

        selModel.select(
            this.getPeopleProgressNoteRecipientsStore().queryBy(function(record) {
                return record !== selectedRecord && record.get('PersonID') === personId;
            }).getRange(),
            true // true to keep existing selection
        );
    },

    onRecipientsGridDeselect: function(selModel, deselectedRecord) {
        var personId = deselectedRecord.get('PersonID');

        selModel.deselect(
            this.getPeopleProgressNoteRecipientsStore().queryBy(function(record) {
                return record !== deselectedRecord && record.get('PersonID') === personId;
            }).getRange()
        );
    },

    onCustomRecipientPersonSelect: function (combo, record) {
        combo.nextSibling('textfield[name=Email]').setValue(record.get('Email'));
    },

    onAddProgressNoteRecipient: function (btn) {
        var me = this,
            menu = btn.up('menu'),
            personField = menu.down('combo[name="Person"]'),
            emailField = menu.down('textfield[name="Email"]'),
            relationshipField = menu.down('textfield[name="Label"]'),
            person = me.getPeopleManager().getSelectedPerson(),
            values = {
                Person: personField.getValue(),
                Label: relationshipField.getValue(),
                Email: emailField.getValue(),
                StudentID: person.getId()
            },
            recipientGrid = me.getProgressNoteRecipientGrid(),
            recipientsStore = me.getPeopleProgressNoteRecipientsStore();

        if (personField.isValid() && emailField.isValid()) {
            recipientGrid.setLoading('Attempting to add custom recipient …');

            SlateAdmin.API.request({
                url: '/notes/addCustomRecipient',
                params: values,
                success: function (res) {
                    var r = Ext.decode(res.responseText);

                    if (r.success) {
                        recipientGrid.getSelectionModel().select(recipientsStore.add(r.data), true);

                        menu.hide();
                        personField.reset();
                        emailField.reset();
                        relationshipField.reset();
                    } else {
                        Ext.Msg.alert('Failure adding recipient', r.message);
                    }

                    recipientGrid.setLoading(false);
                },
                failure: function () {
                    recipientGrid.setLoading(false);
                }
            });
        }
    },

    onDiscardProgressNote: function () {
        Ext.Msg.confirm('Discarding Progress Note', 'Are you sure you want to discard this progress note?', function(btn) {
            if (btn == 'yes') {
                this.getProgressNoteEditorWindow().close();
            }
        }, this);
    },

    onSendProgressNote: function() {
        var me = this,
            recipients = me.getProgressNoteRecipientGrid().getSelectionModel().getSelection();

        if (recipients.length) {
            Ext.Msg.confirm('Sending', 'Are you sure you want to send this message?', function (btn) {
                if (btn == 'yes') {
                    me.doSaveProgressNote(me.getProgressNoteEditorWindow().syncProgressNote(), recipients);
                }
            });
        } else {
            Ext.Msg.alert('Cannot send email', 'Please select recipients before sending.');
        }

    },

    onProgressClassesChange: function () {
        this.getPeopleProgressReportsStore().getProxy().setExtraParam(
            'classes[]',
            Ext.Array.map(this.getClassesSelector().query('menucheckitem[checked]'), function(checkItem) {
                return checkItem.getValue();
            })
        );

        this.bufferedDoFilter();
    },

    onProgressTermChange: function (field, newValue) {
        var reportsStore = this.getPeopleProgressReportsStore(),
            reportsProxy = reportsStore.getProxy();

        reportsProxy.setExtraParam('term', newValue);

        this.bufferedDoFilter();
    },

    onExportProgressClick: function () {
        var reportsPanel = this.getProgressPanel(),
            params = Ext.apply(
                {
                    format: 'pdf'
                },
                this.getPeopleProgressReportsStore().getProxy().getExtraParams()
            );

        Ext.Msg.confirm('Exporting Reports', 'Are you sure want to export the currently loaded reports', function(btn) {
            if (btn != 'yes') {
                return;
            }

            reportsPanel.setLoading({
                msg: 'Preparing PDF, please wait, this may take a minute…'
            });

            SlateAdmin.API.downloadFile('/progress?' + Ext.Object.toQueryString(params), function () {
                reportsPanel.setLoading(false);
            });
        });
    },

    onListLaunchBrowserClick: function() {
        var params = this.getPeopleProgressReportsStore().getProxy().getExtraParams();

        window.open(SlateAdmin.API.buildUrl('/progress?' + Ext.Object.toQueryString(params)));
    },

    onProgressRecordClick: function (view, record) {
        var me = this;

        switch (record.get('Class')) {
            case 'Slate\\Progress\\Note':
                me.onProgressNoteClick(record);
                break;

            case 'Slate\\Progress\\SectionTermReport':
                me.onTermReportClick(record);
                break;

            case 'Slate\\Progress\\SectionInterimReport':
                me.onInterimReportClick(record);
                break;

            default:
                break;
        }
    },

    onPreviewerLaunchBrowserClick: function() {
        window.open(this.getReportPreviewer().getUrl());
    },

    onProgressNoteClick: function (progressRecord) {
        var me = this,
            editor = me.getProgressNoteEditorWindow(),
            recipientsStore = me.getPeopleProgressNoteRecipientsStore();

        me.getNoteEditorCt().getLayout().setActiveItem(me.getProgressNoteViewer());
        recipientsStore.removeAll();
        editor.setProgressNote(null);

        editor.show();
        editor.setLoading({
            msg: 'Loading progress note…'
        });

        me.getPersonProgressProgressNoteModel().load(progressRecord.get('ID'), {
            success: function(noteRecord) {
                editor.setProgressNote(noteRecord);
                editor.setLoading(false);
            }
        });

        recipientsStore.load({
            params: {
                messageID: progressRecord.get('ID')
            },
            callback: function (records) {
                me.getProgressNoteRecipientGrid().getSelectionModel().select(Ext.Array.filter(records, function(record) {
                    return record.get('selected');
                }));
            }
        });
    },

    onTermReportClick: function (record) {
        var reportPreviewer = this.getReportPreviewer();

        reportPreviewer.show();
        reportPreviewer.setReport(record);
    },

    onInterimReportClick: function (record) {
        var reportPreviewer = this.getReportPreviewer();

        reportPreviewer.show();
        reportPreviewer.setReport(record);
    },

    doFilter: function (forceReload, callback) {
        var store = this.getPeopleProgressReportsStore(),
            proxy = store.getProxy();

        if (forceReload || proxy.isExtraParamsDirty()) {
            store.load({
                callback: callback,
                scope: this
            });
        }
    },

    doSaveProgressNote: function (record, recipients) {
        var me = this,
            editorWindow = me.getProgressNoteEditorWindow(),
            reportsProxy = me.getPeopleProgressReportsStore().getProxy(),
            includeFields = reportsProxy.getInclude();

        editorWindow.setLoading('Sending…');

        if (record.phantom) {
            record.save({
                params: {
                    summary: reportsProxy.getSummary(),
                    include: Ext.isArray(includeFields) ? includeFields.join(',') : includeFields
                },
                success: function (savedRecord) {
                    me.getPeopleProgressReportsStore().insert(0, savedRecord);
                    me.doSaveRecipients(savedRecord, recipients);
                },
                failure: function(failedRecord, operation) {
                    editorWindow.setLoading(false);
                    Ext.Msg.alert('Failed to save note', operation.getError() || 'An unknown problem occurred, please try again later or contact support');
                }
            });
        } else {
            me.doSaveRecipients(record, recipients);
        }
    },

    doSaveRecipients: function (record, recipients) {
        var me = this,
            editorWindow = me.getProgressNoteEditorWindow();

        SlateAdmin.API.request({
            url: '/notes/' + record.get('ID') + '/recipients',
            method: 'POST',
            jsonData: {
                data: recipients.map(function(r) {
                    return {
                        PersonID: r.get('PersonID'),
                        Email: r.get('Email')
                    };
                })
            },
            success: function (response) {
                editorWindow.setLoading(false);

                if (response.data.success) {
                    editorWindow.hide();
                } else {
                    Ext.Msg.alert('Failed to add recipients to saved note', response.data.error || 'An unknown problem occurred, please try again later or contact support');
                }
            },
            failure: function() {
                editorWindow.setLoading(false);
                Ext.Msg.alert('Failed to add recipients to saved note', 'An unknown problem occurred, please try again later or contact support');
            }
        });
    }
});<|endoftext|>


<commit_before>/**
 * @file Contain the description of the next classes:
 *

 *
[PageEditor]{@link PageEditor}

 *
[PageEditor.BlockEditor]{@link PageEditor.BlockEditor}

 *

 *
 * @requires Energine
 * @requires ckeditor/ckeditor
 * @requires ModalBox
 * @requires Overlay
 *
 * @author Pavel Dubenko
 * @author Andy Karpov
 * @author Valerii Zinchenko
 *
 * @version 1.0.0
 */

ScriptLoader.load('ckeditor/ckeditor', 'ModalBox', 'Overlay');

/**
 * @class PageEditor
 * @classdesc Page editor.
 */
var PageEditor = new Class(/** @lends PageEditor# */{
    // todo: Make it sense to store this two members in the object? They used only by initialize.
    /**
     * Editor class name.
     * @type {string}
     */
    editorClassName: 'nrgnEditor',

    /**
     * Array of block editors.
     * @type {PageEditor.BlockEditor[]}
     */
    editors: [],

    initialize: function () {
        Asset.css('pageeditor.css');

        CKEDITOR.disableAutoInline = true;
        CKEDITOR.config.extraPlugins = 'sourcedialog,energineimage,energinevideo,energinefile';
        CKEDITOR.config.removePlugins = 'sourcearea';
        CKEDITOR.config.allowedContent = true;
        CKEDITOR.config.toolbar = [
            { name: 'document', groups: [ 'mode' ], items: [ 'Sourcedialog' ] },
            { name: 'clipboard', groups: [ 'clipboard', 'undo' ], items: [ 'Cut', 'Copy', 'Paste', 'PasteText', 'PasteFromWord', '-', 'Undo', 'Redo' ] },
            { name: 'editing', groups: [ 'find', 'selection' ], items: [ 'Find', 'Replace', '-', 'SelectAll' ] },
            { name: 'links', items: [ 'Link', 'Unlink', 'Anchor' ] },
            { name: 'insert', items: [ 'Image', 'Flash', 'Table', 'EnergineImage', 'EnergineVideo', 'EnergineFile' ] },
            { name: 'tools', items: [ 'ShowBlocks' ] },
            '/',
            { name: 'basicstyles', groups: [ 'basicstyles', 'cleanup' ], items: [ 'Bold', 'Italic', 'Underline', 'Strike', 'Subscript', 'Superscript', '-', 'RemoveFormat' ] },
            { name: 'paragraph', groups: [ 'list', 'indent', 'align' ], items: [ 'NumberedList', 'BulletedList', '-', 'Outdent', 'Indent', '-', 'JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock' ] },
            { name: 'styles', items: [ 'Styles', 'Format', 'Font', 'FontSize' ] },
            { name: 'colors', items: [ 'TextColor', 'BGColor' ] }
        ];
        var styles = [];
        if (window['wysiwyg_styles']) {
            Object.each(window['wysiwyg_styles'], function (style) {
                styles.push({
                    name: style['caption'],
                    element: style['element'],
                    attributes: { 'class': style['class'] }
                });
            });
        }
        CKEDITOR.stylesSet.add('energine', styles);
        CKEDITOR.config.stylesSet = 'energine';

        $(document.body).getElements('.' + this.editorClassName).each(function (element) {
            this.editors.push(new PageEditor.BlockEditor(element));
        }, this);

        window.addEvent(((Browser.opera) ? 'unload' : 'beforeunload'), function () {
            if (this.editors.length) {
                this.editors.each(function (editor) {
                    editor.save.call(editor, false);
                }, this);
            }
            if (Browser.opera) {
                // Dirty Opera hack
                window.location.href = window.location.href;
                return '';
            }
        }.bind(this));
    }
});

/**
 * Block editor.
 *
 * @constructor
 * @param pageEditor
 * @param area
 */
PageEditor.BlockEditor = new Class(/** @lends PageEditor.BlockEditor# */{
    // constructor
    initialize: function (area) {
        /**
         * Area element.
         * @type {Element}
         */
        this.area = area;
        this.area.setProperty('contenteditable', true);

        /**
         * Defines whether the editor is active.
         * @type {boolean}
         */
        this.isActive = false;

        /**
         * Single path.
         * @type {string}
         */
        this.singlePath = this.area.getProperty('single_template');

        /**
         * Block editor ID.
         * @type {string}
         */
        this.ID = this.area.getProperty('eID') ? this.area.getProperty('eID') : '';

        /**
         * Text block ID.
         * @type {string}
         */
        this.num = this.area.getProperty('num') || '';

        /**
         * Editor.
         * @type {CKEDITOR}
         */
        this.editor = CKEDITOR.inline(this.area.get('id'), {
            language: Energine.lang
        });
        this.editor.singleTemplate = this.area.getProperty('single_template');
        this.editor.editorId = this.area.get('id');

        /**
         * Overlay.
         * @type {Overlay}
         */
        this.overlay = new Overlay();
        /*this.editor.on('blur', function () {
            console.log(this.area)
            this.area.removeClass('activeEditor');
            this.save();
        }.bind(this));
        this.editor.on('focus', function () {
            console.log(this.area)
            this.area.addClass('activeEditor');
        }.bind(this));*/
    },

    /**
     * Save.
     *
     * @function
     * @public
     * @param {boolean} [async = true] Defines whether the request be asynchronous or not.
     * @param {function} [onSuccess = undefined] User defined function that is called after sucess saving
     */
    save: function (async, onSuccess) {
        if (this.editor.checkDirty()) {
            if (async == undefined) {
                async = true;
            }
            if (!async) {
                this.overlay.show();
            }

            var data = 'data=' + encodeURIComponent(this.editor.getData());
            if (this.ID) {
                data += '&ID=' + this.ID;
            }
            if (this.num) {
                data += '&num=' + this.num;
            }

            new Request({
                url: this.singlePath + 'save-text',
                async: async,
                method: 'post',
                data: data,
                onSuccess: function (response) {
                    // Временно убрано, т.к. сбрасывает курсор в начала редактируемой области
                    // и из за этого неправильно работают некоторые ф-ции эдитора.
                    //this.editor.setData(response);
                    if (onSuccess)onSuccess.call(this);
                    this.editor.resetDirty();
                    if (!async) {
                        this.overlay.hide();
                    }
                }.bind(this)
            }).send();
        }

    }
});
<commit_msg>Мелкие изменения в конфиг CKEDITOR'а, дабы он не схлопывал пустые теги <commit_after>/**
 * @file Contain the description of the next classes:
 *

 *
[PageEditor]{@link PageEditor}

 *
[PageEditor.BlockEditor]{@link PageEditor.BlockEditor}

 *

 *
 * @requires Energine
 * @requires ckeditor/ckeditor
 * @requires ModalBox
 * @requires Overlay
 *
 * @author Pavel Dubenko
 * @author Andy Karpov
 * @author Valerii Zinchenko
 *
 * @version 1.0.0
 */

ScriptLoader.load('ckeditor/ckeditor', 'ModalBox', 'Overlay');

/**
 * @class PageEditor
 * @classdesc Page editor.
 */
var PageEditor = new Class(/** @lends PageEditor# */{
    // todo: Make it sense to store this two members in the object? They used only by initialize.
    /**
     * Editor class name.
     * @type {string}
     */
    editorClassName: 'nrgnEditor',

    /**
     * Array of block editors.
     * @type {PageEditor.BlockEditor[]}
     */
    editors: [],

    initialize: function () {
        Asset.css('pageeditor.css');

        CKEDITOR.disableAutoInline = true;
        CKEDITOR.config.extraPlugins = 'sourcedialog,energineimage,energinevideo,energinefile';
        CKEDITOR.config.removePlugins = 'sourcearea';
        CKEDITOR.config.allowedContent = true;
        CKEDITOR.config.toolbar = [
            { name: 'document', groups: [ 'mode' ], items: [ 'Sourcedialog' ] },
            { name: 'clipboard', groups: [ 'clipboard', 'undo' ], items: [ 'Cut', 'Copy', 'Paste', 'PasteText', 'PasteFromWord', '-', 'Undo', 'Redo' ] },
            { name: 'editing', groups: [ 'find', 'selection' ], items: [ 'Find', 'Replace', '-', 'SelectAll' ] },
            { name: 'links', items: [ 'Link', 'Unlink', 'Anchor' ] },
            { name: 'insert', items: [ 'Image', 'Flash', 'Table', 'EnergineImage', 'EnergineVideo', 'EnergineFile' ] },
            { name: 'tools', items: [ 'ShowBlocks' ] },
            '/',
            { name: 'basicstyles', groups: [ 'basicstyles', 'cleanup' ], items: [ 'Bold', 'Italic', 'Underline', 'Strike', 'Subscript', 'Superscript', '-', 'RemoveFormat' ] },
            { name: 'paragraph', groups: [ 'list', 'indent', 'align' ], items: [ 'NumberedList', 'BulletedList', '-', 'Outdent', 'Indent', '-', 'JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock' ] },
            { name: 'styles', items: [ 'Styles', 'Format', 'Font', 'FontSize' ] },
            { name: 'colors', items: [ 'TextColor', 'BGColor' ] }
        ];
        var styles = [];
        if (window['wysiwyg_styles']) {
            Object.each(window['wysiwyg_styles'], function (style) {
                styles.push({
                    name: style['caption'],
                    element: style['element'],
                    attributes: { 'class': style['class'] }
                });
            });
        }
        CKEDITOR.stylesSet.add('energine', styles);
        CKEDITOR.config.stylesSet = 'energine';
		// allow empty
		CKEDITOR.dtd.$removeEmpty['i'] = false;

        $(document.body).getElements('.' + this.editorClassName).each(function (element) {
            this.editors.push(new PageEditor.BlockEditor(element));
        }, this);

        window.addEvent(((Browser.opera) ? 'unload' : 'beforeunload'), function () {
            if (this.editors.length) {
                this.editors.each(function (editor) {
                    editor.save.call(editor, false);
                }, this);
            }
            if (Browser.opera) {
                // Dirty Opera hack
                window.location.href = window.location.href;
                return '';
            }
        }.bind(this));
    }
});

/**
 * Block editor.
 *
 * @constructor
 * @param pageEditor
 * @param area
 */
PageEditor.BlockEditor = new Class(/** @lends PageEditor.BlockEditor# */{
    // constructor
    initialize: function (area) {
        /**
         * Area element.
         * @type {Element}
         */
        this.area = area;
        this.area.setProperty('contenteditable', true);

        /**
         * Defines whether the editor is active.
         * @type {boolean}
         */
        this.isActive = false;

        /**
         * Single path.
         * @type {string}
         */
        this.singlePath = this.area.getProperty('single_template');

        /**
         * Block editor ID.
         * @type {string}
         */
        this.ID = this.area.getProperty('eID') ? this.area.getProperty('eID') : '';

        /**
         * Text block ID.
         * @type {string}
         */
        this.num = this.area.getProperty('num') || '';

        /**
         * Editor.
         * @type {CKEDITOR}
         */
        this.editor = CKEDITOR.inline(this.area.get('id'), {
            language: Energine.lang
        });
        this.editor.singleTemplate = this.area.getProperty('single_template');
        this.editor.editorId = this.area.get('id');

        /**
         * Overlay.
         * @type {Overlay}
         */
        this.overlay = new Overlay();
        /*this.editor.on('blur', function () {
            console.log(this.area)
            this.area.removeClass('activeEditor');
            this.save();
        }.bind(this));
        this.editor.on('focus', function () {
            console.log(this.area)
            this.area.addClass('activeEditor');
        }.bind(this));*/
    },

    /**
     * Save.
     *
     * @function
     * @public
     * @param {boolean} [async = true] Defines whether the request be asynchronous or not.
     * @param {function} [onSuccess = undefined] User defined function that is called after sucess saving
     */
    save: function (async, onSuccess) {
        if (this.editor.checkDirty()) {
            if (async == undefined) {
                async = true;
            }
            if (!async) {
                this.overlay.show();
            }

            var data = 'data=' + encodeURIComponent(this.editor.getData());
            if (this.ID) {
                data += '&ID=' + this.ID;
            }
            if (this.num) {
                data += '&num=' + this.num;
            }

            new Request({
                url: this.singlePath + 'save-text',
                async: async,
                method: 'post',
                data: data,
                onSuccess: function (response) {
                    // Временно убрано, т.к. сбрасывает курсор в начала редактируемой области
                    // и из за этого неправильно работают некоторые ф-ции эдитора.
                    //this.editor.setData(response);
                    if (onSuccess)onSuccess.call(this);
                    this.editor.resetDirty();
                    if (!async) {
                        this.overlay.hide();
                    }
                }.bind(this)
            }).send();
        }

    }
});
<|endoftext|>


<commit_before>Router = function () {
  this.globals = [];
  this.subscriptions = Function.prototype;

  this._tracker = this._buildTracker();
  this._current = {};

  // tracks the current path change
  this._onEveryPath = new Tracker.Dependency();

  this._globalRoute = new Route(this);

  this._triggersEnter = [];
  this._triggersExit = [];
  this._middleware = [];
  this._routes = [];
  this._routesMap = {};
  this._updateCallbacks();
  this.notFound = this.notfound = null;
  // indicate it's okay (or not okay) to run the tracker
  // when doing subscriptions
  // using a number and increment it help us to support FlowRouter.go()
  // and legitimate reruns inside tracker on the same event loop.
  // this is a solution for #145
  this.safeToRun = 0;

  var self = this;
  this.triggers = {
    enter: self._getRegisterTriggersFn(self._triggersEnter),
    exit: self._getRegisterTriggersFn(self._triggersExit)
  };

  this.env = {
    replaceState: new Meteor.EnvironmentVariable(),
    reload: new Meteor.EnvironmentVariable()
  };
};

Router.prototype.route = function(path, options, group) {
  if (!/^\/.*/.test(path)) {
    var message = "route's path must start with '/'";
    throw new Error(message);
  }

  options = options || {};
  var self = this;
  var route = new Route(this, path, options, group);

  route._handler = function (context, next) {
    var reload = self.env.reload.get();
    if (!reload && self._current.path === context.path) {
      return;
    }

    var oldRoute = self._current.route;

    self._current = {
      path: context.path,
      context: context,
      params: context.params,
      queryParams: self._qs.parse(context.querystring),
      route: route,
      oldRoute: oldRoute
    };

    // to backward compatibility
    self._current.params.query = self._current.queryParams;

    self._invalidateTracker();
  };

  this._routes.push(route);
  if (options.name) {
    this._routesMap[options.name] = route;
  }

  this._updateCallbacks();

  return route;
};

Router.prototype.group = function(options) {
  return new Group(this, options);
};

Router.prototype.path = function(pathDef, fields, queryParams) {
  if (this._routesMap[pathDef]) {
    pathDef = this._routesMap[pathDef].path;
  }

  // remove trailing slash(es)
  // but keep the root slash if it's the only one
  pathDef = pathDef.match(/^\/{1}$/) ? pathDef : pathDef.replace(/\/+$/, "");

  fields = fields || {};
  var regExp = /(:[\w\(\)\\\+\*\.\?]+)+/g;
  var path = pathDef.replace(regExp, function(key) {
    var firstRegexpChar = key.indexOf("(");
    // get the content behind : and (\\d+/)
    key = key.substring(1, (firstRegexpChar > 0)? firstRegexpChar: undefined);
    // remove +?*
    key = key.replace(/[\+\*\?]+/g, "");

    return fields[key] || "";
  });

  var strQueryParams = this._qs.stringify(queryParams || {});
  if(strQueryParams) {
    path += "?" + strQueryParams;
  }

  return path;
};

Router.prototype.go = function(pathDef, fields, queryParams) {
  var path = this.path(pathDef, fields, queryParams);

  var useReplaceState = this.env.replaceState.get();
  if(useReplaceState) {
    this._page.redirect(path);
  } else {
    this._page(path);
  }
};

Router.prototype.reload = function() {
  var self = this;

  self.env.reload.withValue(true, function() {
    self._page.replace(self._current.path);
  });
};

Router.prototype.redirect = function(path) {
  this._page.redirect(path);
};

Router.prototype.setParams = function(newParams) {
  if(!this._current.route) {return false;}

  var pathDef = this._current.route.path;
  var existingParams = this._current.params;
  var params = {};
  _.each(_.keys(existingParams), function(key) {
    params[key] = existingParams[key];
  });

  params = _.extend(params, newParams);
  var queryParams = this._current.queryParams;

  this.go(pathDef, params, queryParams);
  return true;
};

Router.prototype.setQueryParams = function(newParams) {
  if(!this._current.route) {return false;}

  var queryParams = _.clone(this._current.queryParams);
  _.extend(queryParams, newParams);

  for (var k in queryParams) {
    if (queryParams[k] === null || queryParams[k] === undefined) {
      delete queryParams[k];
    }
  }

  var pathDef = this._current.route.path;
  var params = this._current.params;
  this.go(pathDef, params, queryParams);
  return true;
};

// .current is not reactive
// This is by design. use .getParam() instead
// If you really need to watch the path change, use .watchPathChange()
Router.prototype.current = function() {
  return this._current;
};

Router.prototype.reactiveCurrent = function() {
  var warnMessage =
    ".reactiveCurrent() is deprecated. " +
    "Use .watchPathChange() instead";
  console.warn(warnMessage);

  this.watchPathChange();
  return this.current();
};

// Implementing Reactive APIs
var reactiveApis = [
  'getParam', 'getQueryParam',
  'getRouteName', 'watchPathChange'
];
reactiveApis.forEach(function(api) {
  Router.prototype[api] = function(arg1) {
    // when this is calling, there may not be any route initiated
    // so we need to handle it
    var currentRoute = this._current.route;
    if(!currentRoute) {
      this._onEveryPath.depend();
      return;
    }

    // currently, there is only one argument. If we've more let's add more args
    // this is not clean code, but better in performance
    return currentRoute[api].call(currentRoute, arg1);
  };
});

Router.prototype.middleware = function(middlewareFn) {
  console.warn("'middleware' is deprecated. Use 'triggers' instead");
  var self = this;
  var mw = function(ctx, next) {
    // make sure middlewars run after Meteor has been initialized
    // this is very important for specially for fast render and Meteor.user()
    // availability
    Meteor.startup(function() {
      middlewareFn(ctx.pathname, processNext);
    });

    function processNext(path) {
      if(path) {
        return self._page.redirect(path);
      }
      next();
    }
  };

  this._middleware.push(mw);
  this._updateCallbacks();
  return this;
};

Router.prototype.ready = function() {
  console.warn("'FlowRouter.ready()' is deprecated. Use 'FlowRouter.subsReady()' instead");
  return this.subsReady.apply(this, arguments);
};

Router.prototype.subsReady = function() {
  var callback = null;
  var args = _.toArray(arguments);

  if (typeof _.last(args) === "function") {
    callback = args.pop();
  }

  var currentRoute = this.current().route;
  var globalRoute = this._globalRoute;

  // we need to depend for every route change and
  // rerun subscriptions to check the ready state
  this._onEveryPath.depend();

  if(!currentRoute) {
    return false;
  }

  var subscriptions;
  if(args.length === 0) {
    subscriptions = _.values(globalRoute.getAllSubscriptions());
    subscriptions = subscriptions.concat(_.values(currentRoute.getAllSubscriptions()));
  } else {
    subscriptions = _.map(args, function(subName) {
      return globalRoute.getSubscription(subName) || currentRoute.getSubscription(subName);
    });
  }

  var isReady = function() {
    var ready =  _.every(subscriptions, function(sub) {
      return sub && sub.ready();
    });

    return ready;
  };

  if (callback) {
    Tracker.autorun(function(c) {
      if (isReady()) {
        callback();
        c.stop();
      }
    });
  } else {
    return isReady();
  }
};

Router.prototype.withReplaceState = function(fn) {
  return this.env.replaceState.withValue(true, fn);
};

Router.prototype._notfoundRoute = function(context) {
  this._current = {
    path: context.path,
    context: context,
    params: [],
    queryParams: {},
  };

  // XXX this.notfound kept for backwards compatibility
  this.notFound = this.notFound || this.notfound;
  if(!this.notFound) {
    console.error("There is no route for the path:", context.path);
    return;
  }

  this._current.route = new Route(this, "*", this.notFound);
  this._invalidateTracker();
};

Router.prototype.initialize = function() {
  var self = this;
  this._updateCallbacks();
  // initialize
  this._page();
};

Router.prototype._buildTracker = function() {
  var self = this;

  // main autorun function
  var tracker = Tracker.autorun(function () {
    if(!self._current || !self._current.route) {
      return;
    }

    var route = self._current.route;
    var path = self._current.path;

    if(self.safeToRun === 0) {
      var message =
        "You can't use reactive data sources like Session" +
        " inside the `.subscriptions` method!";
      throw new Error(message);
    }

    // We need to run subscriptions inside a Tracker
    // to stop subs when switching between routes
    // But we don't need to run this tracker with
    // other reactive changes inside the .subscription method
    // We tackle this with the `safeToRun` variable
    self._globalRoute.clearSubscriptions();
    self.subscriptions.call(self._globalRoute, path);
    route.callSubscriptions(self._current);

    // otherwise, computations inside action will trigger to re-run
    // this computation. which we do not need.
    Tracker.nonreactive(function() {
      var currentContext = self._current;

      self._processTriggersEnter(currentContext);

      var isRouteChange = currentContext.oldRoute !== currentContext.route;
      var isFirstRoute = !currentContext.oldRoute;
      // first route is not a route change
      if(isFirstRoute) {
        isRouteChange = false;
      }

      currentContext.route.registerRouteChange(currentContext, isRouteChange);
      route.callAction(currentContext);

      Tracker.afterFlush(function() {
        self._onEveryPath.changed();
        if(isRouteChange) {
          // We need to trigger that route (definition itself) has changed.
          // So, we need to re-run all the register callbacks to current route
          // This is pretty important, otherwise tracker
          // can't identify new route's items

          // We also need to afterFlush, otherwise this will re-run
          // helpers on templates which are marked for destroying
          currentContext.oldRoute.registerRouteClose();
        }
      });
    });

    self.safeToRun--;
  });

  return tracker;
};

Router.prototype._invalidateTracker = function() {
  this.safeToRun++;
  this._tracker.invalidate();
};

Router.prototype._getRegisterTriggersFn = function(triggers) {
  var fn = function(triggerFns, options) {
    options = options || {};

    if (options.only && options.except) {
      var message = "triggers does not support 'only' and 'except' at the same time.";
      throw new Error(message);
    }

    _.each(triggerFns, function(fn) {
      if (typeof fn !== 'function') {
        return;
      }

      if (options.only) {
        fn._only = {};
        _.each(options.only, function(name) {
          fn._only[name] = 1;
        });
      }

      if (options.except) {
        fn._except = {};
        _.each(options.except, function(name) {
          fn._except[name] = 1;
        });
      }

      triggers.push(fn);
    });
  };

  return fn;
};

Router.prototype._shouldCallTrigger = function(current, fn) {
  var name = current.route.name;
  var shouldCall;

  if (typeof fn !== 'function') {
    return false;
  }

  if (fn._only) {
    shouldCall = !!fn._only[name];
  } else if (fn._except) {
    shouldCall = !fn._except[name];
  } else {
    shouldCall = true;
  }

  return shouldCall;
};

Router.prototype._processTriggersEnter = function(current) {
  var self = this;

  _.each(this._triggersEnter, function(fn) {
    if (self._shouldCallTrigger(current, fn)) {
      fn(current);
    }
  });
};

Router.prototype._processTriggersExit = function(ctx, next) {
  var self = this;

  _.each(self._triggersExit, function(fn) {
    if (self._shouldCallTrigger(self._current, fn)) {
      fn(self._current);
    }
  });

  next();
};

Router.prototype._registerRouteTriggersExit = function(route) {
  var self = this;

  if (route._triggersExit.length > 0) {
    // add route's exit triggers
    self._page.exit(route.path, function(ctx, next) {
      _.each(route._triggersExit, function(fn) {
        if (typeof fn === 'function') {
          fn(self._current);
        }
      });

      next();
    });
  }
};

Router.prototype._updateCallbacks = function () {
  var self = this;

  self._page.callbacks = [];
  self._page.exits = [];

  // add global middleware
  _.each(self._middleware, function(fn) {
    self._page("*", fn);
  });

  _.each(self._routes, function(route) {
    self._page(route.path, route._handler);
    self._registerRouteTriggersExit(route);
  });

  self._page.exit("*", self._processTriggersExit.bind(self));

  self._page("*", function(context) {
    self._notfoundRoute(context);
  });
};

Router.prototype._page = page;
Router.prototype._qs = qs;
<commit_msg>Overriding 'page.show' for idempotant routes.<commit_after>Router = function () {
  this.globals = [];
  this.subscriptions = Function.prototype;

  this._tracker = this._buildTracker();
  this._current = {};

  // tracks the current path change
  this._onEveryPath = new Tracker.Dependency();

  this._globalRoute = new Route(this);

  this._triggersEnter = [];
  this._triggersExit = [];
  this._middleware = [];
  this._routes = [];
  this._routesMap = {};
  this._updateCallbacks();
  this.notFound = this.notfound = null;
  // indicate it's okay (or not okay) to run the tracker
  // when doing subscriptions
  // using a number and increment it help us to support FlowRouter.go()
  // and legitimate reruns inside tracker on the same event loop.
  // this is a solution for #145
  this.safeToRun = 0;

  var self = this;
  this.triggers = {
    enter: self._getRegisterTriggersFn(self._triggersEnter),
    exit: self._getRegisterTriggersFn(self._triggersExit)
  };

  this.env = {
    replaceState: new Meteor.EnvironmentVariable(),
    reload: new Meteor.EnvironmentVariable()
  };
};

Router.prototype.route = function(path, options, group) {
  if (!/^\/.*/.test(path)) {
    var message = "route's path must start with '/'";
    throw new Error(message);
  }

  options = options || {};
  var self = this;
  var route = new Route(this, path, options, group);

  route._handler = function (context, next) {
    var oldRoute = self._current.route;

    self._current = {
      path: context.path,
      context: context,
      params: context.params,
      queryParams: self._qs.parse(context.querystring),
      route: route,
      oldRoute: oldRoute
    };

    // to backward compatibility
    self._current.params.query = self._current.queryParams;

    self._invalidateTracker();
  };

  this._routes.push(route);
  if (options.name) {
    this._routesMap[options.name] = route;
  }

  this._updateCallbacks();

  return route;
};

Router.prototype.group = function(options) {
  return new Group(this, options);
};

Router.prototype.path = function(pathDef, fields, queryParams) {
  if (this._routesMap[pathDef]) {
    pathDef = this._routesMap[pathDef].path;
  }

  // remove trailing slash(es)
  // but keep the root slash if it's the only one
  pathDef = pathDef.match(/^\/{1}$/) ? pathDef : pathDef.replace(/\/+$/, "");

  fields = fields || {};
  var regExp = /(:[\w\(\)\\\+\*\.\?]+)+/g;
  var path = pathDef.replace(regExp, function(key) {
    var firstRegexpChar = key.indexOf("(");
    // get the content behind : and (\\d+/)
    key = key.substring(1, (firstRegexpChar > 0)? firstRegexpChar: undefined);
    // remove +?*
    key = key.replace(/[\+\*\?]+/g, "");

    return fields[key] || "";
  });

  var strQueryParams = this._qs.stringify(queryParams || {});
  if(strQueryParams) {
    path += "?" + strQueryParams;
  }

  return path;
};

Router.prototype.go = function(pathDef, fields, queryParams) {
  var path = this.path(pathDef, fields, queryParams);

  var useReplaceState = this.env.replaceState.get();
  if(useReplaceState) {
    this._page.redirect(path);
  } else {
    this._page(path);
  }
};

Router.prototype.reload = function() {
  var self = this;

  self.env.reload.withValue(true, function() {
    self._page.replace(self._current.path);
  });
};

Router.prototype.redirect = function(path) {
  this._page.redirect(path);
};

Router.prototype.setParams = function(newParams) {
  if(!this._current.route) {return false;}

  var pathDef = this._current.route.path;
  var existingParams = this._current.params;
  var params = {};
  _.each(_.keys(existingParams), function(key) {
    params[key] = existingParams[key];
  });

  params = _.extend(params, newParams);
  var queryParams = this._current.queryParams;

  this.go(pathDef, params, queryParams);
  return true;
};

Router.prototype.setQueryParams = function(newParams) {
  if(!this._current.route) {return false;}

  var queryParams = _.clone(this._current.queryParams);
  _.extend(queryParams, newParams);

  for (var k in queryParams) {
    if (queryParams[k] === null || queryParams[k] === undefined) {
      delete queryParams[k];
    }
  }

  var pathDef = this._current.route.path;
  var params = this._current.params;
  this.go(pathDef, params, queryParams);
  return true;
};

// .current is not reactive
// This is by design. use .getParam() instead
// If you really need to watch the path change, use .watchPathChange()
Router.prototype.current = function() {
  return this._current;
};

Router.prototype.reactiveCurrent = function() {
  var warnMessage =
    ".reactiveCurrent() is deprecated. " +
    "Use .watchPathChange() instead";
  console.warn(warnMessage);

  this.watchPathChange();
  return this.current();
};

// Implementing Reactive APIs
var reactiveApis = [
  'getParam', 'getQueryParam',
  'getRouteName', 'watchPathChange'
];
reactiveApis.forEach(function(api) {
  Router.prototype[api] = function(arg1) {
    // when this is calling, there may not be any route initiated
    // so we need to handle it
    var currentRoute = this._current.route;
    if(!currentRoute) {
      this._onEveryPath.depend();
      return;
    }

    // currently, there is only one argument. If we've more let's add more args
    // this is not clean code, but better in performance
    return currentRoute[api].call(currentRoute, arg1);
  };
});

Router.prototype.middleware = function(middlewareFn) {
  console.warn("'middleware' is deprecated. Use 'triggers' instead");
  var self = this;
  var mw = function(ctx, next) {
    // make sure middlewars run after Meteor has been initialized
    // this is very important for specially for fast render and Meteor.user()
    // availability
    Meteor.startup(function() {
      middlewareFn(ctx.pathname, processNext);
    });

    function processNext(path) {
      if(path) {
        return self._page.redirect(path);
      }
      next();
    }
  };

  this._middleware.push(mw);
  this._updateCallbacks();
  return this;
};

Router.prototype.ready = function() {
  console.warn("'FlowRouter.ready()' is deprecated. Use 'FlowRouter.subsReady()' instead");
  return this.subsReady.apply(this, arguments);
};

Router.prototype.subsReady = function() {
  var callback = null;
  var args = _.toArray(arguments);

  if (typeof _.last(args) === "function") {
    callback = args.pop();
  }

  var currentRoute = this.current().route;
  var globalRoute = this._globalRoute;

  // we need to depend for every route change and
  // rerun subscriptions to check the ready state
  this._onEveryPath.depend();

  if(!currentRoute) {
    return false;
  }

  var subscriptions;
  if(args.length === 0) {
    subscriptions = _.values(globalRoute.getAllSubscriptions());
    subscriptions = subscriptions.concat(_.values(currentRoute.getAllSubscriptions()));
  } else {
    subscriptions = _.map(args, function(subName) {
      return globalRoute.getSubscription(subName) || currentRoute.getSubscription(subName);
    });
  }

  var isReady = function() {
    var ready =  _.every(subscriptions, function(sub) {
      return sub && sub.ready();
    });

    return ready;
  };

  if (callback) {
    Tracker.autorun(function(c) {
      if (isReady()) {
        callback();
        c.stop();
      }
    });
  } else {
    return isReady();
  }
};

Router.prototype.withReplaceState = function(fn) {
  return this.env.replaceState.withValue(true, fn);
};

Router.prototype._notfoundRoute = function(context) {
  this._current = {
    path: context.path,
    context: context,
    params: [],
    queryParams: {},
  };

  // XXX this.notfound kept for backwards compatibility
  this.notFound = this.notFound || this.notfound;
  if(!this.notFound) {
    console.error("There is no route for the path:", context.path);
    return;
  }

  this._current.route = new Route(this, "*", this.notFound);
  this._invalidateTracker();
};

Router.prototype.initialize = function() {
  var self = this;
  this._updateCallbacks();

  var oldShow = this._page.show;

  this._page.show = function(path, state, dispatch, push) {
    var reload = self.env.reload.get();
    if (!reload && self._current.path === path) {
      return;
    }

    oldShow(path, state, dispatch, push);
  };

  // initialize
  this._page();
};

Router.prototype._buildTracker = function() {
  var self = this;

  // main autorun function
  var tracker = Tracker.autorun(function () {
    if(!self._current || !self._current.route) {
      return;
    }

    var route = self._current.route;
    var path = self._current.path;

    if(self.safeToRun === 0) {
      var message =
        "You can't use reactive data sources like Session" +
        " inside the `.subscriptions` method!";
      throw new Error(message);
    }

    // We need to run subscriptions inside a Tracker
    // to stop subs when switching between routes
    // But we don't need to run this tracker with
    // other reactive changes inside the .subscription method
    // We tackle this with the `safeToRun` variable
    self._globalRoute.clearSubscriptions();
    self.subscriptions.call(self._globalRoute, path);
    route.callSubscriptions(self._current);

    // otherwise, computations inside action will trigger to re-run
    // this computation. which we do not need.
    Tracker.nonreactive(function() {
      var currentContext = self._current;

      self._processTriggersEnter(currentContext);

      var isRouteChange = currentContext.oldRoute !== currentContext.route;
      var isFirstRoute = !currentContext.oldRoute;
      // first route is not a route change
      if(isFirstRoute) {
        isRouteChange = false;
      }

      currentContext.route.registerRouteChange(currentContext, isRouteChange);
      route.callAction(currentContext);

      Tracker.afterFlush(function() {
        self._onEveryPath.changed();
        if(isRouteChange) {
          // We need to trigger that route (definition itself) has changed.
          // So, we need to re-run all the register callbacks to current route
          // This is pretty important, otherwise tracker
          // can't identify new route's items

          // We also need to afterFlush, otherwise this will re-run
          // helpers on templates which are marked for destroying
          currentContext.oldRoute.registerRouteClose();
        }
      });
    });

    self.safeToRun--;
  });

  return tracker;
};

Router.prototype._invalidateTracker = function() {
  this.safeToRun++;
  this._tracker.invalidate();
};

Router.prototype._getRegisterTriggersFn = function(triggers) {
  var fn = function(triggerFns, options) {
    options = options || {};

    if (options.only && options.except) {
      var message = "triggers does not support 'only' and 'except' at the same time.";
      throw new Error(message);
    }

    _.each(triggerFns, function(fn) {
      if (typeof fn !== 'function') {
        return;
      }

      if (options.only) {
        fn._only = {};
        _.each(options.only, function(name) {
          fn._only[name] = 1;
        });
      }

      if (options.except) {
        fn._except = {};
        _.each(options.except, function(name) {
          fn._except[name] = 1;
        });
      }

      triggers.push(fn);
    });
  };

  return fn;
};

Router.prototype._shouldCallTrigger = function(current, fn) {
  var name = current.route.name;
  var shouldCall;

  if (typeof fn !== 'function') {
    return false;
  }

  if (fn._only) {
    shouldCall = !!fn._only[name];
  } else if (fn._except) {
    shouldCall = !fn._except[name];
  } else {
    shouldCall = true;
  }

  return shouldCall;
};

Router.prototype._processTriggersEnter = function(current) {
  var self = this;

  _.each(this._triggersEnter, function(fn) {
    if (self._shouldCallTrigger(current, fn)) {
      fn(current);
    }
  });
};

Router.prototype._processTriggersExit = function(ctx, next) {
  var self = this;

  _.each(self._triggersExit, function(fn) {
    if (self._shouldCallTrigger(self._current, fn)) {
      fn(self._current);
    }
  });

  next();
};

Router.prototype._registerRouteTriggersExit = function(route) {
  var self = this;

  if (route._triggersExit.length > 0) {
    // add route's exit triggers
    self._page.exit(route.path, function(ctx, next) {
      _.each(route._triggersExit, function(fn) {
        if (typeof fn === 'function') {
          fn(self._current);
        }
      });

      next();
    });
  }
};

Router.prototype._updateCallbacks = function () {
  var self = this;

  self._page.callbacks = [];
  self._page.exits = [];

  // add global middleware
  _.each(self._middleware, function(fn) {
    self._page("*", fn);
  });

  _.each(self._routes, function(route) {
    self._page(route.path, route._handler);
    self._registerRouteTriggersExit(route);
  });

  self._page.exit("*", self._processTriggersExit.bind(self));

  self._page("*", function(context) {
    self._notfoundRoute(context);
  });
};

Router.prototype._page = page;
Router.prototype._qs = qs;
<|endoftext|>


<commit_before>var _ = require('underscore');
var CartoValidationError = require('../error-handling/carto-validation-error');

// Taken from https://carto.com/docs/carto-engine/maps-api/tile-aggregation/#columns
var VALID_OPERATIONS = {
  avg: true,
  sum: true,
  min: true,
  max: true,
  mode: true
};

// Taken from https://carto.com/docs/carto-engine/maps-api/tile-aggregation/#placement
var VALID_PLACEMENTS = {
  'point-sample': true,
  'point-grid': true,
  'centroid': true
};

var VALID_RESOLUTIONS = [0.5, 1, 2, 4, 8, 16, 32, 64, 128, 256];

/**
 * An aggregation can be passed to a {@link carto.layer.Layer} to reduce the number of visible points
 * increasing the performance.
 *
 * See {@link https://carto.com/docs/carto-engine/maps-api/tile-aggregation} for more info.
 *
 * @param {object} opts
 * @param {number} opts.threshold - The minimum number of rows in the dataset for aggregation to be applied
 * @param {number} opts.resolution - The cell-size of the spatial aggregation grid [more info]{@link https://carto.com/docs/carto-engine/maps-api/tile-aggregation/#resolution}
 * @param {string} opts.placement - The kind of [aggregated geometry]{@link https://carto.com/docs/carto-engine/maps-api/tile-aggregation/#placement} generated
 * @param {object} opts.columns - The new columns are computed by a applying an aggregate function to all the points in each group
 * @param {string} opts.columns.aggregatedFunction - The Function used to aggregate the points: avg (average), min (minimum), max (maximum) and mode (the most frequent value in the group)
 * @param {string} opts.columns.aggregatedColumn - The name of the original column to be aggregated.
 *
 * @example
 * // Create a layer with aggregated data.
 * const aggregationOptions = {
 *   // Always apply aggregation
 *   threshold: 1,
 *   //
 *   resolution: 1,
 *   // New points will be displayed at the center of the grid
 *   placement: 'point-grid',
 *   columns: {
 *     // The key is the name of the new generated column
 *     agg_population: {
 *     // Points will display the average of the aggregated data.
 *     aggregateFunction: 'avg',
 *     // The column to be aggregated
 *     aggregatedColumn: 'population'
 *    },
 *   }
 * };
 * const aggregation = new Aggregation(options);
 * const layer = new carto.layer.Layer(source, style, { aggregation: aggregation });
 *
 * @constructor
 * @api
 * @memberof carto.layer
 */
function Aggregation (opts) {
  if (!_.isFinite(opts.threshold)) {
    throw _getValidationError('thresholdRequired');
  }

  if (!_.isFinite(opts.threshold) || opts.threshold < 1 || Math.floor(opts.threshold) !== opts.threshold) {
    throw _getValidationError('invalidThreshold');
  }

  if (!_.isFinite(opts.resolution)) {
    throw _getValidationError('resolutionRequired');
  }

  if (!_.contains(VALID_RESOLUTIONS, opts.resolution)) {
    throw _getValidationError('invalidResolution');
  }

  if (!opts.placement) {
    throw _getValidationError('placementRequired');
  }

  if (!VALID_PLACEMENTS[opts.placement]) {
    throw _getValidationError('invalidPlacement');
  }

  _checkColumns(opts.columns);

  return {
    threshold: opts.threshold,
    resolution: opts.resolution,
    placement: opts.placement,
    columns: _transformColumns(opts.columns)
  };
}

function _checkColumns (columns) {
  if (!columns) {
    throw new Error();
  }

  Object.keys(columns).forEach(function (key) {
    _checkColumn(columns, key);
  });
}

function _checkColumn (columns, key) {
  if (!columns[key].aggregatedColumn) {
    throw _getValidationError('columnAggregatedColumnRequired' + key);
  }

  if (!_.isString(columns[key].aggregatedColumn)) {
    throw _getValidationError('invalidColumnAggregatedColumn' + key);
  }

  if (!columns[key].aggregateFunction) {
    throw _getValidationError('columnFunctionRequired' + key);
  }

  if (!VALID_OPERATIONS[columns[key].aggregateFunction]) {
    throw _getValidationError('invalidColumnFunction' + key);
  }
}

function _getValidationError (code) {
  return new CartoValidationError('aggregation', code);
}

// Windshaft uses snake_case for column parameters
function _transformColumns (columns) {
  var returnValue = {};
  Object.keys(columns).forEach(function (key) {
    returnValue[key] = _columnToSnakeCase(columns[key]);
  });
  return returnValue;
}

// Windshaft uses snake_case for column parameters
function _columnToSnakeCase (column) {
  return {
    aggregate_function: column.aggregateFunction,
    aggregated_column: column.aggregatedColumn
  };
}

module.exports = Aggregation;
<commit_msg>Update aggregation docs<commit_after>var _ = require('underscore');
var CartoValidationError = require('../error-handling/carto-validation-error');

// Taken from https://carto.com/docs/carto-engine/maps-api/tile-aggregation/#columns
var VALID_OPERATIONS = {
  avg: true,
  sum: true,
  min: true,
  max: true,
  mode: true
};

// Taken from https://carto.com/docs/carto-engine/maps-api/tile-aggregation/#placement
var VALID_PLACEMENTS = {
  'point-sample': true,
  'point-grid': true,
  'centroid': true
};

var VALID_RESOLUTIONS = [0.5, 1, 2, 4, 8, 16, 32, 64, 128, 256];

/**
 * An aggregation can be passed to a {@link carto.layer.Layer} to reduce the number of visible points
 * increasing the performance.
 *
 * See {@link https://carto.com/docs/carto-engine/maps-api/tile-aggregation} for more info.
 *
 * @param {object} opts
 * @param {number} opts.threshold - The minimum number of rows in the dataset for aggregation to be applied
 * @param {number} opts.resolution - The cell-size of the spatial aggregation grid [more info]{@link https://carto.com/docs/carto-engine/maps-api/tile-aggregation/#resolution}
 * @param {string} opts.placement - The kind of [aggregated geometry]{@link https://carto.com/docs/carto-engine/maps-api/tile-aggregation/#placement} generated
 * @param {object} opts.columns - The new columns are computed by a applying an aggregate function to all the points in each group
 * @param {string} opts.columns.aggregatedFunction - The Function used to aggregate the points: avg (average), sum, min (minimum), max (maximum) and mode (the most frequent value in the group)
 * @param {string} opts.columns.aggregatedColumn - The name of the original column to be aggregated.
 *
 * @example
 * // Create a layer with aggregated data.
 * const aggregationOptions = {
 *   // CARTO applies aggregation if your dataset has more than threshold rows. In this case, more than 1 row.
 *   threshold: 1,
 *   // Defines the cell-size of the aggregation grid. In this case, 1x1 pixel.
 *   resolution: 1,
 *   // Where the new point will be placed. In this case, at the center of the grid.
 *   placement: 'point-grid',
 *   // Here we define the aggregated columns that we want to obtain.
 *   columns: {
 *     // Each property key is the name of the new generated column
 *     avg_population: {
 *       // The aggregated column will contain the average of the original data.
 *       aggregateFunction: 'avg',
 *       // The column to be aggregated
 *       aggregatedColumn: 'population'
 *     }, {
 *     min_population: {
 *       aggregateFunction: 'min',
 *       aggregatedColumn: 'population
 *   }
 * };
 * const aggregation = new Aggregation(options);
 * const layer = new carto.layer.Layer(source, style, { aggregation: aggregation });
 *
 * @constructor
 * @api
 * @memberof carto.layer
 */
function Aggregation (opts) {
  if (!_.isFinite(opts.threshold)) {
    throw _getValidationError('thresholdRequired');
  }

  if (!_.isFinite(opts.threshold) || opts.threshold < 1 || Math.floor(opts.threshold) !== opts.threshold) {
    throw _getValidationError('invalidThreshold');
  }

  if (!_.isFinite(opts.resolution)) {
    throw _getValidationError('resolutionRequired');
  }

  if (!_.contains(VALID_RESOLUTIONS, opts.resolution)) {
    throw _getValidationError('invalidResolution');
  }

  if (!opts.placement) {
    throw _getValidationError('placementRequired');
  }

  if (!VALID_PLACEMENTS[opts.placement]) {
    throw _getValidationError('invalidPlacement');
  }

  _checkColumns(opts.columns);

  return {
    threshold: opts.threshold,
    resolution: opts.resolution,
    placement: opts.placement,
    columns: _transformColumns(opts.columns)
  };
}

function _checkColumns (columns) {
  if (!columns) {
    throw new Error();
  }

  Object.keys(columns).forEach(function (key) {
    _checkColumn(columns, key);
  });
}

function _checkColumn (columns, key) {
  if (!columns[key].aggregatedColumn) {
    throw _getValidationError('columnAggregatedColumnRequired' + key);
  }

  if (!_.isString(columns[key].aggregatedColumn)) {
    throw _getValidationError('invalidColumnAggregatedColumn' + key);
  }

  if (!columns[key].aggregateFunction) {
    throw _getValidationError('columnFunctionRequired' + key);
  }

  if (!VALID_OPERATIONS[columns[key].aggregateFunction]) {
    throw _getValidationError('invalidColumnFunction' + key);
  }
}

function _getValidationError (code) {
  return new CartoValidationError('aggregation', code);
}

// Windshaft uses snake_case for column parameters
function _transformColumns (columns) {
  var returnValue = {};
  Object.keys(columns).forEach(function (key) {
    returnValue[key] = _columnToSnakeCase(columns[key]);
  });
  return returnValue;
}

// Windshaft uses snake_case for column parameters
function _columnToSnakeCase (column) {
  return {
    aggregate_function: column.aggregateFunction,
    aggregated_column: column.aggregatedColumn
  };
}

module.exports = Aggregation;
<|endoftext|>


<commit_before>
/**
 * Module dependencies.
 */

var cluster = require('../')
  , http = require('http');

var server = http.createServer(function(req, res){
  console.log('%s %s', req.method, req.url);
  var body = 'Hello World';
  res.writeHead(200, { 'Content-Length': body.length });
  res.end(body);
});

// Launch the cluster:
//   $ nohup node examples/cli.js &

// Check the status:
//   $ node examples/cli.js status

// View other commands:
//   $ node examples/cli.js --help

cluster(server)
  .use(cluster.pidfiles())
  .use(cluster.debug())
  .use(cluster.cli())
  .listen(80);
<commit_msg>switch port for cli example<commit_after>
/**
 * Module dependencies.
 */

var cluster = require('../')
  , http = require('http');

var server = http.createServer(function(req, res){
  console.log('%s %s', req.method, req.url);
  var body = 'Hello World';
  res.writeHead(200, { 'Content-Length': body.length });
  res.end(body);
});

// Launch the cluster:
//   $ nohup node examples/cli.js &

// Check the status:
//   $ node examples/cli.js status

// View other commands:
//   $ node examples/cli.js --help

cluster(server)
  .use(cluster.pidfiles())
  .use(cluster.debug())
  .use(cluster.cli())
  .listen(3000);
<|endoftext|>


<commit_before>/* Global io, Player, Room, mainColor */
var player;

function showPlayer(player) {
	var li = document.createElement("li");
	li.innerHTML = player.name;
	li.style.color = player.color;
	document.getElementById("players").appendChild(li);
}

function initGame() {
	var startButtom = document.getElementById("start");
	startButtom.onclick = startIO;
}

function startIO() {
	player = new Player(
				document.querySelector("#getname").value,
				mainColor,
				"MYSELF"
			);

	var socket = io(window.location.href);
	socket.emit("newplayer", {
			name: player.name,
			color: player.color
	});
	hide("intro");
	show("game");

	showPlayer(player);
	manageSocketEvents(socket);
}

function hide(id) {
	document.getElementById(id).style.display = "none";
}

function show(id) {
	document.getElementById(id).style.display = "block";
}
function convertToPlayer( player ){
	return new Player(player.name, player.color, player.id);
}

function showStatus(status) {
	document.getElementById("status").innerHTML = status;
}

function countToGame(room) {
	room.timeLeft--;
	if (room.timeLeft) {
		showStatus("Finding players... " + room.timeLeft);
	} else {
		clearInterval(room.timer);
		showStatus("Game starting... ");
	}
}

function countToStart(tmp, room) {
	if (room.timer) clearInterval(room.timer);
	if (tmp.time) {
		showStatus("Start in " + tmp.time);
	} else {
		clearInterval(tmp.timer);
		showStatus("Go!");
	}
	tmp.time--;
}

function manageSocketEvents(socket) {
	var room = new Room();
	socket.on("foundroom", function (newRoom) {
		room.name = newRoom.name;
		room.players = newRoom.players;
		room.numFinished = newRoom.numFinished;
		room.timeLeft = newRoom.timeLeft;

		document.getElementById("room-name").innerHTML += room.name;

		for (var i = 0; i < room.players.length; i++) {
			room.players[i]  = convertToPlayer(room.players[i]);
			showPlayer(room.players[i]);
		}

		showStatus("Finding players... " + room.timeLeft);
		room.timer = setInterval(countToGame, 1000, room);
	});

	socket.on("playerentered", function (player) {
		player = convertToPlayer(player);
		room.players.push(player);
		showPlayer(player);
		player.displayCursor();
	});

	socket.on("gamestart", function (text) {
		room.startTime = new Date();
		room.playing = room.players.slice();
		setTimeout(startGame, 5000, socket, text);
		var tmp = {time: 5};
		tmp.timer = setInterval(countToStart, 1000, tmp, room);

		for (var i = 0; i < text.length; i++) {
			var span = document.createElement("span")
			span.innerHTML = text[i];
			document.getElementById("text").appendChild(span);
		}
		player.typed(0);
		for (var i = 0; i < room.playing.length; i++) {
			room.playing[i].typed(0);
		}
	});

	socket.on("typed", function (data) {
		var player = findPlayer(data.id, room.playing);
		if (!player) return;
		player.typed(data.pos);
	});

	socket.on("finish", function (data) {
		// data = {id}
		var i = find(data.id, room.playing)
		if (i == -1) return;
		room.playing[i].endTime = new Date();
		room.finished.push(room.playing.slice(i ,1)[0]);
		room.numFinished++;
	});

	socket.on("end", function () {
		room.endTime = new Date();
		finishGame();
		// generateStats( room ) - ?
	});

}

function findPlayer(id, players){
	for (var i = 0; i < players.length; i++)
		if (players[i].id == id)
			return players[i];
}

function find(id, players) {
	for (var i = 0; i < players.length; i++)
		if (players[i].id == id)
			return i;
	return -1;
}

function startGame( socket, text ){
	addEventListener("keypress", function(e){
		e.preventDefault();
		keypress(e,socket,text);
	});
}
function finishGame( room ){
	removeEventListener("keypress")
}

function keypress(e, socket, text){
	// TODO
	var char = keysight(e).char;

	if (char == text[player.pos]) {
		player.typed(player.pos + 1);
	} else {
		// Wrong keypress
		return;
	}

	if (text.length === player.pos) {
		// TODO
		socket.emit("finish", {
			time: 1
		})
		finishGame();
	//} else if (text[player.pos].match(/\W/)) {
	} else {
		socket.emit("typed", {
			pos: player.pos
		});
	}
}
<commit_msg>[C] fixed remove event listener<commit_after>/* Global io, Player, Room, mainColor */
var player;

function showPlayer(player) {
	var li = document.createElement("li");
	li.innerHTML = player.name;
	li.style.color = player.color;
	document.getElementById("players").appendChild(li);
}

function initGame() {
	var startButtom = document.getElementById("start");
	startButtom.onclick = startIO;
}

function startIO() {
	player = new Player(
				document.querySelector("#getname").value,
				mainColor,
				"MYSELF"
			);

	var socket = io(window.location.href);
	socket.emit("newplayer", {
			name: player.name,
			color: player.color
	});
	hide("intro");
	show("game");

	showPlayer(player);
	manageSocketEvents(socket);
}

function hide(id) {
	document.getElementById(id).style.display = "none";
}

function show(id) {
	document.getElementById(id).style.display = "block";
}
function convertToPlayer( player ){
	return new Player(player.name, player.color, player.id);
}

function showStatus(status) {
	document.getElementById("status").innerHTML = status;
}

function countToGame(room) {
	room.timeLeft--;
	if (room.timeLeft) {
		showStatus("Finding players... " + room.timeLeft);
	} else {
		clearInterval(room.timer);
		showStatus("Game starting... ");
	}
}

function countToStart(tmp, room) {
	if (room.timer) clearInterval(room.timer);
	if (tmp.time) {
		showStatus("Start in " + tmp.time);
	} else {
		clearInterval(tmp.timer);
		showStatus("Go!");
	}
	tmp.time--;
}

function manageSocketEvents(socket) {
	var room = new Room();
	socket.on("foundroom", function (newRoom) {
		room.name = newRoom.name;
		room.players = newRoom.players;
		room.numFinished = newRoom.numFinished;
		room.timeLeft = newRoom.timeLeft;

		document.getElementById("room-name").innerHTML += room.name;

		for (var i = 0; i < room.players.length; i++) {
			room.players[i]  = convertToPlayer(room.players[i]);
			showPlayer(room.players[i]);
		}

		showStatus("Finding players... " + room.timeLeft);
		room.timer = setInterval(countToGame, 1000, room);
	});

	socket.on("playerentered", function (player) {
		player = convertToPlayer(player);
		room.players.push(player);
		showPlayer(player);
		player.displayCursor();
	});

	socket.on("gamestart", function (text) {
		room.startTime = new Date();
		room.playing = room.players.slice();
		setTimeout(startGame, 5000, socket, text);
		var tmp = {time: 5};
		tmp.timer = setInterval(countToStart, 1000, tmp, room);

		for (var i = 0; i < text.length; i++) {
			var span = document.createElement("span")
			span.innerHTML = text[i];
			document.getElementById("text").appendChild(span);
		}
		player.typed(0);
		for (var i = 0; i < room.playing.length; i++) {
			room.playing[i].typed(0);
		}
	});

	socket.on("typed", function (data) {
		var player = findPlayer(data.id, room.playing);
		if (!player) return;
		player.typed(data.pos);
	});

	socket.on("finish", function (data) {
		// data = {id}
		var i = find(data.id, room.playing)
		if (i == -1) return;
		room.playing[i].endTime = new Date();
		room.finished.push(room.playing.slice(i ,1)[0]);
		room.numFinished++;
	});

	socket.on("end", function () {
		room.endTime = new Date();
		finishGame();
		// generateStats( room ) - ?
	});

}

function findPlayer(id, players){
	for (var i = 0; i < players.length; i++)
		if (players[i].id == id)
			return players[i];
}

function find(id, players) {
	for (var i = 0; i < players.length; i++)
		if (players[i].id == id)
			return i;
	return -1;
}

var listener;
function startGame( socket, text ){
	addEventListener("keypress", listener = function (e){
		e.preventDefault();
		keypress(e,socket,text);
	});
}
function finishGame( room ){
	removeEventListener("keypress", listener)
}

function keypress(e, socket, text){
	// TODO
	var char = keysight(e).char;

	if (char == text[player.pos]) {
		player.typed(player.pos + 1);
	} else {
		// Wrong keypress
		return;
	}

	if (text.length === player.pos) {
		// TODO
		socket.emit("finish", {
			time: 1
		})
		finishGame();
	//} else if (text[player.pos].match(/\W/)) {
	} else {
		socket.emit("typed", {
			pos: player.pos
		});
	}
}
<|endoftext|>