jennydaman/mindmatter

View on GitHub
extension/js/question/app.js

Summary

Maintainability
A
0 mins
Test Coverage
import { Question, pick } from './question.js';

const modal = {
    /**
     * @param {String} message text to display
     */
    show: message => {
        if (message)
            $('#modal-text').text(message);
        $('.modal').addClass('is-active');
        $('html').addClass('is-clipped');
    },
    /**
     * Assign function to modal close event.
     * @param {Function} action function to be called when modal is closed.
     */
    onClose: action => {
        $('#modal-delete').click(action);
        $('#modal-background').click(action);
    },
    close: function () {
        $('.modal').removeClass('is-active');
        $('html').removeClass('is-clipped');
    }
};

/**
 * Shows a modal, set pause to true, open tabs when modal is closed.
 * @param {String} message 
 */
function fail(message) {
    modal.onClose(openAllTabs); 
    chrome.storage.local.set({ pause: true }, function () {
        modal.show(message);
    });
}

const q = new Question();
Question.getTrigger().then(triggerURL => {

    if (triggerURL === 'refresh')
        q.bumpScore(false);

    q.connectWithBackground(triggerURL).then(() => {
        $(document).ready(setup);
    });
});

function setup() {

    modal.onClose(modal.close);

    var triggerDisp = $('#trigger-single');

    const replaceTriggerWithList = siteQueue => {
        triggerDisp.replaceWith(function () {
            let dispList = $('<ul id="trigger-list"></ul>');
            siteQueue.forEach(url => {
                dispList.append($('<li></li>').text(url));
            });
            return dispList;
        });
    };

    if (q.siteQueue.length === 1)
        triggerDisp.text(q.siteQueue[0]);
    else
        replaceTriggerWithList(q.siteQueue);

    q.attachTabListener(siteQueue => {
        if (triggerDisp.attr('id').endsWith('single'))
            replaceTriggerWithList(siteQueue);
        else
            triggerDisp.append(`<li>${siteQueue[siteQueue.length - 1]}</li>`);
    });

    loadQuestion();
}

function loadQuestion() {

    pick().then(questionURL => {
        console.info(questionURL);

        fetch(questionURL).then(question => handleQuestionType(question))
            .catch(error => {
                if (error.statusCode === 404)
                    tryToGetQuestionAgain();
                else
                    fail(error.textStatus == 'timeout' ?
                        'Connection timeout. Please check your internet connection.' :
                        `$.ajax errorThrown: ${error.errorThrown}`
                        + `\nquestionURL: ${error.questionURL}`);
            });
    }).catch(error => {
        console.warn(error, "This shouldn't have happened.");
        tryToGetQuestionAgain();
    });
}

/**
 * Retrieves the question as a JSON object
 * @return {Promise} reject keys: questionURL, textStatus, errorThrown, statusCode
 */
function fetch(questionURL) {
    return new Promise((resolve, reject) => {
        $.ajax({
            url: questionURL,
            dataType: 'json',
            error: (jqXHR, textStatus, errorThrown) =>
                reject({
                    statusCode: jqXHR.status,
                    textStatus: textStatus,
                    errorThrown: errorThrown,
                    questionURL: questionURL
                }),
            statusCode: {
                404: function () {
                    reject({
                        statusCode: 404,
                        questionURL: questionURL
                    });
                },
                200: data => resolve(data)
            },
            timeout: 5000
        });
    });
}

/** 
 * Updates subject indexStructure before trying to pick a fresh question.
 */
