Case360 script editor

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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>";
        }
    }
};