A Bit Of Access

The code behind Alt or not, a browser extension / add-on for Twitter and Tweetdeck

Published by (updated )

This page describes the code behind Alt or not, a browser extension / add-on for Chrome, Firefox, and Edge.

For those interested I'll use this page to share the code, with additional comments to explain things. // Comments in the code look like this

The code here is updated to version 0.6, divided by file. Script.js and alt-or-not.css are used on both Twitter and TweetDeck. Twitter.js is only used on Twitter, and tweetDeck.js is only used on TweetDeck. Go to the main Alt or not page for more information on the plugin and links to download it.

There are some additional files for the options page, these aren't included here as they only deal with retrieving and saving the options.

Script.js: Global variables and functions

// Observer config: we are checking for changes in the target node (element) and all its children
const cfg = { childList: true, subtree: true };
// Setting our initialization attempts to 0
var tries = 0;
// Default values for our settings
var disableButton = false;
var noCheck = false;
var hideLabels = false;
var hideAlt = false;
var toggleAlt = false;
var lightMode = false;
// Loading our values from storage, depending on browser
// This is a function in case we want to reload on the fly later on
const load = function () {
    if (typeof browser !== "undefined") {
        // Firefox uses the browser namespace
        browser.storage.sync.get({
            disableButton: false,
            noCheck: false,
            hideLabels: false,
            hideAlt: false,
            toggleAlt: false,
            lightMode: false
        }, function(items) {
            disableButton = items.disableButton;
            noCheck = items.noCheck;
            hideLabels = items.hideLabels;
            hideAlt = items.hideAlt;
            toggleAlt = items.toggleAlt;
            lightMode = items.lightMode;
        });
    } else {
        if (typeof chrome !== "undefined") {
            // Chrome and Edge use the chrome namespace
            chrome.storage.sync.get({
                disableButton: false,
                noCheck: false,
                hideLabels: false,
                hideAlt: false,
                toggleAlt: false,
                lightMode: false
            }, function(items) {
                disableButton = items.disableButton;
                noCheck = items.noCheck;
                hideLabels = items.hideLabels;
                hideAlt = items.hideAlt;
                toggleAlt = items.toggleAlt;
                lightMode = items.lightMode;
            });
        } 
    }
}
load();
// We add and remove classes a lot, so compact functions are useful
const cla = function(e, c) {
    e.classList.add('tw-alt-'+c); 
};
const clr = function(e, c) {
    e.classList.remove('tw-alt-'+c); 
};
// Function to create our caption element and add it to our container
const caption = function(t, l) {
    if (!hideAlt && t != null) {
        let a = document.createElement('div');
        cla(a, 'txt');
        a.append(l);
        t.append(a);
    }
};
// Function to create our caption container
const captionContainer = function(t) {
    // See if the element already exists for the tweet
    let c = t.querySelector('div.tw-alt-container');
    if (c == null) {
        // If not, we make a new one
        c = document.createElement('div');
        // Hiding it for screen readers, no need for repeated content 
        c.setAttribute('aria-hidden', true);
        // We create a button
        let b = document.createElement('button');
        cla(b, 'toggle');
        cla(c, 'container');
        // Applying the light mode if it's enabled
        if (lightMode) {
            cla(c, 'light');
        }
        // If the feature is selected, we add an action to the button and add it to the container 
        // And yes, creating that button a few lines up should really go in here
        if (toggleAlt) {
            b.innerHTML = 'Show alt';
            b.addEventListener('click', toggleCaptions);
            c.append(b);
            // Applying the class to the container that hides the text and shows the button
            cla(c, 'hidden');
        }
        // Add the container to the tweet
        t.append(c);
    }
    // Return the container element
    return c;
}
// The function that reveals the alt text
const toggleCaptions = function(e) {
    // Finding the parent of the button element
    let a = e.target.parentElement;
    // Toggling the hidden class on the container
    if (a.classList.contains('tw-alt-hidden')) {
        clr(a, 'hidden');
    } else {
        cla(a, 'hidden');
    }
}
// The callback function that runs the checking function on each changed node
const callback = function(ml, o) {
    ml.forEach(function(m) { check(m.target); });
};

Twitter.js: functions specific to Twitter

