Skip to content
Snippets Groups Projects
Forked from it-lab / grade
Source project has a limited visibility.
common.js 14.87 KiB
let doubleNumReg = /.*_(\d+)_(\d+)$/;
let singleNumReg = /\w_(\d+)$/;

class Base {

    static parseID($this) {
        singleNumReg.lastIndex = 0;
        return +singleNumReg.exec($this.attr('id'))[1];
    }

    static parsePosition($this) {
        doubleNumReg.lastIndex = 0;
        return doubleNumReg.exec($this.attr('id')).splice(1, 2);
    }

    static parseSettings() {
        let $hiddenInfo = $('#hidden_div');
        let $disciplineID = $('#disciplineID');
        let $pageType = $('#pageType');

        let settings = $.parseJSON($hiddenInfo.html());
        settings.disciplineID = $disciplineID.val();
        settings.pageType = $pageType.val();

        $hiddenInfo.remove();
        $disciplineID.remove();
        $pageType.remove();

        return settings;
    }
}

class Cursor {
    constructor() {
        this.col = 0;
        this.row = 0;
    }

    update(col, row) {
        this.col = col;
        this.row = row;
    }

    get() {
        return [this.col, this.row];
    }

    parseLocation($this) {
        let [col, row] = Base.parsePosition($this);
        this.update(+col, +row);
    }

    isEmpty() {
        return (this.col == 0) && (this.row == 0);
    }
}

class Cell {
    constructor() {
        this.update(0, 0, 0);
    }

    update(student, submodule, maxRate) {
        this.student = student;
        this.submodule = submodule;
        this.maxRate = maxRate;
    }
}

class RateInfo {
    constructor(settings, cursor, cell) {
        this.settings = settings;
        this.cursor = cursor;
        this.cell = cell;

        this.$submodulesInfo = $('.RatingTableSubmodulesInfo');
        this.$submodulesHead = $('.RatingTableSubmodulesHead');
        this.$submodulesMaxRate = $('.RatingTableSubmodulesHeadMaxRate');
        this.$modulesHead = $('.RatingTableModulesHead');

        this.$tdInfoWrap = $('#tdInfo_wrap');
        this.$tdInfo = this.$tdInfoWrap.children('#tdInfo');
        this.$studentInfo = this.$tdInfo.children('#student').children('b');
        this.$submoduleInfo = this.$tdInfo.children('#submodule').children('b');
        this.$rateInfo = this.$tdInfo.children('#maxRate').children('b');
    }

    show($this) {
        var disciplinePassRate = (this.settings.Type === "exam") ? 38 : 60;
        var [col] = this.cursor.get();
        // Получаем подмодуль
        var jCurSubmoduleInfo = this.$submodulesInfo.children(`.col_${col}:first`);
        var jCurSubmoduleHead = this.$submodulesHead.children(`.col_${col}:first`);

        var submodule = +jCurSubmoduleInfo.attr('id');
        var student = parseID($this.siblings('.studentCell'));
        var maxRate = +this.$submodulesMaxRate.children(`.col_${col}`).text();

        // Проверяем допустимое значение (только для добора)
        if ($this.hasClass('additionalCell')) {
            var semesterRate = parseInt($this.siblings('.semesterRateResultCell').text());
            maxRate = (semesterRate <= disciplinePassRate) ? disciplinePassRate - semesterRate : 0;
        }
        this.cell.update(student, submodule, maxRate);

        let submoduleTitle = (jCurSubmoduleHead.length < 1 && this.$modulesHead.children('.bonus').length > 0)
            ? 'Бонусные баллы'
            : (jCurSubmoduleHead.length == 0) ? 'Добор баллов' : jCurSubmoduleHead.text(); // todo: убрать костыль
        let stdName = $this.siblings('.studentCell').text();

        this.$tdInfoWrap.show();
        this.$studentInfo.html(stdName);
        this.$submoduleInfo.html(submoduleTitle);
        this.$rateInfo.html(this.cell.maxRate);
    };

    hide() {
        this.$tdInfoWrap.hide();
        this.cell.update(0, 0, 0);
    };

}

const defaultCellBackground = '#fff';
const focusCellBackground = '#f1f1f1';
class Highlighting {
    constructor(settings, cursor) {
        this.settings = settings;
        this.cursor = cursor;
    }

    static setRowBackground(color, [col, row]) {
        let $cols = $(`td#col_${col}`);
        let $rows = $(`tr#row_${row}`);
        let $rowStatic = $rows.find('.staticCell');

        $cols.filter('.commonCell')
            .add($cols.filter('.staticCell'))
            .add($rows.find('.commonCell'))
            .add($rowStatic)
            .children('input')
            .add($rowStatic)
            .each((_, elem) => $(elem).css('background-color', color));
    }

