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 // TODO: refactor this out into a separate array that is added on create
70 {name: 'descriptionContainer', classes: 'xv-invisible-wrapper', components: [
71 {controlClasses: 'enyo-inline', components: [
72 {name: "name", classes: "xv-description"},
73 {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();
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.$.label.addRemoveClass("disabled", disabled);
158 _.each(this.$.descriptionContainer.$, function (component) {
159 component.addRemoveClass("disabled", disabled);
164 Used by both autocomplete and keyup.
166 fetchCollection: function (value, rowLimit, callbackName) {
167 var key = this.getKeyAttribute(),
171 operator: "BEGINS_WITH",
173 keySearch: this.keySearch
178 // selectively copy this.getQuery() into a new object. We can't
179 // use a dumb deep copy because the models in there might explode
180 if (this.getQuery()) {
181 if (this.getQuery().orderBy) {
182 query.orderBy = JSON.parse(JSON.stringify(this.getQuery().orderBy));
184 if (this.getQuery().parameters) {
185 query.parameters = [];
186 _.each(this.getQuery().parameters, function (param) {
187 query.parameters.push(param);
192 if (_.isString(key)) {
193 orderBy = [{attribute: key}];
196 _.each(key, function (attr) {
197 orderBy.push({attribute: attr});
201 // Add key search to our query
202 if (query.parameters) {
203 query.parameters.push(param);
205 query.parameters = [param];
208 // Add sort to our query
210 query.orderBy = query.orderby.concat(orderBy);
212 query.orderBy = orderBy;
215 query.rowLimit = rowLimit || 10;
217 this._collection.fetch({
218 success: enyo.bind(this, callbackName),
223 @todo Document the focus method.
226 this.$.input.focus();
228 getFirstKey: function () {
229 var key = this.getKeyAttribute();
230 return _.isString(key) ? key : key[0];
233 @todo Document the getValueToString method.
235 getValueToString: function () {
236 return this.value.getValue(this.getFirstKey());
239 @todo Revisit or remove after ENYO-1104 is resolved.
241 keyDown: function (inSender, inEvent) {
242 this.inherited(arguments);
245 inEvent.activator = this.$.decorator;
246 if (inEvent.keyCode === 9) {
247 this.$.completer.waterfall("onRequestHideMenu", inEvent);
252 We will typically want to query the database upon every keystroke
254 keyUp: function (inSender, inEvent) {
255 var key = this.getFirstKey(),
256 attr = this.getValue() ? this.getValue().get(key) : "",
257 value = this.$.input.getValue(),
258 completer = this.$.completer;
260 inEvent.activator = this.$.decorator;
262 // Look up if value changed
263 if (value && value !== attr && inEvent.keyCode !== 9) {
264 this.fetchCollection(value, 10, "_collectionFetchSuccess");
266 completer.waterfall("onRequestHideMenu", inEvent);
270 @todo Document the itemSelected method.
272 itemSelected: function (inSender, inEvent) {
273 if (inEvent.originator.kind === 'onyx.MenuItem') {
274 this.relationSelected(inSender, inEvent);
276 this.menuItemSelected(inSender, inEvent);
281 @todo Document the labelChanged method.
283 labelChanged: function () {
284 var label = (this.getLabel() || ("_" + this.attr || "").loc());
285 this.$.label.setContent(label + ":");
288 @todo Document the listChanged method.
290 listChanged: function () {
291 var list = this.getList(),
295 delete this._Workspace;
298 if (!list) { return; }
299 this._List = XT.getObjectByName(list);
301 // Get Workspace class
302 workspace = this._List.prototype.getWorkspace();
303 this._Workspace = workspace ? XT.getObjectByName(workspace) : null;
305 // Setup collection instance
306 Collection = this.getCollection() ?
307 XT.getObjectByName(this.getCollection()) : null;
308 if (!Collection) { return; }
309 this._collection = new Collection();
312 @todo Document the menuItemSelected method.
314 menuItemSelected: function (inSender, inEvent) {
316 menuItem = inEvent.originator,
317 list = this.getList(),
318 model = this.getValue(),
319 id = model ? model.id : null,
320 workspace = this._List ? this._List.prototype.getWorkspace() : null,
321 query = this.getQuery() || {},
326 // Convert query to parameter widget friendly
327 _.each(query.parameters, function (param) {
328 // If attribute isn't explicitly set to `editable` then hide any matching control
329 // and pass as fixed condition.
330 if (!_.contains(that._editableParams, param.attribute)) {
331 params.push({name: param.attribute, showing: false});
332 conditions.push(param);
333 // Otherwise user can change value so pass default
335 params.push({name: param.attribute, value: param.value});
339 switch (menuItem.name)
342 callback = function (value) {
343 that.setValue(value);
348 parameterItemValues: params,
349 conditions: conditions,
350 query: this.getQuery()
355 workspace: workspace,
361 // Callback options on commit of the workspace
362 // Find the model with matching id, fetch and set it.
363 callback = function (model) {
364 if (!model) { return; }
365 var Model = that._collection.model,
369 options.success = function () {
370 that.setValue(value);
372 attrs[Model.prototype.idAttribute] = model.id;
373 value = Model.findOrCreate(attrs);
374 value.fetch(options);
377 workspace: workspace,
385 @todo Document the modelChanged method.
387 modelChanged: function (inSender, inEvent) {
390 workspace = List.prototype.getWorkspace(),
391 Workspace = workspace ? XT.getObjectByName(workspace) : null,
393 // If the model that changed was related to and exists on this widget
394 // refresh the local model.
395 if (!List || !Workspace || !this.value) {
398 if (Workspace.prototype.model === inEvent.model &&
399 this.value.id === inEvent.id) {
400 options.success = function () {
401 that.setValue(this.value);
403 this.value.fetch(options);
407 When we get a placeholder we want to actually display it.
409 placeholderChanged: function () {
410 var placeholder = this.getPlaceholder();
411 this.$.input.setPlaceholder(placeholder);
414 @todo Document the receiveBlur method.
416 receiveBlur: function (inSender, inEvent) {
418 this._hasFocus = false;
421 @todo Document the receiveFocus method.
423 receiveFocus: function (inSender, inEvent) {
424 this._hasFocus = true;
425 this._relationSelected = false;
428 @todo Document the relationSelected method.
430 relationSelected: function (inSender, inEvent) {
431 this._relationSelected = true;
432 inEvent.activator = this.$.decorator;
433 this.setValue(inEvent.originator.model);
434 this.$.completer.waterfall("onRequestHideMenu", inEvent);
438 Removes a query parameter by attribute name from the widget's query object.
440 @param {String} Attribute
441 @returns {Object} Receiver
443 removeParameter: function (attr) {
444 var query = this.getQuery();
445 if (query && query.parameters) {
446 query.parameters = _.filter(query.parameters, function (param) {
447 return param.attribute !== attr;
450 this.setQuery(query);
451 this._editableParams = _.without(this._editableParams, attr);
456 Programatically sets the value of this widget. If an id is set then the
457 correct model will be fetched and this function will be called again
458 recursively with the model.
460 *In case of "order" models, each order's workspace is different depending on the order type.
461 This causes the Workspace var to be null so get the value from the getWorkspace
462 defined on the relation.js sub-kind.
464 @param {XM.Model|Number|String} Value; can be a model or the id of a model.
465 @param {Object} options
467 setValue: function (value, options) {
468 options = options || {};
470 newId = value ? value.id : null,
471 oldId = this.value ? this.value.id : null,
472 key = this.getFirstKey(),
473 name = this.getNameAttribute(),
474 descrip = this.getDescripAttribute(),
475 additional = this.getAdditionalAttribute(),
476 inEvent = { value: value, originator: this },
480 additionalValue = "",
481 Workspace = this._Workspace || (newId ? this._List.prototype.getWorkspace(value) : null),
482 Model = this._collection.model,
485 listName = this.getList(),
490 setPrivileges = function () {
491 if (value && newId) {
492 if (value.couldRead) {
493 that.$.openItem.setDisabled(!value.couldRead());
495 that.$.openItem.setDisabled(!value.getClass().canRead());
501 List = XT.getObjectByName(listName);
502 CollectionName = List.prototype.collection;
503 Collection = XT.getObjectByName(CollectionName);
504 ListModel = Collection.prototype.model;
507 // Make sure we get are setting the right kind of object here.
508 // If we're being sent a number or a string, or something that's not a model
509 // then we assume that's the key, we fetch the model, and call this function
510 // again and give it the model for real.
511 // Of course, if the value is falsy, we don't want to fetch a model,
512 // we just want to continue (which will empty out the relation widget)
513 if (value && (_.isNumber(value) || _.isString(value) || !(value instanceof Model))) {
514 if (this.value === value || oldId === value) { return; }
516 // If we got here, but it is not the list's model type, we can't work with this
517 if (!ListModel || (value instanceof XM.Model && !(value instanceof ListModel))) {
518 throw "The type of model passed is incompatible with this widget";
521 id = _.isObject(value) ? value.id : value;
523 newValue = new Model();
526 success: function () {
527 that.setValue(newValue);
530 XT.log("Error setting relational widget value");
533 newValue.fetch(options);
538 if (value && value.getValue) {
539 keyValue = value.getValue(key) || "";
540 nameValue = value.getValue(name) || "";
541 descripValue = value.getValue(descrip) || "";
542 additionalValue = value.getValue(additional) || "";
544 this.$.input.setValue(keyValue);
546 this.$.name.setShowing(nameValue);
547 this.$.name.setContent(nameValue);
548 this.$.description.setShowing(descripValue);
549 this.$.description.setContent(descripValue);
551 // Only notify if selection actually changed
552 if (newId !== oldId && !options.silent) { this.doValueChange(inEvent); }
554 // Handle menu actions
555 that.$.openItem.setShowing(Workspace);
556 that.$.newItem.setShowing(Workspace);
557 that.$.openItem.setDisabled(true);
558 that.$.newItem.setDisabled(_couldNotCreate.apply(this) || this.disabled);
559 if (Model && Workspace) {
563 XT.getStartupManager().registerCallback(setPrivileges);
569 _collectionFetchSuccess: function () {
570 if (!this._hasFocus) { return; }
571 var key = this.getKeyAttribute(),
572 sidecarKey = this.getSidecarAttribute(),
573 value = this.$.input.getValue(),
574 models = this._collection.models,
575 inEvent = { activator: this.$.decorator };
577 this.$.completer.buildList(key, value, models, sidecarKey, this.skipCompleterFilter);
578 if (!this.$.completer.showing) {
579 this.$.completer.waterfall("onRequestShowMenu", inEvent);
581 this.$.completer.adjustPosition();
583 this.$.completer.waterfall("onRequestHideMenu", inEvent);
588 _fetchSuccess: function () {
589 if (this._relationSelected) { return; }
590 var value = this._collection.length ? this._collection.models[0] : null,
591 target = enyo.dispatcher.captureTarget;
592 this.setValue(value);
593 enyo.dispatcher.captureTarget = target;
599 var _couldNotCreate = function () {
600 var Workspace = this._Workspace,
601 Model = this._collection.model,
602 couldNotCreate = true;
604 if (Model && Model.couldCreate) {
605 // model is a list item or relation
606 couldNotCreate = !Model.couldCreate();
608 // model is a first-class model
609 couldNotCreate = !Model.canCreate();
611 return couldNotCreate;