// Checking the tweet forms
const checkForm = function(e) {
    // Finding the tweet button and proceding when it is found
    let b = document.querySelector('div[data-testid="tweetButtonInline"], div[data-testid="tweetButton"]');
    if (b != null) {
        // We start assuming a reminder isn't needed, and check if the button is enabled
        let remind = false;
        // We move through each image that's added to the tweet
        document.querySelectorAll('div[data-testid="attachments"] div[role="group"]').forEach(function(i) {
            if (i.getAttribute('aria-label') == 'Media') {
                // The alt is set to the default "Media" text, make sure the media is not a video
                if (i.querySelector('video') == null) {
                    // Not a video, we need to remind the user
                    remind = true;
                }
            }
        });
        // Finding all tweet buttons, in case we are checking a thread
        document.querySelectorAll('div[data-testid="tweetButtonInline"], div[data-testid="tweetButton"]').forEach(function(btn) {
            // Getting the right spot for our label
            let t = btn.querySelector('span > span'); 
            if (remind) {
                // Add the label
                cla(t, 'mis');
                // If the user chose to disable buttons, we do that here
                if (disableButton) {
                    btn.setAttribute('aria-disabled', true);
                    btn.removeAttribute('tabindex');
                    btn.classList.add('r-icoktb');
                    btn.style.pointerEvents = 'none';
                }
            } else {
                // Clear the label from the button and enable if needed 
                if (t.classList.contains('tw-alt-mis')) {
                    clr(t, 'mis');
                    btn.removeAttribute('aria-disabled');
                    btn.setAttribute('tabindex', 0);
                    btn.classList.remove('r-icoktb');
                    btn.style.pointerEvents = 'auto';
                }
            }
        });
    }
};
// Checking tweets on the timeline for alt text
const check = function(e) {
    // Check if the user doesn't have labels and alt text disabled 
    if (!hideLabels || !hideAlt) {
        // Finding every image container we haven't checked yet
        e.querySelectorAll('section.css-1dbjc4n div[data-testid="tweetPhoto"]:not(.tw-alt-chk)').forEach(function(p) {
            // Finding the alt text in the aria-label attribute of the element
            let l = p.getAttribute('aria-label');
            // Check if the alt text is empty or the default "Image"
            if (l != "Image" && l != "" && l != null) {
                // We have alt text! Getting the container and adding the caption
                let c = captionContainer(target(p));
                caption(c,l);
            } else {
                // Adding the "No alt" label unless the user disabled it
                if (!hideLabels) {
                    cla(p.closest('a'), 'mis');
                }
            }
            // Adding a class to indicate we have checked this one
            cla(p, 'chk');
        });
        e.querySelectorAll('section.css-1dbjc4n div[data-testid="previewInterstitial"]:not(.tw-alt-chk)').forEach(function(g) {
            // Finding the alt text in the aria-label attribute of the element
            let l = g.getAttribute('aria-label');
            // Check if the alt text is empty or the default "Embedded video"
            if (l != "Embedded video" && l != "") {
                // We have alt text! Getting the container and adding the caption
                let c = captionContainer(target(g));
                caption(c,'GIF: '+l);
            } else {
                // Check the text on the play button to see if it is a GIF, and not a video, before we add our label
                let b = g.querySelector('div[data-testid="playButton"]');
                if (b != null && b.getAttribute('aria-label').includes('GIF')) {
                    // Adding the "No alt" label unless the user disabled it
                    if (!hideLabels) {
                        cla(g, 'mis');
                    }
                }
            }
            // Adding a class to indicate we have checked this one
            cla(g, 'chk');
        });
    }
    // Check the inline form unless the user disabled this feature
    if (!noCheck) {
        checkForm();
    }
};
// Function to find the right element to contain the alt text
const target = function(e) {
    let t = e.closest('div[data-testid="tweet"] div[role="link"]');
    if (t == null) {
        t = e.closest('.r-1phboty, .r-18bvks7').closest('div[class="css-1dbjc4n"]');
    }
    return t;
}
// Initialising the observers
const init = function() {
    var o = false; 
    let tw = document.querySelector('main');
    if (tw != null && o != true) {
        // First running our check on tweets already visible
        check(tw);
        // Start observing the tweets
        let otw = new MutationObserver(callback);
        otw.observe(tw, cfg);
        if (!noCheck) {
            // Find and start observing the composer modal
            let frm = document.querySelector('div#layers');
            if (frm != null) {
                let otf = new MutationObserver(checkForm);
                otf.observe(frm, cfg);
            }
        }
        o = true;
    }
    if (o != true) {
        // We aren't observing yet, retry in a second
        tries++;
        // We only retry if we have less than 5 failed attempts 
        if (tries < 5) { 
            setTimeout(init, 1000);
        }
    }
};
// Start our first try at initialising
init();

TweetDeck.js: functions specific to TweetDeck

