com.smartclient.debug.public.sc.client.widgets.GridRenderer.js Maven / Gradle / Ivy
Show all versions of smartgwt Show documentation
/*
* Isomorphic SmartClient
* Version SC_SNAPSHOT-2011-08-08 (2011-08-08)
* Copyright(c) 1998 and beyond Isomorphic Software, Inc. All rights reserved.
* "SmartClient" is a trademark of Isomorphic Software, Inc.
*
* [email protected]
*
* http://smartclient.com/license
*/
//> @class GridRenderer
//
// A flexible, high-speed table that offers consistent cross-platform sizing, clipping, and events.
//
// @treeLocation Client Reference/Foundation
// @visibility external
//<
isc.ClassFactory.defineClass("GridRenderer", "Canvas");
isc.GridRenderer.addClassProperties({
//> @type CellState
// Appearance of the cell -- used to set to different visual states.
// Also used as a suffix to gridRenderer.baseStyle to set CSS styles.
// @group appearance
// @see gridRenderer.getCellStyle()
SELECTED:"Selected", // @value isc.gridRenderer.SELECTED Cell is selected.
DISABLED:"Disabled", // @value isc.gridRenderer.DISABLED Cell is disabled.
OVER:"Over", // @value isc.gridRenderer.OVER Mouse is over the cell.
//<
//> @attr gridRenderer.standardStyleSuffixes (array : array of strings : IR)
// Array of the 12 standard cell style suffix strings ("Over", "SelectedOver", etc.)
// to allow quicker calculation of cell styles.
// @see gridRenderer.getCellStyle()
//<
standardStyleSuffixes:[
"",
"Over",
"Selected",
"SelectedOver",
"Disabled",
"DisabledOver",
"DisabledSelected",
"DisabledSelectedOver",
"Dark",
"OverDark",
"SelectedDark",
"SelectedOverDark",
"DisabledDark"
]
});
isc.GridRenderer.addProperties({
//> @attr gridRenderer.totalRows (number : 0 : [IRW])
// Total number of rows in the grid.
//
// NOTE: in order to create a valid grid, you must either provide a totalRows value or implement
// getTotalRows()
//
// @see method:getTotalRows
// @visibility external
//<
totalRows : 0,
//> @attr gridRenderer.showAllRows (boolean : false : [IRA])
// Whether all rows should be drawn all at once, or only rows visible in the viewport.
//
// Drawing all rows causes longer initial rendering time, but allows smoother vertical scrolling.
// With a very large number of rows, showAllRows will become too slow.
//
// See also +link{drawAheadRatio} and +link{drawAllMaxCells}.
//
// @group performance
// @visibility external
//<
//showAllRows : false,
//> @attr gridRenderer.virtualScrolling (boolean : null : [IRA])
// When incremental rendering is switched on and there are variable record heights, the virtual
// scrolling mechanism manages the differences in scroll height calculations due to the
// unknown sizes of unrendered rows to make the scrollbar and viewport appear correctly.
//
// virtualScrolling is switched on automatically when fixedRowHeights is false but you should
// switch it on any time there are variable record heights.
// @visibility external
//<
//virtualScrolling:true,
//> @attr gridRenderer.showAllColumns (boolean : false : [IRA])
// Whether all columns should be drawn all at once, or only columns visible in the viewport.
//
// Drawing all columns causes longer initial rendering time, but allows smoother horizontal
// scrolling. With a very large number of columns, showAllColumns will become too slow.
//
// @group performance
// @visibility external
//<
//showAllColumns : false,
//>@attr gridRenderer.drawAllMaxCells (integer : 250 : IRWA)
// If drawing all rows would cause less than drawAllMaxCells
cells to be rendered,
// the full dataset will instead be drawn even if +link{listGrid.showAllRecords,showAllRecords}
// is false and the viewport size and +link{drawAheadRatio} setting would normally have caused
// incremental rendering to be used.
//
// The drawAllMaxCells
setting prevents incremental rendering from being used in
// situations where it's really unnecessary, such as a 40 row, 5 column dataset (only 200
// cells) which happens to be in a grid with a viewport showing only 20 or so rows.
// Incremental rendering causes a brief "flash" during scrolling as the visible portion of the
// dataset is redrawn, and a better scrolling experience can be obtained in this situation by
// drawing the entire dataset up front, which in this example would have negligible effect on
// initial draw time.
//
// drawAllMaxCells:0
disables this features. You may want to disable this feature
// if performance is an issue and:
//
// - you are very frequently redraw a grid
//
- you do a lot of computation when rendering each cell (eg formulas)
//
- you are showing many grids on one screen and the user won't scroll most of them
//
//
// @group performance
// @visibility external
//<
drawAllMaxCells:250,
//> @attr gridRenderer.recordCanSelectProperty (string : "canSelect" : [IRA])
// If set to false on a record, selection of that record is disallowed.
//<
recordCanSelectProperty: "canSelect",
//> @attr gridRenderer.isSeparatorProperty (string : "isSeparator" : [IRA])
// If this property is defined on some record, render the record as a separator row.
//<
// Documented at the ListGrid level only. ListGrids will pass the isSeparatorProperty through
// to their body.
isSeparatorProperty:"isSeparator",
//> @attr gridRenderer.singleCellValueProperty (string : "singleCellValue" : [IRA])
// If this property is defined on some record, render the record as a single cell (spanning all
// columns).
//<
// Documented at the ListGrid level only. ListGrids also implement getCellValue() which
// will display record.singleCellValue as the value of the cell.
// ListGrids will pass the singleCellValueProperty through to their body.
singleCellValueProperty:"singleCellValue",
//> @attr gridRenderer.instantScrollTrackRedraw (boolean : true : IRW)
// If true, if the user clicks on the scroll buttons at the end of the track or clicks once on
// the scroll track, there will be an instant redraw of the grid content so that the user
// doesn't see any blank space. For drag scrolling or other types of scrolling, the
// +link{scrollRedrawDelay applies}.
//
// @group performance
// @visibility external
//<
instantScrollTrackRedraw:true,
//> @attr gridRenderer.scrollRedrawDelay (integer : 75 : IRW)
// While drag scrolling in an incrementally rendered grid, time in milliseconds to wait
// before redrawing, after the last mouse movement by the user. See also
// +link{gridRenderer.instantScrollTrackRedraw} for cases where this delay is skipped.
//
// @group performance
// @visibility external
//<
scrollRedrawDelay:75,
//> @attr gridRenderer.drawAheadRatio (float : 1.3 : [IRWA])
// How far should we render rows ahead of the currently visible area? This is expressed as a
// ratio from viewport size to rendered area size.
//
// Tweaking drawAheadRatio allows you to make tradeoffs between continuous scrolling speed vs
// initial render time and render time when scrolling by large amounts.
//
// NOTE: Only applies when showAllRows is false.
//
// @group performance
// @visibility external
//<
drawAheadRatio : 1.3,
//> @attr gridRenderer.quickDrawAheadRatio (float : 1.0 : [IRWA])
// Alternative to +link{drawAheadRatio}, to be used when the user
// is rapidly changing the grids viewport (for example drag scrolling through the grid).
// If unspecified +link{drawAheadRatio} will be used in all cases
// @group performance
// @visibility external
//<
quickDrawAheadRatio:1.0,
//> @attr gridRenderer.cellHeight (number : 20 : [IRW])
// The default height of each row in pixels.
//
// @see gridRenderer.getRowHeight()
// @visibility external
// @group cellStyling
//<
cellHeight:20,
//> @attr gridRenderer.fixedRowHeights (boolean : true : IRWA)
// Should we vertically clip cell contents, or allow rows to expand vertically to show all
// contents?
//
// If we allow rows to expand, the row height as derived from
// +link{gridRenderer.getRowHeight(),getRowHeight()} or the
// default +link{cellHeight} is treated as a minimum.
//
// @group cellStyling
// @visibility external
//<
fixedRowHeights:true,
//enforceVClipping:null,
//> @attr gridRenderer.fixedColumnWidths (boolean : true : IRWA)
// Should we horizontally clip cell contents, or allow columns to expand horizontally to
// show all contents?
//
// If we allow columns to expand, the column width is treated as a minimum.
//
// @group sizing
// @visibility external
//<
fixedColumnWidths:true,
//> @attr gridRenderer.autoFit (boolean : false : IRWA)
// If true, make columns only wide enough to fit content, ignoring any widths specified.
// Overrides fixedFieldWidths.
//
// @group sizing
// @visibility external
//<
//autoFit:false,
//> @attr gridRenderer.wrapCells (boolean : false : IRWA)
// Should content within cells be allowed to wrap?
// @group cellStyling
// @visibility external
//<
//wrapCells:false,
//> @attr gridRenderer.cellSpacing (number : 0 : [IRW])
// The amount of empty space, in pixels, between each cell.
// @group cellStyling
// @visibility internal
//<
cellSpacing:0,
//> @attr gridRenderer.cellPadding (number : 2 : [IRW])
// The amount of empty space, in pixels, surrounding each value in its cell.
// @group cellStyling
// @visibility external
//<
cellPadding:2,
//> @attr gridRenderer.canSelectOnRightMouse (boolean : true : [RW])
// If true, rightMouseDown events will fire 'selectOnRightMouseDown()' for the appropriate cells.
// @group events
// @visibility external
//<
canSelectOnRightMouse:true,
// Hover
// ---------------------------------------------------------------------------------------
//> @attr gridRenderer.canHover (boolean : null : [RW])
// If true, cellHover and rowHover events will fire when the user leaves the mouse over a
// row / cell.
// @group events
// @visibility external
// @see cellHover()
// @see rowHover()
// @see showHover
//<
//> @attr gridRenderer.showHover (boolean : null : [RW])
// If true, and canHover is also true, when the user hovers over a cell, hover text will pop up
// next to the mouse. The contents of the hover is determined by +link{cellHoverHTML()}.
// @group events
// @visibility external
// @see canHover
// @see cellHoverHTML()
//<
// can be set to false to cause Hover to be per-row instead of per-cell
hoverByCell:true,
// CSS styles
// --------------------------------------------------------------------------------------------
backgroundColor:"white",
// style applied to the table element. NOTE: don't expose: styling of a grid should be
// accomplish by styling the surrounding DIV, where we can use the standard methodology to
// detect borders, margins, etc (eg getInnerHeight())
tableStyle:"listTable",
//> @attr gridRenderer.baseStyle (CSSStyleName : "cell" : [IR])
// The base name for the CSS class applied to cells. This style will have "Dark",
// "Over", "Selected", or "Disabled" appended to it according to the state of the cell.
// @visibility external
// @group cellStyling
// @see method:getCellStyle()
// @see method:getBaseStyle()
//<
baseStyle:"cell",
//> @attr gridRenderer.alternateRowStyles (boolean : false : [IRW])
// Whether alternating rows should be drawn in alternating styles, in order to create a "ledger"
// effect for easier reading. If enabled, the cell style for alternate rows will have "Dark"
// appended to it.
// @visibility external
// @group cellStyling
//<
//alternateRowStyles:false,
//> @attr gridRenderer.alternateRowFrequency (number : 1 : [IRW])
// The number of consecutive rows to draw in the same style before alternating, when
// alternateRowStyles is true.
// @visibility external
// @group cellStyling
//<
alternateRowFrequency:1,
//> @attr gridRenderer.emptyCellValue (string : " " : IRW)
// Value to show in empty cells (when getCellValue returns null)
// @group cellStyling
// @visibility external
//<
emptyCellValue:" ",
// Empty messages (what to do if no data is present)
// --------------------------------------------------------------------------------------------
//> @attr gridRenderer.showEmptyMessage (boolean : true : [IRW])
// Indicates whether the text of the emptyMessage property should be displayed if no data is
// available.
// @visibility external
// @group emptyMessage
// @see emptyMessage
//<
//> @attr gridRenderer.emptyMessage (string : null : IRW)
// The string to display in the body of a listGrid with an empty data array, if
// showEmptyMessage is true.
// @group emptyMessage, i18nMessages
// @visibility external
// @see showEmptyMessage
// @see emptyMessageStyle
//<
//> @attr gridRenderer.emptyMessageTableStyle (CSSStyleName : null : IRW)
// CSS styleName for the table as a whole if we're showing the empty message
// @group emptyMessage
// @visibility external
//<
//> @attr gridRenderer.emptyMessageStyle (CSSStyleName : null : IRW)
// The CSS style name applied to the emptyMessage string if displayed.
// @group emptyMessage
// @visibility external
//<
//> @attr gridRenderer.showOfflineMessage (boolean : true : [IRW])
// Indicates whether the text of the offlineMessage property should be displayed if no data
// is available because we are offline and there is no suitable cached response
// @visibility external
// @group emptyMessage, offlineGroup, i18nMessages
// @see offlineMessage
//<
//> @attr gridRenderer.offlineMessageStyle (CSSStyleName : null : IRW)
// The CSS style name applied to the offlineMessage string if displayed.
// @group emptyMessage, offlineGroup, i18nMessages
// @visibility external
//<
//> @attr gridRenderer.offlineMessage (string : null : IRW)
// The string to display in the body of a listGrid with an empty data array, if
// showOfflineMessage is true and the data array is empty because we are offline and there
// is no suitable cached response
// @group offlineGroup, emptyMessage, i18nMessages
// @visibility external
// @see showOfflineMessage
// @see offlineMessageStyle
//<
//> @attr gridRenderer.fastCellUpdates (boolean: true : IRWA)
//
// Note: This property only has an effect in Internet Explorer
//
// Advanced property to improve performance for dynamic styling of gridRenderer cells in
// Internet Explorer, at the expense of slightly slower initial drawing, and some
// limitations on supported styling options.
//
// fastCellUpdates
speeds up the dynamic styling system used by rollovers,
// selections, and custom styling that calls +link{gridRenderer.refreshCellStyle()}, at
// the cost of slightly slower draw() and redraw() times.
//
// Notes:
//
// - When this property is set, ListGrid cells may be styled using the
// +link{listGrid.tallBaseStyle}. See +link{listGrid.getBaseStyle()} for
// more information.
// - If any cell styles specify a a background image URL, the URL will be resolved relative
// to the page location rather than the location of the CSS stylesheet. This means cell
// styles with a background URL should either supply a fully qualified path, or the
// background image media should be made available at a second location for IE.
// - fastCellUpdates will not work if the styles involved are in an external stylesheet loaded
// from a remote host. Either the stylesheet containing cell styles needs to be loaded
// from the same host as the main page, or the cell styles need to be inlined in the html
// of the bootstrap page.
// - fastCellUpdates will not work if the css styles for cells are defined in
// a
.css
file loaded via @import
. Instead the .css
// file should be loaded via a <link ...>
tag.
//
// @visibility external
//<
// This default is overridden in ListGrid
fastCellUpdates:true,
//> @method gridRenderer.setFastCellUpdates()
// Setter for +link{gridRenderer.fastCellUpdates}. Has no effect in browsers other than
// Internet Explorer.
// @param fastCellUpdates (boolean) whether to enable fastCellUpdates.
// @visibility external
//<
setFastCellUpdates : function (fcu) {
if (fcu && !isc.Browser.isIE) {
this.fastCellUpdates = false;
//this.logInfo("fastCellUpdates was enabled - this has no effect " +
// "in browsers other than Internet Explorer. Disabling.");
return;
}
if (fcu == this.fastCellUpdates) return;
this.fastCellUpdates = fcu;
this.markForRedraw();
},
// Standard Canvas settings
// --------------------------------------------------------------------------------------------
overflow:"auto",
_avoidRedrawFlash:true,
canFocus:true
//>Animation
// If a px / second speed is specified for a row animation, cap it at a maximum
// (Inherited from LG / TG if specified there)
,animateRowsMaxTime:1000,
// @attr gridRenderer.snapToCells (boolean : false : IRW)
// Should drag-and-drop operations snap the dragged object into line with the nearest cell?
//
// @group dragdrop
// @visibility external
//<
snapToCells: false,
//> @attr gridRenderer.snapInsideBorder (boolean : false : IRW)
// If true, snap-to-cell drops will snap the dropped object inside the selected cell's border.
// If false, snap-to-cell drops will snap the dropped object to the edge of the selected cell,
// regardless of borders
//
// @group dragdrop
// @see GridRenderer.snapToCells
// @visibility external
//<
snapInsideBorder: false,
snapHDirection: isc.Canvas.BEFORE,
snapVDirection: isc.Canvas.BEFORE
});
isc.GridRenderer.addMethods({
initWidget : function () {
// Make sure we have columnWidths set up - we rely on this for some methods
if (!this._fieldWidths) this.setColumnWidths([]);
if (this.selection) this.setSelection(this.selection);
// If we're overflow visible, we have to write out our entire body content.
if (this.overflow == isc.Canvas.VISIBLE) {
this.showAllRows = true;
}
// turn virtualScrolling on if it's unset and we have variable-height rows
if (!this.fixedRowHeights && this.virtualScrolling == null) this.virtualScrolling = true;
// default for virtualScrolling is now null, so if it's been turned on, also turn
// fixedRowHeights off so that virtualScrolling has an effect
//if (this.fixedRowHeights && this.virtualScrolling) this.fixedRowHeights = false;
if (!this.fixedRowHeights && !this.showAllRows) {
//this._avoidRedrawFlash = true;
if (this.showCustomScrollbars == false) {
this.logInfo("Variable height records cannot be used with native scrollbars;" +
" setting showCustomScrollbars:true on this GridRenderer and using" +
" the special 'NativeScrollbar' class as a scrollbarConstructor.");
this.showCustomScrollbars = true;
this.scrollbarConstructor = "NativeScrollbar";
}
}
// initialize fastCellUpdates via the setter.
// Disables this attribute where not supported
this.setFastCellUpdates(this.fastCellUpdates);
},
shouldShowAllColumns : function () {
if (this.showAllColumns) return true;
// have to force rendering all columns, otherwise, row heights would vary with drawn
// columns.
if (!this.fixedRowHeights && !this.showAllRows) return true;
if (this.overflow == isc.Canvas.VISIBLE) {
return true;
}
return false;
},
// Empty Message handling
// --------------------------------------------------------------------------------------------
isEmpty : function () { return false; },
_showEmptyMessage : function (startCol,endCol) {
return this.getEmptyMessageHTML(startCol,endCol, this.grid.isOffline());
},
//> @method gridRenderer.getEmptyMessageHTML() ([A])
// Return the HTML to show if there's nothing in the list
// @group drawing
// @return (string) HTML for the empty message layer
//<
getEmptyMessageHTML : function (startCol,endCol,offline) {
if (!offline) {
if (!this.showEmptyMessage) return " ";
} else {
if (!this.showOfflineMessage) return " ";
}
if (this.isPrinting) {
if (startCol == null) startCol = 0;
if (endCol == null) endCol = this.fields ? this.fields.getLength() -1 : 0;
return "" +
this.grid.getPrintHeaders(startCol, endCol) +
"" +
(offline ? this.getOfflineMessage() : this.getEmptyMessage())
+ "
";
}
// Always ensure the empty message fills the viewport.
// Respect flag to match the empty message size to the specified field widths -
// if the specified field sizes exceed the viewport size, expand the empty message
// to accommodate it.
var width = this.getInnerWidth(),
extraWidth = 0;
if (this.expandEmptyMessageToMatchFields && this._fieldWidths) {
extraWidth = this._fieldWidths.sum() - width;
if (extraWidth < 0) extraWidth = 0;
}
// Note that if the GR is scrollable, we want the empty message to be visible /
// centered when scrolled to 0/0 so table into 2 cells, centering the empty message
// within the first cell which spans the viewport
var splitTable = extraWidth && this.overflow != isc.Canvas.VISIBLE;
var sb = isc.StringBuffer.create();
sb.append(
"",
// NOTE: empty message can't be too tall, or it will introduce vscrolling in
// shorter grids
(offline ? this.getOfflineMessage() : this.getEmptyMessage()),
(extraWidth && splitTable ? "
" + isc.Canvas.spacerHTML(width,1) : null),
" "
);
if (extraWidth && splitTable) {
sb.append("",
isc.Canvas.spacerHTML(extraWidth, 1), " ");
}
sb.append("
");
return sb.release();
},
//> @method gridRenderer.getEmptyMessage() ([A])
// @group drawing
// return the text for the empty message
// you can ovveride this function if your data has additional semantics
// (eg: initial conditions, loading, filtering, etc)
// @return (string) empty message
//<
getEmptyMessage : function () {
return this.emptyMessage;
},
//> @method gridRenderer.getOfflineMessage() ([A])
// @group drawing
// return the text for the offline message. The default implementation returns the
// value of the containing Grid's offlineMessage property;
// you can ovveride this function if your data has additional semantics
// (eg: initial conditions, loading, filtering, etc)
// @return (string) offline message
//<
getOfflineMessage : function () {
return this.grid.offlineMessage;
},
// Drawing
// --------------------------------------------------------------------------------------------
// use a rel-pos div to apply a z-index to the content.
// required for the ability to float embedded components above or below the table
_$zIndexDivTemplate:["", // 2
, // 3 [table html]
""],
_$fillerDiv:" ",
getInnerHTML : function () {
var tableHTML = this.getTableHTML(),
template = this._$zIndexDivTemplate;
template[1] = this.getTableZIndex();
template[3] = tableHTML;
if (isc.Browser.isMoz) template[5] = this._$fillerDiv;
return template.join(isc.emptyString);
},
isFastScrolling : function () {
return this.isDragScrolling() || this.isRepeatTrackScrolling();
},
shouldUseQuickDrawAheadRatio : function () {
// NOTE: useQuickDrawAheadRatio is a flag you can flip on temporarily when there's some
// reason other than fast scrolling that you want quick redraws (eg column drag resize)
return this.useQuickDrawAheadRatio || this.isFastScrolling();
},
doneFastScrolling : function () {
// if our last redraw was caused by fast scrolling we will have applied the quick
// draw ahead ratio when determining which records to draw. In this case we now want
// to redraw with the standard drawAhead ratio so short distance scrolls around this area
// will requrie fewer redraws
var redrawRequired = this._appliedQuickDrawAhead;
if (redrawRequired) {
// set the flag to suppress drawAhead direction calculation. This ensures that we
// add draw-ahead rows in all directions on the theory that the user is done scrolling
// large increments in one direction.
// We clear this flag when the delayed redraw() actually fires
this._suppressDrawAheadDirection = true;
this.markForRedraw("Done Fast scrolling.");
}
},
// given a range elements (rows or cols) currently visible in the viewport, apply the
// drawAheadRatio to determine the range to draw. The "drawAheadRatio" is a fraction (>1) of
// the viewport. "scrollForward" is the scrolling direction: true (forward), false (backward),
// or null (unknown)
addDrawAhead : function (startIndex, endIndex, maxIndex, scrollForward, vertical) {
// figure out how many elements we intend to draw
var useQuickDrawAhead = this.shouldUseQuickDrawAheadRatio(),
ratio = useQuickDrawAhead && this.quickDrawAheadRatio != null ?
this.quickDrawAheadRatio : this.drawAheadRatio,
numToDraw = Math.ceil((endIndex - startIndex) * ratio);
// respect the flag to suppress the drawAhead scrolling direction logic
if (this._suppressDrawAheadDirection) scrollForward = null;
if (scrollForward != null) {
// we know the scroll direction; render extra elements in the current direction of
// scrolling
if (scrollForward) endIndex = startIndex + numToDraw;
else startIndex = endIndex - numToDraw;
} else {
// we haven't been scrolled yet; if we're flush at the beginning (very common), render
// ahead forward
if (startIndex == 0) endIndex = numToDraw;
else {
// otherwise, render extra rows on either side
var extraElements = Math.ceil((numToDraw - (endIndex - startIndex))/2);
startIndex -= extraElements;
endIndex += extraElements;
}
}
// clamp ends of the range to 0 / maxIndex
if (startIndex < 0) { // shift both ends of the range forward so startIndex = 0
endIndex -= startIndex;
startIndex = 0;
}
if (endIndex >= maxIndex) { // shift both ends of the range back so endIndex < maxIndex
var offset = endIndex - (maxIndex -1);
startIndex = Math.max(0, (startIndex - offset));
endIndex = Math.max(0, maxIndex - 1);
}
// store a flag indicating whether this redraw used the special 'quick draw ahead' code
// this is checked in doneFastScrolling to determine whether a redraw is required.
if (useQuickDrawAhead) this._appliedQuickDrawAhead = true;
else delete this._appliedQuickDrawAhead;
return [startIndex, endIndex];
},
getExtraRowHeight : function (startRow, endRow) {
var total = 0;
for (var rowNum = startRow; rowNum < endRow; rowNum++) {
var height = this.getRowHeight(this.getCellRecord(rowNum, 0), rowNum),
extraHeight = (height - this.cellHeight);
if (extraHeight > 0) {
//this.logWarn("rowNum: " + rowNum +
// " in range: " + [this._firstDrawnRow, this._lastDrawnRow] +
// " with extraHeight: " + extraHeight);
total += extraHeight;
}
}
return total;
},
getDrawArea : function (colNum) {
// Figure out what rows should be drawn
// --------------------------------------------------------------------------------------------
var totalRows = this.getTotalRows(), startRow, endRow, vScrollForward;
// figure out if we should show all cells in case the total displayable cells are less than
// drawAllMaxCells
var totalCells = totalRows * this.fields.length,
showAllCells = totalCells <= this.drawAllMaxCells &&
!isc.EH.dragging && !this.isAnimating() &&
!(this.parentElement && this.parentElement.isAnimating());
if (this.showAllRows || showAllCells) {
// draw all rows
startRow = 0;
endRow = Math.max(totalRows - 1, 0);
} else {
// ordinary incremental rendering
var rowArr = this._getDrawRows();
startRow = rowArr[0];
endRow = rowArr[1];
// just for logging
vScrollForward = rowArr[2]
}
// Figure out which columns to draw
// --------------------------------------------------------------------------------------------
var startCol, endCol, totalCols = this.fields.length, hScrollForward;
if (colNum != null) {
// a column number was specified, draw that column only (needed for legacy Nav4 support, and
// for column auto-sizing)
startCol = colNum;
endCol = colNum + 1;
} else if (showAllCells || this.shouldShowAllColumns()) {
// draw all columns
startCol = 0;
endCol = totalCols - 1;
} else {
// incremental rendering
var visibleColumns = this.getVisibleColumns();
// detect scrolling direction: true (forward), false (backward), or null (unknown)
hScrollForward = (this.lastScrollLeft == null ? null :
this.lastScrollLeft < this.getScrollLeft());
var drawAheadRange = this.addDrawAhead(visibleColumns[0], visibleColumns[1],
totalCols, hScrollForward);
startCol = drawAheadRange[0];
endCol = drawAheadRange[1];
}
// figure out the appropriate chunk size on first draw ever
if (this.cacheDOM && !this._rowChunkSize) {
this._rowChunkSize = endRow - startRow;
this._colChunkSize = endCol - startCol;
}
return [startRow, endRow, startCol, endCol];
},
_getDrawRows : function () {
// figure out which rows we need to draw to minimally fill the viewport
var visibleRows = this._getViewportFillRows();
// detect scrolling direction: true (forward), false (backward), or null (unknown)
var vScrollForward = (this.lastScrollTop == null ? null :
this.lastScrollTop < this.getScrollTop());
var totalRows = this.getTotalRows();
// Note: addDrawAhead will add the draw-ahead rows (rows drawn offscreen for
// scrolling), and clamp the ends of the drawn range to the ends of the data (ensuring
// we don't end up with startRow < 0, or endRow > (totalRows-1)
var drawAheadRange = this.addDrawAhead(visibleRows[0], visibleRows[1],
totalRows, vScrollForward, true);
//this.logWarn("draw range: " + this._getViewportFillRows() + " fwd:" + vScrollForward +
// ", after adding drawAhead:" + drawAheadRange);
// just for logging - return whether we added the fwd scroll
drawAheadRange[2] = vScrollForward;
return drawAheadRange;
},
// get the row at the coordinate, as a floating point number representing a partial distance
// through the row
getRowCoordinate : function (coord) {
var rowNum = this.getEventRow(coord),
// get our offset into it
rowTop = this.getRowTop(rowNum),
offsetIntoRow = coord - rowTop,
rowHeight = this.getRowSize(rowNum),
percentIntoRow = offsetIntoRow / rowHeight;
// detect inconsistency between getEventRow and getRowTop()
//if (offsetIntoRow < 0 || offsetIntoRow > rowHeight) {
// this.logWarn("*******************************\n" +
// ", coord: " + coord +
// ", eventRow: " + rowNum +
// ", rowTop: " + rowTop +
// ", offsetIntoRow: " + offsetIntoRow +
// ", rowSize: " + rowHeight +
// ", firstDrawn: " + this._firstDrawnRow +
// ", lastDrawn: " + this._lastDrawnRow +
// ", heights: " + this._getDrawnRowHeights());
//}
return rowNum + percentIntoRow;
},
// override to interpret ratio in terms of rowNum instead of scrollTop vs scrollHeight
scrollToRatio : function (vertical, ratio, reason,a,b) {
if (!vertical || !this._isVirtualScrolling) {
return this.invokeSuper(isc.GridRenderer, "scrollToRatio", vertical,ratio,reason,a,b);
}
var maxRow = this.getTotalRows() - 1,
exactRowNum = ratio * maxRow,
rowNum = Math.floor(exactRowNum),
rowOffset = Math.round((exactRowNum - rowNum) * this.getRowSize(rowNum));
this._targetRow = rowNum;
this._rowOffset = rowOffset;
this._scrollToTargetRow(reason || "scrollToRatio");
// if scrolling to that position makes us dirty, setup to scroll to the indicated target
// row during redraw
if (this.isDirty()) {
this._scrollRatio = ratio;
this._targetRow = rowNum;
this._rowOffset = rowOffset;
}
},
// override to return ratio in terms of rowNum instead of scrollTop vs scrollHeight
getScrollRatio : function (vertical,b,c,d) {
if (!vertical || !this._isVirtualScrolling) {
return this.invokeSuper(isc.GridRenderer, "getScrollRatio", vertical,b,c,d);
}
if (this.isDirty() && this._scrollRatio != null) return this._scrollRatio;
// if there are 0 or 1 rows, we're at the top
var maxRow = this.getTotalRows() - 1;
if (maxRow <= 0) return 0;
var scrollTop = this.getScrollTop(),
topCoord = this.getRowCoordinate(scrollTop),
ratio = topCoord / maxRow;
//this.logWarn("getScrollRatio: " + ratio +
// ", maxRow: " + maxRow +
// ", topCoord: " + topCoord);
return Math.min(1,ratio);
},
// show a fixed-size thumb in virtualScrolling mode. Otherwise thumb size fluctuates
// meaninglessly.
getViewportRatio : function (vertical,b,c,d) {
if (!vertical || !this._isVirtualScrolling) {
return this.invokeSuper(isc.GridRenderer, "getViewportRatio", vertical,b,c,d);
}
var avgRowHeight = this._viewRatioRowHeight || this.getAvgRowHeight();
return Math.min(1, (this.getViewportHeight() / avgRowHeight) / this.getTotalRows());
},
// take some drawn row that is likely to remain drawn, and store the position it should be in
// relative to the viewport, so that if we have to redraw, we can match user expectation by
// placing rows where the user expects.
_storeTargetRow : function (scrollTop, delta) {
// don't pick up a target row during the special scroll that places us on the target row
if (this._literalScroll) return;
// if we're empty just bail
if (this.isEmpty()) return;
// according to scrolling direction, pick the row at the top or bottom of the viewport as
// the row most likely to remain onscreen
var viewportEdge
if (delta > 0) {
// scrolling down
viewportEdge = scrollTop + this.getViewportHeight();
} else {
viewportEdge = scrollTop;
}
var targetRow = this.getEventRow(viewportEdge),
maxRow = this.getTotalRows()-1,
newScrollTop = scrollTop;
if (targetRow < 0 || targetRow > maxRow) {
this._targetRow = maxRow;
this._rowOffset = 0;
newScrollTop = this.getRowTop(maxRow);
} else {
this._targetRow = targetRow;
// how far into the target row the top of the viewport should be (positive means more
// of row is scrolled offscreen)
this._rowOffset = scrollTop - this.getRowTop(this._targetRow) + delta;
//var drawArea = this.getDrawArea();
//if (targetRow < drawArea[0] || targetRow > drawArea[1]) {
if (Math.abs(this._rowOffset) > this.getViewportHeight()) {
// happens only if we are programmatically scrolled by a large amount to a totally
// new viewport, in which case anchoring to a row from the old viewport is useless
// and could lead to surprises (eg if scrolled near max, scrolling to 10px should
// place us on 10px into the first row but might do something different if we
// calculate coordinates based on drawn row sizes in a viewport near the end).
this.logInfo("storeTargetRow: targetRow: " + targetRow +
" with offset: " + this._rowOffset +
//" wouldn't fall within draw range: " + [drawArea[0],drawArea[1]] +
", clearing", "virtualScrolling");
this._rowOffset = this._targetRow = null;
}
}
return newScrollTop;
},
// scroll the previously stored target row into the stored position
_scrollToTargetRow : function (reason) {
var targetRow = this._targetRow,
offset = this._rowOffset;
var scrollTop = this.getRowTop(targetRow) + offset;
this._literalScroll = true;
this._scrollHeight = null;
this.scrollTo(null, scrollTop, reason || "targetRow");
this._literalScroll = false;
// stop reporting last requested scroll ratio since we've now scrolled to match the
// requested ratio
this._scrollRatio = null;
},
// if we're rendering rows/cols incrementally, we may need to redraw on scroll
scrollTo : function (left, top, reason, animating) {
if (isc._traceMarkers) arguments.__this = this;
if (this._isVirtualScrolling && top != null && reason != "nativeScroll") {
var oldScrollTop = this.getScrollTop(),
delta = top - oldScrollTop;
if (delta != 0) {
this._storeTargetRow(oldScrollTop, delta);
top = Math.min(top, this.getRowTop(this.getTotalRows()-1));
}
}
this.invokeSuper(isc.GridRenderer, "scrollTo", left,top, reason, animating);
// don't check for the need to redraw if we're already dirty. Optimization: for
// scroll-and-scroll-back situations, we could avoid a redraw by undirtying ourselves
if (this.isDirty() || this._scrollFromRedraw) return;
// if we're only drawing rows near the viewport..
var needRedraw = (this._needRowRedraw() || this._needColumnRedraw());
if (needRedraw) {
if (!this.isFastScrolling() && this.instantScrollTrackRedraw) {
this.redraw("scrolled");
} else if (this.scrollRedrawDelay == 0) {
this.markForRedraw("scrolled");
} else {
var _this = this;
this.fireOnPause("scrollRedraw",
function () { _this.markForRedraw("scrolled") },
this.scrollRedrawDelay);
}
this._scrollRedraw = true;
}
},
_needRowRedraw : function () {
if (this.showAllRows) return false;
// we have a range of records that have been drawn, from grid._firstDrawnRow to
// grid._lastDrawnRow (updated in getTableHTML). See if the new viewport falls
// completely into the drawn range.
// NOTE: we use visible rows rather than viewport fill rows because by using
// actual rendered row height we can avoid some redraws when we have several viewports
// worth of drawn data due to tall rows.
// Note also that visible rows is only an approximation if asked about an undrawn area,
// which is fine, because all we care about is whether the new viewport falls
// completely within the drawn range.
var visibleRows = this.getVisibleRows(),
firstVisible = visibleRows[0],
lastVisible = visibleRows[1];
// check that the last visible row doesn't exceed the total number of rows we will
// draw. NOTE: -1 because totalRows is a count and lastVisible is an index.
var totalRows = this.getTotalRows();
if (lastVisible > totalRows-1) lastVisible = totalRows-1;
var needRedraw = (firstVisible < this._firstDrawnRow || lastVisible > this._lastDrawnRow);
return needRedraw;
},
_needColumnRedraw : function () {
// if we're only drawing columns near the viewport..
if (this.shouldShowAllColumns()) return false;
var visibleCols = this.getVisibleColumns(),
firstVisible = visibleCols[0],
lastVisible = visibleCols[1],
needRedraw = (firstVisible < this._firstDrawnCol || lastVisible > this._lastDrawnCol);
return needRedraw;
},
// disable incremental rendering when overflow:visible is set on the fly
setOverflow : function (overflow) {
if (overflow == isc.Canvas.VISIBLE) {
this.showAllRows = true;
}
return this.Super("setOverflow", arguments);
},
// Cache DOM mode
// ---------------------------------------------------------------------------------------
// Mode that caches rendered chunks of the grid area to avoid redrawing as a user revisits the
// same area of the grid without having changed anything. Currently incomplete.
// === cacheDOM mode limitations
// - can't have fixedRecordHeights:false
// - does not support row animation
// - doesn't work with rowSpans
// - shouldn't use with full-row inline edit and large number of columns
// - doesn't support startSpace / endSpace
getRowChunkNum : function (logicalRowNum) {
return Math.round(logicalRowNum / this._rowChunkSize);
},
getColChunkNum : function (logicalColNum) {
return Math.round(logicalColNum / this._colChunkSize);
},
getTableChunk : function (rowChunkNum, colChunkNum) {
var tableCache = this._tableCache;
if (!tableCache) return;
// if row and col are unpassed, return the chunk at 0,0
rowChunkNum = rowChunkNum || 0;
colChunkNum = colChunkNum || 0;
var colCache = tableCache[rowChunkNum];
return colCache ? colCache[colChunkNum] : null;
},
getTableChunkAt : function (logicalRowNum, logicalColNum) {
var rowChunkNum = this.getRowChunkNum(logicalRowNum),
colChunkNum = this.getColChunkNum(logicalColNum),
tableElem = this.getTableChunk(rowChunkNum, colChunkNum);
if (tableElem != null) {
// semi-hack: set the offsets used in getTableElement() to find physical cells from
// logical cells
this._firstDrawRow = rowChunkNum * this._rowChunkSize;
this._firstDrawnCol = colChunkNum * this._colChunkSize;
return tableElem;
}
},
// We need to ensure the table cache is clear after we've reset our inner HTML
// - otherwise we'll be pointing to stale elements.
// Clear the cache before we update inner HTML and
// also set a flag so any inadvertant calls to code that would re-cache coming
// from getInnerHTML will not re-cache stale elements.
// note: _updateInnerHTML is called if we have no children, otherwise
// _updateParentHTML updates the innerHTML
_updateInnerHTML : function (a,b,c,d) {
if (this.cacheDOM) {
this.drawVisibleChunks();
} else {
this._clearTableCache();
this._suppressTableCaching = true;
this.invokeSuper(isc.GridRenderer, "_updateInnerHTML", a,b,c,d);
delete this._suppressTableCaching;
}
},
_updateParentHTML : function (a,b,c,d) {
this._clearTableCache();
this._suppressTableCaching = true;
this.invokeSuper(isc.GridRenderer, "_updateParentHTML", a,b,c,d);
delete this._suppressTableCaching;
},
// in cacheDOM mode, this is called in lieu of normal redraw
drawVisibleChunks : function () {
// figure out what undrawn chunks are visible and draw them
var visibleRows = this.getVisibleRows(),
visibleCols = this.getVisibleColumns(),
startRowChunk = this.getRowChunkNum(visibleRows[0]),
startColChunk = this.getColChunkNum(visibleCols[0]),
endRowChunk = this.getRowChunkNum(visibleRows[1]),
endColChunk = this.getColChunkNum(visibleCols[1]);
for (var rowChunk = startRowChunk; rowChunk < endRowChunk; rowChunk++) {
for (var colChunk = startColChunk; colChunk < endColChunk; colChunk++) {
if (this.getTableChunk(rowChunk, colChunk) == null) {
this.logWarn("drawing chunk: " + [rowChunk, colChunk]);
this.renderTableChunk(rowChunk, colChunk);
}
}
}
var newHTML = this.getTableHTML();
},
renderTableChunk : function (rowChunkNum, colChunkNum) {
// figure out geometry of table to draw
var startRow = rowChunkNum * this._rowChunkSize,
endRow = startRow + this._rowChunkSize,
startCol = colChunkNum * this._colChunkSize,
endCol = startCol + this._colChunkSize;
// draw new table chunk
var html = this.getTableHTML([startCol, endCol], startRow, endRow),
tableElem = isc.Element.insertAdjacentHTML(this.getHandle(), "beforeEnd", html, true);
//this.logWarn("html form chunk: " + [rowChunkNum, colChunkNum] +
// "\n" + html +
// "\nelement: " + this.echo(tableElem));
// cache the table element
var tableCache = this._tableCache = this._tableCache || [],
colCache = tableCache[rowChunkNum] = tableCache[rowChunkNum] || [];
colCache[colChunkNum] = tableElem;
},
getDrawnRows : function () {
return this.getVisibleRows();
},
//>Animation Row Animation support
// ---------------------------------------------------------------------------------------
// Methods to animate a show / hide of multiple rows
//> @method gridRenderer.startRowAnimation()
// Animates a show / hide of rows by growing the rows into view.
// Note: the rows to be shown/hidden should already be in the data, and the calling function
// is responsible for any manipulation to the data / redraw at the end of this method.
// @param show (boolean) are we showing or hiding rows?
// @param startRow (number) first row in range to be shown/hidden
// @param endRow (number) last row in range to be shown/hidden
// @param [callback] (callback) callback to fire when animation completes
// @param [speed] (number) speed for the animation in pixels / second
// @param [duration] (number) if speed is not set, number of milliseconds for the animation to take
// @param [effect] (string) optional acceleration effect for the animation
// @param [slideIn] (boolean) if specified, the rows will appear to slide into view rather than
// being revealed
//<
// additional param indicates this was called from the listGrid - we use this to ensure
// we fire the callback in the listGrid's scope
startRowAnimation : function (show, startRow, endRow, callback, speed, duration, effect, slideIn,
fromListGrid, isDelayed) {
// Always call finishRowAnimation - this will no op if there is no current/pending
// row animation in progress
this.finishRowAnimation();
if (!this.isDrawn() || !this.isVisible()) {
if (callback != null) {
var target = fromListGrid ? this.parentElement : this;
target.fireCallback(callback);
}
return;
}
if (show == null) show = true;
if (startRow == null) startRow = 0;
if (endRow == null) endRow = this.getTotalRows() - 1;
if (startRow == endRow) {
this.logWarn("startRowAnimation passed empty row range, aborting: " +
[startRow, endRow]);
return;
}
var canRedraw = this.readyToRedraw("animating show / hide of rows", false);
if (!canRedraw) {
this._delayedRowAnimationArgs = [show, startRow, endRow, callback, speed,
duration, effect, slideIn, fromListGrid];
this._delayedRowAnimation = isc.Timer.setTimeout(
{target:this, methodName:"_delayedStartRowAnimation"},
0
);
return;
}
// redraw, placing an entire subtable with the rows to be animated inside a single row, and
// measure the size of the rows we're going to reveal or hide
// When doing a 'slide-in' animation, we have to write out every row to be revealed since
// they'll all scroll past the user's nose.
// Have a check for this being a huge number of rows so we don't hit performance issues
// generating this initial HTML!
if ((endRow-startRow) > this.maxAnimateSlideInRows) slideIn = false;
// set up the this._slideInAnimationRows attribute - this allows us to determine
// whether we need to write out every row in the fragment
this._slideInAnimationRows = slideIn;
var fragmentHeight = this._initializeShowHideRow(show, startRow, endRow, callback, fromListGrid);
// Use animateRowHeight to grow or shrink the height
this.animateRowHeight(this._animatedShowStartRow,
// NOTE: animate all the way down to zero, so there is no lurch
// between the final frame of the animation and the subsequent redraw
(show ? fragmentHeight : 0),
{target:this, methodName:"_rowShowComplete"},
speed, duration, effect, slideIn);
},
// maximum number of rows to be animated into view using a 'slide' animation
// this kind of animation requires every row be rendered so we limit this to ensure
// we don't hit a performance roadblock writing out thousands of records' HTML!
maxAnimateSlideInRows:100,
// Helper to start delayed row animation
_delayedStartRowAnimation : function () {
if (this._delayedRowAnimationArgs == null) {
this.logWarn("Unable to perform delayed row animation - bailing");
return;
}
var argsArr = this._delayedRowAnimationArgs,
show = argsArr[0],
startRow=argsArr[1],
endRow = argsArr[2],
callback = argsArr[3],
speed = argsArr[4],
duration = argsArr[5],
effect = argsArr[6],
slideIn = argsArr[7],
fromListGrid = argsArr[8];
this._delayedRowAnimationArgs = null;
this._delayedRowAnimation = null;
// The additional param indicates that the row animation is delayed
this.startRowAnimation(show, startRow, endRow, callback, speed, duration, effect, slideIn,
fromListGrid, true);
},
// helper method to redraw the GR in its state at the beginning of the show/hide row animation
// Returns the height of the table fragment to be written into the animation row.
_initializeShowHideRow : function (show, startRow, endRow, callback, fromListGrid) {
// If we've already been called and performed a redraw to set up the animated row table,
// just return the height of the rows to animate
// This allows us to separate the initial redraw (rendering the animation rows in a clippable
// div) from the row animation.
var fragmentHeight = 0;
if (this._animatedShowStartRow == startRow && this._animatedShowEndRow == endRow) {
// check the anmiation cell only
var animationCell = this.getTableElement(this._animatedShowStartRow, 0),
clipDiv = this._getCellClipDiv(animationCell);
if (!clipDiv) {
fragmentHeight = (endRow - startRow) * this.cellHeight;
} else fragmentHeight = clipDiv.scrollHeight;
} else {
// hang a flag onto this table so we know where the fragment gets written into the normal
// table.
this._animatedShowStartRow = startRow;
this._animatedShowEndRow = endRow;
// if we're hiding visible rows, we can look at their drawn heights now
if (!show) {
var heights = this._getDrawnRowHeights();
for (var i = startRow; i < endRow; i++) {
fragmentHeight += heights[i];
}
// used when writing out the row containing the fragment
this._animatedShowRowHeight = fragmentHeight;
// This redraw writes out the single animation row with an entire table inside it
this.redraw("initializing animated hide row");
// In this case we're going to show rows that are currently undrawn.
} else {
this._animatedShowRowHeight = 1;
this.redraw("initializing animated show row");
// At this point we have written out the fragment and it's clipped by the containing
// cell / div
var animationCell = this.getTableElement(this._animatedShowStartRow, 0),
clipDiv = this._getCellClipDiv(animationCell);
if (!clipDiv) {
fragmentHeight = (endRow - startRow) * this.cellHeight;
} else fragmentHeight = clipDiv.scrollHeight;
}
if (this.isDirty()) this.redraw("Initializing row animation requires second redraw");
}
this._animatedShowCallback = {callback:callback,
target:(fromListGrid ? this.parentElement : this)};
return fragmentHeight;
},
// finishRowAnimation - synchronously short-cut to the end of the current row show/hide
// animation, and fire the callback.
finishRowAnimation : function () {
// a currently running rowAnimation (show/hide rows) implies a running rowHeightAnimation -
// finishing that will jump to the approprate size and fire the callback to finish the
// show/hide animation
if (this._animatedShowStartRow != null) {
this.finishAnimateRowHeight();
} else {
// In this case we're not running a show/hide row animation - but we may have set up
// a delayed one
if (this._delayedRowAnimation != null) {
// don't fire the delayed animation
isc.Timer.clearTimeout(this._delayedRowAnimation);
var args = this._delayedRowAnimationArgs,
show = args[0], startRow = args[1], endRow = args[2],
callback = args[3], duration = args[4], fromListGrid = args[5];
delete this._delayedRowAnimationArgs;
delete this._delayedRowAnimation;
if (!this.readyToRedraw()) {
this.logWarn("Finish row animation called while Grid is not ready to redraw. " +
"GridRenderer HTML will not be updated when callback fires.", "animation");
var target = fromListGrid ? this.parentElement : this;
if (callback) target.fireCallback(callback);
} else {
// redraw the GR with the single animation row containing the table fragment
// at the start of the animation height
var fragmentHeight = this._initializeShowHideRow(show, startRow, endRow, callback, fromListGrid);
// set the height to the final height of that row and fire the 'complete' method
// to fire callbacks / clean up vars (rather than ever performing an animation
this.setRowHeight(startRow, (show ? fragmentHeight : 1));
this._rowShowComplete();
}
}
}
},
// Fired when animated show / hide of rows completes.
_rowShowComplete : function () {
var callback = this._animatedShowCallback;
delete this._animatedShowCallback;
delete this._animatedShowStartRow;
delete this._animatedShowEndRow;
delete this._animatedShowRowHeight;
// We stored the callback as an object {target:... callback:...}
// This allows us to fire the callback on the ListGrid if that's where the method was
// originally called from
if (callback && callback.callback) callback.target.fireCallback(callback.callback);
},
//> @method gridRenderer.animateRowHeight()
// Will animate a resize of a row to the specified height, firing a callback when the resize
// is complete
// @param rowNum (number) Row to resize
// @param toHeight (number) new height for the row
// @param [callback] (callback) Callback to fire when animation completes
// @param [speed] (number) Speed for the animation (pixels / second)
// @param [duration] (number) duration of the resize in ms
// @param [effect] (string) Optionally an acceleration effect can be specified - if not specified
// default is to do a smooth animation (no acceleration)
// @param [slideIn] (boolean) if specified, rows will appear to slide into view rather than
// being revealed
//<
// Additional param 'fromListGrid' indicates this was fired from the ListGrid, so we should
// fire the callback in that scope
_$none:"none",
animateRowHeight : function (rowNum, toHeight, callback, speed, duration, effect,
slideIn, fromListGrid) {
// If we're not drawn, no need to try to animate since this is a visual update only
if (!this.isDrawn()) {
if (callback) {
var target = (fromListGrid ? this.parentElement : this);
target.fireCallback(callback);
}
return;
}
// simultaneous row height animations not currently supported
if (this._rowHeightAnimation != null) {
this.logInfo("early finish of row animation, because new animation started",
"animation")
this.finishAnimateRowHeight();
}
var fromHeight = this.getRowSize(rowNum);
// If speed (pixels / second) is specified, it takes precedence over duration
if (speed != null) {
var change = (toHeight - fromHeight);
if (change < 0) change = 0 - change;
duration = Math.round((change / speed) * 1000);
// Don't let the animation exceed a maximum
if (duration > this.animateRowsMaxTime) duration = this.animateRowsMaxTime;
}
this._rowAnimationInfo = {
_rowNum:rowNum,
_fromHeight:fromHeight,
_toHeight:toHeight,
_callback:callback,
_slideIn:slideIn,
_fromList:fromListGrid
}
effect = (effect || this._$none);
if (this.logIsInfoEnabled("animation")) {
this.logInfo("starting row animation, duration: " + duration + ", effect: " + effect,
"animation")
}
this._rowHeightAnimation = this.registerAnimation(
{target:this, method:this._fireRowAnimation},
duration, effect
);
// suppress adjustOverflow until the row animation completes. This will avoid unnecessary
// scrollbars from showing up
if (this.overflow == isc.Canvas.AUTO || this.overflow == isc.Canvas.SCROLL)
this._suppressAdjustOverflow = true;
},
_fireRowAnimation : function (ratio) {
var info = this._rowAnimationInfo,
rowNum = info._rowNum,
rowHeight = this._getRatioTargetValue(info._fromHeight, info._toHeight, ratio);
if (isc.Browser.isSafari && info._fromHeight > info._toHeight)
this._forceRowRefreshForAnimation = true;
// pass in explict "" as className so we don't adjust sizing for the standard row styling
// (which won't be applied to this row during animation)
this.setRowHeight(rowNum, rowHeight, null, isc.emptyString, true, true, true);
if (isc.Browser.isSafari) delete this._forceRowRefreshForAnimation;
if (info._slideIn) {
var clipDiv = this._getCellClipDiv(this.getTableElement(rowNum,0));
if (clipDiv) {
var scrollHeight = clipDiv.scrollHeight,
offsetHeight = clipDiv.offsetHeight;
if (scrollHeight > offsetHeight) clipDiv.scrollTop = scrollHeight - offsetHeight;
else clipDiv.scrollTop = 0;
}
}
// Fire the completion callback in a separate thread - this means if it does a lot of
// processing we shouldn't see a visual pause before the native repaint at the full-size
if (ratio == 1) {
isc.Timer.setTimeout({target:this, methodName:"_rowAnimationComplete"}, 0);
}
},
// Fired when we're done with a row resize animation
_rowAnimationComplete : function () {
// allow standard adjustOverflow to resume
delete this._suppressAdjustOverflow;
this.adjustOverflow("row animation complete");
var info = this._rowAnimationInfo;
delete this._rowHeightAnimation;
delete this._rowAnimationInfo;
if (info && info._callback) {
var target = info._fromList ? this.parentElement : this;
target.fireCallback(info._callback);
}
},
//> @method gridRenderer.finishAnimatingRowHeight()
// Completes any row height animation currently in progress and fires the callback from that
// animation.
// May be fired automatically to avoid (unsupported) overlapping animations, etc.
//<
// Leave this as unexposed for now
finishAnimateRowHeight : function () {
if (!this._rowHeightAnimation) return;
// cancel upcoming animation cycles
this.cancelAnimation(this._rowHeightAnimation);
// Simply firing the "last step" of the rowHeight animation will jump to the appropriate
// height and fire the callback
this._fireRowAnimation(1);
},
// DEBUG
// timing
var t0 = isc.timeStamp();
// 0) {
for (var i = 0; i < printComponents.length; i++) {
var component = printComponents[i];
if (component._gridBodyPrintHTML != null) continue;
var printComponentContext = {
component:component,
colNum:colNum, startRow:startRow, endRow:endRow, descreteCols:discreteCols,
asyncCallback:asyncCallback
};
// this.logWarn("calling getPrintHTML on embedded component:" + printComponents[i]);
var printCHTML = printComponents[i].getPrintHTML(
this.printProperties,
asyncCallback == null ? null
: {target:this, methodName:"gotComponentPrintHTML",
context:printComponentContext}
);
if (printCHTML != null) {
component._gridBodyPrintHTML = printCHTML;
} else {
// If the printComponent generates its printHTML asynchronously,
// we will be notified and fire the gotComponentPrintHTML() method
// when that returns, at which point we'll re-run this method, skipping
// the component(s) for which we already have HTML and ultimately firing
// the asyncCallback.
// this.logWarn("GR.getTableHTML() - getPrintHTML for component:" +
// component + " went asynchronous", "printing");
return null;
}
}
}
// at this point we've generated HTML for all our print components and stored it
// We'll write it directly into the cells / rows below.
}
var fragment = (startRow != null && endRow != null),
rangeStart = startRow != null ? startRow : 0,
rangeEnd = endRow != null ? endRow : this.getTotalRows();
// Figure out rows and columns to actually draw
// ----------------------------------------------------------------------------------------
var drawRect = this.getDrawArea(),
grid = this.grid,
scrollRowNum
;
if (grid) {
if (grid._scrollCell) {
scrollRowNum = grid._scrollCell == null ? 0 :
isc.isAn.Array(grid._scrollCell) ? grid._scrollCell[0] : grid._scrollCell;
} else if (grid.data && grid.data.getFirstUsedIndex && drawRect[0] == 0) {
scrollRowNum = grid.data.getFirstUsedIndex();
}
if (scrollRowNum) {
var diff = drawRect[1]-drawRect[0],
lastRow = scrollRowNum+diff,
totalRows = this.getTotalRows()
;
if (lastRow >= totalRows) {
scrollRowNum -= (lastRow-(totalRows-1))
lastRow = totalRows-1;
}
if (scrollRowNum < 0) scrollRowNum = 0;
drawRect[0] = scrollRowNum;
drawRect[1] = lastRow;
}
}
if (!fragment) {
this._firstDrawnRow = drawRect[0];
this._lastDrawnRow = drawRect[1];
//>Animation
// If we're doing an animated show/hide of some rows, we need to write out enough rows
// to fill the viewport when the rows to be animated are sized at zero height (will
// happen either initially or at the end of the draw).
if (this._animatedShowStartRow != null) {
this._lastDrawnRow += (this._animatedShowEndRow - this._animatedShowStartRow);
var totalRows = this.getTotalRows();
if (this._lastDrawnRow >= totalRows) this._lastDrawnRow = totalRows -1;
}
// Animation
// A common use of table fragments is animating folder open/close, where we write out
// a bunch of child rows and animate them into view.
// In this case if we're scrolling the rows into view and we're inside the viewport,
// each of the rows will be seen by the user so we can't skip writing any content
// as we don't want to show blank spacers to the user.
if (this._writingAnimatedShowRows) {
// All off the bottom or the top - just draw a big spacer so the scrollbar adjusts
if (viewportTop > endRow || viewportEnd < startRow) {
startRow = endRow;
} else {
if (!this._slideInAnimationRows) {
startRow = Math.max(startRow,viewportTop);
endRow = Math.min(endRow,viewportEnd);
}
}
}
// 1300000) canResizeSpacerDivs = false;
}
if (!fragment) this._canResizeSpacerDivs = canResizeSpacerDivs;
if (totalHeight > 10000000) {
this.logWarn("This grid is showing " + (rangeEnd - rangeStart).toLocalizedString()
+ " rows. Due to native rendering limitations, grids with this many rows"
+ " may not appear correctly on all browsers. Consider filtering the data"
+ " displayed to the user to reduce the total number of rows displayed at a time."
+ " This will improve usability as well as avoiding unpredictable behavior.");
}
if (!this.cacheDOM && !this.isPrinting) {
// If the space is zero sized, we still want to write out the spacer div so we can handle
// setStartSpace() etc without a redraw
// In IE specifying the height as zero px won't work, so set display none instead to ensure
// the spacer takes up no space
// Give the spacer DIV an ID so we can look at it's height, etc. later.
// When we resize this on the fly (in setStartSpace()) we'll set display back to the default
// (inline) if necessary.
output.append(" 0
? "margin-top:" + this._startRowSpacerHeight + "px;" : ""),
// if we plan to scroll immediately after draw, draw the table as hidden, so we don't
// momentarily see it in the wrong scroll position
(this._targetRow != null && !(isc.Browser.isIE && this._avoidRedrawFlash) ?
"visibility:hidden;" : ""),
"'>",
(isc.Browser.isMoz ? "" : "")
);
var vPad = 0, hPad = 0,
// get style we'll use on the first record, used for sizing calculations
firstRecordStyle = this._getFirstRecordStyle();
if (isc.Browser.isStrict && (isc.Browser.isSafari || isc.Browser.isIE)) {
if ((isc.Browser.isIE && !isc.Browser.isIE8) ||
(isc.Browser.isSafari && isc.Browser.safariVersion < 530))
{
hPad = this._getCellHBorderPad();
}
vPad = (this.fixedRowHeights ? 0 : this.cellPadding * 2);
vPad += (this.fixedRowHeights ? isc.Element._getVBorderSize(firstRecordStyle)
: isc.Element._getVBorderPad(firstRecordStyle));
}
// store pad amounts since they are needed on cell refresh
this._vPad = vPad;
this._hPad = hPad;
if (!autoFit && isc.Browser.isDOM) {
for (var i = 0; i < colNums.length; i++) {
output.append(" ");
}
}
output.append("");
}
var cellHeight = this.cellHeight,
cellWrapHTML = (this.wrapCells ? "" : ""),
cellWrapHTMLClose = (this.wrapCells ? "" : " ")
;
var singleCells = 0;
// Draw rows
// --------------------------------------------------------------------------------------------
if (isc.Browser.isDOM) {
// Do we need to write a DIV into the cell (See comments in _writeDiv())
var writeDiv = this._writeDiv(cellHeight);
// template of cell HTML
var cellHTML = [];
cellHTML[0] = "" + cellWrapHTML; // ">" + wrap (per table)
// [24] value range start (per cell)
// [gap]
cellHTML[30] = cellWrapHTMLClose + (writeDiv ? " " : "");
var heightAttrSlot = 1, heightSlot = 2, alignSlot = 4, valignAttrSlot = 5,
valignSlot = 6, widthSlot = 7, ariaSlot = 9,
minHeightCSS = 10, cssStart = 11, styleSlot = 18,
cellID = 20, divStart = 21, cellValue = 24;
var rowStart = " number of remaining cells to skip (for columns where a cell spans into the
// current row)
var cellSkips = [],
// number of cells that will be skipped in this row. Increased when spans start,
// decreased when they end
skipCount = 0,
// colNum -> start row of rowSpanning cell (for columns where a cell spans into the
// current row)
cellSkipSourceRows = [];
this._cacheColumnHTML(colNums, autoFit, hPad, writeDiv);
if (this.isPrinting && (!this._printingChunk || startRow == 0)) {
output.append(this.grid.getPrintHeaders(startCol, endCol));
}
// output each record in turn
for (var rowNum = startRow; rowNum < endRow; rowNum++) {
//>Animation
var isAnimationRow = (!fragment && this._animatedShowStartRow == rowNum);
//Animation
isAnimationRow || //Animation
isAnimationRow ? this._animatedShowRowHeight :
//" + this._$cellClipDivStart +
this._getCellDivCSSHeight(rowHeight, record, rowNum, isAnimationRow);
}
// If we're drawing the record as a single cell, figure out which cells it's spanning
var singleCellSpan = drawRecordAsSingleCell ?
this._getSingleCellSpan(record,rowNum,startCol,endCol) : null;
//if (skipCount > 0) {
// this.logWarn("rowSpan start rows for row: " + rowNum +
// ": " + cellSkipSourceRows);
//}
// output each cell
for (var i = 0; i < colNums.length; i++) {
colNum = colNums[i];
var field = fields[colNum],
cellRecord = record;
if (cellRecord == null) cellRecord = this.getCellRecord(rowNum, colNum);
if (cellSkips[colNum] > 0) {
// this cell will be skipped due to a rowSpanning cell in a previous row
// record the start row of the rowSpanning cell
field._rowSpans[rowNum] = cellSkipSourceRows[colNum];
//this.logWarn("recording start row: " + field._rowSpans[rowNum] +
// " at " + rowNum);
// reduce the count of cells remaining to be skipped due to this rowSpanning
// cell
cellSkips[colNum]--;
if (cellSkips[colNum] == 0) {
// we don't have to skip any more cells due to this rowSpanning cell
skipCount--;
// so clear the colNum -> rowNum with rowSpan cell mapping
cellSkipSourceRows[colNum] = null;
}
continue;
}
// per column HTML (align)
cellHTML[alignSlot] = this.getCellAlign(record, field, rowNum, colNum);
// per column valign
var vAlign = this.getCellVAlign(record, field, rowNum, colNum);
if (vAlign != null) {
cellHTML[valignAttrSlot] = valignAttr
cellHTML[valignSlot] = vAlign;
}
if (singleCellSpan != null && (colNum == singleCellSpan[0])) {
// note singleCells variable used for logging only
singleCells++;
// HTML to cause the cell to span several cells for
// drawRecordAsSingleCell case
cellHTML[widthSlot] = this._getTDSpanHTML(singleCellSpan[1]-singleCellSpan[0]);
// If we're writing out a DIV, we need to close the "'" around the style
if (writeDiv) {
cellHTML[divStart+1] = this._$singleQuote;
}
// We'll write out the rest of the HTML, then increment i to jump to the
// end of the span
} else {
// per column HTML (width)
// XXX Actually a misnomer - this includes some height information too
cellHTML[widthSlot] = field._widthHTML;
// we have a row span function, write rowSpan into the table cell (after
// the specified width)
if (this.getRowSpan) {
var rowSpan = this.getRowSpan(record, rowNum, colNum);
if (rowSpan > 1) {
var rowSpanText = " ROWSPAN=" + rowSpan;
// piggyback the rowSpan on the alignment slot
if (cellHTML[alignSlot] != null)
cellHTML[alignSlot] += rowSpanText;
else
cellHTML[alignSlot] = rowSpanText;
// set up to skip outputting cells in this column
cellSkips[colNum] = rowSpan - 1;
skipCount++;
// remember the start row of the rowSpanning cell
cellSkipSourceRows[colNum] = rowNum;
// field._rowSpans:
// - exists only on field objects where there are rowSpanning
// cells
// - is a map of rowNum -> starting rowNum of cell that spans into
// that cell.
// - only contains rowSpanning cells; other slots have the value
// undefined.
if (field._rowSpans == null) field._rowSpans = {};
field._rowSpans[rowNum] = rowNum;
}
}
if (ariaEnabled && this.getCellRole != null) {
var cellRole = this.getCellRole(rowNum, colNum, record);
if (cellRole != null) {
var cellState = this.getCellAriaState(rowNum, colNum, record);
cellHTML[ariaSlot] = " role='" + cellRole +
(cellState ? isc.Canvas.getAriaStateAttributes(cellState) : "");
} else {
cellHTML[ariaSlot] = null;
}
}
if (writeDiv) {
// also closes the style= attribute
cellHTML[divStart+1] = field._divWidthHTML;
} else {
// Note - if we're not writing a DIV, we're not in the middle of a style
// attribute, so no need for "'"
cellHTML[divStart+1] = null;
}
}
// set per-cell pieces of cell HTML
// -------------------------------------------------------------------------
// cell style (CSS classname)
var cellStyle = this.getCellStyle(record, rowNum, colNum),
// cell CSS text (direct value for STYLE attribute)
customCSSText = (this.getCellCSSText ?
this.getCellCSSText(record, rowNum, colNum) :
null);
//>Animation
// always have the animation row cell have no padding / border, since the
// table written into it already has padding / border for each cell.
if (isAnimationRow) {
var nopad = "padding:0px;border:0px;";
if (customCSSText) customCSSText += ";" + nopad
else customCSSText = nopad;
}
// Animation
if (isAnimationRow) {
// Set a flag so getTableHTML() is aware that the fragment its returning
// is to be used in the animated show/hide row.
this._writingAnimatedShowRows = true;
var tableHTML = this.getTableHTML(null, this._animatedShowStartRow,
this._animatedShowEndRow);
delete this._writingAnimatedShowRows;
if (!writeDiv) {
cellHTML[cellValue] = isc.SB.concat(this._$cellClipDivStart,
this._getCellDivCSSHeight(rowHeight, record, rowNum, isAnimationRow),
this._$singleQuote, this._$rightAngle, tableHTML,
"");
} else {
cellHTML[cellValue] = tableHTML;
}
} else //',
ec._gridBodyPrintHTML, "", rowEnd);
delete ec._gridBodyPrintHTML;
}
}
}
//>Animation
// Skip the rows between animationStartRow and animationEndRow, since they'll be
// written into a single row
if (isAnimationRow) {
rowNum = this._animatedShowEndRow -1;
}
//");
var tailRecords = rangeEnd - endRow,
virtualScrolling = (!fragment && this._isVirtualScrolling);
// ignore endSpace if this.cacheDOM is true - not currently supported in that mode.
var endSpacerHeight = this.cacheDOM ? 0 : (this.endSpace || 0);
// reset this._endRowSpacerHeight
this._endRowSpacerHeight = 0;
if (!this.showAllRows && (tailRecords != 0 || virtualScrolling)) {
var endRowSpacerHeight = tailRecords * this.getAvgRowHeight();
if (virtualScrolling && tailRecords == 0) {
var minHeight = this.getViewportHeight();
if (endRowSpacerHeight < minHeight) {
//this.logWarn("last row drawn: drawing spacer of height: " + spacerHeight);
endRowSpacerHeight = minHeight;
}
}
this._endRowSpacerHeight = endRowSpacerHeight;
endSpacerHeight += this._endRowSpacerHeight;
}
// NOTE: setting overflow:hidden allows later code to shrink the spacer
// without rewriting the spacer content
if (!this.cacheDOM && !this.isPrinting) {
output.append("= 20040113 &&
this.fixedColumnWidths && !this.autoFit) ||
(this.fixedRowHeights && (this.wrapCells || this.enforceVClipping) &&
// Moz or IE Strict (Safari covered above - always on)
(isc.Browser.isMoz ||
(isc.Browser.isStrict && isc.Browser.isIE)
)
)
);
},
// Should the record be drawn as a single cell, spanning all the cols in the table?
_drawRecordAsSingleCell : function (rowNum, record) {
//!DONTCOMBINE
// If this row is a separator or is not loaded yet, we draw a single cell with COLSPAN
// set to extend across the entire table.
return (record &&
(record[this.singleCellValueProperty] != null || record[this.isSeparatorProperty] ||
(Array.isLoading(record) &&
!(isc.Browser.isSafari && (rowNum == 0 || rowNum == this._firstDrawnRow)) )
)
);
},
// Method to return the CSS text to specify the height of the div written into a cell
_getCellDivCSSHeight : function (rowHeight, record, rowNum, isAnimationRow) {
// vertically clip if..
var shouldWriteHeight =
//>Animation it's the animation row on a row reveal animation,
// where if we don't clip, we'll just reveal the new rows
// immediately
isAnimationRow || //", _$divEnd:"",
_getCellValue : function (record, rowNum, colNum) {
//!DONTCOMBINE
var value = this.getCellValue(record, rowNum, colNum, this);
// If a record has an associated component to display, add a spacer underneath the record
// to force the contents to draw above the component.
if (!this.isPrinting && this._writeEmbeddedComponentSpacer(record)) {
var details = this._getExtraEmbeddedComponentHeight(record);
if (details.allWithin) {
if (details.extraHeight && (details.extraHeight > this.cellHeight)) {
value = [value,
// Enclose the spacer in a div. This makes it a block level element
// (Forces it to appear on a new line), without requiring an explicit
//
which can further increase the height if the content of the
// cell included a block level element
this._$divStart,
isc.Canvas.spacerHTML(1, details.extraHeight-this.cellHeight),
this._$divEnd].join(isc.emptyString);
//isc.logWarn("In _getCellValue: details are "+isc.echoAll(details));
}
} else if (details.extraHeight && details.extraHeight > 0) {
value = [value,
this._$divStart,
isc.Canvas.spacerHTML(1, details.extraHeight),
this._$divEnd].join(isc.emptyString);
//isc.logWarn("In _getCellValue: details are "+isc.echoAll(details));
}
// write embedded components right into cells if printing...
} else if (record && record._embeddedComponents != null) {
var components = record._embeddedComponents || [];
for (var i = 0; i < components.length; i++) {
var component = record._embeddedComponents[i];
if (component == null) continue;
if (component._currentColNum != colNum) continue;
var isWithin = (component.embeddedPosition == this._$within);
var cPrintHTML = component._gridBodyPrintHTML;
if (cPrintHTML != null) {
value += isWithin ? this._$divStart + cPrintHTML + this._$divEnd : cPrintHTML;
// clean that property up as we go
delete component._gridBodyPrintHTML;
}
}
}
return value;
},
_writeEmbeddedComponentSpacer : function (record) {
return (record && record._embeddedComponents) != null;
},
getCellValue : function (record, rowNum, colNum) {
return this.emptyCellValue;
},
// Specifying Table Geometry
// --------------------------------------------------------------------------------------------
//> @method gridRenderer.getTotalRows()
// Return the total number of rows in the grid.
//
// NOTE: in order to create a valid grid, you must either provide a totalRows value or implement
// getTotalRows()
//
// @return (number)
// @see attr:totalRows
// @visibility external
//<
getTotalRows : function () {
return this.totalRows;
},
// NOTE: this.fields and the GridRenderer
// fields are currently used for the following config:
// - column width
// - column header alignment
// - column cell alignment
// It may be more appropriate to handle the above via a getColumnWidth()/getAlign interface.
//> @method gridRenderer.setColumnWidth()
// Sets the width of a single column.
//
// @param colNum (number) the number of the column to resize
// @param newWidth (number) the new width
//
// @visibility external
//<
setColumnWidth : function (colNum, newWidth) {
this.fields[colNum].width = this._fieldWidths[colNum] = newWidth;
this._colWidthCSSText = null;
this.markForRedraw("setColumnWidth");
},
//> @method gridRenderer.setColumnWidths()
//
// Sets the width of all columns in the grid.
//
// @param newWidths (Array) array of new widths - one for each column.
//
// @visibility external
//<
setColumnWidths : function (columnWidths) {
var oldWidths = this._fieldWidths;
// copy the widths so we aren't affected if the caller subsequently changes the array
this._fieldWidths = columnWidths.duplicate();
this._colWidthCSSText = null;
if (oldWidths != null && columnWidths != null && oldWidths.length == columnWidths.length) {
// same number of columns
// widths unchanged means no need to redraw
if (oldWidths == columnWidths) return;
var changed = false;
for (var i = 0; i < oldWidths.length; i++) {
if (oldWidths[i] != columnWidths[i]) changed = true;
}
if (!changed) return;
if (!this.fixedColumnWidths && !this.wrapCells && this.isDrawn() &&
columnWidths.length == 1) {
// NOTE: for the oldMinimum, we want the specified size as of the last time we
// drew; in the meantime, resizes that did not cause a redraw may have changed
// the logical column width
var oldMinimum = this._colWidthAtDraw || oldWidths[0],
newMinimum = columnWidths[0],
renderedSize = this.getColumnSize(0);
// If the drawn size was the old minimum, a change in minimum requires a
// redraw: a lower minimum means the content might draw smaller, and a higher
// minimum means it must draw larger.
// If the drawn size is less than the new minimum, the column will have to
// expand to that new minimum.
// Therefore the drawn size will change unless the rendered size exceeded the
// old minimum and exceeds or is equal to the new minimum.
//this.logWarn("oldMinimum: " + oldMinimum + ", newMinimum: " + newMinimum +
// ", renderedSize: " + renderedSize);
if ((oldMinimum == newMinimum) ||
(renderedSize > oldMinimum && renderedSize >= newMinimum)) {
return;
}
}
}
this.markForRedraw("setColumnWidths");
},
shouldRedrawOnResize : function (deltaX, deltaY, animating) {
if (this.redrawOnResize != null) return this.redrawOnResize;
// quick hack: if we are being resized in a Layout because some other member is animating
// don't redraw until the animation completes, otherwise our redraw will make the animation
// lurch.
if (isc.isA.ListGrid(this.parentElement) &&
isc.isA.Layout(this.parentElement.parentElement))
{
var siblings = this.parentElement.parentElement.getMembers();
if (siblings && siblings.map("isAnimating").or()) return false;
}
// redraw if our new size reveals more rows or columns
if (this._needRowRedraw() || this._needColumnRedraw()) return true;
// if we're showing the empty message we need to redraw since we write a static width / height
// into the empty message
if (this.isEmpty()) return true;
return false;
},
// string methods: getRowSpan
getRowHeight : function (record, rowNum) {
var height = this.updateHeightForEmbeddedComponents(record, rowNum, this.cellHeight);
return height;
},
updateHeightForEmbeddedComponents : function (record, rowNum, height) {
if (record && record._embeddedComponents) {
var details = this._getExtraEmbeddedComponentHeight(record, rowNum);
if (details.allWithin && details.extraHeight > 0) {
height = Math.max(height, details.extraHeight);
//this.logWarn("in updateHeightForEmbeddedComponents ("+this.grid+"): details are "+isc.echoAll(details)+"\nheight is "+height);
} else {
height += details.extraHeight;
//this.logWarn("in updateHeightForEmbeddedComponents ("+this.grid+"): details are "+isc.echoAll(details)+"\nheight is "+height);
}
}
return height;
},
_getExtraEmbeddedComponentHeight : function (record, rowNum) {
var components = record._embeddedComponents || [],
maxComponentHeight = 0,
allWithin = true,
isPrinting = this.isPrinting
;
for (var i = 0; i < components.length; i++) {
var component = record._embeddedComponents[i];
// when printing, we write colspanning components into a separate row
if (isPrinting) continue;
// mark the component with the row it currently appears in
if (rowNum != null) component._currentRowNum = rowNum;
var isWithin = (component.embeddedPosition == this._$within);
if (!isWithin) allWithin = false;
var tempHeight = component.getVisibleHeight();
var componentHeight = (isWithin ? (tempHeight > this.cellHeight ? tempHeight : 0) : tempHeight);
// expand the row so that the component appears under the normal cells
if (component._percent_height != null) {
component.height = component._percent_height;
componentHeight = this.cellHeight;
}
var origHeight = component.specifiedHeight;
if (isWithin && origHeight && isc.isA.String(origHeight) && origHeight.contains("%"))
componentHeight = 0;
if (componentHeight > maxComponentHeight) {
maxComponentHeight = componentHeight;
}
}
return { allWithin: allWithin, extraHeight: maxComponentHeight };
},
// get the row where the cell at the given coordinates starts
getCellStartRow : function (rowNum, colNum) {
var spans = this.fields[colNum]._rowSpans;
// no spanning at/above this cell
if (spans == null || spans[rowNum] == null) return rowNum;
//this.logWarn("span at row/col: " + [rowNum, colNum] + " start row: " + spans[rowNum]);
return spans[rowNum];
},
// get the number of cells spanned by the cell at the given coordinates
getCellRowSpan : function (rowNum, colNum) {
var spans = this.fields[colNum]._rowSpans;
var startRow = this.getCellStartRow(rowNum, colNum);
if (startRow == rowNum) return 1; // no spanning at/above this cell
// find where the span starts. NOTE: knowing the startRow, we could call the user-defined
// getRowSpan() again, but it could return a value that differs from the currently rendered
// table, or it might be written in such a way that it crashes if not called from the HTML
// generation loop!
var currentRow = rowNum + 1,
// span extends at least from startRow to rowNum
spannedCells = rowNum - startRow + 1;
// see how much further this span extends
while (currentRow <= this._lastDrawnRow &&
spans[currentRow] == startRow)
{
currentRow++;
spannedCells++;
}
return spannedCells;
},
// Embedded Components
// --------------------------------------------------------------------------------------------
// You can call addEmbeddedComponent to associate a component with a given record and row or cell
// The "position" attribute specifies how the component should appear
// - "expand" (the default): After being added, the component will appear "embedded" in the
// row, beneath the row's data.
// - "within": The component will appear aligned with the top left edge of the row or cell (may use
// snapTo to specify different edge to attach to). If percentage sizing is specified, component
// will size to percentage of record.
//
// NOTE: embedded components are currently only supported for uses of the gridRenderer that
// return some record for each row. We could associate components with row numbers, but in
// most uses an embedded component changes row number (eg ListGrid sort, TreeGrid
// expand/collapse), so we'd need the caller to tell us about rowNum changes.
//
// Method to actually attach a component to a record
_$within:"within",
_$expand:"expand",
_$cell:"cell",
addEmbeddedComponent : function (component, record, rowNum, colNum, position) {
if (position == null) position = this._$expand;
// if position is "expand", or fixedRowHeights is false (and the
// embedded component height > specified row height) we may expand records.
var mayChangeRowHeight = ((position == this._$expand) || !this.fixedRowHeights);
// instantiate the component if it's passed as just properties
if (!isc.isA.Canvas(component)) {
component.autoDraw = false;
var cons = isc.ClassFactory.getClass(component._constructor);
if (cons == null) cons = isc.Canvas;
component = cons.create(component);
}
var moveOnly = false;
// if addEmbeddedComponent is called twice on the same comp, remove before embedding!
if (this._embeddedComponents && this._embeddedComponents.contains(component)) {
// already embedded at the right spot = a no op
// Note:
if (record._embeddedComponents && record._embeddedComponents.contains(component) &&
component.embeddedPosition == position &&
component._currentRowNum == rowNum && component._currentColNum == colNum)
{
return;
}
// we can avoid a redraw if
// position is within, this.fixedRowHeights is true,
// and position is unchanged
// (and we're not dirty already)
if (position == component.embeddedPosition && !mayChangeRowHeight) {
moveOnly = !this.isDirty();
}
// third param to suppress clear / redraw - we'll take care of that
this.removeEmbeddedComponent(component.embeddedRecord, component, true);
} else if (!mayChangeRowHeight) {
moveOnly = !this.isDirty();
}
if (!record._embeddedComponents) record._embeddedComponents = [];
// Make the record hang onto the component
record._embeddedComponents.add(component);
// add the component to the list of embedded components.
if (this._embeddedComponents == null) this._embeddedComponents = [];
this._embeddedComponents.add(component);
component.embeddedPosition = position;
component.embeddedRecord = record;
// set up the current row / colNum passed in
// note that if we redraw to render the embedded component these will be recalculated
// anyway, but by setting up an initial currentColNum we ensure the component is
// embedded in a cell rather than a row!
component._currentRowNum = rowNum;
component._currentColNum = colNum;
// for frozen columns, mark the component with the id of the GridBody it's being stored in
component._embedBody = this.getID();
// if position == "within" we'll handle percentage sizing and snapTo ourselves
// unexposed flag to disable standard snapTo / percent sizing logic
component.percentBox = "custom";
// add it as a child (which will force a draw, and give us a size) - hide it first so it
// doesn't appear and then get moved into place
// temporarily suppress adjustOverflow while we do this so we don't show huge
// scrollbars if the thing is sized to 100% wide or tall
if (component.parentElement != this) {
var wasSuppressingAO = this._suppressAdjustOverflow;
this._suppressAdjustOverflow = true;
component.hide();
this.addChild(component);
if (wasSuppressingAO == null) delete this._suppressAdjustOverflow;
}
this.observe(component, "resized",
"observer._handleEmbeddedComponentResize(observed, deltaX, deltaY)");
// don't redraw the component when the grid redraws, otherwise we'll be redrawing embedded
// components continually during scrolling. NOTE: it may be that this should be the
// default for parents that have a mixture of content and children.
component.__oldRedrawWithParent = component._redrawWithParent;
component._redrawWithParent = false;
// prevent bubbling of most mouse events while the component is embedded. We still want
// mouseWheel events to bubble or it feels like scrolling is broken when embeddedComponents
// scroll under the mouse. We prevent other events because otherwise,
// cellClick, recordClick et al will fire while the component is embedded, which is usually
// wrong.
component._origBubbleMouseEvents = component.bubbleMouseEvents;
if (!component.bubbleMouseEvents) {
// prevent most mouse events from bubbling
component.bubbleMouseEvents = ["mouseDown", "mouseUp", "click", "doubleClick", "contextClick"];
}
// If the component is going to expand the row we'll need a redraw
// Also, if we don't know the rowNum / colNum for the record, this will get picked up at redraw
// time
// Otherwise we just move the canvas into place.
if (moveOnly && (rowNum == -1 || colNum == -1)) {
moveOnly = false;
}
if (moveOnly) {
this.placeEmbeddedComponent(component);
} else {
// redraw, which will draw the row at the new height and place the component
this.markForRedraw("added embedded component");
}
return component;
},
_handleEmbeddedComponentResize : function (component, deltaX, deltaY) {
var position = component.embeddedPosition;
// if the embedded component resizes vertically, redraw so the row becomes the right size
if (position != this._$within) {
if (deltaY!=null && deltaY!=0) this.markForRedraw('embedded component resized');
// If positioned within the cell, respond to resized (EG adjustOverlow) by
// repositioning so snapTo continues to work...
} else {
this.placeEmbeddedComponent(component);
}
},
// updateEmbeddedComponentCoords() called when we render out a record with an embedded component
// rowNum / colNum indicate the rowNum/colNum on which the record has been rendered
// (colNum is ambiguous on 1 record/row model)
// Default behavior leaves colNum untouched. Note that if we have one record per cell we might
// want to update colNum here, but we don't know that about our data model - rely on overrides
// to achieve this if required.
updateEmbeddedComponentCoords : function (components, record, rowNum, colNum) {
components.setProperty("_currentRowNum", rowNum);
},
// place an embedded component over the correct row.
// Ideally this would only be called on sort, dataChanged, etc -- currently being called
// on every body redraw (may impact performance when incremental scrolling, for example)
placeEmbeddedComponent : function (component) {
var rowNum = component._currentRowNum;
if (rowNum == null || rowNum < this._firstDrawnRow || rowNum > this._lastDrawnRow) {
// row is no longer drawn - clear component
if (component.isDrawn()) component.clear();
return;
}
var record = component.embeddedRecord,
position = component.embeddedPosition,
colNum = component._currentColNum,
topOrigin = this.getRowTop(rowNum),
leftOrigin = colNum != null ? this.getColumnLeft(colNum) : 0,
// to make component snap to right of visible area, use getInnerWidth() to demarcate
// the snap area, or the sum of all column widths, whichever is smaller
width = (colNum != null && colNum >= 0) ? this.getColumnWidth(colNum) :
Math.min(this.getInnerWidth() + this.getScrollLeft(), this._fieldWidths.sum()) ;
// this.logInfo("Placing embedded component " + component + ", row/col:" + [rowNum,colNum]
// + ", top/left cell origin:" + [topOrigin,leftOrigin], "embeddedComponents");
if (position == this._$within) {
// Respect "snapTo" if specified
// *Note: we are suppressing standard canvas percent sizing and snap-to behavior
// so we can explicitly size / position based on cell coordinates
var snapTo = this.getEmbeddedComponentSnapTo(component, record, rowNum, colNum),
snapEdge = component.snapEdge || snapTo;
// figure out sizes before placing!
var height = this.getRowSize(rowNum),
cpw = component._percent_width,
cph = component._percent_height,
cw, ch;
// If positioned offset from the left, shrink the target space
if (component.snapOffsetLeft) width -= component.snapOffsetLeft;
if (isc.isA.String(cpw) && cpw.endsWith("%")) {
cw = Math.round((parseInt(cpw) * width) / 100);
}
if (isc.isA.String(cph) && cph.endsWith("%")) {
ch = Math.round((parseInt(cph) * height) / 100);
}
var compHeight = ch != null ? ch : component.getHeight(),
compWidth = cw != null ? cw : component.getWidth();
if (ch || cw) {
component.resizeTo(cw, ch);
// retain percentages so we reflow correctly!
component._percent_width = cpw;
component._percent_height = cph;
}
// pass row/column dimensions to snapToEdge in lieu of a canvas
isc.Canvas.snapToEdge([leftOrigin, topOrigin, width, height], snapTo, component, snapEdge);
} else {
// NOTE: if you need multiple "expand" components to expand a single row, generally
// you're expected to use a Stack or Layout to manage them.
// float at the bottom of the row, rather than the top
//topOrigin += this.cellHeight;
component.moveTo(leftOrigin, topOrigin);
// Note that setWidth() may adjust the visibleHeight of the component.
component.setWidth(width);
}
var showing = this.isDrawn();
if (showing && !component.isDrawn()) component.draw();
// at this point we can measure the component to see if it forces vertical expansion of the
// row
// Note that the row's height as written into the dom is set via getRowHeight() which already
// checks the 'visibleHeight' of all embedded components, so the cases we're catching here are
// if the visibleHeight was misreported before this method ran.
// We've seen this happen when the component overflows its specified size, specifically:
// - if the setWidth() call above caused an already drawn component to reflow and overflow
// vertically in a different manner
// - if the component was scrolled out of view and clear()'d [cleared components return
// specified height from adjustOverflow], and scrolling back into view is draw()ing it again.
//
// Note: We observe the 'resized' method of the embedded component, so when resizing due
// to the component size changing (such as adjustOverflow from the setWidth call above)
// we get a markForRedrawCall() which in turn runs '_placeEmbeddedComponents()' and resizes/
// repositions all embedded components.
// If we're currently in the process of redrawing however, we'll be marked as dirty so this
// if resized trips here in response to the above setWidth or draw(), markForRedraw will have
// no effect - catch this case (via an isDirty() check) and explicitly size the
// row to the new desired height.
// NOTE: when we redraw we run through '_placeEmbeddedComponents' which positions/sizes
// all embedded components. We do this in rowNum order, so if there are subsequent embedded
// components rendered into the grid these should get shifted down automatically if a row
// above them is expanded by this method.
// if (position != this._$within) {
var redrawing = this.isDirty(),
expectedRowHeight = this.getRowHeight(record,rowNum),
// we need to size the row if
// - the resized observation didn't trip and mark us as dirty
// - we are already mid-redraw so being marked as dirty had no effect
needsResize = !this.isDirty() || redrawing;
if (needsResize && (expectedRowHeight != this.getRowSize(rowNum))) {
this.setRowHeight(rowNum, expectedRowHeight, record);
// refreshing the content ensures we re-write the spacer, which causes the
// content to top-align properly
this.refreshRow(rowNum);
}
// }
if (showing) {
if (position != this._$within) {
var offset = this.getDrawnRowHeight(rowNum) - component.getVisibleHeight() - 1;
component.moveTo(null, this.getRowTop(rowNum) + offset);
}
if (!component.isVisible()) {
if (this.shouldAnimateEmbeddedComponent(component)) {
component.animateShow();
} else {
component.show();
}
}
}
this.updateEmbeddedComponentZIndex(component);
},
alignSnapToMap:{
left:{
top:"TL",
center:"L",
bottom:"BL"
},
right:{
top:"TR",
center:"R",
bottom:"BR"
},
center:{
top:"T",
center:"C",
bottom:"B"
}
},
getEmbeddedComponentSnapTo : function (component, record, rowNum, colNum) {
if (component.snapTo != null) return component.snapTo;
if (colNum == null) {
return "TL"
}
var align = this.getCellAlign(record, this.fields[colNum], rowNum, colNum) || "center",
valign = this.getCellVAlign(record, this.fields[colNum], rowNum, colNum) || "center";
var result = this.alignSnapToMap[align][valign];
//this.logWarn("result:"+ result);
return result;
},
// should this embeddedComponent animate show?
shouldAnimateEmbeddedComponent : function (component) {
return false;
},
// update the zindex of embedded components. Overridden at the LG level by default
updateEmbeddedComponentZIndex : function (component) {
},
getEmbeddedComponent : function (record, colNum) {
// support specifying rowNum instead
if (isc.isA.Number(record)) record = this.getCellRecord(record, 0);
var components = record._embeddedComponents;
if (components == null) return;
var component = null;
if (isc.isA.Number(colNum))
component = components.find({_currentColNum: colNum, _embedBody: this.getID()});
return component;
},
removeEmbeddedComponent : function (record, component, suppressRedraw) {
// support specifying rowNum instead
if (isc.isA.Number(record)) record = this.getCellRecord(record, 0);
var components = record._embeddedComponents;
if (components == null) return;
// support specifying component by colNum
if (isc.isA.Number(component))
component = components.find({_currentColNum: component, _embedBody: this.getID()});
if (!component) // a single expansion component
component = components.find({ _embedBody: this.getID() });
if (!components.contains(component)) return;
if (this.isObserving(component, "resized")) {
this.ignore(component, "resized"); // stop watching for resizes
}
record._embeddedComponents.remove(component);
if (record._embeddedComponents.length == 0) record._embeddedComponents = null;
if (this._embeddedComponents) this._embeddedComponents.remove(component);
// reset redraw w/parent flag to original setting
component._redrawWithParent = component.__oldRedrawWithParent;
component.__oldRedrawWithParent = null;
// reset bubbleMouseEvents setting
component.bubbleMouseEvents = component._origBubbleMouseEvents;
var expand = component.embeddedPosition == this._$expand;
component.embeddedPosition = null;
component._currentRowNum = null;
component._currentColNum = null;
component._embedBody = null;
// suppress redraw - used when an embedded component is just being shifted to another record
if (suppressRedraw) {
// hide even if we don't clear/draw -- this ensures we re-animate if appropriate
component.hide();
return;
}
if (component.destroyOnUnEmbed) component.destroy();
else {
// clear it and clear up references to the record
this.removeChild(component);
}
// no need to redraw if the component didn't effect the size of any content
if (expand) {
this.markForRedraw("removed embedded component");
}
},
// before each redraw, clear the property holding the rowNum where the component was found. Hence
// we ensure that if a component isn't found during rendering it gets hidden.
// Leave the colNum intact -
_resetEmbeddedComponents : function () {
var components = this._embeddedComponents;
if (components == null) return;
components.setProperty("_currentRowNum", null);
},
// ensure all embedded components are in the right place. Called after every redraw.
_placeEmbeddedComponents : function () {
var components = this._embeddedComponents;
if (components == null) return;
// sort by current row num. This means we place the components in the order in which they're
// drawn within the table. If their heights change and they expand their containing rows, they
// will therefore also change the top coords of subsequent rows
components.sortByProperty("_currentRowNum", true);
for (var i = 0; i < components.length; i++) {
this.placeEmbeddedComponent(components[i]);
}
},
// Apply a known z-index to the table so we can float embedded components below it if necessary
getTableZIndex : function () {
// default Canvas range starts around 200000
// Give the table a zindex of 1000 - widgets will still float above it by default (even when
// sendToBack() is called), but we can explicitly force them to appear below it if necessary
return 1000;
},
// Cell Styling
// --------------------------------------------------------------------------------------------
//> @attr gridRenderer.recordCustomStyleProperty ( "customStyle" : string : IRW)
// Denotes the name of a property that can be set on records to display a custom style.
// For example if this property is set to "customStyle"
, setting
// record.customStyle
to a css styleName will cause the record in question to
// render out with that styling applied to it. Note that this will be a static
// style - it will not be modified as the state of the record (selected / over etc) changes.
// @see gridRenderer.getCellStyle()
// @visibility external
//<
recordCustomStyleProperty:"customStyle",
//> @attr gridRenderer.showSelectedStyle ( boolean : true : IRW )
// Should the "Selected" style be applied to selected records?
// @see gridRenderer.getCellStyle()
// @visibility external
//<
showSelectedStyle:true,
//> @method gridRenderer.getCellStyle()
// Return the CSS class for a cell. By default this method has the following implementation:
// - return any custom style for the record (see +link{GridRenderer.recordCustomStyleProperty})
// if defined.
// - create a style name based on the result of +link{gridRenderer.getBaseStyle()} and the
// state of the record.
// The state of the record is indicated by adding a suffix to the base style.
// There are four independent boolean states, which are combined in the order given:
//
// - "Disabled" : whether the cell is disabled; enable by setting the "enabled" flag on record
// returned by getCellRecord
//
- "Selected" : whether cell is selected; enable by passing a Selection object as "selection"
//
- "Over" : mouse is over this cell; enable with showRollovers
//
- "Dark" : alternating color bands; enable with alternateRowStyles
//
// For example, with a baseStyle of "myCell", a cell which is selected, which the mouse is over,
// and which is in a dark-colored band will get a styleName of myCellSelectedOverDark.
//
// Cell Styles customizable by:
//
// - attaching a custom style to a record by setting
//
record[this.recordCustomStyleProperty]
to some valid CSS style name.
// - modifying the base style returned by getBaseStyle() [see that method for further
// documentation on this]
//
- overriding this function
//
//
// "Selected" style can be ignored by setting +link{gridRenderer.showSelectedStyle} to false.
//
// @param record (ListGridRecord) record object for this row and column
// @param rowNum (number) number of the row
// @param colNum (number) number of the column
//
// @return (CSSStyleName) CSS style for this cell
// @group appearance
// @visibility external
//<
getCellStyle : function (record, rowNum, colNum) {
// Allow a record to apply it's own style - ignoring our styling code
if (record && record[this.recordCustomStyleProperty] != null) {
return record[this.recordCustomStyleProperty];
}
// If not using an entirely custom style, determine the cell state and
// get the appropriate suffix.
var styleIndex = this.getCellStyleIndex(record, rowNum, colNum);
return this.getCellStyleName(styleIndex, record, rowNum, colNum);
},
getCellStyleName : function (styleIndex, record, rowNum, colNum) {
var standardSuffixes = isc.GridRenderer.standardStyleSuffixes;
// Are we dynamically determining the baseStyle from this.getBaseStyle() ?
// If so, concat baseStyle with the appropriate suffix
if (this.getBaseStyle) {
var baseStyle = this.getBaseStyle(record, rowNum, colNum);
// check if the baseStyle returned is exactly the same String instance as
// this.baseStyle, in which case we can use the precomputed style combinations. This
// would happen if someone defined a custom getBaseStyle that usually returns
// this.baseStyle, infrequently returning special values.
if (baseStyle !== this.baseStyle) {
// append the appropriate suffix to the baseStyle
if (styleIndex == 0) return baseStyle; // styleIndex 0 is the empty suffix
return baseStyle + standardSuffixes[styleIndex];
}
}
// In this case we're using the default baseStyle
// Cache the entire set of cellStyles
if (!this._cellStyles) {
this._cellStyles = [];
for (var i = 0; i < standardSuffixes.length; i++) {
this._cellStyles[i] = this.baseStyle + standardSuffixes[i];
}
}
// return the style
return this._cellStyles[styleIndex];
},
// return the index of the current state. The index is a bitfield containing flags for each of
// the mutually exclusive style states: Over, Selected, Disabled, and Dark (Ledger). The
// purpose of computing an index rather than computing the string directly is for speed.
getCellStyleIndex : function (record, rowNum, colNum) {
// Note - we have an array of the 12 applicable suffixes in gridRenderer.standardSuffixes
//
// 0 = baseStyle
// 1 = Over(1)
// 2 = Selected(2)
// 3 = Selected(2) + Over(1)
// 4 = Disabled(4)
// 5 = Disabled(4) + Over(1)
// 6 = Disabled(4) + Selected(2)
// 7 = Disabled(4) + Selected(2) + Over(1)
// 8 = Dark(8)
// 9 = Dark(8) + Over(1)
// 10 = Dark(8) + Selected(2)
// 11 = Dark(8) + Selected(2) + Over(1)
// 12 = Dark(8) + Disabled(4)
//
// NOTE: By default, disabled is actually mutually exclusive with Selected and Over states,
// so states 5-7 never happen and no style declaration is required.
var styleIndex = 0; // base style
var useAlternateStyle = true;
if (this.grid != null) {
var field = this.grid.getField(this.grid.getFieldNumFromLocal(colNum, this));
useAlternateStyle = !field ? true : field.showAlternateStyle != false;
}
// if alternating record styles, see if the cell is in a dark band
if (this.alternateRowStyles && useAlternateStyle) {
var isOdd = (Math.floor(rowNum / this.alternateRowFrequency) % 2 == 1);
if (isOdd) styleIndex += 8;
}
// Disabled?
if (!this.cellIsEnabled(rowNum, colNum)) {
styleIndex += 4;
// Not disabled - check for selected and/or over
} else {
// if we're over the row or cell - add 1 to get the Over style
if (this.shouldShowRollOver(rowNum, colNum) && !this.isPrinting &&
rowNum == this.lastOverRow &&
(!this.useCellRollOvers || colNum == this.lastOverCol))
{
styleIndex += 1;
}
// if selection is enabled, see if the cell is selected
if (this.showSelectedStyle && this.selectionEnabled()) {
var isSelected = (this.canSelectCells ?
this.selection.cellIsSelected(rowNum, colNum) :
this.selection.isSelected(record));
// if the cell is selected, add 2 to get the Selected style
if (isSelected) styleIndex += 2;
}
}
return styleIndex;
},
//> @method gridRenderer.cellIsEnabled() ([A])
// Whether this cell should be considered enabled. Affects whether events will fire for the
// cell, and the default styling behavior in getCellStyle.
//
// @group selection, appearance
//
// @param rowNum (number) row number for the cell
// @param colNum (number) column number of the cell
// @return (boolean) whether this record is enabled or not
// @visibility external
//<
cellIsEnabled : function (rowNum, colNum) { return true; },
// Element IDs
// --------------------------------------------------------------------------------------------
//> @method gridRenderer.getTableElementId() ([A])
// Get the DOM ID that should be used for the table element. For integration with legacy test
// scripts.
// @visibility testAutomation
//<
getTableElementId : function () {
return this.getCanvasName() + "table";
},
//> @method gridRenderer.getRowElementId() ([A])
// Get the DOM ID that should be used for a row element. For integration with legacy test
// scripts.
//
// When using incremental rendering, the rowNum
param represents the
// rowNum in virtual coordinates, and the physicalRowNum
param represents the
// index that the row will ultimately have in table.rows.
//
// @param rowNum (number) virtual row number
// @param physicalRowNum (number) physical row number
// @visibility testAutomation
//<
//> @method gridRenderer.getCellElementId() ([A])
// Get the DOM ID that should be used for a cell element. For integration with legacy test
// scripts.
//
// When using incremental rendering, the rowNum
and colNum
params
// represents virtual coordinates, and the physicalRowNum
param represents the
// index that the row/cell will ultimately have in table.rows or row.cells.
//
// @param rowNum (number) virtual row number
// @param physicalRowNum (number) physical row number
// @param colNum (number) virtual col number
// @param physicalColNum (number) physical col number
// @visibility testAutomation
//<
// Table Manipulation
// --------------------------------------------------------------------------------------------
getDOMTable : function (logicalRowNum, logicalColNum) {
if (this.cacheDOM) return this.getTableChunkAt(logicalRowNum, logicalColNum);
// bail out fast if asked for a cell that isn't currently rendered
// this method is called from 'getTableElement()', so rowNum / colNum may be null
if ((logicalRowNum != null &&
(logicalRowNum - this._firstDrawnRow < 0 ||
logicalRowNum > this._lastDrawnRow))
||
(logicalColNum != null &&
(logicalColNum - this._firstDrawnCol < 0 ||
logicalColNum > this._lastDrawnCol))
)
return null;
var table = this._tableElement;
if (table == null) {
var tableName = this.getTableElementId();
var table = isc.Element.get(tableName);
if (table == null) return null;
// If we're mid-redraw, don't re-cache the current table element
if (this._suppressTableCaching) {
this.logInfo("getTableElement() called while updating table HTML. " +
"This call may be invalid as the table is being rewritten in the DOM. " +
"Suppressing caching of the current element.", "redrawing");
return table;
}
}
// cache table element
return this._tableElement = table;
},
//> @method gridRenderer.getTableElement() ([A])
// Get the element for the TD that holds a particular cell, specified as row/column indices.
//
// If called with no parameters, returns the table itself.
// If called with rowNum only, returns the row element
// If called with colNum, returns a particular cell.
//
// In all cases, returns null if the table, row, or cell cannot be found.
//
// NOTE: calling this repeatedly is expensive as it makes multiple DOM lookups.
//
// @param [rowNum] (number) Record number to get cell for.
// You DO NOT need subtract the startRow from this.
// @param [colNum] (number) Field number to get cell for.
// @return (DOMElement) Table, row or cell of the list body table.
// Returns null if the element can't be obtained.
// @group drawing
//<
getTableElement : function (logicalRowNum, logicalColNum) {
var table = this.getDOMTable(logicalRowNum, logicalColNum);
if (logicalRowNum == null) return table;
if (!table) return null;
// if we're using incremental rendering, the HTML we draw only contains _firstDrawnRow ->
// _lastDrawnRow, so when asked for row X we subtract _firstDrawnRow to find the
// corresponding DOM element. "rowNum" is now the rendered row number in the DOM table
var rowNum = logicalRowNum - (this._firstDrawnRow > 0 ? this._firstDrawnRow : 0);
if (rowNum < 0) {
//this.logWarn("bailing on negative rowNum");
return null;
}
var row;
// use cached row lookup
if (this._rowElements != null) row = this._rowElements[rowNum];
if (row == null) row = table.rows[rowNum];
if (row == null) return null;
// cache row lookup (invalidated on body redraw)
if (this._rowElements == null) this._rowElements = [];
this._rowElements[rowNum] = row;
if (logicalColNum == null) return row;
// for incremental column rendering: if we're not drawing all columns, the DOM will contain
// cells only for the columns we actually draw.
var colNum = logicalColNum - this._firstDrawnCol;
if (colNum < 0) {
//this.logWarn("bailing on negative colNum");
return null;
}
if (this.getRowSpan) {
var startRow = this.getCellStartRow(logicalRowNum, colNum);
if (startRow != rowNum) {
//this.logWarn("detected spanning cell extending from " + [startRow, colNum] +
// " to " + [logicalRowNum, colNum]);
// cell starts in a previous row - switch to the row that contains this cell
rowNum = startRow;
row = this.getTableElement(startRow);
}
if (row.cells.length < (this._lastDrawnCol - this._firstDrawnCol + 1)) {
// this row has less cells than the number of columns we drew, indicating cells from
// previous rows spanned into this row (but not at this column, which we already
// checked).
// figure out many cells are missing up to the column we're interested in
var skips = 0;
for (var i = 0; i < colNum; i++) {
if (this.fields[i]._rowSpans != null &&
this.fields[i]._rowSpans[rowNum] != null &&
this.fields[i]._rowSpans[rowNum] != rowNum) skips++;
}
//this.logWarn("in row: " + rowNum + " skipping " + skips + " cells");
// and adjust the column number appropriately
colNum -= skips;
}
}
// actually got all the way to the cell level -- return the appropriate cell
return row.cells[colNum];
},
// Cell Style and HTML Updates
// --------------------------------------------------------------------------------------------
//> @method gridRenderer._updateCellStyle()
// Update the CSS styling for a cell. Will also update the row's height, and the cell's
// innerHTML if appropriate.
//
// @group appearance
// @param [record] (object) reference to the record object who's style is being set
// @param rowNum (number) row number of the cell
// @param colNum (number) col number of the cell
// @param cell (DHTML object) reference to the HTML table cell element
// @param [className] (string) name of the CSS class of the style to be used
//
//<
_updateCellStyle : function (record, rowNum, colNum, cell, className) {
// get the DOM cell object if not provided
if (cell == null) cell = this.getTableElement(rowNum, colNum);
if (cell == null) return; // cell not currently drawn
if (record == null) record = this.getCellRecord(rowNum, colNum);
// determine the CSS style className if not provided
if (className == null) className = this.getCellStyle(record, rowNum, colNum);
//this.logWarn("setting: " + [rowNum, colNum] + " to className:" + className);
if (this.fastCellUpdates) {
cell.style.cssText = this._getCompleteCellCSSText(record, rowNum, colNum, className);
} else {
// update the classname on the DOM cell object
if (cell.className != className) cell.className = className;
// If this.getCellCSSText has been defined, set cell.style.cssText
if (this.getCellCSSText) {
// Use this._getCompleteCellCSSText
// This handles the overflow settings for Moz, converting the
// getCellCSSText stringMethod to a method, etc.
cell.style.cssText = this._getCompleteCellCSSText(record, rowNum, colNum, className)
}
}
// if aspects of styling are inncorporated into the cell's innerHTML, refresh the cell
if (this.shouldRefreshCellHTML(record, rowNum, colNum)) {
this.refreshCellValue(rowNum, colNum);
}
if (!this.isDrawn()) return;
var shouldClip = this.fixedRowHeights &&
(this.shouldFixRowHeight == null ||
this.shouldFixRowHeight(record, rowNum) != false),
newHeight = (this.getRowHeight != null ? this.getRowHeight(record, rowNum)
: this.cellHeight);
this.setRowHeight(rowNum, newHeight, record, className, shouldClip);
},
_$nobr:"NOBR",
_$cellClipDiv:"cellClipDiv",
_getCellClipDiv : function (cellElement) {
if (cellElement == null) return null;
var div = cellElement.childNodes[0];
if (!div) return null;
// In IE the first child of a cell is actually a NOBR element - we need to look inside that
// to get the cell clip div
if (div.tagName == this._$nobr) div = div.childNodes[0];
if (div &&
(div.cellClipDiv ||
(div.getAttribute && div.getAttribute(this._$cellClipDiv)) ) )
{
return div;
}
return null;
},
//> @method gridRenderer.setRowHeight()
// Sets the height of some row to the height passed in.
// This is a styling effect only - a redraw will revert to the height as derived from
// this.cellHeight / this.getRowHeight()
// @param rowNum (number) rowNum to set height on
// @param newHeight (number) height for the row
//<
// Additional params are not required, but make the method more efficient
// Also used by showInlineEditor to make a row overflow:visible for tall editors
_$height:"height",
_$minHeight:"minHeight",
setRowHeight : function (rowNum, newHeight, record, className, shouldClip, instantOverflow) {
var firstDrawnCol = this._firstDrawnCol,
lastDrawnCol = this._lastDrawnCol;
if (shouldClip == null) {
if (record == null) record = this.getCellRecord(rowNum, firstDrawnCol);
shouldClip = this.fixedRowHeights &&
(this.shouldFixRowHeight == null ||
this.shouldFixRowHeight(record, rowNum) != false);
}
var firstCell = this.getTableElement(rowNum, firstDrawnCol),
currentSpecifiedHeight = firstCell ? parseInt(firstCell.height) : null,
heightChanged
;
if (!isc.isA.Number(currentSpecifiedHeight)) currentSpecifiedHeight = null;
if ((isc.Browser.isSafari || isc.Browser.isIE) && isc.Browser.isStrict) {
if (record == null) record = this.getCellRecord(rowNum, firstDrawnCol);
var cellStyle = className;
if (cellStyle == null) cellStyle = this.getCellStyle(record, rowNum, firstDrawnCol)
newHeight -= this.fixedRowHeights ? isc.Element._getVBorderSize(cellStyle)
: isc.Element._getVBorderPad(cellStyle);
if (!this.fixedRowHeights) newHeight -= (this.cellPadding *2)
}
// if we were previously clipping and will not any longer
if ((!shouldClip && currentSpecifiedHeight != null) ||
// or we're changing the specified height (clipped or not)
(currentSpecifiedHeight != newHeight &&
!(currentSpecifiedHeight == null && newHeight == isc.emptyString)))
{
// the height of the cell (therefore the row) has changed
heightChanged = true;
}
// If the height of this row has changed, we need to update (or clear) the specified
// heights of each cell in the row.
if (!heightChanged) return;
var numericHeight = isc.isA.Number(newHeight);
if (numericHeight && newHeight <=0) newHeight = shouldClip ? 0 : 1;
// this.logWarn("height changed for cell in row: " + rowNum +
// ", currentSpecifiedHeight: " + currentSpecifiedHeight +
// ", shouldClip?:" + shouldClip +
// " (derived from firstCell.height: " + firstCell.height + ")" +
// ", newHeight: " + newHeight);
var currentRow = this.getTableElement(rowNum);
if (newHeight == 0 && shouldClip) {
currentRow.style.display = "none";
//var firstCell = this.getTableElement(rowNum, firstDrawnCol);
//this.logWarn("first row height is: " + firstCell.offsetHeight);
} else {
// should theoretically be "table-row", but IE doesn't currently support that value,
// and they all seem to accept ""
currentRow.style.display = isc.emptyString;
for (var i = firstDrawnCol; i <= lastDrawnCol; i++) {
var currentCell = this.getTableElement(rowNum, i);
if (currentCell) {
var cssProp = (!isc.Browser.isIE || isc.Browser.isStrict) ? this._$height
: this._$minHeight;
if (shouldClip) {
currentCell.height = newHeight;
currentCell.style[cssProp] = isc.emptyString;
} else {
currentCell.height = isc.emptyString;
currentCell.style[cssProp] = newHeight;
}
var clipDiv = this._getCellClipDiv(currentCell),
clipDivHeight = (shouldClip ? (numericHeight ? newHeight + isc.px : newHeight)
: isc.emptyString);
if (clipDiv) {
if (isc.Browser.isMoz || isc.Browser.isSafari)
clipDiv.style.maxHeight = clipDivHeight;
else
clipDiv.style.height = clipDivHeight;
}
}
}
}
if (isc.Browser.isSafari && this._forceRowRefreshForAnimation) {
var row = this.getTableElement(rowNum);
if (row != null) {
row.innerHTML = row.innerHTML;
}
}
// clear the cache of rowHeights since at least this one has changed, and mark for
// adjustOverflow as the overall height of the body will have changed too.
this._clearTableCache();
if (instantOverflow) {
this.adjustOverflow("cell height changed");
} else {
this._markForAdjustOverflow("cell height changed");
}
},
//> @method gridRenderer._getCompleteCellCSSText() (I)
//
// Returns complete CSS text for a cell.
//
// If this.fastCellUpdates is true, this method will return both the raw CSS text associated
// with the style, and any custom CSS text set up by the public getCellCSSText() method
// If false, this method returns no style CSS text, and just falls through to getCellCSSText()
//
// @param record (ListGridRecord) record for this row or cell
// @param rowNum (number) row number
// @param colNum (number) column number
// @param [style] (string) CSS class style name to apply
//
// @return (CSSText) CSS text to style this cell
// @group appearance
//<
_$semi:";",
_$zeroVPadding:"padding-top:0px;padding-bottom:0px;",
_$overflowHidden:"overflow:hidden;",
_getCompleteCellCSSText : function (record, rowNum, colNum, className) {
var cssText = null;
// Make sure top and bottom padding are set to zero if fixedRowHeights is true
if (this.fixedRowHeights) cssText = this._$zeroVPadding;
else {
cssText = this._getMinHeightCSSText(record,rowNum);
}
if (isc.Browser.isIE8Strict) {
if (cssText == null) cssText = this._$overflowHidden;
else cssText += this._$overflowHidden;
}
// For Moz, pre-pend the width and overflow cssText
if (isc.Browser.isMoz || isc.Browser.isSafari) {
if (cssText == null) cssText = this._getCSSTextForColWidth(colNum);
else cssText += this._getCSSTextForColWidth(colNum);
}
if (this.fastCellUpdates) {
// figure out the style for this cell if not provided
if (className == null) className = this.getCellStyle(record, rowNum, colNum);
//this.logWarn("_getCompleteCellCSSText style: " + className);
// get CSS text for this style
var styleText = isc.Element.getStyleText(className, true);
if (styleText == null && isc.Page._remoteStyling) {
this.logInfo("fastCellUpdates set to true but this page loads styles from a " +
"remote stylesheet. This is unsupported - disabling fastCellUpdates.");
this.fastCellUpdates = false;
this.redraw();
}
if (cssText != null) cssText += styleText;
else cssText = styleText;
}
// Get any custom CSSText derived from this.getCellCSSText
if (this.getCellCSSText) {
var customCSSText = this.getCellCSSText(record, rowNum, colNum)
if (customCSSText != null) {
// Ensure the custom css text ends with a semi
if (!customCSSText.endsWith(this._$semi)) {
customCSSText += this._$semi;
}
if (cssText != null) cssText += customCSSText
else cssText = customCSSText
}
}
return cssText;
},
// does this cell need to update its HTML in order to show hiliting/styling
shouldRefreshCellHTML : function (record, rowNum, colNum) {
return this.showHiliteInCells;
},
// Helper method to check that we can safely refresh a cell (or row) without delaying
_readyToRefreshCell : function (rowNum, colNum) {
if ((isc.EH._handlingMouseUp || isc.EH._handlingMouseDown) && isc.EH.lastEvent.target == this) {
var eventRow = this.getEventRow();
if (eventRow != rowNum) return true;
if (colNum != null) {
var eventCol = this.getEventColumn();
if (colNum != eventCol) return true;
}
// If the event occurred on the same row (and col for a cell), we can't redraw in
// the same thread
return false;
}
return true;
},
//> @method gridRenderer.refreshCellValue() ([A])
// Update just cell value without updating cell styling.
// @param rowNum (number) Row number of the cell to refresh
// @param colNum (number) Column number of the cell to refresh
//<
refreshCellValue : function (rowNum, colNum) {
// get a pointer to the cell, if possible
var cell = this.getTableElement(rowNum, colNum);
if (!cell) return; // cell not currently drawn
// If we need to delay the refresh, fire again after a delay
if (!this._readyToRefreshCell(rowNum, colNum)) {
this.delayCall("refreshCellValue", [rowNum, colNum]);
return;
}
var record = this.getCellRecord(rowNum, colNum),
field = this.fields[colNum]
;
// Allow refreshing of null records - this may occur with separator rows, loading rows,
// etc.
if (!field) {
//>DEBUG
this.logDebug("refreshCell called for invalid field " + colNum); //"
}
// NOBR tags if we're not wrapping cells
if (!this.wrapCells) innerHTML += "";
// Get the actual value for the cell
innerHTML += this._getCellValue(record, rowNum, colNum);
// close the NOBR tag if necessary
if (!this.wrapCells) innerHTML += " ";
// close the DIV if necessary
if (writeDiv) innerHTML += "";
// Actually apply the innerHTML to the innerHTML of the cell.
cell.innerHTML = innerHTML;
},
//> @method gridRenderer.setCellStyle()
// Set the CSS class of a record
// @group appearance
//
// @param rowNum (number) row number to set class of
// @param colNum (number) column number to set class of
// @param [className] (CSSStyleName) name of the CSS class to set to; if not specified,
// will use getCellStyle()
//<
setCellStyle : function (rowNum, colNum, className) {
// Just fall through to setRowStyle
return this.setRowStyle(rowNum, className, colNum);
},
//> @method gridRenderer.setRowStyle()
// Set the CSS class of a record
// @group appearance
//
// @param rowNum (number) record number to set class of. This takes this._firstDrawnRow into account
// @param [className] (CSSStyleName) name of the CSS class to set to; if not specified, will
// use getCellStyle()
// @param [colNum] (number) column number to set class of. If not specified, will
// set all columns in that row.
//<
setRowStyle : function (rowNum, className, colNum) {
if (isc._traceMarkers) arguments.__this = this;
// navigate into the DOM and change the contents of the native table cells
// if the rowNum is null, use this.selection.lastSelectionItem
if (rowNum == null || rowNum < 0) {
this.logWarn("setRowStyle: bad rowNum: " + rowNum);
return false;
}
// verify that we've drawn the table
var cell = this.getTableElement(rowNum, colNum);
if (cell == null) {
// when incremental rendering is on, this is a normal condition indicating that we are
// trying to update some row/cell that has been scrolled out of view, hence no longer
// exists. NOTE: don't log, we might be calling this for thousands of unrendered cells.
//this.logDebug("setRowStyle(): cell (" + rowNum + "," + colNum + ") not present");
return false;
}
var record = this.getCellRecord(rowNum, colNum);
// for eg, rows that are about to be completely refreshed anyway
if (record && record._ignoreStyleUpdates) {
return;
}
// if a colNum was specified, update just the individual cell (we got a pointer to it
// above)
if (colNum != null) {
this._updateCellStyle(record, rowNum, colNum, cell, className);
} else {
var row = this.getTableElement(rowNum);
if (row != null) {
var td = "TD",
firstCol = (!this.shouldShowAllColumns() ? this._firstDrawnCol : 0),
lastCol = (!this.shouldShowAllColumns() ? this._lastDrawnCol : this.fields.length-1),
// If incremental rendering is enabled, the indices of the cells in the DOM
// will not match the colNum of the cell being updated.
renderedCellNum = 0;
for (var fieldNum = firstCol; fieldNum <= lastCol; fieldNum++, renderedCellNum++) {
var cell;
// If we're showing columns separately, we'll style the whole table for the
// column (Nav case)
if (this.showColumnsSeparately || this.cacheDOM) {
cell = this.getTableElement(rowNum, fieldNum);
// Otherwise we'll style the individual cells in the row.
} else {
cell = row.cells[renderedCellNum];
}
if (cell == null) continue;
// Pass in the optional record object, className and cell objects to avoid them
// being re-calculated.
this._updateCellStyle(record, rowNum, fieldNum, cell, className);
}
}
}
// return true to indicate that we were able to update the cell(s)
return true;
},
//> @method gridRenderer.refreshCellStyle()
// Refresh the styling of an individual cell without redrawing the grid.
//
// The cell's CSS class and CSS text will be refreshed, to the current values returned by
// getCellStyle() and getCellCSSText() respectively.
//
// The cell's contents (as returned by getCellValue()) will not be refreshed. To
// refresh both styling and contents, call refreshCell() instead.
//
// @group appearance
// @param rowNum (number) row number of cell to refresh
// @param colNum (number) column number of cell to refresh
//
// @see refreshCell() to update cell contents too
// @visibility external
//<
// NOTE:
// - className param not public because we don't persist the change
// @param [className] (CSSStyleName) name of the CSS class to set to; if not specified,
// will use getCellStyle()
refreshCellStyle : function (row, col, className) {
// this is a synonym for setCellStyle();
// We could also fall through to refreshCellStyles() but this would force us to create an
// array object to pass in.
return this.setCellStyle(row, col, className);
},
//> @method gridRenderer.refreshCell() ([A])
// Refresh an individual cell without redrawing the grid.
//
// The cell's value, CSS class, and CSS text will be refreshed, to the current values returned
// by getCellValue(), getCellStyle() and getCellCSSText() respectively.
//
// @group appearance
// @param rowNum (number) row number of cell to refresh
// @param colNum (number) column number of cell to refresh
//
// @see refreshCellStyle() to update just styling
// @visibility external
//<
refreshCell : function (rowNum, colNum) {
this.refreshCellStyle(rowNum, colNum);
// refresh the value too unless it's already been refreshed as part of styling
if (!this.shouldRefreshCellHTML()) this.refreshCellValue(rowNum, colNum);
},
//> @method gridRenderer.refreshRow() ([A])
// Refresh an entire row of cells without redrawing the grid.
//
// The cells' values, CSS classes, and CSS text will be refreshed, to the current values
// returned by getCellValue(), getCellStyle() and getCellCSSText() respectively.
//
// @group appearance
// @param rowNum (number) row number of cell to refresh
//
// @see refreshCellStyle() to update just styling
// @see refreshCell()
// @visibility external
//<
refreshRow : function (rowNum) {
if (!this._readyToRefreshCell(rowNum)) {
this.delayCall("refreshRow", [rowNum]);
}
for (var i = 0; i < this.fields.length; i++) {
this.refreshCell(rowNum, i);
}
},
//> @method gridRenderer.refreshCellStyles() ([A])
// @group selection, appearance
//
// Update the style of a list of cells. (Used to show selection changes when cell selection is
// enabled)
//
// @param cellList (Array)
// Array of [rowNum, colNum] array pairs.
// @param [className] (CSSStyleName)
// Name of the CSS class to set to; if not specified, will use getCellStyle()
//
// @return (boolean) true == actually updated now, false == will update later
//
//<
refreshCellStyles : function (cellList, className) {
//>DEBUG
this.logDebug("refreshing cell styles: " + cellList.length + " cells");
// @method gridRenderer.getColumnLeft() ([A])
// @group sizing, positioning
// Return the left coordinate (in local coordinate space) of a particular column.
//
// @param colNum (number) number of the column.
//
// @return (number) X-coordinate
//<
getColumnLeft : function (colNum) {
// Note: we don't have to worry about undrawn columns because this._fieldWidths has all
// column widths, not just the drawn ones.
// textDirection: we calculate field sizes from right to left in RTL mode
if (this.isRTL()) {
return this.getScrollWidth() - this._fieldWidths.sum(0, colNum+1) -
(this.vscrollOn ? this.getScrollbarSize() : 0)
} else {
// otherwise return the width of fields 0-colNum
return this._fieldWidths.sum(0, colNum);
}
},
//> @method gridRenderer.getColumnPageLeft() ([A])
// @group sizing, positioning
// get the X-coordinate for a given column number as a GLOBAL coordinate
// @param colNum (number)
// @return (number) X-coordinate
//<
getColumnPageLeft : function (colNum) {
return this.getPageLeft() - this.getScrollLeft() + this.getColumnLeft(colNum) +
(this.isRTL() && this.vscrollOn ? this.getScrollbarSize() : 0);
},
//> @method gridRenderer.getColumnWidth() ([A])
// Return the width of a particular column.
// @group sizing, positioning
// @param colNum (number) number of the column.
// @return (number) width
//<
getColumnWidth : function (colNum) {
// return the width of the column from the _fieldWidths property
return this._fieldWidths[colNum];
},
//> @method gridRenderer.getInnerColumnWidth() ([A])
// Return the width of a particular column adjusted for this.cellPadding / cellSpacing.
// @group sizing, positioning
// @param colNum (number) number of the column.
// @return (number) inner width
//<
getInnerColumnWidth : function (colNum) {
var width = this.getColumnWidth(colNum);
if (width == null) return null;
// Note: cell spacing still breaks alignment with ListGrid headers in both firefox and IE.
// However this is a non-exposed feature for now
return (width - (2* this.cellSpacing + this._getCellHBorderPad()));
},
// method to get, and cache horizontal cell padding size (based on this.cellPadding and styling)
// Used for sizing the cell-level clipping div, etc.
_getCellHBorderPad : function (recalc) {
if (!recalc && this._cellHBorderPad != null) return this._cellHBorderPad;
var firstStyle = this._getFirstRecordStyle(),
padLeft = isc.Element._getLeftPadding(firstStyle, true),
padRight = isc.Element._getRightPadding(firstStyle, true),
border = isc.Element._getHBorderSize(firstStyle);
if (padLeft == null) padLeft = this.cellPadding;
if (padRight == null) padRight = this.cellPadding;
this._cellHBorderPad = (padLeft + padRight + border);
return this._cellHBorderPad;
},
//> @method gridRenderer.getRowTop() ([A])
// Returns the top coordinate for a given row number, relative to the top of body content.
// Note that this method is reliable for all rows visible in the viewport. If
// +link{GridRenderer.showAllRows} is false [or for a ListGrid +link{ListGrid.showAllRecords}
// is false], and this.fixedRowHeights
is not true, the row coordinates are
// estimates for rows that are scrolled out of view.
// @group sizing, positioning
// @param rowNum (number)
// @return (number) Y-coordinate
//<
getRowTop : function (rowNum) {
// undrawn rows before or after the drawn area are treated as having fixed height
if (rowNum < this._firstDrawnRow) return this.getAvgRowHeight() * rowNum;
var undrawnHeight = this._getUndrawnHeight(),
drawnHeights = this._getDrawnRowHeights();
if (rowNum > this._lastDrawnRow) {
// undrawn rows after the drawn area are treated as having fixed height
return undrawnHeight + drawnHeights.sum() +
(((rowNum-1) - this._lastDrawnRow) * this.getAvgRowHeight());
}
// otherwise return the sum of heights of records 0-rowNum
return undrawnHeight + drawnHeights.sum(0, rowNum - this._firstDrawnRow);
},
//> @method gridRenderer.getRowPageTop() ([A])
// Returns the Y-coordinate for a given row number as a page-relative coordinate.
// Note that this method is reliable for all rows visible in the viewport. If
// +link{GridRenderer.showAllRows} is false [or for a ListGrid +link{ListGrid.showAllRecords}
// is false], and this.fixedRowHeights
is not true, the row coordinates are
// estimates for rows that are scrolled out of view.
// @param rowNum (number)
// @group sizing, positioning
// @return (number) Y-coordinate
//<
getRowPageTop : function (rowNum) {
return this.getPageTop() + this.getTopBorderSize() +
(this.getRowTop(rowNum)- this.getScrollTop());
},
//> @method gridRenderer.getRowSize() ([A])
// Get the drawn height of a row.
//
// @param rowNum (number)
//
// @return (number) height
// @group sizing, positioning
// @deprecated As of SmartClient 8.0, use +link{gridRenderer.getDrawnRowHeight}.
//<
getRowSize : function (rowNum) {
return this.getDrawnRowHeight(rowNum);
},
//> @method gridRenderer.getDrawnRowHeight() ([A])
// Get the drawn height of a row.
//
// @param rowNum (number)
//
// @return (number) height
// @group sizing, positioning
//<
getDrawnRowHeight : function (rowNum) {
// treat all undrawn rows as though they were cellHeight tall
if (rowNum < this._firstDrawnRow || rowNum > this._lastDrawnRow) {
return this.getAvgRowHeight();
}
var visibleRowNum = rowNum - this._firstDrawnRow,
heights = this._getDrawnRowHeights();
return heights[visibleRowNum];
},
//> @method gridRenderer.getColumnSize() ([A])
// Get the drawn width of a column.
//
// @param colNum (number)
//
// @return (number) width in pixels
// @group sizing, positioning
//<
// NOTE: this function must be named getColumnSize because getColumnWidth refers to specified
// width.
getColumnSize : function (colNum) {
if ((this.fixedFieldWidths && !this.autoSize) ||
(colNum < this._firstDrawnCol || colNum > this._lastDrawnCol))
{
// fixed sizes, or not rendered; return specified size
return this.getColumnWidth(colNum);
}
var visibleColNum = colNum - this._firstDrawnCol,
widths = this.getColumnSizes();
return widths[visibleColNum];
},
// get the total height of all rows that are not currently drawn because they are above the
// viewport (and out of drawAhead range)
_getUndrawnHeight : function () {
return this._firstDrawnRow * this.getAvgRowHeight();
},
// get the heights of all drawn rows
_getDrawnRowHeights : function () {
//!DONTCOMBINE
if (this._rowHeights != null) return this._rowHeights;
// create a new array and store it in the _recordHeights slot
var heights = this._rowHeights = [];
// make sure that the table is defined by checking to make sure it exists
// -- if it isn't defined, return an empty list
var table = this.getTableElement();
if (!table || !table.rows) {
// otherwise delete the recordHeights so we'll calculate them again
// since this is being called prematurely (???)
delete this._rowHeights;
return heights;
}
var rowRange = this.getDrawnRows(), //this._lastDrawnRow - this._firstDrawnRow + 1,
drawnRows = rowRange[1] - rowRange[0] + 1,
nonZeroHeight = false;
for (var rowNum = 0; rowNum <= drawnRows; rowNum++) {
var row = this.cacheDOM ? this.getTableElement(rowNum + this._firstDrawnRow) : table.rows[rowNum];
if (row) {
var oldSafari = isc.Browser.isSafari && isc.Browser.safariVersion < 500;
if (this.getRowSpan && this.fullRowSpans) {
heights[rowNum] = row.offsetHeight;
nonZeroHeight = true;
continue;
}
var checkAllCellHeights =
(oldSafari &&
(this.fixedRowHeights == false ||
(this.shouldFixRowHeight != null &&
this.shouldFixRowHeight(this.getCellRecord(rowNum), rowNum) == false )
)
),
cell, safariCellArray = [];
if (!oldSafari || !checkAllCellHeights) {
cell = row.cells[row.cells.length - 1];
} else {
for (var k = 0; k < row.cells.length; k++) {
safariCellArray[k] = row.cells[k]
}
}
if (checkAllCellHeights) {
heights[rowNum] = 0;
for (var cellNum = 0; cellNum < safariCellArray.length; cellNum ++) {
var currentCell = safariCellArray[cellNum],
height = currentCell.offsetHeight;
var specifiedHeight = parseInt(currentCell.style ? currentCell.style.height
: null);
if (isc.Browser.isStrict) {
if (this.cellPadding) specifiedHeight += this.cellPadding;
specifiedHeight += isc.Element._getVBorderPad(currentCell.className);
}
if (isc.isA.Number(specifiedHeight) && specifiedHeight > height)
height = specifiedHeight;
if (height > heights[rowNum]) heights[rowNum] = height;
}
heights[rowNum] += this.cellSpacing;
} else if (cell) {
if (!oldSafari) {
heights[rowNum] = cell.offsetHeight;
} else {
// In Safari the offsetHeight is often misreported, and can't exceed
// the specified height for the cell, so use the specified height
// directly
var cellHeight = parseInt(cell.height);
if (cellHeight != null && isc.isA.Number(cellHeight)) {
if (isc.Browser.isStrict) {
cellHeight += isc.Element._getVBorderSize(cell.className);
}
} else {
cellHeight = cell.offsetHeight || 0;
}
heights[rowNum] = cellHeight;
}
heights[rowNum] += this.cellSpacing;
}
}
var height = heights[rowNum];
if (height != 0 && height != null) nonZeroHeight = true;
}
// add the cellSpacing to the first record. This makes it so when the cursor is in the
// spacing region, it actually goes to the lower record, which looks better than it
// going to the upper record
heights[0] += this.cellSpacing;
if (!nonZeroHeight) {
this.logWarn("row heights not yet available; returning all zeroes");
this._rowHeights = null;
}
if (isc.Browser.isSafari && !isc.Page.isLoaded()) this._rowHeights = null;
return heights;
},
//> @method gridRenderer.getColumnSizes() ([A])
// Get rendered column widths
// @group sizing, positioning
// @return (boolean) null | false
//<
// NOTE: sets sets gridRenderer._renderedColumnWidths
getColumnSizes : function () {
if (this._renderedColumnWidths != null) return this._renderedColumnWidths;
if (this.fixedColumnWidths && isc.Browser.version >= 5) {
return (this._renderedColumnWidths = this._fieldWidths.duplicate());
} else {
// inspect the DOM to determine rendered widths
// create a new array and store it in the _recordHeights slot
var widths = this._renderedColumnWidths = [];
// get the first row in the table to test to see if it's drawn
var row = this.getTableElement(this._firstDrawnRow);
// if the row isn't defined,
if (row == null) {
// use the fieldWidths as specified in the settings
this._renderedColumnWidths = widths.concat(this._fieldWidths);
return this._renderedColumnWidths;
}
var sizeDelta = (isc.Browser.isMac ? this.cellSpacing : 0);
// iterate for all of the fields of the table that have been drawn, getting the sizes
for (var colNum = 0; colNum < this.fields.length; colNum++) {
var cell;
if (this.showColumnsSeparately) {
cell = this.getTableElement(this._firstDrawnRow,colNum);
// Note leading not - This code will fire for safari 1.2, and other browsers
} else if (!(isc.Browser.isSafari && isc.Browser.safariVersion < 125)) {
cell = row.cells[colNum];
}
if (cell) {
widths[colNum] = cell.offsetWidth + sizeDelta;
} else {
widths[colNum] = this._fieldWidths[colNum];
}
}
// NOTE: we do this only in the case where we're not setting fixed widths
this.innerWidth = this.getTableElement().offsetWidth;
return widths;
}
},
// Event Row/Col
// --------------------------------------------------------------------------------------------
//> @method gridRenderer.getEventRow()
// Returns the row number of the most recent mouse event.
// @group events, selection
//
// @param [y] (number) optional y-coordinate to obtain row number, in lieu of the y
// coordinate of the last mouse event
//
// @return (number) row number, or -2 if beyond last drawn row
// @visibility external
//<
getEventRow : function (y) {
// if we're empty always return rowNum -2 (beyond the end of any valid data)
if (this.isEmpty()) return -2;
// If a y-coordinate was not passed, get it from the offset of the last event
if (y == null) y = this.getOffsetY();
// if we're showing a start spacer, knock that off from the event coordinate so we can figure
// out which row we hit
if (this.startSpace) y -= this.startSpace;
var undrawnHeight = this._getUndrawnHeight();
// if it's a coordinate before the drawn area, treat all offscreen rows as fixed height
if (y <= undrawnHeight) return Math.floor(y / this.getAvgRowHeight());
var remainder = y - undrawnHeight,
heights = this._getDrawnRowHeights();
// check visible rows. Note that if it's past the end of the visible rows, inWhichPosition
// returns -2, and so do we
var drawnRowNum = this.inWhichPosition(heights, remainder),
pos;
if (drawnRowNum >= 0) {
pos = this._firstDrawnRow + drawnRowNum;
} else {
// assume the rest of the rows are fixed height
var pastDrawnRows = remainder - heights.sum();
pos = this._lastDrawnRow + 1 + Math.floor(pastDrawnRows / this.getAvgRowHeight());
// Avoid returning a number higher than our total number of rows
if (pos >= this.getTotalRows()) pos = -2;
}
//this.logWarn("getEventRow(" + (y == null ? this.getOffsetY() : y) + "): " +
// " rowHeights:" + heights +
// " drawn range: " + [this._firstDrawnRow, this._lastDrawnRow] +
// ", undrawnHeight: " + undrawnHeight +
// ", eventRow:" + pos);
//this.logWarn("getEventRow(" + (y == null ? this.getOffsetY() : y) + "): " + pos);
return pos;
},
//> @method gridRenderer.getEventColumn()
// Returns the column number of the most recent mouse event.
// @group events, selection
//
// @param [x] (number) optional x-coordinate to obtain column number for, in lieu of the x
// coordinate of the last mouse event
//
// @return (number) column number, or -2 if beyond last drawn column
// @visibility external
//<
getEventColumn : function (x) {
var widths = this.getColumnSizes();
// If a x-coordinate was not passed, get it from the offset of the last event
if (x == null) x = this.getOffsetX();
return this.inWhichPosition(widths, x, this.getTextDirection());
},
// getFocusRow / col for keypress events. Overridden at the ListGrid gridBody level.
getFocusRow : function () {
return 0;
},
getFocusCol : function () {
return 0;
},
//> @method gridRenderer.getNearestRowToEvent()
// Returns the nearest row to the event coordinates
// @group events, selection
// @visibility external
//<
getNearestRowToEvent : function () {
var rowNum = this.getEventRow();
if (rowNum < 0) {
var visibleRows = this.getVisibleRows();
if (rowNum == -1) return visibleRows[0];
if (rowNum == -2) return visibleRows[1];
}
return rowNum;
},
//> @method gridRenderer.getNearestColToEvent()
// Returns the nearest column to the event coordinates
// @group events, selection
// @visibility external
//<
getNearestColToEvent : function () {
var colNum = this.getEventColumn();
if (colNum < 0) {
var visibleColumns = this.getVisibleColumns();
if (colNum == -1) return visibleColumns[0];
if (colNum == -2) return visibleColumns[1];
}
return colNum;
},
// Note: viewport rows / visible rows / drawn rows
// =================
// There is a distinction here between:
// - "rows we need to draw to fill the viewport" (getViewportFillRows)
// - "drawn rows that are currently visible in the viewport" (getVisibleRows)
// - "rows we've actually drawn" (this._firstDrawnRow -> this._lastDrawnRow)
//
// With drawAheadRatio > 1, rows we've drawn clearly differ from the other two. With variable
// height cells, viewportFillRows differ from visible rows since we don't know how tall the
// cells will be before we draw them; the last viewportFillRow may actually be rendered below
// the viewport. With fixedRowHeights:false and a drawAheadRatio > 1, the first viewport fill
// row may be below the top of the viewport.
//
// Generally:
// - we use the viewportFillRows only to determine how many rows to draw / whether to redraw
// - we use _firstDrawnRow/_lastDrawnRow to do DOM manipulation
// - event handling code that cares about the viewport (particularly D&D) uses visible rows
_getViewportFillRows : function () {
// scrollTop/cellHeight is the number of rows that fit into the space above the viewport, so
// round down for the index of the first row to stick into the viewport
var firstVisible = Math.floor(this.getScrollTop() / this.getAvgRowHeight()),
lastVisible = firstVisible + Math.ceil(this.getViewportHeight() / this.cellHeight);
// if we're showing an explicit spacer at the top, it'll shift the rows down
// take that into account now
if (this.startSpace) {
var spaceRows = Math.floor(this.startSpace / this.getAvgRowHeight());
firstVisible = Math.max(0, firstVisible - spaceRows);
lastVisible = Math.max(0, lastVisible - spaceRows);
}
// Are we virtual scrolling? Don't rely on this._isVirtualScrolling in case data or
// fixedRowHeights, etc has changed
var vscrolling = this.virtualScrolling && !this.fixedRowHeights && this._targetRow != null;
if (vscrolling) {
if (firstVisible == 0 && lastVisible >= (this.getTotalRows()-1)) vscrolling = false;
}
if (!vscrolling) return [firstVisible, lastVisible];
// when using virtual scrolling, calculate the rows that need to be drawn to fill the
// viewport based on the target row that needs to be scrolled into view (scrollTop is
// irrelevant)
//this.logWarn("calculating viewport based on targetRow: " + this._targetRow +
// ", row offset: " + this._rowOffset);
var startCoord = this._targetRow;
// if we have a large negative offset (targetRow will be well below viewport), ensure
// enough rows are rendered above the targetRow
if (this._rowOffset < 0) startCoord += Math.floor(this._rowOffset / this.cellHeight);
if (startCoord < 0) startCoord = 0;
var endCoord = startCoord + Math.ceil(this.getViewportHeight() / this.cellHeight);
return [startCoord, endCoord];
},
// Arbitrary average row height for incremental rendering and variable row heights
// default to a typical row height given a few lines of wrapping text
avgRowHeight:60,
getAvgRowHeight : function () {
return this.fixedRowHeights ? this.cellHeight : Math.max(this.cellHeight,this.avgRowHeight);
},
//> @method gridRenderer.getVisibleRows()
// Get the rows that are currently visible in the viewport, as an array of
// [firstRowNum, lastRowNum]. If the grid contains no records, will return [-1,-1];
// @return (Array)
// @visibility external
//<
// NOTE: the viewport can extend beyond the last row or column, in which case the last row or
// column is reported as the last visible.
getVisibleRows : function () {
var scrollTop = this.getScrollTop();
var rows = [
this.getEventRow(scrollTop),
this.getEventRow(scrollTop + this.getInnerHeight())
];
// viewport extends beyond last row: return the *index of* the last row
if (rows[1] == -2) {
var totalRows = this.getTotalRows();
if (totalRows == 0) {
rows[0] = -1;
rows[1] = -1;
} else {
rows[1] = this.getTotalRows() - 1;
}
}
return rows;
},
//> @method gridRenderer.getVisibleColumns()
// Get the currently visible columns, as an array of [leftColumnNum, rightColumnNum]
//<
getVisibleColumns : function () {
var widths = this._fieldWidths;
if (this.overflow == isc.Canvas.VISIBLE) return [0, widths.length-1];
var scrollPos = this.getScrollLeft();
if (this.isRTL()) {
var maxScroll = this.getScrollWidth() - this.getInnerWidth(),
scrollPos = maxScroll - scrollPos;
}
var firstCol = this.inWhichPosition(widths, scrollPos),
lastCol = this.inWhichPosition(widths, scrollPos + this.getInnerWidth());
//this.logWarn("scrollLeft: " + scrollPos +
// ", firstCol: " + firstCol +
// ", lastCol: " + lastCol);
if (lastCol == -2) lastCol = this._fieldWidths.length - 1;
return [firstCol, lastCol];
// maxScroll - scrollLeft
// 0 - 0: 0, works with non-reversed traversal
// max - max: 0, works
// max - 0:
},
//> @method gridRenderer.getDrawnRows()
// Get the rows that are currently drawn (exist in the DOM), as an array of [firstRowNum,
// lastRowNum].
//
// The drawn rows differ from the +link{getVisibleRows,visibleRows} because of
// +link{drawAheadRatio,drawAhead}. The drawn rows are the appropriate range to consider if
// you need to, eg, using +link{refreshCell()} to update all the cells in a column.
//
// If the grid is undrawn or the +link{emptyMessage} is currently shown, returns
// [null,null];
//
// @return (Array)
// @visibility external
//<
getDrawnRows : function () {
if (this.cacheDOM) return this.getVisibleRows();
return [this._firstDrawnRow, this._lastDrawnRow]
},
// Synthetic Row/Cell Events (over/out/hover/contextClick)
// --------------------------------------------------------------------------------------------
// shouldShowRollOver
// Should we show the "over" styling for this row when the mouse goes over it?
// By default this is always true if this.showRollOver is true.
shouldShowRollOver : function (rowNum, colNum) {
// NOTE: colNum may be null.
return (this.showRollOver && !this._rowAnimationInfo);
},
// called whenever the current row needs to be updated to reflect a change in the rollOver
// state. This includes both the rollover *leaving* a row (rollover appearance needs to be
// cleared) and entering the row.
updateRollOver : function (rowNum, colNum) {
this.setRowStyle(rowNum, null, (this.useCellRollOvers ? colNum : null));
},
// We fire synthetic events such as 'cellMouseOver' or 'rowMouseOver' on mouseOver of a cell.
// The handling for this is very similar for each event type -
// on X event, if cellX is defined fire it. If rowX is defined fire that.
// (If both are defined, we fire both).
// NOTE: checking for valid (>=0) row and col coordinates is necessary because the table can be
// drawn smaller than the area of the containing GridRenderer Canvas (eg in the LV, with a small
// number of records), so the mouse can be within the GridRenderer Canvas without being over the
// table as such.
// NOTE: most of the pseudo events do not have default handlers - meaning they can be defined
// without the developer having to call "Super". The only case where we currently would
// require an override to call Super is if the developer overrides
// 'selectOnMouseDown(record,rowNum,colNum)' or 'selectOnMouseUp(record,rowNum,colNum)'.
// Override the (unexposed) startHover() method to be a no-op
// This is called by the EH directly to show hover on mouseOver - we're handling our hovers on
// mouseOver of specific cells etc.
startHover : function () { },
//> @method gridRenderer.mouseMove() ([A])
// @group events
// Generate cell/row over/out events
// @return (boolean) false if same cell/row as before
//<
mouseMove : function (arg1, arg2) {
if (this._suppressEventHandling()) return;
var rowNum = this.getEventRow(),
colNum = this.getEventColumn();
//this.logWarn("row: " + rowNum + ", col: " + colNum);
var hasNewCell = (rowNum >= 0 && colNum >= 0 && this.cellIsEnabled(rowNum, colNum));
// On rollOver of cells we do a couple of things:
// - highlight the cell by applying "Over" styling
// - fire cell level events ("rowOver", "rowOut", and also hover events if appropriate).
// The "Over" styling subsystem is also coopted at the ListGrid level for visual feedback with
// keyboard navigation.
// Track the current "Over" styled cell separately from the last "cellOver" event cell - this
// is required to avoid an obscure bug where if a cell is highlighted with the "Over" style
// by keyboard navigation or focus, and then the user rolls over it, the appropriate
// rowOver event never fires.
var lastStyleRow = this.lastOverRow,
lastMouseRow = this.lastMouseOverRow,
lastStyleCol = this.lastOverCol,
lastMouseCol = this.lastMouseOverCol;
// Styling:
if (!(rowNum == lastStyleRow && colNum == lastStyleCol)) {
// If we were showing "over" styling for another row/cell, clear it.
if (lastStyleRow != null && lastStyleCol != null) {
this.lastOverRow = null;
this.lastOverCol = null;
// If we're not over a valid column (we're too far to the right of the listGrid)
// consider this a row change, for the purposes of restyling correctly
if (rowNum != lastStyleRow || colNum < 0 || this.useCellRollOvers) {
this.updateRollOver(lastStyleRow, lastStyleCol, hasNewCell);
}
}
// And show the over style for the new cell:
if (hasNewCell) {
this.lastOverRow = rowNum;
this.lastOverCol = colNum;
if (lastStyleRow != rowNum || this.useCellRollOvers) {
// show rollover hiliting
if (this.shouldShowRollOver(rowNum, colNum)) {
this.updateRollOver(rowNum, colNum);
}
}
}
}
// Events (including hovers):
if (!(rowNum == lastMouseRow && colNum == lastMouseCol)) {
if (lastMouseRow != null && lastMouseCol != null) {
this.lastMouseOverRow = null;
this.lastMouseOverCol = null;
// Once again - if new colNum < 0, we are to the right of the rightmost column, so
// call stopHover even if we're hovering by row.
if ((rowNum != lastMouseRow || colNum < 0 || this.hoverByCell) &&
this.getCanHover() && !this.keepHoverActive)
{
this.stopHover();
}
var lastMouseRecord = this.getCellRecord(lastMouseRow, lastMouseCol);
// support field.cellOut, cell.cellOut?
if (this.cellOut) {
this.cellOut(lastMouseRecord, lastMouseRow, lastMouseCol);
}
if (rowNum != lastMouseRow && this.rowOut) {
this.rowOut(lastMouseRecord, lastMouseRow, lastMouseCol);
}
}
if (hasNewCell) {
this.lastMouseOverRow = rowNum;
this.lastMouseOverCol = colNum;
if (rowNum != lastMouseRow || this.hoverByCell) {
// set hover action
if (this.getCanHover()) {
isc.Hover.setAction(this, this._cellHover, [rowNum, colNum], this.hoverDelay);
}
}
// support field.cellOver, cell.cellOver?
if (this.cellOver) {
this.cellOver(this.getCellRecord(rowNum, colNum), rowNum, colNum);
}
if (rowNum != lastMouseRow && this.rowOver) {
this.rowOver(this.getCellRecord(rowNum, colNum), rowNum, colNum);
}
}
}
if (rowNum >= 0 && colNum >= 0) {
// cellMove / rowMove
// Not currently exposed - used internally to update hovers for validation error icons
// in ListGrid.
if (this.cellMove) {
this.cellMove(this.getCellRecord(rowNum, colNum), rowNum, colNum);
}
if (this.rowMove) {
this.rowMove(this.getCellRecord(rowNum, colNum), rowNum, colNum);
}
}
},
// Support suppressing mouse/keyboard event handling at certain times.
_suppressEventHandling : function () {
//>Animation
// If we're in the process of animate-resizing a row just suppress all event handling!
if (this._rowAnimationInfo != null) return true;
// @method gridRenderer.mouseOut() ([A])
// @group events
// call _cellOut or _rowOut if appropriate
// @return (boolean) false if no hiliting
//<
mouseOut : function () {
// Don't suppress mouseOut handling even if this._suppressEventHandling returns true
// - we don't want the list stuck in an "over" state
// if the mouseOut occurred by the mouse going over a child of an embedded component, don't
// hide rollover / fire mouseOut methods.
var target = isc.EH.getTarget();
if (this._embeddedComponents) {
var components = this._embeddedComponents;
for (var i = 0; i < components.length; i++) {
if (components[i].contains(target, true)) {
return;
}
}
}
// Note that we'll still get a bubbled mouseout when the user rolls out of the embedded
// component so we won't get stuck in an 'over' state.
// If the target == this, we're still over this widget.
// This can happen if we're starting to drag - in this case continue as with any other
// mouseOut (killing the hover is technically unnecessary as in this case as
// EH.handleDragStart() always calls Hover.clear(), but we also want to clear up over-styling
// etc.)
// Otherwise this event was bubbled from the user rolling off an embedded component back
// into the body and we can ignore it.
if (target == this && !isc.EH.getDragTarget()) {
return;
}
// clear any hover timer/window
if (this.getCanHover()) this.stopHover();
// if we were previously over a valid cell, reset the style for that cell and fire
// cellOut / rowOut
if (this.lastOverRow != null && this.lastOverCol != null) {
var lastOverRow = this.lastOverRow,
lastOverCol = this.lastOverCol;
this.lastOverRow = null;
this.lastOverCol = null;
// clear rollover hiliting
if (this.shouldShowRollOver(lastOverRow, lastOverCol)) {
this.updateRollOver(lastOverRow, lastOverCol);
}
}
if (this.lastMouseOverRow != null && this.lastMouseOverCol != null) {
var lastOverRow = this.lastMouseOverRow,
lastOverCol = this.lastMouseOverCol,
lastOverRecord = this.getCellRecord(lastOverRow, lastOverCol);
this.lastMouseOverRow = null;
this.lastMouseOverCol = null;
// support field.cellOut, cell.cellOut?
if (this.cellOut) {
this.cellOut(lastOverRecord, lastOverRow, lastOverCol);
}
if (this.rowOut) {
this.rowOut(lastOverRecord, lastOverRow, lastOverCol);
}
}
},
// support field.cellHover, cell.cellHover, field.showHover, cell.showHover?
_cellHover : function (rowNum, colNum) {
//!DONTCOMBINE
var record = this.getCellRecord(rowNum, colNum);
// call user-defined handler and bail (don't show hover window) if it returns false
var returnVal;
if (this.cellHover && this.cellHover(record, rowNum, colNum) == false) returnVal = false;
if (this.rowHover && this.rowHover(record, rowNum, colNum) == false) returnVal = false;
if (returnVal == false) return;
// show hover window if enabled
if (this.showHover) this._showHover(record, rowNum, colNum);
},
_showHover : function (record, rowNum, colNum) {
var properties = this._getHoverProperties();
var content = this._getCellHoverComponent(record, rowNum, colNum);
if (!content) content = this.cellHoverHTML(record, rowNum, colNum);
isc.Hover.show(content,
properties,
this.cellHoverBoundary(rowNum, colNum),
this.getHoverTarget());
},
_getCellHoverComponent : function (record, rowNum, colNum) {
return this.grid._getCellHoverComponent(record, rowNum, colNum);
},
// getHoverTarget() - returns the 'targetCanvas' passed to Hover.show() in _showHover()
// This allows the developer to call 'updateHover()' on that canvas to change the hover content HTML
// override in LG bodies to point to the ListGrid
getHoverTarget : function () {
return this;
},
cellHoverHTML : function (record, rowNum, colNum) {
return null;
},
getCellHoverComponent : function (record, rowNum, colNum) {
return null;
},
cellHoverBoundary : function (rowNum, colNum) {
return null;
},
// generate cell/row contextClick events
showContextMenu : function () {
if (this._suppressEventHandling()) return false;
var rowNum = this.getEventRow(),
colNum = this.getEventColumn();
// If this came from a keyboard event, use the keyboard focus row / col
var keyboardEvent = isc.EH.isKeyEvent();
if (keyboardEvent) {
rowNum = this.getFocusRow(),
colNum = this.getFocusCol();
}
if (rowNum >= 0 && colNum >= 0 && this.cellIsEnabled(rowNum, colNum)) {
var record = this.getCellRecord(rowNum, colNum), returnVal;
if (this.cellContextClick)
if (this.cellContextClick(record, rowNum, colNum) == false) returnVal = false;
if (this.rowContextClick)
if (this.rowContextClick(record, rowNum, colNum) == false) returnVal = false;
// Legacy:
if (this.recordContextClick)
if (this.recordContextClick(record, rowNum, colNum) == false) returnVal = false;
if (returnVal == false) return false;
}
return this.Super("showContextMenu");
},
// Selection
// --------------------------------------------------------------------------------------------
setSelection : function (selection) {
this.selection = selection;
// update cell/row styling on selection change
if (this.selection.isA("CellSelection")) {
this.observe(this.selection, "selectionChanged",
"observer._cellSelectionChanged(observed.changedCells)");
} else {
this.observe(this.selection, "setSelected",
"observer._rowSelectionChanged(observed.lastSelectionItem,!!observed.lastSelectionState)");
}
},
clearSelection : function () {
if (this.selection) {
if (this.isObserving(this.selection, "selectionChanged"))
this.ignore(this.selection, "selectionChanged");
if (this.isObserving(this.selection, "setSelected"))
this.ignore(this.selection, "setSelected");
delete this.selection;
}
},
_cellSelectionChanged : function (cellList) {
// call user-defined handler and bail (don't hilite cells) if it returns false
if (this.cellSelectionChanged) {
if (this.cellSelectionChanged(cellList) == false) return false;
}
// refresh the affected cells to visually indicate selection
this.refreshCellStyles(cellList);
},
_rowSelectionChanged : function (record, state) {
// call user-defined handler and bail (don't hilite rows) if it returns false.
if (this.handleSelectionChanged(record, state) == false) return false;
// refresh the affected records to visually indicate selection
var selection = this.selection,
lastItem = selection.lastSelectionItem,
rowNum = selection.data.indexOf(lastItem,
this._firstDrawnRow,
this._lastDrawnRow);
if (rowNum == -1) rowNum = selection.data.indexOf(lastItem);
if (rowNum == -1) return;
this.updateRowSelection(rowNum);
},
handleSelectionChanged : function (record,state) {
if (this.selectionChanged) return this.selectionChanged(record,state);
},
updateRowSelection : function (rowNum) {
this.setRowStyle(rowNum);
},
selectionEnabled : function () {
return this.selection != null;
},
canSelectRecord : function (record) {
return (record != null && record[this.recordCanSelectProperty] !== false);
},
//> @method gridRenderer.mouseDown() ([A])
// @group events, selection
// handle a mouseDown event
// @return (boolean) false if record is disabled
//<
mouseDown : function () {
if (this._suppressEventHandling()) return;
var rowNum = this.getEventRow(),
colNum = this.getEventColumn();
// not over a cell - just bail
if (!(rowNum >= 0 && colNum >= 0)) return;
// if the record is explicitly disabled, kill the event
if (!this.cellIsEnabled(rowNum, colNum)) return false;
// hang onto the rowNum / colNum to see if we get a click over the same cell
this._mouseDownRow = rowNum;
this._mouseDownCol = colNum;
// remember the location of the click too.
this._mouseDownX = isc.EH.getX();
this._mouseDownY = isc.EH.getY();
var record = this.getCellRecord(rowNum, colNum);
// call user-defined synthetic mouseDown event handler
if (!isc.EH.rightButtonDown()) {
return this._cellMouseDown(record, rowNum, colNum);
} else {
return this._cellRightMouseDown(record, rowNum, colNum);
}
},
rightMouseDown : function () {
// required to handle remembering which record the mouse went down over. Also fires
// _cellRightMouseDown()
return this.mouseDown();
},
_cellMouseDown : function (record, rowNum, colNum) {
var returnVal;
if (this.cellMouseDown && (this.cellMouseDown(record, rowNum, colNum) == false)) returnVal = false;
if (this.rowMouseDown && (this.rowMouseDown(record, rowNum, colNum) == false)) returnVal = false;
// legacy
if (this.recordMouseDown && this.recordMouseDown(rowNum, colNum) == false) returnVal = false;
if (returnVal == false) return false;
// perform selection
this.selectOnMouseDown(record, rowNum, colNum);
},
selectOnMouseDown : function (record, rowNum, colNum) {
if (!this.selectionEnabled()) return true;
//this.logWarn("mouseDown at: " + [rowNum, colNum]);
if (rowNum >= 0 && colNum >= 0 && this.canSelectRecord(record)) {
this._updateSelectionOnMouseUp = true;
this.selection.selectOnMouseDown(this, rowNum, colNum);
}
},
_cellRightMouseDown : function (record, rowNum, colNum) {
// currently there are no cell / row level rightMouseDown handlers, but this is where we would
// call them if there were.
// perform right-mouse style selection
if (this.canSelectOnRightMouse) this.selectOnRightMouseDown(record, rowNum, colNum);
},
// Default implementation is just to do 'selectOnMouseDown' - override if you want something else.
selectOnRightMouseDown : function (record, rowNum, colNum) {
this.selectOnMouseDown(record, rowNum, colNum);
},
//> @method gridRenderer.mouseUp() ([A])
// @group events, selection
// handle a mouseUp event
// @return (boolean) false if no hiliting; true otherwise
//<
mouseUp : function () {
if (this._suppressEventHandling()) return;
var rowNum = this.getEventRow(),
colNum = this.getEventColumn();
// not over a cell - just bail
if (!(rowNum >=0 && colNum >=0)) return;
// if the record is explicitly disabled, just return
if (!this.cellIsEnabled(rowNum, colNum)) return;
var record = this.getCellRecord(rowNum, colNum);
// call user-defined cell / row level mouseUp handler
var returnVal;
if (this.cellMouseUp && (this.cellMouseUp(record, rowNum, colNum) == false)) returnVal = false;
if (this.rowMouseUp && (this.rowMouseUp(record, rowNum, colNum) == false)) returnVal = false;
// legacy
if (this.recordMouseUp && this.recordMouseUp(rowNum, colNum) == false) returnVal = false;
if (returnVal == false) return returnVal;
this.selectOnMouseUp(record, rowNum, colNum);
},
selectOnMouseUp : function (record, rowNum, colNum) {
if (!this.selectionEnabled()) return true;
//this.logWarn("mouseUp at: " + [rowNum, colNum]);
if (rowNum >= 0 && colNum >= 0) {
this.selection.selectOnMouseUp(this, rowNum, colNum);
if (this._updateSelectionOnMouseUp) {
if (this.fireSelectionUpdated) this.fireSelectionUpdated();
this._updateSelectionOnMouseUp = null;
}
}
},
//> @method gridRenderer.click() ([A])
// @group events
// handle a click event
// fires cell or row level click handler
//
// @return (boolean) false if the event was cancelled by some handler
//<
click : function () {
if (this._suppressEventHandling()) return;
var rowNum = this.getEventRow(),
colNum = this.getEventColumn();
return this._rowClick(rowNum, colNum);
},
// _rowClick - fire rowClick and cellClick handlers
_rowClick : function (rowNum, colNum) {
// Clear out old _clickRow/ _clickCol, which are now out of date.
// [These will be set to meaningful values if the event occurred over a valid cell].
this._clickRow = this._clickCol = null;
var mdR = this._mouseDownRow;
// if the click occurred over a different record from the previous mousedown, just bail
if (rowNum != mdR) {
if (isc.EH.getX() == this._mouseDownX) {
rowNum = this._mouseDownRow;
} else {
return false;
}
}
if (isc.EH.getY() == this._mouseDownY) {
colNum = this._mouseDownCol;
}
// no record - just bail
if (!(rowNum >=0 && colNum >=0)) return;
// if the record is explicitly disabled, return false to kill doubleClick etc
if (!this.cellIsEnabled(rowNum, colNum)) return false;
// record the click cell details for double-click events to check
this._clickRow = rowNum;
var record = this.getCellRecord(rowNum , colNum),
returnVal;
// only fire cellClick if it was on the same column as well as the same row
if (!this._cellClick(record, rowNum, colNum)) returnVal = false;
if (this.rowClick && (this.rowClick(record, rowNum, colNum) == false))
returnVal = false;
// clear out the old mouseDown row
// Note - this method is fired _after_ mouseUp, so we can't clear these values out there.
this._mouseDownRow = null;
return returnVal;
},
// _cellClick - fire cellClick handlers on the specified row/col
_cellClick : function (record, rowNum, colNum) {
// Assertion - this method is only called when we have already verified that the click
// occurred over the same row as the last mousedown
if (this._mouseDownCol != colNum) {
// Clearing out this._clickCol avoids the possibility of the doubleClick handler firing
// over this cell which never received a first click.
this._clickCol = null;
return;
}
// update _clickCol so double clicks can determine whether they occurred over the
// same column as the previous click.
this._clickCol = colNum;
this._mouseDowncol = null;
return !(this.cellClick && (this.cellClick(record, rowNum, colNum) == false));
},
//> @method gridRenderer.doubleClick() ([A])
// @group events
// handle a doubleClick event
// fires cell or row level doubleClick handler
//
// @return (boolean) false if the event was cancelled by some handler
//<
doubleClick : function () {
if (this._suppressEventHandling()) return;
var rowNum = this.getEventRow(),
colNum = this.getEventColumn();
// no record - just bail
if (!(rowNum >= 0 && colNum >= 0)) return;
// if the record is explicitly disabled, kill the event
if (!this.cellIsEnabled(rowNum, colNum)) return false;
// If the double click occurred over a different row from the previous click, fire
// rowClick / cellClick over the new row.
if (rowNum != this._clickRow) {
return this._rowClick(rowNum, colNum);
}
// call user-defined cell / row level click and mouseUp handler
var record = this.getCellRecord(rowNum, colNum),
handlerReturn;
// the click occurred over a different col from the last click, fire a single click on
// that cell (but not that row)
// NOTE: this means if the user double clicks within a row, but the clicks land on different
// columns we'll get a single click on each cell, and a double click on the row.
if (colNum != this._clickCol) {
handlerReturn = this._cellClick(record, rowNum, colNum);
// otherwise fire a double click handler on the cell
} else if (this.cellDoubleClick && (this.cellDoubleClick(record, rowNum, colNum) == false))
{
handlerReturn = false;
}
if (this.rowDoubleClick && (this.rowDoubleClick(record, rowNum, colNum) == false))
handlerReturn = false;
// clear out the temp vars -- we don't want to fire a click on mouseUp after this doubleclick
// (this is fired before mouseUp) and no need to hang onto the clickRow / col.
this._mouseDownRow = this._mouseDownCol = null;
this._clickRow = this._clickCol = null;
if (handlerReturn == false) return false;
},
//> @method gridRenderer.dragMove() ([A])
// @group events, dragging
// drag move event
// @return (boolean)
//<
// XXX
// We may want to add handling for row and cell level rowDragMove() and cellDragMove() handlers.
// If we do this we would also add row and cell level dragStart / dragStop / dropMove / drop, etc.
// - Default (internal) implementation would handle dragSelection if this.canDragSelect
// Not worrying about this for now.
dragMove : function () {
if (this._suppressEventHandling() || !this.selectionEnabled() || !this.canDragSelect)
return true;
var rowNum = this.getNearestRowToEvent(),
colNum = this.getNearestColToEvent();
//this.logWarn("selectOnDragMove: " + [rowNum, colNum]);
this.selection.selectOnDragMove(this, rowNum, colNum);
},
dragStop : function () {
this.fireSelectionUpdated();
},
// Override Drag/drop snap-to-grid functionality from Canvas
// suppress drag offset when snap dragging to cells.
noSnapDragOffset : function (dragTarget) {
return this.snapToCells;
},
getHSnapPosition : function (localCoordinate, dir) {
if ( ! this.snapToCells) {
return this.Super("getHSnapPosition", arguments);
}
var EH = this.ns.EH,
direction = dir || this.snapHDirection,
col = this.snapHGap ? Math.floor(localCoordinate / this.snapHGap) : this.getEventColumn(localCoordinate),
beforeLeft = this.snapHGap ? (col * this.snapHGap) : this.getColumnLeft(col),
beforeRight = this.snapHGap ? beforeLeft + this.snapHGap : this.getColumnLeft(col) + this.getColumnSize(col),
afterCol = this.snapHGap ? col + 1 : this.getEventColumn(beforeRight + 1),
afterLeft;
if (afterCol >= 0 ) {
afterLeft = this.snapHGap ? afterCol * this.snapHGap : this.getColumnLeft(afterCol);
} else {
afterLeft = beforeLeft;
}
var halfway = beforeLeft + (this.snapHGap ? this.snapHGap : this.getColumnSize(col)) / 2;
// Fix up for cell borders if necessary
if (this.snapInsideBorder) {
var lb = isc.Element._getLeftBorderSize(this.baseStyle)
var rb = isc.Element._getRightBorderSize(this.baseStyle)
beforeLeft += lb;
beforeRight -= rb;
afterLeft += lb;
}
// For resize, always extend the drag-target to cover the current "over" cell
if (EH.dragOperation == EH.DRAG_RESIZE) {
var goingLeft = isc.EH.resizeEdge.contains("L");
return goingLeft ? beforeLeft : beforeRight;
} else {
if (direction == isc.Canvas.BEFORE) {
return beforeLeft;
} else if (direction == isc.Canvas.AFTER) {
return afterLeft;
} else {
// If we're exactly inbetween, go left
if (localCoordinate <= halfway) {
return beforeLeft;
} else {
return afterLeft;
}
}
}
},
getVSnapPosition : function (localCoordinate, dir) {
if ( ! this.snapToCells) {
return this.Super("getVSnapPosition", arguments);
}
// this almost works...repositioning gets thrown off when moving up. May be worth exploring
// at some point
//if (this.snapVGap) {
// return this.Super("getVSnapPosition", localCoordinate, dir) + gridInnerPageTop;
//}
var EH = this.ns.EH,
direction = dir || this.snapVDirection,
// for snapVGap, row is just a snapVGap sized chunk of space
row = this.snapVGap ? Math.floor(localCoordinate / this.snapVGap) : this.getEventRow(localCoordinate),
// top coordinate of row
beforeTop = this.snapVGap ? (row * this.snapVGap) : this.getRowTop(row),
// bottom coordinate of row
beforeBot = this.snapVGap ? beforeTop + this.snapVGap : this.getRowTop(row) + this.getRowSize(row),
afterRow = this.snapVGap ? row + 1 : this.getEventRow(beforeBot + 1),
afterTop;
if (afterRow >= 0 ) {
afterTop = this.snapVGap ? afterRow * this.snapVGap : this.getRowTop(afterRow);
} else {
afterTop = beforeTop;
}
var halfway = beforeTop + (this.snapVGap ? this.snapVGap : this.getRowSize(row)) / 2;
// Fix up for borders if necessary
if (this.snapInsideBorder) {
var tb = isc.Element._getTopBorderSize(this.baseStyle)
var bb = isc.Element._getBottomBorderSize(this.baseStyle)
//this.logWarn("tb: " + tb + ", bb: " + bb);
beforeTop += tb;
beforeBot -= bb;
afterTop += tb;
}
if (EH.dragOperation == EH.DRAG_RESIZE) {
var goingUp = isc.EH.resizeEdge.contains("T");
return goingUp ? beforeTop : beforeBot;
} else {
if (direction == isc.Canvas.BEFORE) {
return beforeTop;
} else if (direction == isc.Canvas.AFTER) {
return afterTop;
} else {
// If we're exactly inbetween, go up
if (localCoordinate <= halfway) return beforeTop;
else return afterTop;
}
}
},
// AutoSizing
// --------------------------------------------------------------------------------------------
//> @method gridRenderer.getColumnAutoSize() ([A])
// @group sizing, positioning
// Get the size this column needs to be in order to accommodate it's contents.
//
// Can only be called after draw()
//
// NOTE: if using partial table rendering (showAllRows:false), this is the size for the
// currently visible contents of the column
//<
getColumnAutoSize : function (columnNum, startRow, endRow) {
if (this.getTotalRows() == 0) {
return null;
}
// create an offscreen Canvas to do sizing in
var columnSizer = this._columnSizer = this._columnSizer || isc.Canvas.create({
top:-1000,
width:1, height:1,
autoDraw:false,
_generated:true
});
// get HTML for a table containing only this column, written without column widths and
// with no text wrapping
var autoFit = this.autoFit,
wrapCells = this.wrapCells;
this.autoFit = true;
this.wrapCells = false;
// pass in startRow / endRow
// If not explicitly specified, just use the current draw area
// Passing this parameter in avoids us writing a (unnecessary in this case) spacer
// above / below the cell values and will turn on the "fragment" logic in getTableHTML()
// which avoids writing out DOM IDs on the various parts.
if (startRow == null || endRow == null) {
var drawRect = this.getDrawArea();
startRow = drawRect[0];
// remember drawRect is inclusive, we want exclusive
endRow = drawRect[1]+1;
}
columnSizer.contents = this.getTableHTML(columnNum,startRow,endRow, true);
this.autoFit = autoFit;
this.wrapCells = wrapCells;
// draw the table and figure out how large it is
columnSizer.draw();
var returnVal;
if (isc.isA.Array(columnNum)) {
// We're going to have to reach into the table.
var table,
nodes = columnSizer.getHandle().childNodes;
for (var i = 0; i < nodes.length; i++) {
if (nodes[i].tagName.toLowerCase() == "table") {
table = nodes[i];
break;
}
}
if (table && table.rows[0]) {
var firstRow = table.rows[0],
cells = firstRow.cells;
returnVal = [];
for (var i = 0; i < cells.length; i++) {
returnVal[i] = cells[i].clientWidth;
}
}
} else {
returnVal = columnSizer.getScrollWidth();
}
//this.logWarn("columnWidth: " + columnWidth + " for table: " + columnSizer.contents);
columnSizer.clear();
return returnVal;
},
// Table Cache Clearing
// --------------------------------------------------------------------------------------------
// clear anything we've cached about the HTML table we draw
redraw : function (a,b,c,d) {
this._resetEmbeddedComponents();
this.invokeSuper(isc.GridRenderer, "redraw", a,b,c,d);
// if we're redrawing in response to the end of 'fast scrolling', the suppresDrawAhead flag
// will have been set in markForRedraw()
// clear this now
delete this._suppressDrawAheadDirection;
},
modifyContent : function () {
// resize / place embedded components before
// - restoring virtual scrolling
// - adjusting overflow
// Both of these need to know the drawn heights of rows (including E.C's)
// If we're performing an animated show/hide of row, don't attempt to place embedded
// components until it completes
if (!this._animatedShowStartRow) this._placeEmbeddedComponents();
if (this._targetRow != null) {
this._scrollFromRedraw = true;
this._scrollToTargetRow("scrollToRow in modifyContent");
this._scrollFromRedraw = null;
// show the table element, which is drawn as hidden so we can scroll before we make it
// visible, to prevent it showing the wrong scroll position briefly
var tableElement = this.getTableElement();
if (tableElement) tableElement.style.visibility = "inherit";
}
if (this._isVirtualScrolling) {
// shrink the endSpacer, if any, to avoid scrolling when unnecessary.
var totalRowHeight = this._getDrawnRowHeights().sum();
if (totalRowHeight < this.getViewportHeight()) {
this._endRowSpacerHeight = 0;
var spacer = isc.Element.get(this.getID() + "_endSpacer"),
height = this._endRowSpacerHeight + (this.endSpace || 0);
if (spacer) {
if (height == 0) spacer.style.display = "none"
else spacer.style.display = "";
spacer.style.height = height + "px";
}
// overflow:hidden, so no need to rewrite div content
//this.logWarn("shrank spacer: " + totalRowHeight);
}
var visibleRows = this.getVisibleRows(),
numVisibleRows = Math.max(1, visibleRows[1] - visibleRows[0]),
trueRatio = numVisibleRows/this.getTotalRows(),
approxRatio = this.getViewportRatio(true);
//this.logWarn("viewportHeight: " + this.getViewportHeight() +
// ", visibleRows: " + visibleRows +
// ", approxRatio: " + approxRatio +
// ", trueRatio: " + trueRatio);
if (isc.isA.Number(trueRatio) &&
((approxRatio == 1 && trueRatio < 1) ||
approxRatio/trueRatio > 1.25)
)
{
this._viewRatioRowHeight = Math.max(this.cellHeight,
Math.round(this.getViewportHeight() / numVisibleRows));
//this.logWarn("set average row height to: " + this._viewRatioRowHeight);
}
}
/*
// resize the bottom spacer to keep the scrollHeight constant
var totalRows = this.getTotalRows(),
oldScrollHeight = this._oldScrollHeight;
// if we've got the same size dataset as before, and we're not drawing all the way to the
// end
if (this._lastTotalRows == totalRows && this._lastDrawnRow < totalRows-1) {
var newScrollHeight = this.getScrollHeight(true),
spacer = isc.Element.get(this.getID() + "_endSpacer"),
spacerHeight = parseInt(spacer.offsetHeight),
newSpacerHeight = spacerHeight + (oldScrollHeight-newScrollHeight);
this.logWarn("adding extraHeight. oldScrollHeight: " + oldScrollHeight +
", lastDrawnRow: " + this._lastDrawnRow +
", newScrollHeight: " + newScrollHeight +
", spacerHeight: " + spacerHeight +
", newSpacerHeight: " + newSpacerHeight);
if (newSpacerHeight < 0) {
this.logWarn("************** NOT ENOUGH ROOM to adjust spacer");
newSpacerHeight = this.cellHeight;
}
spacer.style.height = newSpacerHeight + "px";
}
this._lastTotalRows = totalRows;
*/
},
// Helper to update the explicit space above / below the rows in the grid
setStartSpace : function (value) {
if (!isc.isA.Number(value) || value == this.startSpace) return;
var reduction = this.startSpace && this.startSpace > value;
this.startSpace = value;
if (!this.isDrawn()) return;
var height = value + this._startRowSpacerHeight,
spacer = isc.Element.get(this.getID() + "_topSpacer");
if (spacer) {
if (height == 0) spacer.style.display = "none";
else spacer.style.display = ""; // default (== "inline")
if (this._canResizeSpacerDivs) {
spacer.style.height = height + "px";
}
// overflow:hidden so don't have to rewrite contents if we're shrinking
if (!reduction || !this._canResizeSpacerDivs) {
spacer.innerHTML = isc.Canvas.spacerHTML(1,height);
}
this._markForAdjustOverflow();
}
// If there was no spacer we must be in cacheDOM mode where we don't currently
// support startSpace / endSpace
},
setEndSpace : function (value) {
if (!isc.isA.Number(value) || value == this.endSpace) return;
var reduction = this.endSpace && this.endSpace > value;
this.endSpace = value;
if (!this.isDrawn()) return;
var height = value + this._endRowSpacerHeight,
spacer = isc.Element.get(this.getID() + "_endSpacer");
if (spacer) {
if (height == 0) spacer.style.display = "none";
else spacer.style.display = ""; // default (== "inline")
if (this._canResizeSpacerDivs) spacer.style.height = height + "px";
if (!reduction || !this._canResizeSpacerDivs) {
spacer.innerHTML = isc.Canvas.spacerHTML(1,height);
}
this._markForAdjustOverflow();
}
// If there was no spacer we must be in cacheDOM mode where we don't currently
// support startSpace / endSpace
},
clear : function () {
this.Super("clear", arguments);
this._clearTableCache();
// if we're cleared before the delayed redraw from the end of fast scrolling fires,
// clear the suppressDrawAheadDirection flag
delete this._suppressDrawAheadDirection;
},
// clear anything we've cached about the HTML table we draw
_clearTableCache : function () {
// drop our cache of HTML row elements
this._rowElements = null;
this._tableElement = null;
// clear the cached table geometry information so it'll be recalculated the next time it's
// asked for
delete this._renderedColumnWidths;
delete this._undrawnHeight;
delete this._rowHeights;
this._scrollRedraw = false;
}
});
isc.GridRenderer._gridAPIs = {
// customizing cell values
// --------------------------------------------------------------------------------------------
//> @method gridRenderer.getCellRecord()
// Return the record that holds the value for this cell.
//
// Implementing getCellRecord
is optional: the actual HTML placed into each
// grid cell comes from getCellValue
, and a valid grid can be created without any
// notion of "records" at all.
//
// If you do implement getCellRecord
, the value you return is passed to you as the
// "record" parameter in other methods.
//
// @param rowNum (number) row number for the cell
// @param colNum (number) column number of the cell
// @return (object) record for this cell
// @visibility external
//<
getCellRecord : "rowNum,colNum",
//> @method gridRenderer.getCellValue()
// Return the HTML to display in this cell. Implementing this is required to get a non-empty
// grid.
//
// @param record (ListGridRecord) cell record as returned by getCellRecord
// @param rowNum (number) row number for the cell
// @param colNum (number) column number of the cell
// @return (string) HTML to display in this cell
// @visibility external
//<
getCellValue : "record,rowNum,colNum,gridBody",
//> @method gridRenderer.findRowNum()
// Given a record displayed in this grid, find and return the rowNum in which the record
// appears.
//
// As with +link{gridRenderer.getCellRecord()} implementing this method is optional as a valid
// grid may be created without any notion of records.
//
// @param record (ListGridRecord) cell record as returned by getCellRecord
// @return (number) index of the row containing the record or -1 if not found
// @visibility external
//<
findRowNum : "record",
//> @method gridRenderer.findColNum()
// Given a record displayed in this grid, find and return the colNum in which the record
// appears.
//
// As with +link{gridRenderer.getCellRecord()} implementing this method is optional as a valid
// grid may be created without any notion of records, or records may not be displayed in a
// single column (as with the +link{class:ListGrid,ListGrid} class where each record is
// displayed in an entire row.
//
// @param record (ListGridRecord) cell record as returned by getCellRecord
// @return (number) index of the column containing the record or -1 if not found
// @visibility external
//<
findColNum : "record",
// customizing cell styling
// --------------------------------------------------------------------------------------------
//> @method gridRenderer.getBaseStyle() ([A])
// Return the base stylename for this cell. Default implementation just returns this.baseStyle.
// See +link{listGrid.getCellStyle,getCellStyle()} for a general discussion of how to style cells.
//
// @see getCellStyle()
//
// @param record (ListGridRecord) cell record as returned by getCellRecord
// @param rowNum (number) row number for the cell
// @param colNum (number) column number of the cell
// @return (CSSStyleName) CSS class for this cell
// @visibility external
//<
getBaseStyle : "record,rowNum,colNum",
// getCellStyle doc'd above
getCellStyle : "record,rowNum,colNum",
//> @method gridRenderer.getCellCSSText() ([A])
// Return CSS text for styling this cell, which will be applied in addition to the CSS class
// for the cell, as overrides.
//
// "CSS text" means semicolon-separated style settings, suitable for inclusion in a CSS
// stylesheet or in a STYLE attribute of an HTML element.
//
// @see getCellStyle()
//
// @param record (ListGridRecord) cell record as returned by getCellRecord
// @param rowNum (number) row number for the cell
// @param colNum (number) column number of the cell
// @return (string) CSS text for this cell
// @visibility external
//<
getCellCSSText : "record,rowNum,colNum",
// doc'd above
cellIsEnabled : "rowNum,colNum",
// customizing table geometry
// --------------------------------------------------------------------------------------------
//> @method gridRenderer.getRowHeight()
// Return the height this row should be. Default is this.cellHeight. If
// +link{GridRenderer.fixedRowHeights} is false, the row may be rendered taller than this
// specified size.
//
// If records will be variable height,
// you should switch on +link{gridRenderer.virtualScrolling, virtualScrolling}.
// @param record (ListGridRecord) cell record as returned by getCellRecord
// @param rowNum (number) row number
// @return (number) height in pixels
// @visibility external
//<
getRowHeight : "record,rowNum",
//> @method gridRenderer.getRowSpan() ([A])
// Return how many rows this cell should span. Default is 1.
//
// NOTE: if using horizontal incremental rendering, getRowSpan()
may be called for
// a rowNum in the middle of a spanning cell, and should return the remaining span from
// that rowNum onward.
//
// NOTE: if a cell spans multiple rows, getCellRecord/Style/etc will be called with the topmost
// row coordinates only.
//
// @param record (ListGridRecord) cell record as returned by getCellRecord
// @param rowNum (number) row number for the cell
// @param colNum (number) column number of the cell
// @return (number) number of cells to span
// @visibility external
//<
getRowSpan : "record,rowNum,colNum",
// synthetic row/cell events
// --------------------------------------------------------------------------------------------
//> @method gridRenderer.cellOut() ([A])
// Called when the mouse pointer leaves a cell
//
// @group events
// @param record (ListGridRecord) cell record as returned by getCellRecord
// @param rowNum (number) row number for the cell
// @param colNum (number) column number of the cell
// @return (boolean) whether to cancel the event
// @visibility external
//<
cellOut : "record,rowNum,colNum",
//> @method gridRenderer.cellOver() ([A])
// Called when the mouse pointer enters a cell
//
// @group events
// @param record (ListGridRecord) cell record as returned by getCellRecord
// @param rowNum (number) row number for the cell
// @param colNum (number) column number of the cell
// @return (boolean) whether to cancel the event
// @visibility external
//<
cellOver : "record,rowNum,colNum",
//> @method gridRenderer.rowOut() ([A])
// Called when the mouse pointer leaves a row
//
// @group events
// @param record (ListGridRecord) cell record as returned by getCellRecord
// @param rowNum (number) row number for the cell
// @param colNum (number) column number of the cell
// @return (boolean) whether to cancel the event
// @visibility external
//<
rowOut : "record,rowNum,colNum",
//> @method gridRenderer.rowOver() ([A])
// Called when the mouse pointer enters a row
//
// @group events
// @param record (ListGridRecord) cell record as returned by getCellRecord
// @param rowNum (number) row number for the cell
// @param colNum (number) column number of the cell
// @return (boolean) whether to cancel the event
// @visibility external
//<
rowOver : "record,rowNum,colNum",
//> @method gridRenderer.cellMove() ([A])
// Called when the mouse pointer moves within a cell
//
// @group events
// @param record (ListGridRecord) cell record as returned by getCellRecord
// @param rowNum (number) row number for the cell
// @param colNum (number) column number of the cell
// @return (boolean) whether to cancel the event
// @visibility internal
//<
cellMove : "record,rowNum,colNum",
//> @method gridRenderer.rowMove() ([A])
// Called when the mouse pointer moves within a row
//
// @group events
// @param record (ListGridRecord) cell record as returned by getCellRecord
// @param rowNum (number) row number for the cell
// @param colNum (number) column number of the cell
// @return (boolean) whether to cancel the event
// @visibility internal
//<
rowMove : "record,rowNum,colNum",
//> @method gridRenderer.cellContextClick() ([A])
// Called when a cell receives a contextclick event.
//
// @group events
// @param record (ListGridRecord) cell record as returned by getCellRecord
// @param rowNum (number) row number for the cell
// @param colNum (number) column number of the cell
// @return (boolean) whether to cancel the event
// @visibility external
//<
cellContextClick : "record,rowNum,colNum",
//> @method gridRenderer.rowContextClick() ([A])
// Called when a row receives a contextclick event.
// @group events
// @param record (ListGridRecord) cell record as returned by getCellRecord()
// @param rowNum (number) row number for the cell
// @param colNum (number) column number of the cell
// @return (boolean) whether to cancel the event
// @visibility external
//<
rowContextClick : "record,rowNum,colNum",
// legacy support
recordContextClick : "record,recordNum,fieldNum",
//> @method gridRenderer.cellMouseDown() ([A])
// Called when a cell receives a mousedown event.
//
// @group events
// @param record (ListGridRecord) cell record as returned by getCellRecord()
// @param rowNum (number) row number for the cell
// @param colNum (number) column number of the cell
// @return (boolean) whether to cancel the event
// @visibility external
//<
cellMouseDown : "record,rowNum,colNum",
//> @method gridRenderer.rowMouseDown() ([A])
// Called when a row receives a mousedown event.
//
// @group events
// @param record (ListGridRecord) record object returned from 'getCellRecord()'
// @param rowNum (number) row number for the cell
// @param colNum (number) column number of the cell
// @return (boolean) whether to cancel the event
// @visibility external
//<
rowMouseDown : "record,rowNum,colNum",
// legacy
recordMouseDown : "recordNum,fieldNum",
//> @method gridRenderer.cellMouseUp() ([A])
// Called when a cell receives a mouseup event.
//
// @group events
// @param record (ListGridRecord) Record object (retrieved from getCellRecord(rowNum, colNum))
// @param rowNum (number) row number for the cell
// @param colNum (number) column number of the cell
// @return (boolean) whether to cancel the event
// @visibility external
//<
cellMouseUp : "record,rowNum,colNum",
//> @method gridRenderer.rowMouseUp() ([A])
// Called when a row receives a mouseup event.
//
// @group events
// @param record (ListGridRecord) Record object returned from getCellRecord()
// @param rowNum (number) row number for the cell
// @param colNum (number) column number of the cell
// @return (boolean) whether to cancel the event
// @visibility external
//<
rowMouseUp : "record,rowNum,colNum",
recordMouseUp : "recordNum,fieldNum",
//> @method gridRenderer.selectOnMouseDown() ([A])
// Called when a cell / record receives a mouseDown event, if no cell / row level mouseDown
// handlers return false.
// Default implementation handles selection by calling this.selection.selectOnMouseDown()
//
// @group selection
// @param record (ListGridRecord) Record object returned from getCellRecord()
// @param rowNum (number) row number for the cell
// @param colNum (number) column number of the cell
//<
selectOnMouseDown : "record,rowNum,colNum",
//> @method gridRenderer.selectOnRightMouseDown() ([A])
// Called when a cell / record receives a right mouseDown event, if this.canSelectOnRightMouse
// is true.
// Default implementation handles selection by calling this.selectOnMouseDown()
//
// @group selection
// @param record (ListGridRecord) Record object returned from getCellRecord()
// @param rowNum (number) row number for the cell
// @param colNum (number) column number of the cell
// @see selectOnMouseDown()
//<
selectOnRightMouseDown : "record,rowNum,colNum",
//> @method gridRenderer.selectOnMouseUp() ([A])
// Called when a cell / record receives a mouseUp event, if no cell / row level mouseUp
// handlers return false.
// Default implementation handles selection by calling this.selection.selectOnMouseUp()
//
// @group selection
// @param record (ListGridRecord) Record object returned from getCellRecord()
// @param rowNum (number) row number for the cell
// @param colNum (number) column number of the cell
//<
selectOnMouseUp : "record,rowNum,colNum",
//> @method gridRenderer.cellClick() ([A])
// Called when a cell receives a click event.
//
// @group events
// @param record (ListGridRecord) Record object returned from getCellRecord()
// @param rowNum (number) row number for the cell
// @param colNum (number) column number of the cell
// @return (boolean) whether to cancel the event
// @visibility external
//<
cellClick : "record,rowNum,colNum",
//> @method gridRenderer.cellDoubleClick() ([A])
// Called when a cell receives a double click event.
//
// @group events
// @param record (ListGridRecord) Record object returned from getCellRecord()
// @param rowNum (number) row number for the cell
// @param colNum (number) column number of the cell
// @return (boolean) whether to cancel the event
// @visibility external
//<
cellDoubleClick : "record,rowNum,colNum",
//> @method gridRenderer.rowClick() ([A])
// Called when a row receives a click event.
//
// @group events
// @param record (ListGridRecord) Record object returned from getCellRecord()
// @param rowNum (number) row number for the cell
// @param colNum (number) column number of the cell
// @return (boolean) whether to cancel the event
// @visibility external
//<
rowClick : "record,rowNum,colNum",
//> @method gridRenderer.rowDoubleClick() ([A])
// Called when a row receives a double click event.
//
// @group events
// @param record (ListGridRecord) Record object returned from getCellRecord()
// @param rowNum (number) row number for the cell
// @param colNum (number) column number of the cell
// @return (boolean) whether to cancel the event
// @visibility external
//<
rowDoubleClick : "record,rowNum,colNum",
// Hover events
// --------------------------------------------------------------------------------------------
//> @method gridRenderer.cellHover() ([A])
// Called when the mouse hovers over a cell if this.canHover is true.
// Returning false will suppress the hover text from being shown if this.showHover is true.
//
// @group events
// @see canHover
// @param record (ListGridRecord) cell record as returned by getCellRecord
// @param rowNum (number) row number for the cell
// @param colNum (number) column number of the cell
// @return (boolean) whether to cancel the event
// @visibility external
//<
cellHover : "record,rowNum,colNum",
//> @method gridRenderer.rowHover() ([A])
// Called when the mouse hovers over a row if this.canHover is true.
// Returning false will suppress the hover text from being shown if this.showHover is true.
//
// @group events
// @see canHover
// @param record (ListGridRecord) cell record as returned by getCellRecord
// @param rowNum (number) row number for the cell
// @param colNum (number) column number of the cell
// @return (boolean) whether to cancel the event (default behavior of showing the hover)
// @visibility external
//<
rowHover : "record,rowNum,colNum",
//> @method gridRenderer.cellHoverHTML() ([A])
// StringMethod to dynamically assemble an HTML string to show in a hover window over the
// appropriate cell/record when this.canHover and this.showHover are both true.
// Called when the mouse hovers over a cell.
//
// @group events
// @param record (ListGridRecord) cell record as returned by getCellRecord
// @param rowNum (number) row number for the cell
// @param colNum (number) column number of the cell
// @return (html) the html to be shown inside the hover for this cell
// @see canHover
// @see showHover
// @visibility external
//<
cellHoverHTML : "record,rowNum,colNum",
//> @method gridRenderer.getCellHoverComponent() ([A])
// StringMethod to dynamically create a Canvas-based component to show as a hover window
// over the appropriate cell/record when this.canHover and this.showHover are both true and
// when an override of getCellHoverComponent() is present.
// Called when the mouse hovers over a cell.
//
// @group events
// @param record (ListGridRecord) cell record as returned by getCellRecord
// @param rowNum (number) row number for the cell
// @param colNum (number) column number of the cell
// @return (Canvas) a Canvas to be shown as the hover for this cell
// @see canHover
// @see showHover
// @visibility external
//<
getCellHoverComponent : "record,rowNum,colNum",
// selection notification
// --------------------------------------------------------------------------------------------
//> @method gridRenderer.selectionChanged() ([A])
// Called when (row-based) selection changes within this grid. Note this method fires for
// each record for which selection is modified - so when a user clicks inside a grid this
// method will typically fire twice (once for the old record being deselected, and once for
// the new record being selected).
//
// NOTE: For updating other components based on selections or triggering selection-oriented
// events within an application, see the
// +link{dataBoundComponent.selectionUpdated,selectionUpdated} event
// which is likely more suitable.
//
// @param record (ListGridRecord) record for which selection changed
// @param state (boolean) New selection state (true for selected, false for unselected)
// @group selection
// @visibility external
//<
selectionChanged : "record,state",
// Doc'd as DataBoundComponent.selectionUpdated
selectionUpdated : "record,recordList",
//> @method gridRenderer.cellSelectionChanged() ([A])
// Called when (cell-based) selection changes within this grid.
//
// @param cellList (array) Array of cells whos selected state was modified.
// @return (boolean) Returning false will prevent the GridRenderer styling from being updated
// to reflect the selection change.
// @group selection
// @visibility external
//<
cellSelectionChanged : "cellList",
// IDs for legacy test tools; JSDoc above
getRowElementId : "rowNum,physicalRowNum",
getCellElementId : "rowNum,physicalRowNum,colNum,physicalColNum",
// Row Heights and Embedded Components
// ---------------------------------------------------------------------------------------
shouldFixRowHeight : "record,rowNum",
updateEmbeddedComponentZIndex : "component",
updateEmbeddedComponentCoords : "component,record,rowNum,colNum",
// WAI ARIA
// ---------------------------------------------------------------------------------------
getRowRole : "rowNum,record",
getRowAriaState : "rowNum,record",
getCellRole : "rowNum,colNum,record",
getCellAriaState : "rowNum,colNum,record"
};
isc.GridRenderer.registerStringMethods(isc.GridRenderer._gridAPIs);
© 2015 - 2024 Weber Informatics LLC | Privacy Policy
");
}
//>DEBUG timing
if (this.logIsDebugEnabled("gridHTML")) {
var totalTime = (isc.timeStamp() - t0),
numCells = (numCols * (endRow - startRow)),
perCell = (totalTime / numCells),
perSecond = (1000 / perCell);
// toFixed appears not to be supported in Safari
if (perCell.toFixed != null) perCell = perCell.toFixed(2);
if (perSecond.toFixed != null) perSecond = perSecond.toFixed(2);
this.logDebug("getTableHTML: columns " + (discreteCols ? colNums
: startCol + "->" + (endCol-1)) +
", rows " + startRow + "->" + (endRow-1) +
", time: " + totalTime + "ms (" +
numCells + " cells at " +
perCell + "ms per cell, " +
perSecond + " cells per second), " +
"spacerHeights: [" + [startSpacerHeight, endSpacerHeight] + "], " +
"left/right pad: [" + [leftColPad, rightColPad] + "], " +
singleCells + " single cell rows",
"gridHTML");
}
//
");
}
//
// output the start table tag
//
// XXX: If height of the list is screwy in IE5 until the cursor passes over it,
// we should set the height of the table explicitly
// Note: We divide large tables into chunks so we can assemble the HTML in separate threads
// (avoids script is running slowly message). Avoid writing out the outer table tags for every
// chunk so the HTML ends up in a single table.
if (!this._printingChunk || startRow == 0) {
output.append(
"