<commit_before>var rootTest = new RegExp("^\\s*(topic|concept|task|reference)\\s*$");

function validateForm(event) {
  var target = event.target;
  for (var i = 0; i < target.elements.length; i++) {
    var elem = target.elements[i]
    if (elem.className === "required" && !elem.disabled) {
      var value;
      switch (elem.nodeName.toLowerCase()) {
      case "select":
        for (var k = 0; k < elem.options.length; k++) {
          if (elem.options[k].selected) {
            value = elem.options[k].value;
            break;
          }
        }
        break;
      default:
        value = elem.value;
      }
      if (value === "") {
        var label = getLabel(elem);
        alert("Required field " + label.toLowerCase() + " has no value");
        return cancelEvent(event);
      } else if (elem.name === "root" && rootTest.test(value)) {
        var label = getLabel(elem);
        alert("Field " + label.toLowerCase() + " may not use the value '"+ value + "' because specializations may not redefine DITA elements");
        return cancelEvent(event);
      }
    }
  }
  return true;
}

function validatePage() {
    var elements = $(".current .required:enabled");
    var valid = true;
    if (elements.filter(":radio, :checkbox").length > 0 && elements.filter(":checked").length == 0) {
        valid = false;
    }
    if (elements.find("option").length > 0 && elements.find("option:selected").length == 0) {
        value = true;
    }
    elements.filter(":text").each(function(i) {
        var elem = $(this);
        var value = elem.val();
        if (value === "") {
            valid = false;
        }
    });

    if ($(".current").prevAll(".page:first").length === 0) { // first page
        $("#prev").attr("disabled", true);
    } else {
        $("#prev").removeAttr("disabled");
    }
    if ($(".current").nextAll(".page:first").length === 0) { // last page
        $("#next").attr("disabled", true);
        if (valid) {
            $("#generate").removeAttr("disabled");
        } else {
            $("#generate").attr("disabled", true);
        }
    } else {
        if (valid) {
            $("#next").removeAttr("disabled");
        } else {
            $("#next").attr("disabled", true);
        }
        $("#generate").attr("disabled", true);
    }
    //alert(valid);
    return true;
}

function getLabel(elem) {
    return $("label[for=" + elem.name + "]:first").text();
}

function cancelEvent(event) {
    if (event.preventDefault) {
      event.stopPropagation();
      event.preventDefault();
    } else {
      event.cancelBubble = true;
      event.returnValue = false;
    }
    return false;
}

function versionChangeHandler(event) {
    var value = $(":input[name=version]").attr("value");
    if (value == 1.2) {
        $(".v1_2").removeClass("disabled").find("input").removeAttr("disabled");
        $("select[name=file] option[value=ent]").removeClass("disabled");
    } else {
        $(".v1_2").addClass("disabled").find("input").attr("disabled", true);
        $("select[name=file] option[value=ent]").addClass("disabled");
    }
    topicChangeHandler();
    return true;
}

/**
 * Output type handler.
 */
function typeChangeHandler(event) {
    var value = $(":input[name=output]").attr("value")
    if (value == "specialization") {
        $("#stylesheet").show().find(":input").removeAttr("disabled");
        $("input[name=root]").removeAttr("disabled").parents("tr").show();
        $("select[name=file] option[value=mod], select[name=file] option[value=ent]").show(); // advanced
    } else {
        $("#stylesheet").hide().find(":input").attr("disabled", true);
        $("input[name=root]").attr("disabled", true).parents("tr").hide();
        $("select[name=file] option[value=mod], select[name=file] option[value=ent], #stylesheet").hide(); // advanced
    }
    return true;
}

/**
 * Topic or map type handler.
 */
function topicChangeHandler(event) {
    var version = $(":input[name=version]").val();
    var type = $(":input[name=type]:checked").val();
    var cls = (type != "" && type != undefined) ? domains[version][type].domainClass : [];
    // filter domains
    var ds = $(".domain");
    ds.each(function(i) {
        var cur = $(this);
        var enabled = false;
        if (type == "") {
            enabled = true;
        } else {
            $.each(cls, function() {
               if (cur.hasClass(this)) {
                   enabled = true;
                   return false;
               }
            });
       }
       var row = cur.parents("tr:first");
       if (enabled) {
           row.show();
           cur.not("*:input").show();
           cur.filter("*:input").removeAttr("disabled");
       } else {
           row.hide();
           cur.not("*:input").hide();
           cur.filter("*:input").attr("disabled", true);
       }
    });
    // filter nested topics
    if ($.inArray("topic", cls) == -1) {
        $("input[name=nested]").attr("disabled", true);
    } else {
        $("input[name=nested]").removeAttr("disabled");
    }
    return true;
}

function domainEnabled(cur, version, type) {
    if (type == "") {
        enabled = true;
    } else {
        $.each(cls, function() {
            if (cur.hasClass(this)) {
                enabled = true;
                return false;
            }
        });
    }
}

/**
 * Check default domains.
 */
function selectDefaultDomains(event) {
    var version = $(":input[name=version]").val();
    var type = $(":input[name=type]:checked").val();
    if (type != undefined && type != "") {
        var ds = domains[version][type].defaultDomains;
        $("input[name=domain]").val(ds);
    }
}
/**
 * Check all domains.
 */
function selectAllDomains(event) {
    $("input[name=domain]").attr("checked", true);
}
/**
 * Uncheck all domain
 */
function selectNoneDomains(event) {
    $("input[name=domain]").removeAttr("checked");
}

//var virtualPage = 0;

/**
 * Previous page button handler.
 */
function prevHandler(event) {
    var p = $(".current").prevAll(".page:not(.disabled):first");
    $(".current").removeClass("current").hide();
    p.addClass("current").show();
    validatePage();
    //setFragment(--virtualPage);
}
/**
 * Next page button handler.
 */
function nextHandler(event) {
    var n = $(".current").nextAll(".page:not(.disabled):first");
    $(".current").removeClass("current").hide();
    n.addClass("current").show();
    validatePage();
    //setFragment(++virtualPage);
}

//function setFragment(i) {
//    window.location.hash = "p" + i;
//}

