Priority Grace

By Joseph Taylor Last update Nov 12, 2006 — Installed 280 times.
// Priority Grace
// version 0.1
// 2006-11-12
// Copyright (c) 2006, Joseph Taylor
// Released under the GPL license
// http://www.gnu.org/copyleft/gpl.html

// ==UserScript==
// @name          Priority Grace
// @author        Joseph Taylor
// @namespace     http://www.textninja.net
// @description   Allow the user to reorder their 43Things goals by clicking and dragging, rather than suffering through text input hell
// @include       http://www.43things.com/people/reorder_things*
// ==/UserScript==

/* ==================== */
/* = Snippets of Data = */
/* ==================== */

var XPathExpressions = {
    GOAL_FORM : ".//form[@action='/people/savereorder']",
    NECESSARY_HIDDEN_FIELDS : ".//input[@type='hidden'][contains(@name, 'old')]",
    GOAL_LINKS : ".//td[@class='reordergoals']/a",
};

/* =============== */
/* = Error Types = */
/* =============== */

function UnrecognizedHTMLError() {
    this.name = "UnrecognizedHTMLError";
    this.message = "I was unable to understand the source code at \"" + location.href + "\".\n";
    this.message += "Maybe it isn't a goal reordering page, or maybe Robot Co-op changed the layout since this script was last updated.";
}
UnrecognizedHTMLError.prototype = Error.prototype;

function InvalidUseOfConstructorError() {
    this.name = "InvalidUseOfConstructorError";
    this.message = "That's a constructor, not a function.";
}
InvalidUseOfConstructorError.prototype = Error.prototype;

function UniterableResultError() {
    this.name = "UniterableResultError";
    this.message = "The result type you've given cannot be iterated through, and thus cannot be turned into an array.";
}
UniterableResultError.prototype = Error.prototype;

function NotSoupedUpYetError() {
    this.name = "NotSoupedUpYetError";
    this.message = "Sorry, but to perform that operation the goal list must first be \"souped up\".";
}
NotSoupedUpYetError.prototype = Error.prototype;

/* ================================ */
/* = Built-in Object Enhancements = */
/* ================================ */

// XPathResult (This would normally be slapped on to the prototype, but since it's a user script, I won't)
var iterateIntoArray = function(xresult) {
    if (xresult.resultType == 5 || xresult.resultType == 6) {
        var returnedArray = [];
        var nextNode = null;
        while (nextNode = xresult.iterateNext()) {
            returnedArray.push(nextNode);
        }
        return returnedArray;
    } else {
        throw new UniterableResultError;
    }
}

/* =========================== */
/* = Simply & Useful Objects = */
/* =========================== */

function PastelColour(h) {
    // Private functions
    // =================
    var makeRGBString = function(r, g, b) {
        return "rgb(" + Math.floor(r * 255) + "," + Math.floor(g * 255) + "," + Math.floor(b * 255) + ")";
    }
    
    // Properties
    // ==========
    this.h = h;
    this.s = 0.2;
    this.v = 0.85;
    
    // Methods
    // =======
    this.darken = function() {
        this.v = this.v * 0.5;
        return this;
    }
    
    this.toString = function() { // Converts the hsb values into rgb, and returns a CSS formatted string
        var i;
        var r, g, b;
        var f, p, q, t;
        var h = this.h, s = this.s, v = this.v;
        if (s == 0) {
            r = g = b = v;
            return makeRGBString(r,g,b);
        }
        h /= 60;
        i = Math.floor(h);
        f = h - i;
        p = v * (1 - s);
        q = v * (1 - s * f);
        t = v * (1 - s * (1 - f));
        switch  (i) {
            case 0:
                r = v;
                g = t;
                b = p;
                break;
            case 1:
                r = q;
                g = v;
                b = p;
                break;
            case 2:
                r = p;
                g = v;
                b = t;
                break;
            case 3:
                r = p;
                g = q;
                b = v;
                break;
            case 4:
                r = t;
                g = p;
                b = v;
                break;
            default:
                r = v;
                g = p;
                b = q;
                break;
        }
        return makeRGBString(r,g,b);
    };
}

/* ============================ */
/* = The Meat of the Program  = */
/* ============================ */

