/src/w2grid.js

https://github.com/delebash/w2ui · JavaScript · 5177 lines · 4531 code · 201 blank · 445 comment · 1920 complexity · 4ac01842935df7bb7d4875cf0b3ada86 MD5 · raw file

Large files are truncated click here to view the full file

  1. /************************************************************************
  2. * Library: Web 2.0 UI for jQuery (using prototypical inheritance)
  3. * - Following objects defined
  4. * - w2grid - grid widget
  5. * - $().w2grid - jQuery wrapper
  6. * - Dependencies: jQuery, w2utils, w2toolbar, w2fields, w2alert, w2confirm
  7. *
  8. * == NICE TO HAVE ==
  9. * - frozen columns
  10. * - add colspans
  11. * - allow this.total to be unknown (-1)
  12. * - column autosize based on largest content
  13. * - easy bubbles in the grid
  14. * - More than 2 layers of header groups
  15. * - reorder columns/records
  16. * - hidden searches could not be clearned by the user
  17. * - problem with .set() and arrays, array get extended too, but should be replaced
  18. * - move events into prototype
  19. * - add grid.focus()
  20. * - add showExtra, KickIn Infinite scroll when so many records
  21. * - after edit stay on the same record option
  22. * - allow render: function to be filters
  23. * - if supplied array of ids, get should return array of records
  24. * - row drag and drop has bugs
  25. *
  26. * == 1.5 changes
  27. * - $('#grid').w2grid() - if called w/o argument then it returns grid object
  28. * - added statusRange : true,
  29. * statusBuffered : false,
  30. * statusRecordID : true,
  31. * statusSelection : true,
  32. * statusResponse : true,
  33. * statusSort : true,
  34. * statusSearch : true,
  35. * - change selectAll() and selectNone() - return time it took
  36. *
  37. ************************************************************************/
  38. (function () {
  39. var w2grid = function(options) {
  40. // public properties
  41. this.name = null;
  42. this.box = null; // HTML element that hold this element
  43. this.header = '';
  44. this.url = '';
  45. this.routeData = {}; // data for dynamic routes
  46. this.columns = []; // { field, caption, size, attr, render, hidden, gridMinWidth, editable }
  47. this.columnGroups = []; // { span: int, caption: 'string', master: true/false }
  48. this.records = []; // { recid: int(requied), field1: 'value1', ... fieldN: 'valueN', style: 'string', editable: true/false, summary: true/false, changes: object }
  49. this.summary = []; // arry of summary records, same structure as records array
  50. this.searches = []; // { type, caption, field, inTag, outTag, hidden }
  51. this.searchData = [];
  52. this.sortData = [];
  53. this.postData = {};
  54. this.toolbar = {}; // if not empty object; then it is toolbar object
  55. this.show = {
  56. header : false,
  57. toolbar : false,
  58. footer : false,
  59. columnHeaders : true,
  60. lineNumbers : false,
  61. expandColumn : false,
  62. selectColumn : false,
  63. emptyRecords : true,
  64. toolbarReload : true,
  65. toolbarColumns : true,
  66. toolbarSearch : true,
  67. toolbarAdd : false,
  68. toolbarEdit : false,
  69. toolbarDelete : false,
  70. toolbarSave : false,
  71. statusRange : true,
  72. statusBuffered : false,
  73. statusRecordID : true,
  74. statusSelection : true,
  75. statusResponse : true,
  76. statusSort : true,
  77. statusSearch : true,
  78. recordTitles : true,
  79. selectionBorder : true,
  80. skipRecords : true
  81. };
  82. this.autoLoad = true; // for infinite scroll
  83. this.fixedBody = true; // if false; then grid grows with data
  84. this.recordHeight = 24;
  85. this.keyboard = true;
  86. this.selectType = 'row'; // can be row|cell
  87. this.multiSearch = true;
  88. this.multiSelect = true;
  89. this.multiSort = true;
  90. this.reorderColumns = false;
  91. this.reorderRows = false;
  92. this.markSearch = true;
  93. this.total = 0; // server total
  94. this.limit = 100;
  95. this.offset = 0; // how many records to skip (for infinite scroll) when pulling from server
  96. this.style = '';
  97. this.ranges = [];
  98. this.menu = [];
  99. this.method = null; // if defined, then overwrited ajax method
  100. this.recid = null;
  101. this.parser = null;
  102. // events
  103. this.onAdd = null;
  104. this.onEdit = null;
  105. this.onRequest = null; // called on any server event
  106. this.onLoad = null;
  107. this.onDelete = null;
  108. this.onDeleted = null;
  109. this.onSubmit = null;
  110. this.onSave = null;
  111. this.onSelect = null;
  112. this.onUnselect = null;
  113. this.onClick = null;
  114. this.onDblClick = null;
  115. this.onContextMenu = null;
  116. this.onMenuClick = null; // when context menu item selected
  117. this.onColumnClick = null;
  118. this.onColumnResize = null;
  119. this.onSort = null;
  120. this.onSearch = null;
  121. this.onChange = null; // called when editable record is changed
  122. this.onRestore = null; // called when editable record is restored
  123. this.onExpand = null;
  124. this.onCollapse = null;
  125. this.onError = null;
  126. this.onKeydown = null;
  127. this.onToolbar = null; // all events from toolbar
  128. this.onColumnOnOff = null;
  129. this.onCopy = null;
  130. this.onPaste = null;
  131. this.onSelectionExtend = null;
  132. this.onEditField = null;
  133. this.onRender = null;
  134. this.onRefresh = null;
  135. this.onReload = null;
  136. this.onResize = null;
  137. this.onDestroy = null;
  138. this.onStateSave = null;
  139. this.onStateRestore = null;
  140. // internal
  141. this.last = {
  142. field : 'all',
  143. caption : w2utils.lang('All Fields'),
  144. logic : 'OR',
  145. search : '',
  146. searchIds : [],
  147. selection : {
  148. indexes : [],
  149. columns : {}
  150. },
  151. multi : false,
  152. scrollTop : 0,
  153. scrollLeft : 0,
  154. sortData : null,
  155. sortCount : 0,
  156. xhr : null,
  157. range_start : null,
  158. range_end : null,
  159. sel_ind : null,
  160. sel_col : null,
  161. sel_type : null,
  162. edit_col : null
  163. };
  164. $.extend(true, this, w2obj.grid, options);
  165. };
  166. // ====================================================
  167. // -- Registers as a jQuery plugin
  168. $.fn.w2grid = function(method) {
  169. if ($.isPlainObject(method)) {
  170. // check name parameter
  171. if (!w2utils.checkName(method, 'w2grid')) return;
  172. // remember items
  173. var columns = method.columns;
  174. var columnGroups = method.columnGroups;
  175. var records = method.records;
  176. var searches = method.searches;
  177. var searchData = method.searchData;
  178. var sortData = method.sortData;
  179. var postData = method.postData;
  180. var toolbar = method.toolbar;
  181. // extend items
  182. var object = new w2grid(method);
  183. $.extend(object, { postData: {}, records: [], columns: [], searches: [], toolbar: {}, sortData: [], searchData: [], handlers: [] });
  184. if (object.onExpand != null) object.show.expandColumn = true;
  185. $.extend(true, object.toolbar, toolbar);
  186. // reassign variables
  187. for (var p in columns) object.columns[p] = $.extend(true, {}, columns[p]);
  188. for (var p in columnGroups) object.columnGroups[p] = $.extend(true, {}, columnGroups[p]);
  189. for (var p in searches) object.searches[p] = $.extend(true, {}, searches[p]);
  190. for (var p in searchData) object.searchData[p] = $.extend(true, {}, searchData[p]);
  191. for (var p in sortData) object.sortData[p] = $.extend(true, {}, sortData[p]);
  192. object.postData = $.extend(true, {}, postData);
  193. // check if there are records without recid
  194. for (var r in records) {
  195. if (records[r].recid == null || typeof records[r].recid == 'undefined') {
  196. console.log('ERROR: Cannot add records without recid. (obj: '+ object.name +')');
  197. return;
  198. }
  199. object.records[r] = $.extend(true, {}, records[r]);
  200. }
  201. // add searches
  202. for (var c in object.columns) {
  203. var col = object.columns[c];
  204. if (typeof col.searchable == 'undefined' || object.getSearch(col.field) != null) continue;
  205. var stype = col.searchable;
  206. var attr = '';
  207. if (col.searchable === true) { stype = 'text'; attr = 'size="20"'; }
  208. object.addSearch({ field: col.field, caption: col.caption, type: stype, attr: attr });
  209. }
  210. // init toolbar
  211. object.initToolbar();
  212. // render if necessary
  213. if ($(this).length !== 0) {
  214. object.render($(this)[0]);
  215. }
  216. // register new object
  217. w2ui[object.name] = object;
  218. return object;
  219. } else {
  220. var obj = w2ui[$(this).attr('name')];
  221. if (!obj) return null;
  222. if (arguments.length > 0) {
  223. if (obj[method]) obj[method].apply(obj, Array.prototype.slice.call(arguments, 1));
  224. return this;
  225. } else {
  226. return obj;
  227. }
  228. }
  229. };
  230. // ====================================================
  231. // -- Implementation of core functionality
  232. w2grid.prototype = {
  233. // ----
  234. // properties that need to be in prototype
  235. msgDelete : 'Are you sure you want to delete selected records?',
  236. msgNotJSON : 'Returned data is not in valid JSON format.',
  237. msgAJAXerror : 'AJAX error. See console for more details.',
  238. msgRefresh : 'Refreshing...',
  239. // for easy button overwrite
  240. buttons: {
  241. 'reload' : { type: 'button', id: 'w2ui-reload', icon: 'w2ui-icon-reload', hint: 'Reload data in the list' },
  242. 'columns' : { type: 'drop', id: 'w2ui-column-on-off', icon: 'w2ui-icon-columns', hint: 'Show/hide columns', arrow: false, html: '' },
  243. 'search' : { type: 'html', id: 'w2ui-search',
  244. html: '<div class="w2ui-icon icon-search-down w2ui-search-down" title="'+ 'Select Search Field' +'" '+
  245. 'onclick="var obj = w2ui[$(this).parents(\'div.w2ui-grid\').attr(\'name\')]; obj.searchShowFields();"></div>'
  246. },
  247. 'search-go': { type: 'check', id: 'w2ui-search-advanced', caption: 'Search...', hint: 'Open Search Fields' },
  248. 'add' : { type: 'button', id: 'w2ui-add', caption: 'Add New', hint: 'Add new record', icon: 'w2ui-icon-plus' },
  249. 'edit' : { type: 'button', id: 'w2ui-edit', caption: 'Edit', hint: 'Edit selected record', icon: 'w2ui-icon-pencil', disabled: true },
  250. 'delete' : { type: 'button', id: 'w2ui-delete', caption: 'Delete', hint: 'Delete selected records', icon: 'w2ui-icon-cross', disabled: true },
  251. 'save' : { type: 'button', id: 'w2ui-save', caption: 'Save', hint: 'Save changed records', icon: 'w2ui-icon-check' }
  252. },
  253. add: function (record) {
  254. if (!$.isArray(record)) record = [record];
  255. var added = 0;
  256. for (var o in record) {
  257. if (!this.recid && typeof record[o].recid == 'undefined') record[o].recid = record[o][this.recid];
  258. if (record[o].recid == null || typeof record[o].recid == 'undefined') {
  259. console.log('ERROR: Cannot add record without recid. (obj: '+ this.name +')');
  260. continue;
  261. }
  262. this.records.push(record[o]);
  263. added++;
  264. }
  265. var url = (typeof this.url != 'object' ? this.url : this.url.get);
  266. if (!url) {
  267. this.total = this.records.length;
  268. this.localSort();
  269. this.localSearch();
  270. }
  271. this.refresh(); // ?? should it be reload?
  272. return added;
  273. },
  274. find: function (obj, returnIndex) {
  275. if (typeof obj == 'undefined' || obj == null) obj = {};
  276. var recs = [];
  277. var hasDots = false;
  278. // check if property is nested - needed for speed
  279. for (var o in obj) if (String(o).indexOf('.') != -1) hasDots = true;
  280. // look for an item
  281. for (var i = 0; i < this.records.length; i++) {
  282. var match = true;
  283. for (var o in obj) {
  284. var val = this.records[i][o];
  285. if (hasDots && String(o).indexOf('.') != -1) val = this.parseField(this.records[i], o);
  286. if (obj[o] != val) match = false;
  287. }
  288. if (match && returnIndex !== true) recs.push(this.records[i].recid);
  289. if (match && returnIndex === true) recs.push(i);
  290. }
  291. return recs;
  292. },
  293. set: function (recid, record, noRefresh) { // does not delete existing, but overrides on top of it
  294. if (typeof recid == 'object') {
  295. noRefresh = record;
  296. record = recid;
  297. recid = null;
  298. }
  299. // update all records
  300. if (recid == null) {
  301. for (var r in this.records) {
  302. $.extend(true, this.records[r], record); // recid is the whole record
  303. }
  304. if (noRefresh !== true) this.refresh();
  305. } else { // find record to update
  306. var ind = this.get(recid, true);
  307. if (ind == null) return false;
  308. var isSummary = (this.records[ind] && this.records[ind].recid == recid ? false : true);
  309. if (isSummary) {
  310. $.extend(true, this.summary[ind], record);
  311. } else {
  312. $.extend(true, this.records[ind], record);
  313. }
  314. if (noRefresh !== true) this.refreshRow(recid); // refresh only that record
  315. }
  316. return true;
  317. },
  318. get: function (recid, returnIndex) {
  319. // search records
  320. for (var i = 0; i < this.records.length; i++) {
  321. if (this.records[i].recid == recid) {
  322. if (returnIndex === true) return i; else return this.records[i];
  323. }
  324. }
  325. // search summary
  326. for (var i = 0; i < this.summary.length; i++) {
  327. if (this.summary[i].recid == recid) {
  328. if (returnIndex === true) return i; else return this.summary[i];
  329. }
  330. }
  331. return null;
  332. },
  333. remove: function () {
  334. var removed = 0;
  335. for (var a = 0; a < arguments.length; a++) {
  336. for (var r = this.records.length-1; r >= 0; r--) {
  337. if (this.records[r].recid == arguments[a]) { this.records.splice(r, 1); removed++; }
  338. }
  339. }
  340. var url = (typeof this.url != 'object' ? this.url : this.url.get);
  341. if (!url) {
  342. this.localSort();
  343. this.localSearch();
  344. }
  345. this.refresh();
  346. return removed;
  347. },
  348. addColumn: function (before, columns) {
  349. var added = 0;
  350. if (arguments.length == 1) {
  351. columns = before;
  352. before = this.columns.length;
  353. } else {
  354. if (typeof before == 'string') before = this.getColumn(before, true);
  355. if (before === null) before = this.columns.length;
  356. }
  357. if (!$.isArray(columns)) columns = [columns];
  358. for (var o in columns) {
  359. this.columns.splice(before, 0, columns[o]);
  360. before++;
  361. added++;
  362. }
  363. this.refresh();
  364. return added;
  365. },
  366. removeColumn: function () {
  367. var removed = 0;
  368. for (var a = 0; a < arguments.length; a++) {
  369. for (var r = this.columns.length-1; r >= 0; r--) {
  370. if (this.columns[r].field == arguments[a]) { this.columns.splice(r, 1); removed++; }
  371. }
  372. }
  373. this.refresh();
  374. return removed;
  375. },
  376. getColumn: function (field, returnIndex) {
  377. for (var i = 0; i < this.columns.length; i++) {
  378. if (this.columns[i].field == field) {
  379. if (returnIndex === true) return i; else return this.columns[i];
  380. }
  381. }
  382. return null;
  383. },
  384. toggleColumn: function () {
  385. var effected = 0;
  386. for (var a = 0; a < arguments.length; a++) {
  387. for (var r = this.columns.length-1; r >= 0; r--) {
  388. var col = this.columns[r];
  389. if (col.field == arguments[a]) {
  390. col.hidden = !col.hidden;
  391. effected++;
  392. }
  393. }
  394. }
  395. this.refresh();
  396. return effected;
  397. },
  398. showColumn: function () {
  399. var shown = 0;
  400. for (var a = 0; a < arguments.length; a++) {
  401. for (var r = this.columns.length-1; r >= 0; r--) {
  402. var col = this.columns[r];
  403. if (col.gridMinWidth) delete col.gridMinWidth;
  404. if (col.field == arguments[a] && col.hidden !== false) {
  405. col.hidden = false;
  406. shown++;
  407. }
  408. }
  409. }
  410. this.refresh();
  411. return shown;
  412. },
  413. hideColumn: function () {
  414. var hidden = 0;
  415. for (var a = 0; a < arguments.length; a++) {
  416. for (var r = this.columns.length-1; r >= 0; r--) {
  417. var col = this.columns[r];
  418. if (col.field == arguments[a] && col.hidden !== true) {
  419. col.hidden = true;
  420. hidden++;
  421. }
  422. }
  423. }
  424. this.refresh();
  425. return hidden;
  426. },
  427. addSearch: function (before, search) {
  428. var added = 0;
  429. if (arguments.length == 1) {
  430. search = before;
  431. before = this.searches.length;
  432. } else {
  433. if (typeof before == 'string') before = this.getSearch(before, true);
  434. if (before === null) before = this.searches.length;
  435. }
  436. if (!$.isArray(search)) search = [search];
  437. for (var o in search) {
  438. this.searches.splice(before, 0, search[o]);
  439. before++;
  440. added++;
  441. }
  442. this.searchClose();
  443. return added;
  444. },
  445. removeSearch: function () {
  446. var removed = 0;
  447. for (var a = 0; a < arguments.length; a++) {
  448. for (var r = this.searches.length-1; r >= 0; r--) {
  449. if (this.searches[r].field == arguments[a]) { this.searches.splice(r, 1); removed++; }
  450. }
  451. }
  452. this.searchClose();
  453. return removed;
  454. },
  455. getSearch: function (field, returnIndex) {
  456. for (var i = 0; i < this.searches.length; i++) {
  457. if (this.searches[i].field == field) {
  458. if (returnIndex === true) return i; else return this.searches[i];
  459. }
  460. }
  461. return null;
  462. },
  463. toggleSearch: function () {
  464. var effected = 0;
  465. for (var a = 0; a < arguments.length; a++) {
  466. for (var r = this.searches.length-1; r >= 0; r--) {
  467. if (this.searches[r].field == arguments[a]) {
  468. this.searches[r].hidden = !this.searches[r].hidden;
  469. effected++;
  470. }
  471. }
  472. }
  473. this.searchClose();
  474. return effected;
  475. },
  476. showSearch: function () {
  477. var shown = 0;
  478. for (var a = 0; a < arguments.length; a++) {
  479. for (var r = this.searches.length-1; r >= 0; r--) {
  480. if (this.searches[r].field == arguments[a] && this.searches[r].hidden !== false) {
  481. this.searches[r].hidden = false;
  482. shown++;
  483. }
  484. }
  485. }
  486. this.searchClose();
  487. return shown;
  488. },
  489. hideSearch: function () {
  490. var hidden = 0;
  491. for (var a = 0; a < arguments.length; a++) {
  492. for (var r = this.searches.length-1; r >= 0; r--) {
  493. if (this.searches[r].field == arguments[a] && this.searches[r].hidden !== true) {
  494. this.searches[r].hidden = true;
  495. hidden++;
  496. }
  497. }
  498. }
  499. this.searchClose();
  500. return hidden;
  501. },
  502. getSearchData: function (field) {
  503. for (var s in this.searchData) {
  504. if (this.searchData[s].field == field) return this.searchData[s];
  505. }
  506. return null;
  507. },
  508. localSort: function (silent) {
  509. var url = (typeof this.url != 'object' ? this.url : this.url.get);
  510. if (url) {
  511. console.log('ERROR: grid.localSort can only be used on local data source, grid.url should be empty.');
  512. return;
  513. }
  514. if ($.isEmptyObject(this.sortData)) return;
  515. var time = (new Date()).getTime();
  516. var obj = this;
  517. // process date fields
  518. obj.prepareData();
  519. // process sortData
  520. for (var s in this.sortData) {
  521. var column = this.getColumn(this.sortData[s].field);
  522. if (!column) return;
  523. if (typeof column.render == 'string') {
  524. if (['date', 'age'].indexOf(column.render.split(':')[0]) != -1) {
  525. this.sortData[s]['field_'] = column.field + '_';
  526. }
  527. if (['time'].indexOf(column.render.split(':')[0]) != -1) {
  528. this.sortData[s]['field_'] = column.field + '_';
  529. }
  530. }
  531. }
  532. // process sort
  533. this.records.sort(function (a, b) {
  534. var ret = 0;
  535. for (var s in obj.sortData) {
  536. var fld = obj.sortData[s].field;
  537. if (obj.sortData[s].field_) fld = obj.sortData[s].field_;
  538. var aa = a[fld];
  539. var bb = b[fld];
  540. if (String(fld).indexOf('.') != -1) {
  541. aa = obj.parseField(a, fld);
  542. bb = obj.parseField(b, fld);
  543. }
  544. if (typeof aa == 'string') aa = $.trim(aa.toLowerCase());
  545. if (typeof bb == 'string') bb = $.trim(bb.toLowerCase());
  546. if (aa > bb) ret = (obj.sortData[s].direction == 'asc' ? 1 : -1);
  547. if (aa < bb) ret = (obj.sortData[s].direction == 'asc' ? -1 : 1);
  548. if (typeof aa != 'object' && typeof bb == 'object') ret = -1;
  549. if (typeof bb != 'object' && typeof aa == 'object') ret = 1;
  550. if (aa == null && bb != null) ret = 1; // all nuls and undefined on bottom
  551. if (aa != null && bb == null) ret = -1;
  552. if (ret != 0) break;
  553. }
  554. return ret;
  555. });
  556. time = (new Date()).getTime() - time;
  557. if (silent !== true && obj.show.statusSort) {
  558. setTimeout(function () {
  559. obj.status(w2utils.lang('Sorting took') + ' ' + time/1000 + ' ' + w2utils.lang('sec'));
  560. }, 10);
  561. }
  562. return time;
  563. },
  564. localSearch: function (silent) {
  565. var url = (typeof this.url != 'object' ? this.url : this.url.get);
  566. if (url) {
  567. console.log('ERROR: grid.localSearch can only be used on local data source, grid.url should be empty.');
  568. return;
  569. }
  570. var time = (new Date()).getTime();
  571. var obj = this;
  572. this.total = this.records.length;
  573. // mark all records as shown
  574. this.last.searchIds = [];
  575. // prepare date/time fields
  576. this.prepareData();
  577. // hide records that did not match
  578. if (this.searchData.length > 0 && !url) {
  579. this.total = 0;
  580. for (var r in this.records) {
  581. var rec = this.records[r];
  582. var fl = 0;
  583. for (var s in this.searchData) {
  584. var sdata = this.searchData[s];
  585. var search = this.getSearch(sdata.field);
  586. if (sdata == null) continue;
  587. if (search == null) search = { field: sdata.field, type: sdata.type };
  588. var val1 = String(obj.parseField(rec, search.field)).toLowerCase();
  589. if (typeof sdata.value != 'undefined') {
  590. if (!$.isArray(sdata.value)) {
  591. var val2 = String(sdata.value).toLowerCase();
  592. } else {
  593. var val2 = sdata.value[0];
  594. var val3 = sdata.value[1];
  595. }
  596. }
  597. switch (sdata.operator) {
  598. case 'is':
  599. if (rec[search.field] == sdata.value) fl++; // do not hide record
  600. if (search.type == 'date') {
  601. var val1 = w2utils.formatDate(rec[search.field + '_'], 'yyyy-mm-dd');
  602. var val2 = w2utils.formatDate(val2, 'yyyy-mm-dd');
  603. if (val1 == val2) fl++;
  604. }
  605. if (search.type == 'time') {
  606. var val1 = w2utils.formatTime(rec[search.field + '_'], 'h24:mi');
  607. var val2 = w2utils.formatTime(val2, 'h24:mi');
  608. if (val1 == val2) fl++;
  609. }
  610. break;
  611. case 'between':
  612. if (['int', 'float', 'money', 'currency', 'percent'].indexOf(search.type) != -1) {
  613. if (parseFloat(rec[search.field]) >= parseFloat(val2) && parseFloat(rec[search.field]) <= parseFloat(val3)) fl++;
  614. }
  615. if (search.type == 'date') {
  616. var val1 = rec[search.field + '_'];
  617. var val2 = w2utils.isDate(val2, w2utils.settings.date_format, true);
  618. var val3 = w2utils.isDate(val3, w2utils.settings.date_format, true);
  619. if (val3 != null) val3 = new Date(val3.getTime() + 86400000); // 1 day
  620. if (val1 >= val2 && val1 < val3) fl++;
  621. }
  622. if (search.type == 'time') {
  623. var val1 = rec[search.field + '_'];
  624. var val2 = w2utils.isTime(val2, true);
  625. var val3 = w2utils.isTime(val3, true);
  626. val2 = (new Date()).setHours(val2.hours, val2.minutes, val2.seconds ? val2.seconds : 0, 0);
  627. val3 = (new Date()).setHours(val3.hours, val3.minutes, val3.seconds ? val3.seconds : 0, 0);
  628. if (val1 >= val2 && val1 < val3) fl++;
  629. }
  630. break;
  631. case 'in':
  632. var tmp = sdata.value;
  633. if (sdata.svalue) tmp = sdata.svalue;
  634. if (tmp.indexOf(val1) !== -1) fl++;
  635. break;
  636. case 'not in':
  637. var tmp = sdata.value;
  638. if (sdata.svalue) tmp = sdata.svalue;
  639. if (tmp.indexOf(val1) == -1) fl++;
  640. break;
  641. case 'begins':
  642. case 'begins with': // need for back compatib.
  643. if (val1.indexOf(val2) == 0) fl++; // do not hide record
  644. break;
  645. case 'contains':
  646. if (val1.indexOf(val2) >= 0) fl++; // do not hide record
  647. break;
  648. case 'ends':
  649. case 'ends with': // need for back compatib.
  650. if (val1.indexOf(val2) == val1.length - val2.length) fl++; // do not hide record
  651. break;
  652. }
  653. }
  654. if ((this.last.logic == 'OR' && fl != 0) || (this.last.logic == 'AND' && fl == this.searchData.length)) this.last.searchIds.push(parseInt(r));
  655. }
  656. this.total = this.last.searchIds.length;
  657. }
  658. time = (new Date()).getTime() - time;
  659. if (silent !== true && obj.show.statusSearch) {
  660. setTimeout(function () {
  661. obj.status(w2utils.lang('Search took') + ' ' + time/1000 + ' ' + w2utils.lang('sec'));
  662. }, 10);
  663. }
  664. return time;
  665. },
  666. getRangeData: function (range, extra) {
  667. var rec1 = this.get(range[0].recid, true);
  668. var rec2 = this.get(range[1].recid, true);
  669. var col1 = range[0].column;
  670. var col2 = range[1].column;
  671. var res = [];
  672. if (col1 == col2) { // one row
  673. for (var r = rec1; r <= rec2; r++) {
  674. var record = this.records[r];
  675. var dt = record[this.columns[col1].field] || null;
  676. if (extra !== true) {
  677. res.push(dt);
  678. } else {
  679. res.push({ data: dt, column: col1, index: r, record: record });
  680. }
  681. }
  682. } else if (rec1 == rec2) { // one line
  683. var record = this.records[rec1];
  684. for (var i = col1; i <= col2; i++) {
  685. var dt = record[this.columns[i].field] || null;
  686. if (extra !== true) {
  687. res.push(dt);
  688. } else {
  689. res.push({ data: dt, column: i, index: rec1, record: record });
  690. }
  691. }
  692. } else {
  693. for (var r = rec1; r <= rec2; r++) {
  694. var record = this.records[r];
  695. res.push([]);
  696. for (var i = col1; i <= col2; i++) {
  697. var dt = record[this.columns[i].field];
  698. if (extra !== true) {
  699. res[res.length-1].push(dt);
  700. } else {
  701. res[res.length-1].push({ data: dt, column: i, index: r, record: record });
  702. }
  703. }
  704. }
  705. }
  706. return res;
  707. },
  708. addRange: function (ranges) {
  709. var added = 0;
  710. if (this.selectType == 'row') return added;
  711. if (!$.isArray(ranges)) ranges = [ranges];
  712. // if it is selection
  713. for (var r in ranges) {
  714. if (typeof ranges[r] != 'object') ranges[r] = { name: 'selection' };
  715. if (ranges[r].name == 'selection') {
  716. if (this.show.selectionBorder === false) continue;
  717. var sel = this.getSelection();
  718. if (sel.length == 0) {
  719. this.removeRange(ranges[r].name);
  720. continue;
  721. } else {
  722. var first = sel[0];
  723. var last = sel[sel.length-1];
  724. var td1 = $('#grid_'+ this.name +'_rec_'+ first.recid + ' td[col='+ first.column +']');
  725. var td2 = $('#grid_'+ this.name +'_rec_'+ last.recid + ' td[col='+ last.column +']');
  726. }
  727. } else { // other range
  728. var first = ranges[r].range[0];
  729. var last = ranges[r].range[1];
  730. var td1 = $('#grid_'+ this.name +'_rec_'+ first.recid + ' td[col='+ first.column +']');
  731. var td2 = $('#grid_'+ this.name +'_rec_'+ last.recid + ' td[col='+ last.column +']');
  732. }
  733. if (first) {
  734. var rg = {
  735. name: ranges[r].name,
  736. range: [{ recid: first.recid, column: first.column }, { recid: last.recid, column: last.column }],
  737. style: ranges[r].style || ''
  738. };
  739. // add range
  740. var ind = false;
  741. for (var t in this.ranges) if (this.ranges[t].name == ranges[r].name) { ind = r; break; }
  742. if (ind !== false) {
  743. this.ranges[ind] = rg;
  744. } else {
  745. this.ranges.push(rg);
  746. }
  747. added++
  748. }
  749. }
  750. this.refreshRanges();
  751. return added;
  752. },
  753. removeRange: function () {
  754. var removed = 0;
  755. for (var a = 0; a < arguments.length; a++) {
  756. var name = arguments[a];
  757. $('#grid_'+ this.name +'_'+ name).remove();
  758. for (var r = this.ranges.length-1; r >= 0; r--) {
  759. if (this.ranges[r].name == name) {
  760. this.ranges.splice(r, 1);
  761. removed++;
  762. }
  763. }
  764. }
  765. return removed;
  766. },
  767. refreshRanges: function () {
  768. var obj = this;
  769. var time = (new Date()).getTime();
  770. var rec = $('#grid_'+ this.name +'_records');
  771. for (var r in this.ranges) {
  772. var rg = this.ranges[r];
  773. var first = rg.range[0];
  774. var last = rg.range[1];
  775. var td1 = $('#grid_'+ this.name +'_rec_'+ first.recid + ' td[col='+ first.column +']');
  776. var td2 = $('#grid_'+ this.name +'_rec_'+ last.recid + ' td[col='+ last.column +']');
  777. var sel1 = $('#grid_'+ this.name +'_rec_top').next().find('td.w2ui-selected');
  778. var sel2 = $('#grid_'+ this.name +'_rec_bottom').prev().find('td.w2ui-selected');
  779. if ($('#grid_'+ this.name +'_'+ rg.name).length == 0) {
  780. rec.append('<div id="grid_'+ this.name +'_' + rg.name +'" class="w2ui-selection" style="'+ rg.style +'">'+
  781. (rg.name == 'selection' ? '<div id="grid_'+ this.name +'_resizer" class="w2ui-selection-resizer"></div>' : '')+
  782. '</div>');
  783. } else {
  784. $('#grid_'+ this.name +'_'+ rg.name).attr('style', rg.style);
  785. }
  786. // if virtual scrolling kicked in
  787. if ((td1.length == 0 && td2.length != 0) || sel1.length > 0) {
  788. td1 = $('#grid_'+ this.name +'_rec_top').next().find('td[col='+ first.column +']');
  789. }
  790. if ((td2.length == 0 && td1.length != 0) || sel2.length > 0) {
  791. td2 = $('#grid_'+ this.name +'_rec_bottom').prev().find('td[col='+ last.column +']');
  792. }
  793. // display range
  794. if (td1.length > 0 && td2.length > 0) {
  795. $('#grid_'+ this.name +'_'+ rg.name).css({
  796. left : (td1.position().left - 1 + rec.scrollLeft()) + 'px',
  797. top : (td1.position().top - 1 + rec.scrollTop()) + 'px',
  798. width : (td2.position().left - td1.position().left + td2.width() + 3) + 'px',
  799. height : (td2.position().top - td1.position().top + td2.height() + 3) + 'px'
  800. });
  801. }
  802. }
  803. // add resizer events
  804. $(this.box).find('#grid_'+ this.name +'_resizer').off('mousedown').on('mousedown', mouseStart);
  805. //$(this.box).find('#grid_'+ this.name +'_resizer').off('selectstart').on('selectstart', function () { return false; }); // fixes chrome cursror bug
  806. var eventData = { phase: 'before', type: 'selectionExtend', target: obj.name, originalRange: null, newRange: null };
  807. function mouseStart (event) {
  808. var sel = obj.getSelection();
  809. obj.last.move = {
  810. type : 'expand',
  811. x : event.screenX,
  812. y : event.screenY,
  813. divX : 0,
  814. divY : 0,
  815. recid : sel[0].recid,
  816. column : sel[0].column,
  817. originalRange : [{ recid: sel[0].recid, column: sel[0].column }, { recid: sel[sel.length-1].recid, column: sel[sel.length-1].column }],
  818. newRange : [{ recid: sel[0].recid, column: sel[0].column }, { recid: sel[sel.length-1].recid, column: sel[sel.length-1].column }]
  819. };
  820. $(document).off('mousemove', mouseMove).on('mousemove', mouseMove);
  821. $(document).off('mouseup', mouseStop).on('mouseup', mouseStop);
  822. }
  823. function mouseMove (event) {
  824. var mv = obj.last.move;
  825. if (!mv || mv.type != 'expand') return;
  826. mv.divX = (event.screenX - mv.x);
  827. mv.divY = (event.screenY - mv.y);
  828. // find new cell
  829. var recid, column;
  830. var tmp = event.originalEvent.target;
  831. if (tmp.tagName != 'TD') tmp = $(tmp).parents('td')[0];
  832. if (typeof $(tmp).attr('col') != 'undefined') column = parseInt($(tmp).attr('col'));
  833. tmp = $(tmp).parents('tr')[0];
  834. recid = $(tmp).attr('recid');
  835. // new range
  836. if (mv.newRange[1].recid == recid && mv.newRange[1].column == column) return;
  837. var prevNewRange = $.extend({}, mv.newRange);
  838. mv.newRange = [{ recid: mv.recid, column: mv.column }, { recid: recid, column: column }];
  839. // event before
  840. eventData = obj.trigger($.extend(eventData, { originalRange: mv.originalRange, newRange : mv.newRange }));
  841. if (eventData.isCancelled === true) {
  842. mv.newRange = prevNewRange;
  843. eventData.newRange = prevNewRange;
  844. return;
  845. } else {
  846. // default behavior
  847. obj.removeRange('grid-selection-expand');
  848. obj.addRange({
  849. name : 'grid-selection-expand',
  850. range : eventData.newRange,
  851. style : 'background-color: rgba(100,100,100,0.1); border: 2px dotted rgba(100,100,100,0.5);'
  852. });
  853. }
  854. }
  855. function mouseStop (event) {
  856. // default behavior
  857. obj.removeRange('grid-selection-expand');
  858. delete obj.last.move;
  859. $(document).off('mousemove', mouseMove);
  860. $(document).off('mouseup', mouseStop);
  861. // event after
  862. obj.trigger($.extend(eventData, { phase: 'after' }));
  863. }
  864. return (new Date()).getTime() - time;
  865. },
  866. select: function () {
  867. var selected = 0;
  868. var sel = this.last.selection;
  869. if (!this.multiSelect) this.selectNone();
  870. for (var a = 0; a < arguments.length; a++) {
  871. var recid = typeof arguments[a] == 'object' ? arguments[a].recid : arguments[a];
  872. var record = this.get(recid);
  873. if (record == null) continue;
  874. var index = this.get(recid, true);
  875. var recEl = $('#grid_'+ this.name +'_rec_'+ w2utils.escapeId(recid));
  876. if (this.selectType == 'row') {
  877. if (sel.indexes.indexOf(index) >= 0) continue;
  878. // event before
  879. var eventData = this.trigger({ phase: 'before', type: 'select', target: this.name, recid: recid, index: index });
  880. if (eventData.isCancelled === true) continue;
  881. // default action
  882. sel.indexes.push(index);
  883. sel.indexes.sort(function(a, b) { return a-b });
  884. recEl.addClass('w2ui-selected').data('selected', 'yes').find('.w2ui-col-number').addClass('w2ui-row-selected');
  885. recEl.find('.w2ui-grid-select-check').prop("checked", true);
  886. selected++;
  887. } else {
  888. var col = arguments[a].column;
  889. if (!w2utils.isInt(col)) { // select all columns
  890. var cols = [];
  891. for (var c in this.columns) { if (this.columns[c].hidden) continue; cols.push({ recid: recid, column: parseInt(c) }); }
  892. if (!this.multiSelect) cols = cols.splice(0, 1);
  893. return this.select.apply(this, cols);
  894. }
  895. var s = sel.columns[index] || [];
  896. if ($.isArray(s) && s.indexOf(col) != -1) continue;
  897. // event before
  898. var eventData = this.trigger({ phase: 'before', type: 'select', target: this.name, recid: recid, index: index, column: col });
  899. if (eventData.isCancelled === true) continue;
  900. // default action
  901. if (sel.indexes.indexOf(index) == -1) {
  902. sel.indexes.push(index);
  903. sel.indexes.sort(function(a, b) { return a-b });
  904. }
  905. s.push(col);
  906. s.sort(function(a, b) { return a-b }); // sort function must be for numerical sort
  907. recEl.find(' > td[col='+ col +']').addClass('w2ui-selected');
  908. recEl.find('.w2ui-col-number').addClass('w2ui-row-selected');
  909. $(this.box).find('.w2ui-grid-columns td[col='+ col +'] .w2ui-col-header').addClass('w2ui-col-selected');
  910. selected++;
  911. recEl.data('selected', 'yes');
  912. recEl.find('.w2ui-grid-select-check').prop("checked", true);
  913. // save back to selection object
  914. sel.columns[index] = s;
  915. }
  916. // event after
  917. this.trigger($.extend(eventData, { phase: 'after' }));
  918. }
  919. // all selected?
  920. if (sel.indexes.length == this.records.length || (this.searchData.length !== 0 && sel.indexes.length == this.last.searchIds.length)) {
  921. $('#grid_'+ this.name +'_check_all').prop('checked', true);
  922. } else {
  923. $('#grid_'+ this.name +'_check_all').prop('checked', false);
  924. }
  925. this.status();
  926. this.addRange('selection');
  927. return selected;
  928. },
  929. unselect: function () {
  930. var unselected = 0;
  931. var sel = this.last.selection;
  932. for (var a = 0; a < arguments.length; a++) {
  933. var recid = typeof arguments[a] == 'object' ? arguments[a].recid : arguments[a];
  934. var record = this.get(recid);
  935. if (record == null) continue;
  936. var index = this.get(record.recid, true);
  937. var recEl = $('#grid_'+ this.name +'_rec_'+ w2utils.escapeId(recid));
  938. if (this.selectType == 'row') {
  939. if (sel.indexes.indexOf(index) == -1) continue;
  940. // event before
  941. var eventData = this.trigger({ phase: 'before', type: 'unselect', target: this.name, recid: recid, index: index });
  942. if (eventData.isCancelled === true) continue;
  943. // default action
  944. sel.indexes.splice(sel.indexes.indexOf(index), 1);
  945. recEl.removeClass('w2ui-selected').removeData('selected').find('.w2ui-col-number').removeClass('w2ui-row-selected');
  946. if (recEl.length != 0) recEl[0].style.cssText = 'height: '+ this.recordHeight +'px; ' + recEl.attr('custom_style');
  947. recEl.find('.w2ui-grid-select-check').prop("checked", false);
  948. unselected++;
  949. } else {
  950. var col = arguments[a].column;
  951. if (!w2utils.isInt(col)) { // unselect all columns
  952. var cols = [];
  953. for (var c in this.columns) { if (this.columns[c].hidden) continue; cols.push({ recid: recid, column: parseInt(c) }); }
  954. return this.unselect.apply(this, cols);
  955. }
  956. var s = sel.columns[index];
  957. if (!$.isArray(s) || s.indexOf(col) == -1) continue;
  958. // event before
  959. var eventData = this.trigger({ phase: 'before', type: 'unselect', target: this.name, recid: recid, column: col });
  960. if (eventData.isCancelled === true) continue;
  961. // default action
  962. s.splice(s.indexOf(col), 1);
  963. $('#grid_'+ this.name +'_rec_'+ w2utils.escapeId(recid)).find(' > td[col='+ col +']').removeClass('w2ui-selected');
  964. // check if any row/column still selected
  965. var isColSelected = false;
  966. var isRowSelected = false;
  967. var tmp = this.getSelection();
  968. for (var t in tmp) {
  969. if (tmp[t].column == col) isColSelected = true;
  970. if (tmp[t].recid == recid) isRowSelected = true;
  971. }
  972. if (!isColSelected) {
  973. $(this.box).find('.w2ui-grid-columns td[col='+ col +'] .w2ui-col-header').removeClass('w2ui-col-selected');
  974. }
  975. if (!isRowSelected) {
  976. $('#grid_'+ this.name +'_rec_'+ w2utils.escapeId(recid)).find('.w2ui-col-number').removeClass('w2ui-row-selected');
  977. }
  978. unselected++;
  979. if (s.length == 0) {
  980. delete sel.columns[index];
  981. sel.indexes.splice(sel.indexes.indexOf(index), 1);
  982. recEl.removeData('selected');
  983. recEl.find('.w2ui-grid-select-check').prop("checked", false);
  984. }
  985. }
  986. // event after
  987. this.trigger($.extend(eventData, { phase: 'after' }));
  988. }
  989. // all selected?
  990. if (sel.indexes.length == this.records.length || (this.searchData.length !== 0 && sel.indexes.length == this.last.searchIds.length)) {
  991. $('#grid_'+ this.name +'_check_all').prop('checked', true);
  992. } else {
  993. $('#grid_'+ this.name +'_check_all').prop('checked', false);
  994. }
  995. // show number of selected
  996. this.status();
  997. this.addRange('selection');
  998. return unselected;
  999. },
  1000. selectAll: function () {
  1001. var time = (new Date()).getTime();
  1002. if (this.multiSelect === false) return;
  1003. // event before
  1004. var eventData = this.trigger({ phase: 'before', type: 'select', target: this.name, all: true });
  1005. if (eventData.isCancelled === true) return;
  1006. // default action
  1007. var url = (typeof this.url != 'object' ? this.url : this.url.get);
  1008. var sel = this.last.selection;
  1009. var cols = [];
  1010. for (var c in this.columns) cols.push(parseInt(c));
  1011. // if local data source and searched
  1012. sel.indexes = [];
  1013. if (!url && this.searchData.length !== 0) {
  1014. // local search applied
  1015. for (var i = 0; i < this.last.searchIds.length; i++) {