MultiSelect.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. /**
  2. * A control that allows selection of multiple items in a list
  3. */
  4. Ext.define('Ext.ux.form.MultiSelect', {
  5. extend: 'Ext.form.FieldContainer',
  6. mixins: {
  7. bindable: 'Ext.util.Bindable',
  8. field: 'Ext.form.field.Field'
  9. },
  10. alternateClassName: 'Ext.ux.Multiselect',
  11. alias: ['widget.multiselectfield', 'widget.multiselect'],
  12. requires: ['Ext.panel.Panel', 'Ext.view.BoundList', 'Ext.layout.container.Fit'],
  13. uses: ['Ext.view.DragZone', 'Ext.view.DropZone'],
  14. layout: 'fit',
  15. /**
  16. * @cfg {String} [dragGroup=""] The ddgroup name for the MultiSelect DragZone.
  17. */
  18. /**
  19. * @cfg {String} [dropGroup=""] The ddgroup name for the MultiSelect DropZone.
  20. */
  21. /**
  22. * @cfg {String} [title=""] A title for the underlying panel.
  23. */
  24. /**
  25. * @cfg {Boolean} [ddReorder=false] Whether the items in the MultiSelect list are drag/drop reorderable.
  26. */
  27. ddReorder: false,
  28. /**
  29. * @cfg {Object/Array} tbar An optional toolbar to be inserted at the top of the control's selection list.
  30. * This can be a {@link Ext.toolbar.Toolbar} object, a toolbar config, or an array of buttons/button configs
  31. * to be added to the toolbar. See {@link Ext.panel.Panel#tbar}.
  32. */
  33. /**
  34. * @cfg {String} [appendOnly=false] True if the list should only allow append drops when drag/drop is enabled.
  35. * This is useful for lists which are sorted.
  36. */
  37. appendOnly: false,
  38. /**
  39. * @cfg {String} [displayField="text"] Name of the desired display field in the dataset.
  40. */
  41. displayField: 'text',
  42. /**
  43. * @cfg {String} [valueField="text"] Name of the desired value field in the dataset.
  44. */
  45. /**
  46. * @cfg {Boolean} [allowBlank=true] False to require at least one item in the list to be selected, true to allow no
  47. * selection.
  48. */
  49. allowBlank: true,
  50. /**
  51. * @cfg {Number} [minSelections=0] Minimum number of selections allowed.
  52. */
  53. minSelections: 0,
  54. /**
  55. * @cfg {Number} [maxSelections=Number.MAX_VALUE] Maximum number of selections allowed.
  56. */
  57. maxSelections: Number.MAX_VALUE,
  58. /**
  59. * @cfg {String} [blankText="This field is required"] Default text displayed when the control contains no items.
  60. */
  61. blankText: 'This field is required',
  62. /**
  63. * @cfg {String} [minSelectionsText="Minimum {0}item(s) required"]
  64. * Validation message displayed when {@link #minSelections} is not met.
  65. * The {0} token will be replaced by the value of {@link #minSelections}.
  66. */
  67. minSelectionsText: 'Minimum {0} item(s) required',
  68. /**
  69. * @cfg {String} [maxSelectionsText="Maximum {0}item(s) allowed"]
  70. * Validation message displayed when {@link #maxSelections} is not met
  71. * The {0} token will be replaced by the value of {@link #maxSelections}.
  72. */
  73. maxSelectionsText: 'Minimum {0} item(s) required',
  74. /**
  75. * @cfg {String} [delimiter=","] The string used to delimit the selected values when {@link #getSubmitValue submitting}
  76. * the field as part of a form. If you wish to have the selected values submitted as separate
  77. * parameters rather than a single delimited parameter, set this to <tt>null</tt>.
  78. */
  79. delimiter: ',',
  80. /**
  81. * @cfg {Ext.data.Store/Array} store The data source to which this MultiSelect is bound (defaults to <tt>undefined</tt>).
  82. * Acceptable values for this property are:
  83. * <div class="mdetail-params"><ul>
  84. * <li><b>any {@link Ext.data.Store Store} subclass</b></li>
  85. * <li><b>an Array</b> : Arrays will be converted to a {@link Ext.data.ArrayStore} internally.
  86. * <div class="mdetail-params"><ul>
  87. * <li><b>1-dimensional array</b> : (e.g., <tt>['Foo','Bar']</tt>)<div class="sub-desc">
  88. * A 1-dimensional array will automatically be expanded (each array item will be the combo
  89. * {@link #valueField value} and {@link #displayField text})</div></li>
  90. * <li><b>2-dimensional array</b> : (e.g., <tt>[['f','Foo'],['b','Bar']]</tt>)<div class="sub-desc">
  91. * For a multi-dimensional array, the value in index 0 of each item will be assumed to be the combo
  92. * {@link #valueField value}, while the value at index 1 is assumed to be the combo {@link #displayField text}.
  93. * </div></li></ul></div></li></ul></div>
  94. */
  95. ignoreSelectChange: 0,
  96. /**
  97. * @cfg {Object} listConfig
  98. * An optional set of configuration properties that will be passed to the {@link Ext.view.BoundList}'s constructor.
  99. * Any configuration that is valid for BoundList can be included.
  100. */
  101. initComponent: function(){
  102. var me = this;
  103. me.bindStore(me.store, true);
  104. if (me.store.autoCreated) {
  105. me.valueField = me.displayField = 'field1';
  106. if (!me.store.expanded) {
  107. me.displayField = 'field2';
  108. }
  109. }
  110. if (!Ext.isDefined(me.valueField)) {
  111. me.valueField = me.displayField;
  112. }
  113. me.items = me.setupItems();
  114. me.callParent();
  115. me.initField();
  116. me.addEvents('drop');
  117. },
  118. setupItems: function() {
  119. var me = this;
  120. me.boundList = Ext.create('Ext.view.BoundList', Ext.apply({
  121. deferInitialRefresh: false,
  122. border: false,
  123. multiSelect: true,
  124. store: me.store,
  125. displayField: me.displayField,
  126. disabled: me.disabled
  127. }, me.listConfig));
  128. me.boundList.getSelectionModel().on('selectionchange', me.onSelectChange, me);
  129. return {
  130. border: true,
  131. layout: 'fit',
  132. title: me.title,
  133. tbar: me.tbar,
  134. items: me.boundList
  135. };
  136. },
  137. onSelectChange: function(selModel, selections){
  138. if (!this.ignoreSelectChange) {
  139. this.setValue(selections);
  140. }
  141. },
  142. getSelected: function(){
  143. return this.boundList.getSelectionModel().getSelection();
  144. },
  145. // compare array values
  146. isEqual: function(v1, v2) {
  147. var fromArray = Ext.Array.from,
  148. i = 0,
  149. len;
  150. v1 = fromArray(v1);
  151. v2 = fromArray(v2);
  152. len = v1.length;
  153. if (len !== v2.length) {
  154. return false;
  155. }
  156. for(; i < len; i++) {
  157. if (v2[i] !== v1[i]) {
  158. return false;
  159. }
  160. }
  161. return true;
  162. },
  163. afterRender: function(){
  164. var me = this;
  165. me.callParent();
  166. if (me.selectOnRender) {
  167. ++me.ignoreSelectChange;
  168. me.boundList.getSelectionModel().select(me.getRecordsForValue(me.value));
  169. --me.ignoreSelectChange;
  170. delete me.toSelect;
  171. }
  172. if (me.ddReorder && !me.dragGroup && !me.dropGroup){
  173. me.dragGroup = me.dropGroup = 'MultiselectDD-' + Ext.id();
  174. }
  175. if (me.draggable || me.dragGroup){
  176. me.dragZone = Ext.create('Ext.view.DragZone', {
  177. view: me.boundList,
  178. ddGroup: me.dragGroup,
  179. dragText: '{0} Item{1}'
  180. });
  181. }
  182. if (me.droppable || me.dropGroup){
  183. me.dropZone = Ext.create('Ext.view.DropZone', {
  184. view: me.boundList,
  185. ddGroup: me.dropGroup,
  186. handleNodeDrop: function(data, dropRecord, position) {
  187. var view = this.view,
  188. store = view.getStore(),
  189. records = data.records,
  190. index;
  191. // remove the Models from the source Store
  192. data.view.store.remove(records);
  193. index = store.indexOf(dropRecord);
  194. if (position === 'after') {
  195. index++;
  196. }
  197. store.insert(index, records);
  198. view.getSelectionModel().select(records);
  199. me.fireEvent('drop', me, records);
  200. }
  201. });
  202. }
  203. },
  204. isValid : function() {
  205. var me = this,
  206. disabled = me.disabled,
  207. validate = me.forceValidation || !disabled;
  208. return validate ? me.validateValue(me.value) : disabled;
  209. },
  210. validateValue: function(value) {
  211. var me = this,
  212. errors = me.getErrors(value),
  213. isValid = Ext.isEmpty(errors);
  214. if (!me.preventMark) {
  215. if (isValid) {
  216. me.clearInvalid();
  217. } else {
  218. me.markInvalid(errors);
  219. }
  220. }
  221. return isValid;
  222. },
  223. markInvalid : function(errors) {
  224. // Save the message and fire the 'invalid' event
  225. var me = this,
  226. oldMsg = me.getActiveError();
  227. me.setActiveErrors(Ext.Array.from(errors));
  228. if (oldMsg !== me.getActiveError()) {
  229. me.updateLayout();
  230. }
  231. },
  232. /**
  233. * Clear any invalid styles/messages for this field.
  234. *
  235. * **Note**: this method does not cause the Field's {@link #validate} or {@link #isValid} methods to return `true`
  236. * if the value does not _pass_ validation. So simply clearing a field's errors will not necessarily allow
  237. * submission of forms submitted with the {@link Ext.form.action.Submit#clientValidation} option set.
  238. */
  239. clearInvalid : function() {
  240. // Clear the message and fire the 'valid' event
  241. var me = this,
  242. hadError = me.hasActiveError();
  243. me.unsetActiveError();
  244. if (hadError) {
  245. me.updateLayout();
  246. }
  247. },
  248. getSubmitData: function() {
  249. var me = this,
  250. data = null,
  251. val;
  252. if (!me.disabled && me.submitValue && !me.isFileUpload()) {
  253. val = me.getSubmitValue();
  254. if (val !== null) {
  255. data = {};
  256. data[me.getName()] = val;
  257. }
  258. }
  259. return data;
  260. },
  261. /**
  262. * Returns the value that would be included in a standard form submit for this field.
  263. *
  264. * @return {String} The value to be submitted, or null.
  265. */
  266. getSubmitValue: function() {
  267. var me = this,
  268. delimiter = me.delimiter,
  269. val = me.getValue();
  270. return Ext.isString(delimiter) ? val.join(delimiter) : val;
  271. },
  272. getValue: function(){
  273. return this.value;
  274. },
  275. getRecordsForValue: function(value){
  276. var me = this,
  277. records = [],
  278. all = me.store.getRange(),
  279. valueField = me.valueField,
  280. i = 0,
  281. allLen = all.length,
  282. rec,
  283. j,
  284. valueLen;
  285. for (valueLen = value.length; i < valueLen; ++i) {
  286. for (j = 0; j < allLen; ++j) {
  287. rec = all[j];
  288. if (rec.get(valueField) == value[i]) {
  289. records.push(rec);
  290. }
  291. }
  292. }
  293. return records;
  294. },
  295. setupValue: function(value){
  296. var delimiter = this.delimiter,
  297. valueField = this.valueField,
  298. i = 0,
  299. out,
  300. len,
  301. item;
  302. if (Ext.isDefined(value)) {
  303. if (delimiter && Ext.isString(value)) {
  304. value = value.split(delimiter);
  305. } else if (!Ext.isArray(value)) {
  306. value = [value];
  307. }
  308. for (len = value.length; i < len; ++i) {
  309. item = value[i];
  310. if (item && item.isModel) {
  311. value[i] = item.get(valueField);
  312. }
  313. }
  314. out = Ext.Array.unique(value);
  315. } else {
  316. out = [];
  317. }
  318. return out;
  319. },
  320. setValue: function(value){
  321. var me = this,
  322. selModel = me.boundList.getSelectionModel();
  323. // Store not loaded yet - we cannot set the value
  324. if (!me.store.getCount()) {
  325. me.store.on({
  326. load: Ext.Function.bind(me.setValue, me, [value]),
  327. single: true
  328. });
  329. return;
  330. }
  331. value = me.setupValue(value);
  332. me.mixins.field.setValue.call(me, value);
  333. if (me.rendered) {
  334. ++me.ignoreSelectChange;
  335. selModel.deselectAll();
  336. selModel.select(me.getRecordsForValue(value));
  337. --me.ignoreSelectChange;
  338. } else {
  339. me.selectOnRender = true;
  340. }
  341. },
  342. clearValue: function(){
  343. this.setValue([]);
  344. },
  345. onEnable: function(){
  346. var list = this.boundList;
  347. this.callParent();
  348. if (list) {
  349. list.enable();
  350. }
  351. },
  352. onDisable: function(){
  353. var list = this.boundList;
  354. this.callParent();
  355. if (list) {
  356. list.disable();
  357. }
  358. },
  359. getErrors : function(value) {
  360. var me = this,
  361. format = Ext.String.format,
  362. errors = [],
  363. numSelected;
  364. value = Ext.Array.from(value || me.getValue());
  365. numSelected = value.length;
  366. if (!me.allowBlank && numSelected < 1) {
  367. errors.push(me.blankText);
  368. }
  369. if (numSelected < me.minSelections) {
  370. errors.push(format(me.minSelectionsText, me.minSelections));
  371. }
  372. if (numSelected > me.maxSelections) {
  373. errors.push(format(me.maxSelectionsText, me.maxSelections));
  374. }
  375. return errors;
  376. },
  377. onDestroy: function(){
  378. var me = this;
  379. me.bindStore(null);
  380. Ext.destroy(me.dragZone, me.dropZone);
  381. me.callParent();
  382. },
  383. onBindStore: function(store){
  384. var boundList = this.boundList;
  385. if (boundList) {
  386. boundList.bindStore(store);
  387. }
  388. }
  389. });