8
\$\begingroup\$

So, I've created my first Chrome extension! What this does is it highlights every post on Reddit that comes from a source of dubious quality. It provides a few sources by default and allows the user to add or remove from this list. The user also has the choice between either highlighting the offending post or hiding it altogether.

All settings are stored in sync storage which allows them to be used across chrome instances.


Project site: https://github.com/Vannevelj/RedditCrap
Chrome Store: https://chrome.google.com/webstore/detail/redditcrap/kkbdfmnmdigakifakfglcokgohgolgan

Yes, it looks ugly. I take PRs.


redditcrap.js Github

// Register an event listener to check for crap when the stored sites change
chrome.storage.onChanged.addListener(function(changes, area) {
    checkCrap();
})

// Finds all the appropriate domains in the DOM, checks whether they're blacklisted and acts accordingly
function checkCrap() {
    var domains = document.getElementsByClassName('domain');
    chrome.storage.sync.get(['crappySites', 'crappyAction'], function(data) {
        for (var i = 0; i < domains.length; i++) {
            // urls are in the form of '(url)'
            // (surrounded by brackets)
            var url = domains[i].textContent.substring(1, domains[i].textContent.length - 1);

            var parentNode = getParentByClass(domains[i], 'title');
            var entryNode = getThingNode(parentNode);
            if (parentNode.style.backgroundColor === 'red' ||
                entryNode.style.display === 'none') {

                parentNode.style.backgroundColor = '';
                entryNode.style.display = '';
            }

            for (var index = 0; index < data.crappySites.length; index++) {
                var shouldBeColoured = data.crappySites[index].indexOf(url) > -1;
                if (shouldBeColoured) {
                    if (data.crappyAction === 1) {
                        parentNode.style.backgroundColor = 'red';
                    }

                    if (data.crappyAction === 2) {
                        entryNode.style.display = 'none';
                    }

                    break;
                }
            }
        }
    });
}

function getParentByClass(currentNode, className) {
    while (currentNode = currentNode.parentElement) {
        if (currentNode.classList.contains(className)) {
            return currentNode;
        }
    }
}

function getThingNode(parentNode) {
    while (parentNode = parentNode.parentElement) {
        if (parentNode.getAttribute('data-fullname')) {
            return parentNode;
        }
    }
}

// In this section we perform crapchecks when the page has changed
// This is important in the case of NeverEndingReddit which allows a user to scroll down to load a new page
// By introducing a short delay we prevent the page from being crapchecked for every modification (which is problematic if they come in quick succession)
var lastTimeModified;
var timeDelay = 2000; // 2 seconds

// When the subtree is modified for the first time we do an initial crapcheck.
// This allows us to look responsive while we discard all but one closely subsequent events
// After the last event we call checkCrap() again to make sure nothing has been missed due to the subsequent modification events
// Afterwards we reset the value so this can be repeated for other grouped subtree modification events
var performedFastCheck = false;

document.addEventListener('DOMSubtreeModified', function() {
    if (!performedFastCheck) {
        checkCrap();
        performedFastCheck = true;
    }

    lastTimeModified = new Date().getTime();

    setTimeout(function() {
        startTimer();
    }, timeDelay);
});

function startTimer() {
    if (lastTimeModified + timeDelay > new Date().getTime()) {
        clearTimeout();
        setTimeout(function() {
            startTimer()
        }, timeDelay);
    } else {
        checkCrap();
        performedFastCheck = false;
    }
};

popup.html Github

<!doctype html>

<html>

<head>
    <link rel="stylesheet" type="text/css" href="../css/popup.css">
    <title>RedditCrap</title>


</head>

