Greasy Fork is available in English.

Case360 script editor

Try to take over the world and make it a better place!

// ==UserScript==
// @name         Case360 script editor
// @namespace    http://tampermonkey.net/
// @version      0.4
// @author       P. Wasilewski
// @collaborator B. Bergs
// @collaborator J. Elsen
// @description  Try to take over the world and make it a better place!
// @match        */sonora/Admin?op=i
// @supportURL   https://github.com/pwasilewski/Case360-editor
// @updateURL    https://raw.githubusercontent.com/pwasilewski/Case360-editor/master/Case360%20script%20editor.user.js
// @downloadURL  https://raw.githubusercontent.com/pwasilewski/Case360-editor/master/Case360%20script%20editor.user.js
// @resource     functions       https://raw.githubusercontent.com/pwasilewski/Case360-editor/master/functions.json
// @resource     caseMethods     https://raw.githubusercontent.com/pwasilewski/Case360-editor/master/methods.json
// @resource     staticFunctions https://raw.githubusercontent.com/pwasilewski/Case360-editor/master/static_functions.json
// @grant        GM_getResourceText
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js
// @require      https://openuserjs.org/src/libs/sizzle/GM_config.js
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_log
// @grant        GM_registerMenuCommand
// ==/UserScript==

const ACE_CONFIG = '{ "id":"GM_config", "title":"Case360 script editor config", "fields":{ "repository":{ "label":"Ace repository", "type":"text", "default":"ace/" }, "theme":{ "label":"Theme", "type":"select", "default":"piwi", "options":[ "ambiance", "chaos", "chrome", "clouds", "clouds_midnight", "cobalt", "crimson_editor", "dawn", "dracula", "dreamweaver", "eclipse", "github", "gob", "gruvbox", "idle_fingers", "iplastic", "katzenmilch", "kr_theme", "kuroir", "merbivore", "merbivore_soft", "monokai", "mono_industrial", "notepad", "pastel_on_dark", "piwi", "solarized_dark", "solarized_light", "sqlserver", "terminal", "textmate", "tomorrow", "tomorrow_night", "tomorrow_night_blue", "tomorrow_night_bright", "tomorrow_night_eighties", "twilight", "vibrant_ink", "xcode" ] }, "hline":{ "label":"Show horizontal line", "type":"checkbox", "default":true }, "showSnippets":{ "label":"Show snippets", "type":"checkbox", "default":true }, "showScriptCompleter":{ "label":"Show scripts completer", "type":"checkbox", "default":true }, "showFunctionCompleter":{ "label":"Show functions completer", "type":"checkbox", "default":true }, "showStaticFunctionCompleter":{ "label":"Show static functions completer", "type":"checkbox", "default":true }, "showMethodCompleter":{ "label":"Show methods completer", "type":"checkbox", "default":true } } }';

GM_config.init($.parseJSON (ACE_CONFIG));

GM_registerMenuCommand("Configure Case360 script editor", function(){
  GM_config.open();
});

const CASE_EDITOR_ID          = "Script";
const CASE_EDITOR_SELECTOR    = "#" + CASE_EDITOR_ID;
const URL_AJAX_COMPILE_SCRIPT = "CaseAjax?method=ScriptsHelper.compileScript";

const ACE_REPOSITORY          = GM_config.get("repository");
const ACE_CDN_URL             = ACE_REPOSITORY + "ace.js";
const ACE_LANG_CDN_URL        = ACE_REPOSITORY + "ext-language_tools.js";
const ACE_EDITOR_ID           = "editor";
const ACE_EDITOR_SELECTOR     = "#" + ACE_EDITOR_ID;
const ACE_MODE                = "ace/mode/case";
const ACE_THEME               = "ace/theme/" + GM_config.get("theme");

var scriptName = "";
var editor;
var ace_loaded = false;
var autocomplete_loaded = false;

