define( ['aloha', 'aloha/jquery', 'table/table-plugin-utils', 'table/table-cell', 'i18n!table/nls/i18n'], function (Aloha, $, Utils, TableCell, i18n) { /** * The TableSelection object is a helper-object */ var TableSelection = function (table) { this.table = table; }; /** * Gives the type of the cell-selection * possible values are "row" or "col" * also possible value is 'cell', which defines custom cell selections */ TableSelection.prototype.selectionType = undefined; /** * Holds all currently selected table cells as an array of DOM "td" representations */ TableSelection.prototype.selectedCells = new Array(); /** * Holds all table columnIdx if selectiontype is column */ TableSelection.prototype.selectedColumnIdxs = new Array(); /** * Holds all table rowIds if selectiontype is column */ TableSelection.prototype.selectedRowIdxs = new Array(); /** * Holds the active/disabled state of cell selection mode */ TableSelection.prototype.cellSelectionMode = false; /** * Gives the position of the base cell of a selection - [row, column] */ TableSelection.prototype.baseCellPosition = null; /** * Gives the range of last cell selection - [row, column] */ TableSelection.prototype.lastSelectionRange = null; /** * Marks all cells of the specified column or columns as selected * * @return void */ TableSelection.prototype.selectColumns = function ( columnsToSelect ) { this.unselectCells(); var rows = this.table.getRows(); // first row is the selection row (dump it, it's not needed) rows.shift(); var grid = Utils.makeGrid(rows); for (var j = 0; j < columnsToSelect.length; j++) { // check if this column is already selected. if ( -1 !== $.inArray(columnsToSelect[j], this.selectedColumnIdxs) ) { continue; } this.selectedColumnIdxs.push( columnsToSelect[j] ); for (var i = 0; i < grid.length; i++) { var cellInfo = grid[i][columnsToSelect[j]]; if ( Utils.containsDomCell(cellInfo) ) { $(cellInfo.cell).addClass(this.table.get('classCellSelected')); this.selectedCells.push( cellInfo.cell ); } } } this.selectionType = 'column'; }; /** * Marks all cells of the specified row or rows as selected * * @return void */ TableSelection.prototype.selectRows = function ( rowsToSelect ) { this.unselectCells(); var rows = this.table.getRows(); rowsToSelect.sort( function ( a, b ) { return a - b; } ); for (var i = 0; i < rowsToSelect.length; i++) { if ( rows[ rowsToSelect[i] ] ) { // check if this row is already selected. for ( var z = 0; z < this.selectedRowIdxs.length; z++ ) { if ( rowsToSelect[i] == this.selectedRowIdxs[z] ) { return; } } this.selectedRowIdxs.push( rowsToSelect[i] ); // to not select first cell, which is a control cell for ( var j = 1; j < rows[ rowsToSelect[i] ].cells.length; j++ ) { this.selectedCells.push( rows[ rowsToSelect[i] ].cells[j] ); // TODO make proper cell selection method $( rows[ rowsToSelect[i] ].cells[j] ).addClass( this.table.get('classCellSelected') ); } } } this.selectionType = 'row'; }; TableSelection.prototype.selectAll = function () { var rowIndices = $.map( this.table.getRows(), function ( item, i ) { return i; }); //getRows() returns all rows, even the header row which we must not select rowIndices.shift(); this.selectRows( rowIndices ); }; /** * To be called when cells of the table were selected * @see selectRows, selectColumns, selectCellRange * TODO this should be private */ TableSelection.prototype.notifyCellsSelected = function () { Aloha.trigger( 'aloha-table-selection-changed' ); // the UI feels more consisten when we remove the non-table // selection when cells are selected // TODO this code doesn't work right in IE as it causes the table // scope of the floating menu to be lost. Maybe this can be // handled by testing for an empty selection in the // aloha-selection-changed event. //Aloha.getSelection().removeAllRanges(); }; /** * To be called when a cell-selection is entirely removed * @see unselectCells */ TableSelection.prototype._notifyCellsUnselected = function () { Aloha.trigger( 'aloha-table-selection-changed' ); }; /** * This method return true if all sellected cells are TH cells. * * @return boolean */ TableSelection.prototype.isHeader = function ( ) { if ( this.selectedCells.length == 0 ) { return false; } // take 1 column to detect if the header button is pressd for (var i = 0; i < this.selectedCells.length; i++) { if ( !this.selectedCells[i] || this.selectedCells[i].nodeName.toLowerCase() != 'th' ) { return false; } } return true; } /** * This method removes the "selected" class from all selected cells * * @return void */ TableSelection.prototype.unselectCells = function(){ var rows; //don't unselect cells if cellSelectionMode is active if ( this.cellSelectionMode ) { return; } if (this.selectedCells.length > 0) { rows = this.table.getRows(); for (var i = 0; i < rows.length; i++) { for ( var j = 1; j < rows[i].cells.length; j++ ) { // TODO make proper cell selection method $( rows[i].cells[j] ).removeClass( this.table.get('classCellSelected') ); } } this.selectedCells = new Array(); this.selectedColumnIdxs = new Array(); this.selectedRowIdxs = new Array(); //we keep 'cell' as the default selection type instead of //unsetting the selectionType to avoid an edge-case where a //click into a cell doesn't trigger a call to //TableCell.editableFocs (which would set the 'cell' //selection type) which would result in the FloatingMenu //losing the table scope. this.selectionType = 'cell'; this._notifyCellsUnselected(); } }; /** * Returns the index of a given cell, in selectedCells * returns -1 if the given cell is not in selectedCells * @params cell * DOMElement * * @return integer */ TableSelection.prototype.selectionIndex = function(cell){ for(var i = 0; i < this.selectedCells.length; i++){ if(this.selectedCells[i] === cell){ return i; } } return -1; }; /** * Given a contour creates a object representing a rectangle. * This function only gives a useful return value if the given * contour rectangular. * * @param {object} contour * a rectangular contour * @return {object} * an object with the properties top, right, bottom, left, * representing the rectangular contour. */ function getRectFromContour( contour ) { return { 'top' : contour.top[0], 'right' : contour.right[0] + 1, 'bottom': contour.bottom[0] + 1, 'left' : contour.left[0] }; } /** * Given a grid and contour, determines whether the contour is * rectangular, and each cell in the rectangle is selected. * * @param {array} grid * a two-dimensional array representing a grid see Utils.makeGrid * @param {object} contour * an object reprensenting a contour see Utils.makeContour * @param {function} isSelected * a function that determines whether a cell in the given grid * is selected for merging. * @return {boolean} * true if all cells inside the contour are selected and can * be merged. */ function isMergeable(grid, contour, isSelected) { var mergeable = true; if ( -1 !== Utils.indexOfAnyBut( contour.top , contour.top[0] ) || -1 !== Utils.indexOfAnyBut( contour.right , contour.right[0] ) || -1 !== Utils.indexOfAnyBut( contour.bottom, contour.bottom[0] ) || -1 !== Utils.indexOfAnyBut( contour.left , contour.left[0] ) ) { // the outside of the selected area is jagged (not a rectangle) mergeable = false; } else { // the outside of the selected area is a rectangle, but we // must also ensore that there are no holes in the selection var rect = getRectFromContour( contour ) Utils.walkGridInsideRect( grid, rect, function ( cellInfo ) { if ( ! isSelected( cellInfo ) ) { mergeable = false; return false; } }); } return mergeable; } /** * This method merges all selected cells * * @return void */ TableSelection.prototype.mergeCells = function(){ var selectedCells = this.selectedCells; if ( 0 === selectedCells.length ) { return; } var isSelected = function ( cellInfo ) { return -1 != $.inArray( cellInfo.cell, selectedCells ); }; var grid = Utils.makeGrid( this.table.getRows() ); var contour = Utils.makeContour( grid, isSelected ); if ( ! isMergeable( grid, contour, isSelected ) ) { Aloha.showMessage(new Aloha.Message({ title : i18n.t('Table'), text : i18n.t('table.mergeCells.notRectangular'), type : Aloha.Message.Type.ALERT })); return; } var selectedRect = getRectFromContour( contour ); var $firstCell = $( grid[ selectedRect.top ][ selectedRect.left ].cell ); var $firstContainer = $( TableCell.getContainer( $firstCell.get( 0 ) ) ); Utils.walkGridInsideRect( grid, selectedRect, function ( cellInfo, x, y ) { if ( x - cellInfo.spannedX === selectedRect.left && y - cellInfo.spannedY === selectedRect.top ) { return; } var cell = cellInfo.cell; var contents = $( TableCell.getContainer( cell ) ).contents(); // only append the delimiting space if there is some non-whitespace for ( var i = 0; i < contents.length; i++ ) { if ( "string" !== typeof contents[i] || "" !== $.trim( contents[i] ) ) { $firstContainer.append( " " ); $firstContainer.append( contents ); break; } } $( cell ).remove(); }); $firstCell.attr({ 'rowspan': selectedRect.bottom - selectedRect.top, 'colspan': selectedRect.right - selectedRect.left }); //select the merged cell this.selectedCells = [ $firstCell.get( 0 ) ]; //reset flags this.cellSelectionMode = false; this.baseCellPosition = null; this.lastSelectionRange = null; this.selectionType = 'cell'; Aloha.trigger( 'aloha-table-selection-changed' ); }; /** * This method splits all selected cells (if they are already have row or column spans) * * @return void */ TableSelection.prototype.splitCells = function(){ var selection = this; // split the selected cells or currently active cell var cells_to_split = this.selectedCells; if (cells_to_split.length > 0) { $(cells_to_split).each(function(){ Utils.splitCell(this, function () { return selection.table.newActiveCell().obj; }); }); //reset flags this.cellSelectionMode = false; this.baseCellPosition = null; this.lastSelectionRange = null; this.selectionType = 'cell'; Aloha.trigger( 'aloha-table-selection-changed' ); } }; /** * This method checks if the current selection of cells is merge able * * @return {boolean} * true if more than one cell is selected. */ TableSelection.prototype.cellsAreMergeable = function() { var selectedCells = this.selectedCells; if ( selectedCells.length < 2 ) { return false; } var isSelected = function ( cellInfo ) { return -1 != $.inArray( cellInfo.cell, selectedCells ); }; var grid = Utils.makeGrid( this.table.getRows() ); var contour = Utils.makeContour( grid, isSelected ); if ( ! isMergeable( grid, contour, isSelected ) ) { return false; } else { return true; } }; /** * This method checks if the current selection of cells is split able * * @return {boolean} * true if more than one cell is selected. */ TableSelection.prototype.cellsAreSplitable = function() { var splitable = 0; if ( this.selectedCells.length > 0 ) { $(this.selectedCells).each(function(){ var cell = this; var colspan = Utils.colspan( cell ); var rowspan = Utils.rowspan( cell ); if ( colspan > 1 || rowspan > 1 ) { splitable++; } }); if ( splitable > 0 ) { return true; } else { return false; } } else { return false; } }; return TableSelection; });