JoeSimmons / GM_config

// ==UserScript==
// @name           GM_config
// @namespace      http://userscripts.org/users/23652
// @description    GreaseMonkey Script Configurator
// @include        http://*
// @include        https://*
// @include        file:*
// @copyright      JoeSimmons & Sizzlemctwizzle & IzzySoft
// @version        1.2.58
// @license        LGPL version 3 or any later version; http://www.gnu.org/copyleft/lgpl.html
// ==/UserScript==

/* Instructions ---------------------------------------------------------------------------------------------------

    GM_config is cross-browser compatible.

    To use it in a Greasemonkey/Tampermonkey/Violentmonkey script, you can just @require it.

    If you can't @require it, you will need to manually include the code at the beginning of your user script.

    In non-Greasemonkey, stored settings will only be accessible on the same domain on which they were saved.

---------------------------------------------------------------------------------------------------------------- */



/* CHANGELOG --------------------------------------------------------------------------

1.2.58
    - added a 'slider' type; it allows adjusting by sliding it with your mouse
    - re-wrote the entire code

1.2.57
    - Fixed TypeError with the "options" setting of a "select" field
        The "options" setting needs to be a JSON object, not an array

1.2.56
    - started keeping changelog in source
    - added type "password"
    - fixed error when user doesn't define any sections

------------------------------------------------------------------------------------- */