<body>
    <fieldset>
        <legend> Settings </legend>
        <form id="actionForm">
            <select id="crappyAction">
                <option value='none' disabled selected>What happens to blacklisted entries?</option>
                <option value="colour">Colourize</option>
                <option value="hide">Hide</option>
            </select>
        </form>
    </fieldset>

    <form id="siteForm">
        <label for="siteInput"> Enter site: </label>
        <input type="text" id="siteInput">
        <input type="submit" value="Add to blacklist">
    </form>

    <ul id="crappySites">

    </ul>

    <button id='reset'> Reset to default blacklist </button>
</body>

<script src="../js/popup.js"></script>

</html>

popup.css Github

body {
    font-family: "Segoe UI", "Lucida Grande", Tahoma, sans-serif;
    width: 300px;
    height: 100%;
}

input {
    padding: 5px;
    margin: 2px;
    border-radius: 3px;
}

li {
    font-size: 1.5em;
}

fieldset {
    padding: 5px;
    margin: 10px;
    border-radius: 5px;
}

popup.js Github

var shittyCrapSites = ['dailymail.co.uk', 'independent.co.uk', 'ibtimes.co.uk', 'huffingtonpost.com', 'rt.com', 'express.co.uk', 'telegraph.co.uk'];

// We seed the storage with crappy sites if none are found yet
chrome.storage.sync.get(['crappySites', 'crappyAction'], function (data) {
    if (!data.crappySites || data.crappySites.length === 0) {
        chrome.storage.sync.set({ 'crappySites': shittyCrapSites }, function () {});
    }

    var selectElement = document.getElementById('crappyAction');
    if (!data.crappyAction) {
        chrome.storage.sync.set({ 'crappyAction': 1 }, function () {});
    } else {
        selectElement.selectedIndex = data.crappyAction;
    }
});

document.addEventListener('DOMContentLoaded', function () {
    displayExistingFilters();

    document.getElementById('siteForm').addEventListener('submit', add);
    document.getElementById('reset').addEventListener('click', reset);
    document.getElementById('crappyAction').addEventListener('change', actionSettingChanged);
});

function displayExistingFilters() {
    // Get the existing filters from storage
    chrome.storage.sync.get('crappySites', function (data) {
        // Add them to the list
        data.crappySites.forEach(function (element) {
            addListElement(element);
        }, this);
    });
}

function addListElement(value) {
    if (!value) {
        return;
    }

    var resultList = document.getElementById('crappySites');
    var inputField = document.getElementById('siteInput');

    var newItem = document.createElement('li');
    newItem.setAttribute('id', value);
    newItem.appendChild(document.createTextNode(value + ' '));

    var newCancelImage = document.createElement('img');
    newCancelImage.setAttribute('src', '../res/cross.png');
    newCancelImage.addEventListener('click', function () { remove(value); });
    newItem.appendChild(newCancelImage);

    resultList.appendChild(newItem);
    inputField.value = '';
}

function remove(value) {
    chrome.storage.sync.get('crappySites', function (data) {
        var sites = data.crappySites;
        console.log('trying to remove ' + value);

        var newSites = [];
        for (var i = 0; i < sites.length; i++) {
            if (sites[i] !== value) {
                newSites.push(sites[i]);
            }
        }

        console.log('new list: ' + newSites);

        chrome.storage.sync.set({ 'crappySites': newSites }, function () {});

        var resultList = document.getElementById('crappySites');
        resultList.removeChild(document.getElementById(value));
    });
}

function add(e) {
    e.preventDefault();

    var inputField = document.getElementById('siteInput');
    var value = inputField.value;
    addListElement(value);

    chrome.storage.sync.get('crappySites', function (data) {
        var sites = data.crappySites;
        sites.push(value);
        chrome.storage.sync.set({ 'crappySites': sites }, function () {});
    });
}

function reset() {
    chrome.storage.sync.set({ 'crappySites': shittyCrapSites }, function () {});

    var resultList = document.getElementById('crappySites');
    var newResultList = document.createElement('ul');
    newResultList.setAttribute('id', 'crappySites');

    resultList.parentElement.replaceChild(newResultList, resultList);

    shittyCrapSites.forEach(function (element) {
        addListElement(element);
    }, this);
}