function GM_compileScript() {
    var ScriptSource = encodeURIComponent(removeCR(editor.getSession().getValue()));
    var nodeId = document.getElementById("scriptNodeId");
    if (nodeId !== null) {
        var requestUrl = URL_AJAX_COMPILE_SCRIPT;
        var params = "nodeID=" + encodeURIComponent(nodeId.value) + "&scriptSource=" + ScriptSource;
        getASyncAjaxResponse(requestUrl, params, GM_getCompileResults);
    }
}

function GM_getCompileResults(objRequestHttp) {
    if (objRequestHttp.readyState == 4 || objRequestHttp.readyState == "complete") {
        if (objRequestHttp.status == 200) {
            var responseXML = objRequestHttp.responseXML;
            GM_showCompileResults(responseXML);
        }
    }
}

function GM_showCompileResults(responseXML) {
    if (responseXML !== null) {
        var errorMsg = responseXML.getElementsByTagName("errormsg");
        var errorNode = errorMsg.item(0).firstChild;
        if (errorNode !== null) {
            var offsetPos = errorNode.nodeValue.lastIndexOf("near offset ");
            var offset = 0;
            var line = 0;
            var linePos = errorNode.nodeValue.lastIndexOf(" in line ");
            if (linePos == -1) {
                if (offsetPos != -1) {
                    offset = parseInt(errorNode.nodeValue.substring(offsetPos + 12));
                }
                line = 1;
            } else {
                if (offsetPos != -1) {
                    offset = parseInt(errorNode.nodeValue.substring(offsetPos + 12, linePos));
                }
                line = parseInt(errorNode.nodeValue.substring(linePos + 9));
            }

            editor.getSession().setAnnotations([{
                row: line-1,
                column: offset,
                text: errorNode.nodeValue,
                type: "error"
            }]);
        } else {
            editor.getSession().clearAnnotations();
        }
    }
}

function GM_wait() {
    if(!ace_loaded)
    { window.setTimeout(GM_wait,100); }
    else {
        if(!autocomplete_loaded) {
            GM_appendAutoCompleteScript();

            if(!autocomplete_loaded)
            { window.setTimeout(GM_wait,100); }

        } else {
            GM_initializeEditor();
        }
    }
}

function GM_appendAceScript() {
    if($("script[src$='ace.js']").length === 0) {
        var ace      = document.createElement('script');
            ace.src  = ACE_CDN_URL;
            ace.type = 'text/javascript';
            ace.onload = function() {
                ace_loaded = true;
            };

        document.getElementsByTagName('head')[0].appendChild(ace);
    }
}

function GM_appendAutoCompleteScript() {
    if($("script[src$='ext-language_tools.js']").length === 0) {
        var ace_lang      = document.createElement('script');
            ace_lang.src  = ACE_LANG_CDN_URL;
            ace_lang.type = 'text/javascript';
            ace_lang.onload = function() {
                autocomplete_loaded = true;
            };

        document.getElementsByTagName('head')[0].appendChild(ace_lang);
    }
}

function GM_initializeEditor() {
    var GM_EDITOR       = document.createElement('div');
        GM_EDITOR.id    = ACE_EDITOR_ID;
    $(GM_EDITOR).insertBefore(CASE_EDITOR_SELECTOR);

    var langTools = ace.require("ace/ext/language_tools");
    editor = ace.edit(ACE_EDITOR_ID);
    editor.getSession().setMode(ACE_MODE);
    editor.setTheme(ACE_THEME);

    editor.setOptions({
        enableBasicAutocompletion: true,
        enableSnippets : GM_config.get("showSnippets"),
        showPrintMargin : GM_config.get("hline")
    });

    editor.commands.addCommand({
        name: "replace",
        bindKey: {win: "Ctrl-S", mac: "Command-S"},
        exec: function(editor) {
            updateSource(false);
        }
    });

    var textarea = $('textarea[id="' + CASE_EDITOR_ID + '"]');
    $(GM_EDITOR).width("100%");
    $(GM_EDITOR).height($("#rightpane").height()-58);
    textarea.hide();

    editor.getSession().setValue(textarea.val());
    editor.getSession().on('change', function(e){
        textarea.val(editor.getSession().getValue());
        scriptChanges();
    });

    editor.getSession().selection.on('changeCursor', function(e, o) {
        compileScript();
        GM_compileScript();
    });

    if(GM_config.get("showScriptCompleter")){
        langTools.addCompleter(scriptsCompleter);
    }
    if(GM_config.get("showFunctionCompleter")){
        langTools.addCompleter(functionsCompleter);
    }
    if(GM_config.get("showMethodCompleter")){
        langTools.addCompleter(methodsCompleter);
    }
    if(GM_config.get("showStaticFunctionCompleter")){
        langTools.addCompleter(staticFunctionsCompleter);
    }
}

