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'}
71 descriptionComponents: [
72 {controlClasses: 'enyo-inline', components: [
73 {name: "name", classes: "xv-description"},
74 {name: "description", classes: "xv-description"}
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();
148 // create description components
149 _.each(this.descriptionComponents, function (component) {
150 this.$.descriptionContainer.createComponent(component, {owner: this});
154 @todo Document the disabledChanged method.
156 disabledChanged: function () {
157 var disabled = this.getDisabled();
158 this.$.input.setDisabled(disabled);
159 this.$.searchItem.setDisabled(disabled);
160 this.$.newItem.setDisabled(_couldNotCreate.apply(this) || disabled);
161 this.$.label.addRemoveClass("disabled", disabled);
163 _.each(this.$.descriptionContainer.$, function (component) {
164 component.addRemoveClass("disabled", disabled);
169 Used by both autocomplete and keyup.
171 fetchCollection: function (value, rowLimit, callbackName) {
172 var key = this.getKeyAttribute(),
176 operator: "BEGINS_WITH",
178 keySearch: this.keySearch
183 // selectively copy this.getQuery() into a new object. We can't
184 // use a dumb deep copy because the models in there might explode
185 if (this.getQuery()) {
186 if (this.getQuery().orderBy) {
187 query.orderBy = JSON.parse(JSON.stringify(this.getQuery().orderBy));
189 if (this.getQuery().parameters) {
190 query.parameters = [];
191 _.each(this.getQuery().parameters, function (param) {
192 query.parameters.push(param);
197 if (_.isString(key)) {
198 orderBy = [{attribute: key}];
201 _.each(key, function (attr) {
202 orderBy.push({attribute: attr});
206 // Add key search to our query
207 if (query.parameters) {
208 query.parameters.push(param);
210 query.parameters = [param];
213 // Add sort to our query
215 query.orderBy = query.orderby.concat(orderBy);
217 query.orderBy = orderBy;
220 query.rowLimit = rowLimit || 10;
222 this._collection.fetch({
223 success: enyo.bind(this, callbackName),
228 @todo Document the focus method.
231 this.$.input.focus();
233 getFirstKey: function () {
234 var key = this.getKeyAttribute();
235 return _.isString(key) ? key : key[0];
238 @todo Document the getValueToString method.
240 getValueToString: function () {
241 return this.value.getValue(this.getFirstKey());
244 @todo Revisit or remove after ENYO-1104 is resolved.
246 keyDown: function (inSender, inEvent) {
247 this.inherited(arguments);
250 inEvent.activator = this.$.decorator;
251 if (inEvent.keyCode === 9) {
252 this.$.completer.waterfall("onRequestHideMenu", inEvent);
257 We will typically want to query the database upon every keystroke
259 keyUp: function (inSender, inEvent) {
260 var key = this.getFirstKey(),
261 attr = this.getValue() ? this.getValue().get(key) : "",
262 value = this.$.input.getValue(),
263 completer = this.$.completer;
265 inEvent.activator = this.$.decorator;
267 // Look up if value changed
268 if (value && value !== attr && inEvent.keyCode !== 9) {
269 this.fetchCollection(value, 10, "_collectionFetchSuccess");
271 completer.waterfall("onRequestHideMenu", inEvent);
275 @todo Document the itemSelected method.
277 itemSelected: function (inSender, inEvent) {
278 if (inEvent.originator.kind === 'onyx.MenuItem') {
279 this.relationSelected(inSender, inEvent);
281 this.menuItemSelected(inSender, inEvent);
286 @todo Document the labelChanged method.
288 labelChanged: function () {
289 var label = (this.getLabel() || ("_" + this.attr || "").loc());
290 this.$.label.setContent(label + ":");
293 @todo Document the listChanged method.
295 listChanged: function () {
296 var list = this.getList(),
300 delete this._Workspace;
303 if (!list) { return; }
304 this._List = XT.getObjectByName(list);
306 // Get Workspace class
307 workspace = this._List.prototype.getWorkspace();
308 this._Workspace = workspace ? XT.getObjectByName(workspace) : null;
310 // Setup collection instance
311 Collection = this.getCollection() ?
312 XT.getObjectByName(this.getCollection()) : null;
313 if (!Collection) { return; }
314 this._collection = new Collection();
317 @todo Document the menuItemSelected method.
319 menuItemSelected: function (inSender, inEvent) {
321 menuItem = inEvent.originator,
322 list = this.getList(),
323 model = this.getValue(),
324 id = model ? model.id : null,
325 workspace = this._List ? this._List.prototype.getWorkspace() : null,
326 query = this.getQuery() || {},
331 // Convert query to parameter widget friendly
332 _.each(query.parameters, function (param) {
333 // If attribute isn't explicitly set to `editable` then hide any matching control
334 // and pass as fixed condition.
335 if (!_.contains(that._editableParams, param.attribute)) {
336 params.push({name: param.attribute, showing: false});
337 conditions.push(param);
338 // Otherwise user can change value so pass default
340 params.push({name: param.attribute, value: param.value});
344 switch (menuItem.name)
347 callback = function (value) {
348 that.setValue(value);
353 parameterItemValues: params,
354 conditions: conditions,
355 query: this.getQuery()
360 workspace: workspace,
366 // Callback options on commit of the workspace
367 // Find the model with matching id, fetch and set it.
368 callback = function (model) {
369 if (!model) { return; }
370 var Model = that._collection.model,
374 options.success = function () {
375 that.setValue(value);
377 attrs[Model.prototype.idAttribute] = model.id;
378 value = Model.findOrCreate(attrs);
379 value.fetch(options);
382 workspace: workspace,
390 @todo Document the modelChanged method.
392 modelChanged: function (inSender, inEvent) {
395 workspace = List.prototype.getWorkspace(),
396 Workspace = workspace ? XT.getObjectByName(workspace) : null,
398 // If the model that changed was related to and exists on this widget
399 // refresh the local model.
400 if (!List || !Workspace || !this.value) {
403 if (Workspace.prototype.model === inEvent.model &&
404 this.value.id === inEvent.id) {
405 options.success = function () {
406 that.setValue(this.value);
408 this.value.fetch(options);
412 When we get a placeholder we want to actually display it.
414 placeholderChanged: function () {
415 var placeholder = this.getPlaceholder();
416 this.$.input.setPlaceholder(placeholder);
419 @todo Document the receiveBlur method.
421 receiveBlur: function (inSender, inEvent) {
423 this._hasFocus = false;
426 @todo Document the receiveFocus method.
428 receiveFocus: function (inSender, inEvent) {
429 this._hasFocus = true;
430 this._relationSelected = false;
433 @todo Document the relationSelected method.
435 relationSelected: function (inSender, inEvent) {
436 this._relationSelected = true;
437 inEvent.activator = this.$.decorator;
438 this.setValue(inEvent.originator.model);
439 this.$.completer.waterfall("onRequestHideMenu", inEvent);
443 Removes a query parameter by attribute name from the widget's query object.
445 @param {String} Attribute
446 @returns {Object} Receiver
448 removeParameter: function (attr) {
449 var query = this.getQuery();
450 if (query && query.parameters) {
451 query.parameters = _.filter(query.parameters, function (param) {
452 return param.attribute !== attr;
455 this.setQuery(query);
456 this._editableParams = _.without(this._editableParams, attr);
461 Programatically sets the value of this widget. If an id is set then the
462 correct model will be fetched and this function will be called again
463 recursively with the model.
465 *In case of "order" models, each order's workspace is different depending on the order type.
466 This causes the Workspace var to be null so get the value from the getWorkspace
467 defined on the relation.js sub-kind.
469 @param {XM.Model|Number|String} Value; can be a model or the id of a model.
470 @param {Object} options
472 setValue: function (value, options) {
473 options = options || {};
475 newId = value ? value.id : null,
476 oldId = this.value ? this.value.id : null,
477 key = this.getFirstKey(),
478 name = this.getNameAttribute(),
479 descrip = this.getDescripAttribute(),
480 additional = this.getAdditionalAttribute(),
481 inEvent = { value: value, originator: this },
485 additionalValue = "",
486 Workspace = this._Workspace || (newId ? this._List.prototype.getWorkspace(value) : null),
487 Model = this._collection.model,
490 listName = this.getList(),
495 setPrivileges = function () {
496 if (value && newId) {
497 if (value.couldRead) {
498 that.$.openItem.setDisabled(!value.couldRead());
500 that.$.openItem.setDisabled(!value.getClass().canRead());
506 List = XT.getObjectByName(listName);
507 CollectionName = List.prototype.collection;
508 Collection = XT.getObjectByName(CollectionName);
509 ListModel = Collection.prototype.model;
512 // Make sure we get are setting the right kind of object here.
513 // If we're being sent a number or a string, or something that's not a model
514 // then we assume that's the key, we fetch the model, and call this function
515 // again and give it the model for real.
516 // Of course, if the value is falsy, we don't want to fetch a model,
517 // we just want to continue (which will empty out the relation widget)
518 if (value && (_.isNumber(value) || _.isString(value) || !(value instanceof Model))) {
519 if (this.value === value || oldId === value) { return; }
521 // If we got here, but it is not the list's model type, we can't work with this
522 if (!ListModel || (value instanceof XM.Model && !(value instanceof ListModel))) {
523 throw "The type of model passed is incompatible with this widget";
526 id = _.isObject(value) ? value.id : value;
528 newValue = new Model();
531 success: function () {
532 that.setValue(newValue);
535 XT.log("Error setting relational widget value");
538 newValue.fetch(options);
543 if (value && value.getValue) {
544 keyValue = value.getValue(key) || "";
545 nameValue = value.getValue(name) || "";
546 descripValue = value.getValue(descrip) || "";
547 additionalValue = value.getValue(additional) || "";
549 this.$.input.setValue(keyValue);
551 this.$.name.setShowing(nameValue);
552 this.$.name.setContent(nameValue);
553 this.$.description.setShowing(descripValue);
554 this.$.description.setContent(descripValue);
556 // Only notify if selection actually changed
557 if (newId !== oldId && !options.silent) { this.doValueChange(inEvent); }
559 // Handle menu actions
560 that.$.openItem.setShowing(Workspace);
561 that.$.newItem.setShowing(Workspace);
562 that.$.openItem.setDisabled(true);
563 that.$.newItem.setDisabled(_couldNotCreate.apply(this) || this.disabled);
564 if (Model && Workspace) {
568 XT.getStartupManager().registerCallback(setPrivileges);
574 _collectionFetchSuccess: function () {
575 if (!this._hasFocus) { return; }
576 var key = this.getKeyAttribute(),
577 sidecarKey = this.getSidecarAttribute(),
578 value = this.$.input.getValue(),
579 models = this._collection.models,
580 inEvent = { activator: this.$.decorator };
582 this.$.completer.buildList(key, value, models, sidecarKey, this.skipCompleterFilter);
583 if (!this.$.completer.showing) {
584 this.$.completer.waterfall("onRequestShowMenu", inEvent);
586 this.$.completer.adjustPosition();
588 this.$.completer.waterfall("onRequestHideMenu", inEvent);
593 _fetchSuccess: function () {
594 if (this._relationSelected) { return; }
595 var value = this._collection.length ? this._collection.models[0] : null,
596 target = enyo.dispatcher.captureTarget;
597 this.setValue(value);
598 enyo.dispatcher.captureTarget = target;
604 var _couldNotCreate = function () {
605 var Workspace = this._Workspace,
606 Model = this._collection.model,
607 couldNotCreate = true;
609 if (Model && Model.couldCreate) {
610 // model is a list item or relation
611 couldNotCreate = !Model.couldCreate();
613 // model is a first-class model
614 couldNotCreate = !Model.canCreate();
616 return couldNotCreate;