function actionSettingChanged() {
    var selectElement = document.getElementById('crappyAction');
    var selectedOption = selectElement.selectedIndex;

    chrome.storage.sync.set({ 'crappyAction': selectedOption }, function () {});
}

manifest.json Github

{
    "manifest_version": 2,
    "name": "RedditCrap",
    "version": "1.0",
    "description": "This extension highlights all submissions on reddit.com that point to a crappy clickbait source.",
    "author": "Jeroen Vannevel",
    "permissions": [
        "http://www.reddit.com/*",
        "https://www.reddit.com/*",
        "storage"
    ],
    "content_scripts": [
        {
            "matches": [
                "http://www.reddit.com/*",
                "https://www.reddit.com/*"
            ],
            "js": [
                "app/js/redditcrap.js"
            ],
            "run_at": "document_end"
        }
    ],
    "browser_action": {
        "default_icon": "app/res/icon16.png",
        "default_popup": "app/html/popup.html",
        "default_title": "RedditCrap"
    },
    "icons": {
        "16": "app/res/icon16.png",
        "48": "app/res/icon48.png",
        "128": "app/res/icon128.png"
    }
}
\$\endgroup\$
3
\$\begingroup\$
var domains = document.getElementsByClassName('domain');
for (var i = 0; i < domains.length; i++) {

You can covert document.get* results into real arrays by using array.slice. That way, you can take advantage of the handy array methods like forEach.

if (data.crappyAction === 1) {

1 and crappyAction doesn't really tell me anything about what the code block does. Suggesting you put 1 in a variable that is appropriately named (like DESIRED_ACTION_HIGHLIGHT and crappyAction (no matter how thematic the name is) be renamed to something more meaningful, like desiredAction.

The entire checkCrap operation could be simplified into something like this:

chrome.storage.sync.get(['crappySites', 'crappyAction'], flaggedDomains => {

  var domains = document.getElementsByClassName('domain');
  var domainElements = Array.prototype.slice(domains);

  domainElements.map(domainElement => ({

    // For each domain element, we grab essential data about it
    element: domainElement,
    url: getUrl(domainElement),
    parent: getParent(domainElement),
    otherStuff: getOtherStuff(domainElement),
    // and so on

  })).filter(domainData => {

    // Then we filter only those that are flagged
    return flaggedDomains.indexOf(domainData.url) > -1;

  }).forEach(domainData => {

    // What remains that run here are domains that match your list

  });
});

As for your element climbers, they can be simplified using recursion. getThingNode needs a better name.

function getParentByClass(currentNode, className) {
  return currentNode.classList.contains(className) ? currentNode : getParentByClass(currentNode.parent, title);
}

function getThingNode(parentNode) {
  return parentNode.getAttribute('data-fullname') ? parentNode : getThingNode(parentNode.parentElement);
}

To avoid having to loop through domains that already have been marked, suggesting you add an indicator that they have already been processed.

// Get all unprocessed domains
var domains = document.querySelectorAll('domain:not(.crap-processed)');

Adding a class can be one way to do it. In this case, let's use crap-processed You can then use querySelectorAll and the :not pseudo-class to get unprocessed domains.

var newSites = [];
for (var i = 0; i < sites.length; i++) {
    if (sites[i] !== value) {
        newSites.push(sites[i]);
    }
}

Can be simplified into:

var newSites = sites.filter(site => site !== value);
\$\endgroup\$
  • \$\begingroup\$ I have incorporated your suggestions in PR #10. As you can see there are a few discrepancies with your suggested code: Array.prototype.slice.call instead of Array.prototype.slice, no filter clause (otherwise there wouldn't be any resets) & wrong arguments to one of the recursion functions but otherwise I used your suggestions. Thank you a lot, this makes a huge difference! \$\endgroup\$ – Jeroen Vannevel Mar 12 '16 at 20:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.