1 /*jshint node:true, indent:2, curly:true, eqeqeq:true, immed:true, latedef:true, newcap:true, noarg:true,
2 regexp:true, undef:true, trailing:true, white:true, strict:false */
3 /*global XT:true, XV:true, XM:true, Backbone:true, enyo:true, _:true, X:true */
8 @name XV.RelationWidget
9 @class A picker control that implements a dropdown list of items which can be selected.<br />
10 Unlike the {@link XV.PickerWidget}, the collection is not stored local to the widget.<br />
11 Derived from <a href="http://enyojs.com/api/#enyo.Control">enyo.Control</a>.
15 /** @lends XV.RelationWidget# */{
16 name: "XV.RelationWidget",
17 // TODO: needs to inherit from InputWidget
19 classes: "xv-input xv-relationwidget",
29 This can be a string or an array. If an array, all attributes
30 will be searched, the first will be the value used in the widget.
32 keyAttribute: "number",
34 nameAttribute: "name",
36 additionalAttribute: "",
37 // some widgets do not want to filter the completer based on the text of the input
38 skipCompleterFilter: false,
39 query: null, // whatever the subkind wants it to be,
40 keySearch: false // Attach a misc special parameter that can used by the server
48 onModelChange: "modelChanged",
49 onSelect: "itemSelected"
52 {controlClasses: 'enyo-inline', components: [
53 {name: "label", classes: "xv-label"},
54 {kind: "onyx.InputDecorator", name: "decorator", classes: "xv-icon-decorator", components: [
55 {name: "input", kind: "onyx.Input",
56 onkeyup: "keyUp", onkeydown: "keyDown", onblur: "receiveBlur",
57 onfocus: "receiveFocus"},
58 {kind: "onyx.MenuDecorator", components: [
59 {kind: "onyx.IconButton", classes: "icon-folder-open-alt"},
60 {kind: "onyx.Menu", name: 'popupMenu', floating: true, components: [
61 {kind: "XV.MenuItem", name: "searchItem", content: "_search".loc()},
62 {kind: "XV.MenuItem", name: "openItem", content: "_open".loc(), disabled: true},
63 {kind: "XV.MenuItem", name: "newItem", content: "_new".loc(), disabled: true}
66 {name: "completer", kind: "XV.Completer"}
69 {name: 'descriptionContainer', classes: 'xv-invisible-wrapper', components: [
70 {controlClasses: 'enyo-inline', components: [
71 {name: "name", classes: "xv-description"},
72 {name: "description", classes: "xv-description"}
77 Add a parameter to the query object on the widget. Parameter conventions should
78 follow those described in the documentation for `XM.Collection`.
80 @seealso XM.Collection
81 @param {Object} Parameter
82 @param {Boolean} Editable - default false. If true user can edit parameter selection in search.
83 @returns {Object} Receiver
85 addParameter: function (param, editable) {
86 var query = this.getQuery() || {},
87 attr = param.attribute;
89 // Update the query with new parameter
90 if (query.parameters) {
91 query.parameters = _.filter(query.parameters, function (p) {
92 return !_.isEqual(p.attribute, attr);
94 } else { query.parameters = []; }
95 query.parameters.push(param);
98 // Keep track of editable parameters
100 this._editableParams = _.without(this._editableParams, attr);
101 } else if (!_.contains(this._editableParams, attr)) {
102 this._editableParams.push(attr);
107 Fill the value with the selected choice.
109 autocomplete: function (callback) {
110 var key = this.getKeyAttribute(),
111 value = this.getValue(),
112 inputValue = this.$.input.getValue(),
115 if (_.isString(key)) {
116 attrs = value ? value.get(key) : "";
117 changed = inputValue !== attrs;
119 // key must be array. Handle it.
120 _.each(key, function (str) {
121 var attr = value ? value.get(str) : "";
124 // if changed or inputValue doesn't match an attr
125 changed = changed || !(_.find(attrs, function (attr) {return inputValue === attr; }));
128 if (inputValue && changed) {
129 this.fetchCollection(inputValue, 1, "_fetchSuccess");
130 } else if (!inputValue) {
137 clear: function (options) {
138 this.setValue(null, options);
140 create: function () {
141 this.inherited(arguments);
142 this._editableParams = [];
145 this.disabledChanged();
148 @todo Document the disabledChanged method.
150 disabledChanged: function () {
151 var disabled = this.getDisabled();
152 this.$.input.setDisabled(disabled);
153 this.$.searchItem.setDisabled(disabled);
154 this.$.newItem.setDisabled(_couldNotCreate.apply(this) || disabled);
155 this.$.label.addRemoveClass("disabled", disabled);
157 _.each(this.$.descriptionContainer.$, function (component) {
158 component.addRemoveClass("disabled", disabled);
163 Used by both autocomplete and keyup.
165 fetchCollection: function (value, rowLimit, callbackName) {
166 var key = this.getKeyAttribute(),
170 operator: "BEGINS_WITH",
172 keySearch: this.keySearch
177 // selectively copy this.getQuery() into a new object. We can't
178 // use a dumb deep copy because the models in there might explode
179 if (this.getQuery()) {
180 if (this.getQuery().orderBy) {
181 query.orderBy = JSON.parse(JSON.stringify(this.getQuery().orderBy));
183 if (this.getQuery().parameters) {
184 query.parameters = [];
185 _.each(this.getQuery().parameters, function (param) {
186 query.parameters.push(param);
191 if (_.isString(key)) {
192 orderBy = [{attribute: key}];
195 _.each(key, function (attr) {
196 orderBy.push({attribute: attr});
200 // Add key search to our query
201 if (query.parameters) {
202 query.parameters.push(param);
204 query.parameters = [param];
207 // Add sort to our query
209 query.orderBy = query.orderby.concat(orderBy);
211 query.orderBy = orderBy;
214 query.rowLimit = rowLimit || 10;
216 this._collection.fetch({
217 success: enyo.bind(this, callbackName),
222 @todo Document the focus method.
225 this.$.input.focus();
227 getFirstKey: function () {
228 var key = this.getKeyAttribute();
229 return _.isString(key) ? key : key[0];
232 @todo Document the getValueToString method.
234 getValueToString: function () {
235 return this.value.getValue(this.getFirstKey());
238 @todo Revisit or remove after ENYO-1104 is resolved.
240 keyDown: function (inSender, inEvent) {
241 this.inherited(arguments);
244 inEvent.activator = this.$.decorator;
245 if (inEvent.keyCode === 9) {
246 this.$.completer.waterfall("onRequestHideMenu", inEvent);
251 We will typically want to query the database upon every keystroke
253 keyUp: function (inSender, inEvent) {
254 var key = this.getFirstKey(),
255 attr = this.getValue() ? this.getValue().get(key) : "",
256 value = this.$.input.getValue(),
257 completer = this.$.completer;
259 inEvent.activator = this.$.decorator;
261 // Look up if value changed
262 if (value && value !== attr && inEvent.keyCode !== 9) {
263 this.fetchCollection(value, 10, "_collectionFetchSuccess");
265 completer.waterfall("onRequestHideMenu", inEvent);
269 @todo Document the itemSelected method.
271 itemSelected: function (inSender, inEvent) {
272 if (inEvent.originator.kind === 'onyx.MenuItem') {
273 this.relationSelected(inSender, inEvent);
275 this.menuItemSelected(inSender, inEvent);
280 @todo Document the labelChanged method.
282 labelChanged: function () {
283 var label = (this.getLabel() || ("_" + this.attr || "").loc());
284 this.$.label.setContent(label + ":");
287 @todo Document the listChanged method.
289 listChanged: function () {
290 var list = this.getList(),
294 delete this._Workspace;
297 if (!list) { return; }
298 this._List = XT.getObjectByName(list);
300 // Get Workspace class
301 workspace = this._List.prototype.getWorkspace();
302 this._Workspace = workspace ? XT.getObjectByName(workspace) : null;
304 // Setup collection instance
305 Collection = this.getCollection() ?
306 XT.getObjectByName(this.getCollection()) : null;
307 if (!Collection) { return; }
308 this._collection = new Collection();
311 @todo Document the menuItemSelected method.
313 menuItemSelected: function (inSender, inEvent) {
315 menuItem = inEvent.originator,
316 list = this.getList(),
317 model = this.getValue(),
318 id = model ? model.id : null,
319 workspace = this._List ? this._List.prototype.getWorkspace() : null,
320 query = this.getQuery() || {},
325 // Convert query to parameter widget friendly
326 _.each(query.parameters, function (param) {
327 // If attribute isn't explicitly set to `editable` then hide any matching control
328 // and pass as fixed condition.
329 if (!_.contains(that._editableParams, param.attribute)) {
330 params.push({name: param.attribute, showing: false});
331 conditions.push(param);
332 // Otherwise user can change value so pass default
334 params.push({name: param.attribute, value: param.value});
338 switch (menuItem.name)
341 callback = function (value) {
342 that.setValue(value);
347 parameterItemValues: params,
348 conditions: conditions,
349 query: this.getQuery()
354 workspace: workspace,
360 // Callback options on commit of the workspace
361 // Find the model with matching id, fetch and set it.
362 callback = function (model) {
363 if (!model) { return; }
364 var Model = that._collection.model,
368 options.success = function () {
369 that.setValue(value);
371 attrs[Model.prototype.idAttribute] = model.id;
372 value = Model.findOrCreate(attrs);
373 value.fetch(options);
376 workspace: workspace,
384 @todo Document the modelChanged method.
386 modelChanged: function (inSender, inEvent) {
389 workspace = List.prototype.getWorkspace(),
390 Workspace = workspace ? XT.getObjectByName(workspace) : null,
392 // If the model that changed was related to and exists on this widget
393 // refresh the local model.
394 if (!List || !Workspace || !this.value) {
397 if (Workspace.prototype.model === inEvent.model &&
398 this.value.id === inEvent.id) {
399 options.success = function () {
400 that.setValue(this.value);
402 this.value.fetch(options);
406 When we get a placeholder we want to actually display it.
408 placeholderChanged: function () {
409 var placeholder = this.getPlaceholder();
410 this.$.input.setPlaceholder(placeholder);
413 @todo Document the receiveBlur method.
415 receiveBlur: function (inSender, inEvent) {
417 this._hasFocus = false;
420 @todo Document the receiveFocus method.
422 receiveFocus: function (inSender, inEvent) {
423 this._hasFocus = true;
424 this._relationSelected = false;
427 @todo Document the relationSelected method.
429 relationSelected: function (inSender, inEvent) {
430 this._relationSelected = true;
431 inEvent.activator = this.$.decorator;
432 this.setValue(inEvent.originator.model);
433 this.$.completer.waterfall("onRequestHideMenu", inEvent);
437 Removes a query parameter by attribute name from the widget's query object.
439 @param {String} Attribute
440 @returns {Object} Receiver
442 removeParameter: function (attr) {
443 var query = this.getQuery();
444 if (query && query.parameters) {
445 query.parameters = _.filter(query.parameters, function (param) {
446 return param.attribute !== attr;
449 this.setQuery(query);
450 this._editableParams = _.without(this._editableParams, attr);
455 Programatically sets the value of this widget. If an id is set then the
456 correct model will be fetched and this function will be called again
457 recursively with the model.
459 *In case of "order" models, each order's workspace is different depending on the order type.
460 This causes the Workspace var to be null so get the value from the getWorkspace
461 defined on the relation.js sub-kind.
463 @param {XM.Model|Number|String} Value; can be a model or the id of a model.
464 @param {Object} options
466 setValue: function (value, options) {
467 options = options || {};
469 newId = value ? value.id : null,
470 oldId = this.value ? this.value.id : null,
471 key = this.getFirstKey(),
472 name = this.getNameAttribute(),
473 descrip = this.getDescripAttribute(),
474 additional = this.getAdditionalAttribute(),
475 inEvent = { value: value, originator: this },
479 additionalValue = "",
480 Workspace = this._Workspace || (newId ? this._List.prototype.getWorkspace(value) : null),
481 Model = this._collection.model,
484 listName = this.getList(),
489 setPrivileges = function () {
490 if (value && newId) {
491 if (value.couldRead) {
492 that.$.openItem.setDisabled(!value.couldRead());
494 that.$.openItem.setDisabled(!value.getClass().canRead());
500 List = XT.getObjectByName(listName);
501 CollectionName = List.prototype.collection;
502 Collection = XT.getObjectByName(CollectionName);
503 ListModel = Collection.prototype.model;
506 // Make sure we get are setting the right kind of object here.
507 // If we're being sent a number or a string, or something that's not a model
508 // then we assume that's the key, we fetch the model, and call this function
509 // again and give it the model for real.
510 // Of course, if the value is falsy, we don't want to fetch a model,
511 // we just want to continue (which will empty out the relation widget)
512 if (value && (_.isNumber(value) || _.isString(value) || !(value instanceof Model))) {
513 if (this.value === value || oldId === value) { return; }
515 // If we got here, but it is not the list's model type, we can't work with this
516 if (!ListModel || (value instanceof XM.Model && !(value instanceof ListModel))) {
517 throw "The type of model passed is incompatible with this widget";
520 id = _.isObject(value) ? value.id : value;
522 newValue = new Model();
525 success: function () {
526 that.setValue(newValue);
529 XT.log("Error setting relational widget value");
532 newValue.fetch(options);
537 if (value && value.getValue) {
538 keyValue = value.getValue(key) || "";
539 nameValue = value.getValue(name) || "";
540 descripValue = value.getValue(descrip) || "";
541 additionalValue = value.getValue(additional) || "";
543 this.$.input.setValue(keyValue);
545 this.$.name.setShowing(nameValue);
546 this.$.name.setContent(nameValue);
547 this.$.description.setShowing(descripValue);
548 this.$.description.setContent(descripValue);
550 // Only notify if selection actually changed
551 if (newId !== oldId && !options.silent) { this.doValueChange(inEvent); }
553 // Handle menu actions
554 that.$.openItem.setShowing(Workspace);
555 that.$.newItem.setShowing(Workspace);
556 that.$.openItem.setDisabled(true);
557 that.$.newItem.setDisabled(_couldNotCreate.apply(this) || this.disabled);
558 if (Model && Workspace) {
562 XT.getStartupManager().registerCallback(setPrivileges);
568 _collectionFetchSuccess: function () {
569 if (!this._hasFocus) { return; }
570 var key = this.getKeyAttribute(),
571 sidecarKey = this.getSidecarAttribute(),
572 value = this.$.input.getValue(),
573 models = this._collection.models,
574 inEvent = { activator: this.$.decorator };
576 this.$.completer.buildList(key, value, models, sidecarKey, this.skipCompleterFilter);
577 if (!this.$.completer.showing) {
578 this.$.completer.waterfall("onRequestShowMenu", inEvent);
580 this.$.completer.adjustPosition();
582 this.$.completer.waterfall("onRequestHideMenu", inEvent);
587 _fetchSuccess: function () {
588 if (this._relationSelected) { return; }
589 var value = this._collection.length ? this._collection.models[0] : null,
590 target = enyo.dispatcher.captureTarget;
591 this.setValue(value);
592 enyo.dispatcher.captureTarget = target;
598 var _couldNotCreate = function () {
599 var Workspace = this._Workspace,
600 Model = this._collection.model,
601 couldNotCreate = true;
603 if (Model && Model.couldCreate) {
604 // model is a list item or relation
605 couldNotCreate = !Model.couldCreate();
607 // model is a first-class model
608 couldNotCreate = !Model.canCreate();
610 return couldNotCreate;