$('#rightpane').bind('DOMSubtreeModified', function() {
    var script = $('#scriptlocation').val();
    if($(CASE_EDITOR_SELECTOR).length !== 0 && scriptName != script) {

        scriptName = script;

        GM_appendAceScript();

        GM_wait();
    }
});

// ### UTIL FUNCTIONS ###

/**
 * Function to format the list of params and remove useless extension.
 *
 * E.g.: TST.updateActor(Object.PropertyManager.RepositoryObject.FormData actorFormdata, Object.PropertyManager.FmsRow updatedRow)
 *
 * @param {String} methodName The method name (e.g.: TST.updateActor)
 * @param {String} argumentsString The list of arguments (e.g.: Object.PropertyManager.RepositoryObject.FormData actorFormdata, Object.PropertyManager.FmsRow updatedRow)
 *
 * @return {String} The format string. (e.g.: TST.updateActor(FormData actorFormdata, FmsRow updatedRow))
 */
function getSimplifiedMethodFormat(methodName, argumentsString) {
    var result = [];
    var simplified = [];
    var args = argumentsString.split(",");

    $.each( args, function( index, value ) {
        var splitedArg = value.match(/(.+)\s+(.+)/);
        if(splitedArg !== null) {
            simplified.push(splitedArg[1].split('.').pop() + ' ' + splitedArg[2]);
        } else {
            simplified.push(value.trim());
        }
    });

    result.push(methodName, "(", simplified.join(", ") ,")");
    return result.join("");
}

/**
 * Function to format the list of params allowing snippets and remove useless extension.
 *
 * E.g.: TST.updateActor(Object.PropertyManager.RepositoryObject.FormData actorFormdata, Object.PropertyManager.FmsRow updatedRow)
 *
 * @param {String} methodName The method name (e.g.: TST.updateActor)
 * @param {String} argumentsString The list of arguments (e.g.: Object.PropertyManager.RepositoryObject.FormData actorFormdata, Object.PropertyManager.FmsRow updatedRow)
 *
 * @return {String} The format string. (e.g.: TST.updateActor(${1:actorFormdata}, ${2:updatedRow}))
 */
function getSnippetMethodFormat(methodName, argumentsString) {
    var result = [];
    var snippet = [];
    var args = argumentsString.split(",");

    $.each( args, function( index, value ) {
        var splitedArg = value.match(/(.+)\s+(.+)/);
        if(splitedArg !== null) {
            snippet.push('${' + (index+1) + ':' + splitedArg[2] + '}');
        } else {
            snippet.push('${' + (index+1) + ':' + value.trim() + '}');
        }
    });

    result.push(methodName, "(", snippet.join(", ") ,")");
    return result.join("");
}

function buildMethod(prefix, signature) {
    if( ! prefix.endsWith(".") ) {
        prefix += ".";
    }

    return prefix + signature;
}

function getMethodName(fullsignature) {
    return fullsignature.split('(')[0];
}

function getMethodArgs(fullsignature) {
    return fullsignature.match(/\((.*?)\)/)[1];
}

