/*
 * dependencies.js
 * smartForms library for Sundial -- dependency component
 *
 * Author: Zach van Schouwen
 * Date: 01-08-2007
 *
 * This is an interface allowing form elements to express dependencies through
 * HTML attributes, moving more JavaScript code out of view and making form
 * behavior much more maintainable.
 *
 * A new HTML attribute, "owns" is defined. This attribute contains a comma-
 * separated list of IDs of other elements, and should only be associated with
 * a checkable event--a checkbox or radio button (or anything else on which you
 * care to define .checked). It may also be used on a form option.
 *
 * On page load, elements dependent on an UNCHECKED element will be disabled,
 * and elements dependent on a CHECKED element will be visible.
 *
 * When the owner element is toggled, the child is (en/dis)abled as appropriate.
 *
 * A child may have multiple parents, in which case it waits for ALL to be
 * checked.
 *
 * We also define the "implies" attribute, which is similar -- if an element
 * with the "implies" property is checked, all of the elements it implies are
 * checked and given the "implicit" CSS class. If one of those elements is
 * unchecked, so is the implicator. Implies can only point to checkbox/radio.
 *
 * Multiple parents apply in this case as well.
 *
 * Finally, we define the "owns-together" attribute, which causes the target
 * element to be enabled if *any* of its parents are checked.
 */

/* This is called on page load. Loops through all inputs looking for
 * ownership relationships. */
function checkDependencies() {
	addElementNames();
	var elts = document.getElementsByTagName('input');
	var opts = document.getElementsByTagName('option');
	
	for	(var i = 0; i < elts.length; i++) {
		if (!isLegalOwner(elts[i])) continue;
		if	(elts[i].getAttribute('owns') ||
	          	 elts[i].getAttribute('implies'))
			setupDependencies(elts[i]);
		if	(elts[i].getAttribute('owns-together'))
			setupWeakDependencies(elts[i]);
	}

        for	(i = 0; i < opts.length; i++) {
        	if	(opts[i].getAttribute('owns'))
	        	setupDependencies(opts[i]);
	        if 	(opts[i].getAttribute('owns-together'))
	        	setupWeakDependencies(opts[i]);
	}
}

/* Attach elt to its child elements, and add a triggered action for
 * its status changes. */
function setupDependencies(elt) {
	var ownsAttr = elt.getAttribute('owns');
	var impliesAttr = elt.getAttribute('implies');
	if (!isWhitespace(ownsAttr)) {
		var kids = parseElementList(ownsAttr);
		for (var i = 0; i < kids.length; i++) {
			var kid = kids[i];
			if (!kid.dependencyOwners)
				kid.dependencyOwners = new Array();
			/* this element-value pair specifies a required value;
			* this is because radio groups get treated as a single
			* object by the DOM. */
			var pair = new Array();
			pair[0] = elt;
			pair[1] = (elt.getAttribute('type') == 'radio' ?
			           true : elt.value);
			kid.dependencyOwners.push(pair);
			checkDependencyStatus(kid);
		}
	}

	if (elt.nodeName.toLowerCase() == 'input' &&
	    !isWhitespace(impliesAttr)) {
		var implied = parseElementList(impliesAttr);
		for (var i = 0; i < implied.length; i++) {
			var kid = implied[i];
			if (!kid.implicationOwners)
				kid.implicationOwners = new Array();
			var pair = new Array();
			pair[0] = elt;
			pair[1] = (elt.getAttribute('type') == 'radio' ?
			           true : elt.value);
			kid.implicationOwners.push(pair);
			addElementEvent(kid, 'click', function() {
				checkImplicationParents(kid);
				});
			addElementEvent(kid, 'keyup', function() {
				checkImplicationParents(kid);
				});
			if (kid.nodeName.toLowerCase() == 'option') {
				addElementEvent(kid.parentNode, 'change', function() {
					checkImplicationParents(kid);
				});
				addElementEvent(kid.parentNode, 'click', function() {
					checkImplicationParents(kid);
				});
			}
			checkImplicationStatus(kid);
		}
		elt.implicationChildren = implied;
	}
	setupEvents(elt, kids, implied);
}

/* Sets up dependencies based on "owns-together", meaning that if ANY of elt's
 * parents are checked, elt is enabled. */
function setupWeakDependencies(elt) {
	var ownsAttr = elt.getAttribute('owns-together');
	if (!isWhitespace(ownsAttr)) {
		var kids = parseElementList(ownsAttr);
		for (var i = 0; i < kids.length; i++) {
			var kid = kids[i];
			if (!kid.weakDependencyOwners)
				kid.weakDependencyOwners = new Array();
			/* this element-value pair specifies a required value;
			* this is because radio groups get treated as a single
			* object by the DOM. */
			var pair = new Array();
			pair[0] = elt;
			pair[1] = (elt.getAttribute('type') == 'radio' ?
			           true : elt.value);
			kid.weakDependencyOwners.push(pair);
			checkDependencyStatus(kid);
		}
	}
	setupEvents(elt, kids, false);
}

