Commit 63a9bd9d849d20e6ca0c73e9c82f29fd51bdcf8b
1 parent
c127b81d
Javascript necessary for rich selection of a conditional fieldset
git-svn-id: https://kt-dms.svn.sourceforge.net/svnroot/kt-dms/trunk@3760 c91229c3-7414-0410-bfa2-8a42b809f60b
Showing
1 changed file
with
306 additions
and
0 deletions
presentation/lookAndFeel/knowledgeTree/js/conditional_usage.js
0 → 100644
| 1 | +// simple event stacking. | ||
| 2 | +// i don't like Mochikit's one. | ||
| 3 | + | ||
| 4 | +function attachToElementEvent(elem, event_name, func) { | ||
| 5 | + // catch IE (grumble) | ||
| 6 | + if (elem.attachEvent) { | ||
| 7 | + elem.attachEvent('on'+event_name, func); | ||
| 8 | + } else { | ||
| 9 | + elem.addEventListener(event_name, func, false); | ||
| 10 | + } | ||
| 11 | +} | ||
| 12 | + | ||
| 13 | +function removeFromElementEvent(elem, event_name, func) { | ||
| 14 | + // catch IE (grumble) | ||
| 15 | + if (elem.detachEvent) { | ||
| 16 | + elem.detachEvent('on'+event_name, func); | ||
| 17 | + } else { | ||
| 18 | + elem.removeEventListener(event_name, func, false); | ||
| 19 | + } | ||
| 20 | +} | ||
| 21 | + | ||
| 22 | +// quick and dirty helper - find the nearest parent item matching tagName. | ||
| 23 | +// FIXME steal the klass or tagName logic from MochiK. | ||
| 24 | +// FIXME add to a core js-lib, and add some unit-tests. | ||
| 25 | +function breadcrumbFind(elem, tagName) { | ||
| 26 | + var stopTag = 'BODY'; | ||
| 27 | + var currentTag = elem.tagName; | ||
| 28 | + var currentElem = elem; | ||
| 29 | + while ((currentTag != stopTag) && (currentTag != tagName)) { | ||
| 30 | + currentElem = currentElem.parentNode; | ||
| 31 | + currentTag = currentElem.tagName; | ||
| 32 | + } | ||
| 33 | + if (currentTag == tagName) { | ||
| 34 | + return currentElem; | ||
| 35 | + } else { | ||
| 36 | + return null; | ||
| 37 | + } | ||
| 38 | +} | ||
| 39 | + | ||
| 40 | +/* Conditional Metadata Usage | ||
| 41 | + * | ||
| 42 | + * Allows the system to respond to conditional metadata events. | ||
| 43 | + */ | ||
| 44 | + | ||
| 45 | +/** | ||
| 46 | + | ||
| 47 | +== Basic process around Conditional Metadata JS/HTML interaction == | ||
| 48 | + | ||
| 49 | +The system works based on 3 concepts: | ||
| 50 | + | ||
| 51 | + 1. on the appropriate "activation" command, the entire "field" is serialised and replaced with a hidden | ||
| 52 | + input var, and a "user friendly" label. | ||
| 53 | + 2. a undo stack needs to be kept, which provides the user with a way to "un-fix" items. | ||
| 54 | + 3. When an item is activated, the system: | ||
| 55 | + (i) polls the page for fixed input-vars: this _includes_ (for example) fieldset_id, as well as later items. | ||
| 56 | + (ii) submits these to a targeturl (set as a "global var" - currently in _this_ file.) // FIXME: this needs to be programmatically settable. | ||
| 57 | + | ||
| 58 | + | ||
| 59 | + // TODO make this operate on a particular subset of the page, and be _instantiable_. (use fieldset as the "controlling component". | ||
| 60 | + // TODO lazy bind all activation handlers to ensure that the above problem is solveable. | ||
| 61 | + // TODO ensure that this functions across the required browser sets. | ||
| 62 | + // TODO verify that the entire set of "lookup" values works here: select and input seem to work. | ||
| 63 | + | ||
| 64 | +*/ | ||
| 65 | + | ||
| 66 | +var conditional_usage_undostack = new Array(); | ||
| 67 | +var conditional_usage_keys = new Array(); | ||
| 68 | + | ||
| 69 | +// grow and go. | ||
| 70 | +function getStackForFieldset(fieldset) { | ||
| 71 | + for (var i=0; i<conditional_usage_keys.length; i++) { | ||
| 72 | + if (conditional_usage_keys[i] == fieldset) { | ||
| 73 | + simpleLog('DEBUG','found undostack at keyindex '+i); | ||
| 74 | + return conditional_usage_undostack[i]; | ||
| 75 | + } | ||
| 76 | + } | ||
| 77 | + // we would have returned by now. onward, and upward. | ||
| 78 | + // i == conditional_usage_keys.length == conditional_usage_undostack.length | ||
| 79 | + conditional_usage_undostack.push(Array()); | ||
| 80 | + conditional_usage_keys.push(fieldset); | ||
| 81 | + simpleLog('DEBUG','created undostack at keyindex '+i+' for fieldset '+fieldset); | ||
| 82 | + simpleLog('DEBUG','undoStack: '+conditional_usage_undostack+'\nundoKeyStack: '+conditional_usage_keys) | ||
| 83 | + return conditional_usage_undostack[i]; // must be the "new" element, which is 1 past the old size. | ||
| 84 | +} | ||
| 85 | + | ||
| 86 | +// Stack implementation | ||
| 87 | +function pushStack(fieldset, subtree) { | ||
| 88 | + // FIXME how do I bind this to a particular fieldset object. | ||
| 89 | + // FIXME at worst, we need to use the HTMLFieldSet object as a "key" of sorts into a stack it's O(n) initially, unless we can do some other magic ... | ||
| 90 | + simpleLog('DEBUG','pushStack received: '+fieldset); | ||
| 91 | + var undostack = getStackForFieldset(fieldset); | ||
| 92 | + undostack.push(subtree); // onto the end, so it can be popped. | ||
| 93 | + simpleLog('ERROR','added item to undo stack..'); | ||
| 94 | +} | ||
| 95 | + | ||
| 96 | +function popStack(fieldset) { | ||
| 97 | + var undostack = getStackForFieldset(fieldset); | ||
| 98 | + if (undostack.length == 0) { | ||
| 99 | + return ; | ||
| 100 | + } | ||
| 101 | + var last_item = undostack.pop(); | ||
| 102 | + last_item.parentNode.removeChild(last_item); | ||
| 103 | +} | ||
| 104 | + | ||
| 105 | +/** | ||
| 106 | + - creates a replacement widget, | ||
| 107 | + - adds the _old_ widget to the correct stack. | ||
| 108 | +*/ | ||
| 109 | + | ||
| 110 | +function createFixedWidget(fieldset, widget, i_name, i_value, i_label) { | ||
| 111 | + var newWidget = DIV({'class':'widget fixed'}, | ||
| 112 | + INPUT({'type':'hidden','name':i_name, 'value':i_value,'class':'fixed'}), | ||
| 113 | + SPAN(null, 'The value for field '+i_name+' is '+i_label+' ('+i_value+')') | ||
| 114 | + ); | ||
| 115 | + swapDOM(widget, newWidget); | ||
| 116 | + pushStack(fieldset, newWidget); | ||
| 117 | + simpleLog('ERROR','conditional_usage passed in fieldset '+fieldset+' and widget '+newWidget); | ||
| 118 | +} | ||
| 119 | + | ||
| 120 | +/** handles the "update" event. | ||
| 121 | + | ||
| 122 | + needs to: | ||
| 123 | + - "replace" the contents of the widget with a "fixed" input. | ||
| 124 | + - trigger the "updateFieldset" | ||
| 125 | +*/ | ||
| 126 | + | ||
| 127 | +function handleSelectChange(fieldset, widget, select_object) { | ||
| 128 | + simpleLog('ERROR','call to stub: handleSelectChange on select with name "'+select_object.name+'"'); | ||
| 129 | + var i_name = select_object.name; | ||
| 130 | + var i_value = select_object.value; | ||
| 131 | + var i_label = scrapeText(select_object.options[select_object.selectedIndex]); | ||
| 132 | + | ||
| 133 | + createFixedWidget(fieldset, widget, i_name, i_value, i_label); | ||
| 134 | + updateFieldset(fieldset); | ||
| 135 | +} | ||
| 136 | + | ||
| 137 | +function handleRadioChange(fieldset, widget, radio_object) { | ||
| 138 | + simpleLog('ERROR','call to stub: handleRadioChange on radio with name "'+radio_object.name+'"'); | ||
| 139 | + var i_name = radio_object.name; | ||
| 140 | + var i_value = radio_object.value; | ||
| 141 | + | ||
| 142 | + | ||
| 143 | + var oLabel = breadcrumbFind(radio_object, 'LABEL'); | ||
| 144 | + if (oLabel == null) { | ||
| 145 | + simpleLog('ERROR','radiobutton ('+radio_object.name+':'+radio_object.value+') has no associated label. failing.'); | ||
| 146 | + return false; | ||
| 147 | + } else { | ||
| 148 | + var i_label = scrapeText(oLabel); | ||
| 149 | + } | ||
| 150 | + | ||
| 151 | + createFixedWidget(fieldset, widget, i_name, i_value, i_label); | ||
| 152 | + updateFieldset(fieldset); | ||
| 153 | +} | ||
| 154 | + | ||
| 155 | +/** extract all the appropriate input-vars from a given fieldset, so that it can | ||
| 156 | + be passed into a backed. Returns an array ("formKeys" => array(), "formValues" => array()) | ||
| 157 | + that can be passed to be backend. | ||
| 158 | + | ||
| 159 | + // actually, this is ONLY and issue for the "fieldset_id" form-field: | ||
| 160 | + // for the rest of them, the backend should handle this sanely (e.g. in what it sends _us_). Suspect the "best" option is to call this | ||
| 161 | + // 'fieldset_id[]' since the backend can then extract which fieldsets have been called. other vars will get converted | ||
| 162 | + // from <input type="radio" ... name="xxxx"> and <select ... name="xxxx"> to <input type="hidden" class="fixed"> | ||
| 163 | + // | ||
| 164 | +*/ | ||
| 165 | +function parseFieldsetToForm(fieldset) { | ||
| 166 | + simpleLog('ERROR','call to untested fn: parseFieldsetToForm. '); | ||
| 167 | + var formContent = new Array(); | ||
| 168 | + | ||
| 169 | + var input_vars = getElementsByTagAndClassName('input','fixed',fieldset); | ||
| 170 | + formContent["formKeys"] = new Array(); | ||
| 171 | + formContent["formValues"] = new Array(); | ||
| 172 | + | ||
| 173 | + for (var i=0; i<input_vars.length; i++) { | ||
| 174 | + var input_object = input_vars[i]; | ||
| 175 | + // don't delete the undo button. | ||
| 176 | + if (input_object.type != 'button') { | ||
| 177 | + formContent["formKeys"].push(input_object.name); | ||
| 178 | + formContent["formValues"].push(input_object.value); | ||
| 179 | + } | ||
| 180 | + } | ||
| 181 | + | ||
| 182 | + return formContent; | ||
| 183 | +} | ||
| 184 | + | ||
| 185 | +/** bind a "widget" to a particular fieldset, and populate the appropriate event-handlers | ||
| 186 | + - find the various types of input objects and hook in appropriately: | ||
| 187 | + make sure that the function binds: | ||
| 188 | + - fieldset | ||
| 189 | + - pseudo-widget (the div that surrounds each group of options.) | ||
| 190 | + -handler. | ||
| 191 | + // FIXME: this assumes that inputs are either "select" or "<input type='radio'>" | ||
| 192 | + // FIXME: is that a valid assumption? | ||
| 193 | +*/ | ||
| 194 | +function bindToConditionalFieldset(fieldset, widget) { | ||
| 195 | + | ||
| 196 | + // handleChange needs to be bound to each input widget. | ||
| 197 | + // for <input type != "hidden"> type variables this means binding to onclick | ||
| 198 | + // for <select> this means binding to onchange | ||
| 199 | + | ||
| 200 | + var select_fields = widget.getElementsByTagName('SELECT'); | ||
| 201 | + var input_fields = widget.getElementsByTagName('INPUT'); // needs to be filtered - no "hidden" vars. | ||
| 202 | + | ||
| 203 | + for (var i=0; i<select_fields.length; i++) { | ||
| 204 | + var select_object = select_fields[i]; | ||
| 205 | + var handler = partial(handleSelectChange, fieldset, widget, select_object); | ||
| 206 | + attachToElementEvent(select_object, 'change', handler); | ||
| 207 | + } | ||
| 208 | + | ||
| 209 | + for (var i=0; i<input_fields.length; i++) { | ||
| 210 | + var input_object = input_fields[i]; | ||
| 211 | + var handler = partial(handleRadioChange, fieldset, widget, input_object); | ||
| 212 | + if (input_object.type == 'radio') { | ||
| 213 | + attachToElementEvent(input_object, 'click', handler); | ||
| 214 | + } else if (input_object.type == 'hidden') { | ||
| 215 | + ; // this is OK, and expected. | ||
| 216 | + } else { | ||
| 217 | + simpleLog('ERROR','bindToConditionalFieldset found a non-hidden input field of type: '+input_object.type); | ||
| 218 | + } | ||
| 219 | + } | ||
| 220 | + simpleLog('DEBUG','bindToConditionalFieldset complete'); | ||
| 221 | +} | ||
| 222 | + | ||
| 223 | +function clearUnfixedWidgets(fieldset) { | ||
| 224 | + var widgets = getElementsByTagAndClassName('DIV', 'widget', fieldset); | ||
| 225 | + for (var i=0; i<widgets.length; i++) { | ||
| 226 | + var w = widgets[i]; | ||
| 227 | + if (hasElementClass(w, 'fixed')) { | ||
| 228 | + simpleLog('DEBUG','Not deleting widget with class '+w.getAttribute('class')); | ||
| 229 | + } else { | ||
| 230 | + w.parentNode.removeChild(w); | ||
| 231 | + simpleLog('DEBUG','Deleting widget with class '+w.getAttribute('class')); | ||
| 232 | + } | ||
| 233 | + } | ||
| 234 | +} | ||
| 235 | + | ||
| 236 | +/* XMLHttpRequest functions | ||
| 237 | + * | ||
| 238 | + */ | ||
| 239 | + | ||
| 240 | +function updateFieldset(fieldset) { | ||
| 241 | + var targeturl = '/presentation/lookAndFeel/knowledgeTree/ajaxConditional.php'; // test_metadata_update.txt'; | ||
| 242 | + simpleLog('DEBUG','AJAX function called: updateFieldset'); | ||
| 243 | + | ||
| 244 | + var formdata = parseFieldsetToForm(fieldset); | ||
| 245 | + formdata.formKeys.push('action'); | ||
| 246 | + formdata.formValues.push('updateFieldset'); | ||
| 247 | + var POSTval = queryString(formdata.formKeys, formdata.formValues); | ||
| 248 | + | ||
| 249 | + var req = getXMLHttpRequest(); | ||
| 250 | + req.open('POST',targeturl, true); // MUST be async. | ||
| 251 | + //simpleLog('DEBUG','form submission from updateFieldset: '+logFormSubmission(formdata)); | ||
| 252 | + simpleLog('DEBUG','form submission from updateFieldset: '+(formdata)); | ||
| 253 | + req.setRequestHeader('Content-Type','application/x-www-form-urlencoded'); | ||
| 254 | + var deferred = sendXMLHttpRequest(req, POSTval); | ||
| 255 | + | ||
| 256 | + deferred.addErrback(partial(do_handleError, 'updateFieldset')); | ||
| 257 | + deferred.addCallback(partial(do_updateFieldset, fieldset)); | ||
| 258 | +} | ||
| 259 | + | ||
| 260 | +function do_handleError(function_name, err) { | ||
| 261 | + simpleLog('ERROR','AJAX request from function '+function_name+' failed on exception: '+err); | ||
| 262 | +} | ||
| 263 | + | ||
| 264 | +function do_updateFieldset(fieldset, req) { | ||
| 265 | + simpleLog('DEBUG','AJAX function do_updateFieldset received: \n'+req.responseText); | ||
| 266 | + // clear unfixed widgets before we start. | ||
| 267 | + clearUnfixedWidgets(fieldset); | ||
| 268 | + // create an unparented div for HTML insertion. | ||
| 269 | + var t = DIV(null); | ||
| 270 | + t.innerHTML = req.responseText; | ||
| 271 | + var new_widgets = getElementsByTagAndClassName('div','widget', t); | ||
| 272 | + for (var i=0; i<new_widgets.length; i++) { | ||
| 273 | + var w = new_widgets[i]; | ||
| 274 | + fieldset.appendChild(w); | ||
| 275 | + bindToConditionalFieldset(fieldset, w); | ||
| 276 | + } | ||
| 277 | + delete t; // clean this up. | ||
| 278 | +} | ||
| 279 | + | ||
| 280 | +/* HTML callbacks - functions called on-event. | ||
| 281 | + * | ||
| 282 | + */ | ||
| 283 | + | ||
| 284 | + | ||
| 285 | + | ||
| 286 | +/* Fieldset creation and update. | ||
| 287 | + * | ||
| 288 | + */ | ||
| 289 | + | ||
| 290 | +function initialiseConditionalFieldsets() { | ||
| 291 | + simpleLog('ERROR','incomplete function called: initialiseFieldsets.'); | ||
| 292 | + var fieldsets = getElementsByTagAndClassName('FIELDSET','conditional_metadata'); | ||
| 293 | + simpleLog('DEBUG','found fieldsets: '+fieldsets.length); | ||
| 294 | + // triggers initial update - since this contains no "fixed" vars, it'll remove "unfixed" widgets | ||
| 295 | + // and insert the initial (master) field. | ||
| 296 | + for (var i=0; i<fieldsets.length; i++) { | ||
| 297 | + var undo_button = INPUT({'type':'button','value':'undo'},null); | ||
| 298 | + attachToElementEvent(undo_button,'click',partial(popStack, fieldsets[i])); | ||
| 299 | + fieldsets[i].appendChild(undo_button); | ||
| 300 | + // initialise the stack. | ||
| 301 | + getStackForFieldset(fieldsets[i]); | ||
| 302 | + updateFieldset(fieldsets[i]); | ||
| 303 | + } | ||
| 304 | +} | ||
| 305 | + | ||
| 306 | +addLoadEvent(initialiseConditionalFieldsets); |