var questionEditor = new QuestionEditor(); function createNewElement(tag, type, id, cl, value) { var newElement = document.createElement(tag); if (type != undefined) newElement.type = type; if (id != undefined) newElement.id = id; if (cl != undefined) newElement.className = cl; if (value != undefined) { newElement.value = value; newElement.text = value; } return newElement; } function createNewInputLabel(text, target, side) { var newLabel = document.createElement("label"); if (target) newLabel.setAttribute("for",target); newLabel.innerHTML = text; if (side) newLabel.className = side; return newLabel; } function QuestionEditor() { // Properties this.uid = null; // The uid of the question contained in this editor this.saved = false; // Whether or not the question displayed in the editor has been saved already. var me = this; // Retarded self-reference because: Javascript this.element = null; // The parent div element containing the questionEditor this.paramsElement = null; // The parent parameters element where all the input sets will be located this.paramSets = null; // The currently enabled input sets to be displayed in the paramsElement. //Currently only supports a single param set (based on answer type), but functionality will scale to multiple sets as needed. // Methods // Basic functionality this.setValues = function(arguments) { debugger; var question = JSON.parse(arguments)[0]; var qeTypeField = ge("qeTypeField"); ge("qeCodeField").value = question.code; ge("qeBodyTextField").value = question.description; switch (question.type.toLowerCase()) { case "int": // First make sure all the necessary fields are present me.type_Integer(); // Then fill them in using the available data qeTypeField.value = "int"; ge("qeMinValueField").value = question.minValue; ge("qeMaxValueField").value = question.maxValue; break; case "scale": me.type_Scale(); qeTypeField.value = "scale"; ge("qeNumChoicesField").value = question.numChoices; ge("qeLegendsEnabledField").checked = question.legendsEnabled; ge("qeLowerLegendField").value = question.lowerLegend; ge("qeUpperLegendField").value = question.upperLegend; break; case "choice": me.type_Choice(); qeTypeField.value = "choice"; ge("qeNumOptionsFIeld").value = question.numOptions; ge("qeMultipleAnswersField").value = question.multipleAnswers; // then some more to add textboxes (qeParamAnswerGroup) for all the possble choices. // Maybe a central version that appends these groups/textinputs? Maybe not, though. Don't want stuff too abstracted... break; case "text": me.type_Text(); qeTypeField.value = "text"; ge("qeTextMaxLengthField").value = question.maxTextLength; break; } } this.init = function() { // Outer div this.saved = false; me.element = ce("div"); me.element.className = "smallFrame questionEditor"; me.element.id = sequencer.state.selectedObject.uid; me.uid = sequencer.state.selectedObject.uid; // Header var titleDiv = ce("div"); titleDiv.className = "smallTitle"; var numberDiv = ce("div"); numberDiv.className = "listNumber"; numberDiv.innerHTML = "4"; //TODO var nameSpan = ce("span"); nameSpan.id = "qeTitleField"; nameSpan.innerHTML = "New question"; titleDiv.appendChild(numberDiv); titleDiv.innerHTML += "Editing: "; titleDiv.appendChild(nameSpan); me.element.appendChild(titleDiv); //Content area var contentDiv = ce("div"); contentDiv.className = "content"; var bodyText = createNewElement("textarea", null, "qeBodyTextField", "qeBodyTextField", null); bodyText.value = "Question body text goes here"; contentDiv.appendChild(bodyText); // The dynamic questionParams div, where all the control elements and inputs will be located var questionParams = ce("div"); me.paramsElement = questionParams; questionParams.className = "questionParams"; questionParams.id = "qeQuestionParamsDiv"; var basicContainer = ce("div"); basicContainer.id = "basicInputs"; var qeCodeField = createNewElement("input", "text", "qeCodeField", "qeParamField", null); var qeCodeField_lbl = createNewInputLabel("Question code:","qeCodeField", "l"); basicContainer.appendChild(qeCodeField_lbl); basicContainer.appendChild(qeCodeField); var qeTypeField = createNewElement("select", null, "qeTypeField", "qeParamField", null); var qeTypeField_lbl = createNewInputLabel("Answer type:","qeTypeField", "l"); basicContainer.appendChild(qeTypeField_lbl); basicContainer.appendChild(qeTypeField); questionParams.appendChild(basicContainer); qeTypeField.addEventListener("change", function(){ //debugger; me.selectAnswerType(); }, false); // Add the select options. Do this in a block scope to prevent the o1 var from messing things up. // Also helps in structuring code. { var o1 = ce("option"); o1.value = null; o1.text = ""; qeTypeField.appendChild(o1); o1 = ce("option"); o1.value = "int"; o1.text = "Integer"; qeTypeField.appendChild(o1); o1 = ce("option"); o1.value = "scale"; o1.text = "Scale"; qeTypeField.appendChild(o1); o1 = ce("option"); o1.value = "choice"; o1.text = "Multiple choice"; qeTypeField.appendChild(o1); o1 = ce("option"); o1.value = "text"; o1.text = "Text"; qeTypeField.appendChild(o1); } contentDiv.appendChild(questionParams); me.element.appendChild(contentDiv); // Controls bar var controlsDiv = ce("div"); controlsDiv.className = "controls"; var btnDiscard = createNewElement("input", "button", "btnDiscard", null, "Discard"); var btnSave = createNewElement("input", "button", "btnSave", null, "Save"); controlsDiv.appendChild(btnDiscard); controlsDiv.appendChild(btnSave); btnSave.addEventListener("click", function(){ me.save(); }, false); btnDiscard.addEventListener("click", function(){ me.discard(); }, false); me.element.appendChild(controlsDiv); me.paramSets = new Array(); me.paramSets.push("basic"); } this.save = function() { var request = { "title": ge("qeTitleField").innerHTML, "code": ge("qeCodeField").value, "description": ge("qeBodyTextField").value } switch (ge("qeTypeField").value) { case "int": request.answerType = "int"; request.minValue = parseInt(ge("qeMinValueField").value); if (request.minValue == "NaN") request.minValue = -1; // Is this the correct way to do this? request.maxValue = parseInt(ge("qeMaxValueField").value); if (request.maxValue == "NaN") request.maxValue = -1; // Include error checking and form validation!! break; case "scale": request.answerType = "scale"; request.numChoices = parseInt(ge("qeNumChoicesField").value); request.legendsEnabled = ge("qeLegendsEnabledField").checked; request.lowerLegend = ge("qeLowerLegendField").value; request.upperLegend = ge("qeUpperLegendField").value; // Include error checking and form validation!! break; case "choice": request.answerType = "choice"; request.multipleAnswers = ge("qeMultipleAnswersField").checked; request.possibleAnswers = array(); var answerFieldset = ge("qeParamsAnswerFieldset"); var count = ge("qeNumOptionsField").value; for (var i = 0; i < count; i++) { var el = ge("qeAnswerField"+i); request.possibleAnswers.push(el.value); } // Include error checking and form validation!! break; case "text": request.answerType = "text"; request.maxTextLength = ge("qeTextMaxLength").value; // Include error checking and form validation!! break; } requestString = "args="+JSON.stringify(request); newAjaxRequest(requestString, "setQuestion.php", function(result){ // Then add the returned uid, if existing, to the sequencer.survey.questions array and set it for update debugger; var response = JSON.parse(result.responseText); console.log(response); if (response.created == true) { if (response.uid) { // Created a new question, add it to the sequencer content array and set it for an update sequencer.survey.questions.uids.push(response.uid); sequencer.survey.questions.upToDate.push(false); } else { alert("ERROR!"); } } else { if (response.uid){ // Edited an existing question that was already in the sequencer content. Set it for an update sequencer.survey.questions.upToDate[sequencer.survey.questions.uids.indexOf(response.uid)] = false; } else { alert("ERROR!"); } } // Then remove the editor from the sequencer content, so that it can be replaced with the correct question view. me.element.parentNode.removeChild(me.element); sequencer.state.editing = false; updateSequencer(); }, true); } this.discard = function() { debugger; me.element.parentNode.removeChild(me.element); me.init(); sequencer.state.loaded = true; sequencer.state.editing = false; sequencer.survey.questions.upToDate[sequencer.survey.questions.uids.indexOf(me.uid)] = false; updateSequencer(); } this.reset = function() { me.init(); } // Updating input fields this.selectAnswerType = function () { // Switch statement, call this.type_x where x is the relevant answer type. // For all this.type_X funtions: // Use the createNewElement and createNewInputLabel methods to add controls or input fields. // Input field convention: class = questionParamField, id = qTypeField / qScaleSize, etc... // Input label class: "l" or "r" depending on alignment with respect to parent ("for") input element. // Important: use the this.paramsElement to access the questionParams div! // To fully make use of this class-based approach, the editor should be added to a global variable. This global variable should be removed on page unload! var type = ge("qeTypeField").value; switch (type) { case "int": me.type_Integer(); break; case "scale": me.type_Scale(); break; case "choice": me.type_Choice(); break; case "text": me.type_Text(); break; default: //Do nothing break; } } this.checkInputSets = function () { // Loop through all input containers in the paramsField for (var n = 0; n < me.paramsElement.childNodes.length; n++) { if (me.paramsElement.childNodes[n].id == "basicInputs") continue; // Check if the id (inputSet) is currently in paramSets if (me.paramSets.indexOf(me.paramsElement.childNodes[n].id) < 0) { me.paramsElement.childNodes[n].parentNode.removeChild(me.paramsElement.childNodes[n]); n--; } } } this.type_Integer = function () { if (me.paramSets.indexOf("int_basic") < 0) { me.paramSets = new Array("int_basic"); } else return; me.checkInputSets(); var container = ce("div"); container.id = "int_basic"; var qeMinValueField = createNewElement("input", "text", "qeMinValueField", "qeParamField", null); var qeMinValueField_lbl = createNewInputLabel("Minimum value: ", "qeMinValueField", "l"); var qeMaxValueField = createNewElement("input", "text", "qeMaxValueField", "qeParamField", null); var qeMaxValueField_lbl = createNewInputLabel("Maximum value: ", "qeMaxValueField", "l"); container.appendChild(qeMinValueField_lbl); container.appendChild(qeMinValueField); container.appendChild(qeMaxValueField_lbl); container.appendChild(qeMaxValueField); me.paramsElement.appendChild(container); } this.type_Scale = function () { if (me.paramSets.indexOf("scale_basic") < 0) { me.paramSets = new Array("scale_basic"); } else return; // Clear any input sets that should not be there me.checkInputSets(); var container = ce("div"); container.id = "scale_basic"; // Number of choices SELECT var numChoicesField = createNewElement("select", null, "qeNumChoicesField", "qeParamField", null); var numChoicesField_lbl = createNewInputLabel("Number of choices", "qeNumChoicesField", "l"); // SELECT options for (var n = 2; n < 11; n++) { var o = ce("option"); o.value = n; o.text = n; numChoicesField.appendChild(o); } container.appendChild(numChoicesField_lbl); container.appendChild(numChoicesField); // Scale labels CHECKBOX and TEXTs var legendsEnabledField = createNewElement("input", "checkbox", "qeLegendsEnabledField", "qeParamField", null); var legendsEnabledField_lbl = createNewInputLabel("Enable legends", "qeLegendsEnabledField", "l"); container.appendChild(legendsEnabledField_lbl); container.appendChild(legendsEnabledField); var upperLegendText = createNewElement("input", "text", "qeUpperLegendField", "qeParamField", null); var lowerLegendText = createNewElement("input", "text", "qeLowerLegendField", "qeParamField", null); var upperLegendText_lbl = createNewInputLabel("Upper legend", "qeUpperLegendField", "l"); var lowerLegendText_lbl = createNewInputLabel("Lower legend", "qeLowerLegendField", "l"); container.appendChild(lowerLegendText_lbl); container.appendChild(lowerLegendText); container.appendChild(upperLegendText_lbl); container.appendChild(upperLegendText); me.paramsElement.appendChild(container); } this.type_Text = function () { if (me.paramSets.indexOf("text_basic") < 0) { me.paramSets = new Array("text_basic"); } else return; me.checkInputSets(); var container = ce("div"); container.id="text_basic"; } this.type_Choice = function() { //debugger; if (me.paramSets.indexOf("choice_basic") < 0) { me.paramSets = new Array("choice_basic"); } else return; me.checkInputSets(); var container = ce("div"); container.id = "choice_basic"; // num options SELECT var numOptionsSelect = createNewElement("select", null, "qeNumOptionsField", "qeParamField", null); var numOptionsSelect_lbl = createNewInputLabel("Number of options", "qeNumOptionsField", "l"); for (var n = 2; n < 11; n++) { var o = ce("option"); o.value = n; o.text = n; numOptionsSelect.appendChild(o); } container.appendChild(numOptionsSelect_lbl); container.appendChild(numOptionsSelect); numOptionsSelect.addEventListener("change", me.type_Choice_CheckAnswerFields, true); var allowMultiple = createNewElement("input", "checkbox", "qeMultipleAnswersField", "qeParamField", null); var allowMultiple_lbl = createNewInputLabel("Allow multiple answers", "qeMultipleAnswersField", "l"); container.appendChild(allowMultiple_lbl); container.appendChild(allowMultiple); var answersField = ce("div"); answersField.className = "qeParamFieldset"; answersField.id = "qeParamsAnswerFieldset" container.appendChild(answersField); me.paramsElement.appendChild(container); me.type_Choice_CheckAnswerFields(); } this.type_Choice_CheckAnswerFields = function() { var container = ge("qeParamsAnswerFieldset"); var numAnswers = parseInt(document.getElementById("qeNumOptionsField").value, 10); var numAnswerFields = 0; { for (var i = container.childNodes.length-1; i >= 0; i--) { if (container.childNodes[i].className = "qeChoiceAnswerGroup") { numAnswerFields++; } } } // If there already are the correct number of answer fields, exit the function if (numAnswers == numAnswerFields) return; else if (numAnswers > numAnswerFields) { // Extra inputs need to be added var n = numAnswers - numAnswerFields; for (var x = 1; x < n+1; x++) { var group = ce("div"); group.className = "qeChoiceAnswerGroup"; var field = createNewElement("input", "text", "qeAnswerField"+(numAnswerFields+x), "qeParamField", null); var field_lbl = createNewInputLabel((numAnswerFields+x), "qeAnswerField"+(numAnswerFields+x), "l"); group.appendChild(field_lbl); group.appendChild(field); container.appendChild(group); } } else if (numAnswers < numAnswerFields) { // There are too many inputs and some need to be removed. Start at the end! // TODO: This is SO inefficient. There has to be a better way, perhaps adding elements to an array? // TODO: Another approach would be to use the previousSibling property as a way to prevent having to loop through the entire container tree every time an object needs to be removed. var n = numAnswerFields-numAnswers; for (var x = 0; x < n; x++) { for (var y = container.childNodes.length-1; y >= 0; y--) { if (container.childNodes[y].className == "qeChoiceAnswerGroup") { container.removeChild(container.childNodes[y]); break; } } } } } // Editing this.editQuestion = function(uid) { debugger; if (sequencer.state.editing == true) return; if (sequencer.state.loaded == false) return; sequencer.state.editing = true; sequencer.state.loaded = false; var request = new Array({ type: "Question", uid: uid }); me.init(); var oldElement = ge(uid); if (oldElement) { // There really should be.... I don't know why I am doing this check... oldElement.parentNode.replaceChild(me.element, oldElement); } var requestString = "args="+JSON.stringify(request); newAjaxRequest(requestString, "getObject.php", function(result){ // Once results are in me.setValues(result.responseText); sequencer.state.loaded = true; }, true); } this.createNewQuestion = function() { if (sequencer.state.editing == true) return; if (sequencer.state.loading == false) return; sequencer.state.editing = true; me.reset(); var container = ge("seqContentWrapper"); container.appendChild(me.element); } } // IT LIIIIIIVESSSSS // TODO: Add database fields for all the necessary question parameters. Maybe only one question parameter property that holds all the settings, then read it out in javascript? // Why have a question Description? Is this even necessary? // Needed properties: // Also not exactly sure what "question->category" is for. Is this one of those questionSet things that Julia was talking about? //