1 if(!dojo._hasResource['openils.widget.AutoFieldWidget']) {
2 dojo.provide('openils.widget.AutoFieldWidget');
3 dojo.require('openils.Util');
4 dojo.require('openils.User');
5 dojo.require('fieldmapper.IDL');
6 dojo.require('openils.PermaCrud');
7 dojo.requireLocalization("openils.widget", "AutoFieldWidget");
9 dojo.declare('openils.widget.AutoFieldWidget', null, {
15 * idlField -- Field description object from fieldmapper.IDL.fmclasses
16 * fmObject -- If available, the object being edited. This will be used
17 * to set the value of the widget.
18 * fmClass -- Class name (not required if idlField or fmObject is set)
19 * fmField -- Field name (not required if idlField)
20 * parentNode -- If defined, the widget will be appended to this DOM node
21 * dijitArgs -- Optional parameters object, passed directly to the dojo widget
22 * orgLimitPerms -- If this field defines a set of org units and an orgLimitPerms
23 * is defined, the code will limit the org units in the set to those
24 * allowed by the permission
25 * orgDefaultsToWs -- If this is an org unit field and the widget has no value,
26 * set the value equal to the users's workstation org unit. Othwerwise, leave it null
27 * selfReference -- The primary purpose of an AutoFieldWidget is to render the value
28 * or widget for a field on an object (that may or may not link to another object).
29 * selfReference allows you to sidestep the indirection and create a selector widget
30 * based purely on an fmClass. To get a dropdown of all of the 'abc'
31 * objects, pass in {selfReference : true, fmClass : 'abc'}.
32 * labelFormat -- For widgets that are displayed as remote object filtering selects,
33 * this provides a mechanism for overriding the label format in the filtering select.
34 * It must be an array, whose first value is a format string, compliant with
35 * dojo.string.substitute. The remaining array items are the arguments to the format
36 * represented as field names on the remote linked object.
38 * labelFormat : [ '${0} (${1})', 'obj_field_1', 'obj_field_2' ]
39 * Note: this does not control the final display value. Only values in the drop-down.
40 * See searchFormat for controlling the display value
41 * searchFormat -- This format controls the structure of the search attribute which
42 * controls the text used during type-ahead searching and the displayed value in
43 * the filtering select. See labelFormat for the structure.
44 * dataLoader : Bypass the default PermaCrud linked data fetcher and use this function instead.
45 * Function arguments are (link class name, search filter, callback)
46 * The fetched objects should be passed to the callback as an array
47 * disableQuery : dojo.data query passed to FilteringTreeSelect-based widgets to disable
48 * (but leave visible) certain options.
50 constructor : function(args) {
54 if (!this.dijitArgs) {
57 this.dijitArgs['scrollOnFocus'] = false;
60 // find the field description in the IDL if not provided
62 this.fmClass = this.fmObject.classname;
63 this.fmIDL = fieldmapper.IDL.fmclasses[this.fmClass];
65 if(this.fmClass && !this.fmIDL) {
66 fieldmapper.IDL.load([this.fmClass]);
67 this.fmIDL = fieldmapper.IDL.fmclasses[this.fmClass];
70 this.suppressLinkedFields = args.suppressLinkedFields || [];
72 if(this.selfReference) {
73 this.fmField = fieldmapper.IDL.fmclasses[this.fmClass].pkey;
75 // create a mock-up of the idlField object.
78 'class' : this.fmClass,
87 this.fmIDL = fieldmapper.IDL.fmclasses[this.fmClass];
88 var fields = this.fmIDL.fields;
90 if(fields[f].name == this.fmField)
91 this.idlField = fields[f];
96 throw new Error("AutoFieldWidget could not determine which " +
97 "field to render. We need more information. fmClass=" +
98 this.fmClass + ' fmField=' + this.fmField + ' fmObject=' + js2JSON(this.fmObject));
100 this.auth = openils.User.authtoken;
101 this.cache = openils.widget.AutoFieldWidget.cache;
102 this.cache[this.auth] = this.cache[this.auth] || {};
103 this.cache[this.auth].single = this.cache[this.auth].single || {};
104 this.cache[this.auth].list = this.cache[this.auth].list || {};
108 * Turn the widget-stored value into a value oils understands
110 getFormattedValue : function() {
111 var value = this.baseWidgetValue();
112 switch(this.idlField.datatype) {
115 case 'true': return 't';
116 case 'on': return 't';
117 case 'false' : return 'f';
118 case 'unset' : return null;
119 case true : return 't';
123 if(!value) return null;
124 return dojo.date.stamp.toISOString(value);
128 if(isNaN(value)) value = null;
130 return (value === '') ? null : value;
134 baseWidgetValue : function(value) {
135 var attr = (this.readOnly) ? 'content' : 'value';
136 if(arguments.length) this.widget.attr(attr, value);
137 return this.widget.attr(attr);
141 * Turn the widget-stored value into something visually suitable
143 getDisplayString : function() {
144 var value = this.widgetValue;
150 return openils.widget.AutoFieldWidget.localeStrings.INHERITED;
153 switch(this.idlField.datatype) {
158 return openils.widget.AutoFieldWidget.localeStrings.TRUE;
161 return openils.widget.AutoFieldWidget.localeStrings.FALSE;
163 case 'unset' : return openils.widget.AutoFieldWidget.localeStrings.UNSET;
164 case true : return openils.widget.AutoFieldWidget.localeStrings.TRUE;
165 default: return openils.widget.AutoFieldWidget.localeStrings.FALSE;
168 if (!value) return '';
169 return openils.Util.timeStamp(
170 value, {"formatLength": "short"}
173 if(value === null || value === undefined) return '';
174 return fieldmapper.aou.findOrgUnit(value).shortname();
177 if(isNaN(value)) value = 0;
179 if(value === undefined || value === null)
185 isRequired : function() {
188 this.idlField.required || (
190 this.dijitArgs.required || this.dijitArgs.regExp
197 build : function(onload) {
199 if(this.widgetValue == null)
200 this.widgetValue = (this.fmObject) ? this.fmObject[this.idlField.name]() : null;
203 // core widget provided for us, attach and move on
204 if(this.parentNode) // may already be in the "right" place
205 this.parentNode.appendChild(this.widget.domNode);
207 if (this.shove.mode == "update") {
208 if (this.idlField.datatype == "timestamp")
209 this.widgetValue = openils.Util.timeStampAsDateObj(
213 this.widgetValue = this.shove.create;
215 this._widgetLoaded();
216 } else if (this.widget.attr("value") == null) {
217 this._widgetLoaded();
222 if(!this.parentNode) // give it somewhere to live so that dojo won't complain
223 this.parentNode = dojo.create('div');
225 this.onload = onload;
228 dojo.require('dijit.layout.ContentPane');
229 this.widget = new dijit.layout.ContentPane(this.dijitArgs, this.parentNode);
230 if(this.widgetValue !== null)
231 this._tryLinkedDisplayField();
233 } else if(this.widgetClass) {
234 dojo.require(this.widgetClass);
235 eval('this.widget = new ' + this.widgetClass + '(this.dijitArgs, this.parentNode);');
239 switch(this.idlField.datatype) {
242 dojo.require('dijit.form.TextBox');
243 this.widget = new dijit.form.TextBox(this.dijitArgs, this.parentNode);
247 this._buildOrgSelector();
251 // dojo.require('dijit.form.CurrencyTextBox');
252 // the CurrencyTextBox dijit is broken in Dojo 1.3; upon upgrading
253 // to Dojo 1.5 or later, should re-evaluate work-around use of dijit.form.NumberTextBox.
254 // See https://bugs.launchpad.net/evergreen/+bug/702117
255 dojo.require('dijit.form.NumberTextBox');
256 this.dijitArgs = dojo.mixin({constraints:{places:'0,2'}}, this.dijitArgs || {});
257 this.widget = new dijit.form.NumberTextBox(this.dijitArgs, this.parentNode);
261 dojo.require('dijit.form.NumberTextBox');
262 this.dijitArgs = dojo.mixin({constraints:{places:0}}, this.dijitArgs || {});
263 this.widget = new dijit.form.NumberTextBox(this.dijitArgs, this.parentNode);
267 dojo.require('dijit.form.NumberTextBox');
268 this.widget = new dijit.form.NumberTextBox(this.dijitArgs, this.parentNode);
272 dojo.require('dijit.form.DateTextBox');
273 dojo.require('dojo.date.stamp');
274 this.widget = new dijit.form.DateTextBox(this.dijitArgs, this.parentNode);
275 if (this.widgetValue != null) {
276 this.widgetValue = openils.Util.timeStampAsDateObj(
283 if(this.ternary || this.inherits) {
284 dojo.require('dijit.form.FilteringSelect');
285 var store = new dojo.data.ItemFileReadStore({
287 identifier : 'value',
289 {label : (this.inherits ? openils.widget.AutoFieldWidget.localeStrings.INHERITED : openils.widget.AutoFieldWidget.localeStrings.UNSET), value : 'unset'},
290 {label : openils.widget.AutoFieldWidget.localeStrings.TRUE, value : 'true'},
291 {label : openils.widget.AutoFieldWidget.localeStrings.FALSE, value : 'false'}
295 this.widget = new dijit.form.FilteringSelect(this.dijitArgs, this.parentNode);
296 this.widget.searchAttr = this.widget.labelAttr = 'label';
297 this.widget.valueAttr = 'value';
298 this.widget.store = store;
299 this.widget.startup();
300 this.widgetValue = (this.widgetValue === null) ? 'unset' :
301 (openils.Util.isTrue(this.widgetValue)) ? 'true' : 'false';
303 dojo.require('dijit.form.CheckBox');
304 this.widget = new dijit.form.CheckBox(this.dijitArgs, this.parentNode);
305 this.widgetValue = openils.Util.isTrue(this.widgetValue);
310 if(this._buildLinkSelector()) break;
313 if(this.dijitArgs && (this.dijitArgs.required || this.dijitArgs.regExp)) {
314 dojo.require('dijit.form.ValidationTextBox');
315 this.widget = new dijit.form.ValidationTextBox(this.dijitArgs, this.parentNode);
317 dojo.require('dijit.form.TextBox');
318 this.widget = new dijit.form.TextBox(this.dijitArgs, this.parentNode);
323 if(!this.async) this._widgetLoaded();
327 // we want to display the value for our widget. However, instead of displaying
328 // an ID, for exmaple, display the value for the 'selector' field on the object
330 _tryLinkedDisplayField : function(noAsync) {
332 if(this.idlField.datatype == 'org_unit')
333 return false; // we already handle org_units, no need to re-fetch
335 // user opted to bypass fetching this linked data
336 if(this.suppressLinkedFields.indexOf(this.idlField.name) > -1)
339 var linkInfo = this._getLinkSelector();
340 if(!(linkInfo && linkInfo.vfield && linkInfo.vfield.selector))
342 var lclass = linkInfo.linkClass;
347 // first try the store cache
349 if(this.cache[this.auth].list[lclass]) {
350 var store = this.cache[this.auth].list[lclass];
352 query[linkInfo.vfield.name] = ''+this.widgetValue;
354 store.fetch({query:query, onComplete:
357 self.widgetValue = store.getValue(list[0], linkInfo.vfield.selector);
366 // then try the single object cache
367 if(this.cache[this.auth].single[lclass] && this.cache[this.auth].single[lclass][this.widgetValue]) {
368 this.widgetValue = this.cache[this.auth].single[lclass][this.widgetValue];
372 console.log("Fetching sync object " + lclass + " : " + this.widgetValue);
374 // if those fail, fetch the linked object
377 new openils.PermaCrud().retrieve(lclass, this.widgetValue, {
378 async : !this.forceSync,
379 oncomplete : function(r) {
380 var item = openils.Util.readResponse(r);
381 var newvalue = item[linkInfo.vfield.selector]();
383 if(!self.cache[self.auth].single[lclass])
384 self.cache[self.auth].single[lclass] = {};
385 self.cache[self.auth].single[lclass][self.widgetValue] = newvalue;
387 self.widgetValue = newvalue;
388 self.widget.startup();
389 self._widgetLoaded();
394 _getLinkSelector : function() {
395 var linkClass = this.idlField['class'];
396 if(this.idlField.reltype != 'has_a') return false;
397 if(!fieldmapper.IDL.fmclasses[linkClass]) // class neglected by AutoIDL
398 fieldmapper.IDL.load([linkClass]);
399 if(!fieldmapper.IDL.fmclasses[linkClass].permacrud) return false;
400 if(!fieldmapper.IDL.fmclasses[linkClass].permacrud.retrieve) return false;
403 var rclassIdl = fieldmapper.IDL.fmclasses[linkClass];
405 for(var f in rclassIdl.fields) {
406 if(this.idlField.key == rclassIdl.fields[f].name) {
407 vfield = rclassIdl.fields[f];
413 throw new Error("'" + linkClass + "' has no '" + this.idlField.key + "' field!");
416 linkClass : linkClass,
421 _buildLinkSelector : function() {
423 var selectorInfo = this._getLinkSelector();
424 if(!selectorInfo) return false;
426 var linkClass = selectorInfo.linkClass;
427 var vfield = selectorInfo.vfield;
431 if(linkClass == 'pgt')
432 return this._buildPermGrpSelector();
433 if(linkClass == 'aou')
434 return this._buildOrgSelector();
435 if(linkClass == 'acpl')
436 return this._buildCopyLocSelector();
437 if(linkClass == 'acqpro')
438 return this._buildAutoCompleteSelector(linkClass, vfield.selector);
441 dojo.require('dojo.data.ItemFileReadStore');
442 dojo.require('dijit.form.FilteringSelect');
444 this.widget = new dijit.form.FilteringSelect(this.dijitArgs, this.parentNode);
445 this.widget.searchAttr = this.widget.labelAttr = vfield.selector || vfield.name;
446 this.widget.valueAttr = vfield.name;
447 this.widget.attr('disabled', true);
449 var oncomplete = function(list) {
450 self.widget.attr('disabled', false);
453 self.widget.labelAttr = '_label';
455 if(self.searchFormat)
456 self.widget.searchAttr = '_search';
458 function formatString(item, formatList) {
462 // formatList[1..*] are names of fields. Pull the field
463 // values from each object to determine the values for string substitution
465 var format = formatList[0];
466 for(var i = 1; i< formatList.length; i++)
467 values.push(item[formatList[i]]);
469 return dojo.string.substitute(format, values);
473 "openils.widget.AutoFieldWidget: Invalid formatList ["+formatList+"] : "+E);
479 var storeData = {data:fieldmapper[linkClass].toStoreData(list)};
481 if(self.labelFormat) {
482 dojo.forEach(storeData.data.items,
484 item._label = formatString(item, self.labelFormat);
489 if(self.searchFormat) {
490 dojo.forEach(storeData.data.items,
492 item._search = formatString(item, self.searchFormat);
497 self.widget.store = new dojo.data.ItemFileReadStore(storeData);
498 self.cache[self.auth].list[linkClass] = self.widget.store;
501 self.widget.store = self.cache[self.auth].list[linkClass];
504 self.widget.startup();
505 self._widgetLoaded();
508 if(!this.noCache && this.cache[self.auth].list[linkClass]) {
513 if(!this.dataLoader && openils.widget.AutoFieldWidget.defaultLinkedDataLoader[linkClass])
514 this.dataLoader = openils.widget.AutoFieldWidget.defaultLinkedDataLoader[linkClass];
516 if(this.dataLoader) {
518 // caller provided an external function for retrieving the data
519 this.dataLoader(linkClass, this.searchFilter, oncomplete);
523 var _cb = function(r) {
524 oncomplete(openils.Util.readResponse(r, false, true));
527 if (this.searchFilter) {
528 new openils.PermaCrud().search(linkClass, this.searchFilter, {
529 async : !this.forceSync, oncomplete : _cb
532 new openils.PermaCrud().retrieveAll(linkClass, {
533 async : !this.forceSync, oncomplete : _cb
543 * For widgets that run asynchronously, provide a callback for finishing up
545 _widgetLoaded : function(value) {
549 /* -------------------------------------------------------------
550 when using widgets in a grid, the cell may dissapear, which
551 kills the underlying DOM node, which causes this to fail.
552 For now, back out gracefully and let grid getters use
553 getDisplayString() instead
554 -------------------------------------------------------------*/
556 this.baseWidgetValue(this.getDisplayString());
561 this.baseWidgetValue(this.widgetValue);
562 if(this.idlField.name == this.fmIDL.pkey && this.fmIDL.pkey_sequence && (!this.selfReference && !this.noDisablePkey))
563 this.widget.attr('disabled', true);
564 if(this.disableWidgetTest && this.disableWidgetTest(this.idlField.name, this.fmObject))
565 this.widget.attr('disabled', true);
568 this.onload(this.widget, this);
570 if(!this.readOnly && (this.idlField.required || (this.dijitArgs && this.dijitArgs.required))) {
571 // a required dijit is not given any styling to indicate the value
572 // is invalid until the user has focused the widget then left it with
573 // invalid data. This change tells dojo to pretend this focusing has
574 // already happened so we can style required widgets during page render.
575 this.widget._hasBeenBlurred = true;
576 this.widget.validate();
580 _buildOrgSelector : function() {
581 dojo.require('fieldmapper.OrgUtils');
582 dojo.require('openils.widget.FilteringTreeSelect');
583 this.widget = new openils.widget.FilteringTreeSelect(this.dijitArgs, this.parentNode);
584 this.widget.searchAttr = 'shortname';
585 this.widget.labelAttr = 'shortname';
586 this.widget.parentField = 'parent_ou';
587 var user = new openils.User();
589 if(this.widgetValue == null && this.orgDefaultsToWs)
590 this.widgetValue = user.user.ws_ou();
592 // if we have a limit perm, find the relevent orgs (async)
593 if(this.orgLimitPerms && this.orgLimitPerms.length > 0) {
596 user.getPermOrgList(this.orgLimitPerms,
598 self.widget.tree = orgList;
599 self.widget.startup();
600 self._widgetLoaded();
605 this.widget.tree = fieldmapper.aou.globalOrgTree;
606 this.widget.startup();
612 _buildPermGrpSelector : function() {
613 dojo.require('openils.widget.FilteringTreeSelect');
614 this.widget = new openils.widget.FilteringTreeSelect(this.dijitArgs, this.parentNode);
615 this.widget.disableQuery = this.disableQuery;
616 this.widget.searchAttr = 'name';
618 if(this.cache.permGrpTree) {
619 this.widget.tree = this.cache.permGrpTree;
620 this.widget.startup();
621 this._widgetLoaded();
627 new openils.PermaCrud().retrieveAll('pgt', {
628 async : !this.forceSync,
629 oncomplete : function(r) {
630 var list = openils.Util.readResponse(r, false, true);
635 map[list[l].id()] = list[l];
638 var pnode = map[node.parent()];
639 if(!pnode) {root = node; continue;}
640 if(!pnode.children()) pnode.children([]);
641 pnode.children().push(node);
643 self.widget.tree = self.cache.permGrpTree = root;
644 self.widget.startup();
645 self._widgetLoaded();
652 _buildCopyLocSelector : function() {
653 dojo.require('dijit.form.FilteringSelect');
654 this.widget = new dijit.form.FilteringSelect(this.dijitArgs, this.parentNode);
655 this.widget.searchAttr = this.widget.labalAttr = 'name';
656 this.widget.valueAttr = 'id';
658 if(this.cache.copyLocStore) {
659 this.widget.store = this.cache.copyLocStore;
660 this.widget.startup();
666 var ws_ou = openils.User.user.ws_ou();
667 var orgs = fieldmapper.aou.findOrgUnit(ws_ou).orgNodeTrail().map(function (i) { return i.id() });
668 orgs = orgs.concat(fieldmapper.aou.descendantNodeList(ws_ou).map(function (i) { return i.id() }));
671 new openils.PermaCrud().search('acpl', {owning_lib : orgs}, {
672 async : !this.forceSync,
673 oncomplete : function(r) {
674 var list = openils.Util.readResponse(r, false, true);
677 new dojo.data.ItemFileReadStore({data:fieldmapper.acpl.toStoreData(list)});
678 self.cache.copyLocStore = self.widget.store;
679 self.widget.startup();
680 self._widgetLoaded();
687 _buildAutoCompleteSelector : function(linkClass, searchAttr) {
688 dojo.require("openils.widget.PCrudAutocompleteBox");
689 dojo.mixin(this.dijitArgs, {
691 searchAttr : searchAttr,
693 this.widget = new openils.widget.PCrudAutocompleteBox(this.dijitArgs, this.parentNode);
694 this._widgetLoaded();
699 openils.widget.AutoFieldWidget.localeStrings = dojo.i18n.getLocalization("openils.widget", "AutoFieldWidget");
700 openils.widget.AutoFieldWidget.cache = {};
701 openils.widget.AutoFieldWidget.defaultLinkedDataLoader = {};