MultiSelect.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596
  1. /*!
  2. * Ext JS Library 3.0.0
  3. * Copyright(c) 2006-2009 Ext JS, LLC
  4. * licensing@extjs.com
  5. * http://www.extjs.com/license
  6. */
  7. Ext.ns('Ext.ux.form');
  8. /**
  9. * @class Ext.ux.form.MultiSelect
  10. * @extends Ext.form.Field
  11. * A control that allows selection and form submission of multiple list items.
  12. *
  13. * @history
  14. * 2008-06-19 bpm Original code contributed by Toby Stuart (with contributions from Robert Williams)
  15. * 2008-06-19 bpm Docs and demo code clean up
  16. *
  17. * @constructor
  18. * Create a new MultiSelect
  19. * @param {Object} config Configuration options
  20. * @xtype multiselect
  21. */
  22. Ext.ux.form.MultiSelect = Ext.extend(Ext.form.Field, {
  23. /**
  24. * @cfg {String} legend Wraps the object with a fieldset and specified legend.
  25. */
  26. /**
  27. * @cfg {Ext.ListView} view The {@link Ext.ListView} used to render the multiselect 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 The top toolbar of the control. This can be a {@link Ext.Toolbar} object, a
  41. * toolbar config, or an array of buttons/button configs to be added to the toolbar.
  42. */
  43. /**
  44. * @cfg {String} appendOnly True if the list should only allow append drops when drag/drop is enabled
  45. * (use for lists which are sorted, defaults to false).
  46. */
  47. appendOnly:false,
  48. /**
  49. * @cfg {Number} width Width in pixels of the control (defaults to 100).
  50. */
  51. width:100,
  52. /**
  53. * @cfg {Number} height Height in pixels of the control (defaults to 100).
  54. */
  55. height:100,
  56. /**
  57. * @cfg {String/Number} displayField Name/Index of the desired display field in the dataset (defaults to 0).
  58. */
  59. displayField:0,
  60. /**
  61. * @cfg {String/Number} valueField Name/Index of the desired value field in the dataset (defaults to 1).
  62. */
  63. valueField:1,
  64. /**
  65. * @cfg {Boolean} allowBlank False to require at least one item in the list to be selected, true to allow no
  66. * selection (defaults to true).
  67. */
  68. allowBlank:true,
  69. /**
  70. * @cfg {Number} minSelections Minimum number of selections allowed (defaults to 0).
  71. */
  72. minSelections:0,
  73. /**
  74. * @cfg {Number} maxSelections Maximum number of selections allowed (defaults to Number.MAX_VALUE).
  75. */
  76. maxSelections:Number.MAX_VALUE,
  77. /**
  78. * @cfg {String} blankText Default text displayed when the control contains no items (defaults to the same value as
  79. * {@link Ext.form.TextField#blankText}.
  80. */
  81. blankText:Ext.form.TextField.prototype.blankText,
  82. /**
  83. * @cfg {String} minSelectionsText Validation message displayed when {@link #minSelections} is not met (defaults to 'Minimum {0}
  84. * item(s) required'). The {0} token will be replaced by the value of {@link #minSelections}.
  85. */
  86. minSelectionsText:'Minimum {0} item(s) required',
  87. /**
  88. * @cfg {String} maxSelectionsText Validation message displayed when {@link #maxSelections} is not met (defaults to 'Maximum {0}
  89. * item(s) allowed'). The {0} token will be replaced by the value of {@link #maxSelections}.
  90. */
  91. maxSelectionsText:'Maximum {0} item(s) allowed',
  92. /**
  93. * @cfg {String} delimiter The string used to delimit between items when set or returned as a string of values
  94. * (defaults to ',').
  95. */
  96. delimiter:',',
  97. /**
  98. * @cfg {Ext.data.Store/Array} store The data source to which this MultiSelect is bound (defaults to <tt>undefined</tt>).
  99. * Acceptable values for this property are:
  100. * <div class="mdetail-params"><ul>
  101. * <li><b>any {@link Ext.data.Store Store} subclass</b></li>
  102. * <li><b>an Array</b> : Arrays will be converted to a {@link Ext.data.ArrayStore} internally.
  103. * <div class="mdetail-params"><ul>
  104. * <li><b>1-dimensional array</b> : (e.g., <tt>['Foo','Bar']</tt>)<div class="sub-desc">
  105. * A 1-dimensional array will automatically be expanded (each array item will be the combo
  106. * {@link #valueField value} and {@link #displayField text})</div></li>
  107. * <li><b>2-dimensional array</b> : (e.g., <tt>[['f','Foo'],['b','Bar']]</tt>)<div class="sub-desc">
  108. * For a multi-dimensional array, the value in index 0 of each item will be assumed to be the combo
  109. * {@link #valueField value}, while the value at index 1 is assumed to be the combo {@link #displayField text}.
  110. * </div></li></ul></div></li></ul></div>
  111. */
  112. // private
  113. defaultAutoCreate : {tag: "div"},
  114. // private
  115. initComponent: function(){
  116. Ext.ux.form.MultiSelect.superclass.initComponent.call(this);
  117. if(Ext.isArray(this.store)){
  118. if (Ext.isArray(this.store[0])){
  119. this.store = new Ext.data.ArrayStore({
  120. fields: ['value','text'],
  121. data: this.store
  122. });
  123. this.valueField = 'value';
  124. }else{
  125. this.store = new Ext.data.ArrayStore({
  126. fields: ['text'],
  127. data: this.store,
  128. expandData: true
  129. });
  130. this.valueField = 'text';
  131. }
  132. this.displayField = 'text';
  133. } else {
  134. this.store = Ext.StoreMgr.lookup(this.store);
  135. }
  136. this.addEvents({
  137. 'dblclick' : true,
  138. 'click' : true,
  139. 'change' : true,
  140. 'drop' : true
  141. });
  142. },
  143. // private
  144. onRender: function(ct, position){
  145. Ext.ux.form.MultiSelect.superclass.onRender.call(this, ct, position);
  146. var fs = this.fs = new Ext.form.FieldSet({
  147. renderTo: this.el,
  148. title: this.legend,
  149. height: this.height,
  150. width: this.width,
  151. style: "padding:0;",
  152. tbar: this.tbar,
  153. bodyStyle: 'overflow: auto;'
  154. });
  155. this.view = new Ext.ListView({
  156. multiSelect: true,
  157. store: this.store,
  158. columns: [{ header: 'Value', width: 1, dataIndex: this.displayField }],
  159. hideHeaders: true
  160. });
  161. fs.add(this.view);
  162. this.view.on('click', this.onViewClick, this);
  163. this.view.on('beforeclick', this.onViewBeforeClick, this);
  164. this.view.on('dblclick', this.onViewDblClick, this);
  165. this.hiddenName = this.name || Ext.id();
  166. var hiddenTag = { tag: "input", type: "hidden", value: "", name: this.hiddenName };
  167. this.hiddenField = this.el.createChild(hiddenTag);
  168. this.hiddenField.dom.disabled = this.hiddenName != this.name;
  169. fs.doLayout();
  170. },
  171. // private
  172. afterRender: function(){
  173. Ext.ux.form.MultiSelect.superclass.afterRender.call(this);
  174. if (this.ddReorder && !this.dragGroup && !this.dropGroup){
  175. this.dragGroup = this.dropGroup = 'MultiselectDD-' + Ext.id();
  176. }
  177. if (this.draggable || this.dragGroup){
  178. this.dragZone = new Ext.ux.form.MultiSelect.DragZone(this, {
  179. ddGroup: this.dragGroup
  180. });
  181. }
  182. if (this.droppable || this.dropGroup){
  183. this.dropZone = new Ext.ux.form.MultiSelect.DropZone(this, {
  184. ddGroup: this.dropGroup
  185. });
  186. }
  187. },
  188. // private
  189. onViewClick: function(vw, index, node, e) {
  190. this.fireEvent('change', this, this.getValue(), this.hiddenField.dom.value);
  191. this.hiddenField.dom.value = this.getValue();
  192. this.fireEvent('click', this, e);
  193. this.validate();
  194. },
  195. // private
  196. onViewBeforeClick: function(vw, index, node, e) {
  197. if (this.disabled) {return false;}
  198. },
  199. // private
  200. onViewDblClick : function(vw, index, node, e) {
  201. return this.fireEvent('dblclick', vw, index, node, e);
  202. },
  203. /**
  204. * Returns an array of data values for the selected items in the list. The values will be separated
  205. * by {@link #delimiter}.
  206. * @return {Array} value An array of string data values
  207. */
  208. getValue: function(valueField){
  209. var returnArray = [];
  210. var selectionsArray = this.view.getSelectedIndexes();
  211. if (selectionsArray.length == 0) {return '';}
  212. for (var i=0; i<selectionsArray.length; i++) {
  213. returnArray.push(this.store.getAt(selectionsArray[i]).get((valueField != null) ? valueField : this.valueField));
  214. }
  215. return returnArray.join(this.delimiter);
  216. },
  217. /**
  218. * Sets a delimited string (using {@link #delimiter}) or array of data values into the list.
  219. * @param {String/Array} values The values to set
  220. */
  221. setValue: function(values) {
  222. var index;
  223. var selections = [];
  224. this.view.clearSelections();
  225. this.hiddenField.dom.value = '';
  226. if (!values || (values == '')) { return; }
  227. if (!Ext.isArray(values)) { values = values.split(this.delimiter); }
  228. for (var i=0; i<values.length; i++) {
  229. index = this.view.store.indexOf(this.view.store.query(this.valueField,
  230. new RegExp('^' + values[i] + '$', "i")).itemAt(0));
  231. selections.push(index);
  232. }
  233. this.view.select(selections);
  234. this.hiddenField.dom.value = this.getValue();
  235. this.validate();
  236. },
  237. // inherit docs
  238. reset : function() {
  239. this.setValue('');
  240. },
  241. // inherit docs
  242. getRawValue: function(valueField) {
  243. var tmp = this.getValue(valueField);
  244. if (tmp.length) {
  245. tmp = tmp.split(this.delimiter);
  246. }
  247. else {
  248. tmp = [];
  249. }
  250. return tmp;
  251. },
  252. // inherit docs
  253. setRawValue: function(values){
  254. setValue(values);
  255. },
  256. // inherit docs
  257. validateValue : function(value){
  258. if (value.length < 1) { // if it has no value
  259. if (this.allowBlank) {
  260. this.clearInvalid();
  261. return true;
  262. } else {
  263. this.markInvalid(this.blankText);
  264. return false;
  265. }
  266. }
  267. if (value.length < this.minSelections) {
  268. this.markInvalid(String.format(this.minSelectionsText, this.minSelections));
  269. return false;
  270. }
  271. if (value.length > this.maxSelections) {
  272. this.markInvalid(String.format(this.maxSelectionsText, this.maxSelections));
  273. return false;
  274. }
  275. return true;
  276. },
  277. // inherit docs
  278. disable: function(){
  279. this.disabled = true;
  280. this.hiddenField.dom.disabled = true;
  281. this.fs.disable();
  282. },
  283. // inherit docs
  284. enable: function(){
  285. this.disabled = false;
  286. this.hiddenField.dom.disabled = false;
  287. this.fs.enable();
  288. },
  289. // inherit docs
  290. destroy: function(){
  291. Ext.destroy(this.fs, this.dragZone, this.dropZone);
  292. Ext.ux.form.MultiSelect.superclass.destroy.call(this);
  293. }
  294. });
  295. Ext.reg('multiselect', Ext.ux.form.MultiSelect);
  296. //backwards compat
  297. Ext.ux.Multiselect = Ext.ux.form.MultiSelect;
  298. Ext.ux.form.MultiSelect.DragZone = function(ms, config){
  299. this.ms = ms;
  300. this.view = ms.view;
  301. var ddGroup = config.ddGroup || 'MultiselectDD';
  302. var dd;
  303. if (Ext.isArray(ddGroup)){
  304. dd = ddGroup.shift();
  305. } else {
  306. dd = ddGroup;
  307. ddGroup = null;
  308. }
  309. Ext.ux.form.MultiSelect.DragZone.superclass.constructor.call(this, this.ms.fs.body, { containerScroll: true, ddGroup: dd });
  310. this.setDraggable(ddGroup);
  311. };
  312. Ext.extend(Ext.ux.form.MultiSelect.DragZone, Ext.dd.DragZone, {
  313. onInitDrag : function(x, y){
  314. var el = Ext.get(this.dragData.ddel.cloneNode(true));
  315. this.proxy.update(el.dom);
  316. el.setWidth(el.child('em').getWidth());
  317. this.onStartDrag(x, y);
  318. return true;
  319. },
  320. // private
  321. collectSelection: function(data) {
  322. data.repairXY = Ext.fly(this.view.getSelectedNodes()[0]).getXY();
  323. var i = 0;
  324. this.view.store.each(function(rec){
  325. if (this.view.isSelected(i)) {
  326. var n = this.view.getNode(i);
  327. var dragNode = n.cloneNode(true);
  328. dragNode.id = Ext.id();
  329. data.ddel.appendChild(dragNode);
  330. data.records.push(this.view.store.getAt(i));
  331. data.viewNodes.push(n);
  332. }
  333. i++;
  334. }, this);
  335. },
  336. // override
  337. onEndDrag: function(data, e) {
  338. var d = Ext.get(this.dragData.ddel);
  339. if (d && d.hasClass("multi-proxy")) {
  340. d.remove();
  341. }
  342. },
  343. // override
  344. getDragData: function(e){
  345. var target = this.view.findItemFromChild(e.getTarget());
  346. if(target) {
  347. if (!this.view.isSelected(target) && !e.ctrlKey && !e.shiftKey) {
  348. this.view.select(target);
  349. this.ms.setValue(this.ms.getValue());
  350. }
  351. if (this.view.getSelectionCount() == 0 || e.ctrlKey || e.shiftKey) return false;
  352. var dragData = {
  353. sourceView: this.view,
  354. viewNodes: [],
  355. records: []
  356. };
  357. if (this.view.getSelectionCount() == 1) {
  358. var i = this.view.getSelectedIndexes()[0];
  359. var n = this.view.getNode(i);
  360. dragData.viewNodes.push(dragData.ddel = n);
  361. dragData.records.push(this.view.store.getAt(i));
  362. dragData.repairXY = Ext.fly(n).getXY();
  363. } else {
  364. dragData.ddel = document.createElement('div');
  365. dragData.ddel.className = 'multi-proxy';
  366. this.collectSelection(dragData);
  367. }
  368. return dragData;
  369. }
  370. return false;
  371. },
  372. // override the default repairXY.
  373. getRepairXY : function(e){
  374. return this.dragData.repairXY;
  375. },
  376. // private
  377. setDraggable: function(ddGroup){
  378. if (!ddGroup) return;
  379. if (Ext.isArray(ddGroup)) {
  380. Ext.each(ddGroup, this.setDraggable, this);
  381. return;
  382. }
  383. this.addToGroup(ddGroup);
  384. }
  385. });
  386. Ext.ux.form.MultiSelect.DropZone = function(ms, config){
  387. this.ms = ms;
  388. this.view = ms.view;
  389. var ddGroup = config.ddGroup || 'MultiselectDD';
  390. var dd;
  391. if (Ext.isArray(ddGroup)){
  392. dd = ddGroup.shift();
  393. } else {
  394. dd = ddGroup;
  395. ddGroup = null;
  396. }
  397. Ext.ux.form.MultiSelect.DropZone.superclass.constructor.call(this, this.ms.fs.body, { containerScroll: true, ddGroup: dd });
  398. this.setDroppable(ddGroup);
  399. };
  400. Ext.extend(Ext.ux.form.MultiSelect.DropZone, Ext.dd.DropZone, {
  401. /**
  402. * Part of the Ext.dd.DropZone interface. If no target node is found, the
  403. * whole Element becomes the target, and this causes the drop gesture to append.
  404. */
  405. getTargetFromEvent : function(e) {
  406. var target = e.getTarget();
  407. return target;
  408. },
  409. // private
  410. getDropPoint : function(e, n, dd){
  411. if (n == this.ms.fs.body.dom) { return "below"; }
  412. var t = Ext.lib.Dom.getY(n), b = t + n.offsetHeight;
  413. var c = t + (b - t) / 2;
  414. var y = Ext.lib.Event.getPageY(e);
  415. if(y <= c) {
  416. return "above";
  417. }else{
  418. return "below";
  419. }
  420. },
  421. // private
  422. isValidDropPoint: function(pt, n, data) {
  423. if (!data.viewNodes || (data.viewNodes.length != 1)) {
  424. return true;
  425. }
  426. var d = data.viewNodes[0];
  427. if (d == n) {
  428. return false;
  429. }
  430. if ((pt == "below") && (n.nextSibling == d)) {
  431. return false;
  432. }
  433. if ((pt == "above") && (n.previousSibling == d)) {
  434. return false;
  435. }
  436. return true;
  437. },
  438. // override
  439. onNodeEnter : function(n, dd, e, data){
  440. return false;
  441. },
  442. // override
  443. onNodeOver : function(n, dd, e, data){
  444. var dragElClass = this.dropNotAllowed;
  445. var pt = this.getDropPoint(e, n, dd);
  446. if (this.isValidDropPoint(pt, n, data)) {
  447. if (this.ms.appendOnly) {
  448. return "x-tree-drop-ok-below";
  449. }
  450. // set the insert point style on the target node
  451. if (pt) {
  452. var targetElClass;
  453. if (pt == "above"){
  454. dragElClass = n.previousSibling ? "x-tree-drop-ok-between" : "x-tree-drop-ok-above";
  455. targetElClass = "x-view-drag-insert-above";
  456. } else {
  457. dragElClass = n.nextSibling ? "x-tree-drop-ok-between" : "x-tree-drop-ok-below";
  458. targetElClass = "x-view-drag-insert-below";
  459. }
  460. if (this.lastInsertClass != targetElClass){
  461. Ext.fly(n).replaceClass(this.lastInsertClass, targetElClass);
  462. this.lastInsertClass = targetElClass;
  463. }
  464. }
  465. }
  466. return dragElClass;
  467. },
  468. // private
  469. onNodeOut : function(n, dd, e, data){
  470. this.removeDropIndicators(n);
  471. },
  472. // private
  473. onNodeDrop : function(n, dd, e, data){
  474. if (this.ms.fireEvent("drop", this, n, dd, e, data) === false) {
  475. return false;
  476. }
  477. var pt = this.getDropPoint(e, n, dd);
  478. if (n != this.ms.fs.body.dom)
  479. n = this.view.findItemFromChild(n);
  480. var insertAt = (this.ms.appendOnly || (n == this.ms.fs.body.dom)) ? this.view.store.getCount() : this.view.indexOf(n);
  481. if (pt == "below") {
  482. insertAt++;
  483. }
  484. var dir = false;
  485. // Validate if dragging within the same MultiSelect
  486. if (data.sourceView == this.view) {
  487. // If the first element to be inserted below is the target node, remove it
  488. if (pt == "below") {
  489. if (data.viewNodes[0] == n) {
  490. data.viewNodes.shift();
  491. }
  492. } else { // If the last element to be inserted above is the target node, remove it
  493. if (data.viewNodes[data.viewNodes.length - 1] == n) {
  494. data.viewNodes.pop();
  495. }
  496. }
  497. // Nothing to drop...
  498. if (!data.viewNodes.length) {
  499. return false;
  500. }
  501. // If we are moving DOWN, then because a store.remove() takes place first,
  502. // the insertAt must be decremented.
  503. if (insertAt > this.view.store.indexOf(data.records[0])) {
  504. dir = 'down';
  505. insertAt--;
  506. }
  507. }
  508. for (var i = 0; i < data.records.length; i++) {
  509. var r = data.records[i];
  510. if (data.sourceView) {
  511. data.sourceView.store.remove(r);
  512. }
  513. this.view.store.insert(dir == 'down' ? insertAt : insertAt++, r);
  514. var si = this.view.store.sortInfo;
  515. if(si){
  516. this.view.store.sort(si.field, si.direction);
  517. }
  518. }
  519. return true;
  520. },
  521. // private
  522. removeDropIndicators : function(n){
  523. if(n){
  524. Ext.fly(n).removeClass([
  525. "x-view-drag-insert-above",
  526. "x-view-drag-insert-left",
  527. "x-view-drag-insert-right",
  528. "x-view-drag-insert-below"]);
  529. this.lastInsertClass = "_noclass";
  530. }
  531. },
  532. // private
  533. setDroppable: function(ddGroup){
  534. if (!ddGroup) return;
  535. if (Ext.isArray(ddGroup)) {
  536. Ext.each(ddGroup, this.setDroppable, this);
  537. return;
  538. }
  539. this.addToGroup(ddGroup);
  540. }
  541. });