rbutrcom/rbutr-browser-extension

View on GitHub
src/popup.js

Summary

Maintainability
F
6 days
Test Coverage
/*!
 * Rbutr Browser Extension v0.12.0
 * https://github.com/rbutrcom/rbutr-browser-extension
 *
 * Copyright 2012-2017 The Rbutr Community
 * Licensed under LGPL-3.0
 */

/*global browser,$,MutationObserver*/
/*jslint browser:true,esnext:true */


const FIRST_ARRAY_ELEMENT = 0;
let tab;


/**
 * @description Multi-Browser support
 */
window.browser = (() => {

    'use strict';

    return window.msBrowser ||
        window.browser ||
        window.chrome;
})();



/**
 * @method Message
 * @description Composed object handling messages
 *
 * @return {Object} Public object methods
 */
const Message = () => {

    'use strict';

    const $messageContainer = document.getElementById('messages');


    /**
     * @method initialize
     * @description Initialize Message handler
     *
     * @return {void}
     */
    const initialize = () => {

        window.addEventListener('click', (event) => {
            if (event.target.className === 'msg-remove') {
                remove(event.target.parentNode);
            }
        });
    };



    /**
     * @method add
     * @description Add given message to message container
     *
     * @param {String} type - The message type (success|info|warning|error)
     * @param {String} message - The message to be displayed
     * @return {void}
     */
    const add = (type, message) => {

        if (type === 'success' || type === 'info' || type === 'warning' || type === 'error') {
            const messageClose = '<a href="#" class="msg-remove">&times;</a>';
            const $newMessage = document.createElement('div');

            $newMessage.setAttribute('class', 'msg msg-' + type);
            $newMessage.insertAdjacentHTML('beforeend', messageClose + message);

            $messageContainer.appendChild($newMessage);
        } else {
            rbutr.utils.log('warn', `Message of type "${type}" is invalid.`);
        }
    };



    /**
     * @method remove
     * @description Remove message from message container
     *
     * @param {Object} $element - The element to be removed
     * @return {void}
     */
    const remove = ($element) => {

        $messageContainer.removeChild($element);
    };


    return {initialize, add, remove};
};



/**
 * @method View
 * @description Composed object handling views
 *
 * @return {Object} Public object methods
 */
const View = () => {

    'use strict';



    /**
     * @method get
     * @description Retrieve view DOM object
     *
     * @param {String} view - Name of the view to retrieve
     * @return {(Boolean|Object)} Return DOM object if exists, otherwise false
     */
    const get = (view) => {

        const $viewObj = document.getElementById('view-' + view);

        if (typeof $viewObj === 'object' && $viewObj !== null) {
            return $viewObj;
        } else {
            rbutr.utils.log('error', `View "${view}" does not exist.`);
            return false;
        }
    };



    /**
     * @method show
     * @description Display given popup view
     *
     * @param {String} view - Name of the view to display
     * @return {void}
     */
    const show = (view) => {

        rbutr.utils.log('log', 'Show view: ', view);

        get(view).classList.remove('hidden');
    };



    /**
     * @method hide
     * @description Hide given popup view
     *
     * @param {?String} view - Name of the view to hide
     * @return {void}
     */
    const hide = (view) => {

        rbutr.utils.log('log', 'Hide view: ', view);

        if (view === 'all' || view === null) {
            document.querySelectorAll('.view').forEach( x => x.classList.add('hidden'));
        } else {
            get(view).classList.add('hidden');
        }
    };



    /**
     * @method setContent
     * @description Hide given popup view
     *
     * @param {?String} view - Name of the view
     * @param {?String} content - HTML that should be set in given view
     * @return {void}
     */
    const setContent = (view, content) => {

        if (view === 'menu' || view === 'rebuttals') {
            get(view).innerHTML = content;
        } else {
            rbutr.utils.log('error', `It's not allowed to overwrite the content of view "${view}"`);
        }
    };


    return {get, show, hide, setContent};
};



/**
 * @method Popup
 * @description Composed object handling all popup and view logic
 *
 * @return {Object} Public object methods
 */
