define( ['aloha/jquery', 'table/table-plugin-utils'], function (jQuery, Utils) { /** * Constructs a TableCell. * * @param {DomNode} cell * A td/th which will be represente by this TableCell. * @param {Table} tableObj * The Table which contains the cell. The cell will be * activated/dactivated with the table. */ var TableCell = function(originalTd, tableObj) { if (null == originalTd) { originalTd = ' '; } //original Td must be a DOM node so that the this.obj.context property is available //this transformation will properly handle jQuery objects as well as DOM nodes originalTd = jQuery( originalTd ).get( 0 ); this.obj = jQuery(originalTd); this.tableObj = tableObj; tableObj.cells.push(this); }; /** * Reference to the jQuery-representation of the wrapping table * * @see TableCell.table */ TableCell.prototype.tableObj = undefined; /** * Reference to the jQuery td-Object of the cell */ TableCell.prototype.obj = undefined; /** * The jQuery wrapper of the cell */ TableCell.prototype.wrapper = undefined; /** * Flag if the cell has focus */ TableCell.prototype.hasFocus = false; TableCell.prototype.activate = function () { // wrap the created div into the contents of the cell this.obj.wrapInner( '
' ); // create the editable wrapper for the cells var wrapper = this.obj.children( 'div' ).eq( 0 ); wrapper.contentEditable( true ); wrapper.addClass( 'aloha-table-cell-editable' ); var that = this; // attach events to the editable div-object wrapper.bind( 'focus', function ( jqEvent ) { // ugly workaround for ext-js-adapter problem in ext-jquery-adapter-debug.js:1020 if ( jqEvent.currentTarget ) { jqEvent.currentTarget.indexOf = function () { return -1; }; } that._editableFocus( jqEvent ); } ); wrapper.bind( 'mousedown', function ( jqEvent ) { // ugly workaround for ext-js-adapter problem in ext-jquery-adapter-debug.js:1020 if ( jqEvent.currentTarget ) { jqEvent.currentTarget.indexOf = function () { return -1; }; } that._editableMouseDown( jqEvent ); that._startCellSelection(); } ); wrapper.bind( 'blur', function ( jqEvent ) { that._editableBlur( jqEvent ); }); wrapper.bind( 'keyup', function ( jqEvent ) { that._editableKeyUp( jqEvent ); }); wrapper.bind( 'keydown', function ( jqEvent ) { that._editableKeyDown( jqEvent ); }); wrapper.bind( 'mouseover', function ( jqEvent ) { that._selectCellRange(); }); // we will treat the wrapper just like an editable wrapper.contentEditableSelectionChange( function ( event ) { Aloha.Selection.onChange( wrapper, event ); return wrapper; } ); this.obj.bind( 'mousedown', function ( jqEvent ) { setTimeout( function () { that.wrapper.trigger( 'focus' ); }, 1 ); that.tableObj.selection.unselectCells(); that._startCellSelection(); jqEvent.stopPropagation(); } ); if ( this.obj.get( 0 ) ) { this.obj.get( 0 ).onselectstart = function ( jqEvent ) { return false; }; } // set contenteditable wrapper div this.wrapper = this.obj.children(); if ( this.wrapper.get( 0 ) ) { this.wrapper.get( 0 ).onselectstart = function () { window.event.cancelBubble = true; }; // Disabled the dragging of content, since it makes cell selection difficult this.wrapper.get( 0 ).ondragstart = function () { return false }; } return this; }; /** * The deactivate method removes the contenteditable helper div within the * table-data field and wraps the innerHtml to the outerHTML * * @return void */ TableCell.prototype.deactivate = function() { var wrapper = jQuery(this.obj.children('.aloha-table-cell-editable')); if (wrapper.length) { // unwrap cell contents without re-creating dom nodes wrapper.parent().append( wrapper.contents() ); // remove the contenteditable div and its attached events wrapper.remove(); // remove the click event of the this.obj.unbind('click'); if (jQuery.trim(this.obj.attr('class')) == '') { this.obj.removeAttr('class'); } } } /** * Native toString-method * * @return string name of the namespace */ TableCell.prototype.toString = function() { return 'TableCell'; }; /** * Focus method for the contentediable div within a table data-field. The method * requires the event-property Cell as a Cell object. If the * Cell wasn't activated yet it does all relevant actions to activate the cell. * * @param e * the jquery event object * @return void */ TableCell.prototype._editableFocus = function(e) { // only do activation stuff if the cell don't has the focus if (!this.hasFocus) { // set an internal flag to focus the table this.tableObj.focus(); // add an active-class this.obj.addClass('aloha-table-cell_active'); // set the focus flag this.hasFocus = true; // select the whole content in the table-data field this._selectAll(this.wrapper.get(0)); // unset the selection type this.tableObj.selection.selectionType = 'cell'; } }; /** * Blur event for the contenteditable div within a table-data field. The method * requires the event-property TableCell as a TableCell object. It * sets the hasFocus flag of the cell to false and removes the "active" * css-class. * * @param jqEvent * the jquery event object * @return void */ TableCell.prototype._editableBlur = function(jqEvent){ // reset the focus of the cell this.hasFocus = false; // remove "active class" this.obj.removeClass('aloha-table-cell_active'); }; /** * Gives the X (column no) for a cell, after adding colspans */ TableCell.prototype._virtualX = function(){ var $rows = this.tableObj.obj.children().children('tr'); var rowIdx = this.obj.parent().index(); var colIdx = this.obj.index(); return Utils.cellIndexToGridColumn($rows, rowIdx, colIdx); }; /** * Gives the Y (row no) for a cell, after adding colspans */ TableCell.prototype._virtualY = function(){ return this.obj.parent('tr').index(); }; /** * Starts the cell selection mode */ TableCell.prototype._startCellSelection = function(){ if(!this.tableObj.selection.cellSelectionMode){ //unselect currently selected cells this.tableObj.selection.unselectCells(); // activate cell selection mode this.tableObj.selection.cellSelectionMode = true; //bind a global mouseup event handler to stop cell selection var that = this; jQuery('body').bind('mouseup.cellselection', function(){ that._endCellSelection(); }); this.tableObj.selection.baseCellPosition = [this._virtualY(), this._virtualX()]; } }; /** * Ends the cell selection mode */ TableCell.prototype._endCellSelection = function(){ if(this.tableObj.selection.cellSelectionMode){ this.tableObj.selection.cellSelectionMode = false; this.tableObj.selection.baseCellPosition = null; this.tableObj.selection.lastSelectionRange = null; this.tableObj.selection.selectionType = 'cell'; //unbind the global cell selection event jQuery('body').unbind('mouseup.cellselection'); } }; TableCell.prototype._getSelectedRect = function () { var right = this._virtualX(); var bottom = this._virtualY(); var topLeft = this.tableObj.selection.baseCellPosition; var left = topLeft[1]; if (left > right) { left = right; right = topLeft[1]; } var top = topLeft[0]; if (top > bottom) { top = bottom; bottom = topLeft[0]; } return {"top": top, "right": right, "bottom": bottom, "left": left}; }; /** * Toggles selection of cell. * This works only when cell selection mode is active. */ TableCell.prototype._selectCellRange = function(){ if(!this.tableObj.selection.cellSelectionMode) { return; } var rect = this._getSelectedRect(); var table = this.tableObj; var $rows = table.obj.children().children('tr'); var grid = Utils.makeGrid($rows); table.selection.selectedCells = []; var selectClass = table.get('classCellSelected'); Utils.walkGrid(grid, function (cellInfo, j, i) { if ( Utils.containsDomCell(cellInfo) ) { if (i >= rect.top && i <= rect.bottom && j >= rect.left && j <= rect.right) { jQuery( cellInfo.cell ).addClass(selectClass); table.selection.selectedCells.push(cellInfo.cell); } else { jQuery( cellInfo.cell ).removeClass(selectClass); } } }); table.selection.notifyCellsSelected(); }; /** * Selects all inner-contens of an contentEditable-object * * @param editableNode dom-representation of the editable node (div-element) * @return void */ TableCell.prototype._selectAll = function(editableNode) { var e = (editableNode.jquery) ? editableNode.get(0) : editableNode; // Not IE if (!jQuery.browser.msie) { var s = window.getSelection(); // WebKit if ( s.setBaseAndExtent /*&& e> 0 */ ) { s.setBaseAndExtent( e, 0, e, Math.max( 0, e.innerText.length - 1 ) ); } // Firefox and Opera else { // workaround for bug # 42885 if (window.opera && e.innerHTML.substring(e.innerHTML.length - 4) == '
') { e.innerHTML = e.innerHTML + ' '; } var r = document.createRange(); r.selectNodeContents(e); s.removeAllRanges(); s.addRange(r); } } // Some older browsers else if (document.getSelection) { var s = document.getSelection(); var r = document.createRange(); r.selectNodeContents(e); s.removeAllRanges(); s.addRange(r); } // IE else if (document.selection) { var r = document.body.createTextRange(); r.moveToElementText(e); r.select(); } }; /** * The mouse-down event for the editable-div in the thd-field. Unselect all * cells when clicking on the editable-div. * * @param jqEvent * the jquery-event object * @return void */ TableCell.prototype._editableMouseDown = function(jqEvent) { // deselect all highlighted cells registered in the this.tableObj.selection object this.tableObj.selection.unselectCells(); if (this.tableObj.hasFocus) { jqEvent.stopPropagation(); } }; /** * The key-up event for the editable-div in the td-field. Just check if the div * is empty and insert an   * * @param jqEvent * the jquery-event object * @return void */ TableCell.prototype._editableKeyUp = function( jqEvent ) { //TODO do we need to check for empty cells and insert a space? //this._checkForEmptyEvent(jqEvent); }; /** * The key-down event for the ediable-div in the td-field. Check if the the div * is empty and insert an  . Furthermore if cells are selected, unselect * them. * * @param jqEvent * the jquery-event object * @return void */ TableCell.prototype._editableKeyDown = function(jqEvent) { var KEYCODE_TAB = 9; this._checkForEmptyEvent(jqEvent); if ( this.obj[0] === this.tableObj.obj.find('tr:last td:last')[0] ) { // only add a row on a single key-press of tab (so check // that alt-, shift- or ctrl-key are NOT pressed) if (KEYCODE_TAB == jqEvent.keyCode && !jqEvent.altKey && !jqEvent.shiftKey && !jqEvent.ctrlKey) { // add a row after the current row this.tableObj.addRow(this.obj.parent().index() + 1); // firefox needs this for the first cell of the new row // to be selected (.focus() doesn't work reliably in // IE7) this.tableObj.cells[this.tableObj.cells.length - 1]._selectAll(this.wrapper.get(0)); jqEvent.stopPropagation(); return; } } }; /** * The custom keyup event for a table-cell Checks if the cell is empty and * inserts a space (\u00a0) * * @param e * the event object which is given by jquery * @return void */ TableCell.prototype._checkForEmptyEvent = function(jqEvent) { var $wrapper = jQuery(this.wrapper), text = $wrapper.text(); if ( $wrapper.children().length > 0) { return; } // if empty insert a blank space and blur and focus the wrapper if ( text === '' ){ this.wrapper.text('\u00a0'); this.wrapper.get(0).blur(); this.wrapper.get(0).focus(); } }; /** * Given a cell, will return the container element of the contents * of the cell. The container element may be the given cell itself, * or a wrapper element, in the case of activated cells. * * @param {DomNode} cell * the TH/TD of a TableCell that may or may not be actived. * @return {DomNode} * the element that contains the contents of the given cell. */ TableCell.getContainer = function ( cell ) { if ( jQuery( cell.firstChild ).hasClass( "aloha-table-cell-editable" ) ) { return cell.firstChild; } else { return cell; } }; return TableCell; });