1 /**
  2  * @fileOverview Contains the jMatrixBrowse rendering code.
  3  * 
  4  * Handles rendering of jMatrixBrowse and manages the dragging, keyboard
  5  * shortcuts and mouse shortcuts. This doesn't perform reloading of the data
  6  * which is handled by catching corresponding events in jMatrixBrowse.
  7  * 
  8  * @version 0.1
  9  * @author Pulkit Goyal <pulkit110@gmail.com> 
 10 */
 11 
 12 /**
 13  * See (http://jquery.com/).
 14  * @name jQuery
 15  * @class 
 16  * See the jQuery Library  (http://jquery.com/) for full details.  This just
 17  * documents the function and classes that are added to jQuery by this plug-in.
 18  */
 19 
 20 /**
 21  * See (http://jquery.com/)
 22  * @name fn
 23  * @class 
 24  * See the jQuery Library  (http://jquery.com/) for full details.  This just
 25  * documents the function and classes that are added to jQuery by this plug-in.
 26  * @memberOf jQuery
 27  */
 28 
 29 var jMatrixBrowseNs = jMatrixBrowseNs || {};
 30 
 31 (function (jQuery, jMatrixBrowseNs) {
 32 
 33   
 34 
 35   /**
 36    * jMatrixBrowse Renderer manages the rendering of elements as well as row and 
 37    * column headers.
 38    * 
 39    * @param {jQuery Object} elem - element that initiated jMatrixBrowse.
 40    * @param {Object} configuration - configuration for jMatrixBrowse.
 41    * @param {Object} api - api manager for making requests to api.
 42    * @class jMatrixBrowseRenderer
 43    * @memberOf jMatrixBrowseNs
 44    */
 45   jMatrixBrowseNs.jMatrixBrowseRenderer = function(elem, configuration, api) {
 46     var that = this;
 47     
 48     var _dragContainer;       // Drag container that allows dragging using jQuery UI
 49     var _cellElements;        // Array of array of cell elements.
 50     var _headers;             // row and column headers.
 51     var _elem;                // container that initiated jMatrixBrowse
 52     var _configuration;       // configuration for the current instance of jMatrixBrowse
 53     var _api;                 // api manager
 54     var _self;                // reference to self
 55     var _dragActive = false;  // boolean to indicate if drag is active
 56     var _container;           // container for jMatrixBrowse
 57 
 58     var _positions = new Array();            // last few positions for drag.
 59     var _isAnimating = false;          // is the scroller animating.
 60     var _wasAnimating = false;          // was the scroller animating.
 61     var _decelerationVelocity; // velocity of animation.
 62 
 63     _self = that;
 64     _elem = elem;
 65     _configuration = configuration;
 66     _api = api;
 67     
 68     // Add class for jMatrixBrowse container
 69     elem.addClass('jmb-matrix-container');
 70     
 71     /**
 72      * Gets the cell elements.
 73      * @returns {Array of Array of DOM elements} Elements in the cell.
 74      */
 75     this.getCellElements = function() {
 76       return _cellElements;
 77     };
 78     
 79     /**
 80      * Gets the row and column headers.
 81      * @returns {Object} headers - row and column headers.
 82      * @returns {jQuery Object} headers.row - row header.
 83      * @returns {jQuery Object} headers.col - column header.
 84      */
 85     this.getHeaders = function() {
 86       return _headers;
 87     };
 88     
 89     /**
 90      * Gets the container for jMatrixBrowse.
 91      * @returns {jQuery Object} The container for jMatrixBrowse.
 92      */
 93     this.getContainer = function() {
 94       return _container;
 95     };
 96 
 97     /**
 98      * Moves the row to bottom. 
 99      * @param {Number} row - index of the row to be moved.
100      * @returns {boolean} true if the operation was successful. false otherwise.
101      */
102     this.moveRowToEnd = function(row) {
103       // Get index of last cell
104       var height = _cellElements.length;
105       var lastCell = (_cellElements[height-1].length > 0) ? jQuery(_cellElements[height-1][0]) : undefined;
106       if (lastCell === undefined) {
107         console.error('Unable to move row ' + row + ' to the end.')
108         return false;
109       }
110 
111       // Change the position of all elements in the row.
112       var newTop = lastCell.position().top + lastCell.height();
113       for (var i = 0, w = _cellElements[row].length; i < w; ++i) {
114         jQuery(_cellElements[row][i]).css({
115           top: newTop
116         });
117       }
118 
119       // Move row in matrix to end
120       var cellRow = _cellElements.splice(row,1); // Remove row at [backgroundTopRow]
121       if (cellRow.length > 0)
122         _cellElements.push(cellRow[0]);  // Insert row at the end.
123 
124       addSpinners({
125         row1: _cellElements.length-1,
126         row2: _cellElements.length-1
127       }, {
128         col1: 0,
129         col2: _cellElements[0].length-1
130       });
131 
132       return true;
133     };
134     
135     /**
136      * Moves the row to top. 
137      * @param {Number} row - index of the row to be moved.
138      * @returns {boolean} true if the operation was successful. false otherwise.
139      */
140     this.moveRowToTop = function(row) {
141       // Get index of first cell
142       var firstCell = (_cellElements.length > 0 && _cellElements[0].length > 0)?jQuery(_cellElements[0][0]):undefined;
143       if (firstCell === undefined) {
144         console.error('Unable to move row ' + row + ' to top.')
145         return false;
146       }
147 
148       // Change the position of all elements in the row.
149       var newBottom = firstCell.position().top;
150       for (var i = 0, w = _cellElements[row].length; i < w; ++i) {
151         jQuery(_cellElements[row][i]).css({
152           top: newBottom - jQuery(_cellElements[row][i]).height()
153         });
154       }
155       // Move row in matrix to first
156       var cellRow = _cellElements.splice(row,1);  // Remove row at [backgroundBottomRow]
157       if (cellRow.length > 0)
158         _cellElements.splice(0,0,cellRow[0]);  // Insert row at the beginning.
159       
160       addSpinners({
161         row1: 0,
162         row2: 0
163       }, {
164         col1: 0,
165         col2: _cellElements[0].length-1
166       });
167       return true;
168     };
169     
170     /**
171      * Moves a column to right. 
172      * @param {Number} col - index of the column to be moved.
173      * @returns {boolean} true if the operation was successful. false otherwise.
174      */
175     this.moveColToRight = function(col) {
176       if (_cellElements.length <= 0 || _cellElements[0].length <= 0) {
177         console.error('Unable to move col ' + col + ' to right.');
178         return false;
179       }
180 
181       // Change the position of all elements in the column.
182       var w = _cellElements[0].length;
183       var lastCell = jQuery(_cellElements[0][w-1]);
184       var newLeft = lastCell.position().left + lastCell.width();
185       for (var i = 0, h = _cellElements.length; i < h; ++i) {
186         jQuery(_cellElements[i][col]).css({
187           left: newLeft
188         });
189       }
190       // Move col to end in matrix.
191       for (var i = 0, h = _cellElements.length; i < h; ++i) {
192         var cell = _cellElements[i].splice(col, 1); // Remove element at [i][col]
193         _cellElements[i].push(cell[0]); // Insert element at end of row i
194       }
195 
196       addSpinners({
197         row1: 0,
198         row2: _cellElements.length-1
199       }, {
200         col1: _cellElements[0].length-1,
201         col2: _cellElements[0].length-1
202       });
203       return true;
204     };
205     
206     /**
207      * Moves a column to left. 
208      * @param {Number} col - index of the column to be moved.
209      * @returns {boolean} true if the operation was successful. false otherwise.
210      */
211     this.moveColToLeft = function(col) {
212       if (_cellElements.length <= 0 || _cellElements[0].length <= 0) {
213         console.error('Unable to move col ' + col + ' to left.');
214         return false;
215       }
216 
217       var firstCell = jQuery(_cellElements[0][0]);
218       // Change the position of all elements in the column.
219       var newRight = firstCell.position().left;
220       for (var i = 0, h = _cellElements.length; i < h; ++i) {
221         jQuery(_cellElements[i][col]).css({
222           left: newRight - jQuery(_cellElements[i][col]).width()
223         });
224       }
225       // Move col to first in matrix.
226       for (var i = 0, h = _cellElements.length; i < h; ++i) {
227         var cell = _cellElements[i].splice(col, 1); // Remove element at [i][col]
228         _cellElements[i].splice(0,0,cell[0]); // Insert element to [i][0]
229       }
230 
231       addSpinners({
232         row1: 0,
233         row2: _cellElements.length-1
234       }, {
235         col1: 0,
236         col2: 0
237       });
238       return true;
239     };
240     
241     /**
242      * Scrolls the matrix one cell to the right.
243      */
244     this.scrollRight = function() {
245       if (checkScrollBounds('right'))
246         scrollCols('right', 1);
247     };
248 
249     /**
250      * Scrolls the matrix one cell to the left.
251      */
252     this.scrollLeft = function() {
253       if (checkScrollBounds('left'))
254         scrollCols('left', 1);
255     };
256         
257     /**
258      * Scrolls the matrix one row up.
259      */
260     this.scrollUp = function() {
261       if (checkScrollBounds('up'))
262         scrollRows('up', 1);
263     };
264     
265     /**
266      * Scrolls the matrix one row down.
267      */
268     this.scrollDown = function() {
269       if (checkScrollBounds('down'))
270         scrollRows('down', 1);
271     };
272     
273     /**
274      * Scrolls the matrix one page up.
275      */
276     this.pageUp = function() {
277       var nRowsToScroll = getNumberOfRowsForPageScroll('up');
278       scrollRows('up', nRowsToScroll);
279     };
280     
281     /**
282      * Scrolls the matrix one page down.
283      */
284     this.pageDown = function() {
285       var nRowsToScroll = getNumberOfRowsForPageScroll('down');
286       scrollRows('down', nRowsToScroll);
287     };
288     
289     /**
290      * Scrolls the matrix one page left.
291      */
292     this.pageLeft = function() {
293       var nColsToScroll = getNumberOfColsForPageScroll('left');
294       scrollCols('left', nColsToScroll);
295     };
296     
297     /**
298      * Scrolls the matrix one page right.
299      */
300     this.pageRight = function() {
301       var nColsToScroll = getNumberOfColsForPageScroll('right');
302       scrollCols('right', nColsToScroll);
303     };
304 
305     /**
306      * Snap the element to grid.
307      * If called without any argument, it finds the element closest to the boundary (TODO) to snap.
308      * If the direction is not defined, it snaps to both the top and left.
309      * Otherwise, it snaps the given element in the given direction.
310      *
311      * @param {jQuery Object} element - the element to snap to grid (optional).
312      * @param {string} - the direction to snap (from top, left) (optional).
313      */
314     this.snapToGrid = function(element, direction) {
315 
316       if (element === undefined && direction === undefined) {
317         _self.snapToGrid(getCellToSnap());
318         return;
319       }
320       
321       // Get element and container offsets
322       var containerOffset = _container.offset();
323       var elementOffset = element.offset();
324       var dragContainerOffset = _dragContainer.offset();
325 
326       if (direction === 'top' || direction === undefined) {
327         // The posoition.top of the element relative to cotainter.
328         var top = elementOffset.top - containerOffset.top;
329         if (top !== 0) { // Element is not already snapped
330           dragContainerOffset.top -= top;
331         }
332       }
333       if (direction === 'left' || direction === undefined) {
334         // The posoition.left of the element relative to cotainter.
335         var left = elementOffset.left - containerOffset.left;
336         if (left !== 0) { // Element is not already snapped
337           dragContainerOffset.left -= left;
338         }
339       }
340 
341       _dragContainer.offset(dragContainerOffset);
342     }
343 
344     /**
345      * Zooms one level in.
346      */
347     this.zoomIn = function() {
348       // Set the window size in Configuration
349       var currentSize = _configuration.getWindowSize();
350       _configuration.setWindowSize({
351         height: Math.max(1, currentSize.height - jMatrixBrowseNs.Constants.ZOOM_LEVEL_DIFFERENCE),
352         width: Math.max(1, currentSize.width - jMatrixBrowseNs.Constants.ZOOM_LEVEL_DIFFERENCE)
353       });
354 
355       // Remove already existing containers.
356       cleanup();
357 
358       // Initialize with the new window size.
359       init(_self.currentCell);
360     };
361 
362     /**
363      * Zooms one level out.
364      */
365     this.zoomOut = function() {
366       // Set the window size in Configuration
367       var currentSize = _configuration.getWindowSize();
368       var windowSize = {
369         height: Math.min(jMatrixBrowseNs.Constants.ZOOM_MAX_WINDOW_SIZE.height, _api.getMatrixSize().height, currentSize.height + jMatrixBrowseNs.Constants.ZOOM_LEVEL_DIFFERENCE),
370         width: Math.min(jMatrixBrowseNs.Constants.ZOOM_MAX_WINDOW_SIZE.width, _api.getMatrixSize().width, currentSize.width + jMatrixBrowseNs.Constants.ZOOM_LEVEL_DIFFERENCE)
371       };
372       _configuration.setWindowSize(windowSize);
373 
374       // Update the position of window (if required)
375       var matrixSize = _api.getMatrixSize();
376       var windowPosition = _self.currentCell;
377       windowPosition = {
378         row: (windowPosition.row + windowSize.height > matrixSize.height)?(matrixSize.height - windowSize.height):windowPosition.row,
379         col: (windowPosition.col + windowSize.width > matrixSize.width)?(matrixSize.width - windowSize.width):windowPosition.col,
380       };
381 
382       // Remove already existing containers.
383       cleanup();
384       
385       // Initialize with the new window size.
386       init(windowPosition);
387     };
388 
389     this.getIsAnimating = function() {
390       return _isAnimating;
391     };
392 
393     init(_configuration.getWindowPosition());
394 
395     // Private methods
396     /**
397      * Initializes the jMatrixBrowseRenderer component. 
398      * This creates the required contianers and generates content in the matrix.
399      * @param  {Object} windowPosition - position of first cell in window (properties: row and col)
400      */
401     function init(windowPosition) {
402       // TODO: This is a hack
403       _api.setRenderer(that);
404         
405       // Create row and column headers.
406       _headers = createRowColumnHeaderContainer(_elem);
407 
408       // Create draggable area and add matrix to it.
409       var containers = createDragContainer(_elem);
410       _dragContainer = containers.dragContainer;
411       _container = containers.container;
412 
413       // Scroll to the window position
414       scrollTo(windowPosition.row, windowPosition.col);
415 
416       // Generate initial content
417       _content = generateInitialMatrixContent(_dragContainer);
418 
419       // Generate row and column header content
420       generateRowColumnHeaders(_headers);
421 
422       _elem.trigger('jMatrixBrowseRendererInitialized');
423     }
424 
425     /**
426      * Removes all the DOM elements created by jMatrixBrowseRenderer. 
427      */
428     function cleanup() {
429       _headers.row.remove();
430       _headers.col.remove();
431       _dragContainer.remove();
432       _container.remove();
433     }
434 
435     /**
436     * Create the content div and append to container.
437     * @param {jQuery Object} container - container to attacht the content to.
438     * @returns {jQuery Object} content 
439     */
440     function createContentDiv(container) {
441       var content = jQuery(document.createElement('div')).addClass('jMatrixBrowse-content');
442       container.append(content);
443       return content;
444     }
445 
446     /**
447     * Generate class name for given cell position.
448     * @param {Number} row - zero indexed row of the element.
449     * @param {Number} col - zero indexed column of the element.
450     * @returns {string} className - class name for the cell element.
451     */
452     function generateClassNameForCell(row, col) {
453       return "j-matrix-browse-cell-" + "row" + row + "col" + col;
454     }
455 
456     /**
457     * Create the row and column header containers. 
458     * @param {jQuery Object} container - container to attach the content to.
459     * @returns {Object} headersContainer - hash containing column and row containers.
460     * @returns {jQuery Object} headersContainer.row - row container.
461     * @returns {jQuery Object} headersContainer.col - column container.
462     */
463     function createRowColumnHeaderContainer(container) {
464       var colHeaderContainer = jQuery(document.createElement('div'));
465       colHeaderContainer.css({
466         width: '90%',
467         height: '10%',
468         top: '0px',
469         right: '0px',
470         'background-color': 'red',
471         position: 'absolute',
472         overflow: 'hidden'
473       });
474       colHeaderContainer.addClass(jMatrixBrowseNs.Constants.CLASS_BASE + '-col-header');
475       container.append(colHeaderContainer);
476 
477       var rowHeaderContainer = jQuery(document.createElement('div'));
478       rowHeaderContainer.css({
479         width: '10%',
480         height: '90%',
481         bottom: '0px',
482         'background-color': 'green',
483         'float': 'left',
484         position: 'absolute',
485         overflow: 'hidden'
486       });
487       rowHeaderContainer.addClass(jMatrixBrowseNs.Constants.CLASS_BASE + '-row-header');
488       container.append(rowHeaderContainer);
489 
490       return {
491         row: rowHeaderContainer,
492         col: colHeaderContainer
493       };
494     }
495 
496     /**
497     * Create the drag container and make it draggable.
498     * @param {jQuery Object} container - container to attach the content to.
499     * @returns {Object} coantiners
500     * @returns {jQuery Object} coantiners.conatiner coantiner containing matrix content
501     * @returns {jQuery Object} coantiners.dragConatiner dragCoantiner containing matrix content
502     */
503     function createDragContainer(container) {
504       // Create the container that holds the drag container. 
505       var dragContainerContainer = jQuery(document.createElement('div'));
506       // TODO: Move css to stylesheet. 
507       dragContainerContainer.css({
508         'float': 'left',
509         width: '90%',
510         height: '90%',
511         bottom: '0px',
512         right: '0px',
513         position: 'absolute',
514         'overflow': 'hidden'
515       }); 
516       dragContainerContainer.addClass(jMatrixBrowseNs.Constants.CLASS_BASE+'-drag-container-container');
517       container.append(dragContainerContainer);
518 
519       // Create drag container. 
520       var dragContainer = jQuery(document.createElement('div'));
521       dragContainer.draggable({
522         drag: function (event, ui) {
523           dragHandler(event, ui);
524 
525           // Store the positions.
526           _positions.push({
527             position: ui.position, 
528             timestamp: new Date().getTime()
529           });
530 
531           // Keep list from growing infinitely (holding min 10, max 20 measure points)
532           if (_positions.length > 60) {
533             _positions.splice(0, 30);
534           }
535         }, 
536         start: function (event, ui) {
537           dragStartHandler(event, ui);
538         }, 
539         stop: function (event, ui) {
540           dragStopHandler(event, ui);
541         },
542         containment: [ 0, 0, 2000, 2000]
543       });
544       // Override the original _generatePosition in draggable to check for matrix bounds on drag.
545       dragContainer.draggable().data("draggable")._generatePosition = function(event) {
546         return generatePositionsForDrag(dragContainer.draggable().data("draggable"), event);
547       };
548 
549       dragContainer.addClass(jMatrixBrowseNs.Constants.CLASS_BASE+'-drag-container');
550       dragContainerContainer.append(dragContainer);
551 
552       return {
553         dragContainer: dragContainer,
554         container: dragContainerContainer
555       };
556     }
557 
558     /**
559      * Begin animating the matrix using _decelartionVelocity.
560      * This uses cubic easing to ease out the animation. The duration of the animation can be set in configuration.
561      */
562     function startAnimation() {
563       var duration = _configuration.getAnimationDuration();
564 
565       if (_wasAnimating) {
566         // Increase velocity if element was already animating. 
567         _decelerationVelocity.y *= 2;
568         _decelerationVelocity.x *= 2;
569       }
570       _dragContainer.animate({
571         top: '+=' + _decelerationVelocity.y * duration,
572         left: '+=' + _decelerationVelocity.x * duration,
573       }, {
574         duration: duration, 
575         easing: (!_wasAnimating)?'easeOutCubic':'easeInOutCubic', // If the animation was already running, use easeInOutCubic. 
576         step: function(now, fx) {
577           // Trigger the animation step event.
578           _elem.trigger({
579             type: 'jMatrixBrowseAnimationStep',
580             now: now,
581             fx: fx
582           });
583         }, 
584         complete: function() {
585           _isAnimating = false;
586 
587           // Check if the new position crosses bounds and revert to the boundaries of matrix if bounds are crossed. 
588           var position = checkPositionBounds (jQuery(this).position(), {top: 0, left:0}, _dragContainer.draggable().data("draggable"));
589           _dragContainer.animate(position, {
590             duration: 'fast',
591             complete: function() {
592               // Trigger animation complete event.
593               _elem.trigger({
594                 type: 'jMatrixBrowseAnimationComplete'
595               });
596             }
597           });  
598         }
599       });
600 
601       // Animation has started.
602       _isAnimating = true;
603     }
604     
605     /**
606     * Generate new positions of the draggable element. This is used to override
607     * the original generate positions which had no way of specifying dynamic
608     * containment. This checks if the drag is valid by looking at the matrix
609     * coordinates and returns the new top and left positions accordingly.
610     *
611     * @param {Object} draggable - draggable object data.
612     * @param {Object} event - event that initiated the drag.
613     * @returns {Object} positions - new position of the draggable.
614     */
615     function generatePositionsForDrag(draggable, event) {
616       var o = draggable.options, scroll = draggable.cssPosition == 'absolute' && !(draggable.scrollParent[0] != document && $.ui.contains(draggable.scrollParent[0], draggable.offsetParent[0])) ? draggable.offsetParent : draggable.scrollParent, scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName);
617       var pageX = event.pageX;
618       var pageY = event.pageY;
619 
620       var newPosition = {
621         top: (
622                 pageY															// The absolute mouse position
623                 - draggable.offset.click.top												// Click offset (relative to the element)
624                 - draggable.offset.relative.top												// Only for relative positioned nodes: Relative offset from element to offset parent
625                 - draggable.offset.parent.top												// The offsetParent's offset without borders (offset + border)
626                 + (jQuery.browser.safari && jQuery.browser.version < 526 && draggable.cssPosition == 'fixed' ? 0 : ( draggable.cssPosition == 'fixed' ? -draggable.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ))
627         ),
628         left: (
629                 pageX															// The absolute mouse position
630                 - draggable.offset.click.left												// Click offset (relative to the element)
631                 - draggable.offset.relative.left												// Only for relative positioned nodes: Relative offset from element to offset parent
632                 - draggable.offset.parent.left												// The offsetParent's offset without borders (offset + border)
633                 + (jQuery.browser.safari && jQuery.browser.version < 526 && draggable.cssPosition == 'fixed' ? 0 : ( draggable.cssPosition == 'fixed' ? -draggable.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() ))
634         )
635       };
636 
637       // Impose contraints on newPosition to prevent crossing of matrix bounds. 
638       // Compute change in position for the drag.
639       var changeInPosition = {
640         top: (draggable._convertPositionTo("absolute", newPosition).top - draggable.positionAbs.top),
641         left: (draggable._convertPositionTo("absolute", newPosition).left - draggable.positionAbs.left)
642       };
643 
644       return checkPositionBounds(newPosition, changeInPosition, draggable);
645     }
646 
647     /**
648      * Checks the bounds for the matirx from four directions to find if the bounds are violated and returns the new positions.
649      * @param  {Object} newPosition - The new position of the container for which to check the bounds.
650      * @param  {Object} changeInPosition - The change in position. {top:0, left:0} can be passed here.
651      * @param  {Object} draggable - The draggable instance.
652      * @returns {Object} newPosition of the container.
653      */
654     function checkPositionBounds (newPosition, changeInPosition, draggable) {
655       
656       var firstRow = (_self.currentCell.row == 0)?1:0;
657       var firstCol = (_self.currentCell.col == 0)?1:0;
658       // Get element and container offsets
659       var element = jQuery(_cellElements[firstRow][firstCol]);
660       var containerOffset = _container.offset();
661       var elementOffset = element.offset();
662 
663       // If we are at the topmost cell, then check that bounds from the top are maintained.
664       if (_self.currentCell.row <= 1) {
665         // The new posoition.top of the first element relative to cotainter.
666         var top = changeInPosition.top + elementOffset.top - containerOffset.top;
667         if (top > 0) { // The drag crosses matrix bounds from the top.
668           newPosition.top = newPosition.top - top;
669         }
670       }
671 
672       // If we are at the leftmost cell, then check that bounds from the left are maintained.
673       if (_self.currentCell.col <= 1) {
674         // The new posoition.top of the first element relative to cotainter.
675         var left = changeInPosition.left + elementOffset.left - containerOffset.left;
676         if (left > 0) { // The drag crosses matrix bounds from the left.
677           newPosition.left = newPosition.left - left;
678         }
679       }
680 
681       // Get element offset for last element
682       element = jQuery(_cellElements[_cellElements.length-1][_cellElements[0].length-1]);
683       elementOffset = element.offset();
684 
685       // If we are at the bottomost cell, then check that bounds from the bottom are maintained.
686       if (_self.currentCell.row - _configuration.getNumberOfBackgroundCells() + _cellElements.length - 1 >= _api.getMatrixSize().height-1) {
687         var containerBottom = (containerOffset.top + _container.height());
688         var elementBottom = (changeInPosition.top + elementOffset.top + element.height());
689         // The new posoition.bottom of the last element relative to cotainter.
690         var bottom =  containerBottom - elementBottom;
691         if (bottom > 0) { // The drag crosses matrix bounds from the bottom.
692           newPosition.top = newPosition.top + bottom;
693         }
694       }
695 
696       // If we are at the leftmost cell, then check that bounds from the left are maintained.
697       if (_self.currentCell.col - _configuration.getNumberOfBackgroundCells() + _cellElements[0].length - 1 >= _api.getMatrixSize().width-1) {
698         // The new posoition.right of the first element relative to cotainter.
699         var containerRight = (containerOffset.left + _container.width());
700         var newElementRight = (changeInPosition.left + elementOffset.left + element.width());
701         var right =  containerRight - newElementRight;
702         if (right > 0) { // The drag crosses matrix bounds from the left.
703           newPosition.left = newPosition.left + right;
704         }
705       }
706 
707       return newPosition;
708     }
709 
710     /**
711     * Function that handles the click event on cell elements.
712     * @param {jQuery Object} elem - Element that triggered the click event
713     * @param {Object} event - Click event.
714     */
715     function cellClickHandler(elem, event) {
716       event.type = 'jMatrixBrowseClick';
717       event.row = elem.attr('data-row');
718       event.col = elem.attr('data-col');
719       _elem.trigger(event);
720     }
721 
722     /**
723     * Function that handles the drag event on dragContainer.
724     * @param {Object} event - Drag event.
725     * @param {Object} ui
726     */
727     function dragHandler (event, ui) {
728       event.type = 'jMatrixBrowseDrag';
729       _elem.trigger(event);
730     }
731 
732     /**
733     * Function that handles the drag start event on dragContainer.
734     * @param {Object} event - Drag event.
735     * @param {Object} ui
736     */
737     function dragStartHandler (event, ui) {
738 
739       // Stop any existing animations.
740       if (_isAnimating) {
741         _dragContainer.stop();
742         _wasAnimating = true;
743         _isAnimating = false;
744       } else {
745         _wasAnimating = false;
746       }
747 
748       event.type = 'jMatrixBrowseDragStart';
749       _elem.trigger(event);
750     }
751 
752     /**
753     * Function that handles the drag stop event on dragContainer.
754     * @param {Object} event - Drag event.
755     * @param {Object} ui
756     */
757     function dragStopHandler (event, ui) {
758       var timestamp = new Date().getTime();
759 
760       // Check if animation is enabled.
761       if (_configuration.animateEnabled()) {
762         var endPositionIndex = _positions.length - 1;
763         // Check if we need to start animation.
764         if (timestamp - _positions[endPositionIndex].timestamp < 1000) {
765           var startPositionIndex = endPositionIndex;
766           // Get the position where we were 100 msec before
767           for (var i = endPositionIndex; i > 0 && _positions[i].timestamp > (_positions[endPositionIndex].timestamp - 100); --i) {
768             startPositionIndex = i;
769           };
770 
771           // If start and end position are the same, we can't compute the velocity
772           if (startPositionIndex !== endPositionIndex) {
773             // Compute relative movement between these two points
774             var timeOffset = _positions[endPositionIndex].timestamp - _positions[startPositionIndex].timestamp;
775             var movedLeft = _positions[endPositionIndex].position.left - _positions[startPositionIndex].position.left;
776             var movedTop = _positions[endPositionIndex].position.top - _positions[startPositionIndex].position.top;
777 
778             // Compute the deceleration velocity
779             _decelerationVelocity = {
780               x: movedLeft / timeOffset,
781               y: movedTop / timeOffset
782             };
783 
784             // Check if we have enough velocity for animation
785             _decelerationVelocity.x = (Math.abs(_decelerationVelocity.x) < _configuration.getMinVelocityForAnimation()) ? 0 : _decelerationVelocity.x;
786             _decelerationVelocity.y = (Math.abs(_decelerationVelocity.y) < _configuration.getMinVelocityForAnimation()) ? 0 : _decelerationVelocity.y;
787 
788             if (Math.abs(_decelerationVelocity.x) > 0 || Math.abs(_decelerationVelocity.y) > 0) {
789               // Begin animation with deceleration.
790               startAnimation();
791             }
792           }
793         }
794       }
795 
796       // Trigger the drag stop event.
797       event.type = 'jMatrixBrowseDragStop';
798       _elem.trigger(event);
799     }
800 
801     /**
802     * Creates an empty matrix with size obtained from API and appends to content.
803     * @param {jQuery object} container - The element that acts as the matrix container (element that invoked jMatrixBrowse).
804     * @returns {jQuery object} content where matrix is generated.
805     */
806     function generateInitialMatrixContent (container) {
807       var size = _api.getMatrixSize();
808       if (size == undefined) {
809         console.error("Unable to get matrix size");
810         return null;
811       }
812 
813       var windowSize = _configuration.getWindowSize();
814       if (windowSize == undefined) {
815         console.error("Unable to get window size");
816         return null;
817       }
818 
819       var content = createContentDiv(container);
820       content.css({
821         'position' : 'absolute',
822         'top' : 0,
823         'left' : 0
824       });
825 
826       var cellHeight = _container.height()/windowSize.height;
827       var cellWidth = _container.width()/windowSize.width;
828 
829       var windowPosition = _configuration.getWindowPosition();
830 
831       _cellElements = [];
832       var height = windowSize.height + 2*_configuration.getNumberOfBackgroundCells();
833       var width = windowSize.width + 2*_configuration.getNumberOfBackgroundCells();
834       var rowBegin = Math.max(0, windowPosition.row - _configuration.getNumberOfBackgroundCells());
835       var colBegin = Math.max(0, windowPosition.col - _configuration.getNumberOfBackgroundCells());
836 
837       // Generate matrix content for only the rows that are in the window.
838       var frag = document.createDocumentFragment();
839       for (var row= rowBegin; row < rowBegin + height; row++) {
840         _cellElements.push([]);
841         for (var col = colBegin; col < colBegin + width; col++) {
842           // Create cell and set style
843           var elem = document.createElement("div");
844           elem.style.backgroundColor = row%2 + col%2 > 0 ? "#ddd" : "whitesmoke";
845           elem.style.width = cellWidth + "px";
846           elem.style.height = cellHeight + "px";
847           elem.style.position = "absolute";
848           elem.style.top = (row-rowBegin-_configuration.getNumberOfBackgroundCells())*cellHeight + "px";
849           elem.style.left = (col-colBegin-_configuration.getNumberOfBackgroundCells())*cellWidth + "px";
850           elem.style.display = "inline-block";
851           elem.style.textIndent = "6px";
852           elem.innerHTML = row + "," + col;
853           elem.className += " jMatrixBrowse-cell " + generateClassNameForCell(row, col);
854 
855           // Add data-row and data-col to cell
856           jQuery(elem).attr('data-row', row);
857           jQuery(elem).attr('data-col', col);
858 
859           // Append cell to fragment
860           frag.appendChild(elem);
861           _cellElements[row-rowBegin].push(elem);
862         }
863       }    
864       content.append(frag);
865 
866       // Associate click handler with cell
867       _elem.find('.jMatrixBrowse-cell').click(function(event) {
868         // Trigger click only when click is not for drag
869         if (!_dragActive) {
870           cellClickHandler(jQuery(this), event);
871         } else {
872           // Click was triggered due to drag. 
873           _dragActive = false;
874         }
875       });
876 
877       return content;
878     }
879 
880     /**
881     * Creates an empty matrix with size obtained from API and appends to content.
882     * @param {jQuery object} headers - header containers.
883     */
884     function generateRowColumnHeaders(headers) {
885       generateRowHeaders(headers.row);
886       generateColHeaders(headers.col);
887     }
888 
889     /**
890     * Generates elements and appends them to row header container. 
891     * @param {jQuery object} header - row header container.
892     */
893     function generateRowHeaders(header) {  
894       var rowHeaders = _api.getRowHeadersFromTopRow(_self.currentCell.row-_configuration.getNumberOfBackgroundCells());
895       var frag = document.createDocumentFragment();
896       for (var row = 0, nRows = rowHeaders.length; row < nRows; ++row) {
897         var cellElement = jQuery(_cellElements[row][0]);
898         var elem = jQuery(document.createElement("div"));
899         elem.addClass(jMatrixBrowseNs.Constants.CLASS_BASE+'-row-header-cell');
900         var css = {
901           width: '100%',
902           height: cellElement.height(),
903           top: cellElement.position().top,
904           left: 0,
905           position: 'absolute'
906         };
907         elem.css(css);
908         elem.html(rowHeaders[row]);
909         frag.appendChild(elem[0]);
910       }
911       header.append(frag);
912     }
913 
914     /**
915     * Generates elements and appends them to column header container. 
916     * @param {jQuery object} header - column header container.
917     */
918     function generateColHeaders(header) {
919       var colHeaders = _api.getColHeadersFromLeftCol(_self.currentCell.col-_configuration.getNumberOfBackgroundCells());
920       var frag = document.createDocumentFragment();
921       for (var col = 0, nCols = colHeaders.length; col < nCols; ++col) {
922         var cellElement = jQuery(_cellElements[0][col]);
923         var elem = jQuery(document.createElement("div"));
924         elem.addClass(jMatrixBrowseNs.Constants.CLASS_BASE+'-col-header-cell');
925         var css = {
926           width: cellElement.width(),
927           height: '100%',
928           left: cellElement.position().left,
929           top: 0,
930           position: 'absolute'
931         };
932         elem.css(css);
933         elem.html(colHeaders[col]);
934         frag.appendChild(elem[0]);
935       }
936       header.append(frag);
937     }
938 
939     //TODO: Might not work when more than one jMatrixBrowse on the same page. 
940     /**
941       * Get the cell position for cell at (row,col).
942       * @param {Number} row - row index of the cell.
943       * @param {Number} col - column index of the cell.
944       * @returns {Object} position - position of the cell. 
945       * @returns {Number} position.top - top coordinate of the cell. 
946       * @returns {Number} position.left - left coordinate of the cell. 
947       */
948     function getCellPosition(row, col) {
949       return jQuery('.' + generateClassNameForCell(row,col)).position();
950     }
951 
952     /**
953       * Scroll to given position. 
954       * @param {Number} row - row index of the cell.
955       * @param {Number} col - column index of the cell.
956       */
957     function scrollTo (row, col) {
958       _cellPosition = getCellPosition(row, col);
959       _self.currentCell = {
960         row: row,
961         col: col
962       };
963     };
964 
965     /**
966     * Checks if the bounds for scrolling matrix are valid.
967     * @param  {string} direction direction of scroll
968     * @return {boolean} true if the bounds are valid. false otherwise.
969     */
970     function checkScrollBounds(direction) {
971       var size = _api.getMatrixSize();
972 
973       if (direction === 'up' && _self.currentCell.row < 1) {
974         return false;
975       }
976       if (direction === 'down' && _self.currentCell.row - _configuration.getNumberOfBackgroundCells() + _cellElements.length - 1 > size.height - 1) {
977         return false;
978       }
979       if (direction === 'right' && _self.currentCell.col - _configuration.getNumberOfBackgroundCells() + _cellElements[0].length - 1 > size.width - 1) {
980         return false;
981       }
982       if (direction === 'left' && _self.currentCell.col < 1) {
983         return false;
984       }
985       return true;
986     }
987 
988     /**
989     * Finds the cell that is closest to the top left boundaries.
990     * Checks only a few cells on the top.
991     * @returns {jQuery Object} cell that is closest to the top left boundary.
992     */
993     function getCellToSnap() {
994       // Define the number of rows an columns to check starting from the top corner. 
995       var numberOfRowsToCheck = 2 * _configuration.getNumberOfBackgroundCells() + 1;
996       var numberOfColsToCheck = 2 * _configuration.getNumberOfBackgroundCells() + 1;
997       // Store the cells and distances that we check.
998       var cells = [];
999       var distances = [];
1000       // Compute container offset to find distances from cells.
1001       var containerOffset = _container.offset();
1002       for (var i = 0; i < numberOfRowsToCheck && i < _cellElements.length; ++i) {
1003           for (var j = 0; j < numberOfColsToCheck && j < _cellElements[i].length; ++j) {
1004               var offset = jQuery(_cellElements[i][j]).offset();
1005               distances.push((offset.top - containerOffset.top)*(offset.top - containerOffset.top) + (offset.left - containerOffset.left)*(offset.left - containerOffset.left)); // Squared distance of the cell's top,left with container's top, left.
1006               cells.push(jQuery(_cellElements[i][j]));
1007           }
1008       }
1009       return cells[jMatrixBrowseNs.Utils.findIndexOfMin(distances).minIndex];
1010     }
1011     
1012     /**
1013      * Gets the number of rows that can be scrolled for a page up/down event without violating the matrix bounds.
1014      * @param  {string} direction the direction of the scroll.
1015      * @return {Number} the number of rows that can be safely scrolled.
1016      */
1017     function getNumberOfRowsForPageScroll(direction) {
1018       var height = _configuration.getWindowSize().height;
1019       if (direction === 'up') {
1020         var newTopRow = _self.currentCell.row - height;
1021         if (newTopRow < 1) {
1022           // The scroll exceeds bounds.
1023           return Math.max(0, height + newTopRow);
1024         }
1025       } else {
1026         var matrixHeight = _api.getMatrixSize().height;
1027         var newBottomRow = _self.currentCell.row + height + _cellElements.length - _configuration.getNumberOfBackgroundCells() - 1;
1028         if (newBottomRow >= matrixHeight-1) {
1029           // The scroll exceeds bounds
1030           return Math.max(0, height - (newBottomRow - matrixHeight));
1031         }
1032       }
1033       return height;
1034     }
1035   
1036     /**
1037      * Gets the number of cols that can be scrolled for a page left/right event without violating the matrix bounds.
1038      * @param  {string} direction the direction of the scroll.
1039      * @return {Number} the number of cols that can be safely scrolled.
1040      */
1041     function getNumberOfColsForPageScroll(direction) {
1042       var width = _configuration.getWindowSize().width;
1043       if (direction === 'left') {
1044         var newLeftCol = _self.currentCell.col - width;
1045         if (newLeftCol < 0) {
1046           // The scroll exceeds bounds.
1047           return Math.max(0, width + newLeftCol);
1048         }
1049       } else {
1050         var matrixWidth = _api.getMatrixSize().width;
1051         var newRightCol = _self.currentCell.col + width + _cellElements[0].length - _configuration.getNumberOfBackgroundCells() - 1;
1052         if (newRightCol >= matrixWidth) {
1053           // The scroll exceeds bounds
1054           return Math.max(0, width - (newRightCol - matrixWidth));
1055         }
1056       }
1057       return width;
1058     }
1059 
1060     /**
1061      * Scrolls the matrix nRows row in the given direction.
1062      * @param {string} direction - the direction to scroll.
1063      * @param {Number} nRows - number of rows to scroll.
1064      */
1065     function scrollRows(direction, nRows) {
1066       // Dont't scroll if no rows to scroll.
1067       if (nRows === 0)
1068         return;
1069 
1070       var previousCell = jQuery.extend({}, _self.currentCell); // Clone currentCell
1071       
1072       if (direction === 'up') {
1073         for(var i = 0; i < nRows; ++i) {
1074           // Move bottommost row to the top
1075           var row = _cellElements.length-1;
1076           that.moveRowToTop(row);
1077           --_self.currentCell.row;
1078         }
1079       } else {
1080         for(var i = 0; i < nRows; ++i) {
1081           // Move topmost row to the bottom
1082           row = 0;
1083           that.moveRowToEnd(row);
1084           ++_self.currentCell.row;
1085           
1086         }
1087       }
1088       
1089       // Reposition cells to move them nRows cells up/down
1090       for (var i = 0, h = _cellElements.length; i < h; ++i) {
1091         for (var j = 0, w = _cellElements[i].length; j < w; ++j) {
1092           var cell = jQuery(_cellElements[i][j]);
1093           cell.css({
1094             top: cell.position().top + (direction==='up'?nRows:-nRows)*cell.height()
1095           });
1096         }
1097       }
1098   
1099       var currentCell = _self.currentCell;
1100         
1101       // Reposition row headers.
1102       _headers.row.children().each(function(index, element) {
1103         var containerOffset = _container.offset();
1104         var elementOffset = jQuery(_cellElements[index][0]).offset();
1105         var top = elementOffset.top - containerOffset.top;
1106 
1107         jQuery(element).css({
1108           top: top
1109         });
1110       });
1111         
1112       // Set direction of overflow
1113       if (direction === 'down') {
1114         direction = 'top'; 
1115       } else {
1116         direction = 'bottom';
1117       }
1118         
1119       // Trigger event for change
1120       _elem.trigger({
1121         type: 'jMatrixBrowseChange',
1122         previousCell: previousCell,
1123         currentCell: currentCell,
1124         direction: direction
1125       });
1126     };
1127     
1128     /**
1129      * Scrolls the matrix nCols columns in the given direction.
1130      * @param {string} direction - the direction to scroll.
1131      * @param {Number} nCols - number of cols to scroll.
1132      */
1133     function scrollCols(direction, nCols) {
1134       // Dont't scroll if no columns to scroll. 
1135       if (nCols === 0)
1136         return;
1137       var previousCell = jQuery.extend({}, _self.currentCell); // Clone currentCell
1138       
1139       if (direction === 'left') {
1140         for(var i = 0; i < nCols; ++i) {
1141           // Move rightmost column to the left
1142           var col = _cellElements[0].length-1;
1143           that.moveColToLeft(col);
1144           --_self.currentCell.col;
1145         }
1146       } else {
1147         for(var i = 0; i < nCols; ++i) {
1148           // Move rightmost col to the left
1149           col = 0;
1150           that.moveColToRight(col);
1151           ++_self.currentCell.col;
1152         }
1153       }
1154       
1155       // Reposition cells to move them nCols cells left/right
1156       for (var i = 0, h = _cellElements.length; i < h; ++i) {
1157         for (var j = 0, w = _cellElements[i].length; j < w; ++j) {
1158           var cell = jQuery(_cellElements[i][j]);
1159           cell.css({
1160             left: cell.position().left + (direction==='left'?nCols:-nCols)*cell.width()
1161           });
1162         }
1163       }
1164   
1165       var currentCell = _self.currentCell;
1166         
1167       // Reposition column headers.
1168       _headers.col.children().each(function(index, element) {
1169         var containerOffset = _container.offset();
1170         var elementOffset = jQuery(_cellElements[0][index]).offset();
1171         var left = elementOffset.left - containerOffset.left;
1172         jQuery(element).css({
1173           left: left
1174         });
1175       });
1176         
1177       // Set direction of overflow
1178       if (direction === 'left') {
1179         direction = 'right'; 
1180       } else {
1181         direction = 'left';
1182       }
1183         
1184       // Trigger event for change
1185       _elem.trigger({
1186         type: 'jMatrixBrowseChange',
1187         previousCell: previousCell,
1188         currentCell: currentCell,
1189         direction: direction
1190       });
1191     };
1192 
1193     function addSpinners(rowIndex, colIndex) {
1194       for (var i = rowIndex.row1; i <= rowIndex.row2; ++i) {
1195         for (var j = colIndex.col1; j <= colIndex.col2; ++j) {
1196           jQuery(_cellElements[i][j]).html('<div class="jMatrixBrowse-loading"/>');
1197         }
1198       }
1199     }
1200     
1201     return that;
1202   };
1203   
1204 })(jQuery, jMatrixBrowseNs);
1205