// Setting a global variable to store the default "Add description" label in Tweetdeck
// This way we don't need keep track of languages
var tdnoalt = '';
// Checking the tweet form
const checkForm = function(e) {
    // Finding the tweet button and proceding when it is found
    let b = document.querySelector('div[data-drawer="compose"] button.js-send-button');
    if (b != null) {
        // We start assuming a reminder isn't needed, and check if the button is enabled
        let remind = false;
        // We move through each image that's added to the tweet
        document.querySelectorAll('div[data-drawer="compose"] div.js-add-image-description').forEach(function(i) {
            // Getting the content of the "Add description" label, and storing it for later if we haven't already
            let l = i.innerHTML;
            if (tdnoalt == '') {
                tdnoalt = l;
            }
            if (l == tdnoalt) {
                // The alt is set to the standard "Add description" text, we will remind the user
                remind = true;
            }
        });
        if (remind) {
            // Add the label
            cla(b, 'mis');
            // If the user chose to disable buttons, we do that here
            if (disableButton) {
                b.classList.add('is-disabled');
                b.style.pointerEvents = 'none';
            }
        } else {
            // Clear the label from the button and enable if needed 
            if (b.classList.contains('tw-alt-mis')) {
                clr(b, 'mis');
                b.classList.remove('is-disabled');
                b.style.pointerEvents = 'auto';
            }
        }
    }
};
// Checking tweets on the timeline for alt text
const check = function(e) {
    // Finding every image container we haven't checked yet
    e.querySelectorAll('a.js-media-image-link:not(.tw-alt-chk)').forEach(function(p) {
        if (p.querySelectorAll('div').length == 0) {
            // Finding the alt text in the aria-label attribute of the element
            let l = p.getAttribute('title');
            let i = p.querySelector('img');
            if (l == "" && i != null) {
                l = i.getAttribute('alt');
            }
            // Check if the alt text is empty or the default "Image"
            if (l != "Image" && l != "" && l != null) {
                // We have alt text! Getting the container and adding the caption
                let c = captionContainer(p.closest('.js-media').parentElement);
                caption(c,l);
            } else {
                // Adding the "No alt" label unless the user disabled it
                if (!hideLabels) {
                    cla(p, 'mis');
                }
            }
        }
        // Adding a class to indicate we have checked this one
        cla(p, 'chk');
    });
};
// Initialising the observers
const init = function() {
    var o = false; 
    if (document.querySelector('section.js-column') != null) {
        if (!hideLabels || !hideAlt) {
            // Unless the user disabled both the "No alt" label and alt text display, we find and observer all TweetDeck columns
            let otd = new MutationObserver(callback);
            document.querySelectorAll('section.js-column').forEach(function(t) {
                // First running our check on tweets already visible
                check(t);
                // Start observing the tweets
                otd.observe(t, cfg);
            });
        }
        if (!noCheck) {
            // Unless the user disabled the feature, we find and start observing the tweet form
            let tdc = document.querySelector('div[data-drawer="compose"]');
            if (tdc != null) {
                let otdf = new MutationObserver(checkForm);
                otdf.observe(tdc, cfg);
            }
        }
        o = true;
    }
    // We aren't observing yet, retry in a second
    tries++;
    // We only retry if we have less than 5 failed attempts 
    if (tries < 5) { 
        setTimeout(init, 1000);
    }
};
// Start our first try at initialising
init();
                

Alt-or-not.css: styles

// Style for the red "No alt" label
.tw-alt-mis::after { 
    display: inline-block;
    background-color: #800;
    color: #FFF; font-family: sans-serif;
    font-size: 1rem;
    padding: 0.25rem 0.5rem;
    border-radius: 0.5rem;
    content: "No alt";
    font-weight: bold;
    position: absolute;
    top: 0.5rem;
    left: 0.5rem;
    box-shadow: 0 0 2px 0 #000;
}
// Positioning adjustments for the "No alt" label in buttons
div[data-testid="tweetButtonInline"] .tw-alt-mis::after, 
div[data-testid="tweetButton"] .tw-alt-mis::after, 
button.js-send-button.tw-alt-mis::after {
    position: relative;
    top: 0;
    left: 0;
    margin-left: 0.5rem;
}
// The element containing alt texts gets minimal styling
.tw-alt-container { 
    margin: 0;
    padding: 0.5rem;
}
// Style of the alt text elements
.tw-alt-txt {
    display: block;
    box-sizing: border-box;
    background-color: #202020;
    color: #FFF;
    font-family: sans-serif;
    font-size: 1rem;
    line-height: 1.3rem;
    padding: 0.75rem;
    margin: 0;
    white-space: pre-wrap;
    border: 1px solid #505050;
}
// Border adjustments for the first alt text of a tweet
.tw-alt-txt:first-of-type {
    border-top-left-radius: 0.5rem;
    border-top-right-radius: 0.5rem;
    border-bottom-width: 0;
}
// Border adjustments for the last alt text of a tweet (which can also be the first)
.tw-alt-txt:last-of-type {
    border-bottom-left-radius: 0.5rem;
    border-bottom-right-radius: 0.5rem;
    border-bottom-width: 1px;
}
// Hiding the alt text
.tw-alt-hidden .tw-alt-txt {
    display: none;
}
// The styling for the "Show alt" button
.tw-alt-container button.tw-alt-toggle,
.tw-alt-container button.tw-alt-toggle:active,
.tw-alt-container button.tw-alt-toggle:hover,
.tw-alt-container button.tw-alt-toggle:focus {
    display: none; background-color: #17bf63;
    color: #FFF;
    font-family: sans-serif;
    font-size: 1rem;
    font-weight: bold;
    line-height: 1.3rem;
    padding: 0.5rem 0.75rem;
    border-radius: 2.3rem;
    border: 0;
    margin: 0;
    cursor: pointer;
}
// Giving the button a subtle hover effect in line with Twitter styles
.tw-alt-container button.tw-alt-toggle:hover {
    background-color: #15ac59;
}
// Making the button visible when the alt text is hidden
.tw-alt-hidden button.tw-alt-toggle {
    display: inline-block !important;
}
// Inverting the alt text colors for the light mode
.tw-alt-light .tw-alt-txt {
    background-color: #FFF;
    color: #202020;
}