/* * This notice must be untouched at all times. * * visibleIf.js - a cross browser form field manager that hides and shows * form fields depending on the values of other form fields. * * Version 1.0 released Feb 21, 2009 * Version 2.0 (this release) released June 20, 2010. Features new * rules engine originally developed for HTML5Widgets.js and HTML5 custom * data- attribute support. * * Written by: Zoltan Hawryluk. * * Latest release available at http://www.useragentman.com/ * * released under the MIT License: * http://www.opensource.org/licenses/mit-license.php */ var visibleIf = new function(){ var me = this; var formInputCache = new Array(); var changedInput = null; var visibleIfNodes; var mandatoryNodes; var varRe = /\s([a-zA-Z][a-zA-Z0-9\.]*)\s/g; var operatorRe = /\s*(~|!=|==|>={0,1}|<={0,1})\s*/g; var leftBkRe = /\(/g; var rightBkRe = /\)/g; var reRe = /~ \"([^\"]*)\"/g; var equalsRe = / == /g; var quotedStringRe = /"[^"]*"/g; var quotedStringOneOnlyRe = /"[^"]*"/; var placeHolderString = '_pLaCeHoLdEr_'; var placeHolderRe = new RegExp(placeHolderString); var nodesWithEventsAttached = new Array(); var inputsToClear; var req = null; var nameCounter = 0; var CSSHelpers, StringHelpers, XMLHelpers, DOMHelpers; /* * Things to look into: * * I don't think you need the options.isPageLoad checks inside * setFormElementsInside() since we are setting doClear accordingly. */ me.init = function(reset){ if (EventHelpers.hasPageLoadHappened(arguments) && !reset) { return; } visibleIfNodes = CSSHelpers.getElementsByClassName(document, 'visibleIf'); mandatoryNodes = CSSHelpers.getElementsByClassName(document, 'mandatoryIf'); removeDisabledNodes(); me.refreshPage({ isPageLoad: true }); setMandatoryStates(); setEvents(); //strutsHelpers.populateDynamicFormElements(); } function removeDisabledNodes(){ for (var i = 0; i < visibleIfNodes.length; i++) { var node = visibleIfNodes[i]; var els = getAllFormElementsIn(node) for (var j = 0; j < els.length; j++) { var el = els[j]; el.disabled = false; } } } function refreshPageEvent(e){ me.refreshPage(); } me.refreshPage = function(options){ changedInput = this; inputsToClear = new Array(); qsSb = new StringBuffer(); for (var i = 0; i < visibleIfNodes.length; i++) { setVisibility(visibleIfNodes[i], options); } if (!(options && !options.isPageLoaded)) { for (i in inputsToClear) { var el = inputsToClear[i]; if (i != 0) { qsSb.append('&'); } qsSb.append(i).append('='); } var qs = qsSb.toString(); if (qsSb.getLength() > 0) { var url = DOMHelpers.getDatasetItem(document.body, 'visibleif-deletedataurl'); if (url) { req = XMLHelpers.getXMLHttpRequest(url, deleteRequestHandler, 'GET', qs); } } for (var i = 0; i < mandatoryNodes.length; i++) { setMandatoryStates(mandatoryNodes[i], options); } } var formNodes = document.getElementsByTagName('form'); for (var i=0; i 0) { var fieldNode = parentForm[inputVars[j]]; //var fieldNode = document.getElementById(inputVars[j]); if (fieldNode != null) { if (fieldNode.nodeName != "SELECT" && fieldNode.length) { for (var k = 0; k < fieldNode.length; k++) { EventHelpers.addEvent(fieldNode[k], 'click', refreshPageEvent); } } else { EventHelpers.addEvent(fieldNode, 'change', refreshPageEvent); } if (fieldNode.type == 'text') { EventHelpers.addEvent(fieldNode, 'change', refreshPageEvent); } if (fieldNode.type == 'checkbox') { EventHelpers.addEvent(fieldNode, 'click', refreshPageEvent); } } } } } } } } } function updateVisibilityProperties(formNode){ var fields = formNode.elements; for (var i = 0; i < fields.length; i++) { var field = fields[i] if (!isVisible(field) && !CSSHelpers.isMemberOfClass(field, 'visibleIf-submitIfInvisible')) { CSSHelpers.addClass(field, 'visibleIf-notSubmitted'); field.disabled = true; } else { CSSHelpers.removeClass(field, 'visibleIf-notSubmitted'); field.disabled = false; } } } function formSubmitEvent(e){ if (CSSHelpers.isMemberOfClass(this, 'visibleIf-submitInvisibleData')) { return; } updateVisibilityProperties(this); } function isVisible(node){ return node.offsetWidth != 0; } function setVisibility(node, options){ var rule = getRule(node, 'visibleIf-rule') if (rule == null) { throw "There is no rule for with the node with the following HTML:" + XMLHelpers.getOuterXML(node); } var isRuleTrue = evaluateRule(DOMHelpers.getAncestorByTagName(node, 'form'), rule); if (isRuleTrue) { CSSHelpers.addClass(node, 'visibleIf-visible'); setFormElementsInside(node, false, options); } else { CSSHelpers.removeClass(node, 'visibleIf-visible'); if (options && options.isPageLoad) { setFormElementsInside(node, false, options); } else { setFormElementsInside(node, true, options); } } } function setMandatoryState(node){ var rule = getRule(node, 'mandatoryIf-rule') if (rule == null) { throw "There is no rule for with the node with the following HTML:" + XMLHelpers.getOuterXML(node); } var isRuleTrue = evaluateRule(DOMHelpers.getAncestorByTagName(node, 'form'), getRule(node, rule)); if (isRuleTrue) { CSSHelpers.addClass(node, 'mandatoryIf-mandatory'); //setFormElementsInside(node, false); } else { CSSHelpers.removeClass(node, 'mandatoryIf-mandatory'); //setFormElementsInside(node, true); } } function evaluateRule(parentForm, rule){ //node, ruleNode) { //var rule = getRuleString(ruleNode); if (rule == "") { return true; } else { if (rule != null) { //var parentForm = DOMHelpers.getAncestorByTagName(node, 'form'); if (!parentForm) { throw "Error: the rule " + rule + " is not attached to a form." } var stringToEval; var formElem = parentForm[rule.name]; // first, replace all quoted strings with placeholders: var diddledRule = rule.replace(quotedStringRe, placeHolderString); // next, grab all those quoted strings var quotedVals = rule.match(quotedStringRe); var formId = parentForm.id; if (!formId) { formId = 'visibleIf-form' + nameCounter; parentForm.id = formId; nameCounter++; } // now replace all variables with javascript form element values stringToEval = diddledRule.replace(leftBkRe, '( ').replace(rightBkRe, ' )').replace(varRe, 'getFieldValue(document.getElementById("' + formId + '")["$1"]) ').replace(reRe, '.match(/$1/)'); // now, replace placeholders back to the original strings. if (quotedVals) { for (var i = 0; i < quotedVals.length; i++) { stringToEval = stringToEval.replace(placeHolderRe, quotedVals[i]); } } try { if (eval(stringToEval)) { return true; } else { return false; } } catch (ex) { //alert('Bad equation: ' + stringToEval) return false; } } } } function setFormElementsInside(node, doClear, options){ if (!options) { options = {}; } var formElements = getAllFormElementsIn(node); for (var i = 0; i < formElements.length; i++) { var el = formElements[i]; /* Let's disable them so they aren't submitted if (!CSSHelpers.isMemberOfClass(node, 'visibleIf-ignoreDisableAttr')) { el.disabled = doClear ? "disabled" : ""; if (CSSHelpers.isMemberOfClass(el, 'streetName')) { jslog.debug(el.disabled); } }*/ if (el != changedInput) { switch (el.nodeName) { case "INPUT": switch (el.type) { case "checkbox": if (el.checked) { if (doClear) { if (!CSSHelpers.isMemberOfClass(el, 'visibleIf-doNotReset')) { // cache the value formCache.setValue(el.name, el.value); el.checked = false; addToInputToClear(el); } } else if (formCache.getValue(el.name) == el.value) { //el.checked = true; } } break; case "radio": if (doClear) { if (!CSSHelpers.isMemberOfClass(el, 'visibleIf-doNotReset')) { // cache the value if (el.checked) { formCache.setValue(el.name, el.value); el.checked = false; } addToInputToClear(el); } } else if (formCache.getValue(el.name) == el.value) { //el.checked = true; } break; case "file": // do nothing to avoid a security error. break; case "hidden": // don't do anything break; default: if (doClear) { /* * The following code is for use with a seperate * JavaScript library, fileChanger.js. If * it's a fileChanger widget, we need to do an Ajax call */ if (CSSHelpers.isMemberOfClass(el, 'fileList_fileDisplay')) { CSSHelpers.removeClass(el, 'fileList_fileDisplay'); el.disabled = false; el.name = el.name.replace("_ignore", "") var html = XMLHelpers.getOuterXML(el).replace(/text/, 'file'); el.parentNode.innerHTML = html; // insert ajax call to delete file here. url = config.getScriptedValue('visibleIf.urls.deleteFiles', { files: StringHelpers.urlencode(el.value), formProperty: el.name }) req = XMLHelpers.getXMLHttpRequest(url, deleteRequestHandler); } // cache the value formCache.setValue(el.name, el.value); if (options.isPageLoad) { el.value = DOMHelpers.getAttributeValue(el, 'value'); if (el.value == "null") { el.value = ""; } } else if (!CSSHelpers.isMemberOfClass(el, 'visibleIf-doNotReset')) { el.value = ""; addToInputToClear(el); } } else { //el.value = formCache.getValue(el.name); } break; } break; case "TEXTAREA": if (doClear) { // cache the value formCache.setValue(el.name, el.value); if (options.isPageLoad) { //el.value = DOMHelpers.getAttributeValue(el, 'value'); } else if (!CSSHelpers.isMemberOfClass(el, 'visibleIf-doNotReset')) { el.value = ""; addToInputToClear(el); } } else { //el.value = formCache.getValue(el.name); } break; case "SELECT": if (doClear) { if (!CSSHelpers.isMemberOfClass(el, 'visibleIf-doNotReset')) { // cache the value formCache.setValue(el.name, el.selectedIndex); // TODO: should this be 0 or -1? el.selectedIndex = 0; addToInputToClear(el); } el.disabled = true; } else { //el.selectedValue = formCache.getValue(el.name); el.disabled = false; } break; } } } } function addToInputToClear(el){ if (!inputsToClear[el.name]) { inputsToClear[el.name] = el; } } function deleteRequestHandler(){ if (!req) { return; } // only if req shows "complete" if (req.readyState == ReadyState.COMPLETED) { // only if "OK" //DebugHelpers.log(req.getAllResponseHeaders()); if (req.status == HttpCode.OK || req.status == HttpCode.LOCAL_OK) { // whatever //jslog.debug('Deleted Successfully') } else { throw "Something bad happened. HTTP Status: " + req.status; } } } function getInputVars(rule){ rule = rule.replace(leftBkRe, '( ').replace(rightBkRe, ' )'); var vars = rule.match(varRe); if (vars == null) { return new Array(); } for (var i = 0; i < vars.length; i++) { vars[i] = vars[i].trim(); } return vars; } function getFieldValue(formElementNode){ var r = ""; var type; type = formElementNode.type if (!type) { if (formElementNode.length) type = formElementNode[0].type; } switch (type) { case 'text': case 'hidden': case 'password': case 'textarea': case 'select-one': r = formElementNode.value; case 'checkbox': if (formElementNode.checked) { r = formElementNode.value; } break; case 'radio': for (var i = 0; i < formElementNode.length; i++) { if (formElementNode[i].checked) { r = formElementNode[i].value; break; } } } if (formElementNode.length) { for (var i = 0; i < formElementNode.length; i++) { if (formElementNode[i].checked) { r = formElementNode[i].value; } } } return r.replace('\n', '').replace(' ', ''); } function getAllFormElementsIn(node){ if (!node) { node = document; } var r = new Array(); var elems = { inputs: node.getElementsByTagName('input'), selects: node.getElementsByTagName('select'), textareas: node.getElementsByTagName('textarea') }; for (var i in elems) { var elem = elems[i]; for (j = 0; j < elem.length; j++) { r.push(elem[j]); } } return r; } /* Helper routines */ if (window.CSSHelpers) { CSSHelpers = window.CSSHelpers; } else { CSSHelpers = new function(){ var me = this; var blankRe = new RegExp('\\s'); /** * Generates a regular expression string that can be used to detect a class name * in a tag's class attribute. It is used by a few methods, so I * centralized it. * * @param {String} className - a name of a CSS class. */ function getClassReString(className){ return '\\s' + className + '\\s|^' + className + '\\s|\\s' + className + '$|' + '^' + className + '$'; } function getClassPrefixReString(className){ return '\\s' + className + '-[0-9a-zA-Z_]+\\s|^' + className + '[0-9a-zA-Z_]+\\s|\\s' + className + '[0-9a-zA-Z_]+$|' + '^' + className + '[0-9a-zA-Z_]+$'; } /** * Make an HTML object be a member of a certain class. * * @param {Object} obj - an HTML object * @param {String} className - a CSS class name. */ me.addClass = function(obj, className){ if (blankRe.test(className)) { return; } // only add class if the object is not a member of it yet. if (!me.isMemberOfClass(obj, className)) { obj.className += " " + className; } } /** * Make an HTML object *not* be a member of a certain class. * * @param {Object} obj - an HTML object * @param {Object} className - a CSS class name. */ me.removeClass = function(obj, className){ if (blankRe.test(className)) { return; } var re = new RegExp(getClassReString(className), "g"); var oldClassName = obj.className; if (obj.className) { obj.className = oldClassName.replace(re, ''); } } /** * Given an HTML element, find all child nodes of a specific class. * * With ideas from Jonathan Snook * (http://snook.ca/archives/javascript/your_favourite_1/) * Since this was presented within a post on this site, it is for the * public domain according to the site's copyright statement. * * @param {Object} obj - an HTML element. If you want to search a whole document, set * this to the document object. * @param {String} className - the class name of the objects to return * @return {Array} - the list of objects of class cls. */ me.getElementsByClassName = function(obj, className){ if (obj.getElementsByClassName) { return DOMHelpers.nodeListToArray(obj.getElementsByClassName(className)) } else { var a = []; var re = new RegExp(getClassReString(className)); var els = DOMHelpers.getAllDescendants(obj); for (var i = 0, j = els.length; i < j; i++) { if (re.test(els[i].className)) { a.push(els[i]); } } return a; } } /** * Determines if an HTML object is a member of a specific class. * @param {Object} obj - an HTML object. * @param {Object} className - the CSS class name. */ me.isMemberOfClass = function(obj, className){ if (blankRe.test(className)) return false; var re = new RegExp(getClassReString(className), "g"); return (re.test(obj.className)); } } } if (window.DOMHelpers) { DOMHelpers = window.DOMHelpers; } else { DOMHelpers = new function(){ var me = this; /** * Given an tag, find the first ancestor tag of a given tag name. * * @param {Object} obj - a HTML or XML tag. * @param {String} tagName - the name of the ancestor tag to find. * @return {Object} - the ancestor tag, or null if not found. */ /** * Returns all children of an element. Needed if it is necessary to do * the equivalent of getElementsByTagName('*') for IE5 for Windows. * * @param {Object} e - an HTML object. */ me.getAllDescendants = function(obj){ return obj.all ? obj.all : obj.getElementsByTagName('*'); } me.getAncestorByTagName = function(obj, tagName){ for (var node = obj.parentNode; node.nodeName.toLowerCase() != 'body'; node = node.parentNode) { if (tagName.toLowerCase() == node.nodeName.toLowerCase()) { return node; } } return null; } /** * Given an HTML or XML object, find the an attribute by name. * * @param {Object} obj - a DOM object. * @param {String} attrName - the name of an attribute inside the DOM object. * @return {Object} - the attribute object or null if there isn't one. */ me.getAttributeByName = function(obj, attrName){ var i; var attributes = obj.attributes; for (i = 0; i < attributes.length; i++) { var attr = attributes[i] if (attr.nodeName == attrName && attr.specified) { return attr; } } return null; } /** * Given an HTML or XML object, find the value of an attribute. * * @param {Object} obj - a DOM object. * @param {String} attrName - the name of an attribute inside the DOM object. * @return {String} - the value of the attribute. */ me.getAttributeValue = function(obj, attrName){ var attr = me.getAttributeByName(obj, attrName); if (attr != null) { return attr.nodeValue; } else { return null; } } me.getDatasetItem = function(obj, name){ var r = DOMHelpers.getAttributeValue(obj, 'data-' + name); if (!r) { r = DOMHelpers.getAttributeValue(obj, 'data-' + name.toLowerCase()) } return r; } /****** * Converts a DOM live node list to a static/dead array. Good when you don't * want the thing you are iterating in a for loop changing as the DOM changes. * * @param {Object} nodeList - a node list (like one returned by document.getElementsByTagName) * @return {Array} - an array of nodes. * *******/ me.nodeListToArray = function(nodeList){ var ary = []; for (var i = 0, len = nodeList.length; i < len; i++) { ary.push(nodeList[i]); } return ary; } } } if (window.StringHelpers) { StringHelpers = window.StringHelpers; } else { StringHelpers = new function(){ var me = this; me.initWhitespaceRe = /^\s\s*/; me.endWhitespaceRe = /\s\s*$/; me.whitespaceRe = /\s/; me.unentify = function(s){ return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); } me.urlencode = function(str){ return escape(str).replace('+', '%2B').replace('%20', '+').replace('*', '%2A').replace('/', '%2F').replace('@', '%40'); } } } if (window.XMLHelpers) { XMLHelpers = window.XMLHelpers; } else { XMLHelpers = new function(){ var me = this; /** * Given an XML node, return the XML inside as a string and the XML string of the node itself. * Similar to Internet Explorer's outerHTML property, except it is for XML, not HTML. * Created with information from http://www.codingforums.com/showthread.php?t=31489 * and http://www.mercurytide.co.uk/whitepapers/issues-working-with-ajax/ * * @param {Object} node - a DOM object. * @param {Object} options - a JS object containing options. To date, * the only one supported is "insertClosingTags", when set to * true, converts self closing tags, like , to . * @return {String} - the XML String inside the object. */ me.getOuterXML = function(node, options){ var r; // Internet Explorer if (node.xml) { r = node.xml; // Everyone else } else if (node.outerHTML) { r = node.outerHTML; } else if (window.XMLSerializer) { var serializer = new XMLSerializer(); var text = serializer.serializeToString(node); r = text; } else { return null; } /* * If the XML is actually HTML and you are inserting it into an HTML * document, you must use the "insertClosingTags" option, otherwise * Opera will not like you, especially if you have empty tags. */ if (options) { if (options.insertClosingTags) { r = r.replace(selfClosingTagRe, "<$1>"); } } return r; } /** * Wrapper for XMLHttpRequest Object. Grabbing data (XML and/or text) from a URL. * Grabbing data from a URL. Input is one parameter, url. It returns a request * object. Based on code from * http://www.xml.com/pub/a/2005/02/09/xml-http-request.html. IE caching problem * fix from Wikipedia article http://en.wikipedia.org/wiki/XMLHttpRequest * * @param {String} url - the URL to retrieve * @param {Function} processReqChange - the function/method to call at key events of the URL retrieval. * @param {String} method - (optional) "GET" or "POST" (default "GET") * @param {String} data - (optional) the CGI data to pass. Default null. * @param {boolean} isAsync - (optional) is this call asyncronous. Default true. * * @return {Object} a XML request object. */ me.getXMLHttpRequest = function(url, processReqChange) //, method, data, isAsync) { var argv = me.getXMLHttpRequest.arguments; var argc = me.getXMLHttpRequest.arguments.length; var httpMethod = (argc > 2) ? argv[2] : 'GET'; var data = (argc > 3) ? argv[3] : ""; var isAsync = (argc > 4) ? argv[4] : true; var req; // branch for native XMLHttpRequest object if (window.XMLHttpRequest) { req = new XMLHttpRequest(); // branch for IE/Windows ActiveX version } else if (window.ActiveXObject) { try { req = new ActiveXObject('Msxml2.XMLHTTP'); } catch (ex) { req = new ActiveXObject("Microsoft.XMLHTTP"); } // the browser doesn't support XML HttpRequest. Return null; } else { return null; } if (isAsync) { req.onreadystatechange = processReqChange; } if (httpMethod == "GET" && data != "") { url += "?" + data; } req.open(httpMethod, url, isAsync); //Fixes IE Caching problem req.setRequestHeader("If-Modified-Since", "Sat, 1 Jan 2000 00:00:00 GMT"); req.send(data); return req; } } } /* * stringBuffer - ideas from * http://www.multitask.com.au/people/dion/archives/000354.html */ function StringBuffer(){ var me = this; var buffer = []; me.append = function(string){ buffer.push(string); return me; } me.appendBuffer = function(bufferToAppend){ buffer = buffer.concat(bufferToAppend); } me.toString = function(){ return buffer.join(""); } me.getLength = function(){ return buffer.length; } me.flush = function(){ buffer.length = 0; } } /* * Adding trim method to String Object. Ideas from * http://www.faqts.com/knowledge_base/view.phtml/aid/1678/fid/1 and * http://blog.stevenlevithan.com/archives/faster-trim-javascript */ String.prototype.trim = function(){ var str = this; // The first method is faster on long strings than the second and // vice-versa. if (this.length > 6000) { str = this.replace(StringHelpers.initWhitespaceRe, ''); var i = str.length; while (StringHelpers.whitespaceRe.test(str.charAt(--i))) ; return str.slice(0, i + 1); } else { return this.replace(StringHelpers.initWhitespaceRe, '').replace(StringHelpers.endWhitespaceRe, ''); } }; } var formCache = new function(){ var me = this; var values = new Array(); me.setValue = function(name, value){ values[name] = value; } me.getValue = function(name){ if (values[name] == undefined) { return ""; } else { return values[name]; } } } EventHelpers.addPageLoadEvent('visibleIf.init');