function countNumberDots(text) {
    return (text.match(/\./g)||[]).length;
}

function hasDots(text) {
    return countNumberDots(text) > 0;
}

function getPrefix(text) {
    return text.substring(0, text.lastIndexOf("."));
}

// ### COMPLETERS ###

/**
 * Fill in the completer with scripts like :
 *
 * DAMO.Log.log(...) or DAMO.Util.getEnvironment(...)
 *
 * Firstly, the function will be executed if and only if the prefix has at least 1 dot.
 * Secondly, it will make an ajax call to retrieve a list of script based on the prefix.
 *
 * If both conditions are met, the function will format the output and will add it to the completers.
 * If not, nothing will happen.
 */
var scriptsCompleter = {
    getCompletions: function(editor, session, pos, prefix, callback) {
        if(! hasDots(prefix) ) return;

        prefix = getPrefix(prefix);

        $.ajax({
            type: "GET",
            url: "CaseAjax?getmethods=" + prefix,
            dataType: "xml",
            success: function(xml){
                var completers = [];
                if($(xml).find("responseXML").length == 1) {
                    $(xml).find("class").each(function(){
                        $(this).children().each(function(){
                            var method		= buildMethod(prefix, $(this).text());
                            var methodName 	= getMethodName(method);
                            var args		= getMethodArgs(method);

                            simplifiedMethod = getSimplifiedMethodFormat(methodName, args);
                            snippetMethod    = getSnippetMethodFormat(methodName, args);
                            completers.push({definition : simplifiedMethod, value : simplifiedMethod, snippet: snippetMethod, score : 1000, meta : 'script', type: 'script'});
                        });
                    });
                }

                callback(null, completers); return ;
            }
        });
    },
    getDocTooltip: function(item) {
        if (item.type == 'script' && !item.docHTML) {
            var scriptIcon = "<span style=\"background: red;display: inline-block;border-radius: 50%;height: 13px;width: 13px;font-size: 11px;line-height: 14px;text-align: center;color: white;\"><i>s</i></span>";
            item.docHTML = "<h5 style=\"margin:0px;\">" + scriptIcon + " <i>Scripts</i> - " + item.definition + "</h5>";
        }
    }
};

/**
 * Fill in the completer with functions like :
 *
 * optional(...) or doQuery(...)
 *
 * Firstly, the function will be executed if and only if the prefix doesn't contain any dots.
 *
 * If the condition is met, the function will format the output and will add it to the completers based on the ressource file.
 * If not, nothing will happen.
 */
var functionsCompleter = {
    getCompletions: function(editor, session, pos, prefix, callback) {
        if( hasDots(prefix) ) return;

        var ressourceFile = GM_getResourceText("functions", "json");

        callback(null, JSON.parse(ressourceFile).functions.map(function(func) {
            var method		= func.signature;
            var methodName 	= getMethodName(method);
            var args		= getMethodArgs(method);

            simplifiedMethod = getSimplifiedMethodFormat(methodName, args);
            snippetMethod    = getSnippetMethodFormat(methodName, args);
            return {definition: func.definition, value: simplifiedMethod, snippet: snippetMethod, score: 1000, meta : 'function', type: 'function'};
        }));
    },
    getDocTooltip: function(item) {
        if (item.type == 'function' && !item.docHTML) {
            var functionIcon = "<span style=\"background: #7c7;display: inline-block;border-radius: 50%;height: 13px;width: 13px;font-size: 11px;line-height: 14px;text-align: center;color: white;\"><i>f</i></span>";
            item.docHTML = "<h5 style=\"margin:0px;\">" + functionIcon + " <i>Function</i> - " + item.value + "</h5> <p style=\"margin:0px;word-wrap:break-word;\">" + item.definition + "</p>";
        }
    }
};