function setupEvents(elt, kids, implied) {
	elt.checkKids = function() {
		for (var i = 0; kids && i < kids.length; i++)
			checkDependencyStatus(kids[i]);
		for (i = 0; implied && i < implied.length; i++)
			checkImplicationStatus(implied[i]);
		return true;
	};

	if (elt.nodeName.toLowerCase() == 'option') {
		/* Attach events to the SELECT */
		addElementEvent(elt.parentNode, 'click', elt.checkKids);
		addElementEvent(elt.parentNode, 'keyup', elt.checkKids);
	} else {
		addElementEvent(elt, 'click', elt.checkKids);
		addElementEvent(elt, 'keyup', elt.checkKids);
	}

	/* Siblings need to make the same checks, because elt doesn't
	 * get triggered when it's deselected. */
	if (elt.type == "radio") {
		var sibs = document.getElementsByName(elt.name);
		for (i = 0; i < sibs.length; i++) {
			addElementEvent(sibs[i], 'click', elt.checkKids);
			addElementEvent(sibs[i], 'keyup', elt.checkKids);
		}
	}
}

/* Takes a dependency pair as a parameter and returns whether it is, to our
 * satisfaction, "checked" -- that is, it's a checkbox that's checked or a
 * radio button that's set to the appropriate value. */
function checkDependencyPair(pair) {
	return !(pair[0].disabled || (!pair[0].checked && !pair[0].selected) ||
	         (pair[1] != true && pair[0].value != pair[1]));
}

/* Check each parent. */
function checkImplicationParents(elt) {
	if (!elt.implicationOwners) return;
	for (var i = 0; i < elt.implicationOwners.length; i++)
		checkImplicationParent(elt.implicationOwners[i][0]);
}

/* Triggered when one of elt's children changes -- should elt be unset? */
function checkImplicationParent(elt) {
	if (!elt.implicationChildren) return;
	for (var i = 0; i < elt.implicationChildren.length; i++) {
		if (!isBoxChecked(elt.implicationChildren[i])) {
			elt.checked = false;
			elt.selected = false;
			/* checkRelatives() will be called by the caller
			 * function -- totherwise multiple dependencies can
			 * make it impossible to uncheck an element. */
		}
	}
}

function isBoxChecked(elt) {
	if (elt.disabled) return false;
	
	if (elt.nodeName.toLowerCase() == 'option') {
		return (elt.parentNode.value == elt.value);
	}
	
	if (elt.checked) return true;
	if (elt.type == 'checkbox') return false;
	
	if (elt.selected) return true;
	return false;
}

/* Are ANY of its parents checked? If so, it HAS to be. */
function checkImplicationStatus(elt) {
	if (!elt.implicationOwners) return;
	for (var i = 0; i < elt.implicationOwners.length; i++) {
		if (checkDependencyPair(elt.implicationOwners[i])) {
			if (!containsClass(elt, "implicit"))
				elt.className += " implicit";
			elt.checked = true;
			elt.selected = true;
			return checkRelatives(elt);
		}
	}
	removeClass(elt, "implicit");
	return checkRelatives(elt);
}

/* Are its parents checked? Set it accordingly. */
function checkDependencyStatus(elt) {
	if (elt.weakDependencyOwners) {
		for (var i = 0; i < elt.weakDependencyOwners.length; i++) {
			if (checkDependencyPair(elt.weakDependencyOwners[i])) {
				elt.disabled = false;
				return checkRelatives(elt);
			}
		}
		elt.disabled = true;
		/* Still, go through and check for more stuff. */
		if (!elt.dependencyOwners) return checkRelatives(elt);
	}
	
	if (!elt.dependencyOwners) return;
	for (var i = 0; i < elt.dependencyOwners.length; i++) {
		if (!checkDependencyPair(elt.dependencyOwners[i])) {
			elt.disabled = true;
			return checkRelatives(elt);
		}
	}
	elt.disabled = false;
	return checkRelatives(elt);
}

/* If elt has kids, or radio-button siblings, update them too. */
function checkRelatives(elt) {
	if (elt.checkKids) elt.checkKids();
	if (elt.getAttribute('type') == "radio") {
		var sibs = document.getElementsByName(elt.name);
		for (var i = 0; i < sibs.length; i++) {
			sibs[i].disabled = elt.disabled;
		}
	}
	return true;
}

/* Make sure elt is allowed to have "owns" set. */
function isLegalOwner(elt) {
	if (elt.nodeName.toLowerCase() != 'input') {
		return false;
	}
	var t = elt.getAttribute('type');
	return (t == 'radio' || t == 'checkbox');
}

function parseElementList(str) {
	var elts = str.split(',');
	for (var i = 0; i < elts.length; i++) {
		var eltID = elts[i].replace(/^\s*|\s*$/g, '');
		elts[i] = document.getElementById(eltID);
		if (!elts[i]) {
			if (tempElts = document.getElementsByName(eltID))
				elts[i] = tempElts[0];	/* refers to the set */
			if (!elts[i])
				alert("Warning: element '" + eltID +
					"' not found!");
		}
	}
	return elts;
}

addOnload(checkDependencies);