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 |
|