MultiSelect.js 13 KB

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