slickgrid added to third-party
[myslice.git] / third-party / slickgrid-2.1 / slick.core.js
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     }
28   });
29
30   /***
31    * An event object for passing data to event handlers and letting them control propagation.
32    * <p>This is pretty much identical to how W3C and jQuery implement events.</p>
33    * @class EventData
34    * @constructor
35    */
36   function EventData() {
37     var isPropagationStopped = false;
38     var isImmediatePropagationStopped = false;
39
40     /***
41      * Stops event from propagating up the DOM tree.
42      * @method stopPropagation
43      */
44     this.stopPropagation = function () {
45       isPropagationStopped = true;
46     };
47
48     /***
49      * Returns whether stopPropagation was called on this event object.
50      * @method isPropagationStopped
51      * @return {Boolean}
52      */
53     this.isPropagationStopped = function () {
54       return isPropagationStopped;
55     };
56
57     /***
58      * Prevents the rest of the handlers from being executed.
59      * @method stopImmediatePropagation
60      */
61     this.stopImmediatePropagation = function () {
62       isImmediatePropagationStopped = true;
63     };
64
65     /***
66      * Returns whether stopImmediatePropagation was called on this event object.\
67      * @method isImmediatePropagationStopped
68      * @return {Boolean}
69      */
70     this.isImmediatePropagationStopped = function () {
71       return isImmediatePropagationStopped;
72     }
73   }
74
75   /***
76    * A simple publisher-subscriber implementation.
77    * @class Event
78    * @constructor
79    */
80   function Event() {
81     var handlers = [];
82
83     /***
84      * Adds an event handler to be called when the event is fired.
85      * <p>Event handler will receive two arguments - an <code>EventData</code> and the <code>data</code>
86      * object the event was fired with.<p>
87      * @method subscribe
88      * @param fn {Function} Event handler.
89      */
90     this.subscribe = function (fn) {
91       handlers.push(fn);
92     };
93
94     /***
95      * Removes an event handler added with <code>subscribe(fn)</code>.
96      * @method unsubscribe
97      * @param fn {Function} Event handler to be removed.
98      */
99     this.unsubscribe = function (fn) {
100       for (var i = handlers.length - 1; i >= 0; i--) {
101         if (handlers[i] === fn) {
102           handlers.splice(i, 1);
103         }
104       }
105     };
106
107     /***
108      * Fires an event notifying all subscribers.
109      * @method notify
110      * @param args {Object} Additional data object to be passed to all handlers.
111      * @param e {EventData}
112      *      Optional.
113      *      An <code>EventData</code> object to be passed to all handlers.
114      *      For DOM events, an existing W3C/jQuery event object can be passed in.
115      * @param scope {Object}
116      *      Optional.
117      *      The scope ("this") within which the handler will be executed.
118      *      If not specified, the scope will be set to the <code>Event</code> instance.
119      */
120     this.notify = function (args, e, scope) {
121       e = e || new EventData();
122       scope = scope || this;
123
124       var returnValue;
125       for (var i = 0; i < handlers.length && !(e.isPropagationStopped() || e.isImmediatePropagationStopped()); i++) {
126         returnValue = handlers[i].call(scope, e, args);
127       }
128
129       return returnValue;
130     };
131   }
132
133   function EventHandler() {
134     var handlers = [];
135
136     this.subscribe = function (event, handler) {
137       handlers.push({
138         event: event,
139         handler: handler
140       });
141       event.subscribe(handler);
142
143       return this;  // allow chaining
144     };
145
146     this.unsubscribe = function (event, handler) {
147       var i = handlers.length;
148       while (i--) {
149         if (handlers[i].event === event &&
150             handlers[i].handler === handler) {
151           handlers.splice(i, 1);
152           event.unsubscribe(handler);
153           return;
154         }
155       }
156
157       return this;  // allow chaining
158     };
159
160     this.unsubscribeAll = function () {
161       var i = handlers.length;
162       while (i--) {
163         handlers[i].event.unsubscribe(handlers[i].handler);
164       }
165       handlers = [];
166
167       return this;  // allow chaining
168     }
169   }
170
171   /***
172    * A structure containing a range of cells.
173    * @class Range
174    * @constructor
175    * @param fromRow {Integer} Starting row.
176    * @param fromCell {Integer} Starting cell.
177    * @param toRow {Integer} Optional. Ending row. Defaults to <code>fromRow</code>.
178    * @param toCell {Integer} Optional. Ending cell. Defaults to <code>fromCell</code>.
179    */
180   function Range(fromRow, fromCell, toRow, toCell) {
181     if (toRow === undefined && toCell === undefined) {
182       toRow = fromRow;
183       toCell = fromCell;
184     }
185
186     /***
187      * @property fromRow
188      * @type {Integer}
189      */
190     this.fromRow = Math.min(fromRow, toRow);
191
192     /***
193      * @property fromCell
194      * @type {Integer}
195      */
196     this.fromCell = Math.min(fromCell, toCell);
197
198     /***
199      * @property toRow
200      * @type {Integer}
201      */
202     this.toRow = Math.max(fromRow, toRow);
203
204     /***
205      * @property toCell
206      * @type {Integer}
207      */
208     this.toCell = Math.max(fromCell, toCell);
209
210     /***
211      * Returns whether a range represents a single row.
212      * @method isSingleRow
213      * @return {Boolean}
214      */
215     this.isSingleRow = function () {
216       return this.fromRow == this.toRow;
217     };
218
219     /***
220      * Returns whether a range represents a single cell.
221      * @method isSingleCell
222      * @return {Boolean}
223      */
224     this.isSingleCell = function () {
225       return this.fromRow == this.toRow && this.fromCell == this.toCell;
226     };
227
228     /***
229      * Returns whether a range contains a given cell.
230      * @method contains
231      * @param row {Integer}
232      * @param cell {Integer}
233      * @return {Boolean}
234      */
235     this.contains = function (row, cell) {
236       return row >= this.fromRow && row <= this.toRow &&
237           cell >= this.fromCell && cell <= this.toCell;
238     };
239
240     /***
241      * Returns a readable representation of a range.
242      * @method toString
243      * @return {String}
244      */
245     this.toString = function () {
246       if (this.isSingleCell()) {
247         return "(" + this.fromRow + ":" + this.fromCell + ")";
248       }
249       else {
250         return "(" + this.fromRow + ":" + this.fromCell + " - " + this.toRow + ":" + this.toCell + ")";
251       }
252     }
253   }
254
255
256   /***
257    * A base class that all special / non-data rows (like Group and GroupTotals) derive from.
258    * @class NonDataItem
259    * @constructor
260    */
261   function NonDataItem() {
262     this.__nonDataRow = true;
263   }
264
265
266   /***
267    * Information about a group of rows.
268    * @class Group
269    * @extends Slick.NonDataItem
270    * @constructor
271    */
272   function Group() {
273     this.__group = true;
274
275     /**
276      * Grouping level, starting with 0.
277      * @property level
278      * @type {Number}
279      */
280     this.level = 0;
281
282     /***
283      * Number of rows in the group.
284      * @property count
285      * @type {Integer}
286      */
287     this.count = 0;
288
289     /***
290      * Grouping value.
291      * @property value
292      * @type {Object}
293      */
294     this.value = null;
295
296     /***
297      * Formatted display value of the group.
298      * @property title
299      * @type {String}
300      */
301     this.title = null;
302
303     /***
304      * Whether a group is collapsed.
305      * @property collapsed
306      * @type {Boolean}
307      */
308     this.collapsed = false;
309
310     /***
311      * GroupTotals, if any.
312      * @property totals
313      * @type {GroupTotals}
314      */
315     this.totals = null;
316
317     /**
318      * Rows that are part of the group.
319      * @property rows
320      * @type {Array}
321      */
322     this.rows = [];
323
324     /**
325      * Sub-groups that are part of the group.
326      * @property groups
327      * @type {Array}
328      */
329     this.groups = null;
330
331     /**
332      * A unique key used to identify the group.  This key can be used in calls to DataView
333      * collapseGroup() or expandGroup().
334      * @property groupingKey
335      * @type {Object}
336      */
337     this.groupingKey = null;
338   }
339
340   Group.prototype = new NonDataItem();
341
342   /***
343    * Compares two Group instances.
344    * @method equals
345    * @return {Boolean}
346    * @param group {Group} Group instance to compare to.
347    */
348   Group.prototype.equals = function (group) {
349     return this.value === group.value &&
350         this.count === group.count &&
351         this.collapsed === group.collapsed;
352   };
353
354   /***
355    * Information about group totals.
356    * An instance of GroupTotals will be created for each totals row and passed to the aggregators
357    * so that they can store arbitrary data in it.  That data can later be accessed by group totals
358    * formatters during the display.
359    * @class GroupTotals
360    * @extends Slick.NonDataItem
361    * @constructor
362    */
363   function GroupTotals() {
364     this.__groupTotals = true;
365
366     /***
367      * Parent Group.
368      * @param group
369      * @type {Group}
370      */
371     this.group = null;
372   }
373
374   GroupTotals.prototype = new NonDataItem();
375
376   /***
377    * A locking helper to track the active edit controller and ensure that only a single controller
378    * can be active at a time.  This prevents a whole class of state and validation synchronization
379    * issues.  An edit controller (such as SlickGrid) can query if an active edit is in progress
380    * and attempt a commit or cancel before proceeding.
381    * @class EditorLock
382    * @constructor
383    */
384   function EditorLock() {
385     var activeEditController = null;
386
387     /***
388      * Returns true if a specified edit controller is active (has the edit lock).
389      * If the parameter is not specified, returns true if any edit controller is active.
390      * @method isActive
391      * @param editController {EditController}
392      * @return {Boolean}
393      */
394     this.isActive = function (editController) {
395       return (editController ? activeEditController === editController : activeEditController !== null);
396     };
397
398     /***
399      * Sets the specified edit controller as the active edit controller (acquire edit lock).
400      * If another edit controller is already active, and exception will be thrown.
401      * @method activate
402      * @param editController {EditController} edit controller acquiring the lock
403      */
404     this.activate = function (editController) {
405       if (editController === activeEditController) { // already activated?
406         return;
407       }
408       if (activeEditController !== null) {
409         throw "SlickGrid.EditorLock.activate: an editController is still active, can't activate another editController";
410       }
411       if (!editController.commitCurrentEdit) {
412         throw "SlickGrid.EditorLock.activate: editController must implement .commitCurrentEdit()";
413       }
414       if (!editController.cancelCurrentEdit) {
415         throw "SlickGrid.EditorLock.activate: editController must implement .cancelCurrentEdit()";
416       }
417       activeEditController = editController;
418     };
419
420     /***
421      * Unsets the specified edit controller as the active edit controller (release edit lock).
422      * If the specified edit controller is not the active one, an exception will be thrown.
423      * @method deactivate
424      * @param editController {EditController} edit controller releasing the lock
425      */
426     this.deactivate = function (editController) {
427       if (activeEditController !== editController) {
428         throw "SlickGrid.EditorLock.deactivate: specified editController is not the currently active one";
429       }
430       activeEditController = null;
431     };
432
433     /***
434      * Attempts to commit the current edit by calling "commitCurrentEdit" method on the active edit
435      * controller and returns whether the commit attempt was successful (commit may fail due to validation
436      * errors, etc.).  Edit controller's "commitCurrentEdit" must return true if the commit has succeeded
437      * and false otherwise.  If no edit controller is active, returns true.
438      * @method commitCurrentEdit
439      * @return {Boolean}
440      */
441     this.commitCurrentEdit = function () {
442       return (activeEditController ? activeEditController.commitCurrentEdit() : true);
443     };
444
445     /***
446      * Attempts to cancel the current edit by calling "cancelCurrentEdit" method on the active edit
447      * controller and returns whether the edit was successfully cancelled.  If no edit controller is
448      * active, returns true.
449      * @method cancelCurrentEdit
450      * @return {Boolean}
451      */
452     this.cancelCurrentEdit = function cancelCurrentEdit() {
453       return (activeEditController ? activeEditController.cancelCurrentEdit() : true);
454     };
455   }
456 })(jQuery);
457
458