var attributeCounter = 0;
function attributeAddHandler(event) {
    $("#noAttributesRow").hide();

    attributeCounter++;
    var tr = $("

");
    var p = "att." + attributeCounter;
    tr.append("<input name='" + p + ".name'>	");
    tr.append("<select name='" + p + ".type'><option value='props'>props</option><option value='base'>base</option></select>	");
    var d = $("<select name='" + p + ".datatype'><option value='CDATA'>space-separated values</option><option value='NMTOKENS'>enumeration</option></select> "
              + "<input name='" + p + ".values' style='display:none' disabled title='Space-separated list of possible values'>	");
    d.find("select").change(function(event) {
            var t = $(event.target);
            if (t.val() == "NMTOKENS") {
                t.nextAll("input").show().removeAttr("disabled");
            } else {
                t.nextAll("input").hide().attr("disabled", true);
            }
        });
    tr.append(d);
    var r = $("<button type='button'>remove</button>	");
    r.find("button").click(function(event) {
            $(event.target).parents("tr:first").remove();
            if ($("#attributes tbody tr:visible").length === 0) {
                $("#noAttributesRow").show();
            }
        });
    tr.append(r);
    $("#attributes tbody").append(tr);
}

$(document).ready(function() {
    // handlers
    $(".required:input").change(validatePage);
    $(":input[name=output]").change(typeChangeHandler);
    $(":input[name=version]").change(versionChangeHandler);
    $(":input[name=type]").change(topicChangeHandler);
    $("#selectDefaultDomains").click(selectDefaultDomains);
    $("#selectAllDomains").click(selectAllDomains);
    $("#selectNoneDomains").click(selectNoneDomains);
    $("#addAttribute").click(attributeAddHandler);
    $("form").submit(validateForm);
    // wizard pages
    $(".page").each(function(i) { $(this).attr("id", "p" + i); }).hide();
    $(".page:first").addClass("current").show();
    var prev = $("<button type='button' id='prev'>< Previous</button>").click(prevHandler);
    var next = $("<button type='button' id='next'>Next ></button>").click(nextHandler);
    $("#generate").before(prev).before(" ").before(next).before(" ");
    // init
    topicChangeHandler();
    typeChangeHandler();
    versionChangeHandler();
    validatePage();
});
<commit_msg>Add support for pagination using location hash.<commit_after>var rootTest = new RegExp("^\\s*(topic|concept|task|reference)\\s*$");

/** Current location fragment. */
var hash = location.hash;
/** Attribute domain counter. */
var attributeCounter = 0;

function validateForm(event) {
  var target = event.target;
  for (var i = 0; i < target.elements.length; i++) {
    var elem = target.elements[i]
    if (elem.className === "required" && !elem.disabled) {
      var value;
      switch (elem.nodeName.toLowerCase()) {
      case "select":
        for (var k = 0; k < elem.options.length; k++) {
          if (elem.options[k].selected) {
            value = elem.options[k].value;
            break;
          }
        }
        break;
      default:
        value = elem.value;
      }
      if (value === "") {
        var label = getLabel(elem);
        alert("Required field " + label.toLowerCase() + " has no value");
        return cancelEvent(event);
      } else if (elem.name === "root" && rootTest.test(value)) {
        var label = getLabel(elem);
        alert("Field " + label.toLowerCase() + " may not use the value '"+ value + "' because specializations may not redefine DITA elements");
        return cancelEvent(event);
      }
    }
  }
  return true;
}

function validatePage() {
    var elements = $(".current .required:enabled");
    var valid = true;
    if (elements.filter(":radio, :checkbox").length > 0 && elements.filter(":checked").length == 0) {
        valid = false;
    }
    if (elements.find("option").length > 0 && elements.find("option:selected").length == 0) {
        value = true;
    }
    elements.filter(":text").each(function(i) {
        var elem = $(this);
        var value = elem.val();
        if (value === "") {
            valid = false;
        }
    });

    if ($(".current").prevAll(".page:first").length === 0) { // first page
        $("#prev").attr("disabled", true);
    } else {
        $("#prev").removeAttr("disabled");
    }
    if ($(".current").nextAll(".page:first").length === 0) { // last page
        $("#next").attr("disabled", true);
        if (valid) {
            $("#generate").removeAttr("disabled");
        } else {
            $("#generate").attr("disabled", true);
        }
    } else {
        if (valid) {
            $("#next").removeAttr("disabled");
        } else {
            $("#next").attr("disabled", true);
        }
        $("#generate").attr("disabled", true);
    }
    //alert(valid);
    return true;
}

function getLabel(elem) {
    return $("label[for=" + elem.name + "]:first").text();
}

function cancelEvent(event) {
    if (event.preventDefault) {
      event.stopPropagation();
      event.preventDefault();
    } else {
      event.cancelBubble = true;
      event.returnValue = false;
    }
    return false;
}

function versionChangeHandler(event) {
    var value = $(":input[name=version]").attr("value");
    if (value == 1.2) {
        $(".v1_2").removeClass("disabled").find("input").removeAttr("disabled");
        $("select[name=file] option[value=ent]").removeClass("disabled");
    } else {
        $(".v1_2").addClass("disabled").find("input").attr("disabled", true);
        $("select[name=file] option[value=ent]").addClass("disabled");
    }
    topicChangeHandler();
    return true;
}

/**
 * Output type handler.
 */
function typeChangeHandler(event) {
    var value = $(":input[name=output]").attr("value")
    if (value == "specialization") {
        $("#stylesheet").show().find(":input").removeAttr("disabled");
        $("input[name=root]").removeAttr("disabled").parents("tr").show();
        $("select[name=file] option[value=mod], select[name=file] option[value=ent]").show(); // advanced
    } else {
        $("#stylesheet").hide().find(":input").attr("disabled", true);
        $("input[name=root]").attr("disabled", true).parents("tr").hide();
        $("select[name=file] option[value=mod], select[name=file] option[value=ent], #stylesheet").hide(); // advanced
    }
    return true;
}

/**
 * Topic or map type handler.
 */
function topicChangeHandler(event) {
    var version = $(":input[name=version]").val();
    var type = $(":input[name=type]:checked").val();
    var cls = (type != "" && type != undefined) ? domains[version][type].domainClass : [];
    // filter domains
    var ds = $(".domain");
    ds.each(function(i) {
        var cur = $(this);
        var enabled = false;
        if (type == "") {
            enabled = true;
        } else {
            $.each(cls, function() {
               if (cur.hasClass(this)) {
                   enabled = true;
                   return false;
               }
            });
       }
       var row = cur.parents("tr:first");
       if (enabled) {
           row.show();
           cur.not("*:input").show();
           cur.filter("*:input").removeAttr("disabled");
       } else {
           row.hide();
           cur.not("*:input").hide();
           cur.filter("*:input").attr("disabled", true);
       }
    });
    // filter nested topics
    if ($.inArray("topic", cls) == -1) {
        $("input[name=nested]").attr("disabled", true);
    } else {
        $("input[name=nested]").removeAttr("disabled");
    }
    return true;
}

function domainEnabled(cur, version, type) {
    if (type == "") {
        enabled = true;
    } else {
        $.each(cls, function() {
            if (cur.hasClass(this)) {
                enabled = true;
                return false;
            }
        });
    }
}

/**
 * Check default domains.
 */
function selectDefaultDomains(event) {
    var version = $(":input[name=version]").val();
    var type = $(":input[name=type]:checked").val();
    if (type != undefined && type != "") {
        var ds = domains[version][type].defaultDomains;
        $("input[name=domain]").val(ds);
    }
}
/**
 * Check all domains.
 */
function selectAllDomains(event) {
    $("input[name=domain]").attr("checked", true);
}
/**
 * Uncheck all domain
 */
function selectNoneDomains(event) {
    $("input[name=domain]").removeAttr("checked");
}

/**
 * Previous page button handler.
 */
function prevHandler(event) {
    var p = $(".current").prevAll(".page:not(.disabled):first");
    $(".current").removeClass("current").hide();
    p.addClass("current").show();
    validatePage();
    setFragment();
}
/**
 * Next page button handler.
 */
function nextHandler(event) {
    var n = $(".current").nextAll(".page:not(.disabled):first");
    $(".current").removeClass("current").hide();
    n.addClass("current").show();
    validatePage();
    setFragment();
}

/** Set location fragment to current page. */
function setFragment(i) {
    hash = $(".current").attr("id");
    location.hash = hash;
}
/** Check if location fragment has changed and change page accordingly. */
function checkFragment() {
    if (location.hash.substr(1) != hash) {
        //alert("changed: " + location.hash.substr(1) + " " + hash);
        if (location.hash.substr(1) == "") {
            location.replace("#" + $(".page:first").attr("id"));
        }
        hash = location.hash.substr(1);
        $(".current").removeClass("current").hide();
        $("#" + hash).addClass("current").show();
        validatePage();
    }
}

function attributeAddHandler(event) {
    $("#noAttributesRow").hide();

    attributeCounter++;
    var tr = $("

");
    var p = "att." + attributeCounter;
    tr.append("<input name='" + p + ".name'>	");
    tr.append("<select name='" + p + ".type'><option value='props'>props</option><option value='base'>base</option></select>	");
    var d = $("<select name='" + p + ".datatype'><option value='CDATA'>space-separated values</option><option value='NMTOKENS'>enumeration</option></select> "
              + "<input name='" + p + ".values' style='display:none' disabled title='Space-separated list of possible values'>	");
    d.find("select").change(function(event) {
            var t = $(event.target);
            if (t.val() == "NMTOKENS") {
                t.nextAll("input").show().removeAttr("disabled");
            } else {
                t.nextAll("input").hide().attr("disabled", true);
            }
        });
    tr.append(d);
    var r = $("<button type='button'>remove</button>	");
    r.find("button").click(function(event) {
            $(event.target).parents("tr:first").remove();
            if ($("#attributes tbody tr:visible").length === 0) {
                $("#noAttributesRow").show();
            }
        });
    tr.append(r);
    $("#attributes tbody").append(tr);
}

$(document).ready(function() {
    // handlers
    $(".required:input").change(validatePage);
    $(":input[name=output]").change(typeChangeHandler);
    $(":input[name=version]").change(versionChangeHandler);
    $(":input[name=type]").change(topicChangeHandler);
    $("#selectDefaultDomains").click(selectDefaultDomains);
    $("#selectAllDomains").click(selectAllDomains);
    $("#selectNoneDomains").click(selectNoneDomains);
    $("#addAttribute").click(attributeAddHandler);
    $("form").submit(validateForm);
    // wizard pages
    $(".page").each(function(i) { $(this).attr("id", "p" + i); }).hide();
    $(".page:first").addClass("current").show();
    var prev = $("<button type='button' id='prev'>< Previous</button>").click(prevHandler);
    var next = $("<button type='button' id='next'>Next ></button>").click(nextHandler);
    $("#generate").before(prev).before(" ").before(next).before(" ");
    // init
    topicChangeHandler();
    typeChangeHandler();
    versionChangeHandler();
    validatePage();

    setInterval(checkFragment, 100);
});<|endoftext|>


<commit_before>/*global mockForms2, mockInstances*/

/**
 * [FormDataController description]
 * @param {{instanceId: string, entityId: string}} params [description]
 * @constructor
 */
function FormDataController(params){
	params = params || {};
	var originalInstanceId = params.instanceId || null;
	var formDataRepository = new enketo.FormDataRepository();
    var controller = new enketo.FormDataController(
        new enketo.EntityRelationshipLoader(),
        new enketo.FormDefinitionLoader(),
        new enketo.FormModelMapper(formDataRepository, new enketo.SQLQueryBuilder(formDataRepository), new enketo.IdFactory(new enketo.IdFactoryBridge())),
        formDataRepository, new enketo.FormSubmissionRouter());

    /**
	 * Gets instance as JSON from Dristhi DB - Should this be asynchronous?
	 * @return {?*} Form Data JSON object
	 */
    this.get = function(){
		return controller.get(params) || null;
	};

	/**
	 * Passes instance as JSON to store in Dristhi DB - Should this be asynchronous?
	 * @param  {string} instanceId	the new instanceID of the record
	 * @param  {*}		data		Form Data JSON object
	 * @return {boolean}
	 */
	this.save = function(instanceId, data){
		controller.save(params, data);
		androidContext.goBack();
	};

	this.remove = function(instanceId){

	};
}

/**
 * Class maintaining a Drishti JSON Data Definition and deal with JSON <-> XML transformation
 * @param {FormDataJSON} data  Drishti Data Definition JSON
 * @constructor
 */
function JData(data){

	if (!data){
		recordError('No instance query parameter provided!');
	}

	var unboundSubformInstanceProps = ['id'];

	/**
	 * Transforms JSON to an XML string
	 * NOTE: alternatively, we could could overwrite Form.init() to use JSON data instead of XML for instantiation
	 * @return {?string}			XML string
	 */
	this.toXML = function(){
		var i, j, k, field, path, value, subForm, repeatInstance, defaultPath, repeatNodeName,
			$instance = $($.parseXML('<root />'));

		if (typeof data !== 'object'){
			recordError("error: no JSON object provided during instantiation");
		}
		//main form:
		for (i = 0; i<data.form.fields.length; i++){
			defaultPath = defaultPathFixed(data.form.default_bind_path);
			field = data.form.fields[i];
			//we only have to concern ourselves with fields that have a value (incl empty string)
			if (typeof field.value !== 'undefined'){
				path = (typeof field.bind === 'undefined') ? defaultPath + field.name : field.bind;
				value = field.value;
				addXMLNodeAndValue($instance, path, value);
				//console.log('added path: '+path+' with value: "'+value+'"');
			}
		}
		//repeats:
		if (data.form.sub_forms){
			for (i = 0; i<data.form.sub_forms.length; i++){
				subForm = data.form.sub_forms[i];
				defaultPath = defaultPathFixed(subForm.default_bind_path);
				repeatNodeName = defaultPath.match(/.*\/([^\/]*)\/$/)[1];
				if (!subForm.bind_type){
					recordError('Repeat (subform) is missing bind_type.');
				}
				else{
					for (j = 0; j < subForm.instances.length; j++){
						repeatInstance = subForm.instances[j];
						for (k = 0; k < subForm.fields.length; k++){
							field = subForm.fields[k];
							if (typeof repeatInstance[field.name] !== 'undefined' && unboundSubformInstanceProps.indexOf(field.name) == -1){
								path = (typeof field.bind === 'undefined') ? defaultPath + field.name : field.bind;
								value = repeatInstance[field.name];
								//note: also if the value is empty it is added!
								addXMLNodeAndValue($instance, path, value, {name: repeatNodeName, index: j});
								console.log('added path: '+path+' with value: "'+value+'", repeat NodeName: '+repeatNodeName+' and repeat index: '+j);
							}
						}
					}
				}
			}
		}
		return (new XMLSerializer()).serializeToString($instance.find('instance>*:first')[0]);
	};

	/**
	 * Gets current state of form instance in JSON format.
	 */
	this.get = function(){
		var $repeatLeaves, origInstances,
			subFormsStarted = [],
			$mainLeaves = this.getInstanceXML(false).find('*').filter(function(){
				return $(this).children().length === 0;
			}),
			/*
			 * issue: this relies on a template node to be present, which is not required for repeats in OpenRosa
			 * but thankfully is guaranteed in formhub-hosted forms.
			 * Nested repeats are not supported!
			 */
			//$repeats = form.getDataO().$.find('instance:first [template]').siblings().filter(function(){
			$repeats = this.getInstanceXML(true).find('[template]').siblings().filter(function(){
				var nodeName = $(this).prop('nodeName');
				return $(this).siblings(nodeName+'[template]').length > 0;
			});

		//this code has to be refactored, as the unboundSubformInstanceProps stuff was bolted on when the JSON spec changed.
		$repeats.each(function(){
			var i, prop, bindPath, subForm, instanceIndex,
				subForms = [],
				instance = {},
				$repeat = $(this);
			//identifier for subform in JSON format is default_bind_path
			bindPath = '/model/instance'+$repeat.getXPath();
			if (data.form.sub_forms) {
				subForms = $.grep(data.form.sub_forms, function(subfrm){
					//be flexible with trailing slash
					return (subfrm.default_bind_path === bindPath || subfrm.default_bind_path === bindPath+'/');
				});
			}
			if (subForms.length === 0){
				recordError('Repeat definition not found (no subform with default_bind_path: "'+bindPath+'" in JSON format)');
			}
			else if (subForms.length > 1){
				recordError('Multiple repeat definititions found (multiple subforms with default_bind_path: "'+bindPath+'" in JSON format)');
			}
			else{
				subForm = subForms[0];
				if ($.inArray(bindPath, subFormsStarted) === -1){
					subFormsStarted.push(bindPath);
					origInstances = jQuery.extend(true, {}, subForm.instances);
					subForm.instances = [];
				}
				$repeatLeaves = $repeat.find('*').filter(function(){
					return $(this).children().length === 0;
				});
				$mainLeaves = $mainLeaves.takeOut($repeatLeaves);
				$repeatLeaves.each(function(){
					var props = getNodeProps($(this)),
						field = getField(props.path, subForm.fields, subForm.default_bind_path);
					if (field){
						//adding value even if it's empty without checking what it was previously
						instance[field.name] = props.value;
					}
				});
				subForm.instances.push(instance);
				//add subform instance properties that have no equivalent in XML - a quick and dirty addition
				instanceIndex = subForm.instances.indexOf(instance);
				for (i=0; i<unboundSubformInstanceProps.length; i++){
					prop = unboundSubformInstanceProps[i];
					if (origInstances[instanceIndex] && typeof origInstances[instanceIndex][prop] !== 'undefined'){
						//console.log('adding unbound property '+prop+'to repeat instance with index '+instanceIndex);
						instance[prop] = origInstances[instanceIndex][prop];
					}
				}
			}
		});

		$mainLeaves.each(function(){
			var props = getNodeProps($(this)),
				field = getField(props.path, data.form.fields, data.form.default_bind_path);
			if (field && (field.value || props.value.length > 0) ){
				field.value = props.value;
			}
		});
		return data;
	};

	/**
	 * gets XML from form wrapped as a JQuery XML object
	 * @param  {boolean} includeTemplates [description]
	 * @return {jQuery}                  [description]
	 */
	this.getInstanceXML = function(includeTemplates){
		return $($.parseXML(form.getDataStr(includeTemplates)));
	};

	/**
	 * given an XML path, obtains a field from a field array
	 * @param  {string} path
	 * @param  {Array.<{name:string, source: string, value: string, bind: string}>} fieldArr    array of field objects
	 * @param  {string} defaultPath																default bind path of form or subform
	 * @return {?{name:string, source: string, value: string, bind: string}}					field object
	 */
	function getField(path, fieldArr, defaultPath){
		var fields = $.grep(fieldArr, function(field){
			return (typeof field.bind  === 'undefined' && (defaultPath + field.name) === path) ||
				(typeof field.bind !== 'undefined' && field.bind === path);
		});

		if (fields.length > 1){
			recordError('Multiple fields found (multiple nodes with path: '+path+' found in JSON format.');
			return null;
		}
		/*else if (fields.length === 0 && path.indexOf('/meta/deprecatedID') !== -1){
			return null;
		}*/
		else if (fields.length === 0){
			recordError('Field not found (node with path: '+path+' was missing from JSON format).');
			return null;
		}
		else {
			console.debug('found field with path '+path+' in JSON Form Data');
			return fields[0];
		}
	}

	function getNodeProps($node){
		var partialPath = $node.getXPath('model'),
			fullPath = (partialPath.indexOf('/model/instance') === -1) ? '/model/instance'+partialPath : partialPath;
		return {
			nodeName: $node.prop('nodeName'),
			value: $node.text(),
			path: fullPath
		};
	}

	/**
	 * [addXMLNode description]
	 * @param {jQuery} $doc								jQuery doc with root element to add nodes to
	 * @param {string} path								path of node to be added when not present starting with /
	 * @param {string} value							value of node
	 * @param {{name: string, index: number}=} repeatO	repeatObject with repeat nodeName and 0-based index of repeat parent of node to be added
	 * @return {jQuery}									jQuery doc with added node and value
	 */
	function addXMLNodeAndValue ($doc, path, value, repeatO){
		var j, $node,
			$current = $doc.find('root'),
			nodeNames = path.substring(1).split('/'),
			r = repeatO || {};

		for (j = 0; j<nodeNames.length ; j++){
			if ($current.children(nodeNames[j]).length === 0 ||
				(r.name && r.index && nodeNames[j] === r.name && $current.children(nodeNames[j]).eq(r.index).length === 0)){
				//console.log('nodeName does not exist, going to create it as child of ', $current[0]);
				var node = $.parseXML('<'+nodeNames[j]+'/>').documentElement;
				document.adoptChild(node);
				$node = $(node);
				$current.append($node);
				$current = $node;
			}
			else{
				$current = ( r.index && nodeNames[j] === r.name ) ? $current.children(nodeNames[j]).eq(r.index) : $current.children(nodeNames[j]);
			}

			if (j === (nodeNames.length - 1)){
				$current.text(value);
			}
		}
		return $doc;
	}

	//if the default path does not have a trailing slash, add one
	function defaultPathFixed (path){
		return (path.lastIndexOf('/') !== path.length -1) ? path + '/' : path;
	}

	function recordError(errorMsg){
		if (typeof data.errors == 'undefined') data.errors = [];
		data.errors.push(errorMsg);
		console.error(errorMsg);
	}
}

/**
 * Plugin that removes from a collection the nodes in the parameter that have the same Path
 */
(function($){
	$.fn.takeOut = function($nodes){
		return this.filter(function(){
			for (var i = 0 ; i<$nodes.length ; i++){
				if ($(this).getXPath() === $nodes.eq(i).getXPath()){
					//console.log('took out node: ', $(this));
					return false;
				}
			}
			return true;
		});
	};
})(jQuery);<commit_msg>adoptChild != adoptNode<commit_after>/*global mockForms2, mockInstances*/

/**
 * [FormDataController description]
 * @param {{instanceId: string, entityId: string}} params [description]
 * @constructor
 */
function FormDataController(params){
	params = params || {};
	var originalInstanceId = params.instanceId || null;
	var formDataRepository = new enketo.FormDataRepository();
    var controller = new enketo.FormDataController(
        new enketo.EntityRelationshipLoader(),
        new enketo.FormDefinitionLoader(),
        new enketo.FormModelMapper(formDataRepository, new enketo.SQLQueryBuilder(formDataRepository), new enketo.IdFactory(new enketo.IdFactoryBridge())),
        formDataRepository, new enketo.FormSubmissionRouter());

    /**
	 * Gets instance as JSON from Dristhi DB - Should this be asynchronous?
	 * @return {?*} Form Data JSON object
	 */
    this.get = function(){
		return controller.get(params) || null;
	};

	/**
	 * Passes instance as JSON to store in Dristhi DB - Should this be asynchronous?
	 * @param  {string} instanceId	the new instanceID of the record
	 * @param  {*}		data		Form Data JSON object
	 * @return {boolean}
	 */
	this.save = function(instanceId, data){
		controller.save(params, data);
		androidContext.goBack();
	};

	this.remove = function(instanceId){

	};
}

/**
 * Class maintaining a Drishti JSON Data Definition and deal with JSON <-> XML transformation
 * @param {FormDataJSON} data  Drishti Data Definition JSON
 * @constructor
 */
function JData(data){

	if (!data){
		recordError('No instance query parameter provided!');
	}

	var unboundSubformInstanceProps = ['id'];

	/**
	 * Transforms JSON to an XML string
	 * NOTE: alternatively, we could could overwrite Form.init() to use JSON data instead of XML for instantiation
	 * @return {?string}			XML string
	 */
	this.toXML = function(){
		var i, j, k, field, path, value, subForm, repeatInstance, defaultPath, repeatNodeName,
			$instance = $($.parseXML('<root />'));

		if (typeof data !== 'object'){
			recordError("error: no JSON object provided during instantiation");
		}
		//main form:
		for (i = 0; i<data.form.fields.length; i++){
			defaultPath = defaultPathFixed(data.form.default_bind_path);
			field = data.form.fields[i];
			//we only have to concern ourselves with fields that have a value (incl empty string)
			if (typeof field.value !== 'undefined'){
				path = (typeof field.bind === 'undefined') ? defaultPath + field.name : field.bind;
				value = field.value;
				addXMLNodeAndValue($instance, path, value);
				//console.log('added path: '+path+' with value: "'+value+'"');
			}
		}
		//repeats:
		if (data.form.sub_forms){
			for (i = 0; i<data.form.sub_forms.length; i++){
				subForm = data.form.sub_forms[i];
				defaultPath = defaultPathFixed(subForm.default_bind_path);
				repeatNodeName = defaultPath.match(/.*\/([^\/]*)\/$/)[1];
				if (!subForm.bind_type){
					recordError('Repeat (subform) is missing bind_type.');
				}
				else{
					for (j = 0; j < subForm.instances.length; j++){
						repeatInstance = subForm.instances[j];
						for (k = 0; k < subForm.fields.length; k++){
							field = subForm.fields[k];
							if (typeof repeatInstance[field.name] !== 'undefined' && unboundSubformInstanceProps.indexOf(field.name) == -1){
								path = (typeof field.bind === 'undefined') ? defaultPath + field.name : field.bind;
								value = repeatInstance[field.name];
								//note: also if the value is empty it is added!
								addXMLNodeAndValue($instance, path, value, {name: repeatNodeName, index: j});
								console.log('added path: '+path+' with value: "'+value+'", repeat NodeName: '+repeatNodeName+' and repeat index: '+j);
							}
						}
					}
				}
			}
		}
		return (new XMLSerializer()).serializeToString($instance.find('instance>*:first')[0]);
	};

	/**
	 * Gets current state of form instance in JSON format.
	 */
	this.get = function(){
		var $repeatLeaves, origInstances,
			subFormsStarted = [],
			$mainLeaves = this.getInstanceXML(false).find('*').filter(function(){
				return $(this).children().length === 0;
			}),
			/*
			 * issue: this relies on a template node to be present, which is not required for repeats in OpenRosa
			 * but thankfully is guaranteed in formhub-hosted forms.
			 * Nested repeats are not supported!
			 */
			//$repeats = form.getDataO().$.find('instance:first [template]').siblings().filter(function(){
			$repeats = this.getInstanceXML(true).find('[template]').siblings().filter(function(){
				var nodeName = $(this).prop('nodeName');
				return $(this).siblings(nodeName+'[template]').length > 0;
			});

		//this code has to be refactored, as the unboundSubformInstanceProps stuff was bolted on when the JSON spec changed.
		$repeats.each(function(){
			var i, prop, bindPath, subForm, instanceIndex,
				subForms = [],
				instance = {},
				$repeat = $(this);
			//identifier for subform in JSON format is default_bind_path
			bindPath = '/model/instance'+$repeat.getXPath();
			if (data.form.sub_forms) {
				subForms = $.grep(data.form.sub_forms, function(subfrm){
					//be flexible with trailing slash
					return (subfrm.default_bind_path === bindPath || subfrm.default_bind_path === bindPath+'/');
				});
			}
			if (subForms.length === 0){
				recordError('Repeat definition not found (no subform with default_bind_path: "'+bindPath+'" in JSON format)');
			}
			else if (subForms.length > 1){
				recordError('Multiple repeat definititions found (multiple subforms with default_bind_path: "'+bindPath+'" in JSON format)');
			}
			else{
				subForm = subForms[0];
				if ($.inArray(bindPath, subFormsStarted) === -1){
					subFormsStarted.push(bindPath);
					origInstances = jQuery.extend(true, {}, subForm.instances);
					subForm.instances = [];
				}
				$repeatLeaves = $repeat.find('*').filter(function(){
					return $(this).children().length === 0;
				});
				$mainLeaves = $mainLeaves.takeOut($repeatLeaves);
				$repeatLeaves.each(function(){
					var props = getNodeProps($(this)),
						field = getField(props.path, subForm.fields, subForm.default_bind_path);
					if (field){
						//adding value even if it's empty without checking what it was previously
						instance[field.name] = props.value;
					}
				});
				subForm.instances.push(instance);
				//add subform instance properties that have no equivalent in XML - a quick and dirty addition
				instanceIndex = subForm.instances.indexOf(instance);
				for (i=0; i<unboundSubformInstanceProps.length; i++){
					prop = unboundSubformInstanceProps[i];
					if (origInstances[instanceIndex] && typeof origInstances[instanceIndex][prop] !== 'undefined'){
						//console.log('adding unbound property '+prop+'to repeat instance with index '+instanceIndex);
						instance[prop] = origInstances[instanceIndex][prop];
					}
				}
			}
		});

		$mainLeaves.each(function(){
			var props = getNodeProps($(this)),
				field = getField(props.path, data.form.fields, data.form.default_bind_path);
			if (field && (field.value || props.value.length > 0) ){
				field.value = props.value;
			}
		});
		return data;
	};

	/**
	 * gets XML from form wrapped as a JQuery XML object
	 * @param  {boolean} includeTemplates [description]
	 * @return {jQuery}                  [description]
	 */
	this.getInstanceXML = function(includeTemplates){
		return $($.parseXML(form.getDataStr(includeTemplates)));
	};

	/**
	 * given an XML path, obtains a field from a field array
	 * @param  {string} path
	 * @param  {Array.<{name:string, source: string, value: string, bind: string}>} fieldArr    array of field objects
	 * @param  {string} defaultPath																default bind path of form or subform
	 * @return {?{name:string, source: string, value: string, bind: string}}					field object
	 */
	function getField(path, fieldArr, defaultPath){
		var fields = $.grep(fieldArr, function(field){
			return (typeof field.bind  === 'undefined' && (defaultPath + field.name) === path) ||
				(typeof field.bind !== 'undefined' && field.bind === path);
		});

		if (fields.length > 1){
			recordError('Multiple fields found (multiple nodes with path: '+path+' found in JSON format.');
			return null;
		}
		/*else if (fields.length === 0 && path.indexOf('/meta/deprecatedID') !== -1){
			return null;
		}*/
		else if (fields.length === 0){
			recordError('Field not found (node with path: '+path+' was missing from JSON format).');
			return null;
		}
		else {
			console.debug('found field with path '+path+' in JSON Form Data');
			return fields[0];
		}
	}

	function getNodeProps($node){
		var partialPath = $node.getXPath('model'),
			fullPath = (partialPath.indexOf('/model/instance') === -1) ? '/model/instance'+partialPath : partialPath;
		return {
			nodeName: $node.prop('nodeName'),
			value: $node.text(),
			path: fullPath
		};
	}

	/**
	 * [addXMLNode description]
	 * @param {jQuery} $doc								jQuery doc with root element to add nodes to
	 * @param {string} path								path of node to be added when not present starting with /
	 * @param {string} value							value of node
	 * @param {{name: string, index: number}=} repeatO	repeatObject with repeat nodeName and 0-based index of repeat parent of node to be added
	 * @return {jQuery}									jQuery doc with added node and value
	 */
	function addXMLNodeAndValue ($doc, path, value, repeatO){
		var j, $node,
			$current = $doc.find('root'),
			nodeNames = path.substring(1).split('/'),
			r = repeatO || {};

		for (j = 0; j<nodeNames.length ; j++){
			if ($current.children(nodeNames[j]).length === 0 ||
				(r.name && r.index && nodeNames[j] === r.name && $current.children(nodeNames[j]).eq(r.index).length === 0)){
				//console.log('nodeName does not exist, going to create it as child of ', $current[0]);
				var node = $.parseXML('<'+nodeNames[j]+'/>').documentElement;
				document.adoptNode(node);
				$node = $(node);
				$current.append($node);
				$current = $node;
			}
			else{
				$current = ( r.index && nodeNames[j] === r.name ) ? $current.children(nodeNames[j]).eq(r.index) : $current.children(nodeNames[j]);
			}

			if (j === (nodeNames.length - 1)){
				$current.text(value);
			}
		}
		return $doc;
	}

	//if the default path does not have a trailing slash, add one
	function defaultPathFixed (path){
		return (path.lastIndexOf('/') !== path.length -1) ? path + '/' : path;
	}

	function recordError(errorMsg){
		if (typeof data.errors == 'undefined') data.errors = [];
		data.errors.push(errorMsg);
		console.error(errorMsg);
	}
}

/**
 * Plugin that removes from a collection the nodes in the parameter that have the same Path
 */
(function($){
	$.fn.takeOut = function($nodes){
		return this.filter(function(){
			for (var i = 0 ; i<$nodes.length ; i++){
				if ($(this).getXPath() === $nodes.eq(i).getXPath()){
					//console.log('took out node: ', $(this));
					return false;
				}
			}
			return true;
		});
	};
})(jQuery);<|endoftext|>


<commit_before>var SwipeTable = function(dataProviderUrl, tableKeys, elem){
	"use strict";
	/**
	 * Config
	 *
	 * Use the following format to call SwipeTable
	 *----------------------------------------
	 *	(function(root){
	 *
   *		var restApiUrl = '/api';
   *		var keys = [
   *		    "id", // this is the 'pinned' column
   *		    "time",
   *		    "time2",
   *		    "location",
   *		    "location2"
   *		];
   *		var stElem = document.getElementsByClassName('swipe-table')[0];
	 *
   *		root.SwipeTable = new SwipeTable(restApiUrl, keys, stElem);
	 *
   *	}(this));
	 *----------------------------------------
	 */

	//=== Variables ===
	var dataProvider     = dataProviderUrl;
	var keys             = tableKeys;
	var elementReference = elem;
	var swipeReference;

	//TODO: configure table class height via call or test
	var tableClass = 'table';
	var rowHeight = 39;
	tableClass += ' table-condensed'; rowHeight = 33;

	var doneTables    = 0;
	var totalTables   = 0;
	var pageSize      = 1;
	var sortAscending = true;
	var sortColumn;

	//=== Functions ===

	/**
	 * Fetch the viewport size and set the
	 * pageSize(num of rows) based on rowHeight
	 */
	var init = function(){
		// responsejs.com/labs/dimensions/
		var matchMedia = window.matchMedia || window.msMatchMedia;
		var viewportH = (function(win, docElem, mM) {
			var client = docElem.clientHeight,
			    inner = win.innerHeight;
			if (mM && client < inner && true === mM('(min-height:' + inner + 'px)').matches){
				return win.innerHeight;
			}
			else{
				return docElem.clientHeight;
			}
		}(window, document.documentElement, matchMedia));

		// Remove one rowHeight for the header;
		viewportH -= rowHeight;
		pageSize = Math.floor(viewportH / rowHeight);
	};

	/**
	 * Takes an object that contains:
	 * REQ* server: address of the API
	 * REQ* pageSize
	 * REQ* table: container to attach the results to
	 *    * page
	 *    * timestamp
	 *    * sortField
	 *    * sortAsc
	 * Items marked with REQ are required or the request will fail.
	 *
	 * Fetches according to parameters given
	 * and gives results + table to parseResponse.
	 * @param {Object}queries Object containing quieries
	 */
	var makeRequest = function (queries){
		if(!queries.server){
			console.log("No server provided to makeRequest, not making request");
			return;
		}
		console.log("Making request to "+ queries.server+ ".");
		if(queries.timestamp){
			if(queries.sortField || queries.sortAsc){
				if(!queries.sortField || !queries.sortAsc){
					console.log("Missing sort parameter");
					return;
				}

				if(queries.page){
					executeRequest("GET",
					                queries.server +
					                  "?p=" + queries.page +
					                  "&ps=" + pageSize +
					                  "&ts=" + queries.timestamp +
					                  "&sort[field]=" + queries.sortField +
					                  "&sort[asc]=" + queries.sortAsc,
					                queries.table);
				}
				else{
					executeRequest("GET",
					                queries.server +
					                  "?ps=" + pageSize +
					                  "&ts=" + queries.timestamp +
					                  "&sort[field]=" + queries.sortField +
					                  "&sort[asc]=" + queries.sortAsc,
					                queries.table);
				}
			}
			else{
				if(!queries.page){
					console.log("Missing page, not according to spec.");
					return;
				}
				// No sorting, must be page request
				executeRequest("GET",
				                queries.server +
				                  "?p=" + queries.page +
				                  "&ps=" + pageSize +
				                  "&ts=" + queries.timestamp,
				                queries.table);
			}
		}
		else{
			// No timestamp, fresh request
			executeRequest("GET",
			                queries.server +
			                  "?ps=" + pageSize,
			                queries.table);
		}
	};

	/**
	 * Executes a request with method, parameters and the table give to next function
	 * @param  {String} method Valid HTTP method eg. GET, POST
	 * @param  {String} url Complete url string (server + parameters)
	 * @param  {Object} table Partial table object
	 */
	var executeRequest = function(method, url, table){
		var r = new XMLHttpRequest();
		r.open(method, url, true);
		r.onreadystatechange = function(){
			if(r.readyState !== 4 || r.status !== 200){
				console.log("Request went sour");
				return;
			}
			console.log("Request successful.");
			parseResponse(table, r.responseText);
		};
		r.send(null);
	};

	// parseResponse(table, response)
	//  Calls fillTable with table and JSON.parse
	//TODO: Work parseResponse into appropriate function
	var parseResponse = function(table, response){
		console.log("Parsing response.");
		fillTable(table, JSON.parse(response));
	};

	/**
	 * Creates a table with appropriate headers(thead).
	 *
	 * Increments totalTables.
	 *
	 * Returns a partial table.
	 */
	var createTable = (function(){
		console.log("Creating Table.");
		var tableInstance;
		return function(){
			if(tableInstance){
				console.log("Giving table instance");
				totalTables += 1;
				var copy = tableInstance.cloneNode(true);
				return copy;
			}
			else{
				console.log("Creating new Table");
				var i = 0;
				var table = document.createElement("table");
				table.className = tableClass;
				var thead = document.createElement("thead");
				var tr = document.createElement("tr");

				for (i; i < keys.length; i+=1){
					var th = document.createElement("th");
					th.appendChild(document.createTextNode(keys[i]));
					tr.appendChild(th);
				}

				thead.appendChild(tr);
				table.appendChild(thead);
				console.log("Table created.");
				tableInstance = table.cloneNode(true);
				totalTables += 1;
				return table;
			}
		};
	}());

	/**
	 * Goes through dataSet and attaches rows to
	 * the table reference, then attaches the
	 * table to the container in the DOM.
	 *
	 * Calls tableDone() when done.
	 * @param table Partial table to fill
	 * @param dataSet Parsed data to fill with
	 */
	var fillTable = function(table, dataSet){
		console.log("Filling table.");

		var numRows = dataSet.length;
		if(numRows === 0){
			return;
		}

		var tbody = document.createElement("tbody");
		table.appendChild(tbody);

		var i = 0;
		while (i < numRows){
			var col = dataSet[i];
			var tr = document.createElement("tr");
			var colCells = Object.keys(col).length;

			var j = 0;
			while (j < colCells)
			{
				var td = document.createElement("td");
				td.appendChild(document.createTextNode(col[keys[j]]));
				tr.appendChild(td);
				j+=1;
			}

			tbody.appendChild(tr);
			i+=1;
		}
		var container = elementReference.getElementsByClassName('st-wrap')[0];
		var tableContainer = document.createElement('div');
		var scrollableContainer = tableContainer.cloneNode(true);
		var pinnedContainer = tableContainer.cloneNode(true);
		tableContainer.className = 'st-table-wrap';
		scrollableContainer.className = 'st-scrollable';
		pinnedContainer.className = 'st-pinned';
		var cloned = table.cloneNode(true);
		scrollableContainer.appendChild(table);
		pinnedContainer.appendChild(cloned);
		tableContainer.appendChild(scrollableContainer);
		tableContainer.appendChild(pinnedContainer);
		if(swipeReference !== undefined){
			swipeReference.prepareForAddition(tableContainer);
		}
		container.appendChild(tableContainer);
		tableDone();
	};

	/**
	 *  Increments doneTables and executes if equal to totalTables.
	 *  Checks if mySwipe is made and if it isn't,
	 *  makes it and gives arguments to mySwipe.
	 *  If it's the first table to be finished,
	 *  sets up the next one to be fetched and placed,
	 *  also updates the header.
	 *
	 *  If mySwipe was already made, calls mySwipe.setup.
	 */
	var tableDone = function(){
		doneTables += 1;
		if (doneTables === totalTables){
			if(swipeReference === undefined){
				/* global Swipe */
				var doNextPage = nextPage.bind(this);
				var doUpdateHeader = updateHeader.bind(this);
				swipeReference = new Swipe(elementReference,{
					continuous:false,
					callback: function(currentIndex, element){
						if (currentIndex === element.parentNode.childNodes.length - 1){
							console.log("fetching next item");
							doNextPage();
						}
					},
					transitionEnd: function(currentIndex, element){
						doUpdateHeader(element);
					}
				});
				if(doneTables === 1){
					nextPage();
					updateHeader(document.getElementsByClassName('st-table-wrap')[0]);
				}
				/* global -Swipe */
			}
			else {
				swipeReference.setup();
			}
		}
	};

	/**
	 * Gets position from mySwipe.
	 *
	 * Calls createTable(), passes it to makeRequest.
	 */
	var nextPage = function(){
		var pos = swipeReference.getPos() + 1;

		var table = createTable();
		if(sortColumn === undefined){
			makeRequest({
				table: table ,
				server: dataProvider,
				page: pos + 1,
				timestamp: 10
			});
		}
		else{
			makeRequest({
				table: table,
				server: dataProvider,
				page: pos + 1,
				sortField: sortColumn,
				sortAsc: sortAscending
			});
		}
	};

	/**
	 * Copy the given element,
	 * paste it in the header container and attach onscroll listener,
	 * execute updateScroll calls when appropriate to update positions.
	 * @param element
	 */
	var updateHeader = function(element){
		var copy = element.cloneNode(true);
		var header = elementReference.getElementsByClassName('st-header')[0];
		header.innerHTML = '';
		copy.removeAttribute('style');
		copy.removeAttribute('data-index');
		header.appendChild(copy);

		var scrollable = header.getElementsByClassName('st-scrollable')[0];
		if (scrollable){
			var doSetScrollPosition = updateScroll.setPosition.bind(this);
			var doUpdateScrollables = updateScroll.updateScrollables.bind(this);
			scrollable.onscroll = function(){
				console.log("Scrolling is happening!");
				doSetScrollPosition(this.scrollLeft);
				doUpdateScrollables();
			};

			var pos = updateScroll.getPosition();
			if (pos !== undefined){
				scrollable.scrollLeft = pos;
				updateScroll.updateScrollables();
			}
			else{
				updateScroll.setPosition(0);
			}
		}
	};

	/**
	 * Exposes scroll positions to the outside
	 * and function to update all scrollables.
	 */
	var updateScroll = (function(){
		var position;
		console.log("Entered updateScroll");

		return {
			getPosition : function(){
				console.log("Getting position " + position);
				return position;
			},
			setPosition : function(newPos){
				console.log("Setting position " + newPos);
				position = newPos;
			},
			updateScrollables : function(){
				console.log("Updating scrollables");
				var targets = elementReference.getElementsByClassName('st-wrap')[0].getElementsByClassName('st-scrollable');
				var i = 0;
				for(i;i<targets.length;i+=1){
					targets[i].scrollLeft = position;
				}
			}
		};
	}());

	var swipeFunc = {
		next : function(){
			if (swipeReference !== undefined)
			{
				swipeReference.next();
			}
		},
		prev : function(){
			if (swipeReference !== undefined)
			{
				swipeReference.prev();
			}
		}
	};

	//=== Logic ===
	init();

	var dataTable = createTable();
	console.log("dataProvider === " + dataProvider);
	makeRequest({
		table: dataTable,
		server: dataProvider
	});
	updateHeader(dataTable);

	var methods = {
		nextPage : nextPage,
		updateHeader : updateHeader,
		getScrollPosition : updateScroll.getPosition,
		setScrollPosition : updateScroll.setPosition,
		updateScrollables : updateScroll.updateScrollables,
		next : swipeFunc.next,
		prev : swipeFunc.prev
	};

	return methods;
};
/* exported SwipeTable */
<commit_msg>Expose test object to test inner functions<commit_after>var SwipeTable = function(dataProviderUrl, tableKeys, elem){
	"use strict";
	/**
	 * Config
	 *
	 * Use the following format to call SwipeTable
	 *----------------------------------------
	 *	(function(root){
	 *
   *		var restApiUrl = '/api';
   *		var keys = [
   *		    "id", // this is the 'pinned' column
   *		    "time",
   *		    "time2",
   *		    "location",
   *		    "location2"
   *		];
   *		var stElem = document.getElementsByClassName('swipe-table')[0];
	 *
   *		root.SwipeTable = new SwipeTable(restApiUrl, keys, stElem);
	 *
   *	}(this));
	 *----------------------------------------
	 */

	//=== Variables ===
	var dataProvider     = dataProviderUrl;
	var keys             = tableKeys;
	var elementReference = elem;
	var swipeReference;

	//TODO: configure table class height via call or test
	var tableClass = 'table';
	var rowHeight = 39;
	tableClass += ' table-condensed'; rowHeight = 33;

	var doneTables    = 0;
	var totalTables   = 0;
	var pageSize      = 1;
	var sortAscending = true;
	var sortColumn;

	//=== Functions ===

	/**
	 * Fetch the viewport size and set the
	 * pageSize(num of rows) based on rowHeight
	 */
	var init = function(){
		// responsejs.com/labs/dimensions/
		var matchMedia = window.matchMedia || window.msMatchMedia;
		var viewportH = (function(win, docElem, mM) {
			var client = docElem.clientHeight,
			    inner = win.innerHeight;
			if (mM && client < inner && true === mM('(min-height:' + inner + 'px)').matches){
				return win.innerHeight;
			}
			else{
				return docElem.clientHeight;
			}
		}(window, document.documentElement, matchMedia));

		// Remove one rowHeight for the header;
		viewportH -= rowHeight;
		pageSize = Math.floor(viewportH / rowHeight);
	};

	/**
	 * Takes an object that contains:
	 * REQ* server: address of the API
	 * REQ* pageSize
	 * REQ* table: container to attach the results to
	 *    * page
	 *    * timestamp
	 *    * sortField
	 *    * sortAsc
	 * Items marked with REQ are required or the request will fail.
	 *
	 * Fetches according to parameters given
	 * and gives results + table to parseResponse.
	 * @param {Object}queries Object containing quieries
	 */
	var makeRequest = function (queries){
		if(!queries.server){
			console.log("No server provided to makeRequest, not making request");
			return;
		}
		console.log("Making request to "+ queries.server+ ".");
		if(queries.timestamp){
			if(queries.sortField || queries.sortAsc){
				if(!queries.sortField || !queries.sortAsc){
					console.log("Missing sort parameter");
					return;
				}

				if(queries.page){
					executeRequest("GET",
					                queries.server +
					                  "?p=" + queries.page +
					                  "&ps=" + pageSize +
					                  "&ts=" + queries.timestamp +
					                  "&sort[field]=" + queries.sortField +
					                  "&sort[asc]=" + queries.sortAsc,
					                queries.table);
				}
				else{
					executeRequest("GET",
					                queries.server +
					                  "?ps=" + pageSize +
					                  "&ts=" + queries.timestamp +
					                  "&sort[field]=" + queries.sortField +
					                  "&sort[asc]=" + queries.sortAsc,
					                queries.table);
				}
			}
			else{
				if(!queries.page){
					console.log("Missing page, not according to spec.");
					return;
				}
				// No sorting, must be page request
				executeRequest("GET",
				                queries.server +
				                  "?p=" + queries.page +
				                  "&ps=" + pageSize +
				                  "&ts=" + queries.timestamp,
				                queries.table);
			}
		}
		else{
			// No timestamp, fresh request
			executeRequest("GET",
			                queries.server +
			                  "?ps=" + pageSize,
			                queries.table);
		}
	};

	/**
	 * Executes a request with method, parameters and the table give to next function
	 * @param  {String} method Valid HTTP method eg. GET, POST
	 * @param  {String} url Complete url string (server + parameters)
	 * @param  {Object} table Partial table object
	 */
	var executeRequest = function(method, url, table){
		var r = new XMLHttpRequest();
		r.open(method, url, true);
		r.onreadystatechange = function(){
			if(r.readyState !== 4 || r.status !== 200){
				console.log("Request went sour");
				return;
			}
			console.log("Request successful.");
			parseResponse(table, r.responseText);
		};
		r.send(null);
	};

	// parseResponse(table, response)
	//  Calls fillTable with table and JSON.parse
	//TODO: Work parseResponse into appropriate function
	var parseResponse = function(table, response){
		console.log("Parsing response.");
		fillTable(table, JSON.parse(response));
	};

	/**
	 * Creates a table with appropriate headers(thead).
	 *
	 * Increments totalTables.
	 *
	 * Returns a partial table.
	 */
	var createTable = (function(){
		console.log("Creating Table.");
		var tableInstance;
		return function(){
			if(tableInstance){
				console.log("Giving table instance");
				totalTables += 1;
				var copy = tableInstance.cloneNode(true);
				return copy;
			}
			else{
				console.log("Creating new Table");
				var i = 0;
				var table = document.createElement("table");
				table.className = tableClass;
				var thead = document.createElement("thead");
				var tr = document.createElement("tr");

				for (i; i < keys.length; i+=1){
					var th = document.createElement("th");
					th.appendChild(document.createTextNode(keys[i]));
					tr.appendChild(th);
				}

				thead.appendChild(tr);
				table.appendChild(thead);
				console.log("Table created.");
				tableInstance = table.cloneNode(true);
				totalTables += 1;
				return table;
			}
		};
	}());

	/**
	 * Goes through dataSet and attaches rows to
	 * the table reference, then attaches the
	 * table to the container in the DOM.
	 *
	 * Calls tableDone() when done.
	 * @param table Partial table to fill
	 * @param dataSet Parsed data to fill with
	 */
	var fillTable = function(table, dataSet){
		console.log("Filling table.");

		var numRows = dataSet.length;
		if(numRows === 0){
			return;
		}

		var tbody = document.createElement("tbody");
		table.appendChild(tbody);

		var i = 0;
		while (i < numRows){
			var col = dataSet[i];
			var tr = document.createElement("tr");
			var colCells = Object.keys(col).length;

			var j = 0;
			while (j < colCells)
			{
				var td = document.createElement("td");
				td.appendChild(document.createTextNode(col[keys[j]]));
				tr.appendChild(td);
				j+=1;
			}

			tbody.appendChild(tr);
			i+=1;
		}
		var container = elementReference.getElementsByClassName('st-wrap')[0];
		var tableContainer = document.createElement('div');
		var scrollableContainer = tableContainer.cloneNode(true);
		var pinnedContainer = tableContainer.cloneNode(true);
		tableContainer.className = 'st-table-wrap';
		scrollableContainer.className = 'st-scrollable';
		pinnedContainer.className = 'st-pinned';
		var cloned = table.cloneNode(true);
		scrollableContainer.appendChild(table);
		pinnedContainer.appendChild(cloned);
		tableContainer.appendChild(scrollableContainer);
		tableContainer.appendChild(pinnedContainer);
		if(swipeReference !== undefined){
			swipeReference.prepareForAddition(tableContainer);
		}
		container.appendChild(tableContainer);
		tableDone();
	};

	/**
	 *  Increments doneTables and executes if equal to totalTables.
	 *  Checks if mySwipe is made and if it isn't,
	 *  makes it and gives arguments to mySwipe.
	 *  If it's the first table to be finished,
	 *  sets up the next one to be fetched and placed,
	 *  also updates the header.
	 *
	 *  If mySwipe was already made, calls mySwipe.setup.
	 */
	var tableDone = function(){
		doneTables += 1;
		if (doneTables === totalTables){
			if(swipeReference === undefined){
				/* global Swipe */
				var doNextPage = nextPage.bind(this);
				var doUpdateHeader = updateHeader.bind(this);
				swipeReference = new Swipe(elementReference,{
					continuous:false,
					callback: function(currentIndex, element){
						if (currentIndex === element.parentNode.childNodes.length - 1){
							console.log("fetching next item");
							doNextPage();
						}
					},
					transitionEnd: function(currentIndex, element){
						doUpdateHeader(element);
					}
				});
				if(doneTables === 1){
					nextPage();
					updateHeader(document.getElementsByClassName('st-table-wrap')[0]);
				}
				/* global -Swipe */
			}
			else {
				swipeReference.setup();
			}
		}
	};

	/**
	 * Gets position from mySwipe.
	 *
	 * Calls createTable(), passes it to makeRequest.
	 */
	var nextPage = function(){
		var pos = swipeReference.getPos() + 1;

		var table = createTable();
		if(sortColumn === undefined){
			makeRequest({
				table: table ,
				server: dataProvider,
				page: pos + 1,
				timestamp: 10
			});
		}
		else{
			makeRequest({
				table: table,
				server: dataProvider,
				page: pos + 1,
				sortField: sortColumn,
				sortAsc: sortAscending
			});
		}
	};

	/**
	 * Copy the given element,
	 * paste it in the header container and attach onscroll listener,
	 * execute updateScroll calls when appropriate to update positions.
	 * @param element
	 */
	var updateHeader = function(element){
		var copy = element.cloneNode(true);
		var header = elementReference.getElementsByClassName('st-header')[0];
		header.innerHTML = '';
		copy.removeAttribute('style');
		copy.removeAttribute('data-index');
		header.appendChild(copy);

		var scrollable = header.getElementsByClassName('st-scrollable')[0];
		if (scrollable){
			var doSetScrollPosition = updateScroll.setPosition.bind(this);
			var doUpdateScrollables = updateScroll.updateScrollables.bind(this);
			scrollable.onscroll = function(){
				console.log("Scrolling is happening!");
				doSetScrollPosition(this.scrollLeft);
				doUpdateScrollables();
			};

			var pos = updateScroll.getPosition();
			if (pos !== undefined){
				scrollable.scrollLeft = pos;
				updateScroll.updateScrollables();
			}
			else{
				updateScroll.setPosition(0);
			}
		}
	};

	/**
	 * Exposes scroll positions to the outside
	 * and function to update all scrollables.
	 */
	var updateScroll = (function(){
		var position;
		console.log("Entered updateScroll");

		return {
			getPosition : function(){
				console.log("Getting position " + position);
				return position;
			},
			setPosition : function(newPos){
				console.log("Setting position " + newPos);
				position = newPos;
			},
			updateScrollables : function(){
				console.log("Updating scrollables");
				var targets = elementReference.getElementsByClassName('st-wrap')[0].getElementsByClassName('st-scrollable');
				var i = 0;
				for(i;i<targets.length;i+=1){
					targets[i].scrollLeft = position;
				}
			}
		};
	}());

	var swipeFunc = {
		next : function(){
			if (swipeReference !== undefined)
			{
				swipeReference.next();
			}
		},
		prev : function(){
			if (swipeReference !== undefined)
			{
				swipeReference.prev();
			}
		}
	};

	//=== Logic ===
	init();

	var dataTable = createTable();
	console.log("dataProvider === " + dataProvider);
	makeRequest({
		table: dataTable,
		server: dataProvider
	});
	updateHeader(dataTable);

	var methods = {
		// Expose test object to test inner functions
		test : {
			init : init,
			makeRequest : makeRequest,
			executeRequest :executeRequest,
			parseResponse : parseResponse,
			createTable : createTable,
			fillTable : fillTable,
			tableDone : tableDone,
			updateHeader : updateHeader,
			updateScroll: updateScroll
		},
		nextPage : nextPage,
		updateHeader : updateHeader,
		getScrollPosition : updateScroll.getPosition,
		setScrollPosition : updateScroll.setPosition,
		updateScrollables : updateScroll.updateScrollables,
		next : swipeFunc.next,
		prev : swipeFunc.prev
	};

	return methods;
};
/* exported SwipeTable */
<|endoftext|>


<commit_before>// (C) Copyright 2015 Hewlett Packard Enterprise Development LP

// Functions to manage selection via both child index and a specific class name.

// Ensures it is an array.
function normalizeIndexes (selectedIndexes) {
  let result;
  if (undefined === selectedIndexes || null === selectedIndexes) {
    result = [];
  } else if (typeof selectedIndexes === 'number') {
    result = [selectedIndexes];
  } else {
    result = selectedIndexes;
  }
  return result;
}

// Clears any selected items
// options: {containerElement: , selectedClass: }
function clearClass (options) {
  if (options && options.containerElement) {
    const items = options.containerElement
      .querySelectorAll("." + options.selectedClass);
    for (let i = 0; i < items.length; i++) {
      items[i].classList.remove(options.selectedClass);
    }
  }
}

// Sets the selectedClass on all children whose index is in selectedIndexes.
// options: {containerElement: , childSelector: , selectedClass: ,
//    selectedIndexes: []}
function setClassFromIndexes (options) {
  clearClass(options);
  if (options && options.containerElement && options.selectedIndexes) {
    const items = options.containerElement
      .querySelectorAll(options.childSelector);
    options.selectedIndexes.forEach((index) => {
      if (items[index]) {
        items[index].classList.add(options.selectedClass);
      }
    });
  }
}

// Gets the selected selectedClass on all children whose index is in
// selectedIndexes.
// options: {containerElement: , childSelector: , selectedClass: }
function getIndexesFromClass (options) {
  const items = options.containerElement
    .querySelectorAll(options.childSelector);
  let selectedIndexes = [];
  for (let i = 0; i < items.length; i++) {
    if (items[i].classList.contains(options.selectedClass)) {
      selectedIndexes.push(i);
    }
  }
  return selectedIndexes;
}

// Returns a new selectedIndexes array with the latest selected indexes
// options: {containerElement: , childSelector: , //selectedClass: ,
//   multiSelect: true|false, priorSelectedIndexes: []}
function onClick (event, options) {

  // Go up the DOM tree until we match the childSelector
  let item = event.target;
  var matchFunction =
    item.matches || item.matchesElement || item.msMatchesSelector;
  while (matchFunction && item &&
    !matchFunction.bind(item, options.childSelector)()) {
    item = item.parentNode;
    matchFunction =
      item.matches || item.matchesElement || item.msMatchesSelector;
  }

  // determine the index of the clicked element
  let indexInContainer = 0;
  let previousItem = item.previousSibling;
  while (previousItem != null) {
    previousItem = previousItem.previousSibling;
    indexInContainer += 1;
  }

  let selectedIndexes; // what will be returned

  if (! event.ctrlKey && ! event.metaKey && ! event.shiftKey) {

    selectedIndexes = [indexInContainer];

  } else {
    // was it selected?
    let indexInPrior = options.priorSelectedIndexes.indexOf(indexInContainer);

    if (! options.multiSelect) {

      if (-1 !== indexInPrior && (event.ctrlKey || event.metaKey)) {
        selectedIndexes = [];
      } else {
        selectedIndexes = options.priorSelectedIndexes;
      }

    } else { // multi-select

      // make a copy of the prior list so we can modify it
      selectedIndexes = options.priorSelectedIndexes.slice(0);

      if (event.shiftKey) {

        // select from nearest selected item to the currently selected item
        let closestIndex = -1;
        selectedIndexes.forEach(function (selectIndex, arrayIndex) {
          if (-1 === closestIndex) {
            closestIndex = selectIndex;
          } else if (Math.abs(indexInContainer - selectIndex) <
            Math.abs(indexInContainer - closestIndex)) {
            closestIndex = selectIndex;
          }
        });

        for (var i = indexInContainer; i !== closestIndex; ) {
          selectedIndexes.push(i);
          if (closestIndex < indexInContainer) {
            i -= 1;
          } else {
            i += 1;
          }
        }

        // Remove text selection. This often happens when shift multi-selecting
        window.getSelection().removeAllRanges();

      } else {
        // toggle
        if (-1 === indexInPrior) {
          selectedIndexes.push(indexInContainer);
        } else {
          selectedIndexes.splice(indexInPrior, 1);
        }
      }
    }
  }

  return selectedIndexes;
}

export default {
  normalizeIndexes: normalizeIndexes,
  clearClass: clearClass,
  getIndexesFromClass: getIndexesFromClass,
  setClassFromIndexes: setClassFromIndexes,
  onClick: onClick
};
<commit_msg>Selection: Multi-select shift click to deselect<commit_after>// (C) Copyright 2015 Hewlett Packard Enterprise Development LP

// Functions to manage selection via both child index and a specific class name.

// Ensures it is an array.
function normalizeIndexes (selectedIndexes) {
  let result;
  if (undefined === selectedIndexes || null === selectedIndexes) {
    result = [];
  } else if (typeof selectedIndexes === 'number') {
    result = [selectedIndexes];
  } else {
    result = selectedIndexes;
  }
  return result;
}

// Clears any selected items
// options: {containerElement: , selectedClass: }
function clearClass (options) {
  if (options && options.containerElement) {
    const items = options.containerElement
      .querySelectorAll("." + options.selectedClass);
    for (let i = 0; i < items.length; i++) {
      items[i].classList.remove(options.selectedClass);
    }
  }
}

// Sets the selectedClass on all children whose index is in selectedIndexes.
// options: {containerElement: , childSelector: , selectedClass: ,
//    selectedIndexes: []}
function setClassFromIndexes (options) {
  clearClass(options);
  if (options && options.containerElement && options.selectedIndexes) {
    const items = options.containerElement
      .querySelectorAll(options.childSelector);
    options.selectedIndexes.forEach((index) => {
      if (items[index]) {
        items[index].classList.add(options.selectedClass);
      }
    });
  }
}

// Gets the selected selectedClass on all children whose index is in
// selectedIndexes.
// options: {containerElement: , childSelector: , selectedClass: }
function getIndexesFromClass (options) {
  const items = options.containerElement
    .querySelectorAll(options.childSelector);
  let selectedIndexes = [];
  for (let i = 0; i < items.length; i++) {
    if (items[i].classList.contains(options.selectedClass)) {
      selectedIndexes.push(i);
    }
  }
  return selectedIndexes;
}

// Returns a new selectedIndexes array with the latest selected indexes
// options: {containerElement: , childSelector: , //selectedClass: ,
//   multiSelect: true|false, priorSelectedIndexes: []}
function onClick (event, options) {

  // Go up the DOM tree until we match the childSelector
  let item = event.target;
  var matchFunction =
    item.matches || item.matchesElement || item.msMatchesSelector;
  while (matchFunction && item &&
    !matchFunction.bind(item, options.childSelector)()) {
    item = item.parentNode;
    matchFunction =
      item.matches || item.matchesElement || item.msMatchesSelector;
  }

  // determine the index of the clicked element
  let indexInContainer = 0;
  let previousItem = item.previousSibling;
  while (previousItem != null) {
    previousItem = previousItem.previousSibling;
    indexInContainer += 1;
  }

  let selectedIndexes; // what will be returned

  if (! event.ctrlKey && ! event.metaKey && ! event.shiftKey) {

    selectedIndexes = [indexInContainer];

  } else {
    // was it selected?
    let indexInPrior = options.priorSelectedIndexes.indexOf(indexInContainer);

    if (! options.multiSelect) {

      if (-1 !== indexInPrior && (event.ctrlKey || event.metaKey)) {
        selectedIndexes = [];
      } else {
        selectedIndexes = options.priorSelectedIndexes;
      }

    } else { // multi-select

      // make a copy of the prior list so we can modify it
      selectedIndexes = options.priorSelectedIndexes.slice(0);

      if (event.shiftKey) {

        // select from nearest selected item to the currently selected item
        let closestIndex = -1;
        selectedIndexes.forEach(function (selectIndex, arrayIndex) {
          if (-1 === closestIndex) {
            closestIndex = selectIndex;
          } else if (Math.abs(indexInContainer - selectIndex) <
            Math.abs(indexInContainer - closestIndex)) {
            closestIndex = selectIndex;
          }
        });

        for (var i = indexInContainer; i !== closestIndex; ) {
          selectedIndexes.push(i);
          if (closestIndex < indexInContainer) {
            i -= 1;
          } else {
            i += 1;
          }
        }

        if (indexInPrior > -1) {
          selectedIndexes.splice(indexInPrior, 1);
        }

        // Remove text selection. This often happens when shift multi-selecting
        window.getSelection().removeAllRanges();

      } else {
        // toggle
        if (-1 === indexInPrior) {
          selectedIndexes.push(indexInContainer);
        } else {
          selectedIndexes.splice(indexInPrior, 1);
        }
      }
    }
  }

  return selectedIndexes;
}

export default {
  normalizeIndexes: normalizeIndexes,
  clearClass: clearClass,
  getIndexesFromClass: getIndexesFromClass,
  setClassFromIndexes: setClassFromIndexes,
  onClick: onClick
};
<|endoftext|>