stevegrossi/stevegrossi

View on GitHub
app/assets/javascripts/jquery.bigfoot.js

Summary

Maintainability
F
5 days
Test Coverage
//   _______    ________  _______    ______   ______   ______   _________
// /_______/\  /_______/\/______/\  /_____/\ /_____/\ /_____/\ /________/\
// \::: _  \ \ \__.::._\/\::::__\/__\::::_\/_\:::_ \ \\:::_ \ \\__.::.__\/
//  \::(_)  \/_   \::\ \  \:\ /____/\\:\/___/\\:\ \ \ \\:\ \ \ \  \::\ \
//   \::  _  \ \  _\::\ \__\:\\_  _\/ \:::._\/ \:\ \ \ \\:\ \ \ \  \::\ \
//    \::(_)  \ \/__\::\__/\\:\_\ \ \  \:\ \    \:\_\ \ \\:\_\ \ \  \::\ \
//     \_______\/\________\/ \_____\/   \_\/     \_____\/ \_____\/   \__\/
//         _________________________________________________________________
//        /________________________________________________________________/\
//        \________________________________________________________________\/

// PURPOSE -----
// Looks through the page's markup to identify footnote links/ content.
// It then creates footnote buttons in place of the footnote links and hides the content.
// Finally, creates and positions footnotes when the generated buttons are pressed.

// IN ----------
// An optional object literal specifying script settings.

// OUT ---------
// Returns an object with the following methods:
// close: closes footnotes matching the jQuery selector passed to the function.
// activate: activates the footnote button matching the jQuery selector passed to the function.

// INFO --------
// Developed and maintained by Chris Sauve (http://pxldot.com)
// Documentation, license, and other information can be found at http://cmsauve.com/projects/bigfoot.

// TODO --------
// - Better handling of hover
// - Ability to position/ size popover relative to a containing element (rather than the window)
// - Compensate for zoom position on mobile
// - Update numbered style to handle more than 9 footnotes

// KNOWN ISSUES -
// - Safari 7 doesn't properly calculate the scrollheight of the content wrapper and, as a result, will not
//      properly indicate a scrollable footnote
// - Popovers that are instantiated at a smaller font size which is then resized to a larger one won't adhere
//      to your chosen max-height (in CSS) since JS tries to keep it from running off the top/ bottom of the page
//      but does so using pixel values tied to the sizes of the footnote content when it was originally activated.
//      If anyone has any ideas on this, please let me know!