    // Ставим подстветку
    focus($this) {
        this.cursor.parseLocation($this);
        Highlighting.setRowBackground(focusCellBackground, this.cursor.get());
        $this.children('input').css('background-color', defaultCellBackground);
    };

    // Убираем подстветку
    focusOut() {
        Highlighting.setRowBackground(defaultCellBackground, this.cursor.get());
    }
}

class GroupFilter {
    constructor(settings, $table, groupID) {
        this.settings = settings;
        this.$table = $table.children('tbody');
        this.groupSelected = +groupID;
    }

    // Скрываем все остальные группы
    // 0 - показать все
    filterGroups(groupID) {
        let $trs = this.$table.children().slice(3);
        $trs.show();

        if (+groupID !== 0)
            $trs.not(`.group_${groupID}`).hide();
    }

    listen($selector) {
        $selector.children('[value=' + this.groupSelected + ']').prop('selected', true);
        this.filterGroups(this.groupSelected);

        let self = this;
        $selector.change(function () {
            self.groupSelected = +$(this).val();
            if (self.groupSelected >= 0) {
                self.filterGroups(self.groupSelected);
                let disciplineID = self.settings.disciplineID;
                let groupSelected = self.groupSelected;
                $.post(URLdir + 'handler/rating/SelectGroup', {disciplineID, groupSelected});
            }
        });
    }
}

class TableAdjuster {
    constructor($wrapper, $table) {
        this.$wrapper = $wrapper;
        this.$table = $table;
    }

    adjust() {
        let tableWidth = this.$table.get(0).scrollWidth;
        // check scroll bar
        if (tableWidth <= this.$table.width() + 10)
            return;

        //correct
        tableWidth *= 1.1;
        let maxWidth = $(window).width() * 0.95;
        let newWidth = (tableWidth > maxWidth) ? maxWidth : tableWidth;
        this.$wrapper.css('max-width', newWidth);
    };

    autoResize() {
        $(window).resize(() => this.adjust());
    }
}