const Popup = () => {

    'use strict';

    const ZERO = 0;
    const ONE = 1;
    const MAX_TAG_COUNT = 6;
    const MAX_URL_COUNT = 3;

    let notLoggedInMsg = '';

    const msg = Message();
    msg.initialize();

    const view = View();



    /**
     * @method initialize
     * @description Initialize popup
     *
     * @return {void}
     */
    const initialize = () => {

        notLoggedInMsg = `
            You are not logged in!
            rbutr requires you to be logged in to submit rebuttals and to vote.
            <a target="_blank" href="${rbutr.api.getServerUrl(true)}/rbutr/LoginServlet">
                Click here
            </a> to login or register.
        `;

        // Request Menu from Server
        portBg.postMessage({request: 'getMenu'});

        // Request rebuttals from Server
        let rebuttals = rbutr.getProp('rebuttals', tab.id);

        if (rebuttals === null) {
            view.setContent('rebuttals', 'Loading...');
            portBg.postMessage({request: 'getRebuttals', tab: tab});
        } else {
            view.setContent('rebuttals', rebuttals);
        }

    };



    /**
     * @method refreshSubmissionData
     * @description Refresh stored data certain user interactions
     *
     * @return {void}
     */
    const refreshSubmissionData = () => {

        const HTTP_LENGTH = 4;

        if (rbutr.getPropLen('sourceUrls') > ONE) {
            $('#submit-sources').html('<h3 class="source-heading">Rebut these sources</h3>');
        } else {
            $('#submit-sources').html('<h3 class="source-heading">Rebut this source</h3>');
        }

        for (let i = 0; i < rbutr.getPropLen('sourceUrls'); i++) {
            let sourceUrl = rbutr.getProp('sourceUrls', i);
            let source = $('<div class="link-block" id="source_' + i +
                '"><span class="link-title">' + rbutr.getPageTitle(sourceUrl) + '</span><br>' +
                '<span class="link-url">' + sourceUrl + '</span></div>').appendTo('#submit-sources');
            $('<img class="close" src="http://rbutr.com/images/button-removetag.png" id="s_x_' + i + '"/>').click((event) => {
                event.preventDefault();
                event.stopPropagation();
                rbutr.setProp('sourceUrls', i, null);
                refreshSubmissionData();
            }).appendTo(source);
        }

        if (rbutr.getPropLen('sourceUrls') > ZERO) {
            $('#submit-sources').append('<div id="btn-capture-src" class="fake-link">+ add another source</div>');
        } else {
            $('#submit-sources').append('<div id="btn-capture-src" class="btn">Click to capture current page as source link.</div>');
        }

        if (rbutr.getPropLen('rebuttalUrls') > ONE) {
            $('#submit-rebuttals').html('<h3 class="rebuttalHeading">With these pages</h3><div style="clear:both"></div>');
        } else {
            $('#submit-rebuttals').html('<h3 class="rebuttalHeading">With this page</h3><div style="clear:both"></div>');
        }

        for (let j = 0; j < rbutr.getPropLen('rebuttalUrls'); j++) {
            let rebuttalUrl = rbutr.getProp('rebuttalUrls', j);
            let rebuttal = $(
                '<div class="link-block" id="rebuttal_' + j +
                '"><span class="link-title">' + rbutr.getPageTitle(rebuttalUrl) + '</span><br>' +
                '<span class="link-url">' + rebuttalUrl + '</span><br>' +
                '</div>').appendTo('#submit-rebuttals');
            $('<input id="c_x_' + j +
                '" size="60" type="text" placeholder="Optional : Describe the relationship between these two pages in a few words" ' +
                'name="c_x_' + j + '">')
                .val(rbutr.getProp('comment', j))
                .on('keyup', () => {
                    rbutr.setProp('comment', j, this.value);
                })
                .appendTo(rebuttal);
            $('<img class="close" src="http://rbutr.com/images/button-removetag.png" id="r_x_' + j + '">').click((event) => {
                event.preventDefault();
                event.stopPropagation();
                rbutr.setProp('rebuttalUrls', j, null);
                refreshSubmissionData();
            }).appendTo(rebuttal);
        }

        if (rbutr.getPropLen('rebuttalUrls') >= MAX_URL_COUNT) {
            $('#btn-capture-rebuttal').disable();
        } else if (rbutr.getPropLen('rebuttalUrls') > ZERO) {
            $('#submit-rebuttals').append('<div id="btn-capture-rebuttal" class="fake-link">+ add another rebuttal</div>');
        } else {
            $('#submit-rebuttals').append('<div id="btn-capture-rebuttal" class="button">Click to capture current page as rebuttal link.</div>');
        }

        $('#btn-capture-rebuttal').click(() => {
            addUrl('rebuttalUrls');
        });

        $('#submission-error').text(rbutr.getProp('submitError'));

        if (rbutr.getPropLen('sourceUrls') > ZERO &&
            rbutr.getProp('sourceUrls', FIRST_ARRAY_ELEMENT).substring(ZERO, HTTP_LENGTH).toLowerCase() === 'http' &&
            rbutr.getPropLen('rebuttalUrls') > ZERO &&
            rbutr.getProp('rebuttalUrls', FIRST_ARRAY_ELEMENT).substring(ZERO, HTTP_LENGTH).toLowerCase() === 'http' &&
            rbutr.getPropLen('tags') > ZERO) {
            document.forms['data'].submitLink.title = 'Submit this rebuttal';
            document.forms['data'].submitLink.disabled = false;
        } else {
            document.forms['data'].submitLink.title = 'You must have at least one source link, rebuttal link and tag to submit';
            document.forms['data'].submitLink.disabled = true;
        }
    };



    /**
     * @method refreshTags
     * @description Refresh stored tags
     *
     * @return {void}
     */
    const refreshTags = () => {

        $('#tag-holder').html(''); // Wipe and recreate
        for (let i = 0; i < rbutr.getPropLen('tags'); i++) {
            $('#tag-holder').append('<a class="tag-for-submission" href="#">' + rbutr.getProp('tags', i) + '</a>');
        }
        $('.tag-for-submission').click(() => {
            rbutr.removeTag(this.text);
            refreshTags();
            $('#tag-typeahead').val(''); // Somehow this gets reset on removing the actual tags?
            refreshSubmissionData();
        });
    };



    /**
     * @method recordTag
     * @description Add tag to taglist and refresh stored data
     *
     * @param {String} tagText - Content of the tag
     * @return {void}
     */
    const recordTag = (tagText) => {

        // We are getting blank ones due to double ups of events. This is the easy fix.
        if (tagText === '') {
            return;
        }
        rbutr.addTag(tagText);
        refreshTags();
        refreshSubmissionData();
    };



    /**
     * @method setupTagTypeahead
     * @description Setup typeahead autocomplete library
     *
     * @return {void}
     */
    const setupTagTypeahead = () => {

        const KEY_ENTER = 13;
        const KEY_SEMICOLON = 186;
        const KEY_COMMA = 188;

        $('#tag-typeahead').typeahead({
            name: 'tags',
            limit: 10,
            prefetch: rbutr.api.getServerUrl() + '?getPlainTagsJson=true'
        }).on('typeahead:selected', (event, data) => {

            recordTag(data.value);
            document.getElementById('#tag-typeahead').value = '';
        }).keydown((event) => {

            const key = event.which;
            rbutr.utils.log('log', 'Tagging pressed key:', key);
            rbutr.utils.log('log', 'Tagging event:', event);

            if (key === KEY_ENTER || key === KEY_SEMICOLON || key === KEY_COMMA) {
                event.preventDefault();
                recordTag($('#tag-typeahead').val());
                $('#tag-typeahead').val('');
            }
        });
    };



    /**
     * @method displaySubmissionForm
     * @description Prepare and display submission page
     *
     * @return {void}
     */
    const displaySubmissionForm = () => {

        view.hide('all');
        view.show('submission');
        refreshTags();
        setupTagTypeahead();
        refreshSubmissionData();
    };



    /**
     * @method displayVoteForm
     * @description Show voting page if no votes have been made, otherwise thankyou page
     *
     * @param {Object} recordedClick - Object which holds voting click data
     * @return {void}
     */
    const displayVoteForm = (recordedClick) => {

        if (recordedClick.yourVote && recordedClick.yourVote !== ZERO) {
            view.show('thankyou');
        } else {
            view.show('vote');
        }
    };



    /**
     * @method showSubmissionPopup
     * @description Display submission page if user is logged in
     *
     * @param {String} fromTo - Type of URL that should be submitted
     * @return {void}
     */
    const showSubmissionPopup = (fromTo) => {

        if (!rbutr.getProp('loggedIn')) {
            msg.add('warning', notLoggedInMsg);
        } else {
            rbutr.startSubmission(tab.id, fromTo);
            displaySubmissionForm();
        }
    };



    /**
     * @method cancelSubmission
     * @description Stop submission and close popup
     *
     * @return {void}
     */
    const cancelSubmission = () => {

        rbutr.stopSubmission();
        view.show('rebuttals');
        view.show('action');
    };



    /**
     * @method requestRebuttals
     * @description Display rebuttal request page
     *
     * @return {void}
     */
    const requestRebuttals = () => {

        if (!rbutr.getProp('loggedIn')) {
            msg.add('warning', notLoggedInMsg);
        } else {
            view.hide('all');
            view.show('request');
            setupTagTypeahead();
            $('#request-url').val(rbutr.getProp('canonicalUrls', tab.id));
        }
    };



    /**
     * @method submitRebuttalRequest
     * @description Submit rebuttal request data to server
     *
     * @return {Boolean} Returns false if preconditions are not correct
     */
    const submitRebuttalRequest = () => {

        if (rbutr.getPropLen('tags') > MAX_TAG_COUNT) {
            msg.add('error', 'Maximum of 6 tags, please fix before submitting.');
            document.forms['request-rebuttal'].submitLink.disabled = false;
            return false;
        }

        portBg.postMessage({
            request: 'submitRebuttalRequest',
            tab: tab
        });
    };



    /**
     * @method addUrl
     * @description Add canonical url to stored sourceUrls and refresh data
     *
     * @param {String} type - Either `rebuttalUrls` or `sourceUrls`
     * @return {void}
     */
    const addUrl = (type) => {

        if (rbutr.getProp('canonicalUrls', tab.id) === undefined || rbutr.alreadyExists(rbutr.getProp('canonicalUrls', tab.id))) {
            return;
        } else {
            rbutr.setProp(type, rbutr.getPropLen(type), rbutr.getProp('canonicalUrls', tab.id));
            refreshSubmissionData();
        }
    };



    /**
     * @method cancelRequestSubmission
     * @description Return from request to submission page
     *
     * @return {void}
     */
    const cancelRequestSubmission = () => {

        view.hide('all');
        view.show('rebuttals');
        view.show('action');
    };



    /**
     * @method submitData
     * @description Submit data
     *
     * @return {Boolean} Returns false if preconditions are not correct
     */
    const submitData = () => {

        if (rbutr.getPropLen('tags') > MAX_TAG_COUNT) {
            rbutr.setProp('submitError', null, 'Maximum of 6 tags, please fix before submitting.');
            return false;
        }
        if (rbutr.getPropLen('tags') === ZERO) {
            rbutr.setProp('submitError', null, 'Please enter at least one tag for this rebuttal.');
            return false;
        }

        rbutr.submitRebuttals(tab.id);
    };



    /**
     * @method loadData
     * @description Load recorded click data
     *
     * @return {void}
     */
    const loadData = () => {

        // Loads the data from the background tab, which has likely already retrieved it.
        let recordedClick = rbutr.getRecordedClickByToUrl(rbutr.getProp('canonicalUrls', tab.id));
        if (rbutr.getProp('submittingRebuttal') === true) {
            displaySubmissionForm();

            // This means we are on a rebuttal we clicked through to.
        } else if (recordedClick && recordedClick !== null) {
            $('.voting-rebutted-url').attr('href', recordedClick.linkFromUrl);
            $('#current-score').html(recordedClick.score);
            displayVoteForm(recordedClick);
        }
    };



    /**
     * @method vote
     * @description Update vote score with given value
     *
     * @param {Number} voteScore - Integer representing the score of a URL
     * @return {void}
     */
    const vote = (voteScore) => {

        portBg.postMessage({
            request: 'updateVotes',
            tab: tab,
            data: voteScore
        });
    };



    /**
     * @method voteUp
     * @description Increment vote score
     *
     * @return {void}
     */
    const voteUp = () => {

        vote(ONE);
    };



    /**
     * @method voteDown
     * @description Decrement vote score
     *
     * @return {void}
     */
    const voteDown = () => {

        vote(-ONE);
    };



    /**
     * @method submitIdea
     * @description Submit rebuttal idea to the server
     *
     * @return {void}
     */
    const submitIdea = () => {

        const $ideaForm = document.forms['idea-form'];

        $ideaForm.submitLink.value = 'Please wait..';
        $ideaForm.submitLink.disabled = true;

        portBg.postMessage({
            request: 'submitIdea',
            tab: tab,
            data: $ideaForm.idea.value,
        });
    };



    /**
     * @description Set up event listeners
     */
    $(document)
        .on('click', '#tagTo', () => {
            addUrl('rebuttalUrls');
        })
        .on('click', '#tagFrom', () => {
            addUrl('sourceUrls');
        })
        .on('click', '.btn-rebuttal', () => {
            showSubmissionPopup('to');
        })
        .on('click', '.btn-rebutted', () => {
            showSubmissionPopup('from');
        })
        .on('click', '.clickable-down', voteDown)
        .on('click', '.clickable-up', voteUp)
        .on('click', '.menu', () => {

            const menu = document.querySelector('.menu');
            menu.classList.toggle('active');

            if (menu.classList.contains('active')) {
                menu.innerHTML = 'close';
                view.hide('all');
                view.show('menu');
            } else {
                menu.innerHTML = 'Menu';
                view.hide('menu');
                view.show('rebuttals');
                view.show('action');
            }
        })
        .on('submit', '#idea-form', submitIdea)
        .on('submit', '#data', submitData)
        .on('submit', '#request-rebuttal', submitRebuttalRequest)
        .on('click', '#cancel-submission', cancelSubmission)
        .on('click', '#cancel-rebuttal-request', cancelRequestSubmission)
        .on('change', '#direct', () => {
            rbutr.setProp('direct', null, this.checked);
        })
        .on('click', '#requestRebuttals', requestRebuttals)
        // Hook up the clickable stuff that might come back.
        .on('click', '#directShowLink', () => {
            $('#hiddenDirects').show();
            $('#directShower').hide();
            return false;
        })
        .on('click', '#generalShowLink', () => {
            $('#hiddenGenerals').show();
            $('#generalShower').hide();
            return false;
        })
        .on('click', '#directHideLink', () => {
            $('#hiddenDirects').hide();
            $('#directShower').show();
            return false;
        })
        .on('click', '#generalHideLink', () => {
            $('#hiddenGenerals').hide();
            $('#generalShower').show();
            return false;
        })
        .on('click', '#btn-capture-src', () => {
            addUrl('sourceUrls');
        })
        .on('click', '#thanks', () => {
            view.show('rebuttals');
            view.show('action');
        });



    /**
     * @method execute
     * @description Start execution in popup
     *
     * @return {void}
     */
    const execute = () => {

        /**
         * @description Set canonical url in background
         */

        if (!rbutr.getProp('canonicalUrls', tab.id)) {
            browser.runtime.sendMessage({action: 'setCanonical', tab: tab});
        }

        loadData();
    };

    return {initialize, msg, view, execute};
};