(function($) {

    $.bigfoot = function(options) {


        //  ______   ______   _________  _________  ________  ___   __    _______    ______
        // /_____/\ /_____/\ /________/\/________/\/_______/\/__/\ /__/\ /______/\  /_____/\
        // \::::_\/_\::::_\/_\__.::.__\/\__.::.__\/\__.::._\/\::\_\\  \ \\::::__\/__\::::_\/_
        //  \:\/___/\\:\/___/\  \::\ \     \::\ \     \::\ \  \:. `-\  \ \\:\ /____/\\:\/___/\
        //   \_::._\:\\::___\/_  \::\ \     \::\ \    _\::\ \__\:. _    \ \\:\\_  _\/ \_::._\:\
        //     /____\:\\:\____/\  \::\ \     \::\ \  /__\::\__/\\. \`-\  \ \\:\_\ \ \   /____\:\
        //     \_____\/ \_____\/   \__\/      \__\/  \________\/ \__\/ \__\/ \_____\/   \_____\/
        //

        var bigfoot;

        var settings = $.extend(
            {
                actionOriginalFN    : "hide", // "delete", "hide", or "ignore"
                activateCallback    : function() {},
                activateOnHover     : false,
                allowMultipleFN     : false,
                appendPopoversTo    : undefined,
                breakpoints         : {},
                deleteOnUnhover     : false,
                hoverDelay          : 250,
                numberResetSelector : undefined,
                popoverDeleteDelay  : 300,
                popoverCreateDelay  : 100,
                positionNextToBlock : true,
                positionContent     : true,
                preventPageScroll   : true,
                scope               : false,
                useFootnoteOnlyOnce : true,

                contentMarkup       : "<aside class=\"footnote-content bottom\"" +
                                            "data-footnote-number=\"{{FOOTNOTENUM}}\" " +
                                            "data-footnote-identifier=\"{{FOOTNOTEID}}\" " +
                                            "alt=\"Footnote {{FOOTNOTENUM}}\">" +
                                                "<div class=\"footnote-main-wrapper\">" +
                                                    "<div class=\"footnote-content-wrapper\">" +
                                                        "{{FOOTNOTECONTENT}}" +
                                                "</div></div>" +
                                            "<div class=\"tooltip\"></div>" +
                                        "</aside>",

                buttonMarkup        :  "<a href=\"#\" class=\"footnote-button\" " +
                                            "id=\"{{SUP:data-footnote-backlink-ref}}\" " +
                                            "data-footnote-number=\"{{FOOTNOTENUM}}\" " +
                                            "data-footnote-identifier=\"{{FOOTNOTEID}}\" " +
                                            "alt=\"See Footnote {{FOOTNOTENUM}}\" " +
                                            "rel=\"footnote\"" +
                                            "data-footnote-content=\"{{FOOTNOTECONTENT}}\">" +
                                                "<span class=\"footnote-circle\" data-footnote-number=\"{{FOOTNOTENUM}}\"></span>" +
                                                "<span class=\"footnote-circle\"></span>" +
                                                "<span class=\"footnote-circle\"></span>" +
                                        "</a>"
            }, options);



        //  ________  ___   __     ________  _________
        // /_______/\/__/\ /__/\  /_______/\/________/\
        // \__.::._\/\::\_\\  \ \ \__.::._\/\__.::.__\/
        //    \::\ \  \:. `-\  \ \   \::\ \    \::\ \
        //    _\::\ \__\:. _    \ \  _\::\ \__  \::\ \
        //   /__\::\__/\\. \`-\  \ \/__\::\__/\  \::\ \
        //   \________\/ \__\/ \__\/\________\/   \__\/
        //


        // FUNCTION ----
        // Footnote button/ content initializer (run on doc.ready)

        // PURPOSE -----
        // Finds the likely footnote links and then uses their target to find the content

        var footnoteInit = function() {

            // Get all of the possible footnote links
            var footnoteButtonSearchQuery;
            footnoteButtonSearchQuery = !settings.scope ? "a[href*=\"#\"]" : settings.scope + " a[href*=\"#\"]";

            // Filter down to links that:
            // - have an HREF referencing a footnote, OR
            // - have a rel attribute of footnote
            // AND that aren't a descendant of a footnote (prevents backlinks)
            var $footnoteAnchors = $(footnoteButtonSearchQuery).filter(function() {
                var $this = $(this);
                var relAttr = $this.attr("rel");
                if(!relAttr || relAttr == "null") {
                    relAttr = "";
                }
                return ($this.attr("href") + relAttr).match(/(fn|footnote|note)[:\-_\d]/gi) && $this.closest("[class*=footnote]:not(a):not(sup)").length < 1;
            }); // End of footnote link .filter()

            var footnotes       = [],
                footnoteLinks   = [],
                finalFNLinks    = [],
                relatedFN,
                $closestFootnoteLi,
                $actualFootnoteLi;

            // Resolve issues with superscript/ anchor combination
            cleanFootnoteLinks($footnoteAnchors, footnoteLinks);

            // Get the footnote that the link was pointing to
            $(footnoteLinks).each(function() {
                // escape symbols with special jQuery/ CSS selector meaning
                relatedFN = $(this).attr("data-footnote-ref").replace(/[:.+~*\]\[]/g, "\\$&");
                if(settings.useFootnoteOnlyOnce) relatedFN = relatedFN + ":not(.footnote-processed)";
                $closestFootnoteLi = $(relatedFN).closest("li");

                if($closestFootnoteLi.length > 0) {
                    footnotes.push($closestFootnoteLi.first().addClass("footnote-processed"));
                    finalFNLinks.push(this);
                }
            });

            var $lastResetElement,
                $curResetElement,
                footnoteNum = 1,
                footnoteContent,
                footnoteIDNum,
                $relevantFNLink,
                $relevantFootnote,
                footnoteButton,
                $footnoteButton;

            // Initiates the button with the footnote content
            // Also performs the desired action on the original footnotes
            for(var i = 0; i<footnotes.length; i++) {

                // Removes any backlinks and hackily encodes double quotes and >/< symbols to prevent conflicts
                footnoteContent = removeBackLinks($(footnotes[i]).html().trim(), $(finalFNLinks[i])
                                    .data("footnote-backlink-ref")).replace(/"/g, "&quot;")
                                    .replace(/&lt;/g, "&ltsym;").replace(/&gt;/g, "&gtsym;");
                footnoteIDNum = +(i + 1);
                footnoteButton = "";
                $relevantFNLink = $(finalFNLinks[i]);
                $relevantFootnote = $(footnotes[i]);

                // Determines whether this is in the same number reset container (as defined in settings)
                // as the last footnote and changes the footnote number accordingly
                if(settings.numberResetSelector) {
                    $curResetElement = $relevantFNLink.closest(settings.numberResetSelector);
                    if($curResetElement.is($lastResetElement)) {
                        footnoteNum += 1;
                    } else {
                        footnoteNum = 1;
                    }
                    $lastResetElement = $curResetElement;
                } else {
                    footnoteNum = footnoteIDNum;
                }

                // Add a paragraph container if the footnote was written directly in the list element
                if(footnoteContent.indexOf("<") !== 0) {
                    footnoteContent = "<p>" + footnoteContent + "</p>";
                }

                // Gives default button markup unless custom one is defined
                // Gets the easy replacements out of the way
                footnoteButton = settings.buttonMarkup.replace(/\{\{FOOTNOTENUM\}\}/g, footnoteNum)
                                    .replace(/\{\{FOOTNOTEID\}\}/g, footnoteIDNum)
                                    .replace(/\{\{FOOTNOTECONTENT\}\}/g, footnoteContent);

                // Handles replacements of SUP/FN attribute requests
                footnoteButton = replaceWithReferenceAttributes(footnoteButton, "SUP", $relevantFNLink);
                footnoteButton = replaceWithReferenceAttributes(footnoteButton, "FN", $relevantFootnote);

                $footnoteButton = $(footnoteButton).insertBefore($relevantFNLink);

                var $parent = $relevantFootnote.parent();
                switch(settings.actionOriginalFN.toLowerCase()) {
                    case "delete":
                        $relevantFNLink.remove();
                        $relevantFootnote.remove();
                        deleteEmptyOrHR($parent);
                        break;
                    case "hide":
                        $relevantFNLink.addClass("footnote-print-only");
                        $relevantFootnote.addClass("footnote-print-only");
                        deleteEmptyOrHR($parent);
                        break;
                    case "ignore":
                        $relevantFNLink.addClass("footnote-print-only");
                        break;
                }
            } // end of loop through footnotes
        };


        // FUNCTION ----
        // cleanFootnoteLinks

        // PURPOSE -----
        // Groups the ID and HREF of a superscript/ anchor tag pair in data attributes
        // This resolves the issue of the href and backlink id being separated between the two elements

        // IN ----------
        // Anchors that link to footnotes

        // OUT ---------
        // Array of top-level emenets with data attributes for combined ID/ HREF

        var cleanFootnoteLinks = function($footnoteAnchors, footnoteLinks) {
            var $supParent,
                $supChild,
                linkHREF,
                linkID;

            // Problem: backlink ID might point to containing superscript of the fn link
            // Solution: Check if there is a superscript and move the href/ ID up to it.
            // The combined id/ href of the sup/a pair are stored in sup using data attributes
            $footnoteAnchors.each(function() {
                var $this = $(this);
                linkHREF = "#" + ($this.attr("href")).split("#")[1]; // just the fragment ID
                $supParent = $this.closest("sup");
                $supChild = $this.find("sup");

                if($supParent.length > 0) {
                    // Assign the link ID to be the parent's and child's combined
                    linkID = ($supParent.attr("id") || "") + ($this.attr("id") || "");
                    footnoteLinks.push(
                        $supParent.attr({
                            "data-footnote-backlink-ref": linkID,
                            "data-footnote-ref": linkHREF
                        })
                    );
                } else if($supChild.length > 0) {
                    linkID = ($supChild.attr("id") || "") + ($this.attr("id") || "");
                    footnoteLinks.push(
                        $this.attr({
                            "data-footnote-backlink-ref": linkID,
                            "data-footnote-ref": linkHREF
                        })
                    );
                } else {
                    // || "" protects against undefined ID's
                    linkID = $this.attr("id") || "";
                    footnoteLinks.push(
                        $this.attr({
                            "data-footnote-backlink-ref": linkID,
                            "data-footnote-ref": linkHREF
                        })
                    );
                }
            });
        };


        // FUNCTION ----
        // deleteEmptyOrHR

        // PURPOSE -----
        // Propogates the decision of deleting/ hiding the original footnotes up the hierarchy,
        // eliminating any empty/ fully-hidden elements containing the footnotes and
        // any horizontal rules used to denote the start of the footnote section

        // IN ----------
        // Container of the footnote that was deleted/ hidden

        // OUT ---------
        // Array of top-level emenets with data attributes for combined ID/ HREF

        var deleteEmptyOrHR = function($el) {

            var $parent;
            // If it has no children or all children have been hidden
            if($el.is(":empty") || $el.children(":not(.footnote-print-only)").length === 0) {
                $parent = $el.parent();
                if(settings.actionOriginalFN.toLowerCase() === "delete") {
                    $el.remove();
                } else {
                    $el.addClass("footnote-print-only");
                }

                // Propogate up to the container element
                deleteEmptyOrHR($parent);

            } else if($el.children(":not(.footnote-print-only)").length == $el.children("hr:not(.footnote-print-only)").length) {

                // If the only child not hidden/ removed is a horizontal rule, remove the entire container
                $parent = $el.parent();
                if(settings.actionOriginalFN.toLowerCase() === "delete") {
                    $el.remove();
                } else {
                    $el.children("hr").addClass("footnote-print-only");
                    $el.addClass("footnote-print-only");
                }

                // Propogate up to the container element
                deleteEmptyOrHR($parent);
            }
        };


        // FUNCTION ----
        // removeBackLinks

        // PURPOSE -----
        // Removes any links from the footnote back to the footnote link
        // as these don't make sense when the footnote is shown inline

        // IN ----------
        // HTML of the footnote possibly containing the backlink and
        // the ID(s) of the footnote link

        // OUT ---------
        // New HTML string with relevant links taken out

        var removeBackLinks = function(footnoteHTML, backlinkID) {

            // First, though, take care of multiple ID's by getting rid of spaces
            if(backlinkID.indexOf(" ") >= 0) {
                backlinkID = backlinkID.trim().replace(/ +/g, "|").replace(/(.*)/g, "($1)");
            }

            // Regex finds the preceding space/ nbsp, the anchor tag and contents
            var regex = new RegExp("(\\s|&nbsp;)*<\\s*a[^#<]*#" + backlinkID + "[^>]*>(.*?)<\\s*/\\s*a>", "g");
            return footnoteHTML.replace(regex, "").replace("[]", "");
        };


        // FUNCTION ----
        // replaceWithReferenceAttributes

        // PURPOSE -----
        // Replaces the reference attributes (encased in {{}}) with the relevant attributes
        // from the desired element; for example, {{SUP:id}} will be replaced with the ID of the
        // superscript element passed as $referenceElement

        // IN ----------
        // String to do replacements on, the reference keyword to look for (i.e., BUTTON or SUP),
        // and the associated element to search through for the identified attribute(s)

        // OUT ---------
        // New string with replacements performed

        var replaceWithReferenceAttributes = function(string, referenceKeyword, $referenceElement) {
            var refRegex = new RegExp("\\{\\{" + referenceKeyword + ":([^\\}]*)\\}\\}", "g"),
                refMatches,
                refReplaceText,
                refReplaceRegex;

            // Performs the regex and does the replacement until it doesn't find any more matches
            refMatches = refRegex.exec(string);
            while (refMatches) {
                // refMatches[1] stores the attribute that is to be matched
                 if(refMatches[1]) {
                    refReplaceText = $referenceElement.attr(refMatches[1]) || "";
                    string = string.replace("{{" + referenceKeyword + ":" + refMatches[1] + "}}", refReplaceText);
                }
                refMatches = refRegex.exec(string);
            }
            return string;
        };



        //  ________   ______  _________  ________  __   __   ________   _________  ______
        // /_______/\ /_____/\/________/\/_______/\/_/\ /_/\ /_______/\ /________/\/_____/\
        // \::: _  \ \\:::__\/\__.::.__\/\__.::._\/\:\ \\ \ \\::: _  \ \\__.::.__\/\::::_\/_
        //  \::(_)  \ \\:\ \  __ \::\ \     \::\ \  \:\ \\ \ \\::(_)  \ \  \::\ \   \:\/___/\
        //   \:: __  \ \\:\ \/_/\ \::\ \    _\::\ \__\:\_/.:\ \\:: __  \ \  \::\ \   \::___\/_
        //    \:.\ \  \ \\:\_\ \ \ \::\ \  /__\::\__/\\ ..::/ / \:.\ \  \ \  \::\ \   \:\____/\
        //     \__\/\__\/ \_____\/  \__\/  \________\/ \___/_(   \__\/\__\/   \__\/    \_____\/
        //


        // FUNCTION ----
        // buttonHover

        // PURPOSE -----
        // To activate the popover of a hovered footnote button
        // Also removes other popovers, if allowMultipleFN is false

        // IN ----------
        // Event that contains the target of the mouseenter event

        var buttonHover = function(e) {
            if(settings.activateOnHover) {
                var $buttonHovered = $(e.target).closest(".footnote-button"),
                    dataIdentifier = "[data-footnote-identifier=\"" + $buttonHovered.attr("data-footnote-identifier") + "\"]";
                if($buttonHovered.hasClass("active")) return;

                $buttonHovered.addClass("hover-instantiated");

                // Delete other popovers, unless overriden in the settings
                if(!settings.allowMultipleFN) {
                    var otherPopoverSelector = ".footnote-content:not(" + dataIdentifier + ")";
                    removePopovers(otherPopoverSelector);
                }
                createPopover(".footnote-button" + dataIdentifier).addClass("hover-instantiated");
            }
        };


        // FUNCTION ----
        // touchClick

        // PURPOSE -----
        // Activates the button the was clicked/ taps
        // Also removes other popovers, if allowMultipleFN is false
        // Finally, removes all popovers if something non-fn related was clicked/ tapped

        // IN ----------
        // Event that contains the target of the tap/click event

        var touchClick = function(e){
            var $target         = $(e.target),
                $nearButton     = $target.closest(".footnote-button");
                $nearFootnote   = $target.closest(".footnote-content");

            // If a button was tapped/ clicked
            if($nearButton.length > 0) {
                // Button was clicked
                // Cancel the link, if it exists
                e.preventDefault();

                // Do the button clicking
                clickButton($nearButton);

            } else if($nearFootnote.length < 1) {
                // Something other than a button or popover was pressed
                if($(".footnote-content").length > 0) {
                    removePopovers();
                }

            }
        };


        // FUNCTION ----
        // clickButton

        // PURPOSE -----
        // Handles the logic of clicking/ tapping the footnote button
        // That is, activates the popover if it isn't already active (+ deactivate others, if appropriate)
        // or, deactivates the popover if it is already active

        // IN ----------
        // Button being clicked/ pressed

        var clickButton = function($button) {

            // Cancel blur
            $button.blur();

            // Get the identifier of the footnote
            var dataIdentifier = "data-footnote-identifier=\"" + $button.attr("data-footnote-identifier") + "\"";

            // Only create footnote if it's not already active
            // If it's activating, ignore the new activation until the popover is fully formed.
            if($button.hasClass("changing")) {

                return;

            } else if(!$button.hasClass("active")) {

                $button.addClass("changing");
                setTimeout(function() {
                    $button.removeClass("changing");
                }, settings.popoverCreateDelay);
                createPopover(".footnote-button[" + dataIdentifier + "]");
                $button.addClass("click-instantiated");

                // Delete all other footnote popovers if we are only allowing one
                if(!settings.allowMultipleFN) {
                    removePopovers(".footnote-content:not([" + dataIdentifier + "])");
                }

            } else {

                // A fully instantiated footnote; either remove it or all footnotes, depending on settings
                if(!settings.allowMultipleFN) {
                    removePopovers();
                } else {
                    removePopovers(".footnote-content[" + dataIdentifier + "]");
                }

            }
        };


        // FUNCTION ----
        // createPopover

        // PURPOSE -----
        // Instantiates the footnote popover of the buttons matching the passed selector.
        // This includes replacing any variables in the content markup, decoding any special characters,
        // Adding the new element to the page, calling the position function, and adding the scroll handler

        // IN ----------
        // Selector of buttons that are to be activated

        // OUT ---------
        // All footnotes activated by the function

        var createPopover = function(selector) {

            selector = selector || ".footnote-button";

            // Activate all matching if multiple footnotes are allowed
            // Or only the first matching element otherwise
            var $buttons;
            if(typeof(selector) !== "string" && settings.allowMultipleFN) {
                $buttons = selector;
            } else if(typeof(selector) !== "string") {
                $buttons = selector.first();
            } else if(settings.allowMultipleFN) {
                $buttons = $(selector).closest(".footnote-button");
            } else {
                $buttons = $(selector + ":first").closest(".footnote-button");
            }

            var $popoversCreated = $();

            $buttons.each(function() {
                var $this = $(this),
                    content;

                try {
                    // Gets the easy replacements out of the way (try is there to ignore the "replacing undefined" error if it's activated too freuqnetly)
                    content = settings.contentMarkup
                                .replace(/\{\{FOOTNOTENUM\}\}/g, $this.attr("data-footnote-number"))
                                .replace(/\{\{FOOTNOTEID\}\}/g, $this.attr("data-footnote-identifier"))
                                .replace(/\{\{FOOTNOTECONTENT\}\}/g, $this.attr("data-footnote-content")
                                .replace(/&gtsym;/, "&gt;").replace(/&ltsym;/, "&lt;"));

                    // Handles replacements of BUTTON attribute requests
                    content = replaceWithReferenceAttributes(content, "BUTTON", $this);
                } finally {

                    // Create content and activate user-defined callback on it
                    $content = $(content);
                    try { settings.activateCallback($content); } catch(err) {}

                    if(!settings.appendPopoversTo) {

                        // Insert content after next block-level element, or after the nearest footnote
                        $nearestBlock = $this.closest("p, div, pre, li, ul, section, article, main, aside");
                        $siblingFootnote = $nearestBlock.siblings(".footnote-content:last");

                        if($siblingFootnote.length > 0) {
                            $content.insertAfter($siblingFootnote);
                        } else {
                            $content.insertAfter($nearestBlock);
                        }

                    } else {

                        $content.appendTo(settings.appendPopoversTo + ":first");
                    }

                    // Instantiate the max-height for storage and use in repositioning
                    $content.attr("data-bigfoot-max-height", $content.height());

                    repositionFeet();
                    $this.addClass("active");

                    // Bind the scroll handler to the popover
                    $content.find(".footnote-content-wrapper").bindScrollHandler();
                    $popoversCreated = $popoversCreated.add($content);
                }
            });

            // Add active class after a delay to give it time to transition
            setTimeout(function() {
                $popoversCreated.addClass("active");
            }, settings.popoverCreateDelay);

            return $popoversCreated;
        };


        // FUNCTION ----
        // bindScrollHandler

        // PURPOSE -----
        // Prevents scrolling of the page when you reach the top/ bottom
        // of scrolling a scrollable footnote popover

        // IN ----------
        // Run on popover(s) that are to have the event bound

        // SOURCE ------
        // adapted from: http://stackoverflow.com/questions/16323770/stop-page-from-scrolling-if-hovering-div

        $.fn.bindScrollHandler = function() {
            // Don't even bother checking if option is set to false
            if(!settings.preventPageScroll) { return; }

            $(this).on("DOMMouseScroll mousewheel", function(e) {

                var $this = $(this),
                    scrollTop = $this.scrollTop(),
                    scrollHeight = $this[0].scrollHeight,
                    height = parseInt($this.css("height")),
                    $popover = $this.closest(".footnote-content");

                // Fix for Safari 7 not properly calculating scrollHeight()
                // Just add the class as soon as there is any scrolling
                if($this.scrollTop() > 0 && $this.scrollTop() < 10) {
                    $popover.addClass("scrollable");
                }

                // Return if the element isn't scrollable
                if(!$popover.hasClass("scrollable")) { return; }

                var delta = (e.type == 'DOMMouseScroll' ?
                             e.originalEvent.detail * -40 :
                             e.originalEvent.wheelDelta), // Get the change in scroll position
                    up = delta > 0; // Decide whether the scroll was up or down

                var prevent = function() {
                    e.stopPropagation();
                    e.preventDefault();
                    e.returnValue = false;
                    return false;
                };

                if(!up && -delta > scrollHeight - height - scrollTop) {

                    // Scrolling down, but this will take us past the bottom.
                    $this.scrollTop(scrollHeight);
                    $popover.addClass("fully-scrolled"); // Give a class for removal of scroll-related styles
                    return prevent();
                } else if(up && delta > scrollTop) {

                    // Scrolling up, but this will take us past the top.
                    $this.scrollTop(0);
                    $popover.removeClass("fully-scrolled");
                    return prevent();
                } else {
                    $popover.removeClass("fully-scrolled");
                }
            });
        };



        //  ______   ______   ________   ______  _________  ________  __   __   ________   _________  ______
        // /_____/\ /_____/\ /_______/\ /_____/\/________/\/_______/\/_/\ /_/\ /_______/\ /________/\/_____/\
        // \:::_ \ \\::::_\/_\::: _  \ \\:::__\/\__.::.__\/\__.::._\/\:\ \\ \ \\::: _  \ \\__.::.__\/\::::_\/_
        //  \:\ \ \ \\:\/___/\\::(_)  \ \\:\ \  __ \::\ \     \::\ \  \:\ \\ \ \\::(_)  \ \  \::\ \   \:\/___/\
        //   \:\ \ \ \\::___\/_\:: __  \ \\:\ \/_/\ \::\ \    _\::\ \__\:\_/.:\ \\:: __  \ \  \::\ \   \::___\/_
        //    \:\/.:| |\:\____/\\:.\ \  \ \\:\_\ \ \ \::\ \  /__\::\__/\\ ..::/ / \:.\ \  \ \  \::\ \   \:\____/\
        //     \____/_/ \_____\/ \__\/\__\/ \_____\/  \__\/  \________\/ \___/_(   \__\/\__\/   \__\/    \_____\/
        //

        // FUNCTION ----
        // unhoverFeet

        // PURPOSE -----
        // Removes the unhovered footnote content if deleteOnUnhover is true

        // IN ----------
        // Event that contains the target of the mouseout event

        var unhoverFeet = function(e) {
            if(settings.deleteOnUnhover && settings.activateOnHover) {
                setTimeout(function() {
                    // If the new element is NOT a descendant of the footnote button
                    var $target = $(e.target).closest(".footnote-content, .footnote-button");
                    if($(".footnote-button:hover, .footnote-content:hover").length < 1) {
                        removePopovers();
                    }
                }, settings.hoverDelay);
            }
        };


        // FUNCTION ----
        // escapeKeypress

        // PURPOSE -----
        // Removes all popovers on keypress

        // IN ----------
        // Event that contains the key that was pressed

        var escapeKeypress = function(e) {
            if(e.keyCode == 27) {
                removePopovers();
            }
        };


        // FUNCTION ----
        // removePopovers

        // PURPOSE -----
        // Removes/ adds appropriate classes to the footnote content and button
        // After a delay (to allow for transitions) it removes the actual footnote content

        // IN ----------
        // Selector of footnotes to deactivate and timeout before deleting actual elements

        // OUT ---------
        // Footnote buttons that were deactivated

        var removePopovers = function(footnotes, timeout) {
            footnotes = footnotes || ".footnote-content";
            timeout = timeout || settings.popoverDeleteDelay;

            var $buttonsClosed = $(),
                footnoteID,
                $linkedButton,
                $this;

            $(footnotes).each(function() {
                $this = $(this);
                footnoteID = $this.attr("data-footnote-identifier");
                $linkedButton = $(".footnote-button[data-footnote-identifier=\"" + footnoteID + "\"]");

                if(!$linkedButton.hasClass("changing")) {

                    $buttonsClosed = $buttonsClosed.add($linkedButton);
                    $linkedButton.removeClass("active hover-instantiated click-instantiated").addClass("changing");
                    $this.removeClass("active").addClass("disapearing");

                    // Gets rid of the footnote after the timeout
                    setTimeout(function() {
                        $this.remove();
                        $linkedButton.removeClass("changing");
                    }, timeout);
                }
            });

            return $buttonsClosed;
        };



        //  ______    ______   ______   ______   ______    ________  _________  ________  ______   ___   __
        // /_____/\  /_____/\ /_____/\ /_____/\ /_____/\  /_______/\/________/\/_______/\/_____/\ /__/\ /__/\
        // \:::_ \ \ \::::_\/_\:::_ \ \\:::_ \ \\::::_\/_ \__.::._\/\__.::.__\/\__.::._\/\:::_ \ \\::\_\\  \ \
        //  \:(_) ) )_\:\/___/\\:(_) \ \\:\ \ \ \\:\/___/\   \::\ \    \::\ \     \::\ \  \:\ \ \ \\:. `-\  \ \
        //   \: __ `\ \\::___\/_\: ___\/ \:\ \ \ \\_::._\:\  _\::\ \__  \::\ \    _\::\ \__\:\ \ \ \\:. _    \ \
        //    \ \ `\ \ \\:\____/\\ \ \    \:\_\ \ \ /____\:\/__\::\__/\  \::\ \  /__\::\__/\\:\_\ \ \\. \`-\  \ \
        //     \_\/ \_\/ \_____\/ \_\/     \_____\/ \_____\/\________\/   \__\/  \________\/ \_____\/ \__\/ \__\/
        //


        // FUNCTION ----
        // repositionFeet

        // PURPOSE -----
        // Positions each footnote relative to its button

        var repositionFeet = function() {
            if(settings.positionContent) {

                $(".footnote-content").each(function() {

                    // Element Definitions
                    var $this               = $(this),
                        dataIdentifier      = "data-footnote-identifier=\"" + $this.attr("data-footnote-identifier") + "\"",
                        $contentWrapper     = $this.find(".footnote-content-wrapper"),
                        $button             = $(".footnote-button[" + dataIdentifier + "]");

                    // Spacing Information
                    var roomLeft            = roomCalc($button),
                        contentWidth        = parseFloat($this.css("width")),
                        marginSize          = parseFloat($this.css("margin-top")),
                        maxHeightInCSS      = +($this.attr("data-bigfoot-max-height")),
                        totalHeightInCSS    = 2*marginSize + maxHeightInCSS,
                        maxHeightOnScreen   = 10000;

                    // Position tooltip on top if:
                    // total space on bottom is not enough to hold footnote AND
                    // top room is larger than bottom room
                    if(roomLeft.bottomRoom < totalHeightInCSS && roomLeft.topRoom > roomLeft.bottomRoom) {
                        $this.css({"top": "auto", "bottom": roomLeft.bottomRoom + "px"}).addClass("top").removeClass("bottom");
                        maxHeightOnScreen = roomLeft.topRoom - marginSize - 15;
                        $this.css({"transform-origin": (roomLeft.leftRelative*100) + "% 100%"});
                    } else {
                        $this.css({"bottom": "auto", "top": roomLeft.topRoom + "px"}).addClass("bottom").removeClass("top");
                        maxHeightOnScreen = roomLeft.bottomRoom - marginSize - 15;
                        $this.css({"transform-origin": (roomLeft.leftRelative*100) + "% 0%"});
                    }

                    // Sets the max height so that there is no footnote overflow
                    $this.find(".footnote-content-wrapper").css({"max-height": Math.min(maxHeightOnScreen, maxHeightInCSS) + "px"});

                    // Positions the popover
                    $this.css({"left": (roomLeft.leftRoom - (roomLeft.leftRelative * contentWidth)) + "px"});

                    // Position the tooltip
                    positionTooltip($this, roomLeft.leftRelative);

                    // Give scrollable class if the content hight is larger than the container
                    if(parseInt($this.css("height")) < $this.find(".footnote-content-wrapper")[0].scrollHeight) {
                        $this.addClass("scrollable");
                    }
                });
            }
        };


        // FUNCTION ----
        // positionTooltip

        // PURPOSE -----
        // Positions the tooltip at the same relative horizontal position as the button

        // IN ----------
        // Footnote popover to get the tooltip of and the relative horizontal position (as a decimal)

        var positionTooltip = function($popover, leftRelative) {
            leftRelative = leftRelative || 0.5; // default to 50%
            var $tooltip = $popover.find(".tooltip");

            if($tooltip.length > 0) {
                $tooltip.css({"left": leftRelative*100 + "%"});
            }
        };


        // FUNCTION ----
        // roomCalc

        // PURPOSE -----
        // Calculates area on the top, left, bottom and right of the element
        // Also calculates the relative position to the left and top of the screen

        // IN ----------
        // Element to calculate screen position of

        // OUT ---------
        // Object containing room on all sides and top/ left relative positions
        // All measurements are relative to the middle of the element

        var roomCalc = function($el) {
            var elWidth     = parseFloat($el.outerWidth()),
                elHeight    = parseFloat($el.outerHeight()),
                w           = viewportSize(),
                topRoom     = $el.offset().top - $(window).scrollTop() + elHeight/2,
                leftRoom    = $el.offset().left + elWidth/2;

            return {
                topRoom         : topRoom,
                bottomRoom      : w.height - topRoom,
                leftRoom        : leftRoom,
                rightRoom       : w.width - leftRoom,
                leftRelative    : leftRoom / w.width,
                topRelative     : topRoom / w.height
            };
        };


        // FUNCTION ----
        // viewportSize

        // PURPOSE -----
        // Calculates the height and width of the viewport

        // OUT ---------
        // Object with .width and .height properties

        var viewportSize = function() {
            var test = document.createElement("div");

            test.style.cssText = "position: fixed;top: 0;left: 0;bottom: 0;right: 0;";
            document.documentElement.insertBefore(test, document.documentElement.firstChild);

            var dims = { width: test.offsetWidth, height: test.offsetHeight };
            document.documentElement.removeChild(test);

            return dims;
        };



        //    _______   ______    ______   ________   ___   ___   ______   ______    ________  ___   __    _________  ______
        //  /_______/\ /_____/\  /_____/\ /_______/\ /___/\/__/\ /_____/\ /_____/\  /_______/\/__/\ /__/\ /________/\/_____/\
        //  \::: _  \ \\:::_ \ \ \::::_\/_\::: _  \ \\::.\ \\ \ \\:::_ \ \\:::_ \ \ \__.::._\/\::\_\\  \ \\__.::.__\/\::::_\/_
        //   \::(_)  \/_\:(_) ) )_\:\/___/\\::(_)  \ \\:: \/_) \ \\:(_) \ \\:\ \ \ \   \::\ \  \:. `-\  \ \  \::\ \   \:\/___/\
        //    \::  _  \ \\: __ `\ \\::___\/_\:: __  \ \\:. __  ( ( \: ___\/ \:\ \ \ \  _\::\ \__\:. _    \ \  \::\ \   \_::._\:\
        //     \::(_)  \ \\ \ `\ \ \\:\____/\\:.\ \  \ \\: \ )  \ \ \ \ \    \:\_\ \ \/__\::\__/\\. \`-\  \ \  \::\ \    /____\:\
        //      \_______\/ \_\/ \_\/ \_____\/ \__\/\__\/ \__\/\__\/  \_\/     \_____\/\________\/ \__\/ \__\/   \__\/    \_____\/
        //


        // FUNCTION ----
        // addBreakpoint

        // PURPOSE -----
        // Adds a breakpoint within the HTML at which a user-defined function
        // will be called. The minimum requirement is that a min/ max size is
        // provided; after that point, the footnote will stop being positioned
        // (i.e., to allow for bottom-fixed footnotes on small screens).

        // IN ----------
        // size: Size to break at. Can be simple (i.e., ">10px" or "<10em"), full
        // media query (i.e., "(max-width: 400px)"), or a MediaQueryList object.
        // deleteDelay: the delay by which to wait when closing/ reopening footnotes
        // on breakpoint changes. Defaults to settings.popoverDeleteDelay.
        // removeOpen: whether or not to close (and reopen) footnotes that are open
        // at the time the breakpoint changes. Defaults to true.
        // trueCallback: function to call when the media query is initially matched.
        // will be passed the removeOpen option and a copy of the bigfoot object.
        // falseCallback: function to call when the media query is initially not matched.
        // The same variables are passed in.

        // OUT ---------
        // Object indicating whether the breakpoint was added and, if so, the MQList object
        // and listener function.

        var addBreakpoint = function(size, deleteDelay, removeOpen,
                                trueCallback, falseCallback) {

            // Set defaults
            deleteDelay = deleteDelay || settings.popoverDeleteDelay;
            if(removeOpen === null || removeOpen !== false) removeOpen = true;

            var mql, minMax, s;

            // If they passed a string representation
            if(typeof(size) === "string") {

                // Repalce special strings with corresponding widths
                if(size.toLowerCase() === "iphone") {
                    s = "<320px";
                } else if(size.toLowerCase() === "ipad") {
                    s = "<768px";
                } else {
                    s = size;
                }

                // Check on the nature of the string (simple or full media query)
                if(s.charAt(0) === ">") {
                    minMax = "min";
                } else if(s.charAt(0) === "<") {
                    minMax = "max";
                } else {
                    minMax = null;
                }

                // Create the media query
                var query = minMax ? "(" + minMax + "-width: " + s.substring(1) + ")" : s;
                mql = window.matchMedia(query);

            } else {

                // Assumption is that a MediaQueryList object was passed.
                mql = size;
            }

            // If a non-MQList object is passed on the media is invalid
            if(mql.media && mql.media === "invalid") return {
                added: false,
                mq: mql,
                listener: null
            };

            // Determine whether to close/ remove popovers on the true/false callbacks
            var trueDefaultPositionSetting = minMax === "min",
                falseDefaultPositionSetting = minMax === "max";

            // Create default trueCallback
            trueCallback = trueCallback ||
                            makeDefaultCallbacks(
                                removeOpen, deleteDelay,
                                trueDefaultPositionSetting, function($popover) {
                                    $popover.addClass("fixed-bottom");
                                }
                            );

            // Create default falseCallback
            falseCallback = falseCallback ||
                            makeDefaultCallbacks(
                                removeOpen, deleteDelay,
                                falseDefaultPositionSetting, function() {}
                            );

            // MQ Listener function
            var mqListener = function(mq) {
                if(mq.matches) {
                    trueCallback(removeOpen, bigfoot);
                } else {
                    falseCallback(removeOpen, bigfoot);
                }
            };

            // Attach listener and call it for the initial match/ non-match
            mql.addListener(mqListener);
            mqListener(mql);

            // Add to the breakpoints setting
            settings.breakpoints[size] = {
                added: true,
                mq: mql,
                listener: mqListener
            };

            return settings.breakpoints[size];

        };


        // FUNCTION ----
        // makeDefaultCallbacks

        // PURPOSE -----
        // Creates the default callbacks to attach to the MQ events.

        // IN ----------
        // See above for the first three variables.
        // callback: The function to be assigned to the "activateCallback" setting
        // (called when creating new footnotes)

        // OUT ---------
        // Default MQ matches/ non-matches function.

        var makeDefaultCallbacks = function(removeOpen, deleteDelay, positioningBool, callback) {
            return function(removeOpen, bigfoot) {
                var $closedPopovers;

                if(removeOpen) {
                    $closedPopovers = bigfoot.close();
                    bigfoot.updateSetting("activateCallback", callback);
                }
                setTimeout(function() {
                    bigfoot.updateSetting("positionContent", positioningBool);
                    if(removeOpen) bigfoot.activate($closedPopovers);
                }, deleteDelay);
            };
        };


        // FUNCTION ----
        // removeBreakpoint

        // PURPOSE -----
        // Removes a previously-created breakpoint, calling the false condition
        // before doing so (or, a user-provided function instead).

        // IN ----------
        // target: the media query to remove, either by passing the string used to create
        // the breakpoint initially, or by passing the associated MediaQueryList object.
        // callback: the (optional) function to call before removing the listener.

        // OUT ---------
        // true if a media query was found and deleted, false otherwise.

        var removeBreakpoint = function(target, callback) {
            var mq = null,
                b, mqFount = false;
            if(typeof(target) === "string") {
                mqFound = settings.breakpoints[target] !== undefined;
            } else {
                for(b in settings.breakpoints) {
                    if(settings.breakpoints.hasOwnProperty(b) && settings.breakpoints[b].mq === target) {
                        mqFound = true;
                        break;
                    }
                }
            }

            if(mqFound) {
                var breakpoint = settings.breakpoints[b || target];
                // Calls the non-matching callback one last time
                if(callback) {
                    callback({matches: false});
                } else {
                    breakpoint.listener({matches: false});
                }
                breakpoint.mq.removeListener(breakpoint.listener);
                delete settings.breakpoints[b || target];
            }

            return mqFound;
        };



        //  ______   _________  ___   ___   ______   ______
        // /_____/\ /________/\/__/\ /__/\ /_____/\ /_____/\
        // \:::_ \ \\__.::.__\/\::\ \\  \ \\::::_\/_\:::_ \ \
        //  \:\ \ \ \  \::\ \   \::\/_\ .\ \\:\/___/\\:(_) ) )_
        //   \:\ \ \ \  \::\ \   \:: ___::\ \\::___\/_\: __ `\ \
        //    \:\_\ \ \  \::\ \   \: \ \\::\ \\:\____/\\ \ `\ \ \
        //     \_____\/   \__\/    \__\/ \::\/ \_____\/ \_\/ \_\/
        //


        // FUNCTION ----
        // updateSetting

        // PURPOSE -----
        // Updates the specified setting(s) with the value(s) you pass

        // IN ----------
        // Setting to adjust and new value for the setting (or an object
        // with all setting-new value pairs)

        // OUT ---------
        // Returns the old value for the setting (or an object with old settings
        // for each assigned property, if more than one were set)

        var updateSetting = function(newSettings, value) {

            var oldValue;

            if(typeof(newSettings) === "string") {

                oldValue = settings[newSettings];
                settings[newSettings] = value;

            } else {

                oldValue = {};

                for(var prop in newSettings) {
                    if(newSettings.hasOwnProperty(prop)) {
                        oldValue[prop] = settings[prop];
                        settings[prop] = newSettings[prop];
                    }
                }

            }

            return oldValue;
        };


        // FUNCTION ----
        // getSetting

        // PURPOSE -----
        // Returns the settings object

        var getSetting = function(setting) {

            return settings[setting];
        };



        //   _______    ________  ___   __    ______    ________  ___   __    _______
        // /_______/\  /_______/\/__/\ /__/\ /_____/\  /_______/\/__/\ /__/\ /______/\
        // \::: _  \ \ \__.::._\/\::\_\\  \ \\:::_ \ \ \__.::._\/\::\_\\  \ \\::::__\/__
        //  \::(_)  \/_   \::\ \  \:. `-\  \ \\:\ \ \ \   \::\ \  \:. `-\  \ \\:\ /____/\
        //   \::  _  \ \  _\::\ \__\:. _    \ \\:\ \ \ \  _\::\ \__\:. _    \ \\:\\_  _\/
        //    \::(_)  \ \/__\::\__/\\. \`-\  \ \\:\/.:| |/__\::\__/\\. \`-\  \ \\:\_\ \ \
        //     \_______\/\________\/ \__\/ \__\/ \____/_/\________\/ \__\/ \__\/ \_____\/
        //

        $(document).ready(function() {

            footnoteInit();

            $(document).on("mouseenter", ".footnote-button", buttonHover);
            $(document).on("touchend click", touchClick);
            $(document).on("mouseout", ".hover-instantiated", unhoverFeet);
            $(document).on("keyup", escapeKeypress);
            $(window).on("scroll resize", repositionFeet);
        });



        //  ______    ______   _________  __  __   ______    ___   __
        // /_____/\  /_____/\ /________/\/_/\/_/\ /_____/\  /__/\ /__/\
        // \:::_ \ \ \::::_\/_\__.::.__\/\:\ \:\ \\:::_ \ \ \::\_\\  \ \
        //  \:(_) ) )_\:\/___/\  \::\ \   \:\ \:\ \\:(_) ) )_\:. `-\  \ \
        //   \: __ `\ \\::___\/_  \::\ \   \:\ \:\ \\: __ `\ \\:. _    \ \
        //    \ \ `\ \ \\:\____/\  \::\ \   \:\_\:\ \\ \ `\ \ \\. \`-\  \ \
        //     \_\/ \_\/ \_____\/   \__\/    \_____\/ \_\/ \_\/ \__\/ \__\/
        //

        bigfoot = {
            close: function(footnotes, timeout) {
                return removePopovers(footnotes, timeout);
            },
            activate: function(button) {
                return createPopover(button);
            },
            reposition: function() {
                return repositionFeet();
            },
            addBreakpoint: function(size, deleteDelay, removeOpen, trueCallback, falseCallback) {
                return addBreakpoint(size, deleteDelay, removeOpen, trueCallback, falseCallback);
            },
            removeBreakpoint: function(target, callback) {
                return removeBreakpoint(target, callback);
            },
            getSetting: function(setting) {
                return getSetting(setting);
            },
            updateSetting: function(setting, newValue) {
                return updateSetting(setting, newValue);
            }
        };

        return bigfoot;
    };

})(jQuery);