var GM_config = {
    storage : 'GM_config',

    init : function () {
        var i, settings, arg, css, stored, kid,
            l = arguments.length;

            // loop through GM_config.init() arguments
        for (i = 0; i < l; i += 1) {
            arg = arguments[i];
            switch (typeof arg) {
                case 'object': {
                    for (var j in arg) { // could be a callback functions or settings object
                        switch (j) {
                            case "open": {
                                GM_config.onOpen=arg[j];
                                delete arg[j];
                                break; // called when frame is gone
                            }
                            case "close": {
                                GM_config.onClose=arg[j];
                                delete arg[j];
                                break; // called when settings have been saved
                            }
                            case "save": {
                                GM_config.onSave=arg[j];
                                delete arg[j];
                                break; // store the settings objects
                            }
                            default: {
                                settings = arg;
                            }
                        }
                    } break;
                }
                case 'function': {
                    GM_config.onOpen = arg;
                    break; // passing a bare function is set to open
                            // could be custom CSS or the title string
                }
                case 'string': {
                    if (arg.indexOf('{') !== -1 && arg.indexOf('}') !== -1) {
                        css = arg;
                    } else {
                        GM_config.title = arg;
                    }
                    break;
                }
                default: {
                    break;
                }
            }
        }
        if (!GM_config.title) {
            GM_config.title = 'Settings - Anonymous Script'; // if title wasn't passed through init()
        }

        // give the script a unique saving ID for non-firefox browsers
        GM_config.storage = GM_config.title.replace(/\W+/g, '').toLowerCase();

        stored = GM_config.read(); // read the stored settings
        GM_config.passed_values = {};
        for (i in settings) {
            GM_config.doSettingValue(settings, stored, i, null, false);
            if (settings[i].kids) {
                for (kid in settings[i].kids) {
                    GM_config.doSettingValue(settings, stored, kid, i, true);
                }
            }
        }
        GM_config.values = GM_config.passed_values;
        GM_config.settings = settings;
        if (css) {
            GM_config.css.stylish = css;
        }
    },

    // -------------------------------------------------------------------------------------------------------------------

    open: function () {
        if (document.evaluate("//iframe[@id='GM_config']",document,null,9,null).singleNodeValue) return;
        // Create frame
        document.body.appendChild((GM_config.frame=GM_config.create('iframe',{id:'GM_config', style:'position: fixed; top: 0; left: 0; opacity: 0; display: none; z-index: 999999; width: 75%; height: 75%; max-height: 95%; max-width: 95%; border:3px ridge #000000; overflow: auto;'})));
            GM_config.frame.src = 'about:blank'; // In WebKit src cant be set until it is added to the page
        GM_config.frame.addEventListener('load', function () {
            var obj = GM_config, doc = this.contentDocument, frameBody = doc.getElementsByTagName('body')[0], create=obj.create, settings=obj.settings, anch, secNo;
            obj.frame.contentDocument.getElementsByTagName('head')[0].appendChild(create('style',{type:'text/css',textContent:obj.css.basic + "\n\n" + obj.css.stylish}));

            // Add header and title
            frameBody.appendChild(create('div', {id:'header',className:'config_header block center', innerHTML:obj.title}));

            // Append elements
            anch = frameBody; // define frame body
            secNo = 0; // anchor to append elements
            for (var i in settings) {
                var type, field = settings[i], value = obj.values[i], section = (field.section ? field.section : ["Main Options"]),
                    headerExists = doc.evaluate(".//div[@class='section_header_holder' and starts-with(@id, 'section_')]", frameBody, null, 9, null).singleNodeValue;

                if (typeof field.section !== "undefined" || headerExists === null) {
                    anch = frameBody.appendChild(create('div', {className:'section_header_holder', id:'section_'+secNo, kids:new Array(
                      create('a', {className:'section_header center', href:"javascript:void(0);", id:'c_section_kids_'+secNo, textContent:section[0], onclick:function (){GM_config.toggle(this.id.substring(2));}}),
                      create('div', {id:'section_kids_'+secNo, className:'section_kids', style:obj.getValue('section_kids_'+secNo, "")==""?"":"display: none;"})
                    )}));
                    if (section[1]) anch.appendChild(create('p', {className:'section_desc center',innerHTML:section[1]}));
                    secNo++;
                }
                anch.childNodes[1].appendChild(GM_config.addToFrame(field, i, false));
            }

            // Add save and close buttons
            frameBody.appendChild(obj.create('div', {id:'buttons_holder', kids:new Array(
                obj.create('button',{id:'saveBtn',textContent:'Save',title:'Save options and close window',className:'saveclose_buttons',onclick:function (){GM_config.close(true)}}),
                obj.create('button',{id:'cancelBtn', textContent:'Cancel',title:'Close window',className:'saveclose_buttons',onclick:function (){GM_config.close(false)}}),
                obj.create('div', {className:'reset_holder block', kids:new Array(
                    obj.create('a',{id:'resetLink',textContent:'Restore to default',href:'#',title:'Restore settings to default configuration',className:'reset',onclick:obj.reset})
            )}))}));

            obj.center(); // Show and center it
            window.addEventListener('resize', obj.center, false); // Center it on resize
            if (obj.onOpen) obj.onOpen(); // Call the open() callback function
            
            // Close frame on window close
            window.addEventListener('beforeunload', function (){GM_config.remove(this);}, false);
        }, false);
    },

    // -------------------------------------------------------------------------------------------------------------------

    close: function (save) {
        if (save) {
            var type, fields = GM_config.settings, typewhite=/radio|text|hidden|password|checkbox/;

            for (f in fields) {
                var field = GM_config.frame.contentDocument.getElementById('field_'+f), kids=fields[f].kids;

                if ( typewhite.test(field.type) ) {
                    type = field.type;
                } else {
                    type = field.tagName.toLowerCase();
                }
                GM_config.doSave(f, field, type);

                if (kids) for (var kid in kids) {
                    var field = GM_config.frame.contentDocument.getElementById('field_'+kid);
                    if ( typewhite.test(field.type) ) {
                        type=field.type;
                    } else {
                        type=field.tagName.toLowerCase();
                    }
                    GM_config.doSave(kid, field, type, f);
                }
            }
                    if (GM_config.onSave) GM_config.onSave(); // Call the save() callback function
                    GM_config.save();
        }

        if (GM_config.frame) {
            GM_config.remove(GM_config.frame);
        }
        delete GM_config.frame;
        if (GM_config.onClose) {
            GM_config.onClose(); //  Call the close() callback function
        }
    },

    // -------------------------------------------------------------------------------------------------------------------

    set: function (name,val) {
        GM_config.values[name] = val;
    },

    // -------------------------------------------------------------------------------------------------------------------

    get: function (name) {
        return GM_config.values[name];
    },

    // -------------------------------------------------------------------------------------------------------------------

    isGM: (typeof window.opera === "undefined" && typeof window.chrome === "undefined" && typeof GM_info === "object" && typeof GM_registerMenuCommand === "function"),

    // -------------------------------------------------------------------------------------------------------------------

    log: function (str) {
        if (this.isGM) return GM_log(str);
            else if (window.opera) return window.opera.postError(str);
            else return console.log(str);
    },

    // -------------------------------------------------------------------------------------------------------------------

    getValue : function (name, d) {
        var r, def = (typeof d !== "undefined" ? d : "");
        switch (this.isGM === true) {
            case true: r = GM_getValue(name, def); break;
            case false: r = localStorage.getItem(name) || def; break;
        }
        return r;
    },

    // -------------------------------------------------------------------------------------------------------------------

    setValue : function (name, value) {
        switch (this.isGM === true) {
            case true: GM_setValue(name, value); break;
            case false: localStorage.setItem(name, value); break;
        }
    },

    // -------------------------------------------------------------------------------------------------------------------

    deleteValue : function (name) {
        switch (this.isGM === true) {
            case true: GM_deleteValue(name); break;
            case false: localStorage.removeItem(name); break;
        }
    },

    // -------------------------------------------------------------------------------------------------------------------

    save: function (store, obj) {
        try {
            var val = JSON.stringify(obj || GM_config.values);
            GM_config.setValue((store||GM_config.storage),val);
        } catch(e) {
            GM_config.log("GM_config failed to save settings!\n" + e);
        }
    },

    // -------------------------------------------------------------------------------------------------------------------

    read: function (store) {
        var val = GM_config.getValue((store || GM_config.storage), '{}');
        switch (typeof val) {
            case "string": var rval = JSON.parse(val); break;
            case "object": var rval = val; break;
            default: var rval = {};
        }
        return rval;
    },

    // -------------------------------------------------------------------------------------------------------------------

    reset: function (e) {
        e.preventDefault();
        var type, obj = GM_config, fields = obj.settings;
        for (f in fields) {
            var field = obj.frame.contentDocument.getElementById('field_'+f), kids=fields[f].kids;
            if (field.type=='radio'||field.type=='text'||field.type=='checkbox') type=field.type;
            else type=field.tagName.toLowerCase();
            GM_config.doReset(field, type, null, f, null, false);
            if (kids) for (var kid in kids) {
                var field = GM_config.frame.contentDocument.getElementById('field_'+kid);
                if (field.type=='radio'||field.type=='text'||field.type=='checkbox') type=field.type;
                    else type=field.tagName.toLowerCase();
                GM_config.doReset(field, type, f, kid, true);
            }
        }
    },

    // -------------------------------------------------------------------------------------------------------------------

    addToFrame : function (field, i, k) {
        var elem, obj = this,
            anch = this.frame,
            value = obj.values[i],
            Options = field.options,
            label = field.label,
            create = obj.create,
            isKid = (k !== null && k === true),
            kid, kids;
            switch (field.type) {
                    case 'textarea':
                        elem = create(isKid ? "span" : "div", {title:field.title||'', kids:new Array(
                            create('span', {textContent:label, className:'field_label'}),
                            create('textarea', {id:'field_'+i,innerHTML:value, cols:(field.cols?field.cols:20), rows:(field.rows?field.rows:2)})
                        ), className: 'config_var'});
                        break;
                    case 'radio':
                        var boxes = new Array();
                        for (var j = 0,len = Options.length; j<len; j++) {
                            boxes.push(create('span', {textContent:Options[j]}));
                            boxes.push(create('input', {value:Options[j], type:'radio', name:i, checked:Options[j]==value?true:false}));
                        }
                        elem = create(isKid ? "span" : "div", {title:field.title||'', kids:new Array(
                            create('span', {textContent:label, className:'field_label'}),
                            create('span', {id:'field_'+i, kids:boxes})
                        ), className: 'config_var'});
                        break;
                    case 'select':
                        var options = [], j;
                        if (Object.prototype.toString.call(Options) === '[object Array]') {
                            for (j in Options) {
                                options.push( create('option', {textContent : Options[j], value : j, selected: (j === value)}) );
                            }
                        } else {
                            options.push( create("option", {textContent : 'Error - "options" needs to be a JSON object.', value : 'error', selected : 'selected'}));
                        }
                        elem = create(isKid ? "span" : "div", {title:field.title||'', kids:new Array(
                            create('span', {textContent:label, className:'field_label'}),
                            create('select',{id:'field_'+i, kids:options})
                        ), className: 'config_var'});
                        break;
                    case 'checkbox':
                        elem = create(isKid ? "span" : "div", {title:field.title||'', kids:new Array(
                            create('label', {textContent:label, className:'field_label', "for":'field_'+i}),
                            create('input', {id:'field_'+i, type:'checkbox', value:value, checked:value})
                        ), className: 'config_var'});
                        break;
                    case 'button':
                    var tmp;
                        elem = create(isKid ? "span" : "div", {kids:new Array(
                            (tmp=create('input', {id:'field_'+i, type:'button', value:label, size:(field.size?field.size:25), title:field.title||''}))
                        ), className: 'config_var'});
                        if (field.script) obj.addEvent(tmp, 'click', field.script);
                        break;
                    case 'hidden':
                        elem = create(isKid ? "span" : "div", {title:field.title||'', kids:new Array(
                            create('input', {id:'field_'+i, type:'hidden', value:value})
                        ), className: 'config_var'});
                        break;
                    case 'password':
                        elem = create(isKid ? "span" : "div", {title:field.title||'', kids:new Array(
                            create('span', {textContent:label, className:'field_label'}),
                            create('input', {id:'field_'+i, type:'password', value:value, size:(field.size?field.size:25)})
                        ), className: 'config_var'});
                        break;
                    case 'slider':
                        elem = create(isKid ? "span" : "div", {title:field.title||'', kids:new Array(
                            create('span', {textContent:label, className:'field_label'}),
                            create('input', {id:'field_'+i, type:'text', value:value, size:(field.size?field.size:25)})
                        ), className: 'config_var'});
                        break;
                    default:
                        elem = create(isKid ? "span" : "div", {title:field.title||'', kids:new Array(
                            create('span', {textContent:label, className:'field_label'}),
                            create('input', {id:'field_'+i, type:'text', value:value, size:(field.size?field.size:25)})
                        ), className: 'config_var'});
                        break;
                }
        if (field.kids) {
            kids = field.kids;
            for (kid in kids) elem.appendChild(obj.addToFrame(kids[kid], kid, true));
        }
        return elem;
    },

    // -------------------------------------------------------------------------------------------------------------------

    doSave : function (f, field, type, oldf) {
        var isNum=/^[\d\.]+$/, set = oldf ? GM_config.settings[oldf]["kids"] : GM_config.settings;
        switch (type) {
            case 'text':
                GM_config.values[f] = ((set[f].type=='text') ? field.value : ((isNum.test(field.value) && ",int,float".indexOf(","+set[f].type)!=-1) ? parseFloat(field.value) : false));
                if (set[f]===false) {
                    alert('Invalid type for field: '+f+'\nPlease use type: '+set[f].type);
                    return;
                }
                break;
            case 'hidden': case 'password':
                GM_config.values[f] = field.value.toString();
                break;
            case 'textarea':
                GM_config.values[f] = field.value;
                break;
            case 'checkbox':
                GM_config.values[f] = field.checked;
                break;
            case 'select':
                GM_config.values[f] = field.options[field.selectedIndex].value;
                break;
            case 'span':
                var radios = field.getElementsByTagName('input');
                if (radios.length>0) for (var i=radios.length-1; i>=0; i--) {
                    if (radios[i].checked) GM_config.values[f] = radios[i].value;
                }
                break;
        }
    },

    // -------------------------------------------------------------------------------------------------------------------

    doSettingValue : function (settings, stored, i, oldi, k) {
        var set = k!=null && k==true && oldi!=null ? settings[oldi]["kids"][i] : settings[i];
            if (",save,open,close".indexOf(","+i) == -1) {
                // The code below translates to:
                // if a setting was passed to init but wasn't stored then 
                //      if a default value wasn't passed through init() then use null
                //      else use the default value passed through init()
                //         else use the stored value
                try {
                    var value = (stored[i]==undefined ? (set["default"]==undefined ? null : set["default"]) : stored[i]);
                } catch(e) {
                    var value = (stored[i]=="undefined" ? (set["default"]=="undefined" ? null : set["default"]) : stored[i]);
                }
                
                // If the value isn't stored and no default was passed through init()
                // try to predict a default value based on the type
                if (value === null) {
                    switch (set["type"]) {
                        case 'radio': case 'select':
                            value = set.options[0]; break;
                        case 'checkbox':
                            value = false; break;
                        case 'int': case 'float':
                            value = 0; break;
                        default:
                        value = (typeof stored[i]=="function") ? stored[i] : "";
                    }
                }
            
            }

        GM_config.passed_values[i] = value;
    },

    // -------------------------------------------------------------------------------------------------------------------

    doReset : function (field, type, oldf, f, k) {
        var isKid = k!=null && k==true, obj=GM_config,
         set = isKid ? obj.settings[oldf]["kids"][f] : obj.settings[f];

        switch (type) {
            case 'text':
                field.value = set['default'] || '';
                break;
            case 'hidden': case 'password':
                field.value = set['default'] || '';
                break;
            case 'textarea':
                field.value = set['default'] || '';
                break;
            case 'checkbox':
                field.checked = set['default'] || false;
                break;
            case 'select':
                if (set['default']) {
                    for (var i=field.options.length-1; i>=0; i--)
                    if (field.options[i].value==set['default']) field.selectedIndex=i;
                }
                else field.selectedIndex=0;
                break;
            case 'span':
                var radios = field.getElementsByTagName('input');
                if (radios.length>0) for (var i=radios.length-1; i>=0; i--) {
                    if (radios[i].value==set['default']) {
                        radios[i].checked=true;
                    }
                }
                break;
        }
    },

    // -------------------------------------------------------------------------------------------------------------------

    values : {},
    settings : {},

    // -------------------------------------------------------------------------------------------------------------------

    css: {
        basic : 'body {background:#FFFFFF;}\n' +
        '.indent40 {margin-left:40%;}\n' +
        '* {font-family: arial, tahoma, sans-serif, myriad pro;}\n' +
        '.field_label {font-weight:bold; font-size:12px; margin-right:6px;}\n' +
        '.block {display:block;}\n' +
        '.saveclose_buttons {\n' +
        'margin:16px 10px 10px 10px;\n' +
        'padding:2px 12px 2px 12px;\n' +
        '}\n' +
        '.reset, #buttons_holder, .reset a {text-align:right; color:#000000;}\n' +
        '.config_header {font-size:20pt; margin:0;}\n' +
        '.config_desc, .section_desc, .reset {font-size:9pt;}\n' +
        '.center {text-align:center;}\n' +
        '.section_header_holder {margin-top:8px;}\n' +
        '.config_var {margin:0 0 4px 0; display:block;}\n' +
        '.config_var {font-size: 13px !important;}\n' +
        '.section_header {font-size:13pt; background:#414141; color:#FFFFFF; border:1px solid #000000; margin:0;}\n' +
        '.section_desc {font-size:9pt; background:#EFEFEF; color:#575757; border:1px solid #CCCCCC; margin:0 0 6px 0;}\n' +
        'input[type="radio"] {margin-right:8px;}',
        stylish : ''
    },

    // -------------------------------------------------------------------------------------------------------------------

    create: function (a,b) {
        var ret=window.document.createElement(a);
        if (b) for (var prop in b) {
            if (prop.indexOf('on')==0) ret.addEventListener(prop.substring(2),b[prop],false);
            else if (prop=="kids" && (prop=b[prop])) for (var i=0; i<prop.length; i++) ret.appendChild(prop[i]);
            else if (",style,accesskey,id,name,src,href,for".indexOf(","+prop.toLowerCase())!=-1) ret.setAttribute(prop, b[prop]);
            else ret[prop]=b[prop];
        }
        return ret;
    },

    // -------------------------------------------------------------------------------------------------------------------

    center: function () {
        var node = GM_config.frame, style = node.style, beforeOpacity = style.opacity;
        if (style.display=='none') style.opacity='0';
        style.display = '';
        style.top = Math.floor((window.innerHeight/2)-(node.offsetHeight/2)) + 'px';
        style.left = Math.floor((window.innerWidth/2)-(node.offsetWidth/2)) + 'px';
        style.opacity = '1';
    },

    // -------------------------------------------------------------------------------------------------------------------

    run: function () {
        var script = GM_config.getAttribute('script'), func;
        if (script && typeof script === 'string' && script !== '') {
          func = new Function(script);
          window.setTimeout(func, 0);
        }
    },

    // -------------------------------------------------------------------------------------------------------------------

    addEvent: function (el, ev, scr) {
        el.addEventListener(ev, function () {
            typeof scr === 'function' ? window.setTimeout(scr, 0) : eval(scr)
        }, false);
    },

    // -------------------------------------------------------------------------------------------------------------------

    remove: function (el) {
        if (el && el.parentNode) el.parentNode.removeChild(el);
    },

    // -------------------------------------------------------------------------------------------------------------------

    toggle : function (e) {
        var node=GM_config.frame.contentDocument.getElementById(e);
        node.style.display=(node.style.display!='none')?'none':'';
        GM_config.setValue(e, node.style.display);
    }
};


//* EXAMPLE CODE BELOW --------------------------

GM_config.init("Test", {
    "one" : {
        "label" : "Option One",
        "type" : "checkbox",
        "default" : false
    },
    "two" : {
        "label" : "Option Two",
        "type" : "checkbox",
        "default" : false
    },
    "three" : {
        "label" : "Option Three",
        "type" : "password"
    },
     "four" : {
        "label" : "Option Four",
        "type" : "select",
        "options" : {
            "one" : "One",
            "two" : "Two"
        }
    }
});

GM_config.open();

//------------------------------------------------- */