function tryToGetQuestionAgain() {

    /*
     * https://developers.google.com/web/updates/2017/11/dynamic-import
     * 
     * to whoever is reading this, I'm really sorry...
     * I'll refactor it some day, I *Promise*
     */
    import('../util/subjects.js').then(subjectsModule => {
        subjectsModule.default().then(freshSubjects => {
            // after question index is updated,
            pick(freshSubjects).then(url => {
                fetch(url).then(question => handleQuestionType(question))
                    .catch(secondError =>
                        fail('Missed two attempts to retrieve a question.'
                            + '\nPausing myself and giving up...'
                            + `\nSecond questionURL: ${secondError.questionURL}`));
            }).catch(pickError => fail("Couldn't pick a question, "
                + 'even after a successful refresh of the subjects index.\n'
                + pickError));
        }).catch(subjectsUpdateError =>
            fail("Question retrieval 404-ed, couldn't update subjects either."
                + '\n' + subjectsUpdateError));
    }).catch(error => fail(error
        + '\nCould not dynamically import the subjects update helper!'));
}

function handleQuestionType(question) {

    if (question.type === 'blank')
        fillInTheBlank(question);
    else
        fail(`question.type=${question.type}` +
            '\nQuestion type not yet supported. This is a bug.');

    // close activity if pause is toggled
    // attach this listener only after we know that fail() isn't called
    chrome.storage.onChanged.addListener(changes => {
        if (changes.pause && changes.pause.newValue === true)
            openAllTabs();
    });
}

/**
 * @param {*} retrieved question data
 */
function fillInTheBlank(retrieved) {

    /**
     * Compares the response against an array of possible answers.
     * If the response is incorrect, wrongTries is incremented by one.
     * @param {string} user_response
     * @returns true if the response contains
     * any of the key words in the correct array.
     */
    retrieved.checkAns = function (user_response) {

        // first, check strict answers
        if (this.ans_exact) {
            let correct = this.ans_exact.find(exactAns => {
                return user_response == exactAns;
            });
            if (correct)
                return true;
        }
        // next, see if numerical ans fits in specified range
        if (this.ans_range) {
            let unitsPos = user_response.indexOf(' ');
            let num = Number(unitsPos === -1 ? user_response : user_response.substring(0, unitsPos));
            if (num >= this.ans_range.min && num <= this.ans_range.max)
                return true;
            if (this.ans_range.std && this.ansKeyWord) {
                let correct = this.ansKeyWord.find(possibleAns => {
                    return Math.abs(num - possibleAns) <= this.ans_range.std;
                });
                if (correct)
                    return true;
            }
            return true;
        }
        // finally, compare against possible answers
        if (this.ansKeyWord) {
            user_response = user_response.trim().toLowerCase();
            let correct = this.ansKeyWord.find(possibleAns => {
                return typeof possibleAns === 'string' ?
                    user_response.includes(possibleAns) : user_response == possibleAns;
            });
            if (correct)
                return true;
        }
        return false;
    };

    // inflate
    $('#question').text(retrieved.questionText);
    $('#response').load('fill_blank.html', function () {

        let textArea = $('#blank');

        textArea.ready(function () {

            let button = $('#submit');
            button.css('height', '38px');
            //button.css('height', $('#blank').css('height'));
            // HACK wrapper span#response is 4px wider than child input#blank
            button.css('position', 'relative');
            button.css('right', '4px');

            button.click(function () {
                handleResponse(textArea, retrieved);
            });

            textArea.keyup(function (e) {

                if (textArea.val().length === 0)
                    $('#submit').attr('disabled', 'disabled');
                else
                    $('#submit').removeAttr('disabled');

                if (e.which === 13) //enter key
                    handleResponse(textArea, retrieved);
            });
        });
    });
}

function handleResponse(inputElement, question) {

    if (!inputElement || !question)
        throw new Error('Must provide jQuery input field and question data');

    inputElement.attr('disabled', 'disabled'); //Disable textbox to prevent multiple submit

    let correct = question.checkAns(inputElement.val());
    q.bumpScore(correct);

    if (correct)
        Question.setCooldown().then(openAllTabs);
    else {
        inputElement.removeAttr('disabled'); //Enable the textbox again
        modal.show(`Incorrect. Wrong tries: ${q.wrongTries}`);
    }
}

function openAllTabs() {
    window.location.replace(q.openOtherTabs());
}