/**
 * @description Make current tab context globally available
 */
browser.tabs.query({currentWindow: true, active: true}, (currentTab) => {
    tab = currentTab[FIRST_ARRAY_ELEMENT];
});



/**
 * @description Prepare popup to be executed
 */
const popup = Popup();
let rbutr = {};


browser.runtime.getBackgroundPage((background) => {
    rbutr = background.rbutr;

    popup.initialize();
    popup.execute();
});


const portBg = browser.runtime.connect({name: 'popup-background'});

portBg.onMessage.addListener((msg) => {
    if (msg.response === 'getRebuttals') {

        //TODO: Fix this
        if (!rbutr.getProp('canonicalUrls', tab.id)) {
            msg.add('error', 'This doesn\'t look like a real web page.');
            return;
        }

        if (msg.status === 'success') {
            popup.view.setContent('rebuttals', msg.result);
            popup.view.hide('all');
            popup.view.show('rebuttals');
            popup.view.show('action');
        } else if (msg.status === 'error') {
            popup.view.setContent('rebuttals', '');
            popup.msg.add('error', msg.result);
        }

    } else if (msg.response === 'submitRebuttals') {
        if (msg.status === 'success') {
            window.open(msg.result.redirectUrl);
            popup.cancelSubmission(); // Clear the data now that it's submitted.
        } else if (msg.status === 'error') {
            popup.msg.add('error', msg.result);
        }

    } else if (msg.response === 'getMenu') {
        if (msg.status === 'success') {
            popup.view.setContent('menu', msg.result);
        } else if (msg.status === 'error') {
            popup.view.setContent('menu', '');
            popup.msg.add('error', msg.result);
        }

    } else if (msg.response === 'submitIdea') {
        if (msg.status === 'success') {
            popup.msg.add('info', msg.result);
        } else if (msg.status === 'error') {
            popup.msg.add('error', msg.result);
        }

    } else if (msg.response === 'submitRebuttalRequest') {
        if (msg.status === 'success') {
            popup.msg.add('info', msg.result);
        } else if (msg.status === 'error') {
            popup.msg.add('error', msg.result);
        }

    } else if (msg.response === 'submitRebuttalRequest') {
        if (msg.status === 'success') {
            $('#current-score').html(msg.result);
            popup.view.show('thankyou');
        } else if (msg.status === 'error') {
            popup.msg.add('error', msg.result);
        }
    }
});