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