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 if(!this.dijitArgs.constraints) {
275 this.dijitArgs.constraints = {};
277 if(!this.dijitArgs.constraints.datePattern) {
278 var user = new openils.User().user;
280 var datePattern = fieldmapper.aou.fetchOrgSettingDefault(user.ws_ou(), 'format.date');
281 if(datePattern) this.dijitArgs.constraints.datePattern = datePattern.value;
284 this.widget = new dijit.form.DateTextBox(this.dijitArgs, this.parentNode);
285 if (this.widgetValue != null) {
286 this.widgetValue = openils.Util.timeStampAsDateObj(
293 if(this.ternary || this.inherits) {
294 dojo.require('dijit.form.FilteringSelect');
295 var store = new dojo.data.ItemFileReadStore({
297 identifier : 'value',
299 {label : (this.inherits ? openils.widget.AutoFieldWidget.localeStrings.INHERITED : openils.widget.AutoFieldWidget.localeStrings.UNSET), value : 'unset'},
300 {label : openils.widget.AutoFieldWidget.localeStrings.TRUE, value : 'true'},
301 {label : openils.widget.AutoFieldWidget.localeStrings.FALSE, value : 'false'}
305 this.widget = new dijit.form.FilteringSelect(this.dijitArgs, this.parentNode);
306 this.widget.searchAttr = this.widget.labelAttr = 'label';
307 this.widget.valueAttr = 'value';
308 this.widget.store = store;
309 this.widget.startup();
310 this.widgetValue = (this.widgetValue === null) ? 'unset' :
311 (openils.Util.isTrue(this.widgetValue)) ? 'true' : 'false';
313 dojo.require('dijit.form.CheckBox');
314 this.widget = new dijit.form.CheckBox(this.dijitArgs, this.parentNode);
315 this.widgetValue = openils.Util.isTrue(this.widgetValue);
320 if(this._buildLinkSelector()) break;
323 if(this.dijitArgs && (this.dijitArgs.required || this.dijitArgs.regExp)) {
324 dojo.require('dijit.form.ValidationTextBox');
325 this.widget = new dijit.form.ValidationTextBox(this.dijitArgs, this.parentNode);
327 dojo.require('dijit.form.TextBox');
328 this.widget = new dijit.form.TextBox(this.dijitArgs, this.parentNode);
333 if(!this.async) this._widgetLoaded();
337 // we want to display the value for our widget. However, instead of displaying
338 // an ID, for exmaple, display the value for the 'selector' field on the object
340 _tryLinkedDisplayField : function(noAsync) {
342 if(this.idlField.datatype == 'org_unit')
343 return false; // we already handle org_units, no need to re-fetch
345 // user opted to bypass fetching this linked data
346 if(this.suppressLinkedFields.indexOf(this.idlField.name) > -1)
349 var linkInfo = this._getLinkSelector();
350 if(!(linkInfo && linkInfo.vfield && linkInfo.vfield.selector))
352 var lclass = linkInfo.linkClass;
357 // first try the store cache
359 if(this.cache[this.auth].list[lclass]) {
360 var store = this.cache[this.auth].list[lclass];
362 query[linkInfo.vfield.name] = ''+this.widgetValue;
364 store.fetch({query:query, onComplete:
368 if(self.labelFormat) {
369 self.widgetValue = self._applyLabelFormat(item, self.labelFormat);
371 self.widgetValue = store.getValue(item, linkInfo.vfield.selector);
381 // then try the single object cache
382 if(this.cache[this.auth].single[lclass] &&
383 this.cache[this.auth].single[lclass][this.widgetValue] &&
384 this.cache[this.auth].single[lclass][this.widgetValue][self.labelFormat || '']) {
385 this.widgetValue = this.cache[this.auth].single[lclass][this.widgetValue][self.labelFormat || ''];
389 console.log("Fetching sync object " + lclass + " : " + this.widgetValue);
391 // if those fail, fetch the linked object
394 new openils.PermaCrud().retrieve(lclass, this.widgetValue, {
395 async : !this.forceSync,
396 oncomplete : function(r) {
397 var item = openils.Util.readResponse(r);
399 var newvalue = item[linkInfo.vfield.selector]();
401 var labelCacheKey = '';
403 if(self.labelFormat) {
404 labelCacheKey = self.labelFormat;
405 self.widgetValue = self._applyLabelFormat(item.toStoreItem(), self.labelFormat);
407 self.widgetValue = newvalue;
410 if(!self.cache[self.auth].single[lclass])
411 self.cache[self.auth].single[lclass] = {};
412 if(!self.cache[self.auth].single[lclass][self.widgetValue])
413 self.cache[self.auth].single[lclass][self.widgetValue] = {};
414 self.cache[self.auth].single[lclass][self.widgetValue][labelCacheKey] = newvalue;
416 self.widget.startup();
417 self._widgetLoaded();
422 _getLinkSelector : function() {
423 var linkClass = this.idlField['class'];
424 if(this.idlField.reltype != 'has_a') return false;
425 if(!fieldmapper.IDL.fmclasses[linkClass]) // class neglected by AutoIDL
426 fieldmapper.IDL.load([linkClass]);
427 if(!fieldmapper.IDL.fmclasses[linkClass].permacrud) return false;
428 if(!fieldmapper.IDL.fmclasses[linkClass].permacrud.retrieve) return false;
431 var rclassIdl = fieldmapper.IDL.fmclasses[linkClass];
433 for(var f in rclassIdl.fields) {
434 if(this.idlField.key == rclassIdl.fields[f].name) {
435 vfield = rclassIdl.fields[f];
441 throw new Error("'" + linkClass + "' has no '" + this.idlField.key + "' field!");
444 linkClass : linkClass,
449 _applyLabelFormat : function (item, formatList) {
453 // formatList[1..*] are names of fields. Pull the field
454 // values from each object to determine the values for string substitution
456 var format = formatList[0];
457 for(var i = 1; i< formatList.length; i++)
458 values.push(item[formatList[i]]);
460 return dojo.string.substitute(format, values);
464 "openils.widget.AutoFieldWidget: Invalid formatList ["+formatList+"] : "+E);
468 _buildLinkSelector : function() {
470 var selectorInfo = this._getLinkSelector();
471 if(!selectorInfo) return false;
473 var linkClass = selectorInfo.linkClass;
474 var vfield = selectorInfo.vfield;
478 if(linkClass == 'pgt')
479 return this._buildPermGrpSelector();
480 if(linkClass == 'aou')
481 return this._buildOrgSelector();
482 if(linkClass == 'acpl')
483 return this._buildCopyLocSelector();
484 if(linkClass == 'acqpro')
485 return this._buildAutoCompleteSelector(linkClass, vfield.selector);
488 dojo.require('dojo.data.ItemFileReadStore');
489 dojo.require('dijit.form.FilteringSelect');
491 this.widget = new dijit.form.FilteringSelect(this.dijitArgs, this.parentNode);
492 this.widget.searchAttr = this.widget.labelAttr = vfield.selector || vfield.name;
493 this.widget.valueAttr = vfield.name;
494 this.widget.attr('disabled', true);
496 var oncomplete = function(list) {
497 self.widget.attr('disabled', false);
500 self.widget.labelAttr = '_label';
502 if(self.searchFormat)
503 self.widget.searchAttr = '_search';
506 var storeData = {data:fieldmapper[linkClass].toStoreData(list)};
508 if(self.labelFormat) {
509 dojo.forEach(storeData.data.items,
511 item._label = self._applyLabelFormat(item, self.labelFormat);
516 if(self.searchFormat) {
517 dojo.forEach(storeData.data.items,
519 item._search = self._applyLabelFormat(item, self.searchFormat);
524 self.widget.store = new dojo.data.ItemFileReadStore(storeData);
525 self.cache[self.auth].list[linkClass] = self.widget.store;
528 self.widget.store = self.cache[self.auth].list[linkClass];
531 self.widget.startup();
532 self._widgetLoaded();
535 if(!this.noCache && this.cache[self.auth].list[linkClass]) {
540 if(!this.dataLoader && openils.widget.AutoFieldWidget.defaultLinkedDataLoader[linkClass])
541 this.dataLoader = openils.widget.AutoFieldWidget.defaultLinkedDataLoader[linkClass];
543 if(this.dataLoader) {
545 // caller provided an external function for retrieving the data
546 this.dataLoader(linkClass, this.searchFilter, oncomplete);
550 var _cb = function(r) {
551 oncomplete(openils.Util.readResponse(r, false, true));
554 this.searchOptions = dojo.mixin(
555 {async : !this.forceSync, oncomplete : _cb},
559 if (this.searchFilter) {
560 new openils.PermaCrud().search(linkClass, this.searchFilter, this.searchOptions);
562 new openils.PermaCrud().retrieveAll(linkClass, this.searchOptions);
571 * For widgets that run asynchronously, provide a callback for finishing up
573 _widgetLoaded : function(value) {
577 /* -------------------------------------------------------------
578 when using widgets in a grid, the cell may dissapear, which
579 kills the underlying DOM node, which causes this to fail.
580 For now, back out gracefully and let grid getters use
581 getDisplayString() instead
582 -------------------------------------------------------------*/
584 this.baseWidgetValue(this.getDisplayString());
589 this.baseWidgetValue(this.widgetValue);
590 if(this.idlField.name == this.fmIDL.pkey && this.fmIDL.pkey_sequence && (!this.selfReference && !this.noDisablePkey))
591 this.widget.attr('disabled', true);
592 if(this.disableWidgetTest && this.disableWidgetTest(this.idlField.name, this.fmObject))
593 this.widget.attr('disabled', true);
596 this.onload(this.widget, this);
598 if(!this.readOnly && (this.idlField.required || (this.dijitArgs && this.dijitArgs.required))) {
599 // a required dijit is not given any styling to indicate the value
600 // is invalid until the user has focused the widget then left it with
601 // invalid data. This change tells dojo to pretend this focusing has
602 // already happened so we can style required widgets during page render.
603 this.widget._hasBeenBlurred = true;
604 if(this.widget.validate)
605 this.widget.validate();
609 _buildOrgSelector : function() {
610 dojo.require('fieldmapper.OrgUtils');
611 dojo.require('openils.widget.FilteringTreeSelect');
612 this.widget = new openils.widget.FilteringTreeSelect(this.dijitArgs, this.parentNode);
613 this.widget.searchAttr = this.searchAttr || 'shortname';
614 this.widget.labelAttr = this.searchAttr || 'shortname';
615 this.widget.parentField = 'parent_ou';
616 var user = new openils.User();
618 if(this.widgetValue == null && this.orgDefaultsToWs)
619 this.widgetValue = user.user.ws_ou();
621 // if we have a limit perm, find the relevent orgs (async)
622 if(this.orgLimitPerms && this.orgLimitPerms.length > 0) {
625 user.getPermOrgList(this.orgLimitPerms,
627 self.widget.tree = orgList;
628 self.widget.startup();
629 self._widgetLoaded();
634 this.widget.tree = fieldmapper.aou.globalOrgTree;
635 this.widget.startup();
641 _buildPermGrpSelector : function() {
642 dojo.require('openils.widget.FilteringTreeSelect');
643 this.widget = new openils.widget.FilteringTreeSelect(this.dijitArgs, this.parentNode);
644 this.widget.disableQuery = this.disableQuery;
645 this.widget.searchAttr = 'name';
647 if(this.cache.permGrpTree) {
648 this.widget.tree = this.cache.permGrpTree;
649 this.widget.startup();
650 this._widgetLoaded();
656 new openils.PermaCrud().retrieveAll('pgt', {
657 async : !this.forceSync,
658 oncomplete : function(r) {
659 var list = openils.Util.readResponse(r, false, true);
664 map[list[l].id()] = list[l];
667 var pnode = map[node.parent()];
668 if(!pnode) {root = node; continue;}
669 if(!pnode.children()) pnode.children([]);
670 pnode.children().push(node);
672 self.widget.tree = self.cache.permGrpTree = root;
673 self.widget.startup();
674 self._widgetLoaded();
681 _buildCopyLocSelector : function() {
682 dojo.require('dijit.form.FilteringSelect');
683 this.widget = new dijit.form.FilteringSelect(this.dijitArgs, this.parentNode);
684 this.widget.searchAttr = this.widget.labalAttr = 'name';
685 this.widget.valueAttr = 'id';
687 if(this.cache.copyLocStore) {
688 this.widget.store = this.cache.copyLocStore;
689 this.widget.startup();
695 var ws_ou = openils.User.user.ws_ou();
696 var orgs = fieldmapper.aou.findOrgUnit(ws_ou).orgNodeTrail().map(function (i) { return i.id() });
697 orgs = orgs.concat(fieldmapper.aou.descendantNodeList(ws_ou).map(function (i) { return i.id() }));
700 new openils.PermaCrud().search('acpl', {owning_lib : orgs}, {
701 async : !this.forceSync,
702 oncomplete : function(r) {
703 var list = openils.Util.readResponse(r, false, true);
706 new dojo.data.ItemFileReadStore({data:fieldmapper.acpl.toStoreData(list)});
707 self.cache.copyLocStore = self.widget.store;
708 self.widget.startup();
709 self._widgetLoaded();
716 _buildAutoCompleteSelector : function(linkClass, searchAttr) {
717 dojo.require("openils.widget.PCrudAutocompleteBox");
718 dojo.mixin(this.dijitArgs, {
720 searchAttr : searchAttr,
722 this.widget = new openils.widget.PCrudAutocompleteBox(this.dijitArgs, this.parentNode);
723 this._widgetLoaded();
728 openils.widget.AutoFieldWidget.localeStrings = dojo.i18n.getLocalization("openils.widget", "AutoFieldWidget");
729 openils.widget.AutoFieldWidget.cache = {};
730 openils.widget.AutoFieldWidget.defaultLinkedDataLoader = {};