function Rating() {

    let settings = Base.parseSettings();
    let cell = new Cell();
    let cursor = new Cursor();
    let rateInfo = new RateInfo(settings, cursor, cell);
    let highlighter = new Highlighting(settings, cursor);
    let adjuster = new TableAdjuster($('.main_layer'), $('.main_content'));
    let filter = new GroupFilter(settings, $('.studentsRate'), settings['GroupID_Filter']);

    const Direction = {
        Up: 0,
        Right: 1,
        Down: 2,
        Left: 3
    };

    /**
     * @param direction Direction в каком направлении искать следующую ячейку для перемещения фокуса
     * @return jQuery клетка, в которую надо переместиться или пустой jquery list, если в этом направлении нет подходящих ячеек
     */
    let getDesiredCell = function (direction) {
        let [col, row] = cursor.get();
        let dx = (direction == Direction.Left) ? -1
            : (direction == Direction.Right) ? 1 : 0;
        let dy = (direction == Direction.Up) ? -1
            : (direction == Direction.Down) ? 1 : 0;

        let currentCell;
        do {
            row += dy;
            col += dx;
            currentCell = $(`#col_row_${col}_${row}`);
        } while (currentCell.length > 0 && currentCell.children('input').attr('disabled') === 'disabled');
        return currentCell;
    };

    let isFocusCell = false; // Стоит фокус на ячейке или нет
    let oldRate = 0;
    var cancelFlag = false;


    let initGeneralCells = function (finalize) {
        const keyDirs = {
            27: Direction.Down, // esc
            13: Direction.Down, // enter
            40: Direction.Down, // down arrow
            38: Direction.Up, // up arrow
            39: Direction.Right, // right arrow
            37: Direction.Left // left arrow
        };

        $('.studentsRate tbody')
            .on('mouseenter', '.commonCell', function() {
                if (isFocusCell === false)
                    highlighter.focus($(this));
            })
            .on('mouseleave', '.commonCell', function () {
                if (isFocusCell === false)
                    highlighter.focusOut();
            })
            .on('focusin', '.commonCell', function () {
                isFocusCell = true;
                highlighter.focus($(this));
                rateInfo.show($(this));

                var value = $(this).children('input').val();
                oldRate = (value !== '') ? +value : -1;
            })
            .on('focusout', '.commonCell', function () {
                isFocusCell = false;
                if (cancelFlag) {
                    var str = (oldRate != -1) ? oldRate : '';
                    $(this).children('input').val(str);
                    cancelFlag = false;
                } else
                    setRate($(this), +oldRate);

                highlighter.focusOut();
                rateInfo.hide($(this));
                finalize($(this));
            })
            .on('keydown', '.commonCell input', keyDownOnlyNumber) // В inputCredit (где баллы вводить) разрешаем вводить только цифры
            .on('keydown', '.commonCell', function (e) {
                cancelFlag = cancelFlag || +e.keyCode === 27; // escape
                if (!(e.keyCode in keyDirs)) //
                    return;

                let [col, row] = cursor.get();
                let direction = keyDirs[e.keyCode];
                let whereToMoveFocus = getDesiredCell(direction);

                if (whereToMoveFocus.length > 0) {
                    highlighter.focusOut();
                    cursor.update(col, row);
                    whereToMoveFocus.children('input').focus();
                } else
                    $(this).children('input').blur();
            });
    };


    var postRate = function (rate, $this, oldRate, rateResult, studentID, submoduleID) {
        const disciplineID = settings.disciplineID;
        let [col, row] = cursor.get();
        $.postJSON(URLdir + 'handler/rating/setRate', {studentID, disciplineID, submoduleID, rate})
            .success(() => {
                let correctRate = (rateResult > 100) ? '100+' : rateResult;
                $this.siblings('.rateResultCell').text(correctRate);

                // Открываем доступ к след. ячейке добора баллов
                if ($this.hasClass('additionalCell')) {
                    let nextAdditionalCell = $(`#col_row_${col + 1}_${row}`);
                    let placeholderMaxVal = (rateResult < 60) ? (60 - rateResult) : 0;

                    if (nextAdditionalCell.hasClass('additionalCell')) {
                        let placeholderMax = (placeholderMaxVal > 0) ? 'макс. ' + placeholderMaxVal : '---';
                        nextAdditionalCell.find('input').attr('placeholder', placeholderMax);
                    }
                }
                Popup.success('Балл добавлен/изменен');
            }).fail((jqXHR) => {
                $this.children('input').val(oldRate != -1 ? oldRate : '');
                const response = JSON.parse(jqXHR.responseText);
                let status = +jqXHR.status;
                let errorMsg = 'Не удалось добавить/изменить балл';
                switch (status) {
                    case 400: errorMsg = response.message; break;
                    case 403: errorMsg = 'Сессия истекла'; break;
                    case 500:
                    default: // ничего не делать
                        break;
                }
                Popup.error(errorMsg);

                if (status == 403)
                    window.location.replace(URLdir);
        });
    };

    // todo: merge "if" branches
    function recountScores(newRate, $this) {
        let rateResult = Math.max(0, newRate);
        let bonus = +$this.siblings('.bonus').text();

        // считаем баллы по строке
        if (settings.pageType === 'exam') //(jThis.attr('class').indexOf('attemptCell') >= 0)
        {
            // страница сессии
            rateResult += +$this.siblings('.semesterRateResultCell').text();
            rateResult += $this.siblings('.additionalCell').aggregate((init, elem) => +$(elem).children('input').val() + init);

            if (newRate === -1)
                rateResult += $this.siblings('.attemptCell').not('.autoPass').not('.absenceCell')
                    .aggregate((init, elem) => Math.max(init, +$(elem).find('input').val()));
        } else {
            $this.siblings('.commonCell').each(function () { // добавим сумму баллов в соседних ячейках
                rateResult += +$(this).children('input').val() || 0;
            });
            let extraRate = +$this.siblings('.extraCell').text() || 0;
            let examRate = +$this.siblings('.examCell').text() || 0;
            rateResult += extraRate + examRate;
        }
        return rateResult + (bonus || 0);
    }

    /**
     * @param {jQuery} $this
     * @param {int} oldRate
     */
    function setRate($this, oldRate) {
        let $scoreInput = $this.children('input');
        let inputVal = $scoreInput.val();

        // если пустая строка в ячейке, значит ничего не поставлено
        let newRate = (inputVal === '') ? -1 : +inputVal;
        if (newRate === oldRate)
            return;

        // блокируем ячейку пока не обработаем коллбек
        $scoreInput.turnOff();

        var rateResult = recountScores(newRate, $this);

        if (newRate <= cell.maxRate)
            postRate(newRate, $this, oldRate, rateResult, cell.student, cell.submodule);
        else {
            let cellRate = (oldRate > cell.maxRate) ? '0' :
                            (oldRate === -1 ? '' : oldRate);
            $scoreInput.val(cellRate);
            Popup.error('Текущий балл превышает максимальный для данного модуля');
        }
        // todo: move to post callback
        $scoreInput.turnOn();
    }

    return {
        initGeneralCells,

        // + ID - id дисциплины
        // + studyGroupID_Filter - studyGroupID для фильтра (Эффект памяти)
        settings,
        cell,
        cursor,
        rateInfo,
        highlighter,
        adjuster,
        filter
    }
}