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.require('dojo.data.ItemFileReadStore');
8 dojo.requireLocalization("openils.widget", "AutoFieldWidget");
10 dojo.declare('openils.widget.AutoFieldWidget', null, {
16 * idlField -- Field description object from fieldmapper.IDL.fmclasses
17 * fmObject -- If available, the object being edited. This will be used
18 * to set the value of the widget.
19 * fmClass -- Class name (not required if idlField or fmObject is set)
20 * fmField -- Field name (not required if idlField)
21 * parentNode -- If defined, the widget will be appended to this DOM node
22 * dijitArgs -- Optional parameters object, passed directly to the dojo widget
23 * orgLimitPerms -- If this field defines a set of org units and an orgLimitPerms
24 * is defined, the code will limit the org units in the set to those
25 * allowed by the permission
26 * orgDefaultsToWs -- If this is an org unit field and the widget has no value,
27 * set the value equal to the users's workstation org unit. Othwerwise, leave it null
28 * selfReference -- The primary purpose of an AutoFieldWidget is to render the value
29 * or widget for a field on an object (that may or may not link to another object).
30 * selfReference allows you to sidestep the indirection and create a selector widget
31 * based purely on an fmClass. To get a dropdown of all of the 'abc'
32 * objects, pass in {selfReference : true, fmClass : 'abc'}.
33 * labelFormat -- For widgets that are displayed as remote object filtering selects,
34 * this provides a mechanism for overriding the label format in the filtering select.
35 * It must be an array, whose first value is a format string, compliant with
36 * dojo.string.substitute. The remaining array items are the arguments to the format
37 * represented as field names on the remote linked object.
39 * labelFormat : [ '${0} (${1})', 'obj_field_1', 'obj_field_2' ]
40 * Note: this does not control the final display value. Only values in the drop-down.
41 * See searchFormat for controlling the display value
42 * searchFormat -- This format controls the structure of the search attribute which
43 * controls the text used during type-ahead searching and the displayed value in
44 * the filtering select. See labelFormat for the structure.
45 * dataLoader : Bypass the default PermaCrud linked data fetcher and use this function instead.
46 * Function arguments are (link class name, search filter, callback)
47 * The fetched objects should be passed to the callback as an array
48 * disableQuery : dojo.data query passed to FilteringTreeSelect-based widgets to disable
49 * (but leave visible) certain options.
50 * useWriteStore : tells AFW to use a dojo.data.ItemFileWriteStore instead of a ReadStore for
51 * data stores created with dynamic data. This allows the caller to add/remove items from
54 constructor : function(args) {
58 if (!this.dijitArgs) {
61 this.dijitArgs['scrollOnFocus'] = false;
64 // find the field description in the IDL if not provided
66 this.fmClass = this.fmObject.classname;
67 this.fmIDL = fieldmapper.IDL.fmclasses[this.fmClass];
69 if(this.fmClass && !this.fmIDL) {
70 fieldmapper.IDL.load([this.fmClass]);
71 this.fmIDL = fieldmapper.IDL.fmclasses[this.fmClass];
74 this.suppressLinkedFields = args.suppressLinkedFields || [];
76 if(this.selfReference) {
77 this.fmField = fieldmapper.IDL.fmclasses[this.fmClass].pkey;
79 // create a mock-up of the idlField object.
82 'class' : this.fmClass,
91 this.fmIDL = fieldmapper.IDL.fmclasses[this.fmClass];
92 var fields = this.fmIDL.fields;
94 if(fields[f].name == this.fmField)
95 this.idlField = fields[f];
100 throw new Error("AutoFieldWidget could not determine which " +
101 "field to render. We need more information. fmClass=" +
102 this.fmClass + ' fmField=' + this.fmField + ' fmObject=' + js2JSON(this.fmObject));
104 this.auth = openils.User.authtoken;
105 this.cache = openils.widget.AutoFieldWidget.cache;
106 this.cache[this.auth] = this.cache[this.auth] || {};
107 this.cache[this.auth].single = this.cache[this.auth].single || {};
108 this.cache[this.auth].list = this.cache[this.auth].list || {};
110 if (this.useWriteStore) {
111 dojo.require('dojo.data.ItemFileWriteStore');
112 this.storeConstructor = dojo.data.ItemFileWriteStore;
114 this.storeConstructor = dojo.data.ItemFileReadStore;
119 * Turn the widget-stored value into a value oils understands
121 getFormattedValue : function() {
122 var value = this.baseWidgetValue();
123 switch(this.idlField.datatype) {
126 case 'true': return 't';
127 case 'on': return 't';
128 case 'false' : return 'f';
129 case 'unset' : return null;
130 case true : return 't';
134 if(!value) return null;
135 return dojo.date.stamp.toISOString(value);
139 if(isNaN(value)) value = null;
141 return (value === '') ? null : value;
145 baseWidgetValue : function(value) {
146 var attr = (this.readOnly) ? 'content' : 'value';
147 if(arguments.length) this.widget.attr(attr, value);
148 return this.widget.attr(attr);
152 * Turn the widget-stored value into something visually suitable
154 getDisplayString : function() {
155 var value = this.widgetValue;
161 return openils.widget.AutoFieldWidget.localeStrings.INHERITED;
164 switch(this.idlField.datatype) {
169 return openils.widget.AutoFieldWidget.localeStrings.TRUE;
172 return openils.widget.AutoFieldWidget.localeStrings.FALSE;
174 case 'unset' : return openils.widget.AutoFieldWidget.localeStrings.UNSET;
175 case true : return openils.widget.AutoFieldWidget.localeStrings.TRUE;
176 default: return openils.widget.AutoFieldWidget.localeStrings.FALSE;
179 if (!value) return '';
180 return openils.Util.timeStamp(
181 value, {"formatLength": "short"}
184 if(value === null || value === undefined) return '';
185 return fieldmapper.aou.findOrgUnit(value).shortname();
188 if(isNaN(value)) value = 0;
190 if(value === undefined || value === null)
196 isRequired : function() {
199 this.idlField.required || (
201 this.dijitArgs.required || this.dijitArgs.regExp
208 build : function(onload) {
210 if(this.widgetValue == null)
211 this.widgetValue = (this.fmObject) ? this.fmObject[this.idlField.name]() : null;
214 // core widget provided for us, attach and move on
215 if(this.parentNode) // may already be in the "right" place
216 this.parentNode.appendChild(this.widget.domNode);
218 if (this.shove.mode == "update") {
219 if (this.idlField.datatype == "timestamp")
220 this.widgetValue = openils.Util.timeStampAsDateObj(
224 this.widgetValue = this.shove.create;
226 this._widgetLoaded();
227 } else if (this.widget.attr("value") == null) {
228 this._widgetLoaded();
233 if(!this.parentNode) // give it somewhere to live so that dojo won't complain
234 this.parentNode = dojo.create('div');
236 this.onload = onload;
239 dojo.require('dijit.layout.ContentPane');
240 this.widget = new dijit.layout.ContentPane(this.dijitArgs, this.parentNode);
241 if(this.widgetValue !== null)
242 this._tryLinkedDisplayField();
244 } else if(this.widgetClass) {
245 dojo.require(this.widgetClass);
246 eval('this.widget = new ' + this.widgetClass + '(this.dijitArgs, this.parentNode);');
250 switch(this.idlField.datatype) {
253 dojo.require('dijit.form.TextBox');
254 this.widget = new dijit.form.TextBox(this.dijitArgs, this.parentNode);
258 this._buildOrgSelector();
262 // dojo.require('dijit.form.CurrencyTextBox');
263 // the CurrencyTextBox dijit is broken in Dojo 1.3; upon upgrading
264 // to Dojo 1.5 or later, should re-evaluate work-around use of dijit.form.NumberTextBox.
265 // See https://bugs.launchpad.net/evergreen/+bug/702117
266 dojo.require('dijit.form.NumberTextBox');
267 this.dijitArgs = dojo.mixin({constraints:{places:'0,2'}}, this.dijitArgs || {});
268 this.widget = new dijit.form.NumberTextBox(this.dijitArgs, this.parentNode);
272 dojo.require('dijit.form.NumberTextBox');
273 this.dijitArgs = dojo.mixin({constraints:{places:0}}, this.dijitArgs || {});
274 this.widget = new dijit.form.NumberTextBox(this.dijitArgs, this.parentNode);
278 dojo.require('dijit.form.NumberTextBox');
279 this.widget = new dijit.form.NumberTextBox(this.dijitArgs, this.parentNode);
283 dojo.require('dijit.form.DateTextBox');
284 dojo.require('dojo.date.stamp');
285 if(!this.dijitArgs.constraints) {
286 this.dijitArgs.constraints = {};
288 if(!this.dijitArgs.constraints.datePattern) {
289 var user = new openils.User().user;
291 var datePattern = fieldmapper.aou.fetchOrgSettingDefault(user.ws_ou(), 'format.date');
292 if(datePattern) this.dijitArgs.constraints.datePattern = datePattern.value;
295 this.widget = new dijit.form.DateTextBox(this.dijitArgs, this.parentNode);
296 if (this.widgetValue != null) {
297 this.widgetValue = openils.Util.timeStampAsDateObj(
304 if(this.ternary || this.inherits) {
305 dojo.require('dijit.form.FilteringSelect');
306 var store = new dojo.data.ItemFileReadStore({
308 identifier : 'value',
310 {label : (this.inherits ? openils.widget.AutoFieldWidget.localeStrings.INHERITED : openils.widget.AutoFieldWidget.localeStrings.UNSET), value : 'unset'},
311 {label : openils.widget.AutoFieldWidget.localeStrings.TRUE, value : 'true'},
312 {label : openils.widget.AutoFieldWidget.localeStrings.FALSE, value : 'false'}
316 this.widget = new dijit.form.FilteringSelect(this.dijitArgs, this.parentNode);
317 this.widget.searchAttr = this.widget.labelAttr = 'label';
318 this.widget.valueAttr = 'value';
319 this.widget.store = store;
320 this.widget.startup();
321 this.widgetValue = (this.widgetValue === null) ? 'unset' :
322 (openils.Util.isTrue(this.widgetValue)) ? 'true' : 'false';
324 dojo.require('dijit.form.CheckBox');
325 this.widget = new dijit.form.CheckBox(this.dijitArgs, this.parentNode);
326 this.widgetValue = openils.Util.isTrue(this.widgetValue);
331 if(this._buildLinkSelector()) break;
334 if(this.dijitArgs && (this.dijitArgs.required || this.dijitArgs.regExp)) {
335 dojo.require('dijit.form.ValidationTextBox');
336 this.widget = new dijit.form.ValidationTextBox(this.dijitArgs, this.parentNode);
338 dojo.require('dijit.form.TextBox');
339 this.widget = new dijit.form.TextBox(this.dijitArgs, this.parentNode);
344 if(!this.async) this._widgetLoaded();
348 // we want to display the value for our widget. However, instead of displaying
349 // an ID, for exmaple, display the value for the 'selector' field on the object
351 _tryLinkedDisplayField : function(noAsync) {
353 if(this.idlField.datatype == 'org_unit')
354 return false; // we already handle org_units, no need to re-fetch
356 // user opted to bypass fetching this linked data
357 if(this.suppressLinkedFields.indexOf(this.idlField.name) > -1)
360 var linkInfo = this._getLinkSelector();
361 if(!(linkInfo && linkInfo.vfield && linkInfo.vfield.selector))
363 var lclass = linkInfo.linkClass;
368 // first try the store cache
370 if(this.cache[this.auth].list[lclass]) {
371 var store = this.cache[this.auth].list[lclass];
373 query[linkInfo.vfield.name] = ''+this.widgetValue;
375 store.fetch({query:query, onComplete:
379 if(self.labelFormat) {
380 self.widgetValue = self._applyLabelFormat(item, self.labelFormat);
382 self.widgetValue = store.getValue(item, linkInfo.vfield.selector);
392 // then try the single object cache
394 if(this.cache[this.auth].single[lclass] && (
395 item = this.cache[this.auth].single[lclass][this.widgetValue]) ) {
397 this.widgetValue = (this.labelFormat) ?
398 this._applyLabelFormat(item.toStoreItem(), this.labelFormat) :
399 item[linkInfo.vfield.selector]();
404 console.log("Fetching linked object " + lclass + " : " + this.widgetValue);
406 // if those fail, fetch the linked object
409 new openils.PermaCrud().retrieve(lclass, this.widgetValue, {
410 async : !this.forceSync,
411 oncomplete : function(r) {
412 var item = openils.Util.readResponse(r);
414 // cache the true object under its real value
415 if(!self.cache[self.auth].single[lclass])
416 self.cache[self.auth].single[lclass] = {};
417 self.cache[self.auth].single[lclass][self.widgetValue] = item;
419 self.widgetValue = (self.labelFormat) ?
420 self._applyLabelFormat(item.toStoreItem(), self.labelFormat) :
421 item[linkInfo.vfield.selector]();
423 self.widget.startup();
424 self._widgetLoaded();
429 _getLinkSelector : function() {
430 var linkClass = this.idlField['class'];
431 if(this.idlField.reltype != 'has_a') return false;
432 if(!fieldmapper.IDL.fmclasses[linkClass]) // class neglected by AutoIDL
433 fieldmapper.IDL.load([linkClass]);
434 if(!fieldmapper.IDL.fmclasses[linkClass].permacrud) return false;
435 if(!fieldmapper.IDL.fmclasses[linkClass].permacrud.retrieve) return false;
438 var rclassIdl = fieldmapper.IDL.fmclasses[linkClass];
440 for(var f in rclassIdl.fields) {
441 if(this.idlField.key == rclassIdl.fields[f].name) {
442 vfield = rclassIdl.fields[f];
448 throw new Error("'" + linkClass + "' has no '" + this.idlField.key + "' field!");
451 linkClass : linkClass,
456 _applyLabelFormat : function (item, formatList) {
460 // formatList[1..*] are names of fields. Pull the field
461 // values from each object to determine the values for string substitution
463 var format = formatList[0];
464 for(var i = 1; i< formatList.length; i++)
465 values.push(item[formatList[i]]);
467 return dojo.string.substitute(format, values);
471 "openils.widget.AutoFieldWidget: Invalid formatList ["+formatList+"] : "+E);
475 _buildLinkSelector : function() {
477 var selectorInfo = this._getLinkSelector();
478 if(!selectorInfo) return false;
480 var linkClass = selectorInfo.linkClass;
481 var vfield = selectorInfo.vfield;
485 if(linkClass == 'pgt')
486 return this._buildPermGrpSelector();
487 if(linkClass == 'aou')
488 return this._buildOrgSelector();
489 if(linkClass == 'acpl'){
492 /* -----------------------------------------------------
493 When the copy location dropdown is in a link context
494 we need to expand the list to the provided permission
496 ------------------------------------------------------*/
497 if(this.orgLimitPerms){
498 var buildCopyLocSelector = this._buildCopyLocSelector;
499 new openils.User().getPermOrgList(
502 orgs = orgs.concat(orgsi);
503 buildCopyLocSelector(orgs,self2);
505 true, true // descendants, id_list
510 return this._buildCopyLocSelector(orgs,self);
512 if(linkClass == 'acqpro')
513 return this._buildAutoCompleteSelector(linkClass, vfield.selector);
516 dojo.require('dojo.data.ItemFileReadStore');
517 dojo.require('dijit.form.FilteringSelect');
519 this.widget = new dijit.form.FilteringSelect(this.dijitArgs, this.parentNode);
520 this.widget.searchAttr = this.widget.labelAttr = vfield.selector || vfield.name;
521 this.widget.valueAttr = vfield.name;
522 this.widget.attr('disabled', true);
524 var oncomplete = function(list) {
525 self.widget.attr('disabled', false);
528 self.widget.labelAttr = '_label';
530 if(self.searchFormat)
531 self.widget.searchAttr = '_search';
534 var storeData = {data:fieldmapper[linkClass].toStoreData(list)};
536 if(self.labelFormat) {
537 dojo.forEach(storeData.data.items,
539 item._label = self._applyLabelFormat(item, self.labelFormat);
544 if(self.searchFormat) {
545 dojo.forEach(storeData.data.items,
547 item._search = self._applyLabelFormat(item, self.searchFormat);
552 self.widget.store = new self.storeConstructor(storeData);
553 self.cache[self.auth].list[linkClass] = self.widget.store;
556 self.widget.store = self.cache[self.auth].list[linkClass];
559 self.widget.startup();
560 self._widgetLoaded();
563 if(!this.noCache && this.cache[self.auth].list[linkClass]) {
568 if(!this.dataLoader && openils.widget.AutoFieldWidget.defaultLinkedDataLoader[linkClass])
569 this.dataLoader = openils.widget.AutoFieldWidget.defaultLinkedDataLoader[linkClass];
571 if(this.dataLoader) {
573 // caller provided an external function for retrieving the data
574 this.dataLoader(linkClass, this.searchFilter, oncomplete);
578 var _cb = function(r) {
579 oncomplete(openils.Util.readResponse(r, false, true));
582 /* XXX LFW: I want to uncomment the following three lines that refer to ob, but haven't had the time to properly test. */
585 //ob[linkClass] = vfield.selector || vfield.name;
587 this.searchOptions = dojo.mixin(
589 async : !this.forceSync,
592 }, this.searchOptions
595 if (this.searchFilter) {
596 new openils.PermaCrud().search(linkClass, this.searchFilter, this.searchOptions);
598 new openils.PermaCrud().retrieveAll(linkClass, this.searchOptions);
607 * For widgets that run asynchronously, provide a callback for finishing up
609 _widgetLoaded : function(value) {
613 /* -------------------------------------------------------------
614 when using widgets in a grid, the cell may dissapear, which
615 kills the underlying DOM node, which causes this to fail.
616 For now, back out gracefully and let grid getters use
617 getDisplayString() instead
618 -------------------------------------------------------------*/
620 this.baseWidgetValue(this.getDisplayString());
625 this.baseWidgetValue(this.widgetValue);
626 if(this.idlField.name == this.fmIDL.pkey && this.fmIDL.pkey_sequence && (!this.selfReference && !this.noDisablePkey))
627 this.widget.attr('disabled', true);
628 if(this.disableWidgetTest && this.disableWidgetTest(this.idlField.name, this.fmObject))
629 this.widget.attr('disabled', true);
632 this.onload(this.widget, this);
634 if(!this.readOnly && (this.idlField.required || (this.dijitArgs && this.dijitArgs.required))) {
635 // a required dijit is not given any styling to indicate the value
636 // is invalid until the user has focused the widget then left it with
637 // invalid data. This change tells dojo to pretend this focusing has
638 // already happened so we can style required widgets during page render.
639 this.widget._hasBeenBlurred = true;
640 if(this.widget.validate)
641 this.widget.validate();
645 _buildOrgSelector : function() {
646 dojo.require('fieldmapper.OrgUtils');
647 dojo.require('openils.widget.FilteringTreeSelect');
648 this.widget = new openils.widget.FilteringTreeSelect(this.dijitArgs, this.parentNode);
649 this.widget.searchAttr = this.searchAttr || 'shortname';
650 this.widget.labelAttr = this.searchAttr || 'shortname';
651 this.widget.parentField = 'parent_ou';
652 var user = new openils.User();
654 if(this.widgetValue == null && this.orgDefaultsToWs)
655 this.widgetValue = user.user.ws_ou();
657 // if we have a limit perm, find the relevent orgs (async)
658 if(this.orgLimitPerms && this.orgLimitPerms.length > 0) {
661 user.getPermOrgList(this.orgLimitPerms,
663 self.widget.tree = orgList;
664 self.widget.startup();
665 self._widgetLoaded();
670 this.widget.tree = fieldmapper.aou.globalOrgTree;
671 this.widget.startup();
677 _buildPermGrpSelector : function() {
678 dojo.require('openils.widget.FilteringTreeSelect');
679 this.widget = new openils.widget.FilteringTreeSelect(this.dijitArgs, this.parentNode);
680 this.widget.disableQuery = this.disableQuery;
681 this.widget.searchAttr = 'name';
683 if(this.cache.permGrpTree) {
684 this.widget.tree = this.cache.permGrpTree;
685 this.widget.startup();
686 this._widgetLoaded();
692 new openils.PermaCrud().retrieveAll('pgt', {
693 async : !this.forceSync,
694 oncomplete : function(r) {
695 var list = openils.Util.readResponse(r, false, true);
700 map[list[l].id()] = list[l];
703 var pnode = map[node.parent()];
704 if(!pnode) {root = node; continue;}
705 if(!pnode.children()) pnode.children([]);
706 pnode.children().push(node);
708 self.widget.tree = self.cache.permGrpTree = root;
709 self.widget.startup();
710 self._widgetLoaded();
717 _buildCopyLocSelector : function(orgs,self) {
718 dojo.require('dijit.form.FilteringSelect');
719 self.widget = new dijit.form.FilteringSelect(self.dijitArgs, self.parentNode);
720 self.widget.searchAttr = self.widget.labalAttr = 'name';
721 self.widget.valueAttr = 'id';
724 var ws_ou = openils.User.user.ws_ou();
725 orgs = orgs.concat(fieldmapper.aou.findOrgUnit(ws_ou).orgNodeTrail().map(function (i) { return i.id() }));
726 orgs = orgs.concat(fieldmapper.aou.descendantNodeList(ws_ou).map(function (i) { return i.id() }));
728 var search = {owning_lib : orgs, deleted : 'f'};
730 if(self.cache.copyLocStore) {
731 var store = self.cache.copyLocStore;
735 // make sure the copy location the caller cares
736 // about (our value) is present in the cache.
737 // if not, fetch the list, adding our value to
738 // the set of locations to fetch.
741 if (self.widgetValue) {
744 onComplete : function(list) {
745 dojo.forEach(list, function(item) {
746 var id = store.getValue(item, 'id');
747 if (id == self.widgetValue)
759 self.widget.store = self.cache.copyLocStore;
760 self.widget.startup();
765 // cached IDs plus id of self.widgetValue;
766 locIds.push(self.widgetValue);
767 search = {id : locIds, deleted: 'f'};
772 new openils.PermaCrud().search('acpl', search, {
773 async : !self.forceSync,
774 order_by : {"acpl": "name"},
775 oncomplete : function(r) {
776 var list = openils.Util.readResponse(r, false, true);
779 // if we are including any copy locations outside our org
780 // unit scope, tag them with a context org unit to prevent
781 // confusion caused by having multiple like-named entries
782 dojo.forEach(list, function(loc) {
784 loc.name(loc.name() + ' (' +
785 fieldmapper.aou.findOrgUnit(loc.owning_lib()).shortname() + ')');
790 new self.storeConstructor({data:fieldmapper.acpl.toStoreData(list)});
791 self.cache.copyLocStore = self.widget.store;
792 self.widget.startup();
793 self._widgetLoaded();
800 _buildAutoCompleteSelector : function(linkClass, searchAttr) {
801 dojo.require("openils.widget.PCrudAutocompleteBox");
802 dojo.mixin(this.dijitArgs, {
804 searchAttr : searchAttr,
806 this.widget = new openils.widget.PCrudAutocompleteBox(this.dijitArgs, this.parentNode);
807 this._widgetLoaded();
812 openils.widget.AutoFieldWidget.localeStrings = dojo.i18n.getLocalization("openils.widget", "AutoFieldWidget");
813 openils.widget.AutoFieldWidget.cache = {};
814 openils.widget.AutoFieldWidget.defaultLinkedDataLoader = {};