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",
18 classes: "xv-input xv-relationwidget",
28 This can be a string or an array. If an array, all attributes
29 will be searched, the first will be the value used in the widget.
31 keyAttribute: "number",
33 nameAttribute: "name",
35 additionalAttribute: "",
36 // some widgets do not want to filter the completer based on the text of the input
37 skipCompleterFilter: false,
38 query: null, // whatever the subkind wants it to be,
39 keySearch: false // Attach a misc special parameter that can used by the server
47 onModelChange: "modelChanged",
48 onSelect: "itemSelected"
51 {kind: "FittableColumns", components: [
52 {name: "label", content: "", fit: true, classes: "xv-flexible-label"},
53 // TODO: Put the InputDecorator and description in a FittableRows
54 {kind: "onyx.InputDecorator", name: "decorator",
55 classes: "xv-input-decorator", components: [
56 {name: 'input', kind: "onyx.Input", classes: "xv-subinput",
57 onkeyup: "keyUp", onkeydown: "keyDown", onblur: "receiveBlur",
58 onfocus: "receiveFocus", fit: true
60 {kind: "onyx.MenuDecorator", components: [
61 {kind: "onyx.IconButton", classes: "icon-folder-open-alt"},
62 {name: 'popupMenu', floating: true, kind: "onyx.Menu",
64 {kind: "XV.MenuItem", name: 'searchItem', content: "_search".loc()},
65 {kind: "XV.MenuItem", name: 'openItem', content: "_open".loc(),
67 {kind: "XV.MenuItem", name: 'newItem', content: "_new".loc(),
71 {name: "completer", kind: "XV.Completer"}
74 {name: "name", classes: "xv-relationwidget-description"},
75 {name: "description", classes: "xv-relationwidget-description xv-relationwidget-secondarydescription"}
78 Add a parameter to the query object on the widget. Parameter conventions should
79 follow those described in the documentation for `XM.Collection`.
81 @seealso XM.Collection
82 @param {Object} Parameter
83 @param {Boolean} Editable - default false. If true user can edit parameter selection in search.
84 @returns {Object} Receiver
86 addParameter: function (param, editable) {
87 var query = this.getQuery() || {},
88 attr = param.attribute;
90 // Update the query with new parameter
91 if (query.parameters) {
92 query.parameters = _.filter(query.parameters, function (p) {
93 return !_.isEqual(p.attribute, attr);
95 } else { query.parameters = []; }
96 query.parameters.push(param);
99 // Keep track of editable parameters
101 this._editableParams = _.without(this._editableParams, attr);
102 } else if (!_.contains(this._editableParams, attr)) {
103 this._editableParams.push(attr);
108 Fill the value with the selected choice.
110 autocomplete: function (callback) {
111 var key = this.getKeyAttribute(),
112 value = this.getValue(),
113 inputValue = this.$.input.getValue(),
116 if (_.isString(key)) {
117 attrs = value ? value.get(key) : "";
118 changed = inputValue !== attrs;
120 // key must be array. Handle it.
121 _.each(key, function (str) {
122 var attr = value ? value.get(str) : "";
125 // if changed or inputValue doesn't match an attr
126 changed = changed || !(_.find(attrs, function (attr) {return inputValue === attr; }));
129 if (inputValue && changed) {
130 this.fetchCollection(inputValue, 1, "_fetchSuccess");
131 } else if (!inputValue) {
138 clear: function (options) {
139 this.setValue(null, options);
141 create: function () {
142 this.inherited(arguments);
143 this._editableParams = [];
146 this.disabledChanged();
149 @todo Document the disabledChanged method.
151 disabledChanged: function () {
152 var disabled = this.getDisabled();
153 this.$.input.setDisabled(disabled);
154 this.$.searchItem.setDisabled(disabled);
155 this.$.newItem.setDisabled(_couldNotCreate.apply(this) || disabled);
156 this.$.name.addRemoveClass("disabled", disabled);
157 this.$.description.addRemoveClass("disabled", disabled);
158 this.$.label.addRemoveClass("disabled", disabled);
162 Used by both autocomplete and keyup.
164 fetchCollection: function (value, rowLimit, callbackName) {
165 var key = this.getKeyAttribute(),
169 operator: "BEGINS_WITH",
171 keySearch: this.keySearch
176 // selectively copy this.getQuery() into a new object. We can't
177 // use a dumb deep copy because the models in there might explode
178 if (this.getQuery()) {
179 if (this.getQuery().orderBy) {
180 query.orderBy = JSON.parse(JSON.stringify(this.getQuery().orderBy));
182 if (this.getQuery().parameters) {
183 query.parameters = [];
184 _.each(this.getQuery().parameters, function (param) {
185 query.parameters.push(param);
190 if (_.isString(key)) {
191 orderBy = [{attribute: key}];
194 _.each(key, function (attr) {
195 orderBy.push({attribute: attr});
199 // Add key search to our query
200 if (query.parameters) {
201 query.parameters.push(param);
203 query.parameters = [param];
206 // Add sort to our query
208 query.orderBy = query.orderby.concat(orderBy);
210 query.orderBy = orderBy;
213 query.rowLimit = rowLimit || 10;
215 this._collection.fetch({
216 success: enyo.bind(this, callbackName),
221 @todo Document the focus method.
224 this.$.input.focus();
226 getFirstKey: function () {
227 var key = this.getKeyAttribute();
228 return _.isString(key) ? key : key[0];
231 @todo Document the getValueToString method.
233 getValueToString: function () {
234 return this.value.getValue(this.getFirstKey());
237 @todo Revisit or remove after ENYO-1104 is resolved.
239 keyDown: function (inSender, inEvent) {
240 this.inherited(arguments);
243 inEvent.activator = this.$.decorator;
244 if (inEvent.keyCode === 9) {
245 this.$.completer.waterfall("onRequestHideMenu", inEvent);
250 We will typically want to query the database upon every keystroke
252 keyUp: function (inSender, inEvent) {
253 var key = this.getFirstKey(),
254 attr = this.getValue() ? this.getValue().get(key) : "",
255 value = this.$.input.getValue(),
256 completer = this.$.completer;
258 inEvent.activator = this.$.decorator;
260 // Look up if value changed
261 if (value && value !== attr && inEvent.keyCode !== 9) {
262 this.fetchCollection(value, 10, "_collectionFetchSuccess");
264 completer.waterfall("onRequestHideMenu", inEvent);
268 @todo Document the itemSelected method.
270 itemSelected: function (inSender, inEvent) {
271 if (inEvent.originator.kind === 'onyx.MenuItem') {
272 this.relationSelected(inSender, inEvent);
274 this.menuItemSelected(inSender, inEvent);
279 @todo Document the labelChanged method.
281 labelChanged: function () {
282 var label = (this.getLabel() || ("_" + this.attr || "").loc());
283 this.$.label.setContent(label + ":");
286 @todo Document the listChanged method.
288 listChanged: function () {
289 var list = this.getList(),
293 delete this._Workspace;
296 if (!list) { return; }
297 this._List = XT.getObjectByName(list);
299 // Get Workspace class
300 workspace = this._List.prototype.getWorkspace();
301 this._Workspace = workspace ? XT.getObjectByName(workspace) : null;
303 // Setup collection instance
304 Collection = this.getCollection() ?
305 XT.getObjectByName(this.getCollection()) : null;
306 if (!Collection) { return; }
307 this._collection = new Collection();
310 @todo Document the menuItemSelected method.
312 menuItemSelected: function (inSender, inEvent) {
314 menuItem = inEvent.originator,
315 list = this.getList(),
316 model = this.getValue(),
317 id = model ? model.id : null,
318 workspace = this._List ? this._List.prototype.getWorkspace() : null,
319 query = this.getQuery() || {},
324 // Convert query to parameter widget friendly
325 _.each(query.parameters, function (param) {
326 // If attribute isn't explicitly set to `editable` then hide any matching control
327 // and pass as fixed condition.
328 if (!_.contains(that._editableParams, param.attribute)) {
329 params.push({name: param.attribute, showing: false});
330 conditions.push(param);
331 // Otherwise user can change value so pass default
333 params.push({name: param.attribute, value: param.value});
337 switch (menuItem.name)
340 callback = function (value) {
341 that.setValue(value);
346 parameterItemValues: params,
347 conditions: conditions,
348 query: this.getQuery()
353 workspace: workspace,
359 // Callback options on commit of the workspace
360 // Find the model with matching id, fetch and set it.
361 callback = function (model) {
362 if (!model) { return; }
363 var Model = that._collection.model,
367 options.success = function () {
368 that.setValue(value);
370 attrs[Model.prototype.idAttribute] = model.id;
371 value = Model.findOrCreate(attrs);
372 value.fetch(options);
375 workspace: workspace,
383 @todo Document the modelChanged method.
385 modelChanged: function (inSender, inEvent) {
388 workspace = List.prototype.getWorkspace(),
389 Workspace = workspace ? XT.getObjectByName(workspace) : null,
391 // If the model that changed was related to and exists on this widget
392 // refresh the local model.
393 if (!List || !Workspace || !this.value) {
396 if (Workspace.prototype.model === inEvent.model &&
397 this.value.id === inEvent.id) {
398 options.success = function () {
399 that.setValue(this.value);
401 this.value.fetch(options);
405 When we get a placeholder we want to actually display it.
407 placeholderChanged: function () {
408 var placeholder = this.getPlaceholder();
409 this.$.input.setPlaceholder(placeholder);
412 @todo Document the receiveBlur method.
414 receiveBlur: function (inSender, inEvent) {
416 this._hasFocus = false;
419 @todo Document the receiveFocus method.
421 receiveFocus: function (inSender, inEvent) {
422 this._hasFocus = true;
423 this._relationSelected = false;
426 @todo Document the relationSelected method.
428 relationSelected: function (inSender, inEvent) {
429 this._relationSelected = true;
430 inEvent.activator = this.$.decorator;
431 this.setValue(inEvent.originator.model);
432 this.$.completer.waterfall("onRequestHideMenu", inEvent);
436 Removes a query parameter by attribute name from the widget's query object.
438 @param {String} Attribute
439 @returns {Object} Receiver
441 removeParameter: function (attr) {
442 var query = this.getQuery();
443 if (query && query.parameters) {
444 query.parameters = _.filter(query.parameters, function (param) {
445 return param.attribute !== attr;
448 this.setQuery(query);
449 this._editableParams = _.without(this._editableParams, attr);
454 Programatically sets the value of this widget. If an id is set then the
455 correct model will be fetched and this function will be called again
456 recursively with the model.
458 *In case of "order" models, each order's workspace is different depending on the order type.
459 This causes the Workspace var to be null so get the value from the getWorkspace
460 defined on the relation.js sub-kind.
462 @param {XM.Model|Number|String} Value; can be a model or the id of a model.
463 @param {Object} options
465 setValue: function (value, options) {
466 options = options || {};
468 newId = value ? value.id : null,
469 oldId = this.value ? this.value.id : null,
470 key = this.getFirstKey(),
471 name = this.getNameAttribute(),
472 descrip = this.getDescripAttribute(),
473 additional = this.getAdditionalAttribute(),
474 inEvent = { value: value, originator: this },
478 additionalValue = "",
479 Workspace = this._Workspace || (newId ? this._List.prototype.getWorkspace(value) : null),
480 Model = this._collection.model,
483 listName = this.getList(),
488 setPrivileges = function () {
489 if (value && newId) {
490 if (value.couldRead) {
491 that.$.openItem.setDisabled(!value.couldRead());
493 that.$.openItem.setDisabled(!value.getClass().canRead());
499 List = XT.getObjectByName(listName);
500 CollectionName = List.prototype.collection;
501 Collection = XT.getObjectByName(CollectionName);
502 ListModel = Collection.prototype.model;
505 // Make sure we get are setting the right kind of object here.
506 // If we're being sent a number or a string, or something that's not a model
507 // then we assume that's the key, we fetch the model, and call this function
508 // again and give it the model for real.
509 // Of course, if the value is falsy, we don't want to fetch a model,
510 // we just want to continue (which will empty out the relation widget)
511 if (value && (_.isNumber(value) || _.isString(value) || !(value instanceof Model))) {
512 if (this.value === value || oldId === value) { return; }
514 // If we got here, but it is not the list's model type, we can't work with this
515 if (!ListModel || (value instanceof XM.Model && !(value instanceof ListModel))) {
516 throw "The type of model passed is incompatible with this widget";
519 id = _.isObject(value) ? value.id : value;
521 newValue = new Model();
524 success: function () {
525 that.setValue(newValue);
528 XT.log("Error setting relational widget value");
531 newValue.fetch(options);
536 if (value && value.getValue) {
537 keyValue = value.getValue(key) || "";
538 nameValue = value.getValue(name) || "";
539 descripValue = value.getValue(descrip) || "";
540 additionalValue = value.getValue(additional) || "";
542 this.$.input.setValue(keyValue);
543 this.$.name.setShowing(nameValue);
544 this.$.name.setContent(nameValue);
545 this.$.description.setShowing(descripValue);
546 this.$.description.setContent(descripValue);
547 //this.$.additionalInfo.setShowing(additionalValue);
548 //this.$.additionalInfo.setContent(additionalValue);
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;