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;
145 switch(this.idlField.datatype) {
150 return openils.widget.AutoFieldWidget.localeStrings.TRUE;
153 return openils.widget.AutoFieldWidget.localeStrings.FALSE;
155 case 'unset' : return openils.widget.AutoFieldWidget.localeStrings.UNSET;
156 case true : return openils.widget.AutoFieldWidget.localeStrings.TRUE;
157 default: return openils.widget.AutoFieldWidget.localeStrings.FALSE;
160 if (!value) return '';
161 return openils.Util.timeStamp(
162 value, {"formatLength": "short"}
165 if(value === null || value === undefined) return '';
166 return fieldmapper.aou.findOrgUnit(value).shortname();
169 if(isNaN(value)) value = 0;
171 if(value === undefined || value === null)
177 isRequired : function() {
180 this.idlField.required || (
182 this.dijitArgs.required || this.dijitArgs.regExp
189 build : function(onload) {
191 if(this.widgetValue == null)
192 this.widgetValue = (this.fmObject) ? this.fmObject[this.idlField.name]() : null;
195 // core widget provided for us, attach and move on
196 if(this.parentNode) // may already be in the "right" place
197 this.parentNode.appendChild(this.widget.domNode);
199 if (this.shove.mode == "update") {
200 if (this.idlField.datatype == "timestamp")
201 this.widgetValue = openils.Util.timeStampAsDateObj(
205 this.widgetValue = this.shove.create;
207 this._widgetLoaded();
208 } else if (this.widget.attr("value") == null) {
209 this._widgetLoaded();
214 if(!this.parentNode) // give it somewhere to live so that dojo won't complain
215 this.parentNode = dojo.create('div');
217 this.onload = onload;
220 dojo.require('dijit.layout.ContentPane');
221 this.widget = new dijit.layout.ContentPane(this.dijitArgs, this.parentNode);
222 if(this.widgetValue !== null)
223 this._tryLinkedDisplayField();
225 } else if(this.widgetClass) {
226 dojo.require(this.widgetClass);
227 eval('this.widget = new ' + this.widgetClass + '(this.dijitArgs, this.parentNode);');
231 switch(this.idlField.datatype) {
234 dojo.require('dijit.form.TextBox');
235 this.widget = new dijit.form.TextBox(this.dijitArgs, this.parentNode);
239 this._buildOrgSelector();
243 // dojo.require('dijit.form.CurrencyTextBox');
244 // the CurrencyTextBox dijit is broken in Dojo 1.3; upon upgrading
245 // to Dojo 1.5 or later, should re-evaluate work-around use of dijit.form.NumberTextBox.
246 // See https://bugs.launchpad.net/evergreen/+bug/702117
247 dojo.require('dijit.form.NumberTextBox');
248 this.dijitArgs = dojo.mixin({constraints:{places:'0,2'}}, this.dijitArgs || {});
249 this.widget = new dijit.form.NumberTextBox(this.dijitArgs, this.parentNode);
253 dojo.require('dijit.form.NumberTextBox');
254 this.dijitArgs = dojo.mixin({constraints:{places:0}}, this.dijitArgs || {});
255 this.widget = new dijit.form.NumberTextBox(this.dijitArgs, this.parentNode);
259 dojo.require('dijit.form.NumberTextBox');
260 this.widget = new dijit.form.NumberTextBox(this.dijitArgs, this.parentNode);
264 dojo.require('dijit.form.DateTextBox');
265 dojo.require('dojo.date.stamp');
266 this.widget = new dijit.form.DateTextBox(this.dijitArgs, this.parentNode);
267 if (this.widgetValue != null) {
268 this.widgetValue = openils.Util.timeStampAsDateObj(
276 dojo.require('dijit.form.FilteringSelect');
277 var store = new dojo.data.ItemFileReadStore({
279 identifier : 'value',
281 {label : openils.widget.AutoFieldWidget.localeStrings.UNSET, value : 'unset'},
282 {label : openils.widget.AutoFieldWidget.localeStrings.TRUE, value : 'true'},
283 {label : openils.widget.AutoFieldWidget.localeStrings.FALSE, value : 'false'}
287 this.widget = new dijit.form.FilteringSelect(this.dijitArgs, this.parentNode);
288 this.widget.searchAttr = this.widget.labelAttr = 'label';
289 this.widget.valueAttr = 'value';
290 this.widget.store = store;
291 this.widget.startup();
292 this.widgetValue = (this.widgetValue === null) ? 'unset' :
293 (openils.Util.isTrue(this.widgetValue)) ? 'true' : 'false';
295 dojo.require('dijit.form.CheckBox');
296 this.widget = new dijit.form.CheckBox(this.dijitArgs, this.parentNode);
297 this.widgetValue = openils.Util.isTrue(this.widgetValue);
302 if(this._buildLinkSelector()) break;
305 if(this.dijitArgs && (this.dijitArgs.required || this.dijitArgs.regExp)) {
306 dojo.require('dijit.form.ValidationTextBox');
307 this.widget = new dijit.form.ValidationTextBox(this.dijitArgs, this.parentNode);
309 dojo.require('dijit.form.TextBox');
310 this.widget = new dijit.form.TextBox(this.dijitArgs, this.parentNode);
315 if(!this.async) this._widgetLoaded();
319 // we want to display the value for our widget. However, instead of displaying
320 // an ID, for exmaple, display the value for the 'selector' field on the object
322 _tryLinkedDisplayField : function(noAsync) {
324 if(this.idlField.datatype == 'org_unit')
325 return false; // we already handle org_units, no need to re-fetch
327 // user opted to bypass fetching this linked data
328 if(this.suppressLinkedFields.indexOf(this.idlField.name) > -1)
331 var linkInfo = this._getLinkSelector();
332 if(!(linkInfo && linkInfo.vfield && linkInfo.vfield.selector))
334 var lclass = linkInfo.linkClass;
339 // first try the store cache
341 if(this.cache[this.auth].list[lclass]) {
342 var store = this.cache[this.auth].list[lclass];
344 query[linkInfo.vfield.name] = ''+this.widgetValue;
346 store.fetch({query:query, onComplete:
349 self.widgetValue = store.getValue(list[0], linkInfo.vfield.selector);
358 // then try the single object cache
359 if(this.cache[this.auth].single[lclass] && this.cache[this.auth].single[lclass][this.widgetValue]) {
360 this.widgetValue = this.cache[this.auth].single[lclass][this.widgetValue];
364 console.log("Fetching sync object " + lclass + " : " + this.widgetValue);
366 // if those fail, fetch the linked object
369 new openils.PermaCrud().retrieve(lclass, this.widgetValue, {
370 async : !this.forceSync,
371 oncomplete : function(r) {
372 var item = openils.Util.readResponse(r);
373 var newvalue = item[linkInfo.vfield.selector]();
375 if(!self.cache[self.auth].single[lclass])
376 self.cache[self.auth].single[lclass] = {};
377 self.cache[self.auth].single[lclass][self.widgetValue] = newvalue;
379 self.widgetValue = newvalue;
380 self.widget.startup();
381 self._widgetLoaded();
386 _getLinkSelector : function() {
387 var linkClass = this.idlField['class'];
388 if(this.idlField.reltype != 'has_a') return false;
389 if(!fieldmapper.IDL.fmclasses[linkClass]) // class neglected by AutoIDL
390 fieldmapper.IDL.load([linkClass]);
391 if(!fieldmapper.IDL.fmclasses[linkClass].permacrud) return false;
392 if(!fieldmapper.IDL.fmclasses[linkClass].permacrud.retrieve) return false;
395 var rclassIdl = fieldmapper.IDL.fmclasses[linkClass];
397 for(var f in rclassIdl.fields) {
398 if(this.idlField.key == rclassIdl.fields[f].name) {
399 vfield = rclassIdl.fields[f];
405 throw new Error("'" + linkClass + "' has no '" + this.idlField.key + "' field!");
408 linkClass : linkClass,
413 _buildLinkSelector : function() {
415 var selectorInfo = this._getLinkSelector();
416 if(!selectorInfo) return false;
418 var linkClass = selectorInfo.linkClass;
419 var vfield = selectorInfo.vfield;
423 if(linkClass == 'pgt')
424 return this._buildPermGrpSelector();
425 if(linkClass == 'aou')
426 return this._buildOrgSelector();
427 if(linkClass == 'acpl')
428 return this._buildCopyLocSelector();
429 if(linkClass == 'acqpro')
430 return this._buildAutoCompleteSelector(linkClass, vfield.selector);
433 dojo.require('dojo.data.ItemFileReadStore');
434 dojo.require('dijit.form.FilteringSelect');
436 this.widget = new dijit.form.FilteringSelect(this.dijitArgs, this.parentNode);
437 this.widget.searchAttr = this.widget.labelAttr = vfield.selector || vfield.name;
438 this.widget.valueAttr = vfield.name;
439 this.widget.attr('disabled', true);
441 var oncomplete = function(list) {
442 self.widget.attr('disabled', false);
445 self.widget.labelAttr = '_label';
447 if(self.searchFormat)
448 self.widget.searchAttr = '_search';
450 function formatString(item, formatList) {
454 // formatList[1..*] are names of fields. Pull the field
455 // values from each object to determine the values for string substitution
457 var format = formatList[0];
458 for(var i = 1; i< formatList.length; i++)
459 values.push(item[formatList[i]]);
461 return dojo.string.substitute(format, values);
465 "openils.widget.AutoFieldWidget: Invalid formatList ["+formatList+"] : "+E);
471 var storeData = {data:fieldmapper[linkClass].toStoreData(list)};
473 if(self.labelFormat) {
474 dojo.forEach(storeData.data.items,
476 item._label = formatString(item, self.labelFormat);
481 if(self.searchFormat) {
482 dojo.forEach(storeData.data.items,
484 item._search = formatString(item, self.searchFormat);
489 self.widget.store = new dojo.data.ItemFileReadStore(storeData);
490 self.cache[self.auth].list[linkClass] = self.widget.store;
493 self.widget.store = self.cache[self.auth].list[linkClass];
496 self.widget.startup();
497 self._widgetLoaded();
500 if(!this.noCache && this.cache[self.auth].list[linkClass]) {
505 if(!this.dataLoader && openils.widget.AutoFieldWidget.defaultLinkedDataLoader[linkClass])
506 this.dataLoader = openils.widget.AutoFieldWidget.defaultLinkedDataLoader[linkClass];
508 if(this.dataLoader) {
510 // caller provided an external function for retrieving the data
511 this.dataLoader(linkClass, this.searchFilter, oncomplete);
515 var _cb = function(r) {
516 oncomplete(openils.Util.readResponse(r, false, true));
519 if (this.searchFilter) {
520 new openils.PermaCrud().search(linkClass, this.searchFilter, {
521 async : !this.forceSync, oncomplete : _cb
524 new openils.PermaCrud().retrieveAll(linkClass, {
525 async : !this.forceSync, oncomplete : _cb
535 * For widgets that run asynchronously, provide a callback for finishing up
537 _widgetLoaded : function(value) {
541 /* -------------------------------------------------------------
542 when using widgets in a grid, the cell may dissapear, which
543 kills the underlying DOM node, which causes this to fail.
544 For now, back out gracefully and let grid getters use
545 getDisplayString() instead
546 -------------------------------------------------------------*/
548 this.baseWidgetValue(this.getDisplayString());
553 this.baseWidgetValue(this.widgetValue);
554 if(this.idlField.name == this.fmIDL.pkey && this.fmIDL.pkey_sequence && (!this.selfReference && !this.noDisablePkey))
555 this.widget.attr('disabled', true);
556 if(this.disableWidgetTest && this.disableWidgetTest(this.idlField.name, this.fmObject))
557 this.widget.attr('disabled', true);
560 this.onload(this.widget, this);
562 if(!this.readOnly && (this.idlField.required || (this.dijitArgs && this.dijitArgs.required))) {
563 // a required dijit is not given any styling to indicate the value
564 // is invalid until the user has focused the widget then left it with
565 // invalid data. This change tells dojo to pretend this focusing has
566 // already happened so we can style required widgets during page render.
567 this.widget._hasBeenBlurred = true;
568 this.widget.validate();
572 _buildOrgSelector : function() {
573 dojo.require('fieldmapper.OrgUtils');
574 dojo.require('openils.widget.FilteringTreeSelect');
575 this.widget = new openils.widget.FilteringTreeSelect(this.dijitArgs, this.parentNode);
576 this.widget.searchAttr = 'shortname';
577 this.widget.labelAttr = 'shortname';
578 this.widget.parentField = 'parent_ou';
579 var user = new openils.User();
581 if(this.widgetValue == null && this.orgDefaultsToWs)
582 this.widgetValue = user.user.ws_ou();
584 // if we have a limit perm, find the relevent orgs (async)
585 if(this.orgLimitPerms && this.orgLimitPerms.length > 0) {
588 user.getPermOrgList(this.orgLimitPerms,
590 self.widget.tree = orgList;
591 self.widget.startup();
592 self._widgetLoaded();
597 this.widget.tree = fieldmapper.aou.globalOrgTree;
598 this.widget.startup();
604 _buildPermGrpSelector : function() {
605 dojo.require('openils.widget.FilteringTreeSelect');
606 this.widget = new openils.widget.FilteringTreeSelect(this.dijitArgs, this.parentNode);
607 this.widget.disableQuery = this.disableQuery;
608 this.widget.searchAttr = 'name';
610 if(this.cache.permGrpTree) {
611 this.widget.tree = this.cache.permGrpTree;
612 this.widget.startup();
613 this._widgetLoaded();
619 new openils.PermaCrud().retrieveAll('pgt', {
620 async : !this.forceSync,
621 oncomplete : function(r) {
622 var list = openils.Util.readResponse(r, false, true);
627 map[list[l].id()] = list[l];
630 var pnode = map[node.parent()];
631 if(!pnode) {root = node; continue;}
632 if(!pnode.children()) pnode.children([]);
633 pnode.children().push(node);
635 self.widget.tree = self.cache.permGrpTree = root;
636 self.widget.startup();
637 self._widgetLoaded();
644 _buildCopyLocSelector : function() {
645 dojo.require('dijit.form.FilteringSelect');
646 this.widget = new dijit.form.FilteringSelect(this.dijitArgs, this.parentNode);
647 this.widget.searchAttr = this.widget.labalAttr = 'name';
648 this.widget.valueAttr = 'id';
650 if(this.cache.copyLocStore) {
651 this.widget.store = this.cache.copyLocStore;
652 this.widget.startup();
658 var ws_ou = openils.User.user.ws_ou();
659 var orgs = fieldmapper.aou.findOrgUnit(ws_ou).orgNodeTrail().map(function (i) { return i.id() });
660 orgs = orgs.concat(fieldmapper.aou.descendantNodeList(ws_ou).map(function (i) { return i.id() }));
663 new openils.PermaCrud().search('acpl', {owning_lib : orgs}, {
664 async : !this.forceSync,
665 oncomplete : function(r) {
666 var list = openils.Util.readResponse(r, false, true);
669 new dojo.data.ItemFileReadStore({data:fieldmapper.acpl.toStoreData(list)});
670 self.cache.copyLocStore = self.widget.store;
671 self.widget.startup();
672 self._widgetLoaded();
679 _buildAutoCompleteSelector : function(linkClass, searchAttr) {
680 dojo.require("openils.widget.PCrudAutocompleteBox");
681 dojo.mixin(this.dijitArgs, {
683 searchAttr : searchAttr,
685 this.widget = new openils.widget.PCrudAutocompleteBox(this.dijitArgs, this.parentNode);
686 this._widgetLoaded();
691 openils.widget.AutoFieldWidget.localeStrings = dojo.i18n.getLocalization("openils.widget", "AutoFieldWidget");
692 openils.widget.AutoFieldWidget.cache = {};
693 openils.widget.AutoFieldWidget.defaultLinkedDataLoader = {};