var ElementFinder = {
    find : function(xpathExpression, contextNode) {
        var result = document.evaluate(XPathExpressions[xpathExpression], (contextNode || document.body), null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
        result = iterateIntoArray(result);
        if (result.length > 0) {
            return result;
        } else {
            throw new UnrecognizedHTMLError;
        }
    }
};

var ElementWrapper = {
    GoalForm : function() {
        // Private Functions
        // =================
        var createListOfLinks = function(links) {
            var list = document.createElement("ol");
            list.className = "reorder_list";
            for (var i = 0; i < links.length; i++) {
                links[i].addEventListener("click", function(e) {
                    if (!confirm("Are you sure you want to navigate away from this page?  Any unsaved changes will be lost.")) {
                        e.preventDefault();
                    }
                }, false);
                // Choose a random background colour for our list item
                var backgroundColour = Utility.ColourGenerator.generateRandomPastelColour();
                // Create the list item
                var li = document.createElement("li");
                li.style.cursor = "move";
                li.style.backgroundColor = backgroundColour.toString();
                links[i].style.color = backgroundColour.darken().toString();
                links[i].style.textDecoration = "none";
                // li.style.color = backgroundColour.darken().toString(); // The foreground is a darker version of the background
                var goalNumber = /\d+$/.exec(links[i].href)[0];
                li.id = "goal_number_" + goalNumber;
                li.appendChild(links[i]);
                list.appendChild(li);
            }
            return list;
        };
    
        // Properties
        // ==========
        this.node = ElementFinder.find("GOAL_FORM")[0];
        this.node.id = "reorder_goals_form";
        this.node.className = "reorder_form"
        this.goalList = null;
    
        // Methods
        // =======
        this.updateHiddenFields = function() {
            if (!this.goalList) throw new NotSoupedUpYetError;
            var liElements = this.node.getElementsByTagName("li");
            for (var i = 0; i < liElements.length; i++) {
                var hidden = document.createElement("input");
                hidden.setAttribute("type", "hidden");
                hidden.name = "new_" + /\d+$/.exec(liElements[i].id)[0];
                hidden.value = i + 1;
                this.node.insertBefore(hidden, this.goalList);
            }
            return true;
        }
        
        this.soupUp = function() {
            var me = this;
            
            // Grab the hidden fields and links.  Make sure to clone them, since the originals are getting deleted
            var hiddenFields = ElementFinder.find("NECESSARY_HIDDEN_FIELDS", this.node);
            hiddenFields = Utility.NodeCloner.cloneAll(hiddenFields);
            var goalLinks = ElementFinder.find("GOAL_LINKS", this.node);
        
            // Grab the form's table
            var table = this.node.getElementsByTagName("table")[0];
        
            // Attach all the hidden fields to the form (outside the table)
            for (var i = 0; i < hiddenFields.length; i++) {
                this.node.insertBefore(hiddenFields[i], table);
            }
        
            // Replace the table of goals with a simple list
            this.goalList = createListOfLinks(goalLinks);
            this.goalList.id = "myListOfGoals";
            table.parentNode.replaceChild(this.goalList, table);
        
            // Create a save button, then append it to the end of the form
            var submitButton = document.createElement("input");
            submitButton.setAttribute("value", "Save");
            submitButton.setAttribute("type", "submit");
            this.node.appendChild(submitButton);
            
            // Using the scriptaculous API, make the goal list sortable
            unsafeWindow.Sortable.create(this.goalList.id, {});
            
            // Before the form is submitted, I need to update the hidden fields
            unsafeWindow.document.getElementById(this.node.id).onsubmit = function() {
                return me.updateHiddenFields();
            };
        }
    }
};

/* ============= */
/* = Utilities = */
/* ============= */

var Utility = {
    ScriptRunner: {
        run: function(scriptURL) {
            var script = document.createElement("script");
            script.src = scriptURL;
            document.getElementsByTagName("head")[0].appendChild(script);
        }
    },
    NodeCloner: {
        cloneAll: function(nodeArray, deepBoolean) {
            var returnedArray = [];
            for (var i = 0; i < nodeArray.length; i++) {
                returnedArray[i] = nodeArray[i].cloneNode(deepBoolean);
            }
            return returnedArray;
        }
    },
    ColourGenerator: {
        generateRandomPastelColour: function(lowerDegree, upperDegree) {
            var randomHue;
            if (arguments.length == 2) {
                randomHue = Math.floor(lowerDegree + Math.random() * (upperDegree - lowerDegree));
            } else {
                randomHue = Math.floor(Math.random() * 255);
            }
            return new PastelColour(randomHue);
        }
    }
};

/* ========= */
/* = Setup = */
/* ========= */

document.styleSheets[0].insertRule(".reorder_list { -moz-border-radius: 20px; background-color: #f0f0f0; border: solid 2px #ececec; list-style-type: none; margin: 0px; padding: 10px; }", 0);
document.styleSheets[0].insertRule(".reorder_list li { background-color: silver; padding: 5px; font-size: 1.2em; -moz-border-radius: 15px; margin: 10px 0px; text-align: center;  }", 0);
document.styleSheets[0].insertRule(".reorder_form input[type=submit] { display: table; margin-top: 10px; padding: 10px 30px; margin-left: auto; margin-right: auto; }", 0);
document.styleSheets[0].insertRule(".reorder_form a:hover { background-color: white; }", 0);

// Boot up the scriptaculous API
var libraries = ["effects.js", "dragdrop.js"];
for (var i = 0; i < libraries.length; i++) {
    Utility.ScriptRunner.run("http://www.43things.com/javascripts/" + libraries[i]);
}

/* ========== */
/* = Deploy = */
/* ========== */

window.addEventListener("load", function() {
    try {
        var goalForm = new ElementWrapper.GoalForm;
        goalForm.soupUp();
    } catch (e) {
        if (e instanceof UnrecognizedHTMLError) {
            var accountCreationForm = document.evaluate(".//form[@action='/account/create']", document.body, null, XPathResult.ANY_UNORDERED_NODE_TYPE, null).singleNodeValue;
            if (accountCreationForm) {
                return;
            } else {
                throw e;
            }
        } else {
            throw e;
        }
    }
}, false);