UNPKG

19.2 kBJavaScriptView Raw
1/***
2 * Contains core SlickGrid classes.
3 * @module Core
4 * @namespace Slick
5 */
6
7(function ($) {
8 // register namespace
9 $.extend(true, window, {
10 "Slick": {
11 "Event": Event,
12 "EventData": EventData,
13 "EventHandler": EventHandler,
14 "Range": Range,
15 "NonDataRow": NonDataItem,
16 "Group": Group,
17 "GroupTotals": GroupTotals,
18 "EditorLock": EditorLock,
19
20 /***
21 * A global singleton editor lock.
22 * @class GlobalEditorLock
23 * @static
24 * @constructor
25 */
26 "GlobalEditorLock": new EditorLock(),
27 "TreeColumns": TreeColumns,
28
29 "keyCode": {
30 SPACE: 8,
31 BACKSPACE: 8,
32 DELETE: 46,
33 DOWN: 40,
34 END: 35,
35 ENTER: 13,
36 ESCAPE: 27,
37 HOME: 36,
38 INSERT: 45,
39 LEFT: 37,
40 PAGE_DOWN: 34,
41 PAGE_UP: 33,
42 RIGHT: 39,
43 TAB: 9,
44 UP: 38,
45 A: 65
46 },
47 "preClickClassName" : "slick-edit-preclick",
48
49 "GridAutosizeColsMode": {
50 None: 'NOA',
51 LegacyOff: 'LOF',
52 LegacyForceFit: 'LFF',
53 IgnoreViewport: 'IGV',
54 FitColsToViewport: 'FCV',
55 FitViewportToCols: 'FVC'
56 },
57
58 "ColAutosizeMode": {
59 Locked: 'LCK',
60 Guide: 'GUI',
61 Content: 'CON',
62 ContentIntelligent: 'CTI'
63 },
64
65 "RowSelectionMode": {
66 FirstRow: 'FS1',
67 FirstNRows: 'FSN',
68 AllRows: 'ALL',
69 LastRow: 'LS1'
70 },
71
72 "ValueFilterMode": {
73 None: 'NONE',
74 DeDuplicate: 'DEDP',
75 GetGreatestAndSub: 'GR8T',
76 GetLongestTextAndSub: 'LNSB',
77 GetLongestText: 'LNSC'
78 },
79
80 "WidthEvalMode": {
81 CanvasTextSize: 'CANV',
82 HTML: 'HTML'
83 }
84 }
85 });
86
87 /***
88 * An event object for passing data to event handlers and letting them control propagation.
89 * <p>This is pretty much identical to how W3C and jQuery implement events.</p>
90 * @class EventData
91 * @constructor
92 */
93 function EventData() {
94 var isPropagationStopped = false;
95 var isImmediatePropagationStopped = false;
96
97 /***
98 * Stops event from propagating up the DOM tree.
99 * @method stopPropagation
100 */
101 this.stopPropagation = function () {
102 isPropagationStopped = true;
103 };
104
105 /***
106 * Returns whether stopPropagation was called on this event object.
107 * @method isPropagationStopped
108 * @return {Boolean}
109 */
110 this.isPropagationStopped = function () {
111 return isPropagationStopped;
112 };
113
114 /***
115 * Prevents the rest of the handlers from being executed.
116 * @method stopImmediatePropagation
117 */
118 this.stopImmediatePropagation = function () {
119 isImmediatePropagationStopped = true;
120 };
121
122 /***
123 * Returns whether stopImmediatePropagation was called on this event object.\
124 * @method isImmediatePropagationStopped
125 * @return {Boolean}
126 */
127 this.isImmediatePropagationStopped = function () {
128 return isImmediatePropagationStopped;
129 };
130 }
131
132 /***
133 * A simple publisher-subscriber implementation.
134 * @class Event
135 * @constructor
136 */
137 function Event() {
138 var handlers = [];
139
140 /***
141 * Adds an event handler to be called when the event is fired.
142 * <p>Event handler will receive two arguments - an <code>EventData</code> and the <code>data</code>
143 * object the event was fired with.<p>
144 * @method subscribe
145 * @param fn {Function} Event handler.
146 */
147 this.subscribe = function (fn) {
148 handlers.push(fn);
149 };
150
151 /***
152 * Removes an event handler added with <code>subscribe(fn)</code>.
153 * @method unsubscribe
154 * @param fn {Function} Event handler to be removed.
155 */
156 this.unsubscribe = function (fn) {
157 for (var i = handlers.length - 1; i >= 0; i--) {
158 if (handlers[i] === fn) {
159 handlers.splice(i, 1);
160 }
161 }
162 };
163
164 /***
165 * Fires an event notifying all subscribers.
166 * @method notify
167 * @param args {Object} Additional data object to be passed to all handlers.
168 * @param e {EventData}
169 * Optional.
170 * An <code>EventData</code> object to be passed to all handlers.
171 * For DOM events, an existing W3C/jQuery event object can be passed in.
172 * @param scope {Object}
173 * Optional.
174 * The scope ("this") within which the handler will be executed.
175 * If not specified, the scope will be set to the <code>Event</code> instance.
176 */
177 this.notify = function (args, e, scope) {
178 e = e || new EventData();
179 scope = scope || this;
180
181 var returnValue;
182 for (var i = 0; i < handlers.length && !(e.isPropagationStopped() || e.isImmediatePropagationStopped()); i++) {
183 returnValue = handlers[i].call(scope, e, args);
184 }
185
186 return returnValue;
187 };
188 }
189
190 function EventHandler() {
191 var handlers = [];
192
193 this.subscribe = function (event, handler) {
194 handlers.push({
195 event: event,
196 handler: handler
197 });
198 event.subscribe(handler);
199
200 return this; // allow chaining
201 };
202
203 this.unsubscribe = function (event, handler) {
204 var i = handlers.length;
205 while (i--) {
206 if (handlers[i].event === event &&
207 handlers[i].handler === handler) {
208 handlers.splice(i, 1);
209 event.unsubscribe(handler);
210 return;
211 }
212 }
213
214 return this; // allow chaining
215 };
216
217 this.unsubscribeAll = function () {
218 var i = handlers.length;
219 while (i--) {
220 handlers[i].event.unsubscribe(handlers[i].handler);
221 }
222 handlers = [];
223
224 return this; // allow chaining
225 };
226 }
227
228 /***
229 * A structure containing a range of cells.
230 * @class Range
231 * @constructor
232 * @param fromRow {Integer} Starting row.
233 * @param fromCell {Integer} Starting cell.
234 * @param toRow {Integer} Optional. Ending row. Defaults to <code>fromRow</code>.
235 * @param toCell {Integer} Optional. Ending cell. Defaults to <code>fromCell</code>.
236 */
237 function Range(fromRow, fromCell, toRow, toCell) {
238 if (toRow === undefined && toCell === undefined) {
239 toRow = fromRow;
240 toCell = fromCell;
241 }
242
243 /***
244 * @property fromRow
245 * @type {Integer}
246 */
247 this.fromRow = Math.min(fromRow, toRow);
248
249 /***
250 * @property fromCell
251 * @type {Integer}
252 */
253 this.fromCell = Math.min(fromCell, toCell);
254
255 /***
256 * @property toRow
257 * @type {Integer}
258 */
259 this.toRow = Math.max(fromRow, toRow);
260
261 /***
262 * @property toCell
263 * @type {Integer}
264 */
265 this.toCell = Math.max(fromCell, toCell);
266
267 /***
268 * Returns whether a range represents a single row.
269 * @method isSingleRow
270 * @return {Boolean}
271 */
272 this.isSingleRow = function () {
273 return this.fromRow == this.toRow;
274 };
275
276 /***
277 * Returns whether a range represents a single cell.
278 * @method isSingleCell
279 * @return {Boolean}
280 */
281 this.isSingleCell = function () {
282 return this.fromRow == this.toRow && this.fromCell == this.toCell;
283 };
284
285 /***
286 * Returns whether a range contains a given cell.
287 * @method contains
288 * @param row {Integer}
289 * @param cell {Integer}
290 * @return {Boolean}
291 */
292 this.contains = function (row, cell) {
293 return row >= this.fromRow && row <= this.toRow &&
294 cell >= this.fromCell && cell <= this.toCell;
295 };
296
297 /***
298 * Returns a readable representation of a range.
299 * @method toString
300 * @return {String}
301 */
302 this.toString = function () {
303 if (this.isSingleCell()) {
304 return "(" + this.fromRow + ":" + this.fromCell + ")";
305 }
306 else {
307 return "(" + this.fromRow + ":" + this.fromCell + " - " + this.toRow + ":" + this.toCell + ")";
308 }
309 };
310 }
311
312
313 /***
314 * A base class that all special / non-data rows (like Group and GroupTotals) derive from.
315 * @class NonDataItem
316 * @constructor
317 */
318 function NonDataItem() {
319 this.__nonDataRow = true;
320 }
321
322
323 /***
324 * Information about a group of rows.
325 * @class Group
326 * @extends Slick.NonDataItem
327 * @constructor
328 */
329 function Group() {
330 this.__group = true;
331
332 /**
333 * Grouping level, starting with 0.
334 * @property level
335 * @type {Number}
336 */
337 this.level = 0;
338
339 /***
340 * Number of rows in the group.
341 * @property count
342 * @type {Integer}
343 */
344 this.count = 0;
345
346 /***
347 * Grouping value.
348 * @property value
349 * @type {Object}
350 */
351 this.value = null;
352
353 /***
354 * Formatted display value of the group.
355 * @property title
356 * @type {String}
357 */
358 this.title = null;
359
360 /***
361 * Whether a group is collapsed.
362 * @property collapsed
363 * @type {Boolean}
364 */
365 this.collapsed = false;
366
367 /***
368 * Whether a group selection checkbox is checked.
369 * @property selectChecked
370 * @type {Boolean}
371 */
372 this.selectChecked = false;
373
374 /***
375 * GroupTotals, if any.
376 * @property totals
377 * @type {GroupTotals}
378 */
379 this.totals = null;
380
381 /**
382 * Rows that are part of the group.
383 * @property rows
384 * @type {Array}
385 */
386 this.rows = [];
387
388 /**
389 * Sub-groups that are part of the group.
390 * @property groups
391 * @type {Array}
392 */
393 this.groups = null;
394
395 /**
396 * A unique key used to identify the group. This key can be used in calls to DataView
397 * collapseGroup() or expandGroup().
398 * @property groupingKey
399 * @type {Object}
400 */
401 this.groupingKey = null;
402 }
403
404 Group.prototype = new NonDataItem();
405
406 /***
407 * Compares two Group instances.
408 * @method equals
409 * @return {Boolean}
410 * @param group {Group} Group instance to compare to.
411 */
412 Group.prototype.equals = function (group) {
413 return this.value === group.value &&
414 this.count === group.count &&
415 this.collapsed === group.collapsed &&
416 this.title === group.title;
417 };
418
419 /***
420 * Information about group totals.
421 * An instance of GroupTotals will be created for each totals row and passed to the aggregators
422 * so that they can store arbitrary data in it. That data can later be accessed by group totals
423 * formatters during the display.
424 * @class GroupTotals
425 * @extends Slick.NonDataItem
426 * @constructor
427 */
428 function GroupTotals() {
429 this.__groupTotals = true;
430
431 /***
432 * Parent Group.
433 * @param group
434 * @type {Group}
435 */
436 this.group = null;
437
438 /***
439 * Whether the totals have been fully initialized / calculated.
440 * Will be set to false for lazy-calculated group totals.
441 * @param initialized
442 * @type {Boolean}
443 */
444 this.initialized = false;
445 }
446
447 GroupTotals.prototype = new NonDataItem();
448
449 /***
450 * A locking helper to track the active edit controller and ensure that only a single controller
451 * can be active at a time. This prevents a whole class of state and validation synchronization
452 * issues. An edit controller (such as SlickGrid) can query if an active edit is in progress
453 * and attempt a commit or cancel before proceeding.
454 * @class EditorLock
455 * @constructor
456 */
457 function EditorLock() {
458 var activeEditController = null;
459
460 /***
461 * Returns true if a specified edit controller is active (has the edit lock).
462 * If the parameter is not specified, returns true if any edit controller is active.
463 * @method isActive
464 * @param editController {EditController}
465 * @return {Boolean}
466 */
467 this.isActive = function (editController) {
468 return (editController ? activeEditController === editController : activeEditController !== null);
469 };
470
471 /***
472 * Sets the specified edit controller as the active edit controller (acquire edit lock).
473 * If another edit controller is already active, and exception will be throw new Error(.
474 * @method activate
475 * @param editController {EditController} edit controller acquiring the lock
476 */
477 this.activate = function (editController) {
478 if (editController === activeEditController) { // already activated?
479 return;
480 }
481 if (activeEditController !== null) {
482 throw new Error("SlickGrid.EditorLock.activate: an editController is still active, can't activate another editController");
483 }
484 if (!editController.commitCurrentEdit) {
485 throw new Error("SlickGrid.EditorLock.activate: editController must implement .commitCurrentEdit()");
486 }
487 if (!editController.cancelCurrentEdit) {
488 throw new Error("SlickGrid.EditorLock.activate: editController must implement .cancelCurrentEdit()");
489 }
490 activeEditController = editController;
491 };
492
493 /***
494 * Unsets the specified edit controller as the active edit controller (release edit lock).
495 * If the specified edit controller is not the active one, an exception will be throw new Error(.
496 * @method deactivate
497 * @param editController {EditController} edit controller releasing the lock
498 */
499 this.deactivate = function (editController) {
500 if (activeEditController !== editController) {
501 throw new Error("SlickGrid.EditorLock.deactivate: specified editController is not the currently active one");
502 }
503 activeEditController = null;
504 };
505
506 /***
507 * Attempts to commit the current edit by calling "commitCurrentEdit" method on the active edit
508 * controller and returns whether the commit attempt was successful (commit may fail due to validation
509 * errors, etc.). Edit controller's "commitCurrentEdit" must return true if the commit has succeeded
510 * and false otherwise. If no edit controller is active, returns true.
511 * @method commitCurrentEdit
512 * @return {Boolean}
513 */
514 this.commitCurrentEdit = function () {
515 return (activeEditController ? activeEditController.commitCurrentEdit() : true);
516 };
517
518 /***
519 * Attempts to cancel the current edit by calling "cancelCurrentEdit" method on the active edit
520 * controller and returns whether the edit was successfully cancelled. If no edit controller is
521 * active, returns true.
522 * @method cancelCurrentEdit
523 * @return {Boolean}
524 */
525 this.cancelCurrentEdit = function cancelCurrentEdit() {
526 return (activeEditController ? activeEditController.cancelCurrentEdit() : true);
527 };
528 }
529
530 /**
531 *
532 * @param {Array} treeColumns Array com levels of columns
533 * @returns {{hasDepth: 'hasDepth', getTreeColumns: 'getTreeColumns', extractColumns: 'extractColumns', getDepth: 'getDepth', getColumnsInDepth: 'getColumnsInDepth', getColumnsInGroup: 'getColumnsInGroup', visibleColumns: 'visibleColumns', filter: 'filter', reOrder: reOrder}}
534 * @constructor
535 */
536 function TreeColumns(treeColumns) {
537
538 var columnsById = {};
539
540 function init() {
541 mapToId(treeColumns);
542 }
543
544 function mapToId(columns) {
545 columns
546 .forEach(function (column) {
547 columnsById[column.id] = column;
548
549 if (column.columns)
550 mapToId(column.columns);
551 });
552 }
553
554 function filter(node, condition) {
555
556 return node.filter(function (column) {
557
558 var valid = condition.call(column);
559
560 if (valid && column.columns)
561 column.columns = filter(column.columns, condition);
562
563 return valid && (!column.columns || column.columns.length);
564 });
565
566 }
567
568 function sort(columns, grid) {
569 columns
570 .sort(function (a, b) {
571 var indexA = getOrDefault(grid.getColumnIndex(a.id)),
572 indexB = getOrDefault(grid.getColumnIndex(b.id));
573
574 return indexA - indexB;
575 })
576 .forEach(function (column) {
577 if (column.columns)
578 sort(column.columns, grid);
579 });
580 }
581
582 function getOrDefault(value) {
583 return typeof value === 'undefined' ? -1 : value;
584 }
585
586 function getDepth(node) {
587 if (node.length)
588 for (var i in node)
589 return getDepth(node[i]);
590 else if (node.columns)
591 return 1 + getDepth(node.columns);
592 else
593 return 1;
594 }
595
596 function getColumnsInDepth(node, depth, current) {
597 var columns = [];
598 current = current || 0;
599
600 if (depth == current) {
601
602 if (node.length)
603 node.forEach(function(n) {
604 if (n.columns)
605 n.extractColumns = function() {
606 return extractColumns(n);
607 };
608 });
609
610 return node;
611 } else
612 for (var i in node)
613 if (node[i].columns) {
614 columns = columns.concat(getColumnsInDepth(node[i].columns, depth, current + 1));
615 }
616
617 return columns;
618 }
619
620 function extractColumns(node) {
621 var result = [];
622
623 if (node.hasOwnProperty('length')) {
624
625 for (var i = 0; i < node.length; i++)
626 result = result.concat(extractColumns(node[i]));
627
628 } else {
629
630 if (node.hasOwnProperty('columns'))
631
632 result = result.concat(extractColumns(node.columns));
633
634 else
635 return node;
636
637 }
638
639 return result;
640 }
641
642 function cloneTreeColumns() {
643 return $.extend(true, [], treeColumns);
644 }
645
646 init();
647
648 this.hasDepth = function () {
649
650 for (var i in treeColumns)
651 if (treeColumns[i].hasOwnProperty('columns'))
652 return true;
653
654 return false;
655 };
656
657 this.getTreeColumns = function () {
658 return treeColumns;
659 };
660
661 this.extractColumns = function () {
662 return this.hasDepth()? extractColumns(treeColumns): treeColumns;
663 };
664
665 this.getDepth = function () {
666 return getDepth(treeColumns);
667 };
668
669 this.getColumnsInDepth = function (depth) {
670 return getColumnsInDepth(treeColumns, depth);
671 };
672
673 this.getColumnsInGroup = function (groups) {
674 return extractColumns(groups);
675 };
676
677 this.visibleColumns = function () {
678 return filter(cloneTreeColumns(), function () {
679 return this.visible;
680 });
681 };
682
683 this.filter = function (condition) {
684 return filter(cloneTreeColumns(), condition);
685 };
686
687 this.reOrder = function (grid) {
688 return sort(treeColumns, grid);
689 };
690
691 this.getById = function (id) {
692 return columnsById[id];
693 };
694
695 this.getInIds = function (ids) {
696 return ids.map(function (id) {
697 return columnsById[id];
698 });
699 };
700 }
701})(jQuery);
702
703