source: Dev/trunk/js/sequencerScripts.js @ 184

Last change on this file since 184 was 184, checked in by fpvanagthoven, 13 years ago
File size: 26.8 KB
Line 
1/*
2 * You can change the type of sequencer by including a hidden input field with id "contentTypeField" in your document.
3 * The initEditor() method will then adjust internal variables in the sequencer object to match that type of content.
4 */
5
6var sequencer = createVarArray();
7
8function SubmitToolbox(type) {
9    if (sequencer.state.updating == true) return;
10    sequencer.state.updating = true;
11    deselectStep();   
12    var c = "objectToCreate="+type;
13    var u = "createObject.php";
14       
15    newAjaxRequest(c, u, function(result) {
16        sequencer.session.pipeline.uids.push(removeNL(result.responseText));
17        sequencer.session.pipeline.types.push(type);
18        sequencer.session.pipeline.upToDate.push(false);
19        updateSequencer();
20    }, true);
21}
22
23function clickStep(uid) {
24    if (uid == sequencer.state.selectedStep.uid) {  // user deselected a currently selected step.
25        deselectStep();
26    }
27    else {
28        if (sequencer.state.selectedStep.uid != undefined && sequencer.state.selectedStep.uid != "") {
29            deselectStep();
30            selectStep(uid);
31        }
32        else {
33            selectStep(uid);
34        }       
35    }
36}
37
38function selectStep(uid) {
39    var element = document.getElementById(uid);
40    if (element) {
41        addClass(element, "selected");
42        var type = sequencer.session.pipeline.types[sequencer.session.pipeline.uids.indexOf(uid)];
43        ajaxInfoRequest(uid, document.getElementById("infoPanelContent"), type);
44        sequencer.state.selectedStep.uid = uid;
45        sequencer.state.selectedStep.index = null;  //Don't know how to do this yet.
46    }
47}
48
49function deleteStep() {
50    // check if a step is selected
51    if (sequencer.state.selectedStep.uid == null) {
52        return;
53    }
54    var uid = sequencer.state.selectedStep.uid;
55    // deselect the step to reset the info panel and selection code
56    deselectStep();
57    // splice the step's data from the pipeline
58    var index = sequencer.session.pipeline.uids.indexOf(uid);
59    if (index >= 0 && index < sequencer.session.pipeline.uids.length) {
60        sequencer.session.pipeline.uids.splice(index, 1);
61        sequencer.session.pipeline.types.splice(index, 1);
62        sequencer.session.pipeline.upToDate.splice(index, 1);
63        // Then delete the step visually
64        var element = document.getElementById(uid);
65        var divider;
66        if (!element.nextSibling) {
67            // the element is at the end of the pipeline
68            // therefore we remove the previous divider.
69            // Note: it could also be the only element left in the pipeline!
70            divider = (element.previousSibling) ? element.previousSibling : false;
71            if (divider != false) {
72                divider.parentNode.removeChild(divider);
73            }
74        }
75        else {
76            // the element is at any position except the last, therefore we remove the next divider
77            divider = (element.nextSibling) ? element.nextSibling : false;
78            if (divider != false) {
79                divider.parentNode.removeChild(divider);
80            }
81        }
82       
83        // Finally, remove the element itself.
84        element.parentNode.removeChild(element);
85        sequencer.state.numSteps--;
86       
87    }
88}
89
90function deselectStep() {
91    if (!sequencer.state.selectedStep.uid) return;
92    var element = document.getElementById(sequencer.state.selectedStep.uid);
93    removeClass(element, "selected");
94    sequencer.state.selectedStep.uid = null;
95    sequencer.state.selectedStep.index = null;
96    var infoPanel = document.getElementById("infoPanelContent");
97    while (infoPanel.firstChild) infoPanel.removeChild(infoPanel.firstChild);
98}
99
100function ajaxInfoRequest(uid, el, type) {
101    var c = "uid="+uid;
102    c += "&type="+type;
103    var u = "getInfo.php";
104    newAjaxRequest(c, u, function(result) {
105        el.innerHTML = result.responseText;
106    }, true);
107}
108
109function updateSequencer() {
110
111    /*
112     * Description:
113     * This function updates the visual elements in the sequencer content view to match the current state of the sequencer.session.pipeline property.
114     * It queries the database for object properties via AJAX (returnStep/Display/.php), then inserts divider div's in between where needed.
115     */
116    var content = document.getElementById("seqContentWrapper");
117    var requestString, needsUpdating;
118    var args;
119    if (sequencer.state.loaded == false || sequencer.settings.efficientUpdating == false) {  // This is the first update of the sequencer since page load, therefore it contains no previous steps
120        // Stop running this function if the pipeline does not contain any elements to draw
121        if (sequencer.session.pipeline.uids.length == 0 || !sequencer.session.pipeline.uids.length) return;
122        // First clear the entire content wrapper, just for safety and in case efficientUpdating is off
123        while (content.firstChild) {
124            content.removeChild(content.firstChild);
125        }
126        args = [];
127        needsUpdating = [];
128        for (var i = 0; i < sequencer.session.pipeline.uids.length; i++) {
129            args.push({
130                uid: sequencer.session.pipeline.uids[i],
131                type: sequencer.session.pipeline.types[i]
132            });
133            needsUpdating.push(new Array(i, sequencer.session.pipeline.uids[i], sequencer.session.pipeline.types[i]));
134        }
135       
136        requestString = "args="+JSON.stringify(args);
137        newAjaxRequest(requestString, "returnObjectDisplay.php", function(result){
138            content.removeChild(loadingGif);
139            insertNewObjects(result.responseText, needsUpdating);
140        }, true);
141        sequencer.state.loaded = true;
142        var loadingGif = document.createElement("div");
143        loadingGif.innerHTML = "<img src='images/ui/ajax-loader-round.gif' style='float: left; margin:auto auto;' />";
144        content.appendChild(loadingGif);
145    }
146    else {
147        // This means that one or more steps are being added, not an entire pipeline's worth of them
148        needsUpdating = new Array();
149        args = [];
150        // Add steps that need updating to the needsUpdating array (index, uid, type).
151        for (var i = 0; i < sequencer.session.pipeline.uids.length; i++) {
152            if (sequencer.session.pipeline.upToDate[i] == true) continue;
153            needsUpdating.push(new Array(i, sequencer.session.pipeline.uids[i], sequencer.session.pipeline.types[i]));
154            args.push({
155                uid: sequencer.session.pipeline.uids[i],
156                type: sequencer.session.pipeline.types[i]
157            });       
158        }
159       
160        requestString = "args="+JSON.stringify(args);
161        newAjaxRequest(requestString, "returnObjectDisplay.php", function(result){
162            insertNewObjects(result.responseText, needsUpdating);
163        }, true);
164       
165        // Optional bit with the loading GIF
166        for (var i = 0; i < needsUpdating.length; i++) {
167            var loadingDiv = document.createElement("div");
168            loadingDiv.className = "displayStep loading";
169            loadingDiv.innerHTML = "<img src='images/ui/ajax-loader-round.gif' />";
170            if (needsUpdating[i][0] > sequencer.state.numSteps-1) {
171                content.appendChild(loadingDiv);
172                sequencer.state.numSteps++;
173            }
174            else {
175                content.replaceChild(loadingDiv, content.childNodes[i][0]*2);
176            }
177        }
178        updateDividers();
179    // End optional
180    }
181    console.log(sequencer);
182    sequencer.state.updating = false;   // Re-enable new actions
183}
184
185function loadSequencer() {
186    /*
187     * Description:
188     * Load data from hidden fields (put there by PHP), store them in the global var "sequencer" (as well as several initialization properties),
189     * then remove the hidden fields from the HTML document tree.
190     */
191   
192    // Load hidden fields and set required properties in global object var.
193   
194    try {
195        // settings fields first
196        initEditor();
197       
198        // Content-related fields next
199        var fPipelineString = document.getElementById("pipelineStringField");
200        var fPipelineTypes = document.getElementById("pipelineTypeField");
201        var fSessionUid = document.getElementById("sessionField");
202        var fSessionTitle = document.getElementById("sessionTitleField");
203        debugger;
204        sequencer.session.title = fSessionTitle.value;
205        sequencer.session.uid = fSessionUid.value;
206        sequencer.session.pipeline.uids = stringToArray(fPipelineString.value, ",");
207        sequencer.session.pipeline.types = stringToArray(fPipelineTypes.value, ",");
208        sequencer.session.pipeline.upToDate = new Array();
209       
210        for (var i = 0; i < sequencer.session.pipeline.uids.length; i++) {
211            sequencer.session.pipeline.upToDate.push(true);
212        }
213        sequencer.state.numSteps = 0;
214        sequencer.state.loaded = false;
215        sequencer.settings.efficientUpdating = true;
216    }
217    catch (e) {
218        // Alert developer of any errors while setting these variables
219        for (error in e) alert(error.message);
220    }
221   
222    // Then remove the hidden fields from the HTML document
223    // First reset the values to prevent them from being cached.
224    fPipelineString.value = "blaat";
225    fPipelineTypes.value = "blaat2";
226    fSessionUid.value = "blaat3";
227    fSessionTitle.value = "blaat4";
228   
229    var hiddenInputs = document.getElementById("hiddenInputs");
230    hiddenInputs.parentNode.removeChild(hiddenInputs);
231   
232    // finally, run updateSequencer to refresh the visual display of the pipeline
233   
234    updateSequencer();
235}
236
237function insertNewObjects(responseText, needsUpdating) {
238    var response = JSON.parse(responseText);
239    // For now I assume that only one type of element can be displayed in the editor at one time. Therefore, the type of response[0] is the type of all elements of response.
240    switch (sequencer.settings.content.contentType.toLowerCase()) {
241        case "session":
242            insertNewSteps(response, needsUpdating);
243            break;
244        case "question":
245            insertNewQuestions(response, needsUpdating);
246            break;
247        default:
248            break;
249               
250    }
251}
252
253function insertNewSteps(response, needsUpdating) {
254    /*
255     * This is a test function displaying how to handle the visual object representation in solely javascript.
256     * Communication of relevant variables between PHP and JS happens in JSON format.
257     * PHP returns a JSON array of objects to be created by JS
258     * JS then loops through this array and creates DIVS to be inserted into the sequencer.
259     * These are inserted at the position needsUpdating gives us.
260     */
261    var content = document.getElementById("seqContentWrapper");
262    // Remove optional loading images
263    for (var  i = 0; i < content.childNodes.length; i++) {
264        if (hasClass(content.childNodes[i], "loading")) {
265            content.removeChild(content.childNodes[i]);
266        }
267    }
268    // End optional
269   
270    for (var i = 0; i < response.length; i++) {
271        var tempDiv = document.createElement("div");
272        tempDiv.id = response[i].uid;
273        tempDiv.className = "displayStep";
274        var divImageContainer = document.createElement("div");
275        divImageContainer.className = "displayStepIcon";
276        divImageContainer.addEventListener("click", function(){
277            clickStep(this.parentNode.id);
278        }, false);
279        var divImage = document.createElement("img");
280        divImage.src = "images/icons/"+response[i].type.toLowerCase()+".png";
281        divImageContainer.appendChild(divImage);
282        tempDiv.appendChild(divImageContainer);
283        var divLabel = document.createElement("p");
284        divLabel.innerHTML = response[i].title;
285        tempDiv.appendChild(divLabel);
286       
287        // This for needs to loop backwards so that the steps at the end of the pipeline are added or changed first. This keeps the childNodes index for all further steps intact.
288        for (var j = needsUpdating.length-1; j >= 0; j--) {
289            if (needsUpdating[j][1] != response[i].uid) continue;
290            if (needsUpdating[j][0] > sequencer.state.numSteps-1) {
291                content.appendChild(tempDiv);
292            }
293            else {
294                content.replaceChild(tempDiv, content.childNodes[j][0]*2);
295            }
296            sequencer.session.pipeline.upToDate[needsUpdating[j][0]] = true;
297        }
298    }
299   
300    updateDividers();
301}
302
303function insertNewQuestions(response, needsUpdating) {
304    var content = document.getElementById("seqContentWrapper");
305    // Loop through returned question objects
306    for (var i = 0; i < response.length; i++) {
307        // Define the outer frame
308        var frameDiv = document.createElement("div");
309        frameDiv.className = "smallFrame question";
310        frameDiv.id = response[i].uid;
311        var titleDiv = document.createElement("div");
312        titleDiv.className = "smallTitle";
313        var numberDiv = document.createElement("div");
314        numberDiv.className = "listNumber";
315        numberDiv.innerHTML = i.toString();
316        titleDiv.appendChild(numberDiv);
317        titleDiv.innerHTML += response[i].uid;
318        frameDiv.appendChild(titleDiv);
319       
320        // The frame now has a header bar
321        // On to the content frame
322        // Will use new "ce();" function, shorthand for document.createElement
323       
324        var contentDiv = ce("div");
325        contentDiv.className = "content";
326        var questionBody = ce("p");
327        questionBody.innerHTML = response[i].description;
328        var questionParamsDiv = ce("div");
329        questionParamsDiv.className = "questionParamsView";
330        questionParamsDiv.innerHTML = "Object type: "+response[i].type;
331        contentDiv.appendChild(questionBody);
332        contentDiv.appendChild(questionParamsDiv);
333        frameDiv.appendChild(contentDiv);
334       
335        // And finally the controls div
336        var controlsDiv = ce("div");
337        controlsDiv.className = "controls";
338        var editButton = ce("input");
339        var removeButton = ce("input");
340        editButton.value = "Edit";
341        removeButton.value = "Remove";
342        editButton.className = "smallButton";
343        removeButton.className = "smallButton";
344        editButton.addEventListener("click", function(e){
345            alert('Editing not yet supported!');
346        }, false);
347        removeButton.addEventListener("click", function(e){
348            alert('Removing not yet supported!');
349        }, false);
350        controlsDiv.appendChild(editButton);
351        controlsDiv.appendChild(removeButton);
352        frameDiv.appendChild(controlsDiv);
353       
354        // We now have a full question display DIV contained in the frameDiv variable. We should now add this to the sequencer content.
355        for (var j = needsUpdating.length - 1; j >= 0; j--) {
356            if (needsUpdating[j][1] != response[i].uid) continue;
357            if (needsUpdating[j][0] > sequencer.state.numSteps-1) {
358                content.appendChild(frameDiv);
359            }
360            else {
361                content.replaceChild(frameDiv, content.childNodes[needsUpdating[j][0]*2]);
362            }
363            sequencer.state.numSteps++;
364        }
365    }
366}
367
368function updateDividers() {
369    var content = document.getElementById("seqContentWrapper");
370    for (var i = 0; i < content.childNodes.length; i++) {
371        var element = content.childNodes[i];
372        if (!hasClass(element, "displayStep")) {
373            continue;
374        }
375        var lastElement = (element.previousSibling && element.previousSibling.nodeName == "DIV") ? element.previousSibling : false;
376        var nextElement = (element.nextSibling && element.nextSibling.nodeName == "DIV") ? element.nextSibling : false;
377        if (lastElement != false) {
378            if (!hasClass(lastElement, "divider")){
379                var newDivider = document.createElement("div");
380                addClass(newDivider, "divider");
381                addClass(newDivider, sequencer.settings.content.orientation);
382                content.insertBefore(newDivider, element);
383                delete newDivider;
384            }
385        }
386        if (nextElement != false) {
387            if (!hasClass(nextElement, "divider")){
388                var newDivider = document.createElement("div");
389                addClass(newDivider, "divider");
390                addClass(newDivider, sequencer.settings.content.orientation);
391                content.insertBefore(newDivider, nextElement);
392                delete newDivider;
393            }
394        }
395    }
396}
397
398function savePipeline (confirmSave) {
399   
400    var answer;
401    if (confirmSave == true) {
402        answer = confirm("Save changes to pipeline?");
403    }
404    else {
405        answer = true;
406    }
407    if (answer == false) return;
408   
409    var requestString = "uids=";
410    requestString += arrayToString(sequencer.session.pipeline.uids, ",");
411    requestString = requestString.slice(0, requestString.length - 1);   // remove trailing commas
412    requestString += "&types=";
413    requestString += arrayToString(sequencer.session.pipeline.types, ",");
414    requestString = requestString.slice(0, requestString.length - 1);   // remove trailing commas
415    requestString += "&sessionUid=";
416    requestString += sequencer.session.uid;
417    console.log(requestString);
418    newAjaxRequest(requestString, "savesession.php", function(result){
419        console.log(result.responseText);
420    }, true);
421}
422
423function editStep() {
424    // first save
425   
426    //savePipeline(false);
427   
428    var postForm = document.createElement("form");
429    var type = sequencer.session.pipeline.types[sequencer.session.pipeline.uids.indexOf(sequencer.state.selectedStep.uid)];
430    postForm.action = type.toLowerCase()+"Editor.php";
431    postForm.method = "POST";
432    var objectUid = document.createElement("input");
433    objectUid.type = "hidden";
434    objectUid.name = "objectUid";
435    objectUid.value = sequencer.state.selectedStep.uid;
436    postForm.appendChild(objectUid);
437    postForm.submit();
438}
439
440function moveStep (direction) {
441    // Check if a step is selected
442    if (sequencer.state.selectedStep.uid == null || direction == null) return;
443    // Check if the step is not at either end of the pipeline
444    var index = sequencer.session.pipeline.uids.indexOf(sequencer.state.selectedStep.uid);
445    if (index == -1) return;
446    if ((index < 0) || (index >= sequencer.session.pipeline.uids.length) || (index == 0 && direction == -1) || (index == sequencer.session.pipeline.uids.length - 1 && direction == 1)) {
447        alert("Cannot move out of bounds!");
448        return;
449    }
450   
451    // Find the two elements in the editor content display
452    var content = document.getElementById("seqContentWrapper");
453    var element = document.getElementById(sequencer.session.pipeline.uids[index]);
454    var otherElement = document.getElementById(sequencer.session.pipeline.uids[index+direction]);
455    // First buffer the two elements
456    var tempElement = element.cloneNode(true);
457    var tempOtherElement = otherElement.cloneNode(true);
458    var placeHolderElement = document.createElement("div");
459    placeHolderElement.id = "placeholder_element";
460    content.replaceChild(placeHolderElement, otherElement);
461    content.replaceChild(tempOtherElement, element);
462    content.replaceChild(tempElement, placeHolderElement);
463    //This should work.
464    // A-B     Start positions, backup to tA and tB
465    // A-X     Replace B with placeholder X
466    // B-X     Replace A with tB
467    // B-A     Replace placeholder X with tA.
468    // The two elements are now swapped.
469     
470    // Now swap the array entries.
471    sequencer.session.pipeline.uids[index] = sequencer.session.pipeline.uids.splice(index+direction, 1, sequencer.session.pipeline.uids[index])[0];
472    sequencer.session.pipeline.types[index] = sequencer.session.pipeline.types.splice(index+direction, 1, sequencer.session.pipeline.types[index])[0];
473   
474    // Finally, rebind the onCLick events to work properly again. I don't know if this 'clones' the event as well (therefore re-adding them casuses memory bloat...) or not.
475    for (var i = 0; i < tempOtherElement.childNodes.length; i++){
476        var childNode = tempOtherElement.childNodes[i];
477        if (hasClass(childNode, "displayStepIcon")) {
478            childNode.addEventListener("click", function() {
479                clickStep(this.parentNode.id);
480            }, false);
481        }
482    }
483    for (var i = 0; i < tempElement.childNodes.length; i++){
484        var childNode = tempElement.childNodes[i];
485        if (hasClass(childNode, "displayStepIcon")) {
486            childNode.addEventListener("click", function() {
487                clickStep(this.parentNode.id);
488            }, false);
489        }
490    }
491   
492   
493   
494     
495// The alternative is to use event bubbling to capture the event on a higher level.
496// Basically, we bind a super-structure onclick event that uses e.target|| event.srcElement to determine which element to move and select.
497// http://stackoverflow.com/questions/29624/how-to-maintain-correct-javascript-event-after-using-clonenodetrue
498// Pro: clean implementation, less events binded.
499// Con: Difficult, this already works fine, probably tougher to use in conjunction with multifunctionality (sessions, surveys, questionsets, etc in one kind of editor)
500     
501}
502
503function initEditor() {
504    // Reset the editor if the
505   
506    // load settings fields first
507    var fContentType = document.getElementById("sContentTypeField");
508    var content = document.getElementById("seqContent");
509    //sequencer.settings.content.contentType = fContentType.value.toLowerCase();
510    sequencer.settings.content.contentType = fContentType.value;
511    //Then select settings from a few presets
512   
513    switch (sequencer.settings.content.contentType) {
514        case "session":
515            sequencer.settings.content.orientation = "horizontal";
516            sequencer.settings.content.width = 800;
517            sequencer.settings.content.height = 125;
518            content.style.width = sequencer.settings.content.width+"px";
519            content.style.height = sequencer.settings.content.height+"px";
520            addClass(content, "horizontal");
521            break;
522        case "survey":
523            sequencer.settings.content.orientation = "vertical";
524            sequencer.settings.content.width = 800;
525            sequencer.settings.content.height = "auto";
526            content.style.width = sequencer.settings.content.width+"px";
527            content.style.height = sequencer.settings.content.height+"px";
528            addClass(content, "vertical");
529            break;
530        default:
531            break;
532    }
533    fContentType.parentNode.parentNode.removeChild(fContentType.parentNode);
534}
535
536function debug_addQuestion() {
537    // Derp, natuurlijk werkt de addQuestion call niet twee keer. Hij creeert twee keer exact hetzelfde object. Bottom line: het werkt. Nu de editing code voor deze questions gaan schrijven!
538    var response = [{
539        uid: "1234567890testuid",
540        title: "dummyQuestion",
541        description: "This is a dummy question, not a real one!",
542        type: "question"
543    }];
544   
545    var needsUpdating = [[
546    0,
547    "1234567890testuid",
548    "question"
549    ]];
550   
551    insertNewQuestions(response, needsUpdating);
552}
553
554function debug_switchType() {
555    var content = ge("seqContent");
556    if (sequencer.settings.content.contentType == "session") {
557        // Set to survey
558        sequencer.settings.content.contentType = "survey";
559        sequencer.settings.content.orientation = "vertical";
560        removeClass(content, "horizontal");
561        addClass(content, "vertical");
562    }
563    else {
564        // Set to session
565        sequencer.settings.content.contentType = "session";
566        sequencer.settings.content.orientation = "horizontal";
567        removeClass(content, "vertical");
568        addClass(content, "horizontal");
569    }
570}
571
572function createVarArray() {
573    if (sequencer) delete window.sequencer;
574    return {   // GLOBAL VAR TO STORE SEQUENCER SETTINGS!
575        uid: "",                // The unique id of this sequencer (not DB related!). This will help to avoid global var conflicts. Assign this randomly! (STRING)
576        session: {              // Properties of the currently loaded session
577            title: "",              // Title or name (STRING)
578            uid: "",                // Database UID of the current session (STRING)
579            pipeline: {             // Pipeline
580                uids: [],               // Uids of objects in pipeline (STRING)
581                types: [],              // Types of objects in pipeline (STRING)
582                upToDate: []            // Whether or not object displays are up to date (BOOL)
583            }
584        },
585        survey: {               // Properties of the loaded survey, if applicable (either this or the session tree above is filled in!) [THIS IS NOT CURRENTLY USED AND I SHOULD DELIBERATE ON WHAT VARIABLES TO PUT IN THIS TREE TO MAKE A FUNCTIONAL EDITOR THAT ALSO MATCHES THE DB FORMAT!]
586            title: "",              // Title or the name of the survey
587            uid: "",                // Uid of the survey
588            questions: {            // Properties of the questions contained within this survey
589                uids: [],               // An array of uids of the questions, in the order that they appear in the survey
590                upToDate: []            // Whether or not a certain question needs an update
591            }
592        },
593        state: {                // Operating state of the sequencer
594            editing: false,         // Whether or not one of the contained child objects is currently being edited or in edit mode. Which one can be determined from the selectedStep property.
595            updating: false,        // Whether or not new steps are currently being queried (to disable any further actions)
596            numSteps: 0,            // Number of steps currently drawn in the editor (not necessarily same as number of steps in pipeline!) (INTEGER)
597            loaded: false,          // Whether or not the sequencer content has been updated for the first time (BOOL)
598            selectedStep: {         // Properties of the currently selected step
599                uid: "",                // UID of this step (STRING)
600                index: null             // The 'index' of this step in the current sequencer view (NOT the pipeline!) (INTEGER)
601            }       
602        },
603        settings: {             // Various settings to determine the workings of the sequencer
604            content: {              // Properties related to the content view of the sequencer
605                contentType: null,      // Type of the loaded parent object (STRING)
606                width: null,            // Width of the viewing area (INTEGER)
607                height: null,           // Height of the viewing area (INTEGER)
608                maxObjects: null,       // The maximum number of content elements to be displayed at once time (INTEGER)
609                orientation: null       // Whether the editor should be a vertical or horizontal editor (STRING)
610            },
611            efficientUpdating: true // Whether or not to use selective querying of the database for new step objects. True will only refresh out-of-date steps, False will refresh the whole pipeline
612        }
613    };
614}
Note: See TracBrowser for help on using the repository browser.