text: 
  WBLOutlineAutoresizingStyle = {
      NoColumnAutoresizing: 0,                          // Disable column autoresizing
      UniformColumnAutoresizingStyle: 1,                // Autoresize all columns by distributing space equally, simultaneously.
      SequentialColumnAutoresizingStyle: 2,             // Autoresize each column sequentially, from the last auto-resizable column to the first; proceed to the next when the current has reached its minimum or maximum size.
      ReverseSequentialColumnAutoresizingStyle: 3,      // Autoresize each column sequentially, from the first auto-resizable column to the last; proceed to the next when the current has reached its minimum or maximum size.
      LastColumnOnlyAutoresizingStyle: 4,               // Autoresize only the last column.
      FirstColumnOnlyAutoresizingStyle: 5               // Autoresize only the first column.
  };
  
  WBLOutlineController = new Class({
  
      Extends: WBLScrollComponentController,
  
      Implements: [Options],
  
      options: {
  
          adjustColumnWidths: false,
          columnAutoresizingStyle:WBLOutlineAutoresizingStyle.NoColumnAutoresizing,
          complexColumnWidthComputation: false, // If set to true, takes all cell content into account
          clientSideCollapsingAndExpanding: false,
          allowsMultipleSelection: true,
  
          // These methods are generated by WBLAjaxActionFunction
          selectionChangedAction: null,
          toggleIsExpandedAction: null,
          collapseSelectionAction: null,
          expandSelectionAction: null,
          doubleClickAction: null,
          rowIndexBasedScrolling: false,
          isBeeingExported: false
      },
  
      debugName: function(){
          return 'Outline';
      },
  
      initialize: function(outlineElement, options){
  
          //        console.log(PWObjectId(this)+' Initialize MEWOutlineController');
          // Create a keyboard. It is not active by default and will be activated after the user has clicked inside the outline view.
          this.createKeyboard();
          this.classOfSelectedRows = 'selected';
          this.classOfCollapsedRows ='isCollapsed';
  
          this.outlineElement = outlineElement;
          this.parent(outlineElement, options)
  
  //        console.log('this.options.isBeeingExported: '+this.options.isBeeingExported);
      },
  
      createMeasurementCanvas: function(){
          if(!this.measurementCanvas){
              this.measurementCanvas = new Element('canvas', {
                      styles: {
                          width:'0px',
                          height:'0px',
                          visibility: 'hidden',
                          position: 'absolute',
                      }
              });
          }
      },
  
      setupAfterContentDocumentIsReady: function(){
  
          this.parent();
  
          this.initializeInvariants();
  
          // The element used to measure widths can be inside an iFrame.
          // Because iFrames are loaded asynchronosly, we have to wait until it is loaded.
          this.createMeasurementCanvas();
  
          if(this.options.adjustColumnWidths)
              this.adjustColumnWidths();
  
          // Text in elements with a data-all-formatted-values attribute is made invisible if the cell contains an image and its size is below 50px.
          this.updateCellsWithLongText();
  
          // Detect the scroll element and add an observer to update autoformatted cells on scroll.
          this.updateScrollElementObserver();
  
          this.outlineElement.scrollViewController.updateLayout();
  
          this.flatRows = this.rows();
  
          if(this.updatesAutoformattedCells)
              this.updateAutoformattedCellsCache();
  
          this.makeTableColumnsResizable();
          this.makeTableRowsSelectable();
  
          if(this.options.clientSideCollapsingAndExpanding){
              this.prepareRowsForClientSideCollapsingAndExpanding(this.flatRows);
              this.updateRowsVisibility(this.flatRows);
          }
  
          this.prepareDisclosureButtons();
  
          // Detect clicks inside the outline view and set the first responder
          this.updateClickObserver();
  
      },
  
      updatesAutoformattedCells: true, // TODO: During debugging set to false
  
      autoformattedCellsFromRows: function(rows){
  
      },
  
      updateClickObserver: function(){
          var clickElement = this.iFrame ? this.iFrame.contentDocument.defaultView : this.scrollElement();
          var me = this;
          clickElement.addEvent('click', function(event){
                me.clickedInsideOutline(event);
             });
      },
  
      clickedInsideOutline: function(event){
           //console.log('clicked inside outline'); // TODO: Set first responder
      },
  
      dispose: function() {
  
          this.parent();
  
          //console.log('dispose MEWOutlineController');
          this.iFrame = this.outlineElement.getElement('iframe');
          if(this.iFrame){
              if(this.iFrame.contentDocument)
                  this.iFrame.contentDocument.defaultView.removeEvents('scroll');
          }else
              this.scrollElement().removeEvents('scroll');
  
          this.disposeKeyboard();
          if(this.options.selectionChangedAction){
              if(window.MooDropMenu)
                  window.parent.eventBus().removeObserver(MooDropMenu.WillShowMenuNotification, this.sendSelectionToServerIfNeeded, this);
              this.disposeSelectionToServerTimer();
          }
      },
  
      // Creates this.iFrame, this.scrollElement and this.table. Takes into account iFrames.
      initializeInvariants: function(){
          this.iFrame = this.outlineElement.getElement('iframe');
          if(this.iFrame){
              var iFrameBody = this.iFrame.contentDocument.body;
              this.table = iFrameBody.getElement('table');
              this.tableWindow = this.iFrame.contentWindow;
              this.scrollEventSource = this.iFrame.contentDocument.defaultView;
  
              // At this point we need to make sure we have a valid layout. So we need to remove the nolayout optimization
              iFrameBody.getElements('.nolayout').each(function(element){
                  element.removeClass('nolayout');
              });
          }else{
              var tables = this.outlineElement.getElements('table');
              this.table = tables[1]; // 1 because of the header which is also a table
              // For debugging purposes only we fallback to the first table if no other is found
              if(!this.table)
                  this.table = tables[0];
  
              this.tableWindow = window;
              this.scrollEventSource = this.scrollElement();
          }
          this.headerTable = this.outlineElement.getElement('.header');
          this.columns = this.table.getElements('col');
      },
  
      windowKeyboard: function(){
          if (!window.keyboard) {
              window.keyboard = new Keyboard();
              window.keyboard.activate();
          }
          return window.keyboard;
      },
  
      createKeyboard: function(){
          var keyboard = new Keyboard();
          this.windowKeyboard().manage(keyboard);
          var me = this;
          keyboard.addEvents({
              'up':function(e){me.upKeyPressed(e);},
              'shift+up':function(e){me.upKeyPressed(e);},
              'down':function(e){me.downKeyPressed(e);},
              'shift+down':function(e){me.downKeyPressed(e);},
              'left':function(e){me.leftKeyPressed(e);},
              'right':function(e){me.rightKeyPressed(e);},
              'enter':function(e){me.enterKeyPressed(e);},
              'enter+meta':function(e){me.enterKeyPressed(e);},
              'a+meta':function(e){me.selectAllRowsIfPossible(e);},
          });
          this.keyboard = keyboard;
      },
  
      disposeKeyboard: function(){
          var keyboard = this.keyboard;
          keyboard.relinquish();
          keyboard.deactivate();
          window.keyboard.drop(keyboard);
      },
  
      selectAllRowsIfPossible: function(event){
          if(this.options.allowsMultipleSelection){
              this.changedRows = new Array();
              var anchorIndex = null;
              this.rows().each(function(row){
                  if(anchorIndex == null)
                      anchorIndex = row.index;
                  this.addSelectionStyleToRow(row);
              }, this);
              this.setAnchorIndex(anchorIndex);
              this.updateSelectionBorders();
              this.selectionStyleDidChangeInRowsWithIndex(this.changedRows);
          }
          event.stop();
      },
  
      upKeyPressed: function(event){
          this.selectPreviousRow(event);
      },
  
      downKeyPressed: function(event){
          this.selectNextRow(event);
      },
  
      leftKeyPressed: function(event){
          this.collapseSelectedGroups();
      },
  
      rightKeyPressed: function(event){
          this.expandSelectedGroups();
      },
  
      enterKeyPressed: function(event){
      },
  
      collapseSelectedGroups: function(){
          if(this.options.clientSideCollapsingAndExpanding)
              this.collapseRowsOnClientSideOnly(this.collapsibleSelectedRows());
          if(this.options.collapseSelectionAction){
              this.sendSelectionToServerIfNeeded();
              var rowIndices = this.collapsibleSelectedRows().map(function(row){
                  return row.index;
              });
              if(rowIndices.length)
                  this.options.collapseSelectionAction({'rowIndices': JSON.encode(rowIndices)});
          }
      },
  
      expandSelectedGroups: function(){
          if(this.options.clientSideCollapsingAndExpanding)
              this.expandRowsOnClientSideOnly(this.expandableSelectedRows());
          else if(this.options.expandSelectionAction){
              this.sendSelectionToServerIfNeeded();
              var rowIndices = this.expandableSelectedRows().map(function(row){
                  return row.index;
              });
              if(rowIndices.length)
                  this.options.expandSelectionAction({'rowIndices': JSON.encode(rowIndices)});
          }
      },
  
      collapsibleSelectedRows: function() {
          var cssClass = this.classOfCollapsedRows;
          return this.selectedRows.filter(function(row){
              return row.hasClass('isCollapsible') && !row.hasClass(cssClass);
          });
      },
  
      expandableSelectedRows: function() {
          var cssClass = this.classOfCollapsedRows;
          return this.selectedRows.filter(function(row){
              return row.hasClass('isCollapsible') && row.hasClass(cssClass);
          });
      },
  
      prepareRowsForClientSideCollapsingAndExpanding: function(rows){
  
          var stack = [];
          var lastLevel = 0;
          var lastRow = null;
  
          rows.each(function(row){
              var level = row.getProperty('data-level').toInt();
              var index = row.getProperty('data-idx');
              row.level = level;
  
              row.isVisible = true;
  
              if(level > lastLevel)
                  stack.push(lastRow);
              else if(level < lastLevel) {
                  var diff = lastLevel-level;
                  while(diff > 0){
                      stack.pop();
                      diff--;
                  }
              }
              if(level > 0){
                  var parent = stack.getLast();
                  if(!parent.subrows)
                      parent.subrows = [];
                  row.parentRow = parent;
                  parent.subrows.push(row);
              }
              lastRow = row;
              lastLevel = level;
              lastIndex = index;
          });
      },
  
      flatRowsStartingWithRow: function(parentRow){
          var flatRows = [];
          this.addFlatRowsToArray(parentRow, flatRows);
          return flatRows;
      },
  
      addFlatRowsToArray: function(parentRow, array){
          array.push(parentRow);
          if(parentRow.subrows && parentRow.subrows.length > 0) {
              parentRow.subrows.each(function(subrow){
                  this.addFlatRowsToArray(subrow, array);
              }, this);
          }
      },
  
      updateRowsVisibility: function(rows){
  
          var isAnyParentRowCollapsed = function(row){
              var iParent = row;
              do{
                  iParent = iParent.parentRow;
                  if(iParent && iParent.hasClass('isCollapsed'))
                      return true;
              }while(iParent);
              return false;
          }
  
          var classOfSelectedRows = this.classOfSelectedRows;
          var deselectedRows = new Array();
          var lastVisibleRow = null;
          var tableBody = this.table.getElement('tbody');
          rows.each(function(row){
              var parentElement = row.parentElement;
              var isInDOM = !!parentElement;
              var shouldBeVisible = !isAnyParentRowCollapsed(row);
              if(isInDOM && !shouldBeVisible){
                  parentElement.removeChild(row);
                  row.isVisible = false;
                  if(row.hasClass(classOfSelectedRows))
                      deselectedRows.push(row);
              }else if(!isInDOM && shouldBeVisible){
                  if(lastVisibleRow)
                      row.inject(lastVisibleRow, 'after');
                  else
                      tableBody.appendChild(row);
                  row.isVisible = true;
              }
              if(row.isVisible)
                  lastVisibleRow = row;
          }, this);
  
          // Remove invisible rows from the selection
          if(this.selectedRows && deselectedRows.length > 0){
              this.removeRowsFromSelection(deselectedRows);
          }
          this.computeAnchorIndexFromSelection();
          this.updateSelectionBorders();
      },
  
      removeRowsFromSelection: function(rows){
          rows.each(function(row){
              var index = row.index;
              this.removeSelectionStyleFromRow(row);
              this.removeSelectionTopBorderFromRows([row]);
              this.removeSelectionBottomBorderFromRows([row]);
          }, this);
      },
  
      makeTableColumnsResizable: function(){
          var handles = this.headerTable.getElements('.handle');
          var headerTDs = this.headerTable.getElements('td');
          var contentColumns = this.table.getElements('col');
          var dragShield = new PWDragShield('col-resize');
          var me = this;
          this.headerTable.getElements('col').each(function(col, index){
              var contentCol = contentColumns[index];
              var resizing   = col.getAttribute('data-column-resizing');
              if(resizing.toInt() & 2) { // 2 == WBLOutlineColumnUserResizingMask
                  var headerTD = headerTDs[index];
                  var minWidth = headerTD.getStyle('min-width');
                  var maxWidth = headerTD.getStyle('max-width');
                  minWidth = minWidth ? minWidth.toInt() : 30;
                  maxWidth = maxWidth ? maxWidth.toInt() : 10000;
                  col.makeResizable({handle:handles[index],
                                      limit:{x:[minWidth, maxWidth]},
                                      onBeforeStart: function(){
                                          dragShield.show();
                                      },
                                      onDrag: function(el){
                                          me.onColumnResize(el, col, contentCol, index);
                                      },
                                      onStart: function(el){
                                          me.onColumnResizeStart(el, col, contentCol, index);
                                      },
                                      onCancel: function(){
                                          dragShield.hide();
                                      },
                                      onComplete: function(el){
                                          me.onColumnResizeComplete(el, col, contentCol, index);
                                          dragShield.hide();
                                      }});
              } else
                  handles[index].setStyle('cursor','default');
          }, this);
      },
  
      onColumnResizeStart: function(el, headerColumn, contentColumn, index){
          this.registerUserInteraction();
      },
  
      onColumnResize: function(el, headerColumn, contentColumn, index){
          this.registerUserInteraction();
      },
  
      onColumnResizeComplete: function(el, headerColumn, contentColumn, index){
          contentColumn.setStyle('width', el.getStyle('width'));
          this.refreshAutoformattedCellsInColumn(contentColumn);
          this.updateCellsWithLongText();
      },
  
      // Text in elements with a data-all-formatted-values attribute is made invisible if the cell contains an image and its size is below 50px.
      updateCellsWithLongText: function(){
          var spans = this.table.getElements('[data-hide-long-text]');
          spans.each(function(span){
              var parent = span.getParent();
              var cell = parent.getParent();
              var img = cell.getElement('img');
              if(img){
                  var origDisplayStyle = span.origDisplayStyle;
                  if(!origDisplayStyle){
                      origDisplayStyle = span.getStyle('display');
                      span.origDisplayStyle = origDisplayStyle;
                  }
                  var col = this.columnForCell(cell);
                  var width = this.availableTextWidthInCell(cell, parent);
                  var isToSmall = width < 55;
                  span.setStyle('display', isToSmall ? 'none' : origDisplayStyle);
                  //parent.setStyle('text-align', isToSmall ? 'center' : null);
                  img.setStyle('position', isToSmall ? 'relative' : null);
                  img.setStyle('left', isToSmall ? '50%' : null);
                  img.setStyle('transform', isToSmall ? 'translateX(-50%)' : null);
                  img.setStyle('-webkit-transform', isToSmall ? 'translateX(-50%)' : null);
              }
          }, this);
      },
  
      columnForCell: function(cell){
          return this.columns[cell.getProperty('data-idx').toInt()];
      },
  
      // Detect the scroll element and update autoformatted cells on scroll.
      updateScrollElementObserver: function() {
          var me = this;
          this.iFrame = this.outlineElement.getElement('iframe');
          if(this.iFrame)
              this.iFrame.contentDocument.defaultView.addEvent('scroll', function(){
                  me.registerUserInteraction();
  //                 me.replaceContentInVisibleCellsIfNeeded(true);
              });
          else
              this.scrollElement().addEvent('scroll', function(){
                  me.registerUserInteraction();
  //                 me.replaceContentInVisibleCellsIfNeeded(true);
              });
      },
  
      // updates this.rowsWithAutoformattedCells
      updateAutoformattedCellsCache: function() {
  
          // Cells needing an update are added to this queue. The actual update is postponed until the user does no longer scroll.
          this.autoformattedCellsNeedingUpdates = new PWQueue();
  
             var spans = this.table.getElements('[data-all-formatted-values]');
          var cells = [];
          this.rowsWithAutoformattedCells = spans.map(function(span){
  
              // Cache the autoformatted cells on the row
              var row = span.getParent('tr');
              var cell = span.getParent('td');
  //            if(!row.autoformatted)
  //                row.autoformatted = new Array();
  //            row.autoformatted.push(cell);
              cells.push(cell);
              return row;
          }).unique();
  
          this.replaceContentInCellsIfNeeded(cells);
      },
  
      getAutoformattedCellsInColumn: function(column){
          var index = column.getProperty('data-idx').toInt();
          tds = [];
          this.flatRows.each(function(row){
              var cells = row.getElements('td:nth-child('+(index+1)+')');
              cells.each(function(cell){
                  tds.push(cell);
              });
          });
          return tds.filter(function(td){
              return td.getElement('[data-all-formatted-values]');
          });
      },
  
      // Returns the autoformatted cells in the given column
      invalidateAutoformattedCellsInColumn: function(column){
          var cells = this.getAutoformattedCellsInColumn(column);
          if(cells){
              cells.each(function(cell){
                  cell.hasValidContent = false;
              });
          }
          return cells;
      },
  
      refreshAutoformattedCellsInColumn: function(column){
          this.clearCellWidthByIndex();
          var cells = this.invalidateAutoformattedCellsInColumn(column);
          this.replaceContentInCellsIfNeeded(cells);
      },
  
      replaceContentInCellsIfNeeded: function(cellElements, delay)
      {
          var update = false;
          cellElements.each(function(cellElement){
              if(!cellElement.hasValidContent){
                      cellElement.hasValidContent = true;
                      this.autoformattedCellsNeedingUpdates.enqueue(cellElement);
                      update = true;
              }
          }, this);
          if(update)
              this.updateAutoformattedCellsUpdateTimer();
      },
  
      updateAutoformattedCellsUpdateTimer: function(){
  
          clearInterval(this.autoformattedCellsUpdateTimer);
  
          var me = this;
          var update = function(){
  
              // Do nothing if the user is interacting with the app
              if(!!me.lastUserInteraction && (new Date().getTime() - me.lastUserInteraction) < 1000)
                  return;
  
              // Update cells in batches
              var count = Math.min(me.autoformattedCellsNeedingUpdates.getLength(), 100);
              var cells = [];
              while(count>0){
                  cells.push(me.autoformattedCellsNeedingUpdates.dequeue());
                  count--;
              }
              me.updateContentInCells(cells);
  
              if(me.autoformattedCellsNeedingUpdates.isEmpty()){
              //    console.log('clear');
                  clearInterval(me.autoformattedCellsUpdateTimer);
              }
          };
  
          if(!this.autoformattedCellsNeedingUpdates.isEmpty())
              this.autoformattedCellsUpdateTimer = update.periodical(0);
      },
  
      registerUserInteraction: function(){
          this.lastUserInteraction = new Date().getTime();
      },
  
      // Debugging only
      logRowOfCell: function(cellElement){
          var row = cellElement.getParent('tr').getElement('td > div > span').innerHTML;
          console.log('cell is visible ('+row.trim()+')');
      },
  
      clearCellWidthByIndex: function(){
          if(this.cellWidthByIndex)
              this.cellWidthByIndex.empty();
      },
  
      // cellElement must be a td element. element is the element we get the width from.
      // This method gets the width from a cache (this.cellWidthByIndex).
      // If a width is not found it is measured and put into the cache.
      availableTextWidthInCell: function(cellElement, element){
          var idx = cellElement.getProperty('data-idx');
          var index = idx.toInt();
          var widths = this.cellWidthByIndex;
          if(!widths){
              widths = new Array();
              this.cellWidthByIndex = widths;
          }
          var cellWidth = widths[index];
          if(!cellWidth){
              cellWidth = element.offsetWidth;
              widths[index] = cellWidth;
  //            console.log('index='+index+' width='+cellWidth);
          }
          return cellWidth;
      },
  
      updateContentInCells: function(cells){
          var valueContainers = [];
          var texts = [];
  
          cells.each(function(cell){
              var valueContainer = cell.getElement('[data-all-formatted-values]');
              var allValues      = eval(valueContainer.getProperty('data-all-formatted-values'));
              var parent         = valueContainer.getParent();
              var parentWidth    = this.availableTextWidthInCell(cell, parent);
              var styles         = window.getComputedStyle(valueContainer);
              var text           = this.bestFittingTextWithStyleInWidth(allValues, styles, parentWidth);
              valueContainers.push(valueContainer);
              texts.push(text);
          }, this);
  
          valueContainers.each(function(valueContainer, i){
              valueContainer.innerHTML = texts[i];
          }, this);
      },
  
      // Returns the text which fits best
      bestFittingTextWithStyleInWidth: function(textArray, styles, width) {
          var ctx  = this.measurementCanvas.getContext("2d");
          ctx.font = this.canvasFontFromCSSStyles(styles);
          var padding = this.horizontalPaddingFromCSSStyles(styles);
          var text = '';
          var count = textArray.length;
          for(var i=0; i<count; i++){
              text = textArray[i];
              if(ctx.measureText(text).width+padding < width)
                   break;
          }
          return text;
     },
  
     canvasFontFromCSSStyles: function(styles){
          var font = '';
          var fontStyle = styles['font-style'];
          var fontWeight = styles['font-weight'];
          var fontSize = styles['font-size'];
          var fontFamily = styles['font-family'];
          if(fontStyle)
              font += fontStyle+' ';
          if(fontWeight)
              font += fontWeight+' ';
          if(fontSize)
              font += fontSize+' ';
          if(fontFamily)
              font += fontFamily;
          return font;
     },
  
     horizontalPaddingFromCSSStyles: function(styles){
          var padding = 0.0;
          var paddingLeft = styles['padding-left'];
          var paddingRight = styles['padding-right'];
          if(paddingLeft)
              padding += paddingLeft.toFloat();
          if(paddingRight)
              padding += paddingRight.toFloat();
          return padding;
     },
  
      // For all columns without a width, the typical value string is used to determine a width.
      // The computed width is then applied to the header col and outline table col elements.
     adjustColumnWidths: function(){
          var autoresizingStyle = this.options.columnAutoresizingStyle;
          var allHeaderCols = this.outlineElement.getElements('.header col');
          allHeaderCols.each(function(headerCol, index){
              if(!headerCol.style['width'].toInt()){
  
                  var identifier   = headerCol.getProperty('data-identifier');
                  var outlineCol   = this.table.getElement('col[data-identifier='+identifier+']')
                  var outlineCells = this.table.getElements('tr td:nth-child('+(index+1)+')');
                  var headerCell   = this.outlineElement.getElement('tr:first-child td:nth-child('+(index+1)+')');
                  var width = this.proposedColumnWidth(outlineCol, outlineCells, headerCell, identifier);
  
                  var widthInPixel = ''+width+'px';
                  headerCol.setStyle('width', widthInPixel);
                  outlineCol.setStyle('width', widthInPixel);
              }
          }, this);
  
          // TODO: support more autoresizingStyles
          if(autoresizingStyle == WBLOutlineAutoresizingStyle.FirstColumnOnlyAutoresizingStyle)
          {
              allHeaderCols[0].setStyle('width', '100%');
              this.table.getElements('col')[0].setStyle('width', '100%');
          }
      },
  
  
      // The width is computed by taking into account the header text and the typical value text.
      proposedColumnWidth: function(outlineColumn, outlineCells, headerCell, identifier){
  
          // Compute min and max width:
          var headerStyles = window.getComputedStyle(headerCell);
          var minWidth = headerStyles['min-width'].toInt();
          var maxWidth = headerStyles['max-width'].toInt();
          var nonTextWidthString = outlineColumn.getProperty('data-non-text-width');
          var nonTextWidth = nonTextWidthString ? parseFloat(nonTextWidthString) : 0;
  
          // Note: the max width provided on by element is only used in drag operations (headerStyles['max-width'].toInt())
          var typicalValue = outlineColumn.getProperty('data-typical-value');
  
          // Get the proposed header content width:
          var proposedHeaderWidth = 2 + this.getMaxWidthFromElements(headerCell.getChildren(), headerStyles, null, identifier);
  
          // Get the proposed cell content width:
          var proposedCellWidth = 0;
  
          if(outlineCells && outlineCells.length > 0){
              var cells = this.options.complexColumnWidthComputation ? outlineCells : [outlineCells[0]];
              cells.each(function(cell){
                  var outlineCellStyles = window.getComputedStyle(cell);
                  var cellWidth = nonTextWidth + this.getMaxWidthFromElements(cell.getChildren(), outlineCellStyles, typicalValue, identifier);
                  if(cellWidth > proposedCellWidth)
                      proposedCellWidth = cellWidth;
              }, this);
          }
  
          var width = Math.max(proposedHeaderWidth, proposedCellWidth);
  
          // Limit the width to min and max values
          if(width < minWidth)
              width = minWidth;
          else if(maxWidth != 0 && width > maxWidth)
              width = maxWidth;
  
          return width;
      },
  
  
      // Returns the maximum width of the given elements. Text is treated as single line text.
      getMaxWidthFromElements: function(elements, styles, replacementText, identifier){
  
          var ctx  = this.measurementCanvas.getContext("2d");
          ctx.font = this.canvasFontFromCSSStyles(styles);
          var padding = this.horizontalPaddingFromCSSStyles(styles);
  
          // Gather all spans:
          var spans = [];
          elements.each(function(element){
              if(element.get('tag') == 'span')
                  spans.push(element);
              element.getElements('span').each(function(span){
                  spans.push(span);
              });
          }, this);
  
          // Get the max width of the widest element:
          var width = 0;
          var lastIndex = spans.length-1;
  
          var text;
          for(var i=0; i<=lastIndex; i++){
              span = spans[i];
  
              // If long text should be hidden there is no need to measure it,
              // because those cells contain an image too.
              if(span.getProperty('data-hide-long-text'))
                  return 0.0; // Text can be invisble so return 0.
  
              text = span.get('text');
  
              // Replace the text inside the last span if needed:
              if(replacementText && lastIndex == i)
                  text = replacementText;
  
              var spanWidth = ctx.measureText(text).width+padding;
              if( spanWidth > width)
                  width = spanWidth;
          }
  
          return width+padding;
      },
  
      rows: function(){
          return this.table ? this.table.getElements('.row') : [];
      },
  
      rowAtIndex: function(index){
          if(this.table) {
              var rows = this.table.getElements('.row[data-idx="'+index+'"]');
              return rows.length > 0 ? rows[0] : null;
          }
          return null;
      },
  
      clearRowSelection: function(){
          var selectedRows = Array.clone(this.selectedRows);
          selectedRows.each(function(row){
              this.removeSelectionStyleFromRow(row);
          }, this);
      },
  
      computeAnchorIndexFromSelection: function(){
         this.setAnchorIndex(null);
         this.rows().each(function(row){
             if(row.hasClass(this.classOfSelectedRows)){
                 this.setAnchorIndex(row.index);
                 return;
             }
         }, this);
      },
  
      // The setter makes sure the index is in a valid range
      setAnchorIndex: function(index){
          var visibleRows = this.rows();
          var firstRowIndex = 0;
          var lastRowIndex  = 0;
          if(visibleRows.length > 0){
              firstRowIndex = visibleRows[0].index;
              lastRowIndex = visibleRows.getLast().index;
          }
  
          if(index < firstRowIndex)
              index = firstRowIndex;
          else
          {
              if(index > lastRowIndex)
                  index = lastRowIndex;
          }
          this.anchorIndex = index;
      },
  
      selectSingleRow: function(row){
          this.clearRowSelection();
          this.addSelectionStyleToRow(row);
          this.setAnchorIndex(row.index);
      },
  
      selectUpToRow: function(row){
          var index = row.index;
          if(this.anchorIndex == null){
              this.addSelectionStyleToRow(row);
              this.setAnchorIndex(index);
          }else {
              this.clearRowSelection();
              var minIndex = Math.min(this.anchorIndex, index);
              var maxIndex = Math.max(this.anchorIndex, index);
              var rows = this.flatRows;
              for(var i=minIndex; i<=maxIndex; i++)
              {
                  var row = rows[i];
                  if(row.isVisible)
                      this.addSelectionStyleToRow(row);
              }
          }
      },
  
      toggleSelectionOfRow: function(row){
          var selClass = this.classOfSelectedRows;
          if(row.hasClass(selClass)){
              this.removeSelectionStyleFromRow(row);
              this.computeAnchorIndexFromSelection();
          }else{
              this.addSelectionStyleToRow(row);
              this.setAnchorIndex(row.index);
          }
      },
  
      selectedClassProperty: 'data-selected-class',
      notSelectedClassProperty: 'data-not-selected-class',
  
      addSelectionStyleToRow: function(row){
          if(!this.selectedRows.contains(row))
              this.selectedRows.push(row);
  
          // Replace the content of the 'class' property  by the content of the 'data-selected-class' property if available.
          // (on the row and any element below with this property).
          var rowSelectedClass = row.getProperty(this.selectedClassProperty);
          var changed = false;
          if(rowSelectedClass){
              if(this.applySelectionStyleToElement(row))
                  changed = true;
          }
          if(changed)
              this.selectionStyleDidChangeInRow(row);
          row.addClass(this.classOfSelectedRows);
      },
  
      removeSelectionStyleFromRow: function(row){
          this.selectedRows.erase(row);
  
          var rowSelectedClass = row.getProperty(this.selectedClassProperty);
  
          if(rowSelectedClass && this.applyNormalStyleToElement(row))
              this.selectionStyleDidChangeInRow(row);
  
          row.removeClass(this.classOfSelectedRows);
      },
  
      selectionStyleDidChangeInRow: function(r){
          this.changedRows.push({row:r, index:r.index});
      },
  
      applySelectionStyleToElement: function(element){
          var origClass = element.getProperty('class');
          var selClass = element.getProperty(this.selectedClassProperty);
          if(!element.isNotSelectedClass)
              this.cacheNotSelectedClassInElement(element, origClass);
  
          if(origClass != selClass){
              element.setProperty('class', selClass);
              return true;
          }
          return false;
      },
  
      applyNormalStyleToElement: function(element){
          var newClass = element.isNotSelectedClass;
          if(newClass){
              var classOfCollapsedRows = this.classOfCollapsedRows;
              if(element.getProperty('class') != newClass){
                  // adjust the collapsed state
                  if(element.hasClass(classOfCollapsedRows)){
                      if(!newClass.contains(classOfCollapsedRows))
                          newClass += " "+classOfCollapsedRows;
                  }
                  element.setProperty('class', newClass);
                  return true;
              }
          }
          return false;
      },
  
  
      selectionEventInRow: function(row, event){
          this.changedRows = new Array();
  
          // Shift the keyboard focus to this outline view:
          var withKeyboard = event.type == 'keydown';
          var selectedRows = this.selectedRows;
          if(!withKeyboard) // Mouse events
              this.keyboard.activate();
          if(event.meta){
              if(selectedRows.length > 0 && !this.options.allowsMultipleSelection && !selectedRows.contains(row))
                  return;
              this.toggleSelectionOfRow(row);
          }else if(event.shift && this.options.allowsMultipleSelection)
              this.selectUpToRow(row);
          else
              this.selectSingleRow(row);
          this.updateSelectionBorders();
          this.selectionStyleDidChangeInRowsWithIndex(this.changedRows);
          event.preventDefault();
      },
  
      selectedRowIndices: function(){
          var indices = [];
          var count = this.selectedRows.length;
          for(var i=0; i<count; i++){
              var row = this.selectedRows[i];
              if(row.index !== undefined) // Only outline rows have an index attached (in contrast to gantt rows)
                  indices.push(row.index);
          }
          return indices;
      },
  
      selectedRowIndicesAsJSONString: function(){
          var result = '';
          var indices = this.selectedRowIndices();
          var count = indices.length;
          for(var i=0; i<count; i++){
              result += indices[i] + ',';
          }
          if(result.length)
              result = result.substring(0, result.length-1);
          return result;
      },
  
      // Can be overwritten in subclasses
      selectionStyleDidChangeInRowsWithIndex: function(rowsWithIndex){
          if(this.options.selectionChangedAction){
              this.disposeSelectionToServerTimer();
  
              // We wait one second from the last selection change before we send the selection state to the server:
              this.selectionToServerTimer = this.sendSelectionToServer.delay(1000, this);
          }
      },
  
      disposeSelectionToServerTimer: function(){
          clearTimeout(this.selectionToServerTimer);
          this.selectionToServerTimer = undefined;
      },
  
      sendSelectionToServer: function(){
          if(this.options.selectionChangedAction){
              this.disposeSelectionToServerTimer();
              this.options.selectionChangedAction({'indices':this.selectedRowIndicesAsJSONString()});
          }
      },
  
      sendSelectionToServerIfNeeded: function(){
          if(this.options.selectionChangedAction && this.selectionToServerTimer)
              this.sendSelectionToServer();
      },
  
      addSelectionTopBorderToRows: function(rows){
          Array.each(rows, function(row){
              row.addClass('selectionTop');
          });
      },
  
      addSelectionBottomBorderToRows: function(rows){
          Array.each(rows, function(row){
              row.addClass('selectionBottom');
          });
      },
  
      removeSelectionTopBorderFromRows: function(rows){
          Array.each(rows, function(row){
              row.removeClass('selectionTop');
  
          });
      },
  
      removeSelectionBottomBorderFromRows: function(rows){
          Array.each(rows, function(row){
              row.removeClass('selectionBottom');
          });
      },
  
      // Returns an array with rows sorted by index.
      sortedRows: function(rows){
          rows = Array.clone(rows);
          rows.sort(function(rowA, rowB){
              var aIndex = rowA.index;
              var bIndex = rowB.index;
              if (aIndex < bIndex)
                return -1;
              if (aIndex > bIndex)
                return 1;
              return 0;
          });
          return rows;
      },
  
      updateSelectionBorders: function(){
          var selectedOutlineRows = this.sortedRows(this.selectedRows);
          var visibleOutlineRows = this.rows();
  
          var previousRowsWithSelection = [];
          visibleOutlineRows.each(function(row){
              if(row.hasClass('selectionTop') || row.hasClass('selectionBottom'))
                  previousRowsWithSelection.push(row);
          });
  
          if(selectedOutlineRows.length > 0){
  
              var count = visibleOutlineRows.length;
  
              var findVisibleIndexForRow = function(row, startIndex){
                  var rowIndex = row.index;
                  for(var i=startIndex; i<count; i++){
                      var iRow = visibleOutlineRows[i];
                      if(rowIndex == iRow.index)
                          return i;
                  }
                  console.log('Error: selected row is not visible')
              }
  
              var firstIndex = findVisibleIndexForRow(selectedOutlineRows[0], 0);
              var lastIndex  = findVisibleIndexForRow(selectedOutlineRows.getLast(), firstIndex);
  
              var selClass = 'selected';
              var previousRowIsSelected = false;
              var previousRow = null;
  
              count = lastIndex+1-firstIndex;
              for(var i=firstIndex; i<=lastIndex; i++)
              {
                  var row = visibleOutlineRows[i];
                  previousRowsWithSelection.erase(row);
                  var rows = [row];
  
                  var isSelected = row.hasClass(selClass);
                  if(!isSelected){
                      this.removeSelectionTopBorderFromRows(rows);
                      this.removeSelectionBottomBorderFromRows(rows);
                  }
                  if(!previousRowIsSelected && isSelected)
                      this.addSelectionTopBorderToRows(rows);
                  else if(!isSelected && previousRowIsSelected)
                      this.addSelectionBottomBorderToRows([previousRow]);
                  else if(isSelected && previousRowIsSelected) {
                      this.removeSelectionBottomBorderFromRows([previousRow]);
                      this.removeSelectionTopBorderFromRows(rows);
                  }
  
                  if(isSelected && i == lastIndex){
                      this.addSelectionBottomBorderToRows(rows);
                  }
  
                  previousRowIsSelected = isSelected;
                  previousRow = row;
              }
          }
  
          // Remove selection from previously selected rows
          previousRowsWithSelection.each(function(row){
              row.removeClass('selectionTop');
              row.removeClass('selectionBottom');
          });
      },
  
      clickCount: 0,
  
      singleClick: function(row, event){
  //        console.log('singleClick');
      },
  
      doubleClick: function(row, event){
          if(this.options.doubleClickAction){
              this.sendSelectionToServer(); // Amke sure the selection is immediately pushed to the server.
              this.options.doubleClickAction({'rowIndex':row.index});
          }
      },
  
      makeTableRowsSelectable: function(){
  
          this.selectedRows = new Array();
          var selClass = this.classOfSelectedRows;
          this.prepareRowsWithServerSideSelection();
  
          var me = this;
          this.flatRows.each(function(row){
              row.index = row.getProperty('data-idx').toInt(); // Used in computeAnchorIndexFromSelection()
              row.addEvent('mousedown', function(event){
                  me.lastRowIndexSelectedWithShift = undefined;
                  me.selectionEventInRow(row, event);
                  me.registerUserInteraction();
                  event.stop();
              });
              row.addEvent('click', function(event) {
                  me.clickCount++;
                  if (me.clickCount === 1) {
                      me.singleClickTimer = setTimeout(function() {
                          me.clickCount = 0;
                          me.singleClick(row, event);
                      }, 400);
                  } else if (me.clickCount === 2) {
                      clearTimeout(me.singleClickTimer);
                      me.clickCount = 0;
                      me.doubleClick(row, event);
                  }
                  me.registerUserInteraction();
                  event.stop();
              }, false);
  
          });
          this.updateSelectionBorders();
          this.computeAnchorIndexFromSelection();
  
          if(this.options.selectionChangedAction){
              // Get notified before an action menu is opened, so we can push the selection to the server if needed.
              // This is needed, because in order to validate actions the server needs a valid selection.
              if(window.MooDropMenu)
                  window.parent.eventBus().addObserver(MooDropMenu.WillShowMenuNotification, this.sendSelectionToServerIfNeeded, this);
          }
      },
  
      prepareDisclosureButtons: function(){
          // Add event handlers to disclosure buttons.
          this.flatRows.each(function(row){
              var button = row.getElement('.disclosure');
              if(button){
                  var me = this;
                  // Note: button.toggleExpansionState can be called from unit tests as well
                  button.toggleExpansionState = function(event){
                      me.toggleRowExpansionState(row);
                      me.registerUserInteraction();
                      if(event)
                          event.stop();
                  };
                  button.addEvent('mousedown', function(event){
                      button.toggleExpansionState(event);
                  });
              }
          }, this);
      },
  
      // Used in unit tests:
      toggleRowExpansionStateForRowAtIndex: function(index){
          this.toggleRowExpansionState(this.rowAtIndex(index));
      },
  
      toggleRowExpansionState: function(row){
          if(this.options.clientSideCollapsingAndExpanding)
              this.toggleRowExpansionStateOnClientSideOnly(row);
          else if(this.options.toggleIsExpandedAction) {
              if(this.options.selectionChangedAction)
                  this.sendSelectionToServerIfNeeded();
              this.options.toggleIsExpandedAction({'rowIndex':row.index});
          }
      },
  
      toggleRowExpansionStateOnClientSideOnly: function(row){
          var cssClass = this.classOfCollapsedRows;
          if(!row.hasClass(cssClass))
              this.addCollapsedStatusToRow(row);
          else
              this.removeCollapsedStatusFromRow(row);
  
          this.updateRowsVisibility(this.flatRowsStartingWithRow(row));
      },
  
      isRowCollapsed: function(row){
          return row.hasClass(this.classOfCollapsedRows);
      },
  
      expandRowsOnClientSideOnly: function(rows){
          if(rows.length > 0){
              var affectedRows = [];
              rows.each(function(row){
                  this.removeCollapsedStatusFromRow(row);
                  affectedRows.append(this.flatRowsStartingWithRow(row));
              }, this);
              this.updateRowsVisibility(affectedRows);
          }
      },
  
      collapseRowsOnClientSideOnly: function(rows){
          if(rows.length > 0){
              var affectedRows = [];
              rows.each(function(row){
                  this.addCollapsedStatusToRow(row);
                  affectedRows.append(this.flatRowsStartingWithRow(row));
              }, this);
              this.updateRowsVisibility(affectedRows);
          }
      },
  
      addCollapsedStatusToRow: function(row){
          var classOfCollapsedRows = this.classOfCollapsedRows;
  
          // Add the class to the "class" property
          row.addClass(classOfCollapsedRows);
  
          // Add the class to the selected and not selected properties as well
          var selectedProperty = this.selectedClassProperty;
          var notSelectedProperty = this.notSelectedClassProperty;
          var selectedClass = row.getProperty(selectedProperty);
          var notSelectedClass = row.getProperty(notSelectedProperty);
  
          if(selectedClass && !selectedClass.contains(classOfCollapsedRows)){
              selectedClass += " "+classOfCollapsedRows;
              row.setProperty(selectedProperty, selectedClass);
              row.selectedClass = selectedClass;
          }
          if(notSelectedClass && !notSelectedClass.contains(classOfCollapsedRows)){
              notSelectedClass += " "+classOfCollapsedRows;
              row.setProperty(notSelectedProperty, notSelectedClass);
              row.notSelectedClass = notSelectedClass;
          }
      },
  
      removeCollapsedStatusFromRow: function(row){
          var classOfCollapsedRows = this.classOfCollapsedRows;
  
          // Remove the class from the "class" property
          row.removeClass(classOfCollapsedRows);
  
          var selectedProperty = this.selectedClassProperty;
          var notSelectedProperty = this.notSelectedClassProperty;
          var selectedClass = row.getProperty(selectedProperty);
          var notSelectedClass = row.getProperty(notSelectedProperty);
          if(selectedClass){
              selectedClass = selectedClass.replace(/isCollapsed/g, '');
              row.setProperty(selectedProperty, selectedClass);
              row.selectedClass = selectedClass;
          }
          if(notSelectedClass){
              notSelectedClass = notSelectedClass.replace(/isCollapsed/g, '');
              row.setProperty(notSelectedProperty, notSelectedClass);
              row.notSelectedClass = notSelectedClass;
          }
      },
  
  
      // Fills the normal css classes cache of elements with a server side selection.
      prepareRowsWithServerSideSelection: function(){
          var selectedClassProperty = this.selectedClassProperty;
          var notSelectedClassProperty = this.notSelectedClassProperty;
          this.table.getElements('['+notSelectedClassProperty+']').each(function(element){
              this.cacheNotSelectedClassInElement(element, element.getProperty(notSelectedClassProperty));
                 element.setProperty(selectedClassProperty, element.getProperty('class'));
          }, this);
  
          // Initially fill the selected rows array:
          this.selectedRows = this.table.getElements('.row.selected');
      },
  
      cacheNotSelectedClassInElement: function(element, notSelectedClass){
          notSelectedClass = notSelectedClass.replace(/isCollapsed/g, '');
          element.isNotSelectedClass = notSelectedClass;
      },
  
      selectPreviousRow: function(event){
          var rows = this.flatRows;
          var count = rows.length;
          if(!event.shift){
              if(this.lastRowIndexSelectedWithShift  != undefined)
                  this.setAnchorIndex(this.lastRowIndexSelectedWithShift);
              this.lastRowIndexSelectedWithShift = undefined;
          }
          var anchorIndex = this.anchorIndex;
          if(anchorIndex == undefined)
              anchorIndex = count;
          var startIndex = this.lastRowIndexSelectedWithShift != undefined ? this.lastRowIndexSelectedWithShift : anchorIndex;
          var index = this.getPreviousSelectableRowIndex(startIndex);
          var row = rows[index];
          this.selectionEventInRow(row, event);
          if(event.shift)
              this.lastRowIndexSelectedWithShift = row.index;
          this.scrollRowIntoVisibleArea(row);
      },
  
      getPreviousSelectableRowIndex: function(startIndex){
          var flatRows = this.flatRows;
          var currentRow = this.flatRows[startIndex];
          for(var i=startIndex-1; i >= 0; i--){
              if(flatRows[i].isVisible)
                  return i;
          }
          return startIndex;
      },
  
      selectNextRow:function(event){
          var rows = this.flatRows;
          var count = rows.length;
          if(!event.shift){
              if(this.lastRowIndexSelectedWithShift != undefined)
                  this.setAnchorIndex(this.lastRowIndexSelectedWithShift);
              this.lastRowIndexSelectedWithShift = undefined;
          }
          var anchorIndex = this.anchorIndex;
          if(anchorIndex == undefined)
              anchorIndex = -1;
  
          var startIndex = this.lastRowIndexSelectedWithShift != undefined ? this.lastRowIndexSelectedWithShift : anchorIndex;
          var index = this.getNextSelectableRowIndex(startIndex);
          var row = rows[index];
          this.selectionEventInRow(row, event);
          if(event.shift)
              this.lastRowIndexSelectedWithShift = index;
          this.scrollRowIntoVisibleArea(row);
      },
  
      getNextSelectableRowIndex: function(startIndex){
          var flatRows = this.flatRows;
          var count = flatRows.length;
          var currentRow = this.flatRows[startIndex];
          for(var i=startIndex+1; i < count; i++){
              if(flatRows[i].isVisible)
                  return i;
          }
          return startIndex;
      },
  
      //ak
      scrollRowIntoVisibleArea: function(row){
  
          var me = this;
          var scroll = function(){
  
  //            var watch = new PWStopWatch(true);
              var scrollElement = me.outlineElement.scrollViewController.scrollElement();
  
              var oTop    = scrollElement.scrollTop; // This is expensive in FF with 1000 rows -> 45ms
              var oBottom = oTop + scrollElement.getSize().y;
              var rTop    = row.offsetTop;
              var rBottom = rTop + row.offsetHeight;
  
              if(    (rTop < oTop || rBottom > oBottom)
                 && !(rTop < oTop && rBottom > oBottom)) {
                  if(rTop < oTop)
                      scrollElement.pwScrollToTop(rTop);
                  else if (rBottom > oBottom)
                      scrollElement.pwScrollToTop(rBottom-(oBottom-oTop));
              }
  
  //            watch.logDuration('scrollRowIntoVisibleArea');
          };
  
          scroll.delay(100);
  //        scroll();
      }
  
   });
  
  