/**
 * Fill in the completer with Case360's built-in methods like :
 *
 * prefix.saveChanges(...) or prefix.add(...)
 *
 * Firstly, the function will be executed if and only if the prefix has at least 1 dot.
 * Secondly, it checks if the user is not looking for a case scripts. (e.g.: DAMO.Log.)
 *
 * If both conditions are met, the function will format the output and will add it to the completers based on the ressource file.
 * If not, nothing will happen.
 */
var methodsCompleter = {
    getCompletions: function(editor, session, pos, prefix, callback) {
        if(! hasDots(prefix) ) return;

        prefix = getPrefix(prefix);

        var isScriptSearch = false;
        $.ajax({
            type: "GET",
            url: "CaseAjax?getmethods=" + prefix,
            dataType: "xml",
            async: false,
            success: function(xml){
                isScriptSearch = $(xml).find("responseXML").length == 1;
            }
        });

        if(isScriptSearch) return;

        var ressourceFile = GM_getResourceText("caseMethods", "json");

        callback(null, JSON.parse(ressourceFile).methods.map(function(met) {
            var method		= buildMethod(prefix, met.signature);
            var methodName 	= getMethodName(method);
            var args		= getMethodArgs(method);

            simplifiedMethod = getSimplifiedMethodFormat(methodName, args);
            snippetMethod    = getSnippetMethodFormat(methodName, args);
            return {definition: met.definition, value: simplifiedMethod, snippet: snippetMethod, score: 1000, meta : met.meta, type: 'method'};
        }));
    },
    getDocTooltip: function(item) {
        if (item.type == 'method' && !item.docHTML) {
            var methodIcon = "<span style=\"background: #CC00CC;display: inline-block;border-radius: 50%;height: 13px;width: 13px;font-size: 11px;line-height: 14px;text-align: center;color: white;\"><i>m</i></span>";
            item.docHTML = "<h5 style=\"margin:0px;\">" + methodIcon + " <i>" + item.meta + "</i> - " + item.value + "</h5> <p style=\"margin:0px;word-wrap:break-word;\">" + item.definition + "</p>";
        }
    }
};

/**
 * Fill in the completer with static functions/methods like :
 *
 * WorkItem.find(...) or User.find(...)
 *
 * Firstly, the function will be executed if and only if the prefix has 1 dot.
 * Secondly, it checks if the prefix exists as key in the ressource file.
 *
 * If both conditions are met, the function will format the output and will add it to the completers.
 * If not, nothing will happen.
 */
var staticFunctionsCompleter = {
    getCompletions: function(editor, session, pos, prefix, callback) {
        if( countNumberDots(prefix) !== 1 ) return;

        prefix = getPrefix(prefix);

        var ressourceFile = GM_getResourceText("staticFunctions", "json");
        var jsonString = JSON.parse(ressourceFile);

        if(jsonString.hasOwnProperty(prefix.toLowerCase())) {
            callback(null, jsonString[prefix.toLowerCase()].map(function(func) {
                var method		= buildMethod(prefix, func.signature);
                var methodName 	= getMethodName(method);
                var args		= getMethodArgs(method);

                simplifiedMethod = getSimplifiedMethodFormat(methodName, args);
                snippetMethod    = getSnippetMethodFormat(methodName, args);
                return {definition: func.definition, value: simplifiedMethod, snippet: snippetMethod, score: 2000, meta : func.meta, type: 'staticFunction'};
            }));
        }
    },
    getDocTooltip: function(item) {
        if (item.type == 'staticFunction' && !item.docHTML) {
            var functionIcon = "<span style=\"background: #ffae19;display: inline-block;border-radius: 50%;height: 13px;width: 13px;font-size: 11px;line-height: 14px;text-align: center;color: white;\"><i>f</i></span>";
            item.docHTML = "<h5 style=\"margin:0px;\">" + functionIcon + " <i>" + item.meta + "</i> - " + item.value + "</h5> <p style=\"margin:0px;word-wrap:break-word;\">" + item.definition + "</p>";
        }
    }
};