MultiSelect.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. /**
  2. * @class Ext.ux.form.MultiSelect
  3. * @extends Ext.form.field.Base
  4. * A control that allows selection and form submission of multiple list items.
  5. *
  6. * @history
  7. * 2008-06-19 bpm Original code contributed by Toby Stuart (with contributions from Robert Williams)
  8. * 2008-06-19 bpm Docs and demo code clean up
  9. *
  10. * @constructor
  11. * Create a new MultiSelect
  12. * @param {Object} config Configuration options
  13. * @xtype multiselect
  14. */
  15. Ext.define('Ext.ux.form.MultiSelect', {
  16. extend: 'Ext.form.field.Base',
  17. alternateClassName: 'Ext.ux.Multiselect',
  18. alias: ['widget.multiselect', 'widget.multiselectfield'],
  19. uses: [
  20. 'Ext.view.BoundList',
  21. 'Ext.form.FieldSet',
  22. 'Ext.ux.layout.component.form.MultiSelect',
  23. 'Ext.view.DragZone',
  24. 'Ext.view.DropZone'
  25. ],
  26. /**
  27. * @cfg {String} listTitle An optional title to be displayed at the top of the selection list.
  28. */
  29. /**
  30. * @cfg {String/Array} dragGroup The ddgroup name(s) for the MultiSelect DragZone (defaults to undefined).
  31. */
  32. /**
  33. * @cfg {String/Array} dropGroup The ddgroup name(s) for the MultiSelect DropZone (defaults to undefined).
  34. */
  35. /**
  36. * @cfg {Boolean} ddReorder Whether the items in the MultiSelect list are drag/drop reorderable (defaults to false).
  37. */
  38. ddReorder: false,
  39. /**
  40. * @cfg {Object/Array} tbar An optional toolbar to be inserted at the top of the control's selection list.
  41. * This can be a {@link Ext.toolbar.Toolbar} object, a toolbar config, or an array of buttons/button configs
  42. * to be added to the toolbar. See {@link Ext.panel.Panel#tbar}.
  43. */
  44. /**
  45. * @cfg {String} appendOnly True if the list should only allow append drops when drag/drop is enabled
  46. * (use for lists which are sorted, defaults to false).
  47. */
  48. appendOnly: false,
  49. /**
  50. * @cfg {String} displayField Name of the desired display field in the dataset (defaults to 'text').
  51. */
  52. displayField: 'text',
  53. /**
  54. * @cfg {String} valueField Name of the desired value field in the dataset (defaults to the
  55. * value of {@link #displayField}).
  56. */
  57. /**
  58. * @cfg {Boolean} allowBlank False to require at least one item in the list to be selected, true to allow no
  59. * selection (defaults to true).
  60. */
  61. allowBlank: true,
  62. /**
  63. * @cfg {Number} minSelections Minimum number of selections allowed (defaults to 0).
  64. */
  65. minSelections: 0,
  66. /**
  67. * @cfg {Number} maxSelections Maximum number of selections allowed (defaults to Number.MAX_VALUE).
  68. */
  69. maxSelections: Number.MAX_VALUE,
  70. /**
  71. * @cfg {String} blankText Default text displayed when the control contains no items (defaults to 'This field is required')
  72. */
  73. blankText: 'This field is required',
  74. /**
  75. * @cfg {String} minSelectionsText Validation message displayed when {@link #minSelections} is not met (defaults to 'Minimum {0}
  76. * item(s) required'). The {0} token will be replaced by the value of {@link #minSelections}.
  77. */
  78. minSelectionsText: 'Minimum {0} item(s) required',
  79. /**
  80. * @cfg {String} maxSelectionsText Validation message displayed when {@link #maxSelections} is not met (defaults to 'Maximum {0}
  81. * item(s) allowed'). The {0} token will be replaced by the value of {@link #maxSelections}.
  82. */
  83. maxSelectionsText: 'Maximum {0} item(s) allowed',
  84. /**
  85. * @cfg {String} delimiter The string used to delimit the selected values when {@link #getSubmitValue submitting}
  86. * the field as part of a form. Defaults to ','. If you wish to have the selected values submitted as separate
  87. * parameters rather than a single delimited parameter, set this to <tt>null</tt>.
  88. */
  89. delimiter: ',',
  90. /**
  91. * @cfg {Ext.data.Store/Array} store The data source to which this MultiSelect is bound (defaults to <tt>undefined</tt>).
  92. * Acceptable values for this property are:
  93. * <div class="mdetail-params"><ul>
  94. * <li><b>any {@link Ext.data.Store Store} subclass</b></li>
  95. * <li><b>an Array</b> : Arrays will be converted to a {@link Ext.data.ArrayStore} internally.
  96. * <div class="mdetail-params"><ul>
  97. * <li><b>1-dimensional array</b> : (e.g., <tt>['Foo','Bar']</tt>)<div class="sub-desc">
  98. * A 1-dimensional array will automatically be expanded (each array item will be the combo
  99. * {@link #valueField value} and {@link #displayField text})</div></li>
  100. * <li><b>2-dimensional array</b> : (e.g., <tt>[['f','Foo'],['b','Bar']]</tt>)<div class="sub-desc">
  101. * For a multi-dimensional array, the value in index 0 of each item will be assumed to be the combo
  102. * {@link #valueField value}, while the value at index 1 is assumed to be the combo {@link #displayField text}.
  103. * </div></li></ul></div></li></ul></div>
  104. */
  105. componentLayout: 'multiselectfield',
  106. fieldBodyCls: Ext.baseCSSPrefix + 'form-multiselect-body',
  107. // private
  108. initComponent: function(){
  109. var me = this;
  110. me.bindStore(me.store, true);
  111. if (me.store.autoCreated) {
  112. me.valueField = me.displayField = 'field1';
  113. if (!me.store.expanded) {
  114. me.displayField = 'field2';
  115. }
  116. }
  117. if (!Ext.isDefined(me.valueField)) {
  118. me.valueField = me.displayField;
  119. }
  120. me.callParent();
  121. },
  122. bindStore: function(store, initial) {
  123. var me = this,
  124. oldStore = me.store,
  125. boundList = me.boundList;
  126. if (oldStore && !initial && oldStore !== store && oldStore.autoDestroy) {
  127. oldStore.destroy();
  128. }
  129. me.store = store ? Ext.data.StoreManager.lookup(store) : null;
  130. if (boundList) {
  131. boundList.bindStore(store || null);
  132. }
  133. },
  134. // private
  135. onRender: function(ct, position) {
  136. var me = this,
  137. panel, boundList, selModel;
  138. me.callParent(arguments);
  139. boundList = me.boundList = Ext.create('Ext.view.BoundList', {
  140. multiSelect: true,
  141. store: me.store,
  142. displayField: me.displayField,
  143. border: false
  144. });
  145. selModel = boundList.getSelectionModel();
  146. me.mon(selModel, {
  147. selectionChange: me.onSelectionChange,
  148. scope: me
  149. });
  150. panel = me.panel = Ext.create('Ext.panel.Panel', {
  151. title: me.listTitle,
  152. tbar: me.tbar,
  153. items: [boundList],
  154. renderTo: me.bodyEl,
  155. layout: 'fit'
  156. });
  157. // Must set upward link after first render
  158. panel.ownerCt = me;
  159. // Set selection to current value
  160. me.setRawValue(me.rawValue);
  161. },
  162. // No content generated via template, it's all added components
  163. getSubTplMarkup: function() {
  164. return '';
  165. },
  166. // private
  167. afterRender: function() {
  168. var me = this;
  169. me.callParent();
  170. if (me.ddReorder && !me.dragGroup && !me.dropGroup){
  171. me.dragGroup = me.dropGroup = 'MultiselectDD-' + Ext.id();
  172. }
  173. if (me.draggable || me.dragGroup){
  174. me.dragZone = Ext.create('Ext.view.DragZone', {
  175. view: me.boundList,
  176. ddGroup: me.dragGroup,
  177. dragText: '{0} Item{1}'
  178. });
  179. }
  180. if (me.droppable || me.dropGroup){
  181. me.dropZone = Ext.create('Ext.view.DropZone', {
  182. view: me.boundList,
  183. ddGroup: me.dropGroup,
  184. handleNodeDrop: function(data, dropRecord, position) {
  185. var view = this.view,
  186. store = view.getStore(),
  187. records = data.records,
  188. index;
  189. // remove the Models from the source Store
  190. data.view.store.remove(records);
  191. index = store.indexOf(dropRecord);
  192. if (position === 'after') {
  193. index++;
  194. }
  195. store.insert(index, records);
  196. view.getSelectionModel().select(records);
  197. }
  198. });
  199. }
  200. },
  201. onSelectionChange: function() {
  202. this.checkChange();
  203. },
  204. /**
  205. * Clears any values currently selected.
  206. */
  207. clearValue: function() {
  208. this.setValue([]);
  209. },
  210. /**
  211. * Return the value(s) to be submitted for this field. The returned value depends on the {@link #delimiter}
  212. * config: If it is set to a String value (like the default ',') then this will return the selected values
  213. * joined by the delimiter. If it is set to <tt>null</tt> then the values will be returned as an Array.
  214. */
  215. getSubmitValue: function() {
  216. var me = this,
  217. delimiter = me.delimiter,
  218. val = me.getValue();
  219. return Ext.isString(delimiter) ? val.join(delimiter) : val;
  220. },
  221. // inherit docs
  222. getRawValue: function() {
  223. var me = this,
  224. boundList = me.boundList;
  225. if (boundList) {
  226. me.rawValue = Ext.Array.map(boundList.getSelectionModel().getSelection(), function(model) {
  227. return model.get(me.valueField);
  228. });
  229. }
  230. return me.rawValue;
  231. },
  232. // inherit docs
  233. setRawValue: function(value) {
  234. var me = this,
  235. boundList = me.boundList,
  236. models;
  237. value = Ext.Array.from(value);
  238. me.rawValue = value;
  239. if (boundList) {
  240. models = [];
  241. Ext.Array.forEach(value, function(val) {
  242. var undef,
  243. model = me.store.findRecord(me.valueField, val, undef, undef, true, true);
  244. if (model) {
  245. models.push(model);
  246. }
  247. });
  248. boundList.getSelectionModel().select(models, false, true);
  249. }
  250. return value;
  251. },
  252. // no conversion
  253. valueToRaw: function(value) {
  254. return value;
  255. },
  256. // compare array values
  257. isEqual: function(v1, v2) {
  258. var fromArray = Ext.Array.from,
  259. i, len;
  260. v1 = fromArray(v1);
  261. v2 = fromArray(v2);
  262. len = v1.length;
  263. if (len !== v2.length) {
  264. return false;
  265. }
  266. for(i = 0; i < len; i++) {
  267. if (v2[i] !== v1[i]) {
  268. return false;
  269. }
  270. }
  271. return true;
  272. },
  273. getErrors : function(value) {
  274. var me = this,
  275. format = Ext.String.format,
  276. errors = me.callParent(arguments),
  277. numSelected;
  278. value = Ext.Array.from(value || me.getValue());
  279. numSelected = value.length;
  280. if (!me.allowBlank && numSelected < 1) {
  281. errors.push(me.blankText);
  282. }
  283. if (numSelected < this.minSelections) {
  284. errors.push(format(me.minSelectionsText, me.minSelections));
  285. }
  286. if (numSelected > this.maxSelections) {
  287. errors.push(format(me.maxSelectionsText, me.maxSelections));
  288. }
  289. return errors;
  290. },
  291. onDisable: function() {
  292. this.callParent();
  293. this.disabled = true;
  294. this.updateReadOnly();
  295. },
  296. onEnable: function() {
  297. this.callParent();
  298. this.disabled = false;
  299. this.updateReadOnly();
  300. },
  301. setReadOnly: function(readOnly) {
  302. this.readOnly = readOnly;
  303. this.updateReadOnly();
  304. },
  305. /**
  306. * @private Lock or unlock the BoundList's selection model to match the current disabled/readonly state
  307. */
  308. updateReadOnly: function() {
  309. var me = this,
  310. boundList = me.boundList,
  311. readOnly = me.readOnly || me.disabled;
  312. if (boundList) {
  313. boundList.getSelectionModel().setLocked(readOnly);
  314. }
  315. },
  316. onDestroy: function(){
  317. Ext.destroyMembers(this, 'panel', 'boundList', 'dragZone', 'dropZone');
  318. this.callParent();
  319. }
  320. });