
/*

  SmartClient Ajax RIA system
  Version v9.0p_2021-05-01/LGPL Deployment (2021-05-01)

  Copyright 2000 and beyond Isomorphic Software, Inc. All rights reserved.
  "SmartClient" is a trademark of Isomorphic Software, Inc.

  LICENSE NOTICE
     INSTALLATION OR USE OF THIS SOFTWARE INDICATES YOUR ACCEPTANCE OF
     ISOMORPHIC SOFTWARE LICENSE TERMS. If you have received this file
     without an accompanying Isomorphic Software license file, please
     contact licensing@isomorphic.com for details. Unauthorized copying and
     use of this software is a violation of international copyright law.

  DEVELOPMENT ONLY - DO NOT DEPLOY
     This software is provided for evaluation, training, and development
     purposes only. It may include supplementary components that are not
     licensed for deployment. The separate DEPLOY package for this release
     contains SmartClient components that are licensed for deployment.

  PROPRIETARY & PROTECTED MATERIAL
     This software contains proprietary materials that are protected by
     contract and intellectual property law. You are expressly prohibited
     from attempting to reverse engineer this software or modify this
     software for human readability.

  CONTACT ISOMORPHIC
     For more information regarding license rights and restrictions, or to
     report possible license violations, please contact Isomorphic Software
     by email (licensing@isomorphic.com) or web (www.isomorphic.com).

*/

if(window.isc&&window.isc.module_Core&&!window.isc.module_Forms){isc.module_Forms=1;isc._moduleStart=isc._Forms_start=(isc.timestamp?isc.timestamp():new Date().getTime());if(isc._moduleEnd&&(!isc.Log||(isc.Log && isc.Log.logIsDebugEnabled('loadTime')))){isc._pTM={ message:'Forms load/parse time: ' + (isc._moduleStart-isc._moduleEnd) + 'ms', category:'loadTime'};
if(isc.Log && isc.Log.logDebug)isc.Log.logDebug(isc._pTM.message,'loadTime');
else if(isc._preLog)isc._preLog[isc._preLog.length]=isc._pTM;
else isc._preLog=[isc._pTM]}isc.definingFramework=true;

if (window.isc && isc.version != "v9.0p_2021-05-01/LGPL Deployment") {
    isc.logWarn("SmartClient module version mismatch detected: This application is loading the core module from "
        + "SmartClient version '" + isc.version + "' and additional modules from 'v9.0p_2021-05-01/LGPL Deployment'. Mixing resources from different "
        + "SmartClient packages is not supported and may lead to unpredictable behavior. If you are deploying resources "
        + "from a single package you may need to clear your browser cache, or restart your browser."
        + (isc.Browser.isSGWT ? " SmartGWT developers may also need to clear the gwt-unitCache and run a GWT Compile." : ""));
}







isc.Canvas.addClassMethods({

//>    @method    Canvas.applyTableResizePolicy()    (A)
// Given a set of items to be shown in a table, this method determines the sizing / positioning
// to be applied to each item.
//
// We factor the placing of titles next to elements into the table here to have them
// automatically take up columns in the output.
//
// Sets up _rowHeights and _colWidths on the items array
// Sets up _size property (2 element array for width,height) and _titleWidth on each item.
//
// @group drawing
//<
// Note:
// The "_rowTable" property stored on the passed-in items can be reused IF no new items are
// visible that were not visible before.  It is up to the calling function to clear out an old
// _rowHeights if necessary.
//
//

applyTableResizePolicy : function (items, totalWidth, totalHeight,
                                   numCols, colWidths, rowHeights, overflowedAsFixed) {
    var logDebug = this.logIsDebugEnabled("tablePolicy"),
        logInfo = this.logIsInfoEnabled("tablePolicy"),
        logPlacement = this.logIsDebugEnabled("tablePlacement");

    // determine row and column start and end coordinates for each item based on rowSpan,
    // colSpan, startRow and endrow properties

    var rowTable = items._rowTable;

    // If we've previously run the resizePolicy and it is still valid, don't do the
    // work again.
    if (!this._tableResizePolicyIsValid(items)) {

        // determine row and column start and end coordinates for each item based on rowSpan,
        // colSpan, startRow and endrow properties
        rowTable = items._rowTable = [];

        var currentRow = 0,
            currentCol = 0
        ;

        // iterate through the items,
        //    placing them in a rowTable according to their rowSpan and colSpan entries
        for (var itemNum = 0; itemNum < items.length; itemNum++) {
            // get a pointer to the item
            var item = items[itemNum];

            // if the item is not visible, skip it
            // NOTE: an algorithm BEFORE this one might want to mark items as invisible
            //            based on a showIf property or something like that
            if (!item.alwaysTakeSpace && !item.visible) continue;

            var itemCols = item.getColSpan(),
                itemRows = item.getRowSpan();

            // if the item has rowSpan == 0 or colSpan == 0, skip it
            //  this lets us ignore items that should be output (and thus are visible)
            //    but don't factor into the table
            if (itemRows == 0 || itemCols == 0) continue;

            if (itemCols == null) itemCols = 1;
            if (itemRows == null) itemRows = 1;

            var requiredCols = itemCols;
            if (itemCols == "*") requiredCols = 1;

            // add another column for a separate cell for left/right oriented titles
            // NOTE: extra cells not added for top or bottom-oriented titles
            var orientation = item.getTitleOrientation();
            if (item.showTitle &&
                (orientation == isc.Canvas.LEFT || orientation == isc.Canvas.RIGHT))
            {
                // NOTE: we assume colSpan * and showTitle:true means at least two columns
                requiredCols += (item.getTitleColSpan() || 1);
                if (itemCols != "*") itemCols += (item.getTitleColSpan() || 1);
            }

            var startRow = (item.isStartRow ? item.isStartRow() : item.startRow),
                endRow = (item.isEndRow ? item.isEndRow() : item.endRow);

            if (logPlacement) {
                this.logDebug("at: " + ["row" + currentRow, "col" + currentCol] +
                              ", item: " + (item.name || item.Class) +
                              // report colSpan "*" separately since the actual number of
                              // columns we'll take up isn't computed til later, requiredCols
                              // just represents the number of columns the item *must* have
                              (itemCols == "*" ? ", colSpan:'*'" : "") +
                              ", required cols:" + requiredCols +
                              (itemRows > 1 ? ", rowSpan:" + itemRows : "") +
                              (startRow ? ", startRow:true" : "") +
                              (endRow ? ", endRow:true" : ""),
                              "tablePlacement");
            }

            var placeRow = null, placeCol = null;



            if (currentCol >= numCols || (startRow && currentCol != 0)) {
                currentRow++;
                currentCol = 0;
                item._startRow = true;
                //this.logWarn("advanced to row: " + currentRow);
            } else { item._startRow = false; }

            // if we're within the table, see if we can place the item in an existing row
            // NOTE: rowSpanning items in this and previous rows means there may be several
            // partially filled rows to look through for sufficient open space for this item.
            if (currentRow < rowTable.length) {
                //this.logWarn("looking in existing rows starting at: " + currentRow);
                // find the next row with available space
                for (; currentRow < rowTable.length; currentRow++) {
                    var rowSlots = rowTable[currentRow];

                    //this.logWarn("trying row: " + currentRow);

                    // no row created yet
                    if (rowSlots == null) break;

                    // look for an open slot
                    for (; currentCol < numCols; currentCol++) {
                        if (rowSlots[currentCol] != null) continue;

                        // check that there are enough open slots in a row to accommodate the
                        // colSpan of this item.  This covers the case of cells reserved by
                        // rowSpanning items in previous rows.
                        for (var j = currentCol; j < numCols; j++) {
                            //this.logWarn("checking at open spot in column: " + currentCol);
                            // ran into an occupied slot before we found a spot
                            if (rowSlots[j] != null) break;

                            if ((j - currentCol) + 1 >= requiredCols) {
                                // there's enough room to accommodate this item starting at
                                // column i on this row.
                                // Note that we don't have to check for cells reserved in rows
                                // beneath us.  Only items from rows above us could possibly
                                // have reserved cells beneath us, and they'd have to reserve
                                // the intervening cells.
                                placeRow = currentRow;
                                placeCol = currentCol;
                                break;
                            }
                        }
                        if (placeCol != null) break;
                    }
                    if (placeCol != null) break;
                    // moving on to new row, go back to first column
                    //this.logWarn("no spot in row: " + currentRow + ", advancing");
                    currentCol = 0;
                    item._startRow = true;
                }
                //if (placeCol != null) this.logWarn("found spot in row: " + currentRow);
            }
            // no spots in existing rows, create a new row
            if (placeCol == null) {
                //this.logWarn("created new row: " + currentRow);
                placeRow = currentRow;
                placeCol = 0;
                item._startRow = true;
                // NOTE: an item with an invalid colSpan which is > numCols will never be
                // placed on an existing row, hence hits this case and ends up at column 0 of a
                // new row.
            }

            currentCol = placeCol;

            // if colSpan is variable, now that we've picked a row we can resolve it
            if (itemCols == "*") itemCols = numCols - currentCol;

            // NOTE: rowSpan of "*" not supported!
            // this is because we don't know how many rows there will be, so we don't
            //    have a good way to assign the item to each row going down (?)
            if (!isc.isA.Number(itemRows)) itemRows = 1;

            // note the shape of this item in the rowTable (fill in the grid)
            // for each row to output

            for (var r = currentRow; r < currentRow + itemRows; r++) {
                // if there's not a column array in that row, create one
                if (!rowTable[r]) rowTable[r] = [];
                // for each column to output
                for (var c = currentCol; c < currentCol + itemCols; c++) {
                    // stick the number of this item in the row
                    rowTable[r][c] = itemNum;
                }
            }

            // have the item remember how many rows and columns it's actually taking up
            // as an array of numbers:   [startCol, startRow, endCol, endRow]
            //    NOTE: endCol and endRow are NOT inclusive
            item._tablePlacement = [placeCol, placeRow,
                                    placeCol + itemCols, placeRow + itemRows];



            // advance currentCol by the number of columns taken up
            currentCol += itemCols;
            // if the item is configured to end its row, advance past the last column in the
            // row, so the next iteration of the loop will start the new row
            if (endRow) currentCol = numCols;

            if (logPlacement) {
                this.logDebug("item: " + (item.name || item.Class) +
                              " placed at: " + ["row" + placeRow, "col" + placeCol] +
                              (item._startRow ? ", marked startRow " : "") +
                              ", rowTable: " + this.echoAll(rowTable), "tablePlacement");
            }
        }

        // at this point, we know the row and column coordinate where each item will be placed


        var emptyRows = [];
        for (var r = 0; r < rowTable.length; r++) {
            var row = rowTable[r];
            if (row == null) break;

            var emptyCells = 0, lastItem = null;
            for (var c = 0; c < row.length; c++) {
                // empty cell
                if (row[c] == null) {
                    emptyCells++;
                    continue;
                }
                // cell spanned by item in previous row
                if (r > 0 && rowTable[r-1] != null && row[c] == rowTable[r-1][c]) continue;

                // occupied cell
                var itemNum = row[c],
                    item = items[itemNum];

                // if we're still in the same colSpanning item, continue
                if (item == lastItem || item == null) continue;

                // mark this item with the number of empty cells and rows that precede it
                item._emptyRows = emptyRows;
                item._emptyCells = emptyCells;
                if (logPlacement && (emptyCells > 0 || emptyRows.length > 0)) {
                    this.logDebug("itemNum:" + itemNum + " (" + (item.name || item.Class) +
                                  ") at: " + ["row" + placeRow, "col" + placeCol] +
                                  " preceded by " +
                                  (emptyCells > 0 ? emptyCells + " empty cells" : "") +
                                  (emptyRows.length > 0 ?
                                     " " + emptyRows.length + " empty rows" : ""),
                                  "tablePlacement");
                }
                // reset the counter
                emptyCells = 0;
                emptyRows = [];
                lastItem = item;
            }
            // if we didn't encounter any items on this row, we need to skip a row
            // Record how many empty cells are in this row
            if (lastItem == null) {
                emptyRows.add(emptyCells + (numCols-row.length));
                emptyCells = 0;
            }
        }
        // if we have empty rows beyond the last item(s) in the table, reduce the
        // rowSpan specification of those items.

        if (emptyRows != null && rowTable.length > 0) {
            var emptyRowCount = emptyRows.length;
            var row = rowTable[rowTable.length-1];
            for (var c = 0; c < row.length; c++) {
                var itemNum = row[c];
                    item = items[itemNum];
                if (item == null) continue;

                var rowSpan = item._tablePlacement[3] - item._tablePlacement[1];
                rowSpan -= emptyRowCount;
                item._rowSpan = rowSpan;
            }

        }
    }

    // if column widths were not specified, calculate them from the rowTable
    if (!colWidths || !isc.isAn.Array(colWidths)) {    // && !items.colWidths) {
        //>DEBUG
        if (!isc.isAn.Array(colWidths)) {
            this.logWarn(" 'colWidths' not an array - Ignoring.", "tableResizePolicy");
        }
        //<DEBUG


        colWidths = [];
    }

    // transform any "*" or "%" items in the colWidths to things the stretchResizeList can deal
    // with.  NOTE: don't modify the passed-in Array
    colWidths = colWidths.duplicate();
    for (var c = 0; c < colWidths.length; c++) {
        //    colWidths[c] = [colMinWidth, rowMaxWidth, colMaxPercent, colStarCount];

        var width = colWidths[c];
        if (isc.isA.String(width)) {
            if (width == "*") colWidths[c] = [0, 10000, 0, 1];
            else if (width.contains("*")) colWidths[c] = [0, 10000, 0, parseInt(width)];
            else if (width.contains("%")) colWidths[c] = [0, 10000, parseInt(width), 0];
            // catch a quoted number and convert it to a real number
            else {
                var parsed = parseInt(width);
                if (parsed == width) {
                    colWidths[c] = parsed;
                } else {
                    this.logWarn("Failed to understand specified colWidth:"+ width);
                    // treat as "*"
                    colWidths[c] = [0,10000,0,1];
                }
            }
        }
    }

    // remember the colWidths in the items
    items.colWidths = colWidths;

    // look through all the items in each row and gather:
    // [ min pixel height,
    //   max pixel height,
    //   largest "*" size,
    //   largest percent size ]
    if (!rowHeights) {// && !items.rowHeights) {
        rowHeights = [];

        // for each row in the rowTable
        for (var r = 0; r < rowTable.length; r++) {
            var row = rowTable[r],
                rowMinHeight = null,
                rowMaxHeight = 100000,
                rowMaxPercent = 0,
                rowStarCount = 0
            ;
            if (!row) continue;

            // for each column in that row
            for (var c = 0; c < row.length; c++) {
                // get the item and its preferred height
                var item = items[row[c]];
                if (!item) continue;

                var itemHeight = item.getCellHeight(overflowedAsFixed);

                // if the item takes up more than one row, split evenly amongst its rows ???
                var itemRows = (item._tablePlacement[3] - item._tablePlacement[1]);

                if (logDebug) this.logWarn("item at: " + [r,c] + " has height: " + itemHeight +
                                           ", item is: " + item);

                item._isVariableHeight = false;

                // if the itemHeight is a number
                if (isc.isA.Number(itemHeight)) {
                    // NOTE: if the item takes up more than one row, split it evenly across its
                    // rows
                    itemHeight = Math.floor(itemHeight / itemRows);

                    if (logDebug) this.logWarn("item: " + item + " has pixel size: " + itemHeight);

                    // if this is the first item to specify a pixel size, or is larger than any
                    // previous specified size or minimum size, it becomes the new minimum
                    if (rowMinHeight == null || itemHeight > rowMinHeight) {
                        rowMinHeight = itemHeight;
                    }

                    // if this item specifies a pixel size larger than a previously specified
                    // max, raise the max height for the row as a whole
                    if (itemHeight > rowMaxHeight) rowMaxHeight = itemHeight;

                // if the itemHeight is a string (a relative size)
                } else if (isc.isA.String(itemHeight)) {
                    // if height is "*" or "2*"
                    if (itemHeight.contains("*")) {
                        item._isVariableHeight = true;

                        // get the starCount as a number
                        // NOTE: if the item takes up more than one row, split it evenly across
                        // its rows
                        var itemStarCount = (itemHeight == "*" ? 1 : parseFloat(itemHeight))
                                                    / itemRows;

                        if (logDebug) this.logWarn("item: " + item + " has star size: " + itemStarCount);

                        rowStarCount = Math.max(rowStarCount, itemStarCount);

                    // else if height is a percentage
                    } else {
                        item._isVariableHeight = true;

                        // get the percentage as a number
                        // NOTE: if the item takes up more than one row, split it evenly across
                        // its rows
                        var itemPercent = parseFloat(itemHeight) / itemRows;

                        if (logDebug) this.logWarn("item: " + item + " has percent size: " + itemPercent);

                        // and remember it if it's greater than the max percent already seen in
                        // this row
                        if (itemPercent > rowMaxPercent) rowMaxPercent = itemPercent;
                    }

                    // check for minHeight / maxHeight settings on flexible-sized items

                    // the row must be as big as the minHeight of the item
                    if (item.minHeight > rowMinHeight) {
                        rowMinHeight = item.minHeight;
                    }

                    // NOTE: minimums should win out over maximums

                    // allow an item's minHeight to win out over another item's previously
                    // specified maxHeight
                    if (item.minHeight > rowMaxHeight) {
                        rowMaxHeight = item.minHeight;
                    }

                    // lower row maxHeight only to the largest previously specified
                    // item.minHeight (rowMinHeight)
                    if (item.maxHeight < rowMaxHeight &&
                        rowMinHeight < item.maxHeight)
                    {
                        rowMaxHeight = item.maxHeight
                    }
                }

                // remember the characteristics of this row
                // if a percentage or star was found, remember all the values
                if (rowMaxPercent > 0 || rowStarCount > 0) {
                    // no one set a pixel size or minHeight.  Default to 0
                    if (rowMinHeight == null) rowMinHeight = 0;
                    rowHeights[r] = [rowMinHeight, rowMaxHeight, rowMaxPercent, rowStarCount];
                } else {
                    if (rowMinHeight == null) {
                        // there were no specified sizes for the row (pixel, '*' or percent)
                        rowMinHeight = items._defaultRowHeight || 22;
                    }
                    rowHeights[r] = rowMinHeight;
                }
            }
        }
    }
    // remember the rowHeights in the items
    items.rowHeights = rowHeights;

    if (logInfo) this.logInfo("\ntotalWidth: " + totalWidth +
                              ", totalHeight: " + totalHeight +
                              "\nspecified sizes:\n" +
                              "cols:" + this.echoAll(items.colWidths) +
                              ", rows: " + this.echoAll(items.rowHeights),
                              "tablePolicy");

    // get real row and column sizes
    items._colWidths = colWidths = isc.Canvas.stretchResizeList(items.colWidths, totalWidth);
    items._rowHeights = rowHeights = isc.Canvas.stretchResizeList(items.rowHeights, totalHeight);

    if (logInfo) this.logInfo("\nderived sizes:\n" +
                              "cols:" + this.echoAll(items._colWidths) +
                              ", rows: " + this.echoAll(items._rowHeights),
                              "tablePolicy");

    // we have widths and heights for each column and row.  Now apply those sizes to the items,
    // which may span multiple columns or rows
    // NOTE: we currently only support "*" sizes, not percents
    for (itemNum = 0; itemNum < items.length; itemNum++) {
        item = items[itemNum];
        if (!item.visible) continue;
        var isACanvas = isc.isA.Canvas(item),
            // Note - use getWidth() / getHeight() on canvii, to allow us to store height / width
            // with different (internal) property names if we choose to do so.
            width = isACanvas ? item.getWidth() : item.width,
            height = isACanvas ? item.getHeight() : item.getCellHeight(overflowedAsFixed),
            orientation = item.getTitleOrientation(),
            placement = item._tablePlacement,
            // We need the derived title width in order to manage title cell clipping properly
            // in form items. If we're not showing a title, of course this will be zero.
            titleWidth = 0;

        if (placement == null) continue;

        if (item.showTitle) {
            if (orientation == isc.Canvas.LEFT) {
                titleWidth = colWidths[placement[0]];
            } else {
                titleWidth = colWidths[placement[2]];
            }
        }

        // account for variable width items.  NOTE: we don't support percent widths on items
        if (width == "*" || width == "100%") {
            width = 0;

            var colSpan = item.getTitleColSpan() || 1,
                skipBefore = (item.showTitle && orientation == isc.Canvas.LEFT) ? colSpan : 0,
                skipAfter = (item.showTitle && orientation == isc.Canvas.RIGHT) ? colSpan : 0,
                startCol = placement[0] + skipBefore,
                endCol = Math.min(colWidths.length, placement[2] - skipAfter)

            ;

            //this.logWarn("item ID: " + item.ID + ", startCol: " + startCol +
            //             ", endCol: " + endCol + ", colWidths: " + colWidths);

            for (var c = startCol; c < endCol; c++) {
                width += colWidths[c];
            }

        }

        // account for variable height items
        if (item._isVariableHeight) {
            height = 0;
            var startRow = placement[1], endRow = placement[3];

            // NOTE: don't need logic for extra cells for titles, because extra cells aren't
            // added for top or bottom-oriented titles
            for (var c = startRow; c < endRow; c++) {
                height += rowHeights[c];
            }
        }

        // remember the width and height of the item
        item._size = [width, height];
        // Remember the width of the item title
        item._titleWidth = titleWidth
    }
},

// This method should determine whether
// - tableResizePolicy has been run on this table already
// - any items visibility have changed since the policy was run
// - any items have been moved within the items array (or items removed / new items introduced)
_tableResizePolicyIsValid : function (items) {

    if (!items._rowTable) return false;
    return true;
},

// Helper method to mark an already run policy as invalid.
invalidateTableResizePolicy : function (items) {
    delete items._rowTable;
    delete items._rowHeights;
    delete items._colWidths;
},


//>    @method    Canvas.stretchResizeList()    (A)
//         Given a list of inputs sizes as:
//            a number
//                or
//            [minSize, maxSize, maxPercent, starCount]
//        and a totalSize, figure out the size of the dynamically sized items
//        according to the totalSize.
//
//        You can use percentages or fixed sizes to go beyond the totalSize
//
//        @group    drawing
//        @param    inputSizes        (array)        array of sizes (see above)
//        @param    totalSize        (number)    total sizes for the
//
//        @return    (number[])                output sizes (all numbers)
//<
stretchResizeList : function (inputSizes, totalSize) {
    var totalPercent = 0,  // amount "%" items amount to
        starCount = 0,     // number of "*" star items
        totalFixed = 0,    // total space taken up by fixed-size items
        outputSizes = inputSizes.duplicate();

    for (var i = 0; i < inputSizes.length; i++) {
        var size = outputSizes[i];

        if (isc.isA.Number(size)) {
            // fixed size item
            size = Math.max(size,1); // assure at least 1
            totalFixed += size;
            outputSizes[i] = size;
        } else {
            // variable (% / * / both) sized item
            var rowPercent = size[2],
                rowStarCount = size[3]
            ;
            // if a percent without a "*"
            if (rowStarCount == 0) {
                // percentage -- add it to the percentage total
                totalPercent += rowPercent;
            }
            // tracked total amount of "*"s
            starCount += rowStarCount;
        }
    }

    // at this point,
    // - totalFixed is the total of the fixed, absolute sizes
    // - totalPercent is the total percentage numbers (that aren't stars)
    // - starCount is the total number of stars across all rows (even if those rows also have
    //   percents specified)


    // - "stars" are translated to percents, sharing all remaining percent points (of 100)
    //   not allocated to specified percent sizes
    // - stars and percents share all space not allocated to static numbers
    // - if there are any percents or stars, all space is always filled
    if (starCount) {
        var starPercent = 0;
        if (totalPercent < 100) {
            // star sized items share the remaining percent size
            starPercent = (100 - totalPercent) / starCount;
        }

        // assign a percentage to each star item
        //    if a row has both a star and a percentage, keep the larger item

        for (var r = 0; r < inputSizes.length; r++) {
            var size = outputSizes[r];

            if (isc.isA.Number(size)) continue; // skip fixed size items
            var rowPercent = size[2],
                rowStarCount = size[3],
                rowStarPercent = rowStarCount * starPercent;
            // if the total percentage from stars is greater than the fixed percent
            if (rowPercent < rowStarPercent) {
                // change the fixed percent
                size[2] = rowStarPercent;
            }

            // if this item had stars, it has not yet been included in totalPercent (even if it
            // specified both star and percent), so now include it's percent in totalPercent.
            // NB: We rely on "totalPercent" to be correct when we subsequently divy the
            // remainingSpace among items with percents; if it's wrong over/underflow will
            // occur.  However totalPercent does not need to equal 100 because percents are
            // just treated as proportions.
            if (rowStarCount > 0) totalPercent += size[2];
        }
    }

    // at this point,
    // - totalFixed is still the total of the fixed, absolute sizes
    // - totalPercent is the total percentage (including what used to be stars)
    // - we have no stars left

    // if nothing has variable size, we're done
    if (totalPercent <= 0) return outputSizes;

    var remainingSpace = Math.max(0, totalSize - totalFixed);

    //this.logWarn("remaining space: " + remainingSpace +
    //             ", totalPercent: " + totalPercent);

    // apply mins and maximums.  Note if an item gets set to its min or max, the behavior is
    // exactly as though the item had originally specified that fixed size.  remainingSpace is
    // reduced along with the totalPercent it was being divided by.  Note that when this
    // happens for a min, all other items get smaller, or for a max, all other items get
    // larger, so we have to recheck any previous mins or maxs.
    for (var r = 0; r < inputSizes.length; r++) {
        var    pixelsPerPercent = Math.max(0, remainingSpace / totalPercent),
            size = outputSizes[r];

        if (isc.isA.Number(size)) continue;

        var min = size[0];
        if (min == 0) continue;

        var itemPercent = size[2],
            itemPixels = pixelsPerPercent * itemPercent;

        if (itemPixels < min) {
            outputSizes[r] = min;
            remainingSpace -= min;
            totalPercent -= itemPercent;
            // NOTE: we really only have to go back to the last non-zero minimum
            r = 0;
        }
    }

    // check maximums
    for (var r = 0; r < inputSizes.length; r++) {
        var    pixelsPerPercent = Math.max(0, remainingSpace / totalPercent),
            size = outputSizes[r];

        if (isc.isA.Number(size)) continue;

        var max = size[1],
            itemPercent = size[2],
            itemPixels = pixelsPerPercent * itemPercent;

        if (itemPixels > max) {
            outputSizes[r] = max;
            remainingSpace -= max;
            totalPercent -= itemPercent;
            // NOTE: we really only have to go back to the last non-infinite maximum
            r = 0;
        }
    }

    // at this point, all remaining variable-sized items fall within their max and min.  (it's
    // also possible that all variable-sized items have been resolved to their max or min,
    // indicating overflow or underflow)
    pixelsPerPercent = Math.max(0, remainingSpace / totalPercent);
    for (var r = 0; r < inputSizes.length; r++) {
        size = outputSizes[r];
        if (isc.isA.Number(size)) continue;

        // get the percent of the total outstanding percent that goes to this item
        var itemPercent = size[2];
        outputSizes[r] = Math.floor(itemPercent * pixelsPerPercent);
    }

    // XXX do something about "remaining" pixels ???
    // return the output sizes array
    return outputSizes;
}

});    // END isc.Canvas.addMethods()




// ButtonTable: a table of clickable items
isc.ClassFactory.defineClass("ButtonTable",isc.Canvas);
isc.ButtonTable.addProperties({
    //items:null,
    cellSpacing:0,
    cellPadding:2,
    cellBorder:0,
    tableStyle:"menuTable",
    baseButtonStyle:"button",
    backgroundColor:"CCCCCC",
    useEventParts: true
});
isc.ButtonTable.addMethods({
    setItems : function (items) {
        this.items = isc.shallowClone(items);
        this.redraw();
    },
    getInnerHTML : function () {
        var output = isc.SB.newInstance();
        output.append(
            "<TABLE",
                    " CLASS=" , this.tableStyle,
                    // take off space for scrollbar if necessary
                    " WIDTH=" , this.getWidth() - (this.overflow == isc.Canvas.SCROLL || this.overflow == isc.Canvas.AUTO ? this.getScrollbarSize(): 0),
                    " HEIGHT=" , this.getHeight(),
                    " CELLSPACING=", this.cellSpacing,
                    " CELLPADDING=", this.cellPadding,
                    " BORDER=" , this.cellBorder,
                "><TR>");

        for (var r = 0; r < this.items.length; r++) {
            var row = this.items[r];
            output.append("<TR>");

            if (!isc.isAn.Array(row)) row = [row];
            for (var i = 0; i < row.length; i++) {
                var item = row[i];
                if (item.eventPart) {
                    output.append(this.getCellButtonHTML(item.contents,
                                                         item.style, item.disabled, item.selected,
                                                         item.align, item.extraTagStuff,
                                                         item.eventPart, item.eventId));
                } else {
                    output.append(this.getCellHTML(item.contents, item.style, item.align, item.extraTagStuff));
                }
            }
            output.append("</TR>");
        }

        output.append("</TABLE>");

        return output.toString();
    },

    showModal : function () {

        // Note this is not autoHide true...
        // For the date-picker that makes sense as it gives the user a way to hide the MonthMenu
        // (etc.) without hiding the entire date-picker
        this.showClickMask(this.getID() + ".hide()");
        this.show();


        this.unmask();
        this.bringToFront();
    },

    // override hide to hide the clickMask
    hide : function () {
        this.Super("hide", arguments);
        this.hideClickMask();
        this._clickMask = null;
    },

    // base style and state.
    // The "base" style can be modified to be "Over", "Selected" or "Disabled"
    // Note that "Over" and "Disabled" are mutex - we apply the standard "over" state to
    // disabled buttons (though we do support the "SelectedOver" state)

    getButtonBaseStyle : function (element) {
        var baseStyle;
        if (element) baseStyle = element.getAttribute("basestyle");
        if (!baseStyle) baseStyle = this.baseButtonStyle;
        return baseStyle;
    },

    getMouseOutStyle : function (element) {
        var baseStyle = this.getButtonBaseStyle(element);
        if (this.buttonIsSelected(element)) {
            baseStyle += "Selected"
        }
        if (this.buttonIsDisabled(element)) {
            baseStyle += "Disabled"
        }
        return baseStyle;
    },

    buttonIsSelected : function (element) {
        return element && element.getAttribute("buttonselected");
    },

    buttonIsDisabled : function (element) {
        return element && element.getAttribute("buttondisabled");
    },


    cellButtonOver : function (element) {
        var style = this.getButtonBaseStyle(element);
        if (this.buttonIsSelected(element))  style += "Selected";
        if (element) element.className = style + "Over";

    },

    cellButtonOut : function (element) {
        if (!element) return;
        element.className = this.getMouseOutStyle(element);
    },

    cellButtonDown : function (element) {
        if (element) {
            var style = this.getButtonBaseStyle(element);
            if (this.buttonIsSelected(element))  style += "Selected";
            style += "Down"
            element.className = style;
        }
    },

    getCellHTML : function (contents, style, align, extraTagStuff) {
        // No need to write basestyle onto this element - we only show dynamic-styling for
        // buttons, not standard cells
        return isc.StringBuffer.concat(
            "<TD ALIGN=" , (align || isc.Canvas.CENTER), " CLASS=" , (style || this.baseButtonStyle + "Disabled") ,
                (extraTagStuff || extraTagStuff), ">",
                contents,
            "</TD>"
        );
    },

    handleMouseDown : function (event) {

        event.touchStartReturnValue = false;

        var element = event.nativeTarget;

        // call CellButtonDown on the table cell, not the contained HTML
        if (element.tagName != "TD") element = element.parentNode;

        // do not call CellButtonDown for clicks on HTML generated by getCellHTML
        if (!element.getAttribute || !element.getAttribute(this._$eventPart)) return;

        this.cellButtonDown(element);
        this.Super("handleMouseDown", arguments);
    },

    handleMouseUp : function (event) {
        var element = event.nativeTarget;

        // call CellButtonOut on the table cell, not the contained HTML
        if (element.tagName != "TD") element = element.parentNode;

        // do not call CellButtonOut for clicks on HTML generated by getCellHTML
        if (!element.getAttribute || !element.getAttribute(this._$eventPart)) return;

        this.cellButtonOut(element);
        this.Super("handleMouseUp", arguments);
    },

    getCellButtonHTML : function (contents, style, selected, disabled, align,
                                  extraTagStuff, eventPart, id) {

        if (style == null) style = this.baseButtonStyle;
        var modifiedStyle = style;

        if (selected) modifiedStyle += "Selected";
        if (disabled) modifiedStyle += "Disabled";

        // always install an eventpart property to distinguish from getCellHTML()
        var eventHTML = " " + this._$eventPart + "=" + (eventPart ? eventPart : "_noHandler");
        if (id != null) eventHTML += " id=" + this.getID() + "_" + eventPart + "_" + id;

        return isc.StringBuffer.concat(
            "<TD ALIGN=" , (align || isc.Canvas.CENTER), " CLASS=" , modifiedStyle,
                " ONMOUSEOVER='" , this.getID() , ".cellButtonOver(this);return false;' ",
                " ONMOUSEOUT='" , this.getID() , ".cellButtonOut(this);return true;'",
                " basestyle='", style, "'",
                (selected ? " buttonselected='true'" : null),
                (disabled ? " buttondisabled='true'" : null),
                (extraTagStuff ? " " + extraTagStuff : null),
                eventHTML + ">",
                contents,
            "</TD>"
        );
    }
});




// This file creates a mini-calendar that is used to pick a date, for example, you might have a
// button next to a form date field that brings this file up.




//>    @class    DateGrid
//
// A ListGrid subclass that manages calendar views.
//
// @treeLocation Client Reference/Forms
// @visibility external
//<
if (isc.ListGrid == null) {
    isc.Log.logInfo("Source for DateGrid included in this module, but required " +
        "superclass (ListGrid) is not loaded. This can occur if the Forms module is " +
        "loaded without the Grids module. DateGrid class will not be defined within " +
        "this page.", "moduleDependencies");
} else {

// create a customized ListGrid to show the days in a month
isc.ClassFactory.defineClass("DateGrid", "ListGrid");

isc.DateGrid.addProperties({
    width: 10,
    height: 10,
    cellHeight: 20,
    autoFitData: "vertical",
    minFieldWidth: 20,
    autoFitMaxRows: 5,
    useCellRollOvers: true,
    canSelectCells: true,
    leaveScrollbarGap: false,
    canResizeFields: false,
    headerButtonProperties: {
        padding: 0
    },
    headerHeight: 20,
    canSort: false,
    canEdit: false,

    showSortArrow: isc.ListGrid.NONE,
    showFiscalYear: false,
    showFiscalWeek: false,
    showCalendarWeek: false,

    loadingDataMessage: "",
    alternateRecordStyles: false,

    showHeaderMenuButton: false,
    showHeaderContextMenu: false,

    cellPadding: 0,

    // we need to locate rows by cell-value, not PK or whatever else
    locateRowsBy: "targetCellValue",

    fiscalYearFieldTitle: "Year",
    weekFieldTitle: "Wk",

    canReorderFields: false,

    bodyProperties: {
        canSelectOnRightMouse: false,
        overflow: "visible"
    },

    headerProperties: {
        overflow: "visible"
    },

    initWidget : function () {
        this.shortDayNames = isc.Date.getShortDayNames(3);
        this.shortDayTitles = isc.Date.getShortDayNames(this.dayNameLength);
        this.shortMonthNames = isc.Date.getShortMonthNames();

        this.Super("initWidget", arguments);

        this.refreshUI();
    },

    getTitleField : function () {
        return null;
    },

    getCellAlign : function (record, rowNum, colNum) {
        return "center";
    },

    formatCellValue : function (value, record, rowNum, colNm) {
        if (value && value.getDate) return value.getDate();
        return "" + value;
    },

    getCellStyle : function (record, rowNum, colNum) {
        var field = this.getField(colNum),
            weekNum = this.getRecordWeekNumber(record),
            selected = weekNum == this.selectedWeek
        ;

        if (field.name == "fiscalYear") {
            return !selected ? this.baseFiscalYearStyle : this.selectedWeekStyle;
        } else if (field.name == "fiscalWeek" || field.name == "calendarWeek") {
            return !selected ? this.baseWeekStyle : this.selectedWeekStyle;
        }

        var date = this.getCellDate(record, rowNum, colNum),
            isDisabled = this.dateIsDisabled(date),
            isOtherMonth = date.getMonth() != this.workingMonth,
            style = this.Super("getCellStyle", arguments);
        ;

        if (field.isDateField) {
            if ((isDisabled || isOtherMonth)) {

                style = field.isWeekend ? this.disabledWeekendStyle : this.disabledWeekdayStyle;

                var eventRow = this.body.getEventRow(),
                    eventCol = this.body.getEventColumn(),
                    isOver = (eventRow == rowNum && eventCol == colNum),
                    lastSel = this.selection && this.selection.lastSelectedCell,
                    isSelected = lastSel ? lastSel[0] == rowNum && lastSel[1] == colNum :
                                    this.cellSelection ?
                                    this.cellSelection.isSelected(rowNum, colNum) : false,
                    overIndex = style.indexOf("Over"),
                    selectedIndex = style.indexOf("Selected")
                ;

                if (overIndex >= 0) style = style.substring(0, overIndex);
                if (selectedIndex >= 0) style = style.substring(0, selectedIndex);

                if (isSelected) style += "Selected";
                if (isOver) style += "Over";
            }
        }

        return style;

    },

    cellMouseDown : function (record, rowNum, colNum) {
        var date = this.getCellDate(record, rowNum, colNum);
        if (!date) return true;
        if (this.dateIsDisabled(date)) return false;
        return true;
    },

    cellClick : function (record, rowNum, colNum) {
        var date = this.getCellDate(record, rowNum, colNum);
        if (!date) return true;

        if (this.dateIsDisabled(date)) {
            return true;
        }

        this.dateClick(date.getFullYear(), date.getMonth(), date.getDate());
    },
    dateClick : function (year, month, date) {},

    getRecordWeekNumber : function (record) {
        if (!record) return -1;
        return this.showFiscalWeek ? record.fiscalWeek : record.calendarWeek;
    },

    isSelectedWeek : function (record) {
        return this.getRecordWeekNumber(record) == this.selectedWeek;
    },

    cellSelectionChanged : function (cellList) {
        var sel = this.getCellSelection();
        for (var i=0; i<cellList.length; i++) {
            var cell = cellList[i];
            if (sel.cellIsSelected(cell[0], cell[1])) {
                var weekNum = this.getRecordWeekNumber(this.getRecord(cell[0]));
                if (this.selectedWeek != weekNum) {
                    this.setSelectedWeek(weekNum);
                }
                return;
            }
        }
        return;
    },

    setSelectedWeek : function (weekNum) {
        this.selectedWeek = weekNum;
        this.markForRedraw();
        this.selectedWeekChanged(this.selectedWeek);
    },
    selectedWeekChanged : function (weekNum) {},

    getWorkingMonth : function () {
        return this.workingMonth;
    },
    getSelectedDate : function () {
        return null;
    },

    disableMarkedDates : function () {
        this.disabledDateStrings = [];
        if (this.disabledDates && this.disabledDates.length > 0) {
            for (var i=0; i<this.disabledDates.length; i++) {
                this.disabledDateStrings[i] = this.disabledDates[i].toShortDate();
            }
        }
    },

    dateIsDisabled : function (date) {
        var disabled = date ? this.disabledDateStrings.contains(date.toShortDate()) : false;
        return disabled;
    },

    getCellDate : function (record, rowNum, colNum) {
        if (colNum < this.dateColumnOffset || !this.getField(colNum)) return;
        var rDate = record.rowStartDate,
            date = Date.createLogicalDate(rDate.getFullYear(), rDate.getMonth(),
                rDate.getDate()+(colNum - this.dateColumnOffset))
        ;
        return date;
    },

    selectDateCell : function (date) {
        var selection = this.getCellSelection(),
            cell = this.getDateCell(date)
        ;

        if (!cell) return;

        if (cell.colNum != null) selection.selectSingleCell(cell.rowNum, cell.colNum);
        this.setSelectedWeek(this.showFiscalWeek ? cell.record.fiscalWeek : cell.record.calendarWeek);
    },

    getDateCell : function (date) {
        // returns an object with rowNum, colNum and record
        var selection = this.getCellSelection(),
            data = this.data
        ;

        if (date && data && data.length > 0) {
            var dayCount = this.showWeekends == false ? 5 : 7;
            for (var i=0; i<data.length; i++) {
                var record = data[i];
                if (record) {
                    for (var j=0; j<dayCount; j++) {
                        var dateDay = date.getDay();
                        if (Date.compareLogicalDates(record[this.shortDayNames[date.getDay()]], date) == 0) {
                            var fieldName = this.shortDayNames[date.getDay()],
                                field = this.getField(fieldName),
                                fieldNum = field ? this.getFieldNum(field.name) : null
                            ;
                            if (field) {
                                return { rowNum: i, colNum: fieldNum, record: record };
                            }
                            break;
                        }
                    }
                }
            }
        }
    },

    shouldDisableDate : function (date) {
        var result = this.dateIsDisabled(date);
        return result;
    },

    refreshUI : function (startDate) {
        this.setFields(this.getFieldList());

        startDate = startDate || this.startDate;
        if (startDate) this.setStartDate(startDate);
    },

    setStartDate : function (startDate) {
        var year = startDate.getFullYear(),
            month = startDate.getMonth(),
            date = startDate.getDate(),
            monthStart = Date.createLogicalDate(year, month, 1),
            day = monthStart.getDay()
        ;

        var weekDate = monthStart.duplicate();

        var delta=0;
        if (day > this.firstDayOfWeek) {
            // we need to tweak the start date
            delta = (day-this.firstDayOfWeek) * -1;
        } else if (day < this.firstDayOfWeek) {
            delta = (this.firstDayOfWeek-day)-7;
        }

        var weekStart = Date.createLogicalDate(year, month, 1 + delta, 0);

        //this.logWarn("in setStartDate - original is " + startDate.toShortDate() + "\n\n" +
        //    "year, month, date, monthStart, monthDay, delta ***  final date \n" +
        //    year+", "+month+", "+date+", "+monthStart.toShortDate()+", "+day+", "+delta+" - *** " + weekStart.toShortDate()
        //);

        this.workingMonth = startDate.getMonth();
        this.startDate = weekStart;
        this.buildCalendarData();

        this.markForRedraw();
    },

    getFieldList : function () {
        var fields = [];

        this.dateColumnOffset = 0;
        if (this.showFiscalYear) {
            fields.add({ name: "fiscalYear", type: "number", title: this.fiscalYearFieldTitle, width: 30,
                align: "center", cellAlign: "center", showRollOver: false, showDown: false,
                baseStyle: this.baseFiscalYearStyle,
                headerBaseStyle: this.fiscalYearHeaderStyle || this.baseFiscalYearStyle
            });
            this.dateColumnOffset++;
        }
        if (this.showFiscalWeek) {
            fields.add({ name: "fiscalWeek", type: "number", title: this.weekFieldTitle, width: 25,
                align: "center", showRollOver: false, showDown: false,
                baseStyle: this.baseWeekStyle,
                headerBaseStyle: this.weekHeaderStyle || this.baseWeekStyle
            });
            this.dateColumnOffset++;
        }
        if (this.showCalendarWeek) {
            fields.add({ name: "calendarWeek", type: "number", title: this.weekFieldTitle, width: 25,
                align: "center", showRollOver: false, showDown: false,
                baseStyle: this.baseWeekStyle,
                headerBaseStyle: this.weekHeaderStyle || this.baseWeekStyle
            });
            this.dateColumnOffset++;
        }

        var weekendDays = isc.Date.getWeekendDays();

        for (var i=0; i<this.shortDayNames.length; i++) {
            var dayNumber = i + this.firstDayOfWeek;
            if (dayNumber > 6) dayNumber-=7;
            // don't add fields for weekends if showWeekends is false
            if (!this.showWeekends && weekendDays.contains(dayNumber)) continue;
            var field = {
                name: this.shortDayNames[dayNumber],
                title: this.shortDayTitles[dayNumber],
                type: "text",
                align: "center",
                width: "*",
                padding: 0,
                isDateField: true,
                dateOffset: i,
                showRollOver: false,
                showDown: false
            };
            if (weekendDays.contains(dayNumber)) {
                field.isWeekend = true;
                field.baseStyle = this.baseWeekendStyle;
                field.headerBaseStyle = this.weekendHeaderStyle;
            } else {
                field.baseStyle = this.baseWeekdayStyle;
                field.headerBaseStyle = this.headerBaseStyle;
            }
            fields.add(field);
        }

        this.disableMarkedDates();

        return fields;
    },

    buildCalendarData : function (startDate) {
        if (startDate) this.startDate = startDate;
        startDate = this.startDate;

        var records = [],
            date = startDate,
            startMonth = this.startDate.getMonth(),
            // start date is start of the week - likely in the previous month.
            // We may need to jump up a year:
            // - working month is dec - end date will be start of jan of next year
            // - start date is dec, working month is jan (of next year after start date),
            //   end date is start of feb
            yearWrap = (startMonth == 11 || this.workingMonth == 11),
            sDate2 = Date.createLogicalDate(startDate.getFullYear() + (yearWrap ? 1 : 0),
                            (this.workingMonth == 11 ? 0 : this.workingMonth + 1), 1)
        ;
        var delta = (sDate2.getTime() - date.getTime()) / 1000 / 60 / 60 / 24,
            weeks = delta / 7
        ;

        var counter = Math.floor(weeks) + (delta % 7 > 0 ? 1 : 0);

        for (var i =0; i<=counter; i++) {
            var thisDate = Date.createLogicalDate(date.getFullYear(), date.getMonth(), date.getDate() + (i*7));
            if (i == counter && thisDate.getMonth() != this.workingMonth) {
                break;
            }
            records.add(this.getWeekRecord(thisDate));
        }

        this.setData(records);

        this.selectDateCell(this.getSelectedDate());
    },

    getRowHeight : function () {
        if (this._storedRowHeight == null) {
            var bodyHeight = this.body.getVisibleHeight();
            this._storedRowHeight = Math.floor(Math.max(20, Math.floor(bodyHeight / this.data.length)));
        }
        return this._storedRowHeight;
    },

    getFiscalCalendar : function () {
        return this.fiscalCalendar || Date.getFiscalCalendar();
    },


    getWeekRecord : function (date) {
        var fiscalCalendar = this.getFiscalCalendar(),
            // fiscal year object for start date
            fiscalYear = date.getFiscalYear(fiscalCalendar),
            // end of week date
            endDate = new Date(date.getTime() + (6*86400000));

        if (date.logicalDate) endDate.logicalDate = true;

        // use the fourth day of the week to determine which week-number to display
        var weekDate = new Date(date.getTime() + (4*86400000));

        var record = {
            // first date within the row
            rowStartDate: date,

            // fiscalYear for the start date
            fiscalYear: fiscalYear.fiscalYear,
            // fiscalYear for the end date
            fiscalYearEnd: endDate.getFiscalYear(fiscalCalendar).fiscalYear,

            // fiscal week (for the start date)
            fiscalWeek: date.getFiscalWeek(fiscalCalendar, this.firstDayOfWeek),
            // fiscal week end (for the end date)
            fiscalWeekEnd: endDate.getFiscalWeek(fiscalCalendar, this.firstDayOfWeek),

            // calendar week (for the first day of week)
            calendarWeek: weekDate.getWeek(this.firstDayOfWeek),

            weekDate: weekDate
        };



        // If we hit a fiscal week boundary, or a fiscalYear boundary, show the
        // week / year title in which more days in the week fall.

        if (record.fiscalWeek != record.fiscalWeekEnd) {

            var roundUpYear = false,
                roundUpWeek = false;

            if (record.fiscalYear != record.fiscalYearEnd) {
                var newYearStartDay =  Date.getFiscalStartDate(endDate, fiscalCalendar).getDay(),
                    delta = newYearStartDay - this.firstDayOfWeek;
                if (delta < 0) delta += 6;
                if (delta < 3) roundUpYear = true;
            }

            if (!roundUpYear) {
                var yearStartDay = Date.getFiscalStartDate(date, fiscalCalendar).getDay(),
                    delta = yearStartDay - this.firstDayOfWeek;
                if (delta < 0) delta += 6;
                if (delta > 0 && delta < 3) roundUpWeek = true;
            }

            if (roundUpYear) {
                record.fiscalYear = record.fiscalYearEnd;
                record.fiscalWeek = 1;
            } else if (roundUpWeek) {
                record.fiscalWeek += 1;
            }



        }

        var year = date.getFullYear(),
            month = date.getMonth(),
            weekendDays = Date.getWeekendDays()
        ;
        for (var i=0; i<7; i++) {
            var thisDate = Date.createLogicalDate(year, month, date.getDate() + i, 0);
            //if (this.showWeekends || !weekendDays.contains(thisDate.getDay())) {
                var dayName = this.shortDayNames[thisDate.getDay()];
                record[dayName] = thisDate;
            //}
        }

        return record;
    }
});

} // END of if (isc.ListGrid == null) else case


// This file creates a mini-calendar that is used to pick a date, for example, you might have a
// button next to a form date field that brings this file up.




//>    @class    DateChooser
//
// Simple interactive calendar interface used to pick a date.
// Used by the +link{class:dateItem} class.
//
// @treeLocation Client Reference/Forms
// @visibility external
//<

// create a special canvas to show the days in a month
isc.ClassFactory.defineClass("DateChooser", "VLayout");

isc.DateChooser.addProperties({
    width: 10,
    height: 10,
    overflow: "visible",

    // Header
    // ---------------------------------------------------------------------------------------

    //> @attr dateChooser.navigationLayout (AutoChild HLayout : null : IR)
    // An +link{AutoChild} +link{HLayout}, rendered above the +link{class:DateGrid, date grid},
    // and showing a number of widgets for navigating the DateChooser.  These include buttons
    // for moving to the previous +link{dateChooser.previousYearButton, year} or
    // +link{dateChooser.previousMonthButton, month}, the next
    // +link{dateChooser.nextYearButton, year} or +link{dateChooser.nextMonthButton, month},
    // and for selecting a specific +link{dateChooser.yearChooserButton, year},
    // +link{dateChooser.monthChooserButton, month} or
    // +link{dateChooser.weekChooserButton, week}.
    // @visibility external
    //<
    showNavigationLayout:true,
    navigationLayoutConstructor: "HLayout",
    navigationLayoutDefaults: {
        width: 1,
        height: 1,
        layoutAlign: "center",
        align: "center"
    },

    //> @attr DateChooser.showFiscalYearChooser (Boolean : false : IRW)
    // When set to true, show a button that allows the calendar to be navigated by fiscal year.
    // @visibility external
    //<
    showFiscalYearChooser: false,

    //> @attr dateChooser.fiscalYearChooserButton (AutoChild IButton : null : IR)
    // A button shown in the +link{dateChooser.navigationLayout, navigation layout} which,
    // when clicked, shows a picker for selecting a specific fiscal year.
    // @visibility external
    //<
    fiscalYearChooserButtonDefaults: {
        width: 30,
        click : function () {
            this.creator.showFiscalYearMenu();
        },
        autoParent: "navigationLayout",
        align: "center"
    },

    //> @attr DateChooser.showWeekChooser (Boolean : false : IRW)
    // When set to true, show a button that allows the calendar to be navigated by week or
    // fiscal week, depending on the value of +link{showFiscalYearChooser}.
    //
    // @visibility external
    //<
    showWeekChooser: false,

    //> @attr dateChooser.weekChooserButton (AutoChild IButton : null : IR)
    // A button shown in the +link{dateChooser.navigationLayout, navigation layout} which shows
    // a picker for selecting a specific week of the year.  When +link{showFiscalYearChooser}
    // is true, the week number represents a fiscal week number, one offset from the start of
    // the fiscal year.  Otherwise, it represents a week number offset from the start of the
    // calendar year.
    //
    // @visibility external
    //<
    weekChooserButtonDefaults: {
        width: 25,
        click : function () {
            this.creator.showWeekMenu();
        },
        autoParent: "navigationLayout",
        align: "center"
    },

    //> @attr dateChooser.previousYearButton (AutoChild IButton : null : IR)
    // A button shown in the +link{dateChooser.navigationLayout, navigation layout} that shifts
    // the calendar view backward by a year.
    //
    // @visibility external
    //<
    previousYearButtonDefaults: {
        width: 20,
        click : function () {
            this.creator.showPrevYear();
        },
        autoParent: "navigationLayout",
        align: "center"
    },

    //> @attr dateChooser.previousMonthButton (AutoChild IButton : null : IR)
    // A button shown in the +link{dateChooser.navigationLayout, navigation layout} that shifts
    // the calendar view backward by a month.
    //
    // @visibility external
    //<
    previousMonthButtonDefaults: {
        width: 20,
        click : function () {
            this.creator.showPrevMonth();
        },
        autoParent: "navigationLayout",
        align: "center"
    },

    //> @attr dateChooser.monthChooserButton (AutoChild IButton : null : IR)
    // A button shown in the +link{dateChooser.navigationLayout, navigation layout} that shows
    // a picker for selecting a specific month.
    //
    // @visibility external
    //<
    monthChooserButtonDefaults: {
        width: 30,
        click : function () {
            this.creator.showMonthMenu();
        },
        autoParent: "navigationLayout",
        align: "center"
    },

    //> @attr dateChooser.yearChooserButton (AutoChild IButton : null : IR)
    // A button shown in the +link{dateChooser.navigationLayout, navigation layout} that shows
    // a picker for selecting a specific calendar year.
    //
    // @visibility external
    //<
    yearChooserButtonDefaults: {
        width: 32,
        click : function () {
            this.creator.showYearMenu();
        },
        autoParent: "navigationLayout",
        align: "center"
    },

    //> @attr dateChooser.nextMonthButton (AutoChild IButton : null : IR)
    // A button shown in the +link{dateChooser.navigationLayout, navigation layout} that shifts
    // the calendar view forward by a month.
    //
    // @visibility external
    //<
    nextMonthButtonDefaults: {
        width: 20,
        click : function () {
            this.creator.showNextMonth();
        },
        autoParent: "navigationLayout",
        align: "center"
    },

    //> @attr dateChooser.nextYearButton (AutoChild IButton : null : IR)
    // A button shown in the +link{dateChooser.navigationLayout, navigation layout} that shifts
    // the calendar view forward by a year.
    //
    // @visibility external
    //<
    nextYearButtonDefaults: {
        width: 20,
        click : function () {
            this.creator.showNextYear();
        },
        autoParent: "navigationLayout",
        align: "center"
    },

    //> @attr dateChooser.buttonLayout (AutoChild HLayout : null : IR)
    // An +link{AutoChild} +link{HLayout}, rendered below the +link{class:DateGrid, date grid},
    // and showing the +link{dateChooser.todayButton, Today},
    // +link{dateChooser.cancelButton, Cancel} and, when working with "datetime" values,
    // +link{dateChooser.applyButton, Apply} buttons.
    // @visibility external
    //<
    buttonLayoutConstructor: "HLayout",
    buttonLayoutDefaults: {
        width: 1,
        overflow: "visible",
        layoutAlign: "center"
    },


    //> @attr dateChooser.dateGrid (AutoChild DateGrid : null : IR)
    // A +link{ListGrid} subclass, responsible for rendering the calendar view.
    //
    // @visibility external
    //<
    dateGridDefaults: {
        _constructor: "DateGrid",
        autoDraw: false,
        layoutAlign: "center",
        dateClick : function (year, month, date) {
            this.creator.dateClick(year, month, date);
        },
        getSelectedDate : function () {
            return this.creator.chosenDate;
        },
        selectedWeekChanged : function (weekNum) {
            this.creator.updateWeekChooser(weekNum, true);
        }
    },

    bottomButtonConstructor:"IButton",

    //> @attr dateChooser.todayButton (AutoChild IButton : null : IR)
    // A button shown below the +link{class:DateGrid, calendar grid} which, when clicked,
    // navigates the calendar to today.
    //
    // @visibility external
    //<
    todayButtonDefaults: {
        padding: 2,
        autoFit: true,
        autoParent: "buttonLayout",
        click : function () {
            this.creator.todayClick();
        }
    },

    //> @attr dateChooser.cancelButton (AutoChild IButton : null : IR)
    // A button shown below the +link{class:DateGrid, calendar grid} which, when clicked,
    // closes the DateChooser without selecting a value.
    //
    // @visibility external
    //<
    cancelButtonDefaults: {
        padding: 2,
        autoFit: true,
        autoParent: "buttonLayout",
        click : function () {
            this.creator.cancelClick();
        }
    },

    //> @attr dateChooser.applyButton (AutoChild IButton : null : IR)
    // When a DateChooser is configured for +link{dateChooser.timeItem, a "datetime" value},
    // clicking on a date cell in the +link{dateChooser.dateGrid, grid} will not automatically
    // dismiss the DateChooser canvas.  In this case, use the <code>Apply</code> button to
    // accept the selected date and time and dismiss the chooser.
    //
    // @visibility external
    //<
    applyButtonDefaults: {
        padding: 2,
        autoFit: true,
        autoParent: "buttonLayout",
        click : function () {
            this.creator.applyClick();
        }
    },

    //> @attr DateChooser.headerHeight (Integer : 20 : IR)
    // Height of the header area (containing the navigation buttons) in pixels.
    // @visibility external
    // @deprecated in favor of +link{dateChooser.navigationLayoutHeight}
    //<

    //> @attr DateChooser.navigationLayoutHeight (int : 20 : IR)
    // Height of the +link{dateChooser.navigationLayout, navigation area}, containing the
    // various buttons for navigating the +link{dateChooser.dateGrid, calendar view}.
    // @visibility external
    // @deprecated in favor of +link{dateChooser.navigationLayoutHeight}
    //<
    navigationLayoutHeight:20,


    showYearButtons:true,
    showYearChooser:true,
    showMonthButtons:true,
    showMonthChooser:true,

    //> @attr DateChooser.skinImgDir (string : "images/common/" : IRWA)
    // Overridden directory where images for this widget (such as the month and year button icons)
    // may be found.
    // @visibility external
    //<
    skinImgDir:"images/common/",

    //> @attr DateChooser.prevYearIcon (URL : "[SKIN]doubleArrow_left.gif" : IR)
    // Icon for the previous year button
    // @see attr:DateChooser.showDoubleYearIcon
    // @visibility external
    //<
    prevYearIcon:"[SKIN]doubleArrow_left.gif",

    //> @attr DateChooser.prevYearIconRTL (URL : null : IRW)
    // Icon for the previous year button if +link{isc.Page.isRTL()} is true.
    // If not set, and the page is in RTL mode, the +link{nextYearIcon} will be
    // used in place of the +link{prevYearIcon} and vice versa.
    // @see attr:DateChooser.showDoubleYearIcon
    // @visibility external
    //<

    //> @attr DateChooser.prevYearIconWidth (int : 14 : IR)
    // Width of the icon for the previous year button
    // @visibility external
    //<
    prevYearIconWidth:14,
    //> @attr DateChooser.prevYearIconHeight (int : 7 : IR)
    // Height of the icon for the previous year button
    // @visibility external
    //<
    prevYearIconHeight:7,

    //> @attr DateChooser.prevMonthIcon (URL : "[SKIN]arrow_left.gif" : IR)
    // Icon for the previous month button
    // @visibility external
    //<
    prevMonthIcon:"[SKIN]arrow_left.gif",

    //> @attr DateChooser.prevMonthIconRTL (URL : null : IR)
    // Icon for the previous month button if +link{isc.Page.isRTL()} is true.
    // If not set, and the page is in RTL mode, the +link{nextMonthIcon} will be
    // used in place of the +link{prevMonthIcon} and vice versa.
    // @visibility external
    //<

    //> @attr DateChooser.prevMonthIconWidth (int : 7 : IR)
    // Width of the icon for the previous month button
    // @visibility external
    //<
    prevMonthIconWidth:7,

    //> @attr DateChooser.prevMonthIconHeight (int : 7 : IR)
    // Height of the icon for the previous month button
    // @visibility external
    //<
    prevMonthIconHeight:7,

    //> @attr DateChooser.nextYearIcon (URL : "[SKIN]doubleArrow_right.gif" : IR)
    // Icon for the next year button
    // @see attr:DateChooser.showDoubleYearIcon
    // @visibility external
    //<
    nextYearIcon:"[SKIN]doubleArrow_right.gif",

    //> @attr DateChooser.nextYearIconRTL (URL : null : IR)
    // Icon for the next year button if +link{isc.Page.isRTL()} is true.
    // If not set, and the page is in RTL mode, the +link{nextYearIcon} will be
    // used in place of the +link{prevYearIcon} and vice versa.
    // @see attr:DateChooser.showDoubleYearIcon
    // @visibility external
    //<

    //> @attr DateChooser.nextYearIconWidth (int : 14 : IR)
    // Width of the icon for the next year button
    // @visibility external
    //<
    nextYearIconWidth:14,

    //> @attr DateChooser.nextYearIconHeight (int : 7 : IRW)
    // Height of the icon for the next year button
    // @visibility external
    //<
    nextYearIconHeight:7,

    //> @attr DateChooser.nextMonthIcon (URL : "[SKIN]arrow_right.gif" : IRW)
    // Icon for the next month button
    // @visibility external
    //<
    nextMonthIcon:"[SKIN]arrow_right.gif",

    //> @attr DateChooser.nextMonthIconRTL (URL : null : IRW)
    // Icon for the next month button
    // @visibility external
    //<

    //> @attr DateChooser.nextMonthIconWidth (int : 7 : IRW)
    // Width of the icon for the next month button if +link{isc.Page.isRTL()} is true.
    // If not set, and the page is in RTL mode, the +link{nextMonthIcon} will be
    // used in place of the +link{prevMonthIcon} and vice versa.
    // @visibility external
    //<
    nextMonthIconWidth:7,

    //> @attr DateChooser.nextMonthIconHeight (int : 7 : IRW)
    // Height of the icon for the next month button
    // @visibility external
    //<
    nextMonthIconHeight:7,

    //> @attr DateChooser.showDoubleYearIcon (boolean : true : IRW)
    // If this property is set to true the previous and next year buttons will render out the
    // previous and next month button icons twice rather than using the
    // +link{DateChooser.prevYearIcon} and +link{DateChooser.nextYearIcon}.
    // <P>
    // Set to <code>true</code> by default as not all skins contain media for the year icons.
    // @visibility external
    //<
    // This is really for back-compat (pre 6.1).
    // We intend to set this to true and provide year icon media in all skins we provide from this
    // point forward, but we don't want to break existing customized skins
    showDoubleYearIcon:true,

    // Pop-up Year & Month Pickers
    // ---------------------------------------------------------------------------------------

    //> @attr DateChooser.yearMenuStyle (CSSStyleName : "dateChooserYearMenu" : IR)
    // Style for the pop-up year menu.
    // @visibility external
    //<
    yearMenuStyle:"dateChooserYearMenu",

    //> @attr DateChooser.startYear (int : 1995 : IR)
    // Earliest year that may be selected.
    // @visibility external
    //<
    startYear:1995,

    //> @attr DateChooser.endYear (int : 2020 : IR)
    // Last year that may be selected.
    // @visibility external
    //<
    endYear:2020,

    //> @attr DateChooser.monthMenuStyle (CSSStyleName : "dateChooserMonthMenu" : IR)
    // Style for the pop-up year menu.
    // @visibility external
    //<
    monthMenuStyle:"dateChooserMonthMenu",

    //> @attr DateChooser.weekMenuStyle (CSSStyleName : "dateChooserWeekMenu" : IR)
    // Style for the pop-up week menu.
    // @visibility external
    //<
    weekMenuStyle:"dateChooserWeekMenu",

    // Today / Cancel Buttons
    // ---------------------------------------------------------------------------------------

    //> @attr DateChooser.showTodayButton (Boolean : true : IRW)
    // Determines whether the "Today" button will be displayed, allowing the user to select
    // the current date.
    // @visibility external
    //<
    showTodayButton:true,

    //> @attr DateChooser.showCancelButton (Boolean : false : IRW)
    // Determines whether the "Cancel" button will be displayed.
    // @visibility external
    //<
    showCancelButton:false,

    //> @attr DateChooser.showApplyButton (Boolean : null : IRW)
    // Determines whether the +link{applyButton} will be displayed.
    // @visibility external
    //<

    //> @attr DateChooser.todayButtonTitle  (string:"Today":IRW)
    // Title for "Today" button.
    // @group i18nMessages
    // @visibility external
    //<
    todayButtonTitle:"Today",

    //> @attr DateChooser.cancelButtonTitle  (string:"Cancel":IRW)
    // Title for the cancellation button.
    // @group i18nMessages
    // @visibility external
    //<
    cancelButtonTitle:"Cancel",

    //> @attr DateChooser.applyButtonTitle  (string:"Apply":IRW)
    // Title for the +link{dateChooser.applyButton, Apply} button.
    // @group i18nMessages
    // @visibility external
    //<
    applyButtonTitle:"Apply",

    //> @attr DateChooser.todayButtonHeight  (integer:null:IRW)
    // If set specifies a fixed height for the Today and Cancel buttons.
    // @visibility external
    //<
    //todayButtonHeight:null,

    // Weekends
    // ---------------------------------------------------------------------------------------

    //> @attr DateChooser.disableWeekends (Boolean : false : IR)
    // Whether it should be valid to pick a weekend day.  If set to true, weekend days appear
    // in disabled style and cannot be picked.
    // <P>
    // Which days are considered weekends is controlled by +link{Date.weekendDays}.
    //
    // @visibility external
    //<
    disableWeekends: false,

    //> @attr DateChooser.showWeekends (Boolean : true : IR)
    // Whether weekend days should be shown.  Which days are considered weekends is controlled
    // by +link{Date.weekendDays}.
    //
    // @visibility external
    //<
    showWeekends: true,

    //> @attr DateChooser.firstDayOfWeek  (int : 0 : IR)
    // Day of the week to show in the first column.  0=Sunday, 1=Monday, ..., 6=Saturday.  The
    // default value for this attribute is picked up from the current locale and can also be
    // altered system-wide with the +link{Date.setFirstDayOfWeek, global setter}.
    //
    // @group i18nMessages, appearance
    // @visibility external
    //<

    firstDayOfWeek:0,

    // Initial value
    // ---------------------------------------------------------------------------------------

    year:new Date().getFullYear(),        // full year number
    month:new Date().getMonth(),        // 0-11
    chosenDate:new Date(),    // JS date object -- defaults to today

    // Day Buttons styling
    // ---------------------------------------------------------------------------------------

    //> @attr DateChooser.baseButtonStyle (CSSStyleName : "dateChooserButton" : IRW)
    // Base CSS style applied to this picker's buttons. Will have "Over", "Selected" and "Down"
    // suffix appended as the user interacts with buttons.
    // @visibility external
    //<
    baseButtonStyle:"dateChooserButton",

    //> @attr DateChooser.baseWeekdayStyle (CSSStyleName : "dateChooserWeekday" : IRW)
    // Base CSS style applied to weekdays. Will have "Over", "Selected" and "Down"
    // suffix appended as the user interacts with buttons.  Defaults to +link{baseButtonStyle}.
    // @visibility external
    //<
    baseWeekdayStyle: "dateChooserWeekday",

    //> @attr DateChooser.baseWeekendStyle (CSSStyleName : "dateChooserWeekend" : IRW)
    // Base CSS style applied to weekends. Will have "Over", "Selected" and "Down"
    // suffix appended as the user interacts with buttons.  Defaults to +link{baseWeekdayStyle}.
    // @visibility external
    //<
    baseWeekendStyle: "dateChooserWeekend",

    //> @attr DateChooser.baseFiscalYearStyle (CSSStyleName : "dateChooserFiscalYearCell" : IRW)
    // Base CSS style applied to cells in the +link{showFiscalYearChooser, fiscal year column}.
    // @visibility external
    //<
    baseFiscalYearStyle: "dateChooserFiscalYearCell",

    //> @attr DateChooser.fiscalYearHeaderStyle (CSSStyleName : null : IRW)
    // Base CSS style applied to the header of the
    // +link{showFiscalYearChooser, fiscal year column} in the
    // +link{dateChooser.dateGrid, calendar view}.
    // @visibility external
    //<

    //> @attr DateChooser.baseWeekStyle (CSSStyleName : "dateChooserWeekCell" : IRW)
    // Base CSS style applied to cells in the +link{showWeekChooser, fiscal week column}.
    // @visibility external
    //<
    baseWeekStyle: "dateChooserWeekCell",

    //> @attr DateChooser.weekHeaderStyle (CSSStyleName : null : IRW)
    // Base CSS style applied to the header of the
    // +link{showWeekChooser, fiscal or calendar week column} in the
    // +link{dateChooser.dateGrid, calendar view}.
    // @visibility external
    //<

    //> @attr DateChooser.disabledDates (Array of Date : null : IRW)
    // An array of Date instances that should be disabled if they appear in the calendar view.
    // @visibility external
    //<

    //> @attr DateChooser.disabledWeekdayStyle (CSSStyleName : "dateChooserDisabledWeekday" : IRW)
    // Base CSS style applied to weekday dates which have been +link{disabledDates, disabled}.
    // @visibility external
    //<
    disabledWeekdayStyle: "dateChooserDisabledWeekday",

    //> @attr DateChooser.disabledWeekendStyle (CSSStyleName : "dateChooserDisabledWeekend" : IRW)
    // Base CSS style applied to weekend dates which have been +link{disabledDates, disabled}.
    // @visibility external
    //<
    disabledWeekendStyle: "dateChooserDisabledWeekend",

    //> @attr DateChooser.selectedWeekStyle (CSSStyleName : "dateChooserSelectedWeek" : IRW)
    // CSS style applied to the Fiscal Year and Week columns for the currently selected week
    // (the one being displayed in the +link{dateChooser.showWeekChooser, week chooser}).
    // @visibility external
    //<
    selectedWeekStyle: "dateChooserSelectedWeek",

    //> @attr DateChooser.alternateWeekStyles (boolean:null:IRW)
    // Whether alternating weeks should be drawn in alternating styles. If enabled, the cell style
    // for alternate rows will have +link{alternateStyleSuffix} appended to it.
    // @visibility external
    //<

    //> @attr DateChooser.alternateStyleSuffix (string:"Dark":IRW)
    // The text appended to the style name when using +link{alternateWeekStyles}.
    // @visibility external
    //<
    alternateStyleSuffix:"Dark",

    //> @attr DateChooser.headerStyle (CSSStyleName : "dateChooserButtonDisabled" : IRW)
    // CSS style applied to the day-of-week headers. By default this applies to all days of the
    // week. To apply a separate style to weekend headers, set
    // +link{DateChooser.weekendHeaderStyle}
    //
    // @visibility external
    //<
    headerStyle:"dateChooserButtonDisabled",

    //> @attr DateChooser.weekendHeaderStyle (string:null:IRW)
    // Optional CSS style applied to the day-of-week headers for weekend days. If unset
    // +link{DateChooser.headerStyle} will be applied to both weekdays and weekend days.
    // @visibility external
    //<
    //weekendHeaderStyle:null,

    //> @attr DateChooser.baseNavButtonStyle (CSSStyleName : null : IRW)
    // CSS style to apply to navigation buttons and date display at the top of the
    // component. If null, the CSS style specified in +link{baseButtonStyle} is used.
    // @visibility external
    //<

    //> @attr DateChooser.navButtonConstructor (SCClassName : IButton : IRA)
    // Constructor for navigation buttons at the top of the component.
    // @visibility external
    //<
    navButtonConstructor:isc.IButton,

    //> @attr DateChooser.baseBottomButtonStyle (CSSStyleName : null : IRW)
    // CSS style to apply to the buttons at the bottom of the DateChooser ("Today" and
    // "Cancel").  If null, the CSS style specified in +link{baseButtonStyle} is used.
    // @visibility external
    //<



    useBackMask:true,

    canFocus:true,

    //> @attr DateChooser.useFirstDayOfFiscalWeek (Boolean : true : IRW)
    // When showing the +link{showFiscalYearChooser, fiscal year chooser}, should firstDayOfWeek
    // be defaulted to the same day as the fiscal start date?  If true and a fiscal year
    // starts on a Tuesday, the calendar will display Tuesday to Monday from left to right.
    // @visibility external
    //<
    useFirstDayOfFiscalWeek: true,

    timeFormDefaults: {
        _constructor: "DynamicForm",
        layoutAlign: "center",
        extraSpace: 1
    },

    //> @attr dateChooser.closeOnEscapeKeypress (boolean : false : IR)
    // Should this dateChooser be dismissed if the user presses the Escape key?
    // @visibility external
    //<
    closeOnEscapeKeypress: false,

    //> @attr dateChooser.timeItem (AutoChild TimeItem : null : R)
    // +link{TimeItem} for editing the time portion of dates.  Visible by default for fields
    // of type "datetime" and can be controlled by setting +link{dateChooser.showTimeItem}.
    //
    // @visibility external
    //<

    //> @attr dateChooser.timeItemProperties (TimeItem properties : null : IRA)
    // Custom properties to apply to the +link{dateChooser.timeItem,time field} used
    // for editing the time portion of the date.
    // @visibility external
    //<

    //> @attr DateChooser.showTimeItem  (Boolean : null : IRW)
    // Whether to show the +link{dateChooser.timeItem, time field} for editing the time portion
    // of the date.  When unset, the time field is shown automatically if the field type is
    // "datetime".
    // @visibility external
    //<
    timeItemDefaults: {
        name: "time",
        editorType: "TimeItem",
        useTextField: false,
        showTitle: false
    },

    //> @attr DateChooser.timeItemTitle  (string : "Time" : IRW)
    // Title for the +link{dateChooser.timeItem,time field}.
    // @group i18nMessages
    // @visibility external
    //<
    timeItemTitle: "Time",

    //> @attr DateChooser.use24HourTime (Boolean : true : IRW)
    // When showing the +link{showTimeItem, time field}, whether the
    // +link{class:TimeItem, TimeItem} should be set to use 24-hour time.  The default is true.
    // @visibility external
    //<
    use24HourTime: true,

    //> @attr DateChooser.fiscalYearFieldTitle  (string : "Year" : IRW)
    // Title for the +link{dateChooser.showFiscalYear,fiscal year} field in the date grid.
    // @group i18nMessages
    // @visibility external
    //<
    fiscalYearFieldTitle: "Year",

    //> @attr DateChooser.weekFieldTitle  (string : "Wk" : IRW)
    // Title for the +link{dateChooser.showWeek,week} field in the date grid.
    // @group i18nMessages
    // @visibility external
    //<
    weekFieldTitle: "Wk"

    //> @attr DateChooser.showSecondItem  (Boolean : null : IRW)
    // When showing the +link{dateChooser.timeItem, time field}, whether to show the "second"
    // picker.  When unset, the second field is not shown.
    // @visibility external
    //<

});

//!>Deferred
isc.DateChooser.addMethods({

    initWidget : function () {
        this.Super("initWidget", arguments);

        if (this.showFiscalYearChooser && this.useFirstDayOfFiscalWeek) {
            var fDate = Date.getFiscalStartDate(new Date(), this.getFiscalCalendar());
            this.firstDayOfWeek = fDate.getDay();
        }

        if (this.headerHeight != null) this.navigationLayoutHeight = this.headerHeight;

        if (this.showNavigationLayout != false) {
            this.addAutoChild("navigationLayout", {}, this.navigationLayoutConstructor);
            this.addMember(this.navigationLayout);

            this.addAutoChild("fiscalYearChooserButton", {
                baseStyle:(this.baseNavButtonStyle || this.baseButtonStyle),
                title: this.chosenDate.getFiscalYear(this.getFiscalCalendar()).fiscalYear,
                autoDraw: false
            },
            this.navButtonConstructor);

            this.addAutoChild("weekChooserButton", {
                baseStyle:(this.baseNavButtonStyle || this.baseButtonStyle),
                title: this.showFiscalYearChooser ?
                        this.chosenDate.getFiscalWeek(this.getFiscalCalendar()) :
                        this.chosenDate.getWeek(this.firstDayOfWeek),
                autoDraw: false
            },
            this.navButtonConstructor);

            if (this.showYearButtons) {
                this.addAutoChild("previousYearButton", {
                    baseStyle:(this.baseNavButtonStyle || this.baseButtonStyle),
                    title: this.getPreviousYearIconHTML()
                },
                this.navButtonConstructor);
            }
            if (this.showMonthButtons) {
                this.addAutoChild("previousMonthButton", {
                    baseStyle:(this.baseNavButtonStyle || this.baseButtonStyle),
                    title: this.getPreviousMonthIconHTML()
                },
                this.navButtonConstructor);
            }
            if (this.showMonthChooser != false) {
                this.addAutoChild("monthChooserButton", {
                    baseStyle:(this.baseNavButtonStyle || this.baseButtonStyle),
                    title: this.chosenDate.getShortMonthName()
                },
                this.navButtonConstructor);
            }
            if (this.showYearChooser != false) {
                this.addAutoChild("yearChooserButton", {
                    baseStyle:(this.baseNavButtonStyle || this.baseButtonStyle),
                    title: this.chosenDate.getFullYear()
                },
                this.navButtonConstructor);
            }
            if (this.showMonthButtons) {
                baseStyle:(this.baseNavButtonStyle || this.baseButtonStyle),
                this.addAutoChild("nextMonthButton", {
                    baseStyle:(this.baseNavButtonStyle || this.baseButtonStyle),
                    title: this.getNextMonthIconHTML()
                },
                this.navButtonConstructor);
            }
            if (this.showYearButtons) {
                baseStyle:(this.baseNavButtonStyle || this.baseButtonStyle),
                this.addAutoChild("nextYearButton", {
                    baseStyle:(this.baseNavButtonStyle || this.baseButtonStyle),
                    title: this.getNextYearIconHTML()
                },
                this.navButtonConstructor);
            }

        }

        var gridProps = { startDate: this.chosenDate, dayNameLength: this.dayNameLength,
            showFiscalYear: this.showFiscalYearChooser,
            fiscalYearFieldTitle: this.fiscalYearFieldTitle,
            showFiscalWeek: this.showFiscalYearChooser && this.showWeekChooser,
            showCalendarWeek: !this.showFiscalYearChooser && this.showWeekChooser,
            weekFieldTitle: this.weekFieldTitle,
            disabledDates: this.disabledDates,
            firstDayOfWeek: this.firstDayOfWeek,
            headerBaseStyle: this.headerStyle,
            weekendHeaderStyle: this.weekendHeaderStyle || this.headerStyle,
            baseFiscalYearStyle: this.baseFiscalYearStyle,
            fiscalYearHeaderStyle: this.fiscalYearHeaderStyle || this.baseFiscalYearStyle,
            baseWeekStyle: this.baseWeekStyle,
            weekHeaderStyle: this.weekHeaderStyle || this.baseWeekStyle,
            baseWeekdayStyle: this.baseWeekdayStyle || this.baseButtonStyle,
            baseWeekendStyle: this.baseWeekendStyle || this.baseWeekdayStyle || this.baseButtonStyle,
            alternateRecordStyles: this.alternateWeekStyles,
            disabledWeekdayStyle: this.disabledWeekdayStyle,
            disabledWeekendStyle: this.disabledWeekendStyle,
            selectedWeekStyle: this.selectedWeekStyle,
            fiscalCalendar: this.getFiscalCalendar(),
            showWeekends: this.showWeekends,
            locatorParent: this,
            width: "100%", height: "*"
        };

        this.addAutoChild("dateGrid", gridProps);
        this.addMember(this.dateGrid);

        var item = isc.addProperties({},
                { title: this.timeItemTitle, use24HourTime: this.use24HourTime },
                this.timeItemDefaults,
                this.timeItemProperties,
                { name: "time" }
        );
        this.addAutoChild("timeForm", { items: [item] });
        this.addMember(this.timeForm);
        this.timeForm.hide();

        if (this.showTodayButton || this.showCancelButton) {
            var props = {};
            if (this.todayButtonHeight != null) props.height = this.todayButtonHeight;

            this.addAutoChild("buttonLayout", props, this.buttonLayoutConstructor);
            this.addMember(this.buttonLayout);

            props.baseStyle = this.baseBottomButtonStyle || this.baseButtonStyle;

            props.title = this.todayButtonTitle;
            this.addAutoChild("todayButton", props, this.bottomButtonConstructor);

            props.title = this.cancelButtonTitle;
            this.addAutoChild("cancelButton", props, this.bottomButtonConstructor);

            props.title = this.applyButtonTitle;
            this.addAutoChild("applyButton", props, this.bottomButtonConstructor);
            if (this.applyButton) this.applyButton.hide();
        }
        this.markForRedraw();
        if (this.chosenDate) {
            if (this.showTimeItem) this.chosenTime = isc.Date.getLogicalTimeOnly(this.chosenDate);
            this.chosenDate = isc.Date.getLogicalDateOnly(this.chosenDate);
            this.year = this.chosenDate.getFullYear();
            this.month = this.chosenDate.getMonth();
            this.day = this.chosenDate.getDate();
        }
        this.updateUI();
    },

    getTimeItem : function () {
        if (this.timeForm) return this.timeForm.getItem("time");
    },
    recreateTimeItem : function (value) {
        var item = isc.addProperties({}, { title: this.timeItemTitle,
                    use24HourTime: this.use24HourTime, showSecondItem: !!this.showSecondItem },
                this.timeItemDefaults,
                this.timeItemProperties,
                { name: "time", value: value }
        );
        this.timeForm.setItems([item]);
    },

    resized : function () {
        //if (this.navigationLayout && this.navigationLayout.isDrawn()) this.navigationLayout.redraw();
    },

    handleKeyPress : function () {
        var returnVal = this.Super("handleKeyPress", arguments);
        if (returnVal != false) {
            if ((this.closeOnEscapeKeypress) && ("Escape" == isc.EH.getKey())) {
                this.cancelClick();
            }
        }
    },

    getPreviousYearIconHTML : function () {
        var prevYearIconHTML,
            displayDate = new Date(this.year, this.month, 1),
            disableNextYear = displayDate.getFullYear() == 9999
        ;
        if (this.showDoubleYearIcon) {
            var monthIconHTML = this.getPreviousMonthIconHTML();
            prevYearIconHTML = disableNextYear ? "&nbsp;" :
                   "<NOBR>"+ monthIconHTML + monthIconHTML + "<\/NOBR>";
        } else {
            var icon = this.isRTL() ?
                    this.prevYearIconRTL || this.nextYearIcon : this.prevYearIcon;
            prevYearIconHTML = disableNextYear ? "&nbsp;" :
                        this.imgHTML(icon, this.prevYearIconWidth,
                                         this.prevYearIconHeight);
        }

        return prevYearIconHTML;
    },

    getPreviousMonthIconHTML : function () {
        var icon = this.isRTL() ?
                this.prevMonthIconRTL || this.nextMonthIcon : this.prevMonthIcon,
            monthIconHTML = this.imgHTML(icon, this.prevMonthIconWidth,
                                                 this.prevMonthIconHeight);
        return monthIconHTML;
    },

    getNextMonthIconHTML : function () {
        var icon = this.isRTL() ?
                this.nextMonthIconRTL || this.prevMonthIcon : this.nextMonthIcon,
            monthIconHTML = this.imgHTML(icon, this.nextMonthIconWidth,
                                                 this.nextMonthIconHeight);
        return monthIconHTML;
    },

    getNextYearIconHTML : function () {
        var nextYearIconHTML,
            displayDate = new Date(this.year, this.month, 1),
            disableNextYear = displayDate.getFullYear() == 9999
        ;
        if (this.showDoubleYearIcon) {
            var monthIconHTML = this.getNextMonthIconHTML();
            nextYearIconHTML = disableNextYear ? "&nbsp;" :
                               "<NOBR>"+ monthIconHTML + monthIconHTML + "<\/NOBR>";
        } else {
            var icon = this.isRTL() ?
                    this.nextYearIconRTL || this.prevYearIcon : this.nextYearIcon;
            nextYearIconHTML = disableNextYear ? "&nbsp;" :
                                    this.imgHTML(icon,
                                                 this.nextYearIconWidth,
                                                 this.nextYearIconHeight);
        }

        return nextYearIconHTML;
    },

    // Override show() to show the clickMask if autoClose is true
    // Note: If we're showing this date chooser in a separate window, this is unnecessary, as the
    // user will be unable to click on any part of the window that isn't covered by the date-chooser
    // but will do no harm.
    show : function () {
        var returnVal = this.Super("show", arguments);


        if (this.autoClose) {
            // pass this dateChooser as an unmasked widget to showClickMask because
            // when the dateChooser is shown from a modal window, the dateChooser
            // ends up being masked by its own clickmask for some unknown reason.
            this.showClickMask(this.getID()+".close();", true, this);
            this.bringToFront();
        }
    },

    // picker interface

    //> @method DateChooser.setData()
    // Set the picker to show the given date.
    //
    // @param date (Date) new value
    // @visibility external
    //<
    setData : function (data) {
        if (!isc.isA.Date(data)) data = new Date();

        var type = "datetime";
        if (this.callingFormItem) {
            type = this.callingFormItem.type;
        }

        var dateOnly = Date.getLogicalDateOnly(data),
            timeOnly = Date.getLogicalTimeOnly(data)
        ;

        this.year = dateOnly.getFullYear();
        this.month = dateOnly.getMonth();
        this.day = dateOnly.getDate();

        this.chosenDate = dateOnly;
        this.chosenTime = timeOnly;

        // set the timeItem's value, if it's there
        var timeItem = this.getTimeItem();
        if (timeItem) timeItem.setValue(this.chosenTime);

        this.updateUI();
        this.dateGrid.setStartDate(this.chosenDate);
    },

    updateGridData : function (date) {
        date.setDate(1);

        var fy = Date._getFiscalYearObjectForDate(date),
            fiscalStart = fy.startDate
        ;

        this.dateGrid.showWeekends = this.showWeekends;

        this.dateGrid.showFiscalYear = this.showFiscalYearChooser;
        this.dateGrid.showFiscalWeek = this.showFiscalYearChooser && this.showWeekChooser;
        this.dateGrid.showCalendarWeek = !this.showFiscalYearChooser && this.showWeekChooser;

        if (this.showFiscalYearChooser) {
            if (this.useFirstDayOfFiscalWeek) {
                // if using fiscal startDate.getDay() as firstDayOfWeek, we need to use the
                // fiscalYear in which the startDate exists, not the one in which the start of
                // the month exists
                var nfy = Date.getFiscalYear(fy.fiscalYear + 1);
                if (nfy.year < fy.fiscalYear) nfy = Date.getFiscalYear(nfy.fiscalYear + 1);
                this.dateGrid.firstDayOfWeek = this.firstDayOfWeek = nfy.startDate.getDay();
            }
        }
        this.dateGrid.refreshUI(date);
    },

    //> @method DateChooser.getData()
    // Get the current value of the picker.
    // <P>
    // See +link{dataChanged()} for how to respond to the user picking a date.
    //
    // @return (Date) current date
    // @visibility external
    //<
    getData : function () {
        var date = this.chosenDate.duplicate();
        if (this.showTimeItem) date = isc.Date.combineLogicalDateAndTime(date, this.chosenTime);
        return date;
    },

    redraw : function () {
        this.Super("redraw", arguments);
        this.updateUI();
    },

    //> @attr DateChooser.dayNameLength (number : 2 : IR)
    // How long (how many characters) should be day names be. May be 1, 2 or 3 characters.
    // @visibility external
    //<
    dayNameLength:2,

    getDayNames : function () {
        if (isc.DateChooser._dayNames == null) {
            // Don't hard-code day-names -- we need them to be localizeable
            // isc.DateChooser._dayNames = ["Su", "Mo","Tu", "We", "Th", "Fr", "Sa"]
            // Support 1, 2 or 3 chars
            isc.DateChooser._dayNames = [Date.getShortDayNames(1),Date.getShortDayNames(2),Date.getShortDayNames(3)];
        }
        return isc.DateChooser._dayNames[this.dayNameLength-1];
    },

    getDayCellButtonHTML : function (date, style, state) {
        // null date == Special case for dates beyond 9999
        // This limit is enforced due to dates greater than 9999 causing a browser crash in IE
        // - also our parsing logic assumes a 4 digit date
        if (date == null)
            return this.getCellButtonHTML("&nbsp;", null, style, false, false, isc.Canvas.CENTER);


        var selected = (this.chosenDate && (Date.compareLogicalDates(date,this.chosenDate) == 0)),
            disabled = (date.getMonth() != this.month);

        var partEvent = "dateFromId",
            id = date.getFullYear() + "_" + date.getMonth() + "_" + date.getDate();

        // check for weekends
        if (this.disableWeekends && Date.getWeekendDays().contains(date.getDay())) {
            disabled = true;
            partEvent = null;
        }
        return this.getCellButtonHTML(date.getDate(), style, selected, disabled,
                                      isc.Canvas.CENTER, null, partEvent, id);
    },

    dateIsSelected : function (date) {
        return null
    },

    showPrevMonth : function () {
        if (--this.month == -1) {
            this.month = 11;
            this.year--;
        }
        this.updateUI();
    },

    showNextMonth : function () {
        if (++this.month == 12) {
            this.month = 0;
            this.year++;
        }
        this.updateUI();
    },

    updateHeader : function () {
        if (!this.showNavigationLayout && this.navigationLayout) {
            this.navigationLayout.hide();
        } else if (this.showNavigationLayout) {
            this.navigationLayout.show();

            var members = this.navigationLayout.members;
            if (this.weekChooserButton) {
            if (this.showWeekChooser && !members.contains(this.weekChooserButton)) {
                    this.navigationLayout.addMember(this.weekChooserButton, 0);
                    this.weekChooserButton.show();
            } else if (!this.showWeekChooser && members.contains(this.weekChooserButton)) {
                    this.navigationLayout.removeMember(this.weekChooserButton);
                    this.weekChooserButton.clear();
            }
            }
            if (this.fiscalYearChooserButton) {
            if (this.showFiscalYearChooser && !members.contains(this.fiscalYearChooserButton)) {
                    this.navigationLayout.addMember(this.fiscalYearChooserButton, 0);
                    this.fiscalYearChooserButton.show();
            } else if (!this.showFiscalYearChooser && members.contains(this.fiscalYearChooserButton)) {
                    this.navigationLayout.removeMember(this.fiscalYearChooserButton);
                    this.fiscalYearChooserButton.clear();
                }
            }
        }
    },
    updateUI : function (weekNum) {
        // update month/year button titles
        var date = new Date(this.year, this.month, this.day);

        if (date.getMonth() > this.month) date = isc.DateUtil.getEndOf(new Date(this.year, this.month, 1), "M", true);

        this.updateHeader();

        this.monthChooserButton.setTitle(date.getShortMonthName());
        this.yearChooserButton.setTitle("" + this.year);
        if (this.fiscalYearChooserButton) {
            this.fiscalYearChooserButton.setTitle("" + date.getFiscalYear(this.getFiscalCalendar()).fiscalYear);
        }
        this.updateWeekChooser(weekNum != null ? weekNum :
                (this.fiscalYearChooserButton ? date.getFiscalWeek(this.getFiscalCalendar()) :
                new Date(date.getTime() + (4*86400000)).getWeek(this.firstDayOfWeek)));

        var isFirstYear = this.startYear && this.startYear == date.getFullYear(),
            isLastYear = this.endYear && this.endYear == date.getFullYear()
        ;
        this.previousYearButton.setDisabled(isFirstYear);
        this.previousMonthButton.setDisabled(isFirstYear && date.getMonth() == 0);
        this.nextMonthButton.setDisabled(isLastYear && date.getMonth() == 11);
        this.nextYearButton.setDisabled(isLastYear);

        if (!this.showTimeItem && this.timeForm) {
            this.timeForm.hide();
            if (this.applyButton) this.applyButton.hide();
        } else if (this.showTimeItem) {
            this.recreateTimeItem(this.chosenTime);
            this.timeForm.show();
            if (this.applyButton) this.applyButton.show();
        }

        this.updateGridData(date);
    },

    updateWeekChooser : function (weekNum, skipGridUpdate) {
        if (this.weekChooserButton) {
            this.weekChooserButton.setTitle("" + weekNum);
            if (!skipGridUpdate) this.dateGrid.setSelectedWeek(weekNum);
        }
    },

    showMonth : function (monthNum) {
        this.month = monthNum;
        if (this.monthMenu) this.monthMenu.hide();
        this.bringToFront();
        this.updateUI();
    },


    //> @method DateChooser.getFiscalCalendar()
    // Returns the +link{FiscalCalendar} object that will be used by this DateChooser.
    //
    // @return (FiscalCalendar) the fiscal calendar for this chooser, if set, or the global
    //            one otherwise
    // @visibility external
    //<
    getFiscalCalendar : function () {
        return this.fiscalCalendar || Date.getFiscalCalendar();
    },

    //> @method DateChooser.setFiscalCalendar()
    // Sets the +link{FiscalCalendar} object that will be used by this DateChooser.  If unset,
    // the _link{Date.getFiscalCalendar, global fiscal calendar} is used.
    //
    // @param [fiscalCalendar] (FiscalCalendar) the fiscal calendar for this chooser
    // @visibility external
    //<
    setFiscalCalendar : function (fiscalCalendar) {
        this.fiscalCalendar = fiscalCalendar;
    },

    showWeek : function (weekNum) {
        if (this.fiscalYearChooserButton) {
            var displayDate = Date.createLogicalDate(this.year, this.month, this.chosenDate.getDate());
            var cal = this.getFiscalCalendar(),
                fiscalStart = Date.getFiscalStartDate(displayDate),
                date = new Date(fiscalStart.getFullYear(), cal.defaultMonth, cal.defaultDate + (7 * weekNum))
            ;
        } else {
            date = new Date(this.year, 0, 1 + (7 * weekNum));
        }

        this.year = date.getFullYear();
        this.month = date.getMonth();
        if (this.weekMenu) this.weekMenu.hide();
        this.bringToFront();
        this.updateUI(weekNum);
    },

    showMonthMenu : function () {
        if (!this.monthMenu) {
            // create the menu items using the date.getShortMonthName() for internationalization
            var monthItems = [[]],
                date = Date.createLogicalDate(2001,0,1);
            for (var i = 0; i < 12; i++) {
                date.setMonth(i);
                monthItems[monthItems.length-1].add(
                                    {    contents:date.getShortMonthName(),
                                        eventPart: "showMonth",
                                        eventId: i
                                    }
                    );
                if ((i+1)%3 == 0) monthItems.add([]);
            }
            this.monthMenu = isc.MonthChooser.newInstance({
                styleName:this.monthMenuStyle,
                left:this.monthChooserButton.getPageLeft()+5,
                top:this.getPageTop()+this.navigationLayoutHeight,
                width:Math.min(this.getVisibleWidth(), 120),
                height:Math.min(this.getVisibleHeight()-this.navigationLayoutHeight, 80),
                items:monthItems,
                visibility:isc.Canvas.HIDDEN,
                baseButtonStyle:this.baseButtonStyle,
                dateChooser: this
            });
            // (autoDraw is true, so it is drawn, with visibility hidden at this point)
            var left = this.monthChooserButton.getPageLeft() -
                        ((this.monthMenu.getWidth() - this.monthChooserButton.getWidth()) /2);
            this.monthMenu.setPageLeft(Math.max(left, 0));
        } else {
            // L, T, W, H
            var top = this.getPageTop()+this.navigationLayoutHeight,
                width = Math.min(this.getVisibleWidth(), 120),
                height = Math.min(this.getVisibleHeight()-this.navigationLayoutHeight, 80),
                buttonWidth = this.monthChooserButton.getWidth(),
                left = this.monthChooserButton.getPageLeft() - ((width - buttonWidth)/2)
            ;
            this.monthMenu.setPageRect(left, top, width, height);
        }

        // We show the month menu modally.  This means if the user clicks outside it, we
        // will not allow the click to carry on down, so it will hide the month menu (and then
        // dismiss the monthMenu's click mask), but won't fire the click action on the
        // DateChooser's click mask and hide the entire date chooser.
        // As with all modal clickMasks, for us to float the month menu above it, we need the
        // month menu to be a top-level element (which is how it's currently implemented)
        this.monthMenu.showModal();
    },

    showWeekMenu : function () {
        if (!this.weekMenu) {
            // create the menu items using the date.getShortMonthName() for internationalization
            var weekItems = [[]],
                date = Date.createLogicalDate(2001,0,1);
            for (var i = 1; i < 53; i++) {
                weekItems[weekItems.length-1].add(
                                    {    contents:"" + i,
                                        eventPart: "showWeek",
                                        eventId: i
                                    }
                    );
                if ((i)%7 == 0) weekItems.add([]);
            }

            this.weekMenu = isc.WeekChooser.newInstance({
                styleName:this.weekMenuStyle,
                left:this.weekChooserButton.getPageLeft()+5,
                top:this.getPageTop()+this.navigationLayoutHeight,
                width:Math.min(this.getVisibleWidth(), 120),
                height:Math.min(this.getVisibleHeight()-this.navigationLayoutHeight, 80),
                items:weekItems,
                visibility:isc.Canvas.HIDDEN,
                baseButtonStyle:this.baseButtonStyle,
                dateChooser: this
            });
            // (autoDraw is true, so it is drawn, with visibility hidden at this point)
            var left = this.weekChooserButton.getPageLeft() -
                        ((this.weekMenu.getWidth() - this.weekChooserButton.getWidth()) /2);
            this.weekMenu.setPageLeft(Math.max(left, 0));
        } else {
            // L, T, W, H
            var top = this.getPageTop()+this.navigationLayoutHeight,
                width = Math.min(this.getVisibleWidth(), 120),
                height = Math.min(this.getVisibleHeight()-this.navigationLayoutHeight, 80),
                buttonWidth = this.weekChooserButton.getWidth(),
                left = this.weekChooserButton.getPageLeft() - ((width - buttonWidth)/2)
            ;
            this.weekMenu.setPageRect(Math.max(left, 0), top, width, height);
        }

        this.weekMenu.showModal();
    },

    showPrevYear : function () {
        this.year--;
        this.updateUI();
    },

    showNextYear : function () {
        if (this.year < this.endYear) {
            this.year++;
            this.updateUI();
        }
    },

    showYear : function (yearNum) {
        if (yearNum < this.startYear || yearNum > this.endYear) return;
        this.year = yearNum;
        if (this.yearMenu) this.yearMenu.hide();
        this.updateUI();
    },

    showFiscalYear : function (yearNum) {
        var f = Date.getFiscalYear(yearNum, this.getFiscalCalendar());

        this.year = f.year;
        this.month = f.month;
        if (this.yearMenu) this.yearMenu.hide();
        this.updateUI();
    },

    showFiscalYearMenu : function () {
        this.showYearMenu(true);
    },

    showYearMenu : function (fiscal) {
        var component = !fiscal ? this.yearChooserButton : this.fiscalYearChooserButton;

        var yearDiff = (this.endYear-this.startYear),
            colCount = Math.round(yearDiff/10) > 3 ? Math.round(yearDiff/10) : 3;

        var yearItems = [[]];
        for (var i = 0; i <= (this.endYear-this.startYear); i++) {
            var year = i+this.startYear;
            yearItems[yearItems.length-1].add({
                contents: year,
                eventPart: "showYear",
                eventId: year
            });
            if ((i+1)%colCount == 0) yearItems.add([]);
        }

        if (!this.yearMenu) {
            this.yearMenu = isc.YearChooser.newInstance({
                styleName:this.yearMenuStyle,
                top:this.getPageTop()+this.navigationLayoutHeight,
                width:Math.min(this.getVisibleWidth(), (40*colCount)),
                height:Math.min(this.getVisibleHeight()-this.navigationLayoutHeight, 80),
                items:yearItems,
                visibility:isc.Canvas.HIDDEN,
                baseButtonStyle:this.baseButtonStyle,
                dateChooser: this
            });
            // (autoDraw is true, so it is drawn, with visibility hidden at this point)
            //this.yearMenu.setPageLeft(this.getPageLeft() + ((this.width - this.yearMenu.width)/2));
            var left = component.getPageLeft() - ((this.yearMenu.getWidth() - component.getWidth()) /2);
            this.yearMenu.setPageLeft(Math.max(left, 0));

        } else {
            // L, T, W, H
            var top = this.getPageTop()+this.navigationLayoutHeight,
                width = Math.min(this.getVisibleWidth(), (40*colCount)),
                height = Math.min(this.getVisibleHeight()-this.navigationLayoutHeight, 80),
                buttonWidth = component.getWidth(),
                left = component.getPageLeft() - ((width - buttonWidth)/2)
            ;

            this.yearMenu.items = yearItems;
            this.yearMenu.setPageRect(Math.max(left,0), top, width, height);
        }

        var _fiscal = fiscal;
        this.yearMenu.showYearClick = function (element, id) {
            if (_fiscal) this.dateChooser.showFiscalYear(parseInt(id));
            else this.dateChooser.showYear(parseInt(id));
        }

        //XXX it'd be nice to hilite the current year somehow...
        this.yearMenu.showModal();
    },

    dateClick : function (year, month, day, selectNow) {
        var date = this.chosenDate = Date.createLogicalDate(year, month, day);
        // set this.month / this.year - this ensures we actually show the selected
        // date if the user hits the today button while viewing another month

        var yearChanged = this.year != year;
        if (yearChanged) this.year = year;
        if (yearChanged || this.month != month) this.showMonth(month);

        this.month = month;
        this.year = year;
        this.day = day;

        if (this.showTimeItem) {
            // if we're showing the timeItem, update the local logicalTime with it's current value
            this.chosenTime = this.getTimeItem().getValue();
        }

        if (selectNow) this.dateGrid.selectDateCell(date);
        this.dataChanged();

        if (window.dateClickCallback) {
            // if it's a string, normalize it to a function
            if (isc.isA.String(window.dateClickCallback)) {
                window.dateClickCallback = new Function("date",window.dateClickCallback);
            }
            // and call it, passing the date
            window.dateClickCallback(date)
        }

        if (this.autoHide) this.hide();
        if (this.autoClose) this.close();

        return date;
    },

    // Observable dataChanged function (fired from dateClick)

    //> @method DateChooser.dataChanged()
    // Method to override or observe in order to be notified when a user picks a date value.
    // <P>
    // Has no default behavior (so no need to call Super).
    // <P>
    // Use +link{getData()} to get the current date value.
    //
    // @visibility external
    //<
    dataChanged : function () {
    },

    //> @method DateChooser.cancelClick()
    // Fired when the user clicks the cancel button in this date chooser. Default implementation
    // clears the date chooser.
    // @visibility external
    //<

    cancelClick : function () {
        this.close();
    },

    //> @method DateChooser.todayClick()
    // Fired when the user clicks the Today button. Default implementation will select the current
    // date in the date chooser.
    // @visibility external
    //<

    todayClick : function () {
        var date = new Date();
        this.dateClick(date.getFullYear(), date.getMonth(), date.getDate(), true);
    },

    //> @method DateChooser.applyClick()
    // Fired when the user clicks the Apply button. Default implementation will select the current
    // date in the date chooser.
    //<
    applyClick : function () {
        var date = this.chosenDate.duplicate();
        this.dateClick(date.getFullYear(), date.getMonth(), date.getDate(), true);
    },

    //> @method DateChooser.close()
    // Close the DateChooser.
    //<
    close : function () {
        this.hideClickMask();
        if (this.yearMenu && this.yearMenu.isVisible()) this.yearMenu.hide();
        if (this.monthMenu && this.monthMenu.isVisible()) this.monthMenu.hide();
        if (this.isDrawn()) this.clear();
    },

    dateFromIdClick : function (element, id) {
        var parts = id.split("_");
        if (parts.length != 3) return null;

        var year  = parseInt(parts[0]),
            month = parseInt(parts[1]),
            day   = parseInt(parts[2]);

        return this.dateClick(year, month, day);
    }

});
//!<Deferred




// For efficiency we want to re-use a single date-chooser widget in most cases.
// Add a class method for this
isc.DateChooser.addClassMethods({

    // getSharedDateChooser()   Simple method to return a standard date chooser.
    // Used by the DateItem
    getSharedDateChooser : function (properties) {

        if (!this._globalDC) {

            this._globalDC = this.create(properties, {

                _generated:true,
                // When re-using a DateChooser, we're almost certainly displaying it as a
                // floating picker rather than an inline element. Apply the common options for
                // a floating picker
                autoHide:true,
                showCancelButton:true,
                closeOnEscapeKeypress: true

            });

            return this._globalDC;
        }

        isc.addProperties(this._globalDC, properties);
        return this._globalDC;
    }

});

isc.ClassFactory.defineClass("WeekChooser", "ButtonTable");
isc.WeekChooser.addMethods({

    showWeekClick : function (element, id) {
        this.dateChooser.showWeek(parseInt(id));
    }

});

isc.ClassFactory.defineClass("MonthChooser", "ButtonTable");
isc.MonthChooser.addMethods({

    showMonthClick : function (element, id) {
        this.dateChooser.showMonth(parseInt(id));
    }

});

isc.ClassFactory.defineClass("YearChooser", "ButtonTable");
isc.YearChooser.addMethods({

    showYearClick : function (element, id) {
        this.dateChooser.showYear(parseInt(id));
    }

});


/*---------->    isc.Slider.js    <----------*/

//    The Slider class was developed as an instructional/documentation example of
//    creating a new widget class, covering a broad range of ISC client-side
//    framework concepts and conventions.

// Questions: jeff@isomorphic.com





//----------  Description  ----------\\
//> @class Slider
//    The Slider class implements a GUI slider widget allowing the user to select a numeric
//  value from within a range by dragging a visual indicator up and down a track.
//    <p>
//  The slider will generate events as the user interacts with it and changes its value.
//  If slider.sliderTarget is specified, moving the slider thumb generates a custom
//    event named 'sliderMove', sent to the sliderTarget.
//  If a <code>sliderMove</code> handler stringMethod is defined on the target, it will be
//  fired when the slider is moved. The second parameter (available via the variable name
//  <code>eventInfo</code> if the handler is a string) is a pointer back to the slider.
//  <p>
//  The slider will also fire a <code>valueChanged()</code> method whenever its value is
//  changed.  This can be observed or overridden on the Slider instance to perform some action.
//
//  @treeLocation Client Reference/Control
//  @visibility external
//  @example slider
//<

//----------  Create the class  ----------\\
isc.ClassFactory.defineClass("Slider", isc.Canvas);



//----------  Define static properties  ----------\\
isc.Slider.addClassProperties({
    // isc.Slider.DOWN                   down state for the slider thumb
    DOWN:"down",
    // isc.Slider.UP                     up (enabled) state for the slider thumb
    UP:"",
    // isc.Slider.EVENTNAME              name of event sent to sliderTarget when thumb moved
    EVENTNAME:"sliderMove"
});


//----------  Define instance properties  ----------\\
isc.Slider.addProperties({

    //>    @attr    slider.title        (String : "Set Value" : [IRW])
    // Optional display title for the slider.
    //      @see attr:showTitle
    //      @visibility external
    //<
    title:"Set Value",

    //>    @attr    slider.length        (int : 200 : [IRW])
    // Used to set slider height if vertical, slider width if horizontal.
    // Applied to the slider track, not necessarily the entire widget.
    // Overridden by an explicit width/height specification for the widget.
    //      @visibility external
    //<
    length:200,

    //>    @attr    slider.vertical        (Boolean : true : [IRW])
    // Indicates whether this is a vertical or horizontal slider.
    //      @visibility external
    //      @example slider
    //<
    vertical:true,

    //>    @attr    slider.thumbThickWidth        (int : 23 : [IRW])
    // The dimension of the thumb perpendicular to the slider track.
    //      @visibility external
    //<
    thumbThickWidth:23,

    //>    @attr    slider.thumbThinWidth        (int : 17 : [IRW])
    // The dimension of the thumb parallel to the slider track.
    //      @visibility external
    //<
    thumbThinWidth:17,

    //>    @attr    slider.trackWidth        (int : 7 : [IRW])
    // The thickness of the track. This is the width, for a vertical slider, or the height, for
    // a horizontal slider.
    //      @visibility external
    //<
    trackWidth:7,

    //> @attr slider.hThumbStyle (CSSStyleName : null : IR)
    // Optional CSS style for the thumb for a horizontally oriented slider.
    // <P>
    // Will have the suffix "down" added when the mouse is down on the thumb, and "Disabled"
    // added when the slider is disabled.
    //
    // @visibility external
    //<

    //> @attr slider.vThumbStyle (CSSStyleName : null : IR)
    // Optional CSS style for the thumb for a vertically oriented slider.  See
    // +link{hThumbStyle} for state suffixes.
    // @visibility external
    //<

    //> @attr slider.hTrackStyle (CSSStyleName : null : IR)
    // Optional CSS style for the track for a horizontally oriented slider.
    // <P>
    // Will have the suffix "Disabled" added when the slider is disabled.
    //
    // @visibility external
    //<

    //> @attr slider.vTrackStyle (CSSStyleName : null : IR)
    // Optional CSS style for the track for a vertically oriented slider.
    // <P>
    // Will have the suffix "Disabled" added when the slider is disabled.
    // @visibility external
    //<

    // skinImgDir       subdirectory for slider skin images
    skinImgDir:"images/Slider/",

    //>    @attr    slider.thumbSrc        (String : "thumb.gif" : [IRW])
    // The base filename for the slider thumb images.
    // The filenames for the thumb icons are assembled from this base filename and the state of the
    // thumb, as follows:<br>
    // Assume the thumbSrc is set to <code>{baseName}.{extension}</code><br>
    // The full set of images to be displayed is:<br>
    // For horizontal sliders:
    // <ul>
    // <li><code>h{baseName}.{extension}</code>: default enabled appearance.
    // <li><code>h{baseName}_down.{extension}</code>:  appearance when the slider is enabled and the
    //     thumb is clicked.
    // <li><code>h{baseName}_Disabled.{extension}</code>:  appearance when the slider is disabled.
    // </ul>
    // For vertical sliders:
    // <ul>
    // <li><code>v{baseName}.{extension}</code>: default enabled appearance.
    // <li><code>v{baseName}_down.{extension}</code>:  appearance when the slider is enabled and the
    //     thumb is clicked.
    // <li><code>v{baseName}_Disabled.{extension}</code>:  appearance when the slider is disabled.
    // </ul>
    //      @visibility external
    //<
    thumbSrc:"thumb.gif",

    //>    @attr    slider.trackSrc        (String : "track.gif" : [IRW])
    // The base filename for the slider track images.
    // The filenames for the track icons are assembled from this base filename and the state of the
    // slider, as follows:<br>
    // Assume the trackSrc is set to <code>{baseName}.{extension}</code><br>
    // The full set of images to be displayed is:<br>
    // For horizontal sliders:
    // <ul>
    // <li><code>h{baseName}_start.{extension}</code>: start (left edge) of the track for a slider
    //     that is enabled.
    // <li><code>h{baseName}_stretch.{extension}</code>:  the track for an enabled slider; this may
    //     be centered, tiled, or stretched.
    // <li><code>h{baseName}_end.{extension}</code>:  end (right edge) of the track for a slider
    //     that is enabled.
    // <li><code>h{baseName}_Disabled_start.{extension}</code>: start (left edge) of the track for a slider
    //     that is disabled.
    // <li><code>h{baseName}_Disabled_stretch.{extension}</code>:  the track for a disabled slider; this
    //     may be centered, tiled, or stretched.
    // <li><code>h{baseName}_Disabled_end.{extension}</code>:  end (right edge) of the track for a slider
    //     that is disabled.
    // </ul>
    // For vertical sliders:
    // <ul>
    // <li><code>v{baseName}_start.{extension}</code>: start (bottom edge) of the track for a slider
    //     that is enabled.
    // <li><code>v{baseName}_stretch.{extension}</code>:  the track for an enabled slider; this may
    //     be centered, tiled, or stretched.
    // <li><code>v{baseName}_end.{extension}</code>:  end (top edge) of the track for a slider
    //     that is enabled.
    // <li><code>v{baseName}_Disabled_start.{extension}</code>: start (bottom edge) of the track for a slider
    //     that is disabled.
    // <li><code>v{baseName}_Disabled_stretch.{extension}</code>:  the track for a disabled slider; this
    //     may be centered, tiled, or stretched.
    // <li><code>v{baseName}_end.{extension}</code>:  end (top edge) of the track for a slider
    //     that is disabled.
    // </ul>
    //      @see attr:trackImageType
    //      @visibility external
    //<
    trackSrc:"track.gif",

    //>    @attr    slider.trackCapSize        (int : 6 : [IRW])
    // The height of vertical slider start and end images, or width of horizontal slider start and
    // end images.
    //      @visibility external
    //<
    trackCapSize:6,

    //>    @attr    slider.trackImageType        (ImageStyle : "stretch" : [IRW])
    // The imageType setting for the slider track.
    //      @see type:ImageStyle
    //      @see attr:stretchImg.imageType
    //      @visibility external
    //<
    trackImageType:isc.Img.STRETCH,

    //>    @attr    slider.showTitle        (Boolean : true : [IRW])
    // Indicates whether the slider's title should be displayed. The default position for this label
    // is to the left of a horizontal slider, or above a vertical slider.
    //      @see attr:title
    //      @visibility external
    //<
    showTitle:true,

    //>    @attr    slider.showRange        (Boolean : true : [IRW])
    // Indicates whether labels for the min and max values of the slider should be displayed. The
    // default positions for these labels are below the start/end of a horizontal slider, or to the
    // right of the start/end of a vertical slider.
    //      @see attr:minValueLabel
    //      @see attr:maxValueLabel
    //      @visibility external
    //<
    showRange:true,

    //>    @attr    slider.showValue        (Boolean : true : [IRW])
    // Indicates whether a label for the value of the slider should be displayed. The
    // default position for this label is to the right of a vertical slider, or below a horizontal
    // slider.
    //      @see attr:value
    //      @visibility external
    //<
    showValue:true,

    //>    @attr    slider.labelWidth        (int : 50 : [IRW])
    // The width of the labels used to display the minimum, maximum and current values of the
    // slider.
    //      @visibility external
    //<
    labelWidth:50,

    //>    @attr    slider.labelHeight        (int : 20 : [IRW])
    // The height of the labels used to display the minimum, maximum and current values of the
    // slider.
    //      @visibility external
    //<
    labelHeight:20,

    //>    @attr    slider.labelSpacing        (int : 5 : [IRW])
    // The space around the labels used to display the minimum, maximum and current values of the
    // slider.
    //      @visibility external
    //<
    labelSpacing:5,
    titleStyle:"sliderTitle",
    rangeStyle:"sliderRange",
    valueStyle:"sliderValue",
    //XXX need to create and use these CSS styles
    //XXX need mechanism for overriding default layouts


    //>    @attr    slider.value        (float : 1 : [IRW])
    // The slider value. This value should lie between the minValue and maxValue and increases as
    // the thumb is moved up (for a vertical slider) or right (for a horizontal slider) unless
    // flipValues is set to true.
    //      @see attr:minValue
    //      @see attr:maxValue
    //      @see attr:flipValues
    //      @see attr:showValue
    //      @visibility external
    //<
    value:1,

    //>    @attr    slider.minValue        (float : 1 : [IRW])
    // The minimum slider value. The slider value is equal to minValue when the thumb is at the
    // bottom or left of the slider (unless flipValues is true, in which case the minimum value
    // is at the top/right of the slider)
    //      @see attr:slider.flipValues
    //      @visibility external
    //      @example slider
    //<
    minValue:1,

    //>    @attr    slider.minValueLabel        (String : null : [IRW])
    // The text displayed in the label for the minimum value of the slider. If left as null, then
    // slider.minValue will be displayed.
    //      @see attr:showRange
    //      @see attr:minValue
    //      @visibility external
    //<

    //>    @attr    slider.maxValue        (float : 100 : [IRW])
    // The maximum slider value. The slider value is equal to maxValue when the thumb is at the
    // top or right of the slider (unless flipValues is true, in which case the maximum value
    // is at the bottom/left of the slider)
    //      @see attr:slider.flipValues
    //      @visibility external
    //      @example slider
    //<
    maxValue:100,

    //>    @attr    slider.maxValueLabel        (String : null : [IRW])
    // The text displayed in the label for the maximum value of the slider. If left as null, then
    // slider.maxValue will be displayed.
    //      @see attr:showRange
    //      @see attr:maxValue
    //      @visibility external
    //<

    //>    @attr slider.numValues        (integer : null : [IRW])
    // The number of discrete values represented by slider. If specified, the range of valid
    // values (between <code>minValue</code> and <code>maxValue</code>) will be divided into
    // this many steps. As the thumb is moved along the track it will only select these values
    // and appear to jump between the steps.
    //      @visibility external
    //      @example slider
    //<

    //>    @attr slider.roundValues        (Boolean : true : [IRW])
    // Specifies whether the slider value should be rounded to the nearest integer.  If set to
    // false, values will be rounded to a fixed number of decimal places controlled by
    // +link{roundPrecision}.
    //
    //      @visibility external
    //<
    roundValues:true,

    //> @attr slider.roundPrecision (int : 1 : [IRW])
    // If +link{slider.roundValues} is false, the slider value will be rounded to this number of
    // decimal places. If set to null the value will not be rounded
    // @visibility external
    //<
    roundPrecision:1,

    //>    @attr    slider.flipValues        (Boolean : false : [IRW])
    // Specifies whether the value range of the slider should be flipped so that values increase as
    // the thumb is moved down (for a vertical slider) or to the left (for a horizontal slider).
    //      @visibility external
    //<
    flipValues:false,

    //>    @attr    slider.sliderTarget        (Canvas : null : [IRW])
    // The target widget for the <code>sliderMove</code> event generated when the slider thumb
    // is moved.
    //      @visibility external
    //<

    //>    @attr    slider.canFocus        (Boolean : true : [IRW])
    // Indicates whether keyboard manipulation of the slider is allowed.
    //      @visibility external
    //<
    canFocus:true,

    //>    @attr    slider.stepPercent        (float : 5 : [IRW])
    // The percentage of the total slider that constitutes one discrete step. The slider will move
    // one step when the appropriate arrow key is pressed.
    //      @visibility external
    //<
    stepPercent:5,

    //>    @attr    slider.animateThumb        (Boolean : true : [IRW])
    // Should the thumb be animated to its new position when the value is changed programmatically,
    // or by clicking in the slider track.
    //      @visibility animation
    //      @group animation
    //<
    //animateThumb:false,

    //>    @attr    slider.animateThumbTime        (int : 250 : [IRW])
    // Duration of thumb animation, in milliseconds.
    //      @visibility animation
    //      @group animation
    //<
    animateThumbTime:250,

    //>    @attr    slider.animateThumbInit        (Boolean : false : [IRW])
    // If thumb animation is enabled, should the thumb be animated to its initial value?
    //      @visibility animation
    //      @group animation
    //<
    //animateThumbInit:false,

    // undocumented for now; possibly make this internal
    animateThumbAcceleration:"slowStartandEnd",


    valueChangedOnDrag:true,   // default false may be more appropriate, but has backcompat problems
    valueChangedOnRelease:true, // can set this to false to exactly match the pre-5.5 behavior
    valueChangedOnClick:true // actually on mouseUp, but that is too confusable with thumb release
});



//!>Deferred
//----------  Define instance methods  ----------\\
isc.Slider.addMethods({


//------  initWidget()
// Extends superclass initWidget() to set slider dimensions, create the track and thumb child
// widgets, and initialize the slider's target, value, and enabled state.
initWidget : function () {
    this.Super("initWidget", arguments);
    // If passed a minValue that's greater than a max value, swap them.
    // If they are equal just leave them for now - we'll always return that value.
    if (!(this.minValue <= this.maxValue)) {
        this.logWarn("Slider specified with minValue:"+ this.minValue
                    + ", greater than maxValue:"+ this.maxValue
                    + " - reversing max and min value.");
        var minValue = this.minValue;
        this.minValue = this.maxValue;
        this.maxValue = minValue;
    }

    // Enforce rounding precision on min/max values.

    if (this.minValue != null) this.minValue = this._getRoundedValue(this.minValue);
    if (this.maxValue != null) this.maxValue = this._getRoundedValue(this.maxValue);

    this.setUpSize();

    // create track and thumb
    this._createTrackLayout();

    // create title, range, value labels if specified
    if (this.showTitle) this._titleLabel = this.addChild(this._createTitleLabel());
    if (this.showRange) {
        this._minLabel = this.addChild(this._createRangeLabel("min"));
        this._maxLabel = this.addChild(this._createRangeLabel("max"));
    }
    if (this.showValue) {
        this._valueLabel = this._thumb.addPeer(this._createValueLabel());
        this._valueLabel.sendToBack();
        // Ensure the valueLabel is drawn at the correct position.
        this._updateValueLabel();
    }

    // If an event is sent with a null target, the event handling system determines the
    // target based on the last mouse event. We definitely don't want that, so make this
    // slider the target if no target has been specified.


    this.setValue(this.value, !(this.animateThumbInit==true));
},


// setUpSize() - sets up width/height/length (track length)
// If width / height is explicitly specified, determine length from this
// Otherwise determine width/height based on specified length
setUpSize : function () {
    var specifiedWidth = this._userWidth,
        specifiedHeight = this._userHeight;

    // If the user didn't specify a width / height, default them based on which components are
    // being shown.
    if (this.vertical) {
        if (specifiedWidth == null) {

            var width = Math.max(this.thumbThickWidth, this.trackWidth);
            // value shows on one side of the slider, range (min/max labels) show on the
            // other side
            if (this.showValue) width += this.labelWidth + this.labelSpacing;
            if (this.showRange) width += this.labelWidth + this.labelSpacing;

            // Note: titleLabel width is derived from the width of the slider so no need to account
            // for it here

            //>DEBUG
            this.logInfo("defaulting width to " + width + "px");
            //<DEBUG
            this.setWidth(width);
        }
        if (specifiedHeight == null) {
            var height = this.length;

            if (this.showTitle) height += this.labelHeight + this.labelSpacing;

            // if we show the floating value label, it can overflow beyond the
            // end of the track - account for this when sizing the widget so we don't
            // overflow by default
            if (this.showValue && (this.labelHeight > this.thumbThinWidth)) {
                height += (this.labelHeight-this.thumbThinWidth);
            }

            //>DEBUG
            this.logInfo("no specified height on vertical Slider - defaulting to:" + height +
                         " based on slider.length of " + this.length);
            //<DEBUG
            this.setHeight(height);
        } else {
            // if the user specifies both length and height, let height win.
            this.length = this.getHeight();
            if (this.showTitle) this.length -= (this.labelHeight + this.labelSpacing);
            if (this.showValue && (this.labelHeight > this.thumbThinWidth)) {
                this.length -= (this.labelHeight-this.thumbThinWidth);
            }
            //>DEBUG
            this.logInfo("setting slider track length to:"+ this.length
                        + ", based on specified height");
            //<DEBUG
        }
    } else {
        if (specifiedHeight == null) {
            var height = Math.max(this.thumbThickWidth, this.trackWidth);
            if (this.showValue) height += this.labelHeight + this.labelSpacing;
            if (this.showRange) height += this.labelHeight + this.labelSpacing;
            //>DEBUG
            this.logInfo("defaulting height to " + height + "px");
            //<DEBUG
            this.setHeight(height);
        }
        if (specifiedWidth == null) {
            var width = (this.length + (this.showTitle ? this.labelWidth + this.labelSpacing: 0));
            if (this.showValue && (this.labelWidth > this.thumbThinWidth)) {
                width += (this.labelWidth - this.thumbThinWidth);
            }
            //>DEBUG
            this.logInfo("no specified width on horizontal Slider - defaulting to:" + width +
                         " based on slider.length of " + this.length);
            //<DEBUG
            this.setWidth(width);
        } else {

            // if the user specifies both length and width let width win.
            this.length = this.getWidth();
            if (this.showTitle) this.length -= (this.labelWidth + this.labelSpacing);
            // We don't use labelWidth for the valueLabel - we use a smaller value
            // (undocumented 'hValueWidth' on the assumption that the value will
            // overflow if necessary)
            if (this.showValue && (this.hValueLabelWidth > this.thumbThinWidth)) {
                // We use a small label width for the horizontal valueLabel and
                // allow the content to overflow if necessary
                this.length -= (this.hValueLabelWidth - this.thumbThinWidth);
            }
            //>DEBUG
            this.logInfo("setting slider track length to:"+ this.length
                        + ", based on specified width");
            //<DEBUG
        }
    }


    // calculate usable length and step size, in pixels, for use in ongoing calculations

    this._usableLength = this.length-this.thumbThinWidth;
    if (this.numValues && this.numValues > 1) {
        this._stepSize = this._usableLength/(this.numValues-1);
    }

},

// Override resizeBy to resize the track.
// setWidth / setHeight / setRect et al. fall through to this method
resizeBy : function (deltaX, deltaY) {
    this.Super("resizeBy", arguments);
    if (!this._track) return;

    var vertical = this.vertical;

    if ((vertical && deltaY != 0) || (!vertical && deltaX != 0)) {
        // Update length / usable length for caculations...
        this.length += vertical ? deltaY : deltaX;
        this._usableLength = this.length-this.thumbThinWidth;
        // resize the track
        if (vertical) this._track.resizeBy(0, deltaY)
        else this._track.resizeBy(deltaX, 0);

        // re-calculate stepSize if numValues is defined
        if (this.numValues && this.numValues > 1) {
            this._stepSize = this._usableLength/(this.numValues-1);
        }

        // fire setValue to update the thumb.
        this.setValue(this.value, true, true); // no animation, no logical value change
        // Also move the max (or min) marker
        if (this.showRange) {
            if (this.vertical) {
                var endMarker = this.flipValues ? this._maxLabel : this._minLabel;
                endMarker.moveBy(0, deltaY);
            } else {
                var endMarker = this.flipValues ? this._minLabel : this._maxLabel;
                endMarker.moveBy(deltaX, 0);
            }
        }
    }
},

//------  _createRangeLabel(minOrMax)
// Creates, initializes, and returns a new Label widget to be the slider's mix or max value
// label. minOrMax must be the string "min" or "max".
_createRangeLabel : function (minOrMax) {
    var labelLeft, labelTop, labelAlign, labelValign,
        // Should the label be at the start (top / left) or end (bottom/right) of the slider?
        atStartPosition = (this.vertical ? minOrMax == "max" : minOrMax == "min");
     if (this.flipValues) atStartPosition = !atStartPosition;

    // For vertical sliders, range labels appear to the right of the slider track
    // for horizontal sliders, they appear below the slider track.
    if (this.vertical) {
        labelLeft = Math.max(this.thumbThickWidth, this.trackWidth) + this.labelSpacing +
                    (this.showValue ? this.labelWidth + this.labelSpacing : 0);
        labelAlign = isc.Canvas.LEFT;
        if (atStartPosition) {
            labelTop = (this.showTitle ? this.labelHeight + this.labelSpacing : 0);
            labelValign = isc.Canvas.TOP;
        } else {
            labelTop = (this.showTitle ? this.labelHeight + this.labelSpacing: 0)
                        + (this.length - this.labelHeight);
            labelValign = isc.Canvas.BOTTOM;
        }
    } else { // this.horizontal
        labelTop = Math.max(this.thumbThickWidth, this.trackWidth) + this.labelSpacing +
                    (this.showValue ? this.labelHeight + this.labelSpacing : 0);

        labelValign = isc.Canvas.TOP;
        if (atStartPosition) {
            labelLeft = (this.showTitle ? this.labelWidth + this.labelSpacing : 0);
            labelAlign = isc.Canvas.LEFT;
        } else {
            labelLeft = (this.showTitle ? this.labelWidth + this.labelSpacing : 0)
                            + this.length - this.labelWidth;
            labelAlign = isc.Canvas.RIGHT;
        }
    }

    return isc.Label.create({
        ID:this.getID()+"_"+minOrMax+"Label",
        autoDraw:false,
        left:labelLeft,
        top:labelTop,
        width:this.labelWidth,
        height:this.labelHeight,
        wrap:false,
        align:labelAlign,
        valign:labelValign,
        className:this.rangeStyle,
        contents:(minOrMax == "min" ?
            (this.minValueLabel ? this.minValueLabel : this.minValue) :
            (this.maxValueLabel ? this.maxValueLabel : this.maxValue) )

    });
},


//------  _createTitleLabel()
// Creates, initializes, and returns a new Label widget to be the slider's title label.
_createTitleLabel : function () {
    // Title label will always float at 0,0 within the slider.
    var labelAlign = (this.vertical ? isc.Canvas.CENTER : isc.Canvas.RIGHT);

    return isc.Label.create({
        ID:this.getID()+"_titleLabel",
        autoDraw:false,
        left:0,
        top:0,
        width:(this.vertical ? this.getWidth() : this.labelWidth),
        height:(this.vertical ? this.labelHeight : this.getHeight()),
        align:labelAlign,
        className:this.titleStyle,
        contents:this.title
    });
},


//------  _createValueLabel()
// Creates, initializes, and returns a new Label widget to be the slider's dynamic value label.
hValueLabelWidth:5,
_createValueLabel : function () {
    var labelLeft, labelTop, labelWidth, labelAlign, labelValign;

    if (this.vertical) {
        labelLeft = this._thumb.getLeft() - this.labelWidth - this.labelSpacing;
        // align the center of the label with the center of the thumb
        labelTop = this._thumb.getTop()
                    + parseInt(this._thumb.getHeight()/2 - this.labelHeight/2);
        labelAlign = isc.Canvas.RIGHT;
        labelValign = isc.Canvas.CENTER;
        labelWidth = this.labelWidth;
    } else {
        labelLeft = this._thumb.getLeft()
                    + parseInt(this._thumb.getWidth()/2 - this.labelWidth/2);
        labelTop = this._thumb.getTop() - this.labelHeight - this.labelSpacing;
        labelAlign = isc.Canvas.CENTER;
        labelValign = isc.Canvas.BOTTOM;
        // Specify a small size for the label, and allow it's content to
        // overflow.
        labelWidth = this.hValueLabelWidth;
    }

    var label = isc.Label.create({
        ID:this.getID()+"_valueLabel",
        autoDraw:false,
        left:labelLeft,
        top:labelTop,
        width:labelWidth,
        height:this.labelHeight,
        wrap:false,
        align:labelAlign,
        className:this.valueStyle,
        contents:this.value,
        mouseUp:function () { return false; },
        moveWithMaster:false,   // We'll explicitly handle moving the valueLabel
        observes:[{source:this, message:"valueChanged", action:"this._updateValueLabel();"}]
    });

    if (!this.vertical) {
        isc.addMethods(label, {
            // Override draw() to reposition the label after drawing.
            // we have to do this as we don't know the drawn size of the label until it has been
            // drawn in the DOM, and the desired position depends on the drawn size.
            draw : function () {
                var prevVis = this.visibility
                // avoid a flash by drawing with visibility hidden initially
                this.hide();
                this.Super("draw", arguments);
                this.parentElement._updateValueLabel();
                this.setVisibility(this.prevVis);
            }
        });
    };

    return label;
},


//_createTrackLayout()
// Internal function fired once at init time to create the track and thumb for the slider

_createTrackLayout : function () {

    // Determine the rect for the trackLayout.  We will center the thumb and track along the
    // long axis of this rect.
    var layoutRect = this._getTrackLayoutPos(),
        trackLeft, trackTop,
        trackWidth = (this.vertical ? this.trackWidth : this.length),
        trackHeight = (this.vertical ? this.length : this.trackWidth),
        thumbLeft, thumbTop,
        thumbWidth = (this.vertical ? this.thumbThickWidth : this.thumbThinWidth),
        thumbHeight = (this.vertical ? this.thumbThinWidth : this.thumbThickWidth)
    ;


    var thumbThicker = this.thumbThickWidth > this.trackWidth;
    if (thumbThicker) {
        if (this.vertical) {
            thumbLeft = layoutRect[0];
            trackLeft = thumbLeft + parseInt(this.thumbThickWidth/2 - this.trackWidth/2);
            trackTop = layoutRect[1];
            // Doesn't really matter where we put the thumb vertically - it'll be shifted via
            // 'setValue()'
            thumbTop = layoutRect[1];
        } else {
            thumbTop = layoutRect[1];
            trackTop = thumbTop + parseInt(this.thumbThickWidth/2 - this.trackWidth/2);
            trackLeft = layoutRect[0];
            thumbLeft = layoutRect[0];
        }
    // track is thicker than the thumb
    } else {
        if (this.vertical) {
            trackLeft = layoutRect[0];
            thumbLeft = trackLeft + parseInt(this.trackWidth/2 - this.thumbThinWidth/2);
            trackTop = layoutRect[1];
            thumbTop = layoutRect[1];
        } else {
            trackTop = layoutRect[1];
            thumbTop = trackTop + parseInt(this.trackWidth/2 - this.thumbThinWidth/2);
            trackLeft = layoutRect[0];
            thumbLeft = layoutRect[0];
        }
    }

    //>DEBUG
    this.logDebug("calculated coords for track:"+ [trackLeft, trackTop, trackWidth, trackHeight]);
    this.logDebug("calculated coords for thumb:"+ [thumbLeft, thumbTop, thumbWidth, thumbHeight]);
    //<DEBUG

    this._track = this.addChild(this._createTrack(trackTop, trackLeft, trackWidth, trackHeight));
    // Make the thumb a peer of the track. When the track gets moved, so will the thumb
    // (but the thumb can move without moving the track, of course)
    this._thumb = this._track.addPeer(this._createThumb(thumbTop, thumbLeft, thumbWidth, thumbHeight));
},

// _getTrackLayoutPos()
_getTrackLayoutPos : function () {
    // value floats to the left of a vertical slider and above a horizontal one
    // title floats above a vertical slider and to the left of a horizontal one.
    var left = this.vertical ? (this.showValue ? this.labelWidth + this.labelSpacing: 0)
                             : (this.showTitle ? this.labelWidth + this.labelSpacing: 0),
        // title always floats above a slider
        top = this.vertical ? (this.showTitle ? this.labelHeight + this.labelSpacing : 0)
                            : (this.showValue ? this.labelHeight + this.labelSpacing: 0);

    // if the valueLabel can overflow the ends of the track (because it's wider or taller
    // than the thumb), add padding at the start of the track to account for it.
    // (We've already accounted for this difference when determining the track length so no
    // need to also account for this on the end of the track)
    if (this.showValue) {
        if (this.vertical && (this.labelHeight > this.thumbThinWidth))
            top += Math.round((this.labelHeight - this.thumbThinWidth)/2);
        if (this.horizontal && (this.labelWidth > this.thumbThinWidth))
            left += Math.round((this.labelWidth - this.thumbThinWidth)/2);
    }

    return [left, top];
},

//------  _createTrack()
// Creates, initializes, and returns a new StretchImg widget to be the slider's track.

//> @attr slider.track (AutoChild StretchImg : null : IR)
//<
trackConstructor: "StretchImg", // note: RangeSlider.js gets the trackConstructor instance property
trackDefaults: {
    showDisabled: true
},

_createTrack : function (top, left, width, height) {

    return this.createAutoChild("track", {
        left:left,
        top:top,
        width:width,
        height:height,
        vertical:this.vertical,

        // image-based appearance: StretchImg props
        capSize:this.trackCapSize,
        src:"[SKIN]" + (this.vertical ? "v" : "h") + this.trackSrc,
        skinImgDir:this.skinImgDir,
        imageType:this.trackImageType,

        // allows a Label to be used with pure CSS styling
        styleName:this[(this.vertical ? "v" : "h") + "TrackStyle"],
        overflow:"hidden",

        // allow the thumb and the track to have focus, but set exclude them from the tab order
        // this allows for bubbling of keypress events after the user has clicked on the thumb or
        // track of the slider
        canFocus:true,
        tabIndex:-1,
        cacheImageSizes: false
        //backgroundColor:"#666666"    // in case images aren't available
    });
},


//------  _createThumb()
// Creates, initializes, and returns a new Img widget to be the slider's thumb.
thumbConstructor:"Img",
_createThumb : function (top, left, width, height) {
    var vPrefix
    return this.createAutoChild("thumb", {
        left:left,
        top:top,
        width:width,
        height:height,

        // image-based appearance: Img props
        src:"[SKIN]" + (this.vertical ? "v" : "h") + this.thumbSrc,
        skinImgDir:this.skinImgDir,

        // allows a Label to be used with pure CSS styling
        overflow:"hidden",
        showDisabled:true,
        styleName:this[(this.vertical ? "v" : "h") + "ThumbStyle"],

        canDrag:true,
        dragAppearance:isc.EventHandler.NONE,
        cursor:isc.Canvas.HAND,
        dragMove:function () { this.parentElement._thumbMove(); return false },
        // We want the thumb to move with the track, but NOT resize with it.
        _resizeWithMaster:false,


        dragStart:function(){
            var EH = isc.EventHandler;
            EH.dragOffsetX = -1 * (this.getPageLeft() - EH.mouseDownEvent.x);
            EH.dragOffsetY = -1 * (this.getPageTop() - EH.mouseDownEvent.y);
            this.parentElement._thumbIsDragging = true;
            return EH.STOP_BUBBLING;
        },
        dragStop:function(){
            this.parentElement._thumbIsDragging = false;
            this.setState(isc.Slider.UP);
            if (this.parentElement.valueChangedOnRelease) {
                this.parentElement.valueChanged(this.parentElement.value);
            }
            return false;
        },
        mouseDown:function () { this.setState(isc.Slider.DOWN) },
        mouseUp:function () { this.setState(isc.Slider.UP); return false },
        // allow the thumb and the track to have focus, but set exclude them from the tab order
        // this allows for bubbling of keypress events after the user has clicked on the thumb or
        // track of the slider
        canFocus:true,
        tabIndex:-1
        //backgroundColor:"#AAAAAA"    // in case images aren't available
    });
},


//------  _thumbMove()
// Called by the dragMove handler for the slider thumb (this._thumb). Calculates
// the new thumb position, and if the position has changed: moves the thumb widget,
// calculates the new slider value (this.value) and sends the 'sliderMove' event
// to the target (this.sliderTarget).
// The 'fromClick' parameter indicates whether this movement is called from a click
// (eg elsewhere on the track) instead of a drag, in which case we might animate the thumb.
_thumbMove : function (fromClick) {
    var thumbPosition, rawValue;

    if (this.vertical) {
        var trackTop = this._track.getTop(),
            trackEnd = this._usableLength + trackTop;

        // determine the desired position on the track
        thumbPosition = isc.EventHandler.getY() - isc.EventHandler.dragOffsetY - this.getPageTop();
        thumbPosition = Math.max(trackTop, Math.min(trackEnd, thumbPosition));
        // for values calculations we want positions relative to trackTop
        var thumbOffset = thumbPosition - trackTop;
        if (this.numValues) {
            // do not round thumbOffset yet, since it is used to calculate the raw value below
            thumbOffset = Math.round(thumbOffset/this._stepSize) * this._stepSize;
            thumbPosition = Math.round(thumbOffset) + trackTop;
        }
        if (thumbPosition == this._thumb.getTop()) return; // no thumb movement
        //>DEBUG
        this.logDebug("drag-moving thumb to:"+ thumbPosition)
        //<DEBUG
        if (fromClick && this.animateThumb) {
            this._thumbAnimation = this._thumb.animateMove(this._thumb.getLeft(), thumbPosition,
                null, this.animateThumbTime, this.animateThumbAcceleration);
        } else {
            this._thumb.setTop(thumbPosition);
        }

        rawValue = (this.flipValues ? thumbOffset/this._usableLength : 1-thumbOffset/this._usableLength);
    } else {
        var trackLeft = this._track.getLeft(),
            trackEnd = this._usableLength + trackLeft;

        thumbPosition = isc.EventHandler.getX() - isc.EventHandler.dragOffsetX - this.getPageLeft();
        thumbPosition = Math.max(trackLeft, Math.min(trackEnd, thumbPosition));
        var thumbOffset = thumbPosition - trackLeft;
        if (this.numValues) {
            // do not round thumbOffset yet, since it is used to calculate the raw value below
            thumbOffset = Math.round(thumbOffset/this._stepSize) * this._stepSize;
            thumbPosition = Math.round(thumbOffset) + trackLeft;
        }
        if (thumbPosition == this._thumb.getLeft()) return; // no thumb movement
        //>DEBUG
        this.logDebug("drag-moving thumb to:"+ thumbPosition)
        //<DEBUG
        if (fromClick && this.animateThumb) {
            this._thumbAnimation = this._thumb.animateMove(thumbPosition, this._thumb.getTop(),
                null, this.animateThumbTime, this.animateThumbAcceleration);
        } else {
            this._thumb.setLeft(thumbPosition);
        }

        rawValue = (this.flipValues ? 1-thumbOffset/this._usableLength : thumbOffset/this._usableLength);
    }

    if (this.maxValue == this.minValue) {
        this.value = this.minValue;
    } else {
        var finalValue = rawValue*(this.maxValue-this.minValue) + this.minValue
        finalValue = this._getRoundedValue(finalValue);

        this.value = finalValue;

    }
    //>DEBUG
    this.logDebug("slider value from drag-move:"+ this.value);
    //<DEBUG

    // NB: second part of this conditional is required because slider.mouseUp calls slider._thumbMove
    if (this.valueChangedOnDrag || !this._thumbIsDragging) {
        this.valueChanged(this.value);    // observable method
    }

    if (this.sliderTarget) isc.EventHandler.handleEvent(this.sliderTarget, isc.Slider.EVENTNAME, this);
},

_getRoundedValue : function (value) {
    if (this.roundValues) value = Math.round(value);
    else if (this.roundPrecision != null) {
        var multiplier = Math.pow(10, this.roundPrecision);
        value = (Math.round(value * multiplier))/multiplier;
    }
    return value;
},

// _updateValueLabel is called on 'valueChanged' observation when the valueLabel is set up
_updateValueLabel : function () {
    var label = this._valueLabel;
    if (label == null) return;

    label.setContents(this.getValue());

    var thumb = this._thumb;

    if (this.vertical) {
        label.setTop(parseInt((thumb.getTop() + thumb.getHeight()/2) - label.getHeight() / 2));
    } else {
        // Center the label over the thumb, but avoid it overflowing the slider

        if (label.isDrawn()) label.redraw("sizing label");
        var width = label.getVisibleWidth(),
            desiredLeft = parseInt((thumb.getLeft() + thumb.getWidth()/2) - width/2);

        // clamp the label over the available space.
        if (desiredLeft + width > this.getWidth()) {
            desiredLeft = this.getWidth() - width;
            //this.logWarn("width:" + width + ", would overflow so clamping:" + desiredLeft);
        }
        if (desiredLeft < 0) desiredLeft = 0;
        label.setLeft(desiredLeft);
    }
},


mouseUp : function() {

    isc.EventHandler.dragOffsetX = isc.EventHandler.dragOffsetY =
             Math.floor(this.thumbThinWidth/2);
    if (this.valueChangedOnClick) this._thumbMove(true);
},


//------ setValue(newValue)
//> @method slider.setValue()   ([])
// Sets the slider value to newValue and moves the slider thumb to the appropriate position for this
// value. Sends the 'sliderMove' event to the sliderTarget.
//
// @param newValue (float) the new value
// <var class="smartgwt"><b>Note:</b>Use Doubles rather Floats when manipulating decimal
// values.  See +link{groupDef:gwtFloatVsDouble} for details</var>
// @param noAnimation (boolean) do not animate the slider thumb to the new value
// @visibility external
//<
setValue : function (newValue, noAnimation, noValueChange) {
    var rawValue, thumbOffset;
    if (!isc.isA.Number(newValue)) return;

    // Ensure minValue<=newValue<=maxValue.
    newValue = Math.max(this.minValue, (Math.min(newValue, this.maxValue)));

    // Set value, rounding if specified.
    newValue = this._getRoundedValue(newValue);
    this.value = newValue;

    // Calculate rawValue and resulting thumbOffset.
    if (this.minValue == this.maxValue) rawValue = 1;
    else rawValue = (this.value-this.minValue)/(this.maxValue-this.minValue);
    thumbOffset = rawValue * this._usableLength;

    // Set the thumb position.
    var thumbPosition;
    if (this.vertical) {
        thumbPosition = this._track.getTop() +
                        parseInt(this.flipValues ? thumbOffset : this._usableLength - thumbOffset);
        if (this.animateThumb && !noAnimation) {
            this._thumbAnimation = this._thumb.animateMove(this._thumb.getLeft(), thumbPosition,
                null, this.animateThumbTime, this.animateThumbAcceleration);
        } else {
            this._thumb.setTop(thumbPosition);
        }
    } else {
        thumbPosition = this._track.getLeft() +
                        parseInt(this.flipValues ? this._usableLength - thumbOffset : thumbOffset);
        if (this.animateThumb && !noAnimation) {
            this._thumbAnimation = this._thumb.animateMove(thumbPosition, this._thumb.getTop(),
                null, this.animateThumbTime, this.animateThumbAcceleration);
        } else {
            this._thumb.setLeft(thumbPosition);
        }
    }

    if (!noValueChange) this.valueChanged(this.value);    // observable method

    if (this.sliderTarget) isc.EventHandler.handleEvent(this.sliderTarget, isc.Slider.EVENTNAME, this);
},


//------ getValue()
//>    @method    slider.getValue()   ([])
// Returns the current slider value.
//
// @return    (float)    current slider value
// <var class="smartgwt"><b>Note:</b>Use Doubles rather Floats when manipulating decimal
// values.  See +link{groupDef:gwtFloatVsDouble} for details</var>
// @visibility external
//<
getValue : function () {
    return this.value;
},


//------ valueChanged()
//> @method slider.valueChanged()   ([A])
// This method is called when the slider value changes. This occurs when the setValue method is
// called, or when the slider is moved. Observe this method to be notified when the slider value
// changes.
//
// @param value (number) the new value
//
// @see method:class.observe
// @visibility external
// @example slider
//<
valueChanged : function (value) {
},


//> @method slider.valueIsChanging()   ([A])
// Call this method in your +link{slider.valueChanged()} handler to determine whether the
// value change is due to an ongoing drag interaction (true) or due to a thumb-release,
// mouse click, keypress, or programmatic event (false). You may choose to execute temporary or
// partial updates while the slider thumb is dragged, and final updates or persistence of the value
// in response to the other events.
//
// @return  (Boolean)   true if user is still dragging the slider thumb, false otherwise
//
// @visibility external
//<

valueIsChanging : function () {
    return (this._thumbIsDragging == true);
},


// HandleKeyPress:
// If Home, End, or the arrow keys are pressed while this slider has focus, move the slider
// appropriately.
// 20050912: Thumb animation is explicitly disabled by setting the noAnimation parameter of
// setValue(), because the thumb jumps around when one of the arrow keys is held down. Not worth
// tracking down, since the effect is already pretty close to an animation in this case.
handleKeyPress : function (event, eventInfo) {

    var keyName = event.keyName;

    // Note: if this.flipValues is true, vertical sliders will start at the top, and increase
    // toward the bottom, horizontal sliders will start at the right and increase towards the
    // left

    // "Home" will move the slider all the way to the min value (may be either end depending on
    // flipValues)
    if (keyName == "Home") {
        this.setValue(this.minValue, true);
        return false;
    }
    // "End" will move the slider all the way to the max value
    if (keyName == "End") {
        this.setValue(this.maxValue, true);
        return false;
    }

    // If an arrow key was pressed, move the slider one step in the direction indicated

    // Calculate one step from this.stepPercent:
    var change = (this.maxValue - this.minValue) * this.stepPercent / 100;
    // if roundValues is enabled, ensure we always move (a change < 1 could be rounded to no
    // change)
    if (this.roundValues && change < 1) change = 1;

    if (this.vertical) {
        if ((this.flipValues && keyName == "Arrow_Up") ||
            (!this.flipValues && keyName == "Arrow_Down"))
        {
            this.setValue(this.getValue() - change, true);
            return false;

        } else if ( (this.flipValues && keyName == "Arrow_Down") ||
                    (!this.flipValues && keyName == "Arrow_Up"))
        {
            this.setValue(this.getValue() + change, true);
            return false
        }

    } else {
        if ((this.flipValues && keyName == "Arrow_Left") ||
            (!this.flipValues && keyName == "Arrow_Right"))
        {
            this.setValue(this.getValue() + change, true)
            return false;

        } else if ( (this.flipValues && keyName == "Arrow_Right") ||
                    (!this.flipValues && keyName == "Arrow_Left"))
        {
            this.setValue(this.getValue() - change, true)
            return false;
        }
    }

    if (this.keyPress) {
        this.convertToMethod("keyPress");
        return this.keyPress(event, eventInfo);
    }
},

// override setCanFocus to set the canFocus property on the track and the thumb as well
setCanFocus : function (canFocus) {
    this.Super("canFocus", arguments);
    if (this._thumb != null) this._thumb.setCanFocus(canFocus);
    if (this._track != null) this._track.setCanFocus(canFocus);

},

//>    @method    slider.setMinValue()   ([])
// Sets the +link{slider.minValue, minimum value} of the slider
//
// @param newValue (float) the new minimum value
// <var class="smartgwt"><b>Note:</b>Use Doubles rather Floats when manipulating decimal
// values.  See +link{groupDef:gwtFloatVsDouble} for details</var>
// @visibility external
//<
setMinValue : function (newValue) {
    newValue = this._getRoundedValue(newValue);
    this.minValue = newValue;
    if (this._minLabel) this._minLabel.setContents(newValue);
    // only update the current value if it's less than the new minValue
    if (this.getValue() < this.minValue) this.setValue(this.minValue);
},

//>    @method    slider.setMaxValue()   ([])
// Sets the +link{slider.maxValue, maximum value} of the slider
//
// @param newValue (float) the new maximum value
// <var class="smartgwt"><b>Note:</b>Use Doubles rather Floats when manipulating decimal
// values.  See +link{groupDef:gwtFloatVsDouble} for details</var>
// @visibility external
//<
setMaxValue : function (newValue) {
    // If we're rounding, round the min/max value as well

    newValue = this._getRoundedValue(newValue);
    this.maxValue = newValue;
    if (this._maxLabel) this._maxLabel.setContents(newValue);
    // only update the current value if it's larger than the new maxValue
    if (this.getValue() > this.maxValue) this.setValue(this.maxValue);
},

//>    @method    slider.setNumValues()   ([])
// Sets the +link{slider.numValues, number of values} for the slider
//
// @param newNumValues (float) the new number of values
// <var class="smartgwt"><b>Note:</b>Use Doubles rather Floats when manipulating decimal
// values.  See +link{groupDef:gwtFloatVsDouble} for details</var>
// @visibility external
//<
setNumValues : function (newNumValues) {
    this.numValues = newNumValues;
    this._stepSize = this._usableLength/(this.numValues-1);
    this.setValue(this.minValue);
},

//> @method slider.setTitle()
// Sets the +link{title} of the slider
//
// @param newTitle (string) new title for the slider
// @visibility external
//<
setTitle : function (newTitle) {
    this._titleLabel.setContents(newTitle);
},

//> method slider.setLength()
// Sets the +link{length} of the slider
//
// @param newLength (number) the new length to set the slider to
// @visibility external
//<
setLength : function (newLength) {
    this.length = newLength;
    this.setUpSize();
},

_refreshChildren : function () {
    this._titleLabel.destroy();
    this._track.destroy();
    this._thumb.destroy();
    this._valueLabel.destroy();
    this._minLabel.destroy();
    this._maxLabel.destroy();

    this.initWidget();
},

//> @method slider.setVertical()
// Sets the +link{vertical} property of the slider
//
// @param isVertical (boolean) is the slider vertical
// @visibility external
//<
setVertical : function (isVertical) {
    this.vertical = isVertical;
    this._refreshChildren();
},

//> @method slider.setThumbThickWidth()
// Sets the +link{thumbThickWidth} property of the slider
//
// @param newWidth (number) new thumbThickWidth
// @visibility external
//<
setThumbThickWidth : function (newWidth) {
    this.thumbThickWidth = newWidth;
    this._refreshChildren();
},

//> @method slider.setThumbThinWidth()
// Sets the +link{thumbThinWidth} property of the slider
//
// @param newWidth (number) new thumbThinWidth
// @visibility external
//<
setThumbThinWidth : function (newWidth) {
    this.thumbThinWidth = newWidth;
    this._refreshChildren();
},

//> @method slider.setTrackWidth()
// Sets the +link{trackWidth} property of the slider
//
// @param newWidth (number) new trackWidth
// @visibility external
//<
setTrackWidth : function (newWidth) {
    this.trackWidth = newWidth;
    this._refreshChildren();
},

//> @method slider.setThumbSrc()
// Sets the +link{thumbSrc} property of the slider
//
// @param newSrc (string) new thumbSrc
// @visibility external
//<
setThumbSrc : function (newSrc) {
    this.thumbSrc = newSrc;
    this._refreshChildren();
},

//> @method slider.setTrackSrc()
// Sets the +link{trackSrc} property of the slider
//
// @param newSrc (string) new trackSrc
// @visibility external
//<
setTrackSrc : function (newSrc) {
    this.trackSrc = newSrc;
    this._refreshChildren();
},

//> @method slider.setTrackCapSize()
// Sets the +link{trackCapSize} property of the slider
//
// @param newSize (number) new trackCapSize
// @visibility external
//<
setTrackCapSize : function (newSize) {
    this.trackCapSize = newSize;
    this._refreshChildren();
},

//> @method slider.setTrackImageType()
// Sets the +link{trackImageType} property of the slider
//
// @param newType (string) new trackImageType
// @visibility external
//<
setTrackImageType : function (newType) {
    this.trackImageType = newType;
    this._refreshChildren();
},

//> @method slider.setShowTitle()
// Sets the +link{showTitle} property of the slider
//
// @param showTitle (Boolean) show the slider title?
// @visibility external
//<
setShowTitle : function (showTitle) {
    this.showTitle = showTitle;
    this._refreshChildren();
},

//> @method slider.setShowRange()
// Sets the +link{showRange} property of the slider
//
// @param showRange (boolean) show the slider range?
// @visibility external
//<
setShowRange : function (showRange) {
    this.showRange = showRange;
    this._refreshChildren();
},

//> @method slider.setShowValue()
// Sets the +link{showValue} property of the slider
//
// @param showValue (boolean) show the slider value?
// @visibility external
//<
setShowValue : function (showValue) {
    this.showValue = showValue;
    this._refreshChildren();
},

//> @method slider.setLabelWidth()
// Sets the +link{labelWidth} property of the slider
//
// @param labelWidth (number) new label width
// @visibility external
//<
setLabelWidth : function (labelWidth) {
    this.labelWidth = labelWidth;
    this._refreshChildren();
},

//> @method slider.setLabelHeight()
// Sets the +link{labelHeight} property of the slider
//
// @param newHeight (number) new label height
// @visibility external
//<
setLabelHeight : function (newHeight) {
    this.labelHeight = newHeight;
    this._refreshChildren();
},

//> @method slider.setLabelSpacing()
// Sets the +link{labelSpacing} property of the slider
//
// @param labelWidth (number) new label spacing
// @visibility external
//<
setLabelSpacing : function (newSpacing) {
    this.labelSpacing = newSpacing;
    this._refreshChildren();
},

//> @method slider.setMaxValueLabel()
// Sets the +link{maxValueLabel} property of the slider
//
// @param labelText (string) new label text
// @visibility external
//<
setMaxValueLabel : function (labelText) {
    this._maxLabel.setContents(labelText);
},

//> @method slider.setRoundValues()
// Sets the +link{roundValues} property of the slider
//
// @param roundValues (boolean) round slider values?
// @visibility external
//<
setRoundValues : function (roundValues) {
    this.roundValues = roundValues;
    this._refreshChildren();
},

//> @method slider.setRoundPrecision()
// Sets the +link{roundPrecision} property of the slider
//
// @param roundPrecision (number) new round precision
// @visibility external
//<
setRoundPrecision : function (roundPrecision) {
    this.roundPrecision = roundPrecision;
    this._refreshChildren();
},

//> @method slider.setFlipValues()
// Sets the +link{flipValues} property of the slider
//
// @param flipValues (boolean) flip slider values?
// @visibility external
//<
setFlipValues : function (flipValues) {
    this.flipValues = flipValues;
    this._refreshChildren();
},

//> @method slider.setStepPercent()
// Sets the +link{stepPercent} property of the slider
//
// @param stepPercent (number) new slider step percent
// @visibility external
//<
setStepPercent : function (stepPercent) {
    this.stepPercent = stepPercent;
    this._refreshChildren();
}

});


isc.Slider.registerStringMethods({
    valueChanged : "value"
})

//!<Deferred




//> @class RangeSlider
// A "double slider" allowing the user to select a range via two draggable thumbs.
//
//@treeLocation Client Reference/Widgets/RangeSlider
//
// @visibility external
//<

//> @attr rangeSlider.startThumb (AutoChild Snapbar : null : IR)
// Thumb for the start of the range.
//
// @visibility external
//<

//> @attr rangeSlider.endThumb (AutoChild Snapbar : null : IR)
// Thumb for the end of the range
//
// @visibility external
//<

//> @method rangeSlider.changed()
// Notification fired when the selected range is changed by the end user.
//
// @param startValue (float) new start value
// @param endValue (float) new end value
// @param isDragging (boolean) whether the user is still in the middle of a drag, so that
//  expensive operations can be avoided if needed
//
// @visibility external
//<

//> @attr rangeSlider.track (AutoChild Canvas : null : IR)
// Optional track of the RangeSlider.  Set <code>showTrack</code> false to avoid showing
// a track so the RangeSlider can be superimposed over something else.
//
// @visibility external
//<

//> @attr rangeSlider.scrollbar (AutoChild Scrollbar : null : IR)
// Optional Scrollbar shown as a second way of adjusting the range.
//
// @visibility external
//<

isc.defineClass("RangeSlider", isc.Canvas);

isc.RangeSlider.addClassProperties({

    _epsilon: 1e-6
});

isc.RangeSlider.addProperties ({

//> @attr rangeSlider.vertical (boolean : false : IR)
// Whether the rangeSlider should be vertical or horizontal.  Default is horizontal.
//
// @visibility external
//<
    vertical: false,

//> @attr rangeSlider.minValue (float : 0 : IRW)
// Set the minimum value (left/top of slider).
//
// @visibility external
//<
    minValue: 0,

//> @attr rangeSlider.maxValue (float : 0 : IRW)
// Set the maximum value (right/bottom of slider).
//
// @visibility external
//<
    maxValue: 0,

//> @attr rangeSlider.startValue (float : 0 : IRW)
// The beginning of the selected range.
//
// @visibility external
//<
    startValue: 0,

//> @attr rangeSlider.endValue (float : 0 : IRW)
// The end of the selected range.
//
// @visibility external
//<
    endValue:0,

 // @attr rangeSlider.baseStyle (CSSStyleName : "rangeSlider" : IR)
 // Base style name for CSS styles applied to the background of the rangeSlider.  The following
 // suffixes are applied for different areas of the slider:
 // <ul>
 // <li> "Start": area of the slider before the startThumb
 // <li> "Selected": area of the slider between the thumbs (the selected range)
 // <li> "End": area of the slider after the endThumb
 // </ul>
 // .. and the following suffixes are applied in addition according to the slider's dynamic state:
 // <ul>
 // <li> "Over": if the mouse is over the segment
 // <li> "Down": if the mouse is down on the segment
 // </ul>
 // For example, if the mouse is down in the area before the start thumb, that area will have the
 // CSS style "rangeSliderStartDown".
 //
 // @visibility external
 //<
    baseStyle:"rangeSlider",

    overflow:"hidden",
    thumbSize:"7px",

    labelStartDefaults : {
        _constructor:isc.Label,
        wrap:false,
        overflow:"hidden"
    },

    startThumbDefaults : {
        _constructor:isc.Snapbar,
        wrap:false,
        overflow:"hidden",
        canDrag:true,
        keepInParentRect: true,
        canCollapse:false,
        showGrip:true,
        showClosedGrip:false,
        _canDragWhenTargetIsHidden:true
     },

    labelDragDefaults : {
        _constructor:isc.Label,
        overflow:"hidden",
        canDrag:true,
        keepInParentRect: true,
        dragAppearance:"none"
    },

    labelEndDefaults : {
        _constructor:isc.Label,
        overflow:"hidden"
    },

    endThumbDefaults : {
        _constructor:isc.Snapbar,
        canDrag:true,
        overflow:"hidden",
        keepInParentRect: true,
        canCollapse:false,
        showGrip:true,
        _canDragWhenTargetIsHidden:true
     }
});

isc.RangeSlider.addMethods ({

    initWidget: function() {
        this.Super("initWidget", arguments);

        // value sanity checks
        if (this.maxValue < this.minValue) {
            var x = this.minValue;
            this.minValue = this.maxValue;
            this.maxValue = x;
        }

        if (this.startValue<this.minValue) {
            this.startValue = this.minValue;
        }

        if (this.endValue > this.maxValue) {
            this.endValue = this.endValue;
        }

        // lazily initialize track defaults
        if (!this.trackDefaults) {
            isc.RangeSlider.setInstanceProperty("trackDefaults", this.getTrackDefaults());
        }

        // create the controls either vertically or horizontally
        if (this.vertical) {
            this.createControls(true);
        }
        else {
            this.createControls();
        }
    },

    // return the default properties for the track.
    getTrackDefaults : function() {

        return {
            overflow:"hidden",
            showDisabled:true,
            cacheImageSizes: false,
            _constructor:isc.Slider.getInstanceProperty("trackConstructor"),
            capSize:isc.Slider.getInstanceProperty("trackCapSize"),
            skinImgDir:isc.Slider.getInstanceProperty("skinImgDir"),
            imageType:isc.Slider.getInstanceProperty("trackImageType"),
            trackSrc : isc.Slider.getInstanceProperty("trackSrc")
        };
    },

    // overwrite resized so track size and thumb positions are updated on resize
    resized : function() {
        this.Super("resized", arguments);

        if (this.showTrack) {
            if (this.vertical) {
                this.track.setWidth(isc.Slider.getInstanceProperty("trackWidth"));
                this.track.setHeight(this.height);
            } else {
                this.track.setHeight(isc.Slider.getInstanceProperty("trackWidth"));
                this.track.setWidth(this.width);
            }
        }

        this.updatePositions();
    },

    // create the child controls and set up the callback functions
    createControls: function(vertical) {
        var that =  this;
        var remaining;
        var trackWidth = isc.Slider.getInstanceProperty("trackWidth");

        if (vertical) {
            this.scrollbar = this.createAutoChild("scrollbar", {
                _constructor:this.scrollbarConstructor,
                vertical:true,
                height:"100%"
            });

            remaining = this.getWidth() - this.scrollbar.getWidth();

            this.scrollbar.setLeft(remaining);
            this.scrollbar.setTop(0);

            this.labelStart = this.createAutoChild("labelStart", {
                width:remaining,
                baseStyle:this.baseStyle+"Start"
            });

            this.labelDrag = this.createAutoChild("labelDrag", {
                width:remaining,
                baseStyle:this.baseStyle+"Selected"
            });

            this.labelEnd = this.createAutoChild("labelEnd", {
                width:remaining,
                baseStyle:that.baseStyle+"End"
            });

            this.startThumb = this.createAutoChild("startThumb", {
                height:this.thumbSize,
                width:remaining,
                target:this.labelStart,
                vertical:false
            });

            this.endThumb = this.createAutoChild("endThumb", {
                height:this.thumbSize,
                width:remaining,
                target:this.labelEnd,
                vertical:false,

                makeLabel:function() {
                    this.Super("makeLabel", arguments);
                    this.label.addMethods({
                        getCustomState : function () {
                            // don't show closed state if showClosedGrip is set to false
                            if (isc.Snapbar.getInstanceProperty("showClosedGrip")) {
                                return "closed";
                            }
                        }
                    });
                }
            });

            this.track = this.addAutoChild("track", {
                left:Math.round(remaining/2-trackWidth/2),
                width:trackWidth,
                height:this.height,
                vertical:this.vertical,

                src:"[SKIN]"+(this.vertical? "v" : "h")+isc.Slider.getInstanceProperty("trackSrc"),
                styleName:isc.Slider.getInstanceProperty((this.vertical ? "v" : "h") + "TrackStyle")
            });
        } else {
            this.scrollbar = this.addAutoChild("scrollbar", {
                _constructor:this.scrollbarConstructor,
                vertical:false,
                width:"100%"
            });

            remaining = this.getHeight() - this.scrollbar.getHeight();

            this.scrollbar.setLeft(0);
            this.scrollbar.setTop(remaining);


            this.labelStart = this.createAutoChild("labelStart", {
                height:remaining,
                baseStyle:this.baseStyle+"Start"
            });

            this.labelDrag = this.createAutoChild("labelDrag", {
                height:remaining,
                baseStyle:this.baseStyle+"Selected"
            });

            this.labelEnd = this.createAutoChild("labelEnd", {
                height:remaining,
                baseStyle:this.baseStyle+"End"
            });

            this.startThumb = this.createAutoChild("startThumb", {
                width:this.thumbSize,
                height:remaining,
                target:this.labelStart
            });

            this.endThumb = this.createAutoChild("endThumb", {
                width:this.thumbSize,
                height:remaining,
                target:this.labelEnd,

                makeLabel:function() {
                    this.Super("makeLabel", arguments);
                    this.label.addMethods({
                        getCustomState : function () {
                            // don't show closed state if showClosedGrip is set to false
                            if (isc.Snapbar.getInstanceProperty("showClosedGrip")) {
                                return "closed";
                            }
                        }
                    });
                }
            });

            this.track = this.addAutoChild("track", {
                top:Math.round(remaining/2-trackWidth/2),
                height:trackWidth,
                width:this.width,
                vertical:this.vertical,

                src:"[SKIN]"+(this.vertical? "v" : "h")+isc.Slider.getInstanceProperty("trackSrc"),
                styleName:isc.Slider.getInstanceProperty((this.vertical ? "v" : "h") + "TrackStyle")
            });

        };

        this.scrollbar.thumbDragStop = function() {
            this.Super("thumbDragStop",    arguments);

            that.thumbdragging = false;
            that.isDragging = false;

            that.updatePositions();
            that.fireChangedEvent();
        };

        this.scrollbar.thumbDragStart = function() {
            this.Super("thumbDragStart",    arguments);

            that.thumbdragging = true;
            that.isDragging = true;

            that.oldStartValue = that.startValue;
            that.oldEndValue = that.endValue;

            that.dragpoint = this.getEventCoord();
            that.fireChangedEvent();
        };

        this.labelDrag.dragMove = function() {

            // get pixel range and convert it to value range
            var val = that.vertical?that.getValuesForPixels(isc.Event.lastEvent.y-that.dragpoint):
                that.getValuesForPixels(isc.Event.lastEvent.x-that.dragpoint);

            that.setValues(that.oldStartValue+val, that.oldEndValue+val);

            that.fireChangedEvent();

            return true;
        };

        this.labelDrag.dragStart = function() {
            that.oldStartValue = that.startValue;
            that.oldEndValue = that.endValue;

            that.dragpoint = that.vertical?isc.Event.mouseDownEvent.y:isc.Event.mouseDownEvent.x;

            that.isDragging = true;

            that.fireChangedEvent();
        };

        this.labelDrag.dragStop = function() {
            that.isDragging = false;

            that.fireChangedEvent();
        };

        this.startThumb.dragStart = function() {
            that.oldStartValue = that.startValue;
            that.dragpoint = that.vertical?isc.Event.mouseDownEvent.y:isc.Event.mouseDownEvent.x;

            that.isDragging = true;

            that.fireChangedEvent();
        };

        this.startThumb.dragMove = function() {
            // get pixel range and convert it to value range
            var val = that.vertical?that.getValuesForPixels(isc.Event.lastEvent.y-that.dragpoint):
                that.getValuesForPixels(isc.Event.lastEvent.x-that.dragpoint);

            that.setStartValue(that.oldStartValue+val);

            that.fireChangedEvent();

            return true;
        };

        this.startThumb.dragStop = function() {
            that.isDragging = false;
            that.fireChangedEvent();
        };

        this.endThumb.dragStart = function() {
            that.oldEndValue = that.endValue;
            that.dragpoint = that.vertical?isc.Event.mouseDownEvent.y:isc.Event.mouseDownEvent.x;

            that.isDragging = true;
            that.fireChangedEvent();
        };

        this.endThumb.dragMove = function() {
            // get pixel range and convert it to value range
            var val = that.vertical?that.getValuesForPixels(isc.Event.lastEvent.y-that.dragpoint):
                that.getValuesForPixels(isc.Event.lastEvent.x-that.dragpoint);

            that.setEndValue(that.oldEndValue+val);
            that.scrollbar.moveThumb();

            that.fireChangedEvent();
            return true;
        };


        this.endThumb.dragStop = function() {
            that.isDragging = false;

            that.fireChangedEvent();
        };

        if (this.track) {
            this.track.sendToBack();
        }
        this.addChild(this.labelStart);
        this.addChild(this.startThumb);
        this.addChild(this.labelDrag);
        this.addChild(this.endThumb);
        this.addChild(this.labelEnd);
        this.addChild(this.scrollbar);

        this.scrollbar.setScrollTarget(this);

        this.updatePositions();

    },

    // called when mouse-up happens on the control.
    // this is not called when dragging happens
    mouseUp : function() {

        if (this.vertical) {
            var val = this.getOffsetY()-this.startThumb.getHeight();
        } else {
            var val = this.getOffsetX()-this.startThumb.getWidth();
        }

        this.slideSelectedRangeByPoints(val);

        this.fireChangedEvent();
    },

    // slide the selected range to so it's middle point will be at given point
    // values are clamped at  [minValue,maxValue] range
    slideSelectedRangeByPoints : function (pixels) {
        var val = this.getValuesForPixels(pixels);

        // compute it's middle value
        var avg = (this.endValue-this.startValue)/2;

        // simulate dragging so values are clamped instead of ignored if they're
        // outside of bounds
        this.isDragging = true;

        this.setValues(val-avg+this.minValue, val+avg+this.minValue);

        this.isDragging = false;
    },

    // this is called when the user drags the scrollbar
    // scroll the scrollbar to a ratio. If the scrollbar is at either one of its
    // ends, then the entire selected range is slided out - if dragging - or
    // it has both startValue and endValue 0,0 or maxValue, maxValue
    scrollToRatio : function(vertical, ratio, reason) {

        // compute the ratio of how much was thumb dragged vs the track size, since
        // we need to know how much we need to move the selected range towards min or max value
        var dragratio = (this.scrollbar.getEventCoord() - this.dragpoint)/(this.scrollbar.trackSize());
        var val = this.getValueForScrollRatio(dragratio);
        this.setValues(this.oldStartValue+val,this.oldEndValue+val);

        this.fireChangedEvent();
    },

    // scroll by a small amount (20px) when scroll buttons are clicked
    scrollByDelta : function(vertical, direction, reason) {
        var range = this.endValue-this.startValue;

        // compute the value/pixel ratio, without the width/height of the splitter bars
        if (this.vertical) {
            var w = this.getHeight()-this.startThumb.getHeight()-this.endThumb.getHeight();

        } else {
            var w = this.getWidth()-this.startThumb.getWidth()-this.endThumb.getWidth();
        }

        var ratio =  (this.maxValue-this.minValue)/w;

        // compute how much of range is 20 pixels and move the range
        var amount = 20*ratio*direction;

        var start = this.startValue + amount;
        var end = this.endValue + amount;

        // make sure when end is reached, rage is not changed
        if (start<this.minValue) {
            start = this.minValue;
            end = this.minValue + range;
        }

        if (end>this.maxValue) {
            end = this.maxValue;
            start = this.maxValue-range;
        }

        this.isDragging = true;

        this.setValues(start, end);
        this.isDragging = false;
        this.fireChangedEvent();
    },

    // scroll by a viewport (the selected range)
    scrollByPage: function(vertical, direction, reason) {
        // compute the width of viewport and either add it or remove it from
        // the start value and end value
        var amount = Math.max((this.endValue-this.startValue),0)*direction;

        var start = this.startValue + amount;
        var end = this.endValue + amount;

        // make sure when end is reached, rage is not changed
        if (start<this.minValue) {
            start = this.minValue;
            end = this.minValue + Math.abs(amount);
        }

        if (end>this.maxValue) {
            end = this.maxValue;
            start = this.maxValue-Math.abs(amount);
        }

        this.isDragging = true;

        this.setValues(start, end);
        this.isDragging = false;
        this.fireChangedEvent();
    },


    getViewportRatio : function (vertical) {
        var range = this.maxValue - this.minValue,
            selectedRange = 0;
        if (this.thumbdragging) {
            selectedRange = this.oldEndValue - this.oldStartValue;
        } else {
            selectedRange = this.endValue - this.startValue;
        }
        return (Math.abs(range) < isc.RangeSlider._epsilon ? 0 : selectedRange / range);
    },


    getScrollRatio : function (vertical) {
        var range = this.maxValue - this.minValue,
            selectedRange = this.endValue - this.startValue;
        return (
            Math.abs(range - selectedRange) < isc.RangeSlider._epsilon
                ? 0 : (this.startValue - this.minValue) / (range - selectedRange));
    },

    // Convert a scroll ratio to a value.
    getValueForScrollRatio : function(val) {
        return val * (this.maxValue - this.minValue);
    },

    // convert a length in pixels in length in values
    getValuesForPixels : function(val) {

        if (this.vertical) {
            return val*(this.maxValue-this.minValue)/(this.getHeight());
        }
        else {
            return val*(this.maxValue-this.minValue)/(this.getWidth());
        }
    },

    // recompute the position of all components according to the current
    // minValue, maxValue, startValue, endValue
    // this will implicitly resize also the scrollbar in concordance with
    // the ratio of the selected range vs minValue-maxValue range
    updatePositions : function() {

        // compute the pixel/value ratio, without the width/height of the splitter bars
        if (this.vertical) {
            var w = this.getHeight()-this.startThumb.getHeight()-this.endThumb.getHeight();

        } else {
            var w = this.getWidth()-this.startThumb.getWidth()-this.endThumb.getWidth();
        }

        // make sure we have a valid ratio and we don't divide by 0 if this.minValue = this.maxValue
        var ratio = 0;

        if (this.maxValue-this.minValue > 0) {
            ratio =  w/(this.maxValue-this.minValue);
        }

        // compute the width of each segment
        var d1 = Math.round((this.startValue-this.minValue)*ratio);
        var d2 = Math.round((this.endValue-this.startValue)*ratio);
        var d3 = Math.round((this.maxValue-this.endValue)*ratio);
        var sum = Math.round((this.startValue-this.minValue+this.endValue-this.startValue)*ratio);

        if (this.vertical) {

            if (d1 == 0) {
                this.startThumb.target = this.labelDrag;
            } else {
                this.labelStart.show();

                this.labelStart.setTop(0);
                this.labelStart.setHeight(d1);
            }

            if (d2 == 0) {
                this.labelDrag.hide();
            } else {
                this.labelDrag.show();
                this.labelDrag.setHeight(d2);
                this.labelDrag.setTop(d1+this.startThumb.getHeight());
            }

            if (d3 == 0) {
                this.labelEnd.hide();
            } else {
                this.labelEnd.show();
                this.labelEnd.setTop(sum+this.startThumb.getHeight()+this.endThumb.getHeight());
                this.labelEnd.setHeight(d3);
            }

            this.startThumb.setTop(d1);
            this.endThumb.setTop(sum+this.startThumb.getHeight());
        }
        else {

            if (d1 == 0) {
                this.labelStart.hide();
            }
            else {
                this.labelStart.show();

                this.labelStart.setLeft(0);
                this.labelStart.setWidth(d1);
            }

            if (d2 == 0) {
                this.labelDrag.hide();
            }
            else {
                this.labelDrag.show();

                this.labelDrag.setWidth(d2);
                this.labelDrag.setLeft(d1+this.startThumb.getWidth());
            }


            if (d3 == 0) {
                this.labelEnd.hide();
            } else {
                this.labelEnd.show();

                this.labelEnd.setLeft(sum+this.startThumb.getWidth()+this.endThumb.getWidth());
                this.labelEnd.setWidth(d3);
            }

            this.startThumb.setLeft(d1);
            this.endThumb.setLeft(sum+this.startThumb.getWidth());
        }

        this.scrollbar.setThumb();
    },

    // clamp a given value to the allowed [minValue maxValue] range
    clampToMinMax : function (value) {
        if (value<=this.minValue) {
            value = this.minValue;
        }
        if (value >= this.maxValue) {
            value = this.maxValue;
        }

        return value;
    },

    // check if a value is in the allowed [minValue maxValue] range
    isInMinMaxRange : function(value) {
        if (value<this.minValue) {
            return false;
        }
        if (value > this.maxValue) {
            return false;
        }

        return true;
    },

    // set both startValue and endValue of the selected area simultaneously.
    // if change of values occurs while dragging , then we're clamping to
    // [minValue maxValue] range
    // if no dragging occurs, then change is made by user request and we accept
    setValues : function (startv, endv) {

        if (this.isDragging) {
            startv = this.clampToMinMax(startv);
            endv = this.clampToMinMax(endv);

            this.startValue = startv;
            this.endValue = endv;

            this.updatePositions();
        } else {
            if (this.isInMinMaxRange(startv) && this.isInMinMaxRange(endv) &&
                    startv<=endv) {
                this.startValue = startv;
                this.endValue = endv;

                this.updatePositions();
            }

            // otherwise ignore
        }
    },

    // if change of startValue occurs while dragging , then we're clamping to
    // [minValue maxValue] range
    // if no dragging occurs, then change is made by user request and we accept
    // the values only if they're legal
    setStartValue : function (value) {

        if (this.isDragging) {
            value = this.clampToMinMax(value);
            if (value >= this.endValue) {
                value =  this.endValue;
            }
            this.startValue = value;
            this.updatePositions();
        } else {
            if (this.isInMinMaxRange(value) && value <= this.endValue) {
                this.startValue = value;
                this.updatePositions();
            } else {
                isc.logWarn("Ignoring setStartValue to "+value+" (out of range).");
            }
        }
    },

    // if change of endValue occurs while dragging , then we're clamping to
    // [minValue maxValue] range
    // if no dragging occurs, then change is made by user request and we accept
    // the values only if they're legal
    setEndValue : function( value ) {
        if (this.isDragging) {
            value = this.clampToMinMax(value);
            if (value <= this.startValue) {
                value =  this.startValue;
            }
            this.endValue = value;
            this.updatePositions();
        } else {
            if (this.isInMinMaxRange(value) && value >= this.startValue) {
                this.endValue = value;
                this.updatePositions();
            } else {
                isc.logWarn("Ignoring setEndValue to "+value+" (out of range).");
            }
        }
    },

    getStartValue : function() {
        return this.startValue;
    },

    getEndValue : function() {
        return this.endValue;
    },

    setMinValue : function(value) {
        this.minValue = value;
        this.updatePositions();
    },

    setMaxValue : function (value) {
        this.maxValue = value;
        this.updatePositions();
    },

    getMinValue : function() {
        return this.minValue;
    },

    getMaxValue : function() {
        return this.maxValue;
    },

    fireChangedEvent : function() {
        this.changed(this.startValue, this.endValue, this.isDragging);
    },

    changed : function (startValue, endValue, isDragging) {}
});


// Class will not work without the ListGrid
if (isc.ListGrid) {




//>    @class    ScrollingMenu
//
//    Implements a scrollable, user-selectable, menu
//
//  @treeLocation Client Reference/Control
//<


// define us as a subclass of the ListGrid
isc.ClassFactory.defineClass("ScrollingMenu", "ListGrid");

isc.ScrollingMenu.addProperties({


    useBackMask:true,

    // Explicitly default canFocus to true.
    canFocus:true,

    showHeader:false,
    // Avoid showing edges.
    showEdges:false,

    autoDraw:false,

    // don't use the default ListGrid component/body styles, which usually have partial borders
    className:"scrollingMenu",
    bodyStyleName:"scrollingMenuBody",

    selectionType:"single",
    leaveScrollbarGap:false,

    // keyboard - we respond to clicks and Enter keypresses identically.
    // Space has no meaning.
    generateClickOnSpace : false,
    generateDoubleClickOnEnter : false,
    generateClickOnEnter : true,
    // By default show as a modal component.
    showModal:true,

    // arrowKeyAction will select by default

    arrowKeyAction:"select",
    enableSelectOnRowOver: true,

    // default to filter on keypress if we show a filter editor
    filterOnKeypress:true

});

isc.ScrollingMenu.changeDefaults("filterEditorDefaults", {
        // If the filter editor is showing, explicitly give it a solid bg color.
        // This prevents things showing through under the transparent background of
        // the filter button image
        backgroundColor:"white",

        // Override editor keypress -- allow the user to move around and select
        // records as expected
        editorKeyPress : function (item, keyName, characterValue) {
            if (keyName == "Arrow_Down") {
                this.sourceWidget._navigateToNextRecord(1);
                return false;
            }
            if (keyName == "Arrow_Up") {
                this.sourceWidget._navigateToNextRecord(-1);
                return false;
            }
            if (keyName == "Enter") {
                this.sourceWidget._generateFocusRecordClick();
                return;
            }
            return this.Super("editorKeyPress", arguments);
        }
});

isc.ScrollingMenu.addMethods({
    show : function () {

        if (this.showModal) this.showClickMask({target:this, methodName:"cancel"}, false, [this]);

        this.Super("show", arguments);
       if (this.showModal) this.body.focus();
    },

    // override recordClick to fire the 'itemClick' method.
    recordClick : function (viewer,record,recordNum,field,fieldNum,value,rawValue) {
        // hide before firing itemClick.
        // This avoids issues with focus, where the itemClick action is expected to put focus
        // somewhere that is masked until this menu hides.
        this.hide();
        // add support for click handlers on the individual rows
        // make itemClick a stringMethod?
        if (record != null) this.itemClick(record);

    },

    // override this for click handling behavior
    itemClick : function (record) {},

    // On RowOver change selection. The user can then use arrow keys to further modify selection
    // This matches behavior in native select item drop-downs.

    rowOver : function (record,rowNum,colNum) {
        if (this.enableSelectOnRowOver) this.selection.selectSingle(record);
    },
    createSelectionModel : function (a,b,c,d,e) {
        var returnVal = this.invokeSuper("ScrollingMenu", "createSelectionModel", a,b,c,d,e);
        // Override selection so we can tell the difference between selection from rollOver and
        // from keyboard events / clicks.
        // This is required so we can do the right thing on Enter keypress
        this.selection.addProperties({
            selectOnRowOver : function (record) {
                this.selectSingle(record);
                this.selectionFromMouse = true;
            },

            setSelected : function (record, state) {
                this.selectionFromMouse = false;
                return this.Super("setSelected", arguments);
            }
        });

        return returnVal;
   },
    // Keyboard handling:

    // Override bodyKeyPress to handle firing 'cancel()' on escape click.
    bodyKeyPress : function (event, eventInfo) {
        var keyName = event.keyName;

        if (keyName == this._$Enter) {
            var selection = this.selection;
            if (selection && selection.selectionFromMouse) {
                this.cancel();
                return false;
            }
        }
        if (keyName == "Escape") {
            this.cancel();

            // stop bubbling!
            return false;
        }
        return this.Super("bodyKeyPress", arguments);
    },


    cancel : function () {
        this.hide();
    },

    // Override hide to ensure that the clickMask gets hidden too
    hide : function () {
        this.hideClickMask();
        return this.Super("hide", arguments);
    },

    // Always select the first item in the list *IF* nothing is selected

    _selectFirstOnDataChanged:true,
    dataChanged : function () {
        var returnVal = this.Super("dataChanged", arguments);
        if (!this._selectFirstOnDataChanged) return;

        if (this.data && this.data.getLength() > 0 && this.selection && !this.selection.anySelected() &&
            (isc.isA.ResultSet==null || !isc.isA.ResultSet(this.data) || this.data.rowIsLoaded(0)))
        {
            this.selection.selectItem(0);
        }
        return returnVal;
    }

});

}







//>    @class    DynamicForm
//
// The DynamicForm manages a collection of FormItems which represent user input controls.  The
// DynamicForm provides +link{group:FormLayout,layout}, value management, validation and
// databinding for the controls it manages.
// <P>
// <var class="smartgwt">
// To create a DynamicForm, create several +link{FormItem}s and pass them to
// +link{setItems()}.  For example:
// <pre>
//    DynamicForm form = new DynamicForm();
//    TextItem textItem = new TextItem("userName");
//    SelectItem selectItem = new SelectItem("usState");
//    form.setItems(textItem, selectItem);
// </pre>
// </var>
// <var class="smartclient">
// To create a DynamicForm, set +link{dynamicForm.fields} to an Array of Objects describing the
// FormItems you want to use.  For example:
// <pre>
//    isc.DynamicForm.create({
//        fields:[
//            {name:"userName", type:"text"},  // creates a TextItem
//            {name:"usState", type:"select"}  // creates a SelectItem
//        ]
//    })
// </pre>
// </var>
// The item <code>name</code> is an identifier for the item that must be unique just within
// this form.  It is used:
// <ul>
// <li> as the name under which the item's value is stored in the form (the form's
//      current values are accessible as +link{dynamicForm.getValues,form.getValues()}
// <li> when retrieving the FormItem's current value (via
//      +link{dynamicForm.getValue,form.getValue()})
// <li> to retrieve the item itself via +link{dynamicForm.getItem(),form.getItem()}
// </ul>
// FormItems can also be created by binding the form to a DataSource via
// <code>setDataSource()</code>.  In this case, FormItems are
// chosen based on the data type of the field - see +link{type:FormItemType}.  You can override
// the automatically chosen FormItem via +link{DataSourceField.editorType}.
// <P>
// When using DataSource binding, you can also add additional FormItems not specified in the
// DataSource, or override any properties on the automatically generated FormItems, without
// having to re-declare any information that comes from the DataSource.  See the QuickStart
// Guide chapter on Data Binding for an overview.
// <P>
// All FormItems share a common set of properties for controlling +link{group:formLayout,form
// layout}.  Other properties common to all FormItems are documented on the +link{FormItem}
// class, and properties specific to particular FormItems are documented on the respective
// FormItems.
// <P>
// NOTE: For very simple forms consisting of exactly one item, you still use a DynamicForm.
// See the "fontSelector" form in the +explorerExample{toolstrip,Toolstrip example}.
//
//  @implements DataBoundComponent
//  @treeLocation Client Reference/Forms
//  @visibility external
//<

// create the form as a descendant of the Canvas
isc.ClassFactory.defineClass("DynamicForm", "Canvas", "DataBoundComponent");

// Synonym for use by ValuesManagers working with distributed 'FormLayouts'
isc.addGlobal("FormLayout", isc.DynamicForm);


//> @groupDef items
// Manipulating the items that belong to a form.
// <BR><br>
// An item manages an atomic value (eg a String, Number, Date, etc) that appears as one of the
// properties in the overall form's values.  Some items exist purely for layout or appearance
// purposes (eg SpacerItem) and do not manage a value.
// @title Form Items
// @visibility external
//<

//> @groupDef values
// Manipulating the values stored in the form.
// @visibility external
//<

//> @groupDef valueMap
// A +link{type:ValueMap} defines the set of legal values for a field, and optionally allows you to provide
// a mapping from stored values to values as seen by the end user.
//
// @visibility external
//<

//> @groupDef validation
// Validation
// @visibility external
//<

//> @groupDef formTitles
// Properties that affect form item title placement and styling.
// @title Form Titles
// @visibility external
//<

//> @groupDef errors
// Validation errors and how they are shown
// @visibility external
//<

//> @groupDef submitting
// Direct submission of forms to a target URL
// <P>
// <b>NOTE:</b> directly submitting forms is only done for specialized purposes, such as
// integration with certain legacy systems.  Normal form usage contacts the server via
// +link{group:dataBoundComponentMethods,DataBound Component Methods}, through the RPCManager system.
// @visibility external
//<

//> @groupDef elements
// Manipulating native form elements
//<


// add constants
isc.DynamicForm.addClassProperties({


    //>    @type    FormMethod
    //            Form METHOD parameters - how the form fields are submitted to the server
    GET:"GET",                            //    @value    isc.DynamicForm.GET        GET request -- URL encoding (~4K max)
    POST:"POST",                        //    @value    isc.DynamicForm.POST    POST request -- separate field encoding (no max)
    //            @visibility external
    //            @group    submitting
    //<

    //>    @type    Encoding
    // Form encoding types - these translate to Form ENCTYPE parameters.
    // @value DynamicForm.NORMAL    normal form encoding ("application/x-www-form-urlencoded")
    NORMAL:    "normal",
    // @value DynamicForm.MULTIPART form encoding for forms with INPUT file elements, that is, forms
    //                              that upload files ("multipart/form-data")
    MULTIPART:    "multipart",
    //            @group    submitting
    //            @visibility external
    //<
    // NOTE: EncodingTypes has the values that we actually write into the form in HTML.

    //>    @type    EncodingTypes
    // Form ENCTYPE parameters - how data is encoded when sent to the server.
    // See:  http://www.w3.org/TR/html4/interact/forms.html#adef-enctype
    //            @group    submitting
    // normal form encoding
    NORMAL_ENCODING:    "application/x-www-form-urlencoded",
    // multipart encoding for file upload
    MULTIPART_ENCODING:    "multipart/form-data",
    //<

    // Attributes written into containers for form items / form item elements
    _containsItem : "_containsItem",
    _itemPart : "_itemPart",
    // Options for the itemPart setting
    _element : "_element",
    _textBoxString : "_textBox",
    _controlTableString : "_controlTable",
    _inlineErrorString : "inlineErrorHandle",
    _title : "_title",

    buildOperatorIndex : function () {
        if (isc.DataSource == null) return;
        var list = isc.getValues(isc.DataSource.getSearchOperators());

        list = list.sortByProperties(["symbol"], [false],
            [function (item, propertyName, context) {
                var value = item[propertyName],
                    length = isc.isA.String(value) ? value.length : 0
                ;

                return length;
            }]
        );

        this._operatorIndex = list.makeIndex("symbol", true);
    },
    getOperatorIndex : function () {
        return this._operatorIndex;
    },

    _defaultItemHoverHTMLImpl : function (item) {
        // Just bail if a native prompt is shown
        if (item.implementsPromptNatively) return null;
        var prompt = item.prompt;
        if (!prompt && item.parentItem) prompt = isc.DynamicForm._defaultItemHoverHTMLImpl(item.parentItem);
        return prompt
    },

    _defaultValueHoverHTMLImpl : function (item) {
        var returnVal = item.getDisplayValue();
        if (returnVal != null) {
            returnVal = "" + returnVal;

            // Don't escape &nbsp; unless that's actually the data value
            var value;
            if (returnVal == item._$nbsp &&
                ((value = item.getValue()) == null || value == isc.emptyString))
            {
                returnVal = "";

            // If escapeHTML is irrelevant (e.g. TextItems), then explicitly escape the value
            // here because mapValueToDisplay() will not.

            } else if (!item.canEscapeHTML) {
                returnVal = returnVal.asHTML();
            }
        }
        return returnVal;
    }
});



isc.DynamicForm.addProperties({

    // Basic Definition: items and values
    // --------------------------------------------------------------------------------------------

    //>    @attr    dynamicForm.items        (Array of FormItem Properties : null : [IRW])
    // Synonym for +link{attr:dynamicForm.fields}
    //
    // @see attr:dynamicForm.fields
    // @group items
    // @setter setItems()
    // @visibility external
    //<

    //> @attr dynamicForm.fields (Array of FormItem Properties : null : [IRW])
    // An array of field objects, specifying the order, layout, and types of each field in the
    // DynamicForm.
    // <p>
    // When both <code>dynamicForm.fields</code> and <code>dynamicForm.dataSource</code> are
    // set, <code>dynamicForm.fields</code> acts as a set of overrides as explained in
    // +link{attr:DataBoundComponent.fields}.
    // <P>
    // See +link{group:formLayout,Form Layout} for information about how flags specified on
    // the FormItems control how the form is laid out.
    //
    // @see class:FormItem
    // @setter setFields()
    // @group items
    // @visibility external
    //<

    //>    @attr    dynamicForm.defaultItems    (Array of FormItem Properties : null : [ARW])
    // An array of FormItem objects, defining the default set of elements this form
    // creates. (Typically set at a class level on the instance prototype).
    // @group items
    //<
    // NOTE: not external; used for making specialized form subclasses

    //>    @attr    dynamicForm.values        (Object : null : [IRW])
    // An Object containing the initial values of the form as properties, where each
    // propertyName is the name of a +link{items,form item} in the form, and each property
    // value is the value held by that form item.
    // <P>
    // The form's values may contain values that are not managed by any FormItem, and these
    // values will be preserved and available when the form values are subsequently retrieved
    // via +link{getValues()}.
    // <P>
    // Providing values on initialization is equivalent to calling +link{setValues()}.
    // <P>
    // As the user manipulates form items to change values, change events fire
    // +link{formitem.change,on the items} and
    // +link{dynamicForm.itemChange,on the form as a whole}.
    // <P>
    // Note that form values are logical values, for example, the value of a +link{DateItem} is
    // a JavaScript Date object, not a String, even if the user enters the date via a text
    // input.  Likewise the value of a +link{TextItem} or +link{CheckboxItem} that started out
    // null remains null until the user changes it; the value will not be automatically
    // converted to the null string ("") or false respectively, as happens with native HTML
    // elements.
    //
    // @group formValues
    // @visibility external
    //<

    // Table Layout
    // --------------------------------------------------------------------------------------------

    //> @groupDef formLayout
    // <b>FormItem Placement in Columns and Rows</b>
    // <P>
    // With the default tabular layout mechanism, items are laid out in rows from left to
    // right until the number of columns, specified by +link{dynamicForm.numCols,form.numCols},
    // is filled, then a new row is begun.  Flags on FormItems, including
    // +link{FormItem.startRow,startRow}, +link{FormItem.endRow,endRow},
    // +link{FormItem.colSpan,colSpan} and +link{FormItem.rowSpan,rowSpan}, control row and
    // column placement and spanning.
    // <P>
    // Note that the most common form items (TextItem, SelectItem, etc) take up <b>two</b>
    // columns by default: one for the form control itself, and one for it's title.  The
    // default setting of +link{dynamicForm.numCols,form.numCols:2} will result in one TextItem
    // or SelectItem per row.
    // <P>
    // Note also that ButtonItems have both startRow:true and endRow:true by default.  You must
    // set startRow and/or endRow to <code>false</code> on a ButtonItem in order to place a
    // button in the same row as any other item.
    // <P>
    // The log category "tablePlacement" can be enabled from the Developer Console to watch
    // items being placed.  You can also set +link{dynamicForm.cellBorder,form.cellBorder:1} to
    // reveal the table structure for layout troubleshooting purposes.
    // <P>
    // <b>Row and Column Sizing</b>
    // <P>
    // +link{DynamicForm.colWidths} controls the widths of form columns.  FormItems that have
    // "*" for +link{formItem.width} will fill the column.  FormItems with a numeric width will
    // have that width in pixels regardless of the column's specified width, which may cause the
    // column to overflow as described under +link{DynamicForm.fixedColWidths}.
    // <P>
    // For row heights, the largest pixel height specified on any item in the row is taken as a
    // minimum size for the row.  Then, any rows that have "*" or "%" height items will share
    // any height not taken up by fixed-sized items.
    // <P>
    // Individual item heights are controlled by +link{item.height}. This may be specified as
    // an integer (pixel value), or a percentage string, or the special string "*", which
    // indicates an item should fill the available space.<br>
    // Percentages allow developers to determine how the available space in the form
    // is split amongst items. For example if a form has 4 items in a single column,
    // 2 of which have an  absolute pixel height specified, and 2 of which are have
    // heights of <code>"30%"</code> and <code>"70%"</code> respectively, the percentage
    // sized items will split up the available space after the fixed size items have been
    // rendered.<br>
    // Note that +link{item.cellHeight} may be specified to explicitly control the height of
    // an item's cell. In this case the specified +link{item.height} will govern the size
    // of the item within the cell (and if set to a percentage, this will be interpreted as
    // a percentage of the cellHeight).
    // <P>
    // <b>Managing Overflow</b>
    // <P>
    // Forms often contain labels, data values, or instructional text which can vary in
    // size based on the skin, data values, or internationalization settings.  There are a few
    // ways to deal with a form potentially varying in size:
    // <ol>
    // <li> Allow scrolling when necessary, using +link{Canvas.overflow,overflow:auto}, either
    // on the immediate form, or on some parent.
    // <li> Place the form in a Layout along with a component that can render any specified
    // size, such as a +link{ListGrid}.  In this case, the Layout will automatically shrink the
    // grid in order to accommodate the form.
    // <li> Ensure that the form can always render at a designed minimum size by reducing
    // the number of cases of variable-sized text, and testing remaining cases across all
    // supported skins.  For example, move help text into hovers on help icons, or clip
    // long text values at a maximum length and provide a hover to see the rest.
    // </ol>
    //
    // Several examples of Form Layout are available +explorerExample{formsLayout,here}.
    //
    // @treeLocation Client Reference/Forms
    // @title Form Layout
    // @visibility external
    //<


    //> @attr dynamicForm.itemLayout   (FormLayoutType : "table" : IRWA)
    // Layout style to use with this form.
    // <P>
    // The default of "table" uses a tabular layout similar to HTML tables, but with much more
    // powerful control over sizing, item visibility and reflow, overflow handling, etc.
    // <P>
    // <code>itemLayout:"absolute"</code> allows absolute positioning of every form item.  This
    // provides maximum flexibility in placement, with the following limitations:<ul>
    // <li> titles, which normally take up an adjacent cell, are not shown.  Use
    //      StaticTextItems to show titles
    // <li> no automatic reflow when showing or hiding items.  +link{method:FormItem.setLeft()}
    //      and +link{method:FormItem.setTop()} can be used for manual reflow.
    // <li> only pixel and percent sizes are allowed, no "*".  Percent widths mean percentage
    //      of the overall form size rather than the column size
    // <li> with different font styling or internationalized titles, items may overlap that did
    //      not overlap in the skin used at design time
    // </ul>
    //
    // @group formLayout
    // @visibility absForm
    //<
    //itemLayout:"table",

    //> @attr dynamicForm.flattenItems (boolean : false : IR)
    // If set, the form will set +link{numCols} automatically such that all form items will be
    // laid out in a single row.
    // <P>
    // +link{colWidths} may still be set.  If unset, they will be generated so that all columns
    // showing a title will have +link{titleWidth} and all other columns will have width:"*".
    //
    // @group formLayout
    //<
    flattenItems:false,

    //>    @attr dynamicForm.numCols        (number : 2 : [IRW])
    // The number of columns of titles and items in this form's layout grid. A title and
    // corresponding item each have their own column, so to display two form elements per
    // row (each having a title and item), you would set this property to 4.
    //
    // @group formLayout
    // @visibility external
    //<
    numCols:2,

    //>    @attr dynamicForm.fixedColWidths    (Boolean : false : IRW)
    // If true, we ensure that column widths are at least as large as you specify them.  This
    // means that if any single column overflows (due to, eg, a long unbreakable title),
    // the form as a whole overflows.
    // <P>
    // If false, columns will have their specified sizes as long as no column overflows.  If
    // any column overflows, space will be taken from any other columns that aren't filling the
    // available room, until there is no more free space, in which case the form as a whole
    // overflows.
    //
    // @group formLayout
    // @visibility external
    //<

    fixedColWidths:false,

    // fixedRowHeights - undocumented property that causes heights to be written into cells,
    // which, like fixedColumnWidths, puts you into a situation where you're more likely to
    // overflow.
    fixedRowHeights:false,

    //>    @attr    dynamicForm.colWidths        (Array : null : [IRW])
    // An array of widths for the columns of items in this form's layout grid.
    // <P>
    // If specified, these widths should sum to the total width of the form (form.width).
    // If not specified, we assume every other column will contain form item titles, and so
    // should have <code>form.titleWidth</code>, and all other columns should share the
    // remaining space.
    // <P>
    // Acceptable values for each element in the array are:<br>
    // <ul>
    // <li>A number (e.g. 100) representing the number of pixel widths to allocate to a
    //     column.
    // <li>A percent (e.g. 20%) representing the percentage of the total form.width to
    //     allocate to a column.
    // <li>"*" (all) to allocate remaining width (form.width minus all specified column
    //     widths). Multiple columns can use "*", in which case remaining width is divided
    //     between all columns marked "*".
    // </ul>
    // @group formLayout
    // @visibility external
    // @example columnSpanning
    //<
    colWidths:null,

    //>    @attr dynamicForm.minColWidth        (number : 20 : IRW)
    // Minimum width of a form column.
    // @group formLayout
    // @visibility external
    //<
    minColWidth:20,

    //>    @attr    dynamicForm.cellSpacing        (number : 0 : [IRW])
    // The amount of empty space, in pixels, between form item cells in the layout grid.
    // @group formLayout
    // @visibility internal
    //<

    cellSpacing:0,

    //>    @attr dynamicForm.cellPadding        (number : 2 : [IRW])
    // The amount of empty space, in pixels, surrounding each form item within its cell in
    // the layout grid.
    // @group formLayout
    // @visibility external
    //<
    cellPadding:2,

    //>    @attr dynamicForm.cellBorder        (number : 0 : [IRW])
    // Width of border for the table that form is drawn in. This is primarily used for debugging
    // form layout.
    // @group formLayout
    // @visibility external
    //<
    cellBorder:0,

    // default height for a table row where there are no specified sizes at all (pixel, '*', or
    // percent)
    defaultRowHeight:22,

    //> @attr DynamicForm.sectionVisibilityMode (VisibilityMode : "multiple" : [IRW])
    // If the form has sections, [implemented as +link{SectionItem}s], this attribute controls
    // whether multiple sections can be expanded at once.
    //
    // @see type:VisibilityMode
    // @see class:SectionItem
    // @group formLayout
    // @visibility external
    //<
    sectionVisibilityMode: "multiple",

    // Embedded widgets
    // --------------------------------------------------------------------------------------------
    // Turn on allowContentAndChildren for Canvas Items.
    // NOTE: this has no actual effect unless a CanvasItem is used

    allowContentAndChildren : true,
    separateContentInsertion: true,
    _avoidRedrawFlash:true,
    // necessary because the default determination assumes anything with children doesn't have
    // inherent height
    hasInherentHeight : function () {
        if (this.inherentHeight != null) return this.inherentHeight;
        return (this.overflow == isc.Canvas.VISIBLE || this.overflow == isc.Canvas.CLIP_H);
    },

    // DataBinding
    // --------------------------------------------------------------------------------------------
    //>    @attr    dynamicForm.fieldIdProperty        (string : "name" : IRWA)
    // Name of the column in the fields array that holds the name of the item property that holds
    // the value
    //        @group    data
    //<
    fieldIdProperty:"name",

    //>    @attr    dynamicForm.titleField        (string : "title" : IRWA)
    // Name of the column in the fields array that holds the name of the title property that holds
    // the title
    //        @group    appearance
    //<
    titleField:"title",

    //>    @attr    dynamicForm.showDetailFields (Boolean : true : IR)
    // For databound forms, whether to show fields marked as detail fields.
    // @visibility external
    //<
    showDetailFields: true,

    //>    @attr dynamicForm.longTextEditorThreshold (number : 255 : IRW)
    // When creating form items for fields with text type data, if the specified length of the
    // field exceeds this threshold we will create form item of type
    // <code>this.longTextEditorType</code> (a TextAreaItem by default), rather than a simple
    // text item.  Overridden by explicitly specifying <code>editorType</code> for the field.
    // @group appearance
    // @visibility external
    //<
    longTextEditorThreshold:255,
    //>    @attr dynamicForm.longTextEditorType (string  : "textArea" : IRW)
    // Name of the Form Item class to use for text fields which exceed the
    // longTextEditorThreshold for this form.
    // @group appearance
    // @visibility external
    //<
    longTextEditorType:"textArea",

    // Values formatting

    //> @attr dynamicForm.dateFormatter (DateDisplayFormat : null : IRW)
    // Default +link{DateDisplayFormat} for Date type values displayed in this form.
    // <P>
    // If some field's value is set to a native Date object, how should it be displayed to the
    // user? If specified this is the default display format to use, and will apply to all fields
    // except those specified as +link{formItem.type,type:"time"}
    // (See +link{dynamicForm.timeFormatter}).
    // <P>
    // May be overridden at the component level for fields of type <code>datetime</code> via
    // +link{dynamicForm.datetimeFormatter}.
    // <P>
    // Note that if specified, +link{formItem.dateFormatter} and +link{formItem.timeFormatter}
    // take precedence over the format specified at the component level.
    // <P>
    // If no explicit formatter is specified at the field or component level, dates will be
    // formatted according to the system-wide
    // +link{Date.setShortDisplayFormat(),short date display format} or
    // +link{Date.setShortDatetimeDisplayFormat(),short datetime display format} depending on the
    // specified field type.
    // @visibility external
    //<

    //> @attr dynamicForm.timeFormatter (TimeDisplayFormat : null : IRW)
    // Default +link{TimeDisplayFormat} for +link{formItem.type,type:"time"} field values displayed
    // in this form.
    // <P>
    // Note that if specified, +link{formItem.dateFormatter} and +link{formItem.timeFormatter}
    // take precedence over the format specified at the component level.
    // <P>
    // If no explicit formatter is specified at the field or component level, time values will be
    // formatted according to the system-wide
    // +link{Time.setNormalDisplayFormat(),normal time display format}.
    // specified field type.
    // @visibility external
    //<

    //> @attr dynamicForm.datetimeFormatter (DateDisplayFormat : null : IRW)
    // Default +link{DateDisplayFormat} for Date type values displayed in this form in fields
    // of type <code>datetime</code>.
    // <P>
    // For datetime fields, this attribute will be used instead of +link{dynamicForm.dateFormatter}
    // when formatting Date values.
    // <P>
    // Note that if specified, +link{formItem.dateFormatter} and +link{formItem.timeFormatter}
    // take precedence over the format specified at the component level.
    // <P>
    // If no explicit formatter is specified at the field or component level, datetime field
    // values will be formatted according to the system-wide
    // +link{Date.setShortDatetimeDisplayFormat(),short datetime display format}.
    // @visibility external
    //<

    //>ValuesManager

    // ValuesManager
    // ----------------------------------------------------------------------------------------
    //>@attr dynamicForm.valuesManager  (ValuesManager instance or global ID : null : [IA])
    // If set at init time, this dynamicForm will be created as a member form for the
    // specified valuesManager.  To update the valuesManager to which a form belongs after init
    // use <code>valuesManager.addMember(form)</code> and
    // <code>valuesManager.removeMember(form)</code>
    // @see class:ValuesManager
    // @visibility external
    // @group formValuesManager
    //<
    //<ValuesManager


    // Title Formatting
    // --------------------------------------------------------------------------------------------

    //> @type  TitleOrientation
    // Orientation of titles relative to the FormItem being labeled.  Can be set a the
    // DynamicForm level as a default, or on individual items.
    //
    // @value  "left"
    // @value  "top"
    // @value  "right"
    // @group formTitles
    // @see DynamicForm.titleOrientation
    // @see FormItem.titleOrientation
    // @visibility external
    //<

    //>    @attr    dynamicForm.titleOrientation    (TitleOrientation : "left" : [IRW])
    // Default orientation for titles for items in this form.  +link{type:TitleOrientation}
    // lists valid options.
    // <P>
    // Note that titles on the left or right take up a cell in tabular
    // +link{group:formLayout,form layouts}, but titles on top do not.
    //
    //      @group  formTitles
    //      @visibility external
    // @example formLayoutTitles
    //<

    //>    @attr dynamicForm.titlePrefix (HTMLString : "" : [IRW])
    // The string prepended to the title of every item in this form.  See also +{requiredTitlePrefix} for
    // fields that are required.
    // @group formTitles
    // @visibility external
    //<
    titlePrefix:"",

    //>    @attr dynamicForm.rightTitlePrefix (HTMLString : ":&nbsp;" : [IRW])
    // The string prepended to the title of an item in this form if its
    // titleOrientation property is set to "right".
    // @group formTitles
    // @visibility external
    //<
    rightTitlePrefix:":&nbsp;",

    //>    @attr dynamicForm.titleSuffix (HTMLString : "&nbsp;:" : [IRW])
    // The string appended to the title of every item in this form.  See also +{requiredTitleSuffix} for
    // fields that are required.
    // @group formTitles
    // @visibility external
    //<
    titleSuffix:"&nbsp;:",

    //> @attr dynamicForm.rightTitleSuffix (HTMLString : "" : [IRW])
    // The string appended to the title of an item in this form if its titleOrientation
    // property is set to "right".
    // @group formTitles
    // @visibility external
    //<
    rightTitleSuffix:"",

    exclusiveTitlePrefix:"",
    exclusiveRightTitlePrefix:"",
    exclusiveTitleSuffix:"",
    exclusiveRightTitleSuffix:"",

    //>    @attr    dynamicForm.titleWidth        (number or "*": 100 : [IRW])
    //          The width in pixels allocated to the title of every item in this form.  If you
    //          don't specify explicit +link{attr:dynamicForm.colWidths}, you can set this
    //          value to the string "*" to divide the usable space evenly between titles and
    //          fields.
    //      @group  formTitles
    //      @visibility external
    //<
    titleWidth:100,

    //> @attr dynamicForm.clipItemTitles (boolean : false : [IRW])
    // Should the titles for form items be clipped if they are too large for the available
    // space?
    // <p>
    // Can be overridden for individual items via +link{FormItem.clipTitle}.
    // @visibility external
    //<
    clipItemTitles:false,

    //>    @attr    dynamicForm.wrapItemTitles (boolean : null : [IRW])
    // Whether titles for form items should wrap.  If not specified, titles will wrap by
    // default.  Can be overridden for individual items via +link{formItem.wrapTitle}
    // @visibility external
    // @group formTitles
    //<
//    wrapItemTitles:null,

    //> @attr   dynamicForm.showInlineErrors (Boolean : true : [IRW])
    // If true, field errors are written into the form next to the item(s) where the errors
    // occurred.  Errors may appear as text or just an icon (via +link{showErrorText}:false).
    // <P>
    // If false, errors are written at the top of the form.
    // <P>
    // To do some other kind of error display, override +link{showErrors()}.
    //
    // @group validation
    // @visibility external
    //<
    showInlineErrors: true,

    // customization of inline errors appearance on items

    // showErrorIcons doc contains an overview of error styling to be reused as the docs for
    // showErrorText / showErrorStyle as well
    //> @attr dynamicForm.showErrorIcons (Boolean : true : IRW)
    // +link{dynamicForm.showErrorIcons,showErrorIcons},
    // +link{dynamicForm.showErrorText,showErrorText}, and
    // +link{dynamicForm.showErrorStyle,showErrorStyle} control how validation errors are
    // displayed when they are displayed inline in the form (next to the form item where there
    // is a validation error).  To instead display all errors at the top of the form, set
    // +link{showInlineErrors}:false.
    // <P>
    // <code>showErrorIcons</code>, <code>showErrorText</code> and <code>showErrorStyle</code>
    // are all boolean properties, and can be set on a DynamicForm to control the behavior
    // form-wide, or set on individual FormItems.
    // <P>
    // The HTML displayed next to a form item with errors is generated by
    // +link{FormItem.getErrorHTML()}.
    // The default implementation of that method respects <code>showErrorIcons</code> and
    // <code>showErrorText</code> as follows:
    // <P>
    // <code>showErrorIcons</code>, or <code>showErrorIcon</code> at the FormItem level controls
    // whether an error icon should appear next to fields which have validation errors.  The icon's
    // appearance is governed by +link{FormItem.errorIconSrc}, +link{FormItem.errorIconWidth} and
    // +link{FormItem.errorIconHeight}
    // <P>
    // <code>showErrorText</code> determines whether the text of the validation error should be
    // displayed next to fields which have validation errors. The attribute
    // +link{dynamicForm.showTitlesWithErrorMessages} may be set to prefix error messages with the
    // form item's title + <code>":"</code> (may be desired if the item has
    // +link{formItem.showTitle} set to false).
    // <P>
    // +link{dynamicForm.errorOrientation} controls where the error HTML should appear relative
    // to form items. Therefore the combination of +link{showErrorText}<code>:false</code> and
    // +link{errorOrientation}<code>:"left"</code> creates a compact validation error display
    // consisting of just an icon, to the left of the item with the error message
    // available via a hover (similar appearance to ListGrid validation error display).
    // <P>
    // In addition to this, <code>showErrorStyle</code> determines whether fields  with validation
    // errors should have special styling applied to them. See +link{type:FormItemBaseStyle} for a
    // discussion for how error styling is calculated.
    //
    // @group  validation
    // @visibility external
    //<
    showErrorIcons: true,

    //> @attr dynamicForm.showErrorText (Boolean : false : IRW)
    // @include dynamicForm.showErrorIcons
    // @group  validation
    // @visibility external
    //<
    showErrorText:false,

    //> @attr dynamicForm.showErrorStyle (Boolean : true : IRW)
    // @include dynamicForm.showErrorIcons
    // @group  validation
    // @visibility external
    //<
    showErrorStyle: true,

    //> @attr dynamicForm.errorOrientation (align : "left" : IRW)
    // If +link{dynamicForm.showInlineErrors} is true, where should the error icon and text appear
    // relative to form items?  Valid options are <code>"top"</code>,
    // <code>"bottom"</code>, <code>"left"</code> or <code>"right"</code>.<br>
    // May be overridden at the item level via +link{formItem.errorOrientation}.
    // @group validation, appearance
    // @visibility external
    //<
    errorOrientation: "left",

    // Enable customization of the error item
    errorItemDefaults : {
        type:"blurb",
        wrap:true,
        showIf:function () {
            return !this.form.showInlineErrors && this.form.hasErrors();
        },
        defaultDynamicValue : function (item,form,values) {
            return form.getErrorsHTML(form.getErrors());
        }
    },
    //> @attr dynamicForm.errorItemProperties (object : null : [IRA])
    // If +link{dynamicForm.showInlineErrors} is false we show all errors for the form item in
    // a single item rendered at the top of the form.<br>
    // This attribute contains a properties block for this item.
    // @group validation
    // @visibility external
    //<
    //errorItemProperties : {},

    //> @attr dynamicForm.errorItemCellStyle (string  : "formCellError" : [IR])
    // If +link{dynamicForm.showInlineErrors} is false we show all errors for the form item in
    // a single item rendered at the top of the form.<br>
    // This attribute specifies the cellStyle to apply to this item.
    // @group validation
    // @visibility external
    //<
    errorItemCellStyle:"formCellError",

    //> @attr dynamicForm.errorsPreamble (HTMLString :"The following errors were found:" : IR)
    // If +link{dynamicForm.showInlineErrors} is <code>false</code>, all errors for the items
    // in the form are rendered as a single item at the top of the form. This attribute specifies
    // an introductory message rendered out before the individual error messages.
    // @group validation, i18nMessages
    // @visibility external
    //<
    errorsPreamble:"The following errors were found:",

    //>    @attr    dynamicForm.showTitlesWithErrorMessages     (Boolean : false : [IRW])
    //          Indicates whether on validation failure, the error message displayed to the
    //          user should be prepended with the title for the item.
    //      @group  validation
    //      @visibility external
    //<
    // This property is referenced by 'formItem.getErrorHTML()'
//    showTitlesWithErrorMessages : false,

    //>    @attr    dynamicForm.hiliteRequiredFields        (Boolean : true : IRW)
    // Indicates whether the titles of required items in this form should use the special
    // prefix and suffix specified by the next two properties, instead of the standard
    // prefix and suffix.
    //      @group  formTitles
    //      @visibility external
    //<
    hiliteRequiredFields:true,


    //>    @attr dynamicForm.requiredTitlePrefix (HTMLString : "<b>" : IRW)
    // The string prepended to the title of every required item in this form if
    // highlightRequiredFields is true.
    // @group formTitles
    // @visibility external
    //<
    requiredTitlePrefix:"<b>",

    //>    @attr dynamicForm.requiredRightTitlePrefix (HTMLString : "<b>:&nbsp;" : IRW)
    // The string prepended to the title of every required item in this form if
    // highlightRequiredFields is true and the titleOrientation property is set to "right".
    // @group formTitles
    // @visibility external
    //<
    requiredRightTitlePrefix:"<b>:&nbsp;",

    //>    @attr dynamicForm.requiredTitleSuffix (HTMLString : "&nbsp;:</b>" : [IRW])
    // The string appended to the title of every required item in this form if
    // highlightRequiredFields is true.
    // @group  formTitles
    // @visibility external
    //<
    requiredTitleSuffix:"&nbsp;:</b>",

    //>    @attr dynamicForm.requiredRightTitleSuffix (HTMLString : "</b>" : [IRW])
    // The string appended to the title of every required item in this form if
    // highlightRequiredFields is true and the titleOrientation property is set to "right".
    // @group formTitles
    // @visibility external
    //<
    requiredRightTitleSuffix:"</b>",

    exclusiveRequiredTitlePrefix:null,
    exclusiveRequiredRightTitlePrefix:null,
    exclusiveRequiredTitleSuffix:null,
    exclusiveRequiredRightTitleSuffix:null,

    //> @attr dynamicForm.requiredMessage (HTMLString : null : [IRW])
    // The required message for required field errors.
    // @group formTitles
    // @visibility external
    //<


    // Generic item defaults
    // ---------------------------------------------------------------------------------------

    //> @attr dynamicForm.canEdit (Boolean : null : IRWA)
    // If set to <code>false</code>, the form will be marked read-only. A widget on the form
    // is editable if either (1) beginning with the widget and continuing up the containment
    // hierarchy, including the form, the first widget to have a non-null <code>canEdit</code>
    // attribute has canEdit:true, or (2) neither the widget nor any parent has a non-null
    // <code>canEdit</code> attribute. This setting allows you to enable or disable the default
    // editability of the form's items at one time.
    // <p>
    // This setting differs from the enabled/disabled state in that most form items will
    // allow copying of the contents while read-only but do not while disabled.
    // <p>
    // Note that a form is considered editable if <code>canEdit</code> is null (default) or
    // <code>true</code>.
    // @see DynamicForm.readOnlyDisplay
    // @group readOnly
    // @visibility external
    //<

    //> @type ReadOnlyDisplayAppearance
    // Dictates the appearance of form items when +link{FormItem.canEdit} is set to
    // <code>false</code>.
    //
    // @value "static" Item value should appear within the form as a static block of text,
    // similar to the default appearance of a +link{StaticTextItem}. This appearance may be
    // modified via +link{FormItem.readOnlyTextBoxStyle} and +link{formItem.clipStaticValue}.
    // @value "readOnly" Item should appear unchanged, but user interaction to edit the item
    // will be disallowed. Note that some interactions will be allowed, such as selecting text
    // within a read-only +link{TextItem} for copy and paste. Exact implementation may vary by
    // form item type.
    // @value "disabled" Item will appear disabled.
    //
    // @see attr:DynamicForm.readOnlyDisplay
    // @see attr:FormItem.readOnlyDisplay
    // @visibility external
    //<

    //> @attr dynamicForm.readOnlyDisplay (ReadOnlyDisplayAppearance : "readOnly" : IRW)
    // If +link{DynamicForm.canEdit} is set to <code>false</code>, how should the items in this
    // form be displayed to the user?
    // <p>
    // Can be overridden via +link{FormItem.readOnlyDisplay} on individual form items.
    // @group readOnly
    // @visibility external
    //<
    readOnlyDisplay: "readOnly",

    //> @attr dynamicForm.readOnlyTextBoxStyle (FormItemBaseStyle : "staticTextItem" : IRW)
    // Default +link{FormItem.readOnlyTextBoxStyle} setting for items in this form.
    // @visibility external
    //<
    readOnlyTextBoxStyle: "staticTextItem",

    //> @attr dynamicForm.clipStaticValue (Boolean : null : IR)
    // Default +link{FormItem.clipStaticValue} setting for items in this form. When unset, this
    // is equivalent to <code>false</code>.
    // @visibility external
    //<
    //clipStaticValue: null,

    //> @attr dynamicForm.showDeletions (Boolean : null : IRA)
    // Default +link{FormItem.showDeletions} setting for items in this form.
    // @visibility external
    //<
    //showDeletions: null,


    // Hovers
    // ---------------------------------------------------------------------------------------

    // Turn off standard form item hover handling - we're doing our own custom handling instead.
    canHover:false,

    //> @attr dynamicForm.itemHoverDelay (number : 500 : [IRW])
    // If the user rolls over an item, how long a delay before we fire any hover action / show
    // a hover for that item?
    // @see FormItem.hoverDelay
    // @group Hovers
    // @visibility external
    //<
    itemHoverDelay:500,

    //> @attr dynamicForm.itemHoverWidth (measure : null : [IRW])
    // A default width for hovers shown for items
    // @see FormItem.hoverWidth
    // @group Hovers
    // @visibility external
    // @example itemHoverHTML
    //<

    //> @attr dynamicForm.itemHoverHeight (measure : null : [IRW])
    // A default height for hovers shown for items
    // @see FormItem.hoverHeight
    // @group Hovers
    // @visibility external
    //<

    //> @attr dynamicForm.itemHoverAlign (Alignment  : null : [IRW])
    // Text alignment for hovers shown for items
    // @see FormItem.hoverAlign
    // @group Hovers
    // @visibility external
    //<

    //> @attr dynamicForm.itemHoverVAlign (measure : null : [IRW])
    // Vertical text alignment for hovers shown for items
    // @see FormItem.hoverVAlign
    // @group Hovers
    // @visibility external
    //<

    //> @attr dynamicForm.itemHoverStyle (CSSStyleName  : "formHover" : [IRW])
    // CSS Style for hovers shown for items
    // @see FormItem.hoverStyle
    // @group Hovers
    // @visibility external
    //<
    itemHoverStyle:"formHover",

    //> @attr dynamicForm.itemHoverOpacity (number : null : [IRW])
    // Opacity for hovers shown for items
    // @see FormItem.hoverOpacity
    // @group Hovers
    // @visibility external
    //<

    //> @attr dynamicForm.itemHoverRect (object : null : [IRWA])
    // Object of the form <code>{left:[value], top:[value], width:[value], height:[value]}</code>
    // for specifying an explicit position for the item hovers to appear.
    // @see FormItem.hoverRect
    // @group Hovers
    // @visibility internal
    //<


    // Sizing
    // --------------------------------------------------------------------------------------------

    // we can't perfectly control the drawn sizes of all form elements, hence by default we
    // show overflow.  Our defaultHeight acts as a minimum.
    overflow:isc.Canvas.VISIBLE,
    defaultHeight:20,

    // Validation
    // --------------------------------------------------------------------------------------------
    //>    @attr    dynamicForm.errors        (array : null : [IRW])
    //          A property list of itemName:errorMessage pairs, specifying the set of error messages
    //          displayed with the corresponding form elements. Each errorMessage may be either a
    //          single string or an array of strings.
    // @group validation
    //      @visibility external
    //<

    //> @attr dynamicForm.validateOnChange (Boolean : false : IRW)
    // If true, form fields will be validated when each item's "change" handler is fired
    // as well as when the entire form is submitted or validated.
    // <p>
    // Note that this property can also be set at the item level or on each validator
    // to enable finer granularity validation in response to user interaction.
    // If true at the form or field level, validators not explicitly set with
    // <code>validateOnChange:false</code> will be fired on change - displaying errors and
    // rejecting the change on validation failure.
    // @group validation
    // @visibility external
    // @see FormItem.validateOnChange
    //<

    validateOnChange:false,

    //>@attr dynamicForm.rejectInvalidValueOnChange (boolean : null : IRWA)
    // If validateOnChange is true, and validation fails for an item on change, with no suggested
    // value, should we revert to the previous value, or continue to display the bad value entered
    // by the user. May be set at the item or form level.
    // @visibility external
    //<
    // Introduced for back-compat: pre 7.0beta2 this was the default behavior, so enable this flag
    // at the item or form level if required for backcompat.
    //rejectInvalidValueOnChange:null,

    //> @attr dynamicForm.unknownErrorMessage (HTMLString : "Invalid value" : [IRW])
    // The error message for a failed validator that does not specify its own errorMessage.
    // @group validation, i18nMessages
    // @visibility external
    //<
    // Inherited from DBC
//    unknownErrorMessage : "Invalid value",

    //> @attr dynamicForm.validateOnExit (Boolean : false : IRW)
    // If true, form items will be validated when each item's "editorExit" handler is fired
    // as well as when the entire form is submitted or validated.
    // <P>
    // Note that this property can also be set at the item level to enable finer granularity
    // validation in response to user interaction - if true at either level, validation
    // will occur on editorExit.
    // @visibility external
    // @see formItem.validateOnExit
    //<

    //> @attr dynamicForm.implicitSave (Boolean : false : IRW)
    // When true, indicates that changes to items in this form will be automatically saved on a
    // +link{dynamicForm.implicitSaveDelay, delay}, as well as when the entire form is
    // submitted.  Unless +link{dynamicForm.implicitSaveOnBlur, form.implicitSaveOnBlur} is set
    // to false, changes will also be automatically saved on editorExit for each item.  This
    // attribute can also be set directly on FormItems.
    // @visibility external
    //<

    //> @attr dynamicForm.implicitSaveOnBlur (Boolean : false : IRW)
    // If true, form item values will be automatically saved when each item's "editorExit"
    // handler is fired as well as on a delay and when the entire form is submitted.  This
    // attribute can also be set directly on FormItems.
    // @visibility external
    //<

    //> @attr dynamicForm.implicitSaveDelay (number : 2000 : IRW)
    // When +link{dynamicForm.implicitSave, implicitSave} is true, this attribute dictates the
    // millisecond delay after which form items are automatically saved during editing.
    // @visibility external
    //<
    implicitSaveDelay: 2000,

    //> @attr dynamicForm.stopOnError (boolean : null : IR)
    // Indicates that if validation fails, the user should not be allowed to exit
    // the field - focus will be forced back into the field until the error is corrected.
    // <p>
    // Enabling this property also implies +link{FormItem.validateOnExit} is automatically
    // enabled. If there are server-based validators on this item, setting this property
    // also implies that +link{FormItem.synchronousValidation} is forced on.
    //
    // @visibility external
    //<

    //> @attr  dynamicForm.synchronousValidation (Boolean : false : IR)
    // If enabled, whenever validation is triggered and a request to the server is required,
    // user interactivity will be blocked until the request returns. Can be set for the entire
    // form or individual FormItems.
    // <p>
    // If false, the form will try to avoid blocking user interaction until it is strictly
    // required. That is until the user attempts to use a FormItem whose state could be
    // affected by a server request that has not yet returned.
    //
    // @visibility external
    //<
    synchronousValidation:false,

    // Focus
    // --------------------------------------------------------------------------------------------

    //>    @attr    dynamicForm.autoFocus        (Boolean : false : IRW)
    // If true, when the form is drawn, focus will automatically be put into the first focusable
    // element in the form.<br>
    // Note that to put focus in a different item you can explicitly call
    // <code>dynamicForm.focusInItem(<i>itemName</i>)</code>
    // @group focus
    // @visibility external
    // @see focusInItem()
    //<
    autoFocus:false,

    //> @attr dynamicForm.autoFocusOnError (Boolean : true : IRW)
    // If true, when +link{dynamicForm.validate(),validation} fails focus will automatically
    // be put into the first focusable field which failed validation.
    // @group focus
    // @visibility external
    //<
    autoFocusOnError:true,

    //>    @attr    dynamicForm.selectOnFocus    (Boolean : false : IRW)
    // If this property is set to true, whenever a text-based field in this form
    // (+link{class:TextItem}, +link{class:TextAreaItem}) is given focus programmatically
    // (see +link{DynamicForm.focusInItem()}), all text within the item will be selected.
    // <P>
    // Note that this flag affects only programmatic focus.  It's the normal behavior of text
    // fields to select all text if the user navigates into them via keyboard, or if the user
    // navigates via mouse, to place the text insertion point at the mouse click, and
    // SmartClient preserves these behaviors.  <code>selectOnFocus</code> is only needed for
    // cases like a form within a pop-up dialog that should have the first field selected.
    // <P>
    // If you also want the value to be selected when the user clicks on the field, set
    // +link{dynamicForm.selectOnClick, selectOnClick} instead.
    // <P>
    // If <code>selectOnFocus</code> is false, the selection is not modified on focus - any
    // previous selection within the item will be maintained.
    // <P>
    // May be overridden at the form item level via +link{formItem.selectOnFocus}.
    //
    // @group focus
    // @visibility external
    //<
    selectOnFocus:false,

    //>    @attr    dynamicForm.selectOnClick    (Boolean : false : IRW)
    // If this property is set to true, whenever a text-based field in this form
    // (+link{class:TextItem}, +link{class:TextAreaItem}) is given focus - whether
    // programmatically (see +link{DynamicForm.focusInItem()}), or via a mouse click, all text
    // within the item will be selected.
    // <P>
    // If you only want the value to be selected when on programmatic focus or keyboard
    // navigation (this is the native browser behavior), set
    // +link{dynamicForm.selectOnFocus, selectOnFocus} instead.
    // <P>
    // May be overridden at the form item level via +link{formItem.selectOnClick}.
    //
    // @group focus
    // @visibility external
    //<
    selectOnClick:false,

    //> @attr   dynamicForm.canFocus    (Boolean : true : IRWA)
    // DynamicForms are considered to have focus if any of their form items have focus.
    // Note that setting <code>dynamicForm.canFocus</code> to false will have no effect on
    // whether form items within the form may receive focus. This property will only govern
    // whether the form may receive focus if the form contains no focusable items.
    // @group focus
    // @visibility external
    //<
    // Focus behavior for forms is a little different than for other elements.
    // o _canFocus() always returns true if the form contains any focusable items
    //   (required to allow programmatic focus() on the form / proper keyboard event handling)
    // o Set _useNativeTabIndex to false - we don't want the form to ever have native focus (instead
    //   native focus will always go to the form items).
    //   Note - we don't want to set tabIndex to -1, as the form items will default to having their
    //   form's tabIndex as their own tabIndex.
    // o Set _useFocusProxy to false - same reason as setting _useNativeTabIndex to false.
    // o Override focus() to call focusInItem() (below)
    // o Override _focusChanged() to blur the focus item on a blur() call (below)
    // - see also comments on form item tabIndex in formItem.js
    canFocus : true,
    _useNativeTabIndex:false,
    _useFocusProxy:false,

    // AutoComplete
    // --------------------------------------------------------------------------------------------
    //>    @attr    dynamicForm.autoComplete   (AutoComplete : null : IRW)
    // Whether to do inline autoComplete in text fields within this form.
    // <p>
    // Can be individually enabled per TextItem, or if enabled for the form as a whole, can
    // be disabled for individual items.
    //
    // @see formItem.autoComplete
    // @group autoComplete
    // @visibility autoComplete
    //<
    //autoComplete:null     null is the same as "none": no-autoComplete


    //>    @attr    dynamicForm.uniqueMatch   (boolean : true : IRW)
    // When autoComplete is enabled, whether to offer only unique matches to the user.
    // <p>
    // Can be individually enabled per TextItem, or if set for the form as a whole, can
    // be set differently for individual items.
    //
    // @see formItem.uniqueMatch
    // @group autoComplete
    // @visibility autoComplete
    //<
    uniqueMatch:true,


    // Spellcheck:
    //>@attr    DynamicForm.browserSpellCheck   (Boolean : true : IRW)
    // If this browser has a 'spellCheck' feature for text-based form item elements, should
    // it be used for items in this form? Can be overridden at the item level via
    // +link{FormItem.browserSpellCheck}
    // <P>
    // Notes:<br>
    // - this property only applies to text based items such as TextItem and TextAreaItem.<br>
    // - this property is not supported on all browsers.
    //
    // @see formItem.browserSpellCheck
    // @visibility external
    //<

    browserSpellCheck:true,

    // Direct Submit
    // --------------------------------------------------------------------------------------------
    //>    @attr dynamicForm.validationURL        (URL : null : IRW)
    // validationURL can be set to do server-side validation against a different URL from where
    // the form will ultimately save, as part of an incremental upgrade strategy for Struts and
    // Struts-like applications.
    // <P>
    // If set, calling +link{method:DynamicForm.submit()} causes an RPC to be sent to this URL to
    // perform server-side validation of the form values.  If the validation fails, the
    // validation errors returned by the server are rendered in the form.  If the validation
    // succeeds, the form is submitted to the URL specified by +link{attr:DynamicForm.action}.
    // <p>
    // The form values are available on the server as request parameters (just like a normal form
    // submit) and also as the values of a DSRequest sent as an RPC alongside the normal
    // submit.
    // <p>
    // The expected response to this request is a DSResponse sent via the RPC mechanism.  If
    // validation is successful, an empty response with the STATUS_SUCCESS status code is
    // sufficient.  If there are validation errors, the DSResponse should have the status set to
    // STATUS_VALIDATION_ERROR and the errors should be set on the response via the
    // addError()/setErrorReport() API on DSResponse.  See the javadoc for DSResponse for
    // details.
    // <P>
    // See the Struts examples in <code>[webroot]/examples/struts</code> for usage examples.
    //
    // @group validation
    // @visibility external
    // @see DynamicForm.saveData()
    // @see DynamicForm.submit()
    //<

    //>    @attr dynamicForm.disableValidation        (boolean : null : IRW)
    //
    // If set to true, client-side validators will not run on the form when validate() is
    // called.  Server-side validators (if any) will still run on attempted save.
    //
    // @group validation
    // @visibility external
    // @see DynamicForm.saveData()
    // @see DynamicForm.submit()
    //<

    //> @attr dynamicForm.cancelParamName (String : "org.apache.struts.taglib.html.CANCEL" : IRW)
    // The name of the special field sent to the server as part of +link{method:DynamicForm.cancel()}
    // @visibility external
    //<
    cancelParamName: "org.apache.struts.taglib.html.CANCEL",


    //> @attr dynamicForm.cancelParamValue (String : "cancel" : IRW)
    // The value of the special field sent to the server as part of +link{method:DynamicForm.cancel()}
    // @visibility external
    //<
    cancelParamValue: "cancel",

    //>    @attr    dynamicForm.action        (string : "#" : IRW)
    // The URL to which the form will submit its values.
    // <p>
    // <b>NOTE:</b> this is used only in the very rare case that a form is used to submit data
    // directly to a URL.  Normal server contact is through RPCManager.<br>
    // See +link{DynamicForm.canSubmit} for more on this.
    //
    // @see group:operations
    // @see class:RPCManager
    //
    //      @visibility external
    //      @group  submitting
    //<
    //    XXX SHOULD SUPPORT [APP], [ISOMORPHIC], etc. special directories
    // Note: if this property is modified from the class default, and saveData() is called,
    // the rpcManager code will perform its request as a direct submission to the action URL
    // by setting request.directSubmit
    action:"#",

    //>    @attr    dynamicForm.target        (string : null : IRWA)
    // The name of a window or frame that will receive the results returned by the form's
    // action. The default null indicates to use the current frame.
    // <p>
    // <b>NOTE:</b> this is used only in the very rare case that a form is used to submit data
    // directly to a URL.  Normal server contact is through
    // +link{group:dataBoundComponentMethods,DataBound Component Methods}.
    //      @group  submitting
    //      @visibility external
    //<

    //>    @attr    dynamicForm.method        (FormMethod : isc.DynamicForm.POST : [IRW])
    // The mechanism by which form data is sent to the action URL. See FormMethod type
    // for details.
    // <p>
    // <b>NOTE:</b> this is used only in the very rare case that a form is used to submit data
    // directly to a URL.  Normal server contact is through
    // +link{group:dataBoundComponentMethods,DataBound Component Methods}.
    //      @group  submitting
    //      @visibility external
    //<
    method:isc.DynamicForm.POST,

    //>    @attr    dynamicForm.encoding        (Encoding : DynamicForm.NORMAL : IRWA)
    // encoding for the form, use MULTIPART_ENCODING for file upload forms
    // @group submitting
    // @visibility external
    //<
    encoding:isc.DynamicForm.NORMAL_ENCODING,

    //>    @attr    dynamicForm.canSubmit        (Boolean : false : IRWA)
    // Governs whether this form will be used to perform a standard HTML form submission.
    // Note that if true, +link{DynamicForm.submit()} will perform a native HTML submission
    // to the specified +link{DynamicForm.action} URL.<br>
    // Wherever possible we strongly recommend using the
    // +link{group:dataBoundComponentMethods,DataBound Component Methods} to send data to
    // the server as they provide a far more sophisticated interface, with built in
    // options for server validation, required fields, etc.<br>
    // @group    submitting
    // @visibility external
    //<
    // Defaulted to false, as we usually do not want direct submission behavior.
    // Note: if true, and saveData() is called, the rpcManager code will perform its request
    // as a direct submission to the action URL by setting request.directSubmit

    // whether to write the <form> tag

    writeFormTag:true,


    //> @attr   dynamicForm.saveOnEnter (Boolean : false :IRW)
    // If <code>true</code>, when the user hits the Enter key while focused in a text-item in
    // this form, we automatically submit the form to the server using the
    // +link{dynamicForm.submit()} method.
    // @visibility external
    // @group submitting
    //<

    //>    @attr    dynamicForm.autoSendTarget        (boolean : false : IRWA)
    // Should we send the form target name to the server automatically?
    //        @group    submitting
    //<
    // if autoSendTarget is true, we automatically add a hidden field to the form that tells the
    // server the name of the target the form was submitting to.  This is useful for
    // reauthentication purposes.

    //>    @attr    dynamicForm.autoSendTargetFieldName        (string : "__target__" : IRWA)
    // Name of the field in which the form target will be set
    //        @group    submitting
    //<
    autoSendTargetFieldName:"__target__",

    // useNativeSelectItems
    // Determines whether items of type "select" or "SelectItem" should be rendered as
    // our ISC SelectItems or NativeSelectItems

    useNativeSelectItems:false,


    hideUsingDisplayNone: isc.Browser.isMoz && isc.Browser.isMac,


    //> @attr dynamicForm.operator (OperationId : "and" : IR)
    // When +link{formItem.operator} has been set for any +link{FormItem} in this form, what
    // logical operator should be applied across the +link{Criterion,criteria} produced by the form
    // items?  Only applicable to forms that have a +link{DataBoundComponent.dataSource,dataSource}.
    //
    // @visibility external
    //<
    operator: "and",


    //> @attr dynamicForm.showComplexFieldsRecursively (Boolean : null : IR)
    // If set, this <code>DynamicForm</code> will set both
    // +link{DataBoundComponent.showComplexFields,showComplexFields} and
    // <code>showComplexFieldsRecursively</code> on any nested component used for showing/editing
    // a complex field.  Thus any of this form's items that handle complex fields will themselves
    // also show complex fields.  This allows for handling of field structures of any complexity.
    // <p>
    // If set, this value automatically sets +link{DataBoundComponent.showComplexFields,showComplexFields}
    // as well.
    //
    // @visibility external
    //<

    //> @attr dynamicForm.nestedEditorType (String : "NestedEditorItem" : IRW)
    // +link{class:FormItem} class to use for any singular (ie, non-list) complex fields
    // on this DynamicForm.
    //
    // @see nestedListEditorType
    // @visibility external
    //<
    nestedEditorType: "NestedEditorItem",

    //> @attr dynamicForm.nestedListEditorType (String : "NestedListEditorItem" : IRW)
    // +link{class:FormItem} class to use for any list-type complex fields on this DynamicForm.
    // List-type fields are denoted by marking them <code>multiple: true</code> in the
    // DataSource.
    //
    // @see nestedEditorType
    // @visibility external
    //<
    nestedListEditorType: "NestedListEditorItem",

    canDropItems: false,
    canAddColumns: true

    //> @attr dynamicForm.dataFetchMode (FetchMode : "paged" : IRW)
    // @include dataBoundComponent.dataFetchMode
    //<

});

// add default methods
isc.DynamicForm.addMethods({


//---------------------------
//    Data initialization
//---------------------------


//>    @method    dynamicForm.initWidget()    (A)
//            initialize the form object
//
//            initializes th list of fields
//            sets up the data (if specified)
//            clears the errors array
//
//        @param    [all arguments]    (object)    objects with properties to override from default
//<
initWidget : function () {
    if (isc._traceMarkers) arguments.__this = this;

    if (!isc.DynamicForm._operatorIndex) isc.DynamicForm.buildOperatorIndex();

    // does String -> Array conversion if needed
    this.setColWidths(this.colWidths);

    // call the superclass function
    this.Super("initWidget",arguments);

    // Set this-level showComplexFields if showComplexFieldsRecursively has been set
    if (this.showComplexFieldsRecursively) this.showComplexFields = true;

    // allow for fields instead of items specification
    if (this.fields && this.items == null) this.items = this.fields;

    // If we have a set of 'defaultItems' in an array, and the developer hasn't set the items
    // property, use the defaultItems array instead.
    // Notes:
    // - The 'defaultItems' property would typically be set on the instance prototype this class
    //   (or subclasses).
    // - In each instance we *copy* the defaultItems array into this.items, and avoid manipulating
    //   it directly.  This means specific instances will not write properties out into the
    //   instance prototype's 'defaultItems' array (which would happen if manipulated directly as
    //   it is passed by reference to each instance, so all instances point to the same object)
    // When creating a DynamicForm subclass, for which each instance should show a specific set
    // of items by default, the defaultItems property should be set on the instance prototype.
    // Settting the items property directly on the instance prototype is a bad idea as each
    // instance will then point to the same items array.
    // (Used in Editor.js)
    if (this.defaultItems != null && this.items == null) {
        this.items = [];
        for (var i = 0; i < this.defaultItems.length; i++) {
            this.items[i] = isc.addProperties({}, this.defaultItems[i]);
        }
    }

    // Default values to an empty list.
    if (this.values == null) this.values = {};

    // explicitly call setAction() if the action has been overridden so we set the explicitAction
    // flag
    if (this.action != isc.DynamicForm.getPrototype().action &&
        this.action != null && !isc.isA.emptyString(this.action))
    {
        this.setAction(this.action);
    }


    if (this.valuesManager != null) {
        // If we have a valuesManager and it is a string, check if it's a global ID for a VM
        // and use that - otherwise, initializeValuesManager() will auto-create it later
        if (isc.isA.String(this.valuesManager)) {
            if (window[this.valuesManager]) this.valuesManager = window[this.valuesManager];
        }

        if (isc.isA.ValuesManager(this.valuesManager)) {
            if (this.dataSource == null && this.valuesManager.dataSource != null) {
                this.dataSource = this.valuesManager.dataSource;
            }
        }
    }

    // If the form or any of its items specify dataPath, but not dataSource, this implies that
    // the form will later be rebound.  This introduces all sorts of implications because the
    // field properties should now inherit from the corresponding dataSource field.  So, we
    // must hold onto the original field config so the rebinding process can use it.

    if (!this.dataSource) {
        var items = this.items || [];
        for (var i = 0; i < items.length; i++) {
            if (items[i] == null) continue;
            if (this.dataPath || items[i].dataPath) {
                this._itemsConfig = isc.shallowClone(items);
                break;
            }
        }
    }


    // initialize the list of fields, defaulting to an empty list
    // Note: We set up the items (and set their values / eval defaultDynamicValue) at Form init
    // time so that a developer can define a form and then work with the items before drawing the
    // form using the standard form item APIs.
    // This is in contrast to the approach used (for example) in the ListGrid, where the component
    // parts of the LG (header, body, etc.) are not created until draw in order to minimize the
    // cost associated with changing the dataSource / data /etc. while the widget is undrawn.
    this.setItems(this.items ? this.items : [], true);

    // If we've been marked as disabled explicitly disable all form items.
    if (this.isDisabled()) {
        this.setDisabled(true);
    }

    // intialize the form errors, defaulting to an empty list
    this.setErrors(this.errors ? this.errors : {});

    // initialize the form values, via 'setValues()'
    // this automatically remembers the old values for us as well
    this.setValues(this.values, true);

    // If we have a selectionComponent, call the setter method to set up observation of selection
    if (this.selectionComponent != null) this.setSelectionComponent(this.selectionComponent,true);

},

_destroyItems : function (items) {
    if (!items) return;
    if (!isc.isA.FormItem(items[0])) return;
    items.map("destroy");

    this.destroyOrphanedItems("containing form destroyed");
},

destroy : function () {
    if (this.valuesManager && this.valuesManager.removeMember) {
        this.valuesManager.removeMember(this);
    }
    this._destroyItems(this.items);
    this.Super("destroy", arguments);
},

// Override 'setHandleDisabled' to disable / enable all items
setHandleDisabled : function (disabled) {
    if (this.isDrawn()) {
        if (this.redrawOnDisable) this.markForRedraw("setDisabled");
        this.disableKeyboardEvents(disabled, null, true);
    }

    var items = this.getItems();
    for (var i = 0; i < items.length; i ++) {

        items[i].updateDisabled(true);
    }
},

disableKeyboardEvents : function (disabled, recursive, disablingForm) {
    this.Super("disableKeyboardEvents", arguments);
    // by default disabling the form will also disable all items within it (no need to explicitly
    // suppress keyboard access to them)
    // If the form is not being disabled but just having keyboard access suppressed (EG for
    // a clickMask), notify the form items individually
    if (!disablingForm) {

        if (disabled) {
            this._enabledTI = this.getTabIndex();
            this._setTabIndex(-1);
        } else {
            this._setTabIndex(this._enabledTI);
        }
    }
},

//>    @method    dynamicForm.applyFieldDefaults()
//        @group    data
//         Selects the appropriate form item type for fields if not specified,
//         based on schema information.
//<
applyFieldDefaults : function (fields) {
    if (fields == null) return;

    for (var i = 0; i < fields.length; i++) {
        var field = fields[i];

        // This null check will avoid JS errors if someone defines an array of fields with
        // a trailing comma in IE
        if (field == null) return;

    }
},

//>    @method dynamicForm.getEditorType()  ([A])
//
// Returns the form item type (Class Name) to be created for some field.<br>
// By default <code>field.editorType</code> will be used if present - otherwise backs off to
// deriving the appropriate form item type from the data type of the field (see
// +link{type:FormItemType} for details.
//
//  @group  editing
//
//  @param  field   (object)    field definition for which we are deriving form item type.
//  @return         (string)  form item type for the field
//  @visibility external
//<
getEditorType : function (field) {
    return this.getClass().getEditorType(field, this);
},

//>    @method    dynamicForm.setItems()
// Synonym for +link{DynamicForm.setFields()}
//
// @group elements
// @param itemList        (Array of FormItem Properties)    list of new items to show in the form
// @visibility external
//<
setItems : function (itemList, firstInit) {
    // mark any items which had explicitly defined types, so we don't override them with our logic
    // for picking default types
    if (itemList != null) {
        for (var i = 0; i < itemList.length; i++) {
            var invalidItem = false;
            if (itemList[i] == null) {
                this.logWarn("Encountered empty entry in items array - removing this entry.")
                invalidItem = true;
            }
            if (isc.isA.Canvas(itemList[i])) {
                this.logWarn("Encountered a Canvas instance:" + itemList[i] + " in the items " +
                             "array - the DynamicForm items array should contain only FormItem " +
                             "definitions. Removing this entry.");
                 invalidItem = true;
            }
            if (invalidItem) {
                itemList.removeAt(i);
                i -= 1;
            }
        }
    }

    // get field data by binding to a DataSource, if we were provided one.  NOTE we do this first
    // because the returned list of items may be a new list
    itemList = this.bindToDataSource(itemList);
    //this.logWarn("itemList is : " + this.echo(itemList) +
//                  ", this.items is : " + this.echo(this.items) + "\n\n" + this.getStackTrace());
    if (!itemList) itemList = [];
    // If the itemList passed in is the same array object as this.items, duplicate it, as
    // the removeItems call (below) will clear out that array.
    else if (itemList == this.items) itemList = itemList.duplicate();

    // remove all existing items (destroy FormItem objects we created)
    if (this.items != null && this.items.length > 0 && !firstInit) this.removeItems(this.items);

    this._addItems(itemList, null, true, firstInit);
},

//>    @method    dynamicForm.setFields()
// Set the +link{dynamicForm.fields,items} for this DynamicForm.  Takes an array of item
// definitions, which will be converted to +link{FormItem}s and displayed in the form.
// <P>
// <var class="SmartClient">
// Note: Do not attempt to create +link{FormItem} instances directly. This method should be
// passed the raw properties for each item only.
// </var>
// <P>
// Objects passed to <code>setFields()</code> may not be reused in other forms and may not be
// used in subsequent calls to <code>setFields()</code> with the same form, new objects must be
// created instead.
// <P>
// To create a form where some items are conditionally present, rather than repeated calls to
// <code>setFields()</code> or <code>setItems()</code>, you should generally use
// +link{formItem.hide()} and +link{formItem.show()} and/or +link{formItem.showIf} rather than
// calling <code>setItems() or setFields()</code>.  <code>setItems()</code> and
// <code>setFields()</code> are appropriate for dynamically generated forms where there are
// few if any items that are the same each time the form is used.
//
// @param itemList        (Array of FormItem Properties)    list of new items to show in the form
// @group elements
// @visibility external
//<
setFields : function (fieldList) {
    this.setItems(fieldList);
},

//>    @method    dynamicForm.getFields()
// Method to retrieve the +link{dynamicForm.fields, items} for this DynamicForm.
//
// @return (Array of FormItem)
//
// @group elements
// @visibility external
//<
getFields : function () {
    return this.items;
},

//>    @method    dynamicForm.getItems()
// Method to retrieve the +link{dynamicForm.fields, items} for this DynamicForm.
//
// @return (Array of FormItem)
// @group elements
// @visibility external
//<
getItems : function () {
    return this.items;
},

// Override visibleAtPoint to return true if we have any items contained in containerWidgets
// which would be visible at the specified point.

visibleAtPoint : function (x, y, withinViewport, ignoreWidgets) {

    if (this.invokeSuper(isc.DynamicForm, "visibleAtPoint", x,y,withinViewport,ignoreWidgets))
        return true;


    var items = this.items || [],
        containerWidgets = {},
        focusItemIndex = items.indexOf(this.getFocusSubItem());

    for (var i = -1; i < items.length; i++) {

        var itemIndex = i;
        if (i == -1) {
            itemIndex = focusItemIndex;
        // avoid checking the focus item twice
        } else if (itemIndex == focusItemIndex) continue;

        // Catch the case where we had no focusItem;
        if (itemIndex == -1) continue;
        var item = items[itemIndex],
            cw = item.containerWidget;
        if (cw == this || !item.isDrawn() || !item.isVisible()) continue;


        var cwID = cw.getID();
        if (containerWidgets[cwID] == null) {
            containerWidgets[cwID] = cw.visibleAtPoint(x,y,withinViewport, ignoreWidgets);
        }
        if (!containerWidgets[cwID]) continue;


        var PL = item.getPageLeft(),
            PT = item.getPageTop();
        if (PL <= x && (PL + item.getVisibleWidth()) >= x && PT <= y && (PT + item.getVisibleHeight()) >= y) {
            return true;
        }
    }

    return false;
},

// addItems - slot new items into the appropriate position in the items in this DynamicForm

addItems : function (newItems, position) {
    if (!isc.isAn.Array(newItems)) newItems = [newItems];
    if (this.dataSource) {
        var ds = isc.DS.get(this.dataSource);
        for (var i = 0; i < newItems.length; i++) {

            newItems[i] = this.combineFieldData(newItems[i]);

            // on name collision, remove the old item.

            var itemName = newItems[i].name;
            if (itemName && this.getItem(itemName)) {
                this.removeItem(itemName);
            }


        }
    }
    this.addFieldValidators(newItems);
    if (position == null || position > this.items.length) position = this.items.length;

    this._addItems(newItems, position);
},


_$upload : "upload",_$uploadItem:"UploadItem", _$tUploadItem:"TUploadItem",
_$mutex:"mutex",
_addItems : function (newItems, position, fromSetItems, firstInit) {
    // adding items will almost always change the tab-index-span used by the form
    // If this increases, we need to catch the case where the tabIndex of our items overlaps
    // the next widget on the page
    var drawn = this.isDrawn(),
        oldSpan = drawn ? this.getTabIndexSpan() : null;

    //this.logWarn("addItems: " + this.echoAll(newItems));

    // apply type-based field defaults to the items passed in
    // Note: this will not change the type of an already-instantiated form item, so we do this
    // before converting the items init objects to FormItems
    this.applyFieldDefaults(newItems);

    var sectionItems = [];

    // iterate through all the items, creating FormItems from object literals
    var haveUploadFields = false,
        foundFileItem = false,
        mutexSections = (this.sectionVisibilityMode == this._$mutex);

    for (var itemNum = 0; itemNum < newItems.length; itemNum++) {
        var item = newItems[itemNum];

        // remove any empty items from the list
        if (!item) {
            newItems.removeItem(itemNum);
            itemNum--;
            continue;
        }


        var canEditItem = isc.DynamicForm.canEditField(item, this);
        if (!canEditItem && item.readOnlyEditorProperties) {
            item = isc.addProperties({}, item, item.readOnlyEditorProperties);
        } else if (item.editorProperties) {
            item = isc.addProperties({}, item, item.editorProperties);
        }

        var itemType = this.getEditorType(item);
        newItems[itemNum] = item = this.createItem(item, itemType);

        if (itemType == this._$upload || itemType == this._$uploadItem ||
                itemType == this._$tUploadItem)
        {
            haveUploadFields = true;
        }

        if (isc.FileItem && isc.isA.FileItem(item) && foundFileItem) {
            this.logWarn("Attempting to creating a form with multiple FileItems. This is " +
                         "not currently supported - only the first file type field value will " +
                         "be committed on submission of this form.");
        }

        // add to list of form sections that should start out hidden
        if (isc.isA.SectionItem(item)) {
            sectionItems.add(item);
            // remember the last visible section for mutex operation

            if (item.sectionExpanded && mutexSections)
                this._lastExpandedSection = item;
        }
    }

    // Actually store the items in this.items

    if (fromSetItems) this.items = newItems
    else this.items.addListAt(newItems, position);


    if (!firstInit) {
        this.setItemValues(this.getValues(), false, true, newItems);
    }

    // enable multipart encoding if upload fields are included
    // NOTE: imperfect: we aren't detecting all the ways you can include an UploadItem, eg
    // editorType:"UploadItem" isn't caught, neither would any subclasses be.
    if (haveUploadFields) this.encoding = isc.DynamicForm.MULTIPART_ENCODING;

    for (var i = 0; i < sectionItems.length; i++) {
        var sectionItem = sectionItems[i],
            isVisible = sectionItem.sectionExpanded;


            if (isVisible && (!mutexSections || (this._lastExpandedSection == sectionItem))) {
                // call expandSection on visible items to ensure that sections defined with an
                // inline items array have added their items to the form.
                sectionItem.expandSection();
            } else {
                // hide form sections for section items that have sectionExpanded property set
                // to false
                // do this as separate for loop to ensure that all form items to be hidden have
                // been initialized
                sectionItem.collapseSection();
            }
    }

    // set the _itemsChanged flag so we recalculate the layout
    this._itemsChanged = true;

    // If necessary, shift the next widget's tabIndex forward to make room for our new items.
    var tabIndex = this.getTabIndex();
    if (drawn && tabIndex != -1) {
        // we have to explicitly call _assignTabIndices here so that getTabIndexSpan() will
        // return an updated value. Normally the items' tabIndices are assigned when
        // 'getTabIndex()' is called on them, which wouldn't happen until getInnerHTML() from
        // the delayed redraw (below).
        this._assignTabIndices();
        var span = this.getTabIndexSpan();
        if (span > oldSpan) {
            var nextWidget = this._getNextTabWidget();
            if (nextWidget) {
                var nextWidgetIndex = nextWidget.getTabIndex();
                if (nextWidgetIndex < (tabIndex+ span)) {
                    nextWidget._shiftTabIndexForward((tabIndex + span) - nextWidgetIndex);
                }
            }
        }
    }

    this.markForRedraw("Form items added");

},

_knownProps : ["name", "editorType", "readOnlyEditorType", "type",
               "valueMap", "defaultValue", "showTitle",
               "left", "top", "width", "height"],
copyKnownProperties : function (target, props, propNames) {
    var undef;
    for (var i = 0; i < propNames.length; i++) {
        var propName = propNames[i],
            value = props[propName];
        if (value !== undef) {
            target[propName] = value;
            delete props[propName];
        }
    }
},
createItem : function (item, type) {
    // We may want to support having the user specify which form an item belongs to before it
    // is initialized as a FormItem instance.  (The specified form will then handle values
    // management, etc.)
    // However this is not currently supported - we'll always have form items point back to the
    // form that created them.
    // (Note: We may want a customizable 'formProperty' property, rather than hardcoding the
    // "form" property)
    if (item.form != null && !(item.form == this.getID() || item.form != this)) {
        this.logWarn("Unsupported 'form' property [" + item.form + "] set on item:" +
                      item + ".  Ignoring.");
    }

    if (item.destroyed && isc.isA.FormItem(item)) {
        this.logWarn("destroyed FormItem passed to setItems()/addItem(): FormItems cannot be " +
                     "re-used with different DynamicForms");
    }

    // convert from a simple object into a FormItem
    var className = isc.FormItemFactory.getItemClassName(item, type, this),
        classObject = isc.FormItemFactory.getItemClass(className);

    if (!classObject) {
        this.logWarn("Problem initializing item: "+isc.Log.echo(item)
                    +" - derived FormItem class is: "+className
                    +".  Please make sure the relevant module is loaded");
        return;
    }

    // If the classObject is an SGWTFactory, then our type actually pointed
    // to a SmartGWT class, not a SmartClient class. In that case, we need
    // to figure out what SmartClient class to create! We can't just call
    // SGWTFactory.create() in the usual way, because the SGWT side of FormItem
    // only creates a properties block on initialization, and that's what we've
    // got already ... we need to turn it into a real SC FormItem, and this is
    // the only place where that happens.
    if (isc.SGWTFactory && isc.isA.SGWTFactory(classObject)) {
        // First, create the desired SGWT FormItem object. We supply the
        // properties block in case there is something there that is really an
        // SGWT property ...  this allows the SGWT side to define a new
        // property and have it picked up on creation. SGWTFactory will set
        // uknown properties on the JavaScriptObject we get back. Thus, in the
        // ordinary case, we get back a copy of what we put in, but backed by a
        // SmartGWT FormItem.
        //
        // We delete the editorType if supplied with the item, since we want to
        // pick that up from SGWT -- we don't want to clobber what SGWT is about
        // to tell us.
        var config = item;
        if (config.editorType) {
            config = isc.addProperties({}, item);
            delete config.editorType;
        }
        item = classObject.create(config);

        // Now, what we have is a normal situation, with a properties block
        // that is backed by a SmartGWT FormItem. So, just call ourselves
        // recursively with the correct type, and everything should happen
        // jut as it should.
        return this.createItem(item, item.editorType);
    }

    var itemConfig = item;

    item = classObject.createRaw();

    // set up a pointer back to this form, and to the containerWidget, which might be a
    // different widget, eg a grid doing inline editing.
    // Note: several FormItem methods assume item.form will be set before init() is called.
    // CanvasItems at least need containerWidget in init as well.
    // set this up as the item's eventParent (for ISC bubbling)
    item.form = item.containerWidget = item.eventParent = this;


    var baseValidators = null;
    if (item["validators"] != null && itemConfig["validators"] != null) {
        baseValidators = item.validators;
    }


    if (isc.Browser.isIE && this.canAlterItems) {
        this.copyKnownProperties(item, itemConfig, this._knownProps);
    }

    if (this.autoChildItems) {
        // use the autoChild system to instantiate items with FormItem class-specific defaults


        // ensure an auto-ID is not assigned by the autoChild system
        if (item.ID == null) item.ID = null;

        this._completeCreationWithDefaults(classObject.Class, item, itemConfig);
    } else {
        //this.logWarn("item: " + this.echoLeaf(item) + ", item.form is: " + item.form +
        //             ", itemConfig is: " + this.echo(itemConfig));
        item.completeCreation(itemConfig);

        if (baseValidators != null) {
            // Add base validator(s) to item
            if (!item.validators) {
                item.validators = baseValidators;
            } else {
                if (!isc.isAn.Array(item.validators)) {
                    item.validators = [item.validators];
                }
                // if the field is using the shared, default validators for the type,
                // make a copy before modifying
                if (item.validators._typeValidators) {
                    item.validators = item.validators.duplicate();
                }
                item.validators.addList(baseValidators);
            }
        }
    }


    item.form = this;
    if (item.destroyed) item.destroyed = false;

    // Log a warning if this item has no name, but is expected to save values
    // See comment in formItem.js next to the 'shouldSaveValue' property declaration.
    // (Note: we could put this check into FormItem.init)
    if (item.shouldSaveValue &&
        (item[this.fieldIdProperty] == null ||
         isc.isAn.emptyString(item[this.fieldIdProperty])) &&
        (item.dataPath == null || isc.isAn.emptyString(item.dataPath))
        )
    {

        // 'shouldSaveValue' is a property denoting whether this item should be included
        // in the form's values object.
        // False by default for non-data items.
        this.logWarn(item.getClass() + " form item defined with no '" +
                     this.fieldIdProperty + "' property - Value will not be saved." +
                     " To explicitly exclude a form item from the set of values to " +
                     "be saved, set 'shouldSaveValue' to false for this item.")

        item.shouldSaveValue = false;
    }

    return item;
},

//>    @method    dynamicForm.removeItems()
// Removes some items from this form.
// Marks form to be redrawn.
//
//        @group    elements
//        @param    items   (object[])  list of form items to remove from the form
//<
removeItems : function (items) {
    if (items == null) return;

    if (!isc.isAn.Array(items)) items = [items];

    // If passed this.items, duplicate it - we want to be able to manipulate this.items without
    // changing the array passed in.
    if (items == this.items) items = this.items.duplicate();

    items = this.map("getItem", items);

    this.items.removeList(items);

    if (this._orphanedItems == null) {
        this._orphanedItems = [];
    }

    // If any item(s) had advanced criteria, apply them to our "extraAdvancedCriteria"
    // object so we can continue to return the right thing from getValuesAsCriteria()
    // (If an item with the same name is reintroduced, we'll also update from
    // the extraAdvancedCriteria object)
    for (var i = 0 ; i < items.length; i++) {
        if (item == null) continue;
        if (items[i].hasAdvancedCriteria()) {
            var crit = items[i].getCriterion();

            if (crit != null) {
                if (this._extraAdvancedCriteria == null) {
                    this._extraAdvancedCriteria = {
                        _constructor:"AdvancedCriteria",
                        operator:"and",
                        criteria:[]
                    }
                }
                this._extraAdvancedCriteria.criteria.add(crit);
                // Also clear off "values" so we don't assemble into
                // the criteria object as part of "getValuesAsCriteria()" in addition
                // to the stored out advanced criteria
                delete this.values[items[i].name];
            }
        }
    }

    // if we've removed any items from this form, destroy() them too
    for (var i = 0; i < items.length; i++) {
        var item = items[i];

        // bad item name passed in, getItem() failed
        if (item == null) continue;

        // If this has sub-items, slot them in after this item in the items array
        if (item.items != null) {
            items.addList(item.items, i+1);
        }

        // don't leave a pointer to a destroyed focus item.
        if (this._focusItem == item) delete this._focusItem;


        if (!this.items.contains(item) && isc.isA.FormItem(item)) {
            if (this.isDrawn()) {

                if (item._destroyCanvas) item._destroyCanvas();
                this._orphanedItems.add(item);
            } else {
                item.destroy();
            }
        }
    }

    // set the _itemsChanged flag so we recalculate the layout
    this._itemsChanged = true;
    this.markForRedraw("Form items removed")
},

// canvas overrides
addField : function (field, position) { this.addItems(field, position) },
removeField : function (field) { this.removeItems(field); },

// obvious synonyms for single items
addItem : function (item, position) { this.addItems(item, position); },
removeItem : function (item) { this.removeItems(item); },

// Synonymous addFields / removeFields methods for completeness
addFields : function (items, pos) {
    return this.addItems(items, pos);
},
removeFields : function (items) {
    return this.removeItems(items);
},


// tabIndex management
// ---------------------------------------------------------------------------------------

// Widget level _canFocus
// If this method returns false we will not get keyboard events on the form.
// Therefore check for our items' _canFocus() instead.
// Only respect canFocus:false if we have no focusable items
_canFocus : function (a,b,c,d) {
    // shortcut: allow canFocus:true
    if (this.canFocus == true) return true;
    var items = this.getItems();
    for (var i = 0; i < items.length; i++) {
        if (items[i]._canFocus()) return true;
    }

    return this.invokeSuper(isc.DynamicForm, "_canFocus", a,b,c,d);
},


// Assign ascending tabIndices to form items with no explicitly assigned tab-index.

_assignTabIndices : function () {
    var items = this.items;
    if (!items || items.length == 0) return;

    // We want to ensure the auto-allocated tabIndices don't collide with the explicitly
    // specified index of some other form item, so we can't just use items.indexOf(item) for
    // each item.
    var explicitTabIndexArray = [], warnedTIs = {};
    for (var i = 0; i < items.length; i++) {

        var item = items[i], ti = item.tabIndex;
        if (ti != null && ti != -1) {
            // Warn if we have explicit tabIndices that collide

            if (explicitTabIndexArray[ti] != null && !warnedTIs[ti]) {
                this.logWarn("More than one item in this form have an explicitly specified tabIndex of '"
                            + ti + "'. Tab order cannot be guaranteed within this form.");
                // avoid warning over and over for the same tab index.
                warnedTIs[ti] = true;
            }
            // Making a sparse array of previously assigned tabIndices.
            explicitTabIndexArray[ti] = item;
        }
    }

    // iterate through a second time actually setting up the local tabIndices
    // We'll do this by setting the local tabIndex to the index in the items array offset by
    // any tab-indices already explicitly populated.
    // (Start with an offset of 1 - we want to use 1-based rather than 0-based tab indices for
    // simplicity)
    var tabIndexOffset = 1;
    for (var i = 0; i < items.length; i++) {
        var item = items[i];
        // Don't increment the next tabIndex if:
        // - this item has not yet been initialized
        // - this item already has an explicit tabIndex
        // - it can't receive focus

        if (!isc.isA.FormItem(item)) {
            if (this.logIsDebugEnabled())
                this.logDebug("_assignTabIndices() fired before all form items have been initialized"
                             + this.getStackTrace());

            continue;
        }
        if (!item._canFocus() || item.tabIndex != null || item.globalTabIndex != null) {
            continue;
        }
        tabIndexOffset += 1;
        // Avoid colliding with explicitly specified local tab indices
        while (explicitTabIndexArray[tabIndexOffset] != null) {
            tabIndexOffset += 1;
        }
        item._localTabIndex = tabIndexOffset;
        if (isc.isA.CanvasItem(item)) {
            var canvas = item.canvas;
            if (canvas && canvas.getTabIndexSpan) {
                tabIndexOffset += canvas.getTabIndexSpan();
            }
        }

    }

},

// Have _slotChildrenIntoTabOrder() no-op - our children come from CanvasItems and we're already
// managing their tab indices
_slotChildrenIntoTabOrder : function () {
    return;
},

// We will take up multiple slots in the page's tab order due to our set of items
// We're not concerned about items with an explicitly specified global tab index - they won't
// take up any slots next to the form itself.
getTabIndexSpan : function () {
    var items = this.items;
    // Even though we wont really take up a slot if we have no items, never allow
    // our tabIndexSpan to be 0.
    var slots = 1;
    if (!items) {
        return slots;
    }

    for (var i = 0; i < items.length; i++) {
        var item = items[i];

        if (!isc.isA.FormItem(item)) {
            return items.length;
        }

        if (!item._canFocus() || item.globalTabIndex != null) {
            continue;
        }
        var tabIndex = item.tabIndex || item._localTabIndex;
        if (tabIndex == null) {
            this._assignTabIndices();
            tabIndex = item._localTabIndex;
        }
        if (isc.isA.CanvasItem(item)) {
            var canvas = item.canvas,
                canvasTISpan = 0;
            if (canvas && canvas.getTabIndexSpan) canvasTISpan = canvas.getTabIndexSpan();

            tabIndex += canvasTISpan;
        }
        if (tabIndex != null && tabIndex > slots) slots = tabIndex;
    }
    return slots;
},


// When the tabIndex changes, notify form items - since their tab indices are most likely to be
// local
_setTabIndex : function () {
    this.Super("_setTabIndex", arguments);
    if (this.items) {
        for (var i = 0; i < this.items.length; i++) {
            // If we've never been drawn and haven't instantiated our items skip this
            if (!isc.isA.FormItem(this.items[i])) continue;
            this.items[i].updateTabIndex();
        }
    }
},

// Item notifications
// ---------------------------------------------------------------------------------------

// Whenever this DynamicForm is moved, notify all the items that they have been moved.

handleMoved : function (a,b,c,d) {
    this.invokeSuper(isc.DynamicForm, "handleMoved", a,b,c,d);
    this.itemsMoved();
},

handleParentMoved : function (a,b,c,d) {
    this.invokeSuper(isc.DynamicForm, "handleParentMoved", a,b,c,d);
    this.itemsMoved();
},

// Also notify the items if the zIndex is modified
zIndexChanged : function (a,b,c,d) {
    this.invokeSuper(isc.DynamicForm, "zIndexChanged", a,b,c,d);
    this.itemsZIndexChanged();
},

parentZIndexChanged : function (a,b,c,d) {
    this.invokeSuper(isc.DynamicForm, "parentZIndexChanged", a,b,c,d);
    this.itemsZIndexChanged();
},


// Since the container widget for form items manages their position / HTML we need to fire
// a notification function to let them know if they have moved.
// itemsMoved is a helper method to fire 'moved()' on each item in this form.
itemsMoved : function () {
    var items = this.getItems();
    if (!items) return;
    for (var i = 0; i < items.length; i++) {
        if (items[i].isVisible) items[i].moved();
    }
},

// When our visibility changes, notify all our items of the visibility change.

itemsVisibilityChanged : function () {
    var items = this.getItems();
    if (!items) return;
    for (var i = 0; i < items.length; i++) {
        if (items[i].visibilityChanged) items[i].visibilityChanged();
    }
},

itemsZIndexChanged : function () {
    var items = this.getItems();
    if (!items) return;
    for (var i = 0; i < items.length; i++) {
        items[i].zIndexChanged();
    }
},

// Override scrollTo to notify our form items that they have moved.
scrollTo : function (left, top, reason) {
    var oldLeft = this.getScrollLeft(),
        oldTop = this.getScrollTop();

    this.Super("scrollTo", arguments);

    // If the scroll position changed, notify our form items that they have moved.
    if (oldLeft != this.getScrollLeft() || oldTop != this.getScrollTop()) this.itemsMoved();
},

//>Animation
// We override scrollTo() which normally causes _canAnimateClip to return false but there's no
// reason for us not to support animateShow() / animateHide() in DynamicForms, so override
// _canAnimateClip to explicitly return true (unless 'canAnimateClip' is set)
_canAnimateClip : function () {
    if (this.canAnimateClip != null) return this.canAnimateClip;
    return true;
},
//<Animation

//> @method dynamicForm.setTitleOrientation ()
// Modify this form's +link{titleOrientation} at runtime
// @param (TitleOrientation) new default item titleOrientation
// @group  formTitles
// @visibility external
// @example formLayoutTitles
//<
setTitleOrientation : function (orientation) {
    this.titleOrientation = orientation;
    this._itemsChanged = true;
    this.markForRedraw();
},

// EditMode setters
// ---------------------------------------------------------------------------------------

//>EditMode

setNumCols : function (numCols) {
    this.numCols = numCols;
    this._itemsChanged = true;
    this.markForRedraw();
},
//<EditMode


// AutoComplete
// --------------------------------------------------------------------------------------------

//> @method dynamicForm.setAutoComplete()
// Change the autoCompletion mode for the form as a whole.
//
// @param   newSetting (AutoComplete)  new setting
// @group autoComplete
// @visibility autoComplete
//<
setAutoComplete : function (newSetting) {
    this.autoComplete = newSetting;
    // have items change mode if applicable
    for (var i = 0; i < this.items.length; i++) {
        this.items[i]._handleAutoCompleteChange();
    }
},

/////////
// Form Values handling
// --------------------------------------------------------------------------------------------
//
// From a developers' point of view:
//  - You can initialize a form with form.values set (an array of field / value pairs).
//    - you can include fields that are not in the items array for the form.
//
//  - You can retrieve the entire set of values via form.getValues();
//    - this is basically this.values, so includes values set via setValues() that don't have
//      an associated form item.
//    - In theory this will always show you the visible value in each form element (value-mapped
//      back to the appropriate raw value if applicable).
//
//  - You can set this.values with a call to setValues()
//    - again you can include fields that are not in the items array for the form.
//    - the form will be redrawn to show the changes in the actual form elements
//
//  - form.resetValues() will reset the values to the last values set programmatically via
//    form.setValues or form.setValue();
//
//  - form.clearValues() will set this.values to {}
//    - for form items with a defaultValue or defaultDynamicValue, this will be respected in this
//      case.
//
//  - You can set the value for an individual form item via "form.setValue(item, value);" or
//    "form.getItem(itemName).setValue(value)"
//  - You can retrieve the value for an individual form item via form.getValue(item), or
//    form.getItem(itemName).getValue();
//      - the value retrieved by these getter methods will be determined by looking at the
//        stored formItem._value (set on every 'change' event) first.  If that is not present,
//        this method will fall through to form.getSavedItemValue() which will look for the value
//        in the form.values array, and if it's not there return the default value for the item.
//  These four methods do not allow you to set values in the form.values array for fields that
//  are not included as actual form items.
//
//
// Internally:
//  There are several sets of values to consider:
//  - form.values - the values we return to the user from getValues() calls - should always be in
//    synch with the form item element values, but may include fields that are not in the set of
//    form items.
//  - form._oldValues - which is set up via form.rememberValues().
//    This is used for resetting values on an explicit call to resetValues(), or after a
//    failed validation attempt.
//    form.rememberValues() is called every time a form value is set programattically - from
//    setValues() and setValue() calls.
//  - formItem._value.  This is the FormItem's internal representation of the form item value.
//    it is updated whenever the value is saved, so on programatic 'setValue()', on change (and
//    keypress for some widgets).
//    Only used by code in FormItem.js (the form knows nothing each formItem's _value property).
//    Returned by FormItem.getValue().
//    Note - We store _oldValues on the form rather than on each item because:
//    - Having form._oldValues rather than just formItem._oldValue for each item allows us to store
//      values for non-form item fields
//  - The value displayed in the html element for each form item.  This differs from formItem._value
//    in a couple of ways:
//      - for form elements that have valueMaps, the display value will not match the "data" value
//      - form elements grouped into a container where there are multiple form elements for one
//        logical value (such as date items).
//      - Anything where 'mapValueToDisplay()' and 'mapDisplayToValue()' is non trivial (allowing
//        checkboxes to represent values other than true and false, for example)
//      Important:
//      - The value displayed in the element can be out of synch with the _value for a form item,
//        for example while typing in a form item with 'changeOnKeypress' set to false (such as the
//        time item).  The form item is responsible for updating it's _value whenever appropriate
//        via the 'updateValue()' method, as the APIs to get directly at the value stored in the
//        element are not public.
//        *One case where it may not be in synch is items which have to validate / or reformat their
//         element values to , such as time items and date items.
//         If a user is in the process of entering a time into a Time itme, the element may display
//         "1:", but the _value will not be updated (and saved in the form item values) until the
//         change handler fires on the element, meaning we won't be interfering with a user's typing
//         by attempting to verify the time on every keypress.
//         In this case, if a developer was to call 'getValue()' on a time item while focus was
//         still in that item (and the user theoretically still typing), the stored time value
//         would be returned, rather that attempting to parse the partially typed value.
//
//  - formItem.defaultValue and formItem.defaultDynamicValue.
//    - whenever an item's value is programmatically set to null, the appropriate default value will
//      be applied to the form item.
//
//  form.values is updated in the following places:
//      - form.setValues().
//        - Sets this.values to the object passed in,
//        - Saves the values in this._oldValues
//        - Calls 'setItemValues()' to take care of updating the values for each form item.
//        - Redraws the form to re-evaluate show-ifs
//        Called by:
//          - init() - call to this.setValues() with this.values or {}.
//          - this.clearValues() - falls through to this.setValues({});
//          - this.resetValues() - falls through to this.setValues(this._oldValues);
//
//      - this.saveItemValue() (Basically used to keep form.values in synch with the values for each
//        form item).
//        - Updates this.values[item] for an item.
//        - Clears the '_valueIsDirty' flag for the form item
//        Called from:
//          - form.elementChanged() (fired from an item's native change handler)
//          - item.handleKeyPress() (fired from a text / textArea item's keyPress handler)
//          - form.getValues() - if the current focus item is marked as dirty, this.values[...] for
//            the item will be updated to match the element value for the dirty form item.  (Other
//            form items than the focus item should not be out of synch because of the
//            elementChanged call to this method above).  Form items are marked as dirty via an
//            '_valueIsDirty' flag, which is set on keyDown in text / textArea type fields only.
//          - item.setValue() - which is called by form.setValue(item, value)
//
//  form._oldValues is updated when form.setValues(), formItem.setValue(), or form.setValue() is
//  called.
//
//  formItem._value (and form.values[item]) are updated via 'formItem.saveValue(newValue)'.
//  This method is called on formItem.setValue() [programmatically updating a form item's value], or
//  formItem.updateValue(), which is called as a result of the native onchange handler for form
//  items as well as the onclick handler for checkbox / radio items, and the onkeypress handler for
//  text items (where changeOnKeypress is true).
//  When these values are updated as a result of user interaction, the change handler will always
//  fire first (due to 'updateValue()').
//
//  The values displayed in the HTML form elements (and sub-elements) is updated by
//  form.setItemValues() and formItem.setElementValue().  Every method that can effect the value
//  of a form item should fall through to these, or force a form redraw (which will also update the
//  values displayed).
//
//  Additional methods on the form:
//      - form.valuesHaveChanged - compares this.getValues() (effectively the current values for
//        each item) with this._oldValues (the values as they were last set via setValue() or
//        setValues()) - used in resetValues() for example.
//
//  Additional methods on the form item:
//      - formItem.resetValue() - this will reset the value of the form item to the value stored in
//        form._oldValues[colName]
//      - formItem.elementChanged() - an internal method fired when the native element changed handler
//        is fired.  This is mentioned above as one of the callers for form.saveItemValue().  It
//        performs some other functions too, such as performing validation on the form item, and
//        setting up errors if necessary.  It has a number of "XXX" type comments and probably
//        warrants reviewing!
//      - formItem.updateValue() - called on change (and keypress if change on keypress is true)
//        determines value (mapped to data value) from element, called 'handleChange()' and
//        'saveValue()'
//      - formItem.handleChange() - internal method fired from updateValue() - will fire validators
//        and change handlers.  If this method returns false, the value in the form item element
//        will not be saved.
//      - formItem.saveValue() - called from 'setValue()' or 'updateValue()', this will save the
//        value passed in as this._value, and update this.form.values[this.name], if the item has
//        been marked as 'shouldSaveValue' true.
//
// Notes:
//  - direct submission of the HTML form drawn out by the dynamicForm widget is supported in a
//    couple of ways
//      - completely standard HTML submission is supported when canSubmit is true.
//        tripped from SubmitItem click, explicit call to "submit()" or "submitForm()".
//        Direct submission of course requires the values for form items to be present in real
//        HTML form elements - we handle this by writing out hidden elements with the intended
//        values where necessary.
//      - We also support an rpcManager direct submit transaction. This is tripped by
//        the saveData() code path if
//          a) this.canSubmit is true
//          b) this.isMultipart() [required for upload fields]
//          c) this.action has been specified.
//      Note that in rpcManager direct submit, the server pays attention to the _transaction
//      parameter, which is a structure that contains the intended field values wherever
//      possible.
//
//////////////////

// Override 'dataArity' - dynamicForms deal with single records
// Used by the valuesManager class
dataArity:"single",


//>    @method    dynamicForm.setValues()
// Replaces the current values of the entire form with the values passed in.
// <P>
// Note: when working with a form that is saving to a DataSource, you would typically call
// either +link{editRecord()} for an existing record, or +link{editNewRecord()} for a new
// record.  In addition to setting the current values of the form, these APIs establish the
// +link{DSRequest.operationType} used to save ("update" vs "add").
// <P>
// Values should be provided as an Object containing the new values as properties, where each
// propertyName is the name of a +link{items,form item} in the form, and each property value is
// the value to apply to that form item via +link{FormItem.setValue()}.
// <P>
// Values with no corresponding form item may also be passed, will be tracked by the form
// and returned by subsequent calls to +link{getValues()}.
// <P>
// Any +link{FormItem} for which a value is not provided will revert to its
// +link{formItem.defaultValue,defaultValue}.  To cause all FormItems to revert to default
// values, pass null.
// <P>
// This method also calls +link{rememberValues()} so that a subsequent later call to
// +link{resetValues()} will revert to the passed values.
//
// @param [newData] (Object) values for the form, or null to reset all items to default values
//
// @group formValues
// @visibility external
//<
setValues : function (newData, initTime) {
    // clear any extra advancedCriteria stored by setValuesAsCriteria()
    // getValuesAsCriteria() should return whatever was passed into this method rather than
    // hanging onto a stale advanced criteria object.
    /*if (this._extraAdvancedCriteria != null) {

        this.logWarn("clearing stored _extraAdvancedCriteria due to setValues. values:"
            + this.echo(newData) + ", old stored crit:" + isc.Comm.serialize(this._extraAdvancedCriteria) +
            " stack:" + this.getStackTrace());
    }*/
    delete this._extraAdvancedCriteria;

    if (isc.isAn.Array(newData)) {
        var useFirst = isc.isA.Object(newData[0]);
        this.logWarn("values specified as an array." +
                    (useFirst ? " Treating the first item in the array as intended values."
                              : " Ignoring specified values (resetting to defaults)."));
        if (useFirst) newData = newData[0];
        else newData= null;
    }

    if (newData == null) {
        newData = {};
    } else {
        // Duplicate the values object passed in.
        // This ensures that we don't directly manipulate a record that may be
        // referenced elsewhere (and vice-versa).

        // Use _duplicateValues() - this performs a recursive duplication using dataPaths to
        // access nested values.
        var clonedData = {};
        isc.DynamicForm._duplicateValues(this, newData, clonedData);
        newData = clonedData;
    }

    // store the new values object
    this._saveValues(newData);

    // If any of our items have a specified 'displayField', call the method to create a
    // special valueMap on that item so the value for that field is displayed rather than
    // the fields own value.

    var items = this.items;

    for (var i = 0; i < items.length; i++) {
        if (items[i].shouldSaveValue && this._useDisplayFieldValue(items[i])) {
            items[i]._displayFieldValueFromFormValues();
        }
    }

    // and set the values in the form elements

    this.setItemValues(newData, null, initTime);

    // remember the values so we can undo things
    this.rememberValues();

    // If we have a specified rulesEngine, notify it that we're editing a new set of values
    if (this.rulesEngine != null) {
        this.rulesEngine.processEditStart(newData);
    }

    // fire valuesChanged if it's been installed
    if (isc.isA.Function(this.valuesChanged)) this.valuesChanged();

    // redraw so that we will re-evaluate showIfs
    this.markForRedraw("setValues");
},



// Helper method to detect the case where we a field should display the value from a
// different field (field.displayField) in this form's values object
// The logic behind this is that if we're editing a record from the DataSource, we already have
// both the data value and the display value in the record values we were passed, and
// don't need to perform a fetch against the ds to get another display value.
//
// This is only valid if we have a specified display field and no optionDataSource / valueField
// specified

_useDisplayFieldValue : function (field) {
    if (!field || !field.displayField) return false;


    if (field.optionDataSource != null) return false;

    // If we're looking at a different underlying field on the optionDataSource, even if it's
    // the same dataSource, we don't want the display field value from this record
    if (field.getValueFieldName() != field.getFieldName()) return false;

    return true;
},

//>    @method    dynamicForm.setData()
//            Pass-through to the standard setData interface.
//        @group formValues
//
//        @param    newData        (object)    data to display in the form
//<
setData : function (newData) {
    this.setValues(newData);
},

// clear validation errors on rebind.  NOTE: should probably go to generic DataBinding
// framework when validation becomes a generic databinding behavior such that individual
// widgets just choose validation presentation.
setDataSource : function (dataSource, fields) {
    this.Super("setDataSource", arguments);
    this.clearErrors();
},

//>    @method    dynamicForm.rememberValues()
//            Make a snapshot of the current set of values, so we can reset to them later.
//            Creates a new object, then adds all non-method properties of values
//            to the new object.  Use <code>resetValues()</code> to revert to these values.
//          Note that this method is automatically called when the values for this form are
//          set programmatically via a call to +link{DynamicForm.setValues()}.
//
//      @visibility external
//        @group formValues
//
//        @return    (object)    copy of current form values
//<

rememberValues : function () {
    var values = this.getValues();

    var oldVals = {},
        rememberedDefault = [];

    // Recursively duplicate values so further edits won't manipulate the remembered values
    // directly.
    isc.DynamicForm._duplicateValues(this, values, oldVals, rememberedDefault);

    // Remember the duplicated values object
    this._oldValues = oldVals;
    // rememberedDefault array will contain dataPaths for every item that had its value
    // set to the default in the 'values' object we passed in.
    // We need this information so 'resetValues' can set these items to null and
    // potentially re-evaluate a dynamicDefault rather than resetting to whatever the
    // value is at this moment.
    // [still store the current val for valuesHaveChanged() checks]
    this._rememberedDefault = rememberedDefault;

    return this._oldValues;
},

//>    @method    dynamicForm.resetValues()   ([])
//
// Same as +link{method:DynamicForm.reset()}.
//
// @group formValues
// @visibility external
//<

resetValues : function () {
    // reset the form errors as well as the values
    this.clearErrors();

    // pull the values from form._oldValues into ValuesManager.values
    var values = {};
    isc.DynamicForm._duplicateValues(this, this._oldValues, values);
    // clear any remembered defaults so they get re-eval'd
    for (var i = 0; i < this._rememberedDefaults; i++) {
        isc.DynamicForm._clearFieldValue(this._rememberedDefaults[i], values, this);
    }

    this.setValues(values);

},

//>    @method    dynamicForm.clearValues()
// Reset to default form values and clear errors
//        @group formValues
// @visibility external
//<
clearValues : function () {
    // call setValues() to clear out all our saved values
    this.setValues();

    // also iterate through every unnamed form item, setting its value to null.

    var items = this.getItems();
    for (var i = 0; i < items.length; i++) {
        if (items[i].shouldSaveValue == false) items[i].setValue(null);
    }

    // reset the form errors
    this.clearErrors();

    // remember the current values for future calls to 'resetValues()'
    this.rememberValues();

    // redraw the form
    this.markForRedraw("clearValues");
},

//>    @method    dynamicForm.valuesHaveChanged() ([])
// Compares the current set of values with the values stored by the call to the
// +link{dynamicForm.rememberValues()} method.  <code>rememberValues()</code> runs when the
// form is initialized and on every call to +link{dynamicForm.setValues()}.
// Returns true if the values have changed, and false otherwise.
// @return    (Boolean)    true if current values do not match remembered values
//
// @see getChangedValues()
// @see getOldValues()
//
// @group formValues
// @visibility external
//<
valuesHaveChanged : function (returnChangedVals, values, oldValues) {
    if (values == null) values = this.getValues();
    // form._oldValues is used to store the values in rememberValues()
    if (oldValues == null) oldValues = this._oldValues || {};

    return isc.DynamicForm.valuesHaveChanged(this,returnChangedVals,values,oldValues);
},

//> @method dynamicForm.getOldValues() ([])
// Returns the set of values last stored by +link{dynamicForm.rememberValues()}.
// Note that <code>rememberValues()</code> is called automatically by
// +link{dynamicForm.setValues()}, and on form initialization, so this typically contains
// all values as they were before the user edited them.
//
// @return (Object) old values in the form
// @group formValues
// @see getChangedValues()
// @visibility external
//<
getOldValues : function () {
    var oldValues = {};
    isc.addProperties(oldValues, this._oldValues);
    return oldValues;
},


getOldValue : function (itemName) {
    return this.getOldValues()[itemName];
},

//> @method dynamicForm.getChangedValues()  ([])
// Returns all values within this DynamicForm that have changed since
// +link{dynamicForm.rememberValues()} last ran. Note that +link{dynamicForm.rememberValues()}
// runs on dynamicForm initialization, and with every call to +link{dynamicForm.setValues()}
// so this will typically contain all values the user has explicitly edited since then.
// @return (Object) changed values in the form
// @group formValues
// @see getOldValues()
// @visibility external
//<
getChangedValues : function () {
    return this.valuesHaveChanged(true);
},

//>    @method    dynamicForm.getValues() ([])
// An Object containing the values of the form as properties, where each propertyName is
// the name of a +link{items,form item} in the form, and each property value is the value
// held by that form item.
//
// @visibility external
// @group formValues
// @return (Object) values in the form
//<
getValues : function () {

    // Note: this method will not validate each field - to run validators on all the field, a
    // developer should explicitly call the 'validate()' method on the form (or the item in
    // question).
    // Call updateFocusItemValue() to ensure that if we have focus our values are up to date.
    // This makes sure that all the active field's value is saved when filtering, saving a
    // form, etc.
    this.updateFocusItemValue();

    return this.values;
},

//> @method updateFocusItemValue()
//  If we're currently focused in an item, whos value has been changed since last being
//  saved in this DynamicForm, call item.updateValue().
//<
updateFocusItemValue : function () {

    var focusItem = this.getFocusSubItem();
    if (!this._setValuesPending && focusItem != null && focusItem._itemValueIsDirty()) {
        focusItem.updateValue();
    }
},



//>    @method    dynamicForm.getData()
//            Return the values stored in the form.
//            Pass-through to dynamicForm.getValues();
//        @group    data
//        @return    (object)    values in the form
//<
getData : function () {
    return this.getValues();
},

//> @groupDef criteriaEditing
// DynamicForms may be used to edit +link{Criteria} or +link{AdvancedCriteria} for filtering
// data from a DataSource.
// <P>
// The main APIs for this are +link{dynamicForm.getValuesAsCriteria()} and
// +link{dynamicForm.setValuesAsCriteria()}.
// <P>
// <code>getValuesAsCriteria()</code> will return an AdvancedCriteria object in the following
// cases:
// <ul>
// <li>The form was previously passed AdvancedCriteria via <code>setValuesAsCriteria()</code></li>
// <li>The form has a specified +link{dynamicForm.operator} of <code>"or"</code></li>
// <li>+link{FormItem.hasAdvancedCriteria()} returns true for some item(s) within the form</li>
// </ul>
// <P>
// <var class="smartclient">
// Note that at the form item level, individual items can support editing of advanced criteria
// via overrides to the +link{formitem.hasAdvancedCriteria()}, +link{formItem.canEditCriterion()},
// +link{formItem.setCriterion()} and +link{formItem.getCriterion()} methods.
// </var>
// <var class="smartgwt">
// Note that at the form item level, individual items can support editing of advanced criteria
// by registering <code>FormItemCanEditCriterionPredicate</code>, <code>FormItemCriterionSetter</code>,
// and <code>FormItemCriterionGetter</code> objects to implement the methods <code>canEditCriterion()</code>,
// <code>setCriterion()</code>, and <code>getCriterion()</code>, respectively.
// </var>
// <P>
// The common pattern of using nested dynamicForms to edit arbitrary advanced criteria has been
// implemented via overrides to these methods in the +link{CanvasItem} class. See
// <var class="smartclient">+link{CanvasItem.getCriterion()}</var>
// <var class="smartgwt"><code>CanvasItem.setCriterionGetter()</code></var> for details.
// <P>
// For completely user-driven advanced criteria editing see also the +link{FilterBuilder} class.
//
// @title Criteria Editing
// @treeLocation Client Reference/Forms
// @visibility external
//<


//>    @method    dynamicForm.getValuesAsCriteria()
// Return search criteria based on the current set of values within this form.
// <p>
// The returned search criteria will be a simple +link{Criteria} object, except for
// in the following cases, in which case an +link{AdvancedCriteria} object will be returned:
// <ul>
// <li>The <code>advanced</code> parameter may be passed to explicitly request a
// <code>AdvancedCriteria</code> object be returned</li>
// <li>If +link{setValuesAsCriteria()} was called with an <code>AdvancedCriteria</code>
//     object, this method will return advanced criteria.</li>
// <li>If +link{dynamicForm.operator} is set to <code>"or"</code> rather than
//     <code>"and"</code> the generated criteria will always be advanced.</li>
// <li>If any item within this form is marked as producing advanced criteria by
//     +link{FormItem.hasAdvancedCriteria()} returning true.</li>
// </ul>
// The criteria returned will be picked up from the current values for this form. For simple
// criteria, each form item simply maps its value to it's fieldName. See
// <var class="smartclient">+link{formItem.getCriterion()}</var>
// <var class="smartgwt"><code>FormItem.setCriterionGetter()</code></var>
// for details on how form items generate advanced criteria.
// Note that any values or criteria specified via +link{setValues()} or
// +link{setValuesAsCriteria()} which do not correspond to an item within the form will be
// combined with the live item values when criteria are generated.
// <P>
// The returned criteria object can be used to filter data via methods such as
// +link{ListGrid.fetchData()}, +link{DataSource.fetchData()}, or, for more advanced usage,
// +link{ResultSet.setCriteria()}.
// <P>
// Note that any form field which the user has left blank is omitted as criteria, that is,
// a blank field is assumed to mean "allow any value for this field" and not "this field must
// be blank".  Examples of empty values include a blank text field or SelectItem with an empty
// selection.
//
// @param advanced (boolean) if true, return an +link{AdvancedCriteria} object even if the
//   form item values could be represented in a simple +link{Criterion} object.
// @param [textMatchStyle] (TextMatchStyle) This parameter may be passed to indicate whether
//   the criteria are to be applied to a substring match (filter) or exact match (fetch).
//   When advanced criteria are returned this parameter will cause the appropriate
//   <code>operator</code> to be generated for individual fields' criterion clauses.
//
// @group criteriaEditing
// @return (Criteria or AdvancedCriteria) a +link{Criteria} object, or +link{AdvancedCriteria}
//
// @visibility external
//<


getValuesAsCriteria : function (advanced, textMatchStyle, returnNulls) {

    if (advanced == null) {
        advanced = (this.operator != "and") ||
                   this.getItems().map("hasAdvancedCriteria").contains(true) ||
                   this.allowExpressions ||
                   (this._extraAdvancedCriteria != null);
    }

    // Simple criteria:
    // - criteria basically == values object
    // - remap specific items according to getCriteriaFieldName() and getCriteriaValue()
    // - pass through DS.filterCriteriaforFormValues() to clear nulls and handle arrays
    if (!advanced) {
        var values = this._getMappedCriteriaValues();

        // filterCriteriaForFormValues will clear out null values, and handle arrays returned
        // by multi-selects.

        if (returnNulls) return values;
        return isc.DataSource.filterCriteriaForFormValues(values);
    }

    // Advanced criteria:
    // - top level operator comes from form.operator
    // - if advanced criteria was already set combine live values into it, otherwise
    //   use values object as base and combine live values into that
    // - add each item value as a sub criterion (remapping field name and value according to
    //   getCriteraiFieldName() and getCriteriaValue();

    // The _extraAdvancedCriteria gets set when 'setValuesAsCriteria()' is called.
    // It represents the criteria passed in, excluding any sub-criteria for which we
    // have a live formItem editor - we want to overlay live values from form items
    // on top of it.
    var baseCriteria = this._extraAdvancedCriteria ? isc.clone(this._extraAdvancedCriteria)
                        : { operator:this.operator, _constructor: "AdvancedCriteria",
                            criteria:[]};


    var criteria = this._getMappedCriteriaValues(true, textMatchStyle);
    criteria.removeEmpty();
    if (criteria && criteria.length > 0) baseCriteria.criteria.addList(criteria);

    // don't return nonsensical criteria (advanced crit with no sub-crit)

    var result = isc.DS.checkEmptyCriteria(baseCriteria);
    return result;
},

// _getMappedCriteriaValues()
// Pick up the criteria field name and criteria value for each item in the form.
//
// Combine this with items from the form values object so we don't omit criteria fields
// without a specified item
_getMappedCriteriaValues : function (advanced, textMatchStyle) {

    // Note we iterate through all the items in the form, but we also need to look at the
    // form's values object, since there may be values set for fields that have no associated
    // item.
    // Cases where this could happen:
    // - setValues() was called, with a simple values object including fields with no item.
    //   In this case this._extraAdvancedCriteria will have been wiped
    // - the items in the form have changed since setValuesAsCriteria() was called.
    var values = isc.addProperties({},this.getValues()),
        simpleCriteria = {},
        advancedCriteria = [];


    var items = this.getFields();
    for (var i = 0; i < items.length; i++) {
        if (!items[i].shouldSaveValue) continue;
        var item = items[i],
            itemName = items[i].getTrimmedDataPath() || items[i].getFieldName(),
            // getCriteriaFieldName already handles trimming data path to be relative to the
            // values within this form
            criterionName = items[i].getCriteriaFieldName();

        // clear the value from the values object if it has an associated item!
        // We do this so we can retain values that don't have an associated item, but for
        // those that do we can remap values to a new criteria field name and a new
        // value via getCriteriaValue()
        isc.Canvas._clearFieldValue(itemName, values);

        if (!advanced) {
            // If the item returns a criteriaFieldName of null, exclude it from the criteria
            // altogether
            if (criterionName != null) {
                // If the values object already contains a value for this "criterionName"
                // because it is a field with both a name and a dataPath, remove the version
                // keyed by name
                if (values[items[i].name]) delete values[items[i].name];

                if (items[i].displayField && items[i]._value == null &&
                        values[items[i].displayField] == items[i].emptyDisplayValue)
                {
                    delete values[items[i].displayField];
                }
                simpleCriteria[criterionName] = items[i].getCriteriaValue();
            }
        } else {
            var criterion = item.getCriterion(textMatchStyle);
            if (criterion != null) advancedCriteria.add(criterion);
        }
    }
    // overlay the values from actual items on top of the values from the values object.
    if (!advanced) {
        return isc.addProperties(values, simpleCriteria);
    } else {
        for (var fieldName in values) {
            if (advancedCriteria.find("fieldName", fieldName)) continue;
            // we don't want null values adding as criteria elements
            if (values[fieldName] == null) continue;

            advancedCriteria.add({
                // DF's can be used as a filter (substring match) or a fetch (exact match)
                // allow a textMatchStyle param to configure what operator we produce here
                operator:isc.DataSource.getCriteriaOperator(values[fieldName], textMatchStyle),
                fieldName:fieldName,
                value:values[fieldName]
            });
        }
        return advancedCriteria;
    }

},

//>!BackCompat 2005.3.21
getFilterCriteria : function () {
    return this.getValuesAsCriteria();
},
//<!BackCompat


removeOtherFieldCriteria : function (fieldNames, criteria) {
    var advFields = [];
    isc.DS._getAdvancedCriteriaFields(criteria, advFields);
    for (var i=0; i<advFields.length; i++) {
        var fieldName = advFields[i];
        if (!fieldNames.contains(fieldName)) {
            this.removeFieldCriteria(fieldName, criteria);
        }
    }
},

removeFieldCriteria : function (fieldName, criteria) {
    if (!criteria || !criteria.criteria) return false;

    for (var i = criteria.criteria.length-1; i>=0; i--) {
        if (criteria.criteria[i].criteria) {
            this.removeFieldCriteria(fieldName, criteria.criteria[i]);
            if (criteria.criteria[i].criteria.length == 0) criteria.criteria.removeAt(i);
        } else {
            if (criteria.criteria[i].fieldName == fieldName) criteria.criteria.removeAt(i);
        }
    }
    //return criteria;
},

//> @method dynamicForm.setValuesAsCriteria()
// This method will display the specified criteria in this form for editing. The criteria
// parameter may be a simple +link{criterion} object or an +link{AdvancedCriteria} object.
// <P>
// For simple criteria, the specified fieldName will be used to apply criteria to form items,
// as with a standard setValues() call.
// <P>
// For AdvancedCriteria, behavior is as follows:
// <ul>
// <li>If the top level operator doesn't match the +link{dynamicForm.operator,operator} for
//  this form, the entire criteria will be nested in an outer advanced criteria object with
//  the appropriate operator.</li>
// <li>Each criterion within AdvancedCriteria will be applied to a form item if
//  +link{formItem.shouldSaveValue} is true for the item and
//  +link{formItem.canEditCriterion()} returns true for the criterion in question. By default
//  this method checks for a match with both the <code>fieldName</code> and <code>operator</code>
//  of the criterion. The criterion is actually passed to the item for editing via
//  <var class="smartclient">+link{formItem.setCriterion()}</var>
//  <var class="smartgwt">the <code>FormItemCriterionSetter</code>'s <code>setCriterion()</code> method</var>.
//  Note that these methods may be overridden for custom
//  handling. Also note that the default <var class="smartclient">+link{CanvasItem.setCriterion()} implementation</var>
//  <var class="smartgwt"><code>FormItemCriterionSetter.setCriterion()</code> implementation
//  used by +link{CanvasItem}</var> handles editing nested criteria via embedded dynamicForms.</li>
// <li>Criteria which don't map to any form item will be stored directly on the form and
//  recombined with the edited values from each item when +link{getValuesAsCriteria()} is
//  called.</li>
// </ul>
// @param criteria (Criterion) criteria to edit.
//
// @group criteriaEditing
// @visibility external
//<
// advanced parameter used when we're using nested forms to edit advanced criteria. In this
// case we don't have the "AdvancedCriteria" constructor property set on the inner criteria
// but we still want to use the 'advanced' type handling to apply it to our form items.
setValuesAsCriteria : function (criteria, advanced, dropExtraCriteria) {
    if (!advanced && !isc.DataSource.isAdvancedCriteria(criteria)) {
        // In this case the criteria passed in is a simple values object of fieldName-> value
        // mappings.
        // We could just do 'setValues(criteria)' and it would work in most cases, however we
        // support having items work with simple criteria but use a different criteria field
        // (EG ComboBoxItem with display field set and addUnknownValues:true).
        // Therefore we want to actually go through all our items and allow them to grab specific
        // criteria they're interested in.

        this._saveValues(criteria);
        var items = this.items || [];
        var itemsToClear = [];

        if (dropExtraCriteria) delete this._extraAdvancedCriteria;
        else if (this._extraAdvancedCriteria) {
            var fieldNames = criteria ? isc.getKeys(criteria) : [];
            if (isc.getKeys(criteria).length == 0) {
                // no criteria, clear out saved _extraAdvancedCriteria, it no longer applies
                delete this._extraAdvancedCriteria;
            } else {
                // remove any _extraAdvancedCriteria entries for fields not in the passed criteria
                this.removeOtherFieldCriteria(fieldNames, this._extraAdvancedCriteria);
                var eAC = this._extraAdvancedCriteria;
                if (!eAC || !eAC.criteria || eAC.criteria.length == 0) {
                    delete this._extraAdvancedCriteria;
                }
            }
        }

        for (var i = 0; i < items.length; i++) {
            var item = items[i],
                itemName = item.getFieldName(),
                itemModified = false;
            if (isc.propertyDefined(criteria, itemName) && item.canEditSimpleCriterion(itemName)) {
                item.setSimpleCriterion(criteria[itemName], itemName);
                itemModified = true;
            } else {
                for (var fieldName in criteria) {
                    if (fieldName != itemName && item.canEditSimpleCriterion(fieldName)) {
                        item.setSimpleCriterion(criteria[fieldName], fieldName);
                        itemModified = true;
                        break;
                    }
                }
            }
            if (!itemModified) {
                itemsToClear.add(item);
            }
        }

        // Explicitly empty any items we didn't touch
        for (var i = 0; i < itemsToClear.length; i++) {
            if (!itemsToClear[i].shouldSaveValue) continue;
            itemsToClear[i].clearValue();
        }

        this.rememberValues();
    } else {

        // Wipe out any existing "values" object.
        // We'll update the values for each item that can edit subcriterion of the criteria
        // passed in below, which will also store their simple value in the values object,
        // but this ensures we don't hang onto values for stale keys.

        this._saveValues({});

        // copy the crit object - we don't want to directly manipulate it and confuse other
        // code
        criteria = isc.clone(criteria);

        var topOperator = criteria.operator;
        if (topOperator != this.operator) {
            // this doesn't necessarily indicate an error but it might be unexpected.
            // Log a warning and wrap in a top level AC object.
            this.logInfo("Dynamic Form editing advanced criteria object:" +
                isc.Comm.serialize(criteria) + ". Form level operator specified as '" +
                this.operator + "' - Criteria returned from this form will be nested in an outer " +
                this.operator + " clause.", "AdvancedCriteria");


            criteria._constructor = null;
            criteria = {
                _constructor:"AdvancedCriteria",
                operator:this.operator,
                criteria:[criteria]
            }
        }

        // We have to determine which items will edit which of the criteria.
        // For each inner criterion - see if we have an item that can edit it. If so,
        // clear it off the stored "extra criteria" and apply it directly to the item for
        // editing. getValuesAsCriteria() will reconstitute it when it runs!
        // Note: Some items have the ability to edit composite ("and" / "or") criteria - for
        // example if editing expressions a user can enter ">1 and <2".
        // This means we can't assume a 1:1 mapping between top level criterion objects and
        // items - we may have to combine multiple top level criteria acting on a particular field
        // into a single composite criterion and apply this to an item.
        // getValuesAsCriteria() simplifies criteria down so we don't need to worry about introducing
        // extra levels of nesting - the returned criteria will be logically equivalent and as
        // simple as possible.

        var items = this.getItems(),
            innerCriteria = criteria.criteria,
            assigned = {},
            itemsToClear = {};

        for (var i = 0; i < items.length; i ++) {
            itemsToClear[items[i].getID()] = true;
        }

        for (var i = 0; i < innerCriteria.length; i++) {

            for (var ii = 0; ii < items.length; ii++) {
                if (!items[ii].shouldSaveValue) {
                    itemsToClear[items[ii].getID()] = false;
                    continue;
                }
                var item = items[ii];

                if (this.shouldApplyCriterionToItem(items[ii], innerCriteria[i])) {
//                      this.logWarn("applying advanced criterion:" + isc.Comm.serialize(innerCriteria[i]) +
//                          "to item:" + items[ii]);
                    var itemID = items[ii].getID();
                    if (assigned[itemID] == null) {
                        assigned[itemID] = innerCriteria[i];
                        itemsToClear[itemID] = false;
                    } else {
                        var existingCriteria = assigned[itemID];
                        var compositeCriterion = isc.DataSource.combineCriteria(
                            existingCriteria, innerCriteria[i],

                            this.operator, null, true);


                        if (!item.canEditCriterion(compositeCriterion)) {
                            this.logInfo("setValuesAsCriteria(): criteria include:" +
                                this.echoFull(existingCriteria) + " and " +
                                this.echoFull(innerCriteria[i]) + ". Both of these " +
                                "could be applied to item:" + item +
                                ". However, the item is unable to edit a composite criterion " +
                                "resulting from combining these criteria. Therefore " +
                                this.echoFull(innerCriteria[i]) + " will not be applied to this item",
                                "AdvancedCriteria");

                            // Don't clear the inner criteria - we'll see if another item can
                            // edit it, otherwise we'll leave it around as "extraAdvancedCriteria"
                            continue;

                        } else {
                            this.logDebug("setValuesAsCriteria(): Combined multiple criteria into " +
                                "composite criterion:" +
                                this.echoFull(compositeCriterion) + " and assigned to item:" + item,
                                "AdvancedCriteria");
                            assigned[itemID] = compositeCriterion;
                            itemsToClear[itemID] = false;
                        }
                    }
                    innerCriteria[i] = null;
                    // no need to go through the rest of the items for this criterion...
                    break;
//                 } else {
//                     this.logWarn("Not applying adv criterion:"
//                      + isc.Comm.serialize(innerCriteria[i]) + " to item:" + items[ii]);
                }


            }
        }
        innerCriteria.removeEmpty();

        // actually call 'setCriterion' to apply the criteria to the items
        for (var itemID in assigned) {
            var item = window[itemID];
            item.setCriterion(assigned[itemID]);
        }
        // Clear any editable fields that aren't editing anything specific in the criterion.
        for (var itemID in itemsToClear) {
            if (!itemsToClear[itemID]) continue;
            var item = window[itemID];
            item.clearValue();
        }
        // store the fields we're not directly editing -- these will be recombined with
        // live values as part of getValuesAsCriteria();
        this._extraAdvancedCriteria = criteria;
    }
},

shouldApplyCriterionToItem : function (item, criterion) {
    if (item.canEditCriterion(criterion)) return true;
    if (criterion.fieldName != null && criterion.fieldName == item.getCriteriaFieldName()) {
        // This is actually valid - we may have 2 items in the form used to edit the
        // same field with different operators (for example a number range with ">" and "<" operators)
        this.logInfo("Editing AdvancedCriteria in a dynamicForm. Criteria " +
                    "includes a value for field:" + criterion.fieldName +
                    ". This form includes an item " + item + " with the same fieldName" +
                    " but the specified operator '" +
                    criterion.operator + "' does not match the operator for this form item:" +
                    item.getOperator() +
                    ". Original criterion will be retained and combined with any " +
                    "criterion returned from this item.", "AdvancedCriteria");
    }
    return false;
},

//>    @method    dynamicForm.getValuesAsAdvancedCriteria()
// Return an AdvancedCriteria object based on the current set of values within this form.
// <p>
// Similar to +link{dynamicForm.getValuesAsCriteria()}, except the returned criteria object
// is guaranteed to be an AdvancedCriteria object, even if none of the form's fields has a
// specified +link{formItem.operator}
//
// @param [textMatchStyle] (TextMatchStyle) If specified the text match style will be used to
//   generate the appropriate <code>operator</code> for per field criteria.
// @group criteriaEditing
// @return (AdvancedCriteria) a +link{AdvancedCriteria} based on the form's current values
//
// @visibility external
//<
getValuesAsAdvancedCriteria : function (textMatchStyle, returnNulls) {
    return this.getValuesAsCriteria(true, textMatchStyle, returnNulls);

},

//>    @method    dynamicForm.getItem()
// Retrieve a +link{FormItem} in this form by it's +link{formItem.name,name},
// +link{formItem.dataPath,dataPath}, or index within
// the +link{dynamicForm.items,items} array.
// <P>
// FormItems that also have a +link{formItem.ID} may be accessed directly as a global variable
// <code>window[itemID]</code> or just <code>itemID</code>
//
// @param itemName (string or int) name of the item you're looking for
//
// @return (FormItem) FormItem object or null if not found
// @see getItem()
// @group items
// @visibility external
//<
getItem : function (itemName, isFieldName) {
    // if passed a null itemName, just bail
    if (itemName == null) return null;

    if (isc.isA.FormItem(itemName)) return itemName;

    var item = isc.Class.getArrayItem(itemName, this.items, this.fieldIdProperty);

    if (item != null) return item;

    // handle being passed a dataPath
    var targetDataPath = isc.DynamicForm._trimDataPath(itemName, this);
    for (var i = 0; i < this.items.length; i++) {
        var path = this.items[i].dataPath;
        path = isc.DynamicForm._trimDataPath(path, this);
        if (path == targetDataPath) return this.items[i];
    }

    // If we couldn't find an item with the same name - check that we weren't passed
    // a quoted index (like the string "0")

    if (!isFieldName && isc.isA.Number(itemName - 1)) {
        return this.items[parseInt(itemName)];
    }

    return null;
},

//>    @method    dynamicForm.getField()   ([])
// Synonym for dynamicForm.getItem()
//
// @param itemName (string) name of the item you're looking for
//
// @return (FormItem) FormItem object or null if not found
// @see getItem()
// @group items
// @visibility external
//<
getField : function (fieldID) {
    return this.getItem(fieldID);
},


//>    @method    dynamicForm.getSubItem()
//            Synonym for getItem()
//        @group    items
//        @param    itemID        (string)    name of the element you're looking for.
//        @return    (object)    form item object, or null if not found
//      @deprecated As of SmartClient 5.5, use +link{dynamicForm.getItem}.
//<
getSubItem : function (itemID) {
    return this.getItem(itemID);
},

//>    @method    dynamicForm.getItemById()
//    Gets a pointer to a form item from it's global ID.
//    (the form item is also available globally as window[itemID])
//
//        @param    itemID        (string)    ID of the element you're looking for.
//        @return    (object)    form item object or null if not found
//<
getItemById : function (itemID) {
    var item;

    if (isc.isA.String(itemID)) {
        item = window[itemID];
    } else item = itemID;

    if (isc.isA.FormItem(item)) return item;
    return null;
},


//>    @method    dynamicForm.getValue()  ([])
//  Returns the value stored in the form for some field.
//  Shorthand for dynamicForm.getValues()[fieldName];
//      @visibility external
//        @group formValues
//
//        @param    fieldName (string)    name of the field for which you're retrieving a value. Nested
//          values may be retrieved by passing in a +link{type:DataPath}
//        @return    (any)    value of the field
//      @example dateItem
//<
getValue : function (fieldName) {

     var item = this.getItem(fieldName);
     if (item) {
         var fieldName = item.getTrimmedDataPath() || item.name;
     }
    return this._getValue(fieldName);
},

_getValue : function (fieldName) {
    return isc.DynamicForm._getFieldValue(fieldName, null, this.values, this, true);
},

//>    @method    dynamicForm.setValue()  ([])
//   Sets the value for some field
//        @group formValues
//
//        @param    fieldName   (string)    Name of the field being updated. A +link{type:DataPath} may
//                          be passed to set nested values
//        @param    value        (string)    New value.
//      @visibility external
//<

storeAtomicValues:false,
setValue : function (fieldName, value, updatingDisplayValue) {
    var item = this.getItem(fieldName, updatingDisplayValue);
    // setValue on the item will update this.values.

    if (item != null) {
        // Handle this being a field with an 'opaque' data type with a get/set atomic value method
        // If this is the case, extract the atomic value and pass it to the item.
        if (!this.storeAtomicValues) {
            var type = item.type ? isc.SimpleType.getType(item.type) : null;
            if (type && type.getAtomicValue && type.updateAtomicValue) {
                // store the new atomic type on our values object

                fieldName = item.getTrimmedDataPath() || item.name;
                this._saveValue(fieldName, value);
                // extract the atomic value which we'll pass to item.setValue()
                value = type.getAtomicValue(value);
            }
        }
        return item.setValue(value);

    } else if (this.values != null) {
        this._saveValue(fieldName, value);
        return value;
    }
},

//> @method dynamicForm.clearValue()
// Clears the value for some field via a call to +link{FormItem.clearValue()} if appropriate.
// If there is no item associated with the field name, the field will just be cleared within
// our stored set of values.
// @param fieldName (string) Name of the field being cleared. A +link{type:DataPath} may be used for
//  clearing details of nested data structures.
// @visibility external
//<
clearValue : function (fieldName) {
    var item = this.getItem(fieldName);
    if (item != null) item.clearValue();
    else if (this.values) isc.DynamicForm._clearFieldValue(fieldName, this.values);
},

//>    @method    dynamicForm.showItem()  ([])
// Show a form item via +link{FormItem.show()}
//        @group formValues
//
//        @param    itemName    (string)    Name of the item to show
//      @visibility external
//<
showItem : function (fieldName) {
    var item = this.getItem(fieldName);
    if (item != null) return item.show();
},

//>    @method    dynamicForm.hideItem()  ([])
// Hide a form item via +link{FormItem.hide()}
//        @group formValues
//
//        @param    itemName    (string)    Name of the item to show
//      @visibility external
//<
hideItem : function (fieldName) {
    var item = this.getItem(fieldName);
    if (item != null) return item.hide();
},



//>    @method    dynamicForm.saveItemValue()
// Save the value passed in in the values array associated with the item.
//        @group formValues
//
//        @param    item        (FormItem)    Item to save value for (cannot be a string or number, etc).
//        @param    value        (string)    New value to set.
//<
saveItemValue : function (item, value) {

    // if this is not supposed to be included in our values array, return
    if (item.shouldSaveValue == false) return;
    var dataFieldID = item.getDataPath() || item.getFieldName();
    if (dataFieldID == null) return;
    this._saveAtomicValue(item, value);

    // If this is an item with a display field, store the display field value as well.
    // This will update any auto-generated valueMap for the field.


    if (this._useDisplayFieldValue(item) && (item.displayField != dataFieldID)) {

        var displayValue = item.mapValueToDisplay(value);
        this.setValue(item.displayField, displayValue, true);
    }

    //this.logWarn("saveItemValue: " + itemName + ": " + this.echoLeaf(value));
    // Mark the item as no longer being dirty
    item._markValueAsNotDirty();
},

// _saveValue and _saveValues - actually update this.values

_$slash:"/",
// _saveAtomicValue() - this is fired from 'saveItemValue' - IE the user has edited an atomic value
// (a string etc) and we need to save it.
// If the field has a specified simpleType with 'setAtomicType()' we'll make use of it here.
_saveAtomicValue : function (field, value) {
    this._saveValue(field, value, true);
},
_saveValue : function (field, value, isAtomicValue) {


    var fieldName, origFieldName;
    origFieldName = fieldName = field;
    if (this.storeAtomicValues) {
        if (isc.isAn.Object(fieldName)) {
            fieldName = field.getTrimmedDataPath() || field[this.fieldIdProperty];
            field = null;
        }
    } else {

        if (!isc.isA.String(fieldName)) {
            // we'll handle extracting the fieldName in DBC._saveFieldValue

            fieldName = null;
        } else {
            if (isAtomicValue) {
                field = this.getField(fieldName);
                if (field == null) {
                    var ds = this.getDataSource();
                    if (ds) field = ds.getField(fieldName) || ds.getFieldForDataPath(fieldName);
                }
            }
        }
    }
    isc.DynamicForm._saveFieldValue(fieldName, field, value, this.values, this, true);

    // If this form is part of a valuesManager, notify that of the change.
    // Note that the presence of a selectionComponent means we skip this - instead of
    // interacting with the VM values object directly, our selectionComponent will interact
    // with the VM values
    var selComponent = this.selectionComponent;
    if (!selComponent && this.valuesManager != null) {
        // If called during init, we may have not yet been added to the valuesManager as a member
        // or vm may be set to an ID, etc
        if (isc.isA.ValuesManager(this.valuesManager) && this.valuesManager.members &&
            this.valuesManager.members.contains(this))
        {
            // Normalize to a string - that's what _updateValue on the VM expects to be passed.
            if (!isc.isA.String(origFieldName)) {
                origFieldName = origFieldName.dataPath || origFieldName.name;
            }
            this.valuesManager._updateValue(origFieldName, value, this);
        }
    }
},

// clearItemValue()
// Internal method to clear the value for some field from the values object for this form.
// Called from item.clearValue()
clearItemValue : function (item) {
    var fieldName = isc.DynamicForm._combineDataPaths(this.dataPath, item.getDataPath() ||
                                                                     item.getFieldName());
    isc.DynamicForm._clearFieldValue(fieldName, this.values);
    if (!this.selectionComponent && isc.isA.ValuesManager(this.valuesManager) &&
         this.valuesManager.members && this.valuesManager.members.contains(this))
    {
        this.valuesManager._clearValue(fieldName, this);
    }
},

// _saveValues() updates this.values with the object passed in

_saveValues : function (values) {

    this.values = values;

    //>ValuesManager    If this form is part of a valuesManager, notify that of each field
    // affected by the change
    if (!this.selectionComponent && isc.isA.ValuesManager(this.valuesManager) &&
         this.valuesManager.members && this.valuesManager.members.contains(this))
    {
        var oldFields = isc.getKeys(this.values);
        for (var i in values) {
            this.valuesManager._updateValue(i, values[i], this);
            oldFields.remove(i);
        }
        // Clear any values in the VM that have been cleared by this
        for (var i = 0; i < oldFields.length; i++) {
            this.valuesManager._clearValue(oldFields[i], this);
        }
    }   //<ValuesManager
},

//>    @method    dynamicForm.getSavedItemValue()
// Save the value passed in in the values array associated with the item.
//        @group formValues
//
//        @param    item        (formItem)    Form item instance to check for the saved item value
//        @return    (any)                    Value saved for that item
//<
getSavedItemValue : function (item) {
    // If this is marked as a value we don't want to save, skip it.
    if (item.shouldSaveValue == false) return null;

    var    fieldName = isc.DynamicForm._combineDataPaths(this.dataPath, item.getDataPath() ||
                                                                     item.getFieldName());
    return this._getValue(fieldName);
},


//>    @method    dynamicForm.resetValue()
//        @group formValues
//
//        @param    itemName        (string)    name of the element you're looking for
//<
resetValue : function (itemName) {
    var item = this.getItem(itemName);
    return (item ? item.resetValue() : null);
},



//>    @method    dynamicForm.getValueMap()
//        return the valueMap for a specified item
//        @group formValues
//        @param    itemName        (string)    name of the element you're looking for
//<
getValueMap : function (itemName) {
    var item = this.getItem(itemName);
    return (item ? item.getValueMap() : null);
},

//>    @method    dynamicForm.setValueMap()
//        Set the valueMap for a specified item
//        @group formValues
// @param itemName (string) itemName of the item upon which the valueMap should be set.
// @param valueMap (ValueMap) new valueMap for the field in question.
// @visibility external
//<
setValueMap : function (itemName, valueMap) {
    var item = this.getItem(itemName);
    return (item ? item.setValueMap(valueMap) : null);
},

//>    @method    dynamicForm.getOptions()
//        Get the options for a specified item.  Pass-through to form.getValueMap()
//        @group formValues
//        @param    itemName        (string)    name of the element you're looking for
//<
getOptions : function (itemName) {
    return this.getValueMap(itemName);
},

//>    @method    dynamicForm.setOptions()
//        Set the options for a specified item.  Pass-through to form.setValueMap()
//        @group formValues
//        @param    itemName        (string)            name of the element you're looking for
//        @param    valueMap    (array | object)    new value map to set
//<
setOptions : function (itemName, valueMap) {
    return this.setValueMap(itemName, valueMap);
},

//>    @method    dynamicForm.getForm()
// Return the DOM form object.  Returns null if not found
//
//        @param    [form]        (form | string | number)    identifier for the form or an actual form
//
//        @return    (form)    Form object
//<
getForm : function (form) {
    var args = (form == null ? [this.getFormID()] : arguments);
    return this.Super("getForm", args);
},

//>    @method    dynamicForm.getFormID()    (A)
//        @group    drawing
//            return the ID for this form
//
//        @return    (string)    ID for this form in the DOM
//<
_$form:"form",
getFormID : function () {
    return this._getDOMID(this._$form);
},

getSerializeableFields : function(removeFields, keepFields) {
    removeFields = removeFields || [];

    // items and fields are the same thing, but items is deprecated and printing both would
    // produce a backref - so remove one of them
    removeFields.addList(["items"]);

    return this.Super("getSerializeableFields", [removeFields, keepFields], arguments);
},

// Form Sections
// --------------------------------------------------------------------------------------------

//> @attr DynamicForm.canTabToSectionHeaders (boolean : null : IRA)
// If true, the headers for any +link{SectionItems} will be included in the page's tab
// order for accessibility. May also be set at the item level via +link{SectionItem.canTabToHeader}
// <P>
// If unset, section headers will be focusable if +link{isc.setScreenReaderMode} has been called.
// See +link{group:accessibility}.
//
// @visibility external
//<

expandSection : function (sectionID) {
    var section = this.getItem(sectionID);
    if (isc.isA.SectionItem(section)) section.expandSection();
},

collapseSection : function (sectionID) {
    var section = this.getItem(sectionID);
    if (isc.isA.SectionItem(section)) section.collapseSection();
},

// Notification functions fired when a section is about to be expanded or collapsed - allows
// us to handle mutex sections.
_sectionExpanding : function (section) {

    if (this.isDrawn()) {
        this._specifiedNotifyAncestorsOnReflow = this.notifyAncestorsOnReflow;
        this.notifyAncestorsOnReflow = true;
    }

    if (this.sectionVisibilityMode == "mutex" && this._lastExpandedSection &&
         this._lastExpandedSection != section)
    {
        this._lastExpandedSection.collapseSection();
    }
    this._lastExpandedSection = section;
},

_sectionCollapsing : function (section) {
    if (this.isDrawn()) {
        this._specifiedNotifyAncestorsOnReflow = this.notifyAncestorsOnReflow;
        this.notifyAncestorsOnReflow = true;
    }

},

// Validation error management
// --------------------------------------------------------------------------------------------

//> @method dynamicForm.getErrors()
// Returns the current set of validation errors for this form.
// @return (object) Errors are returned as an object of the format<br>
// <code>{fieldName:errors, fieldName:errors}</code><br>
// where each <code>errors</code> object will be either an error message string or an array
// of error message strings.
// @group errors
// @visibility external
//<
getErrors : function () {
    return this.errors;
},


//> @method dynamicForm.getFieldErrors()
// Returns the current set of validation errors for some field in this form.
// @param fieldName (string) fieldName to check for errors
// @return (string | array of strings) Error message string, or if there is more than one error
//      associated with this field, array of error message strings.
// @group errors
// @visibility external
//<
// Note that the fieldName doesn't have to be associated with a form item - this could be
// a validator on a dataSource field too.
getFieldErrors : function (fieldName) {
    if (!this.errors) return null;
    var dataPath;
    if (isc.isA.FormItem(fieldName)) {
        var formItem = fieldName;
        fieldName = formItem.getFieldName();
        dataPath = this.buildFieldDataPath(this.getFullDataPath(), formItem)
    }
    var err = this.errors[fieldName];
    if (isc.isA.String(err) || isc.isAn.Array(err)) {
        return err;
    }
    if (dataPath != null) {
        //var err = this.getDataPathErrors(dataPath);
        if (isc.isA.String(err) || isc.isAn.Array(err)) return err;
    }
    return null;
},

getDataPathErrors : function (dataPath) {
    var elements = dataPath.split("/");
    var work = this.errors;
    for (var i = 0; i < elements.length; i++) {
        work = work[elements[i]];
        if (!work) return null;
    }
    return work;
},


//>    @method    dynamicForm.setErrors() ([A])
// Setter for validation errors on this form. Errors passed in should be a Javascript object
// of the format<br>
// <code>{fieldName1:errors, fieldName2:errors}</code><br>
// Where the <code>errors</code> value may be either a string (single error message) or an
// array of strings (if multiple errors should be applied to the field in question).
// @param    errors        (object)    list of errors as an object with the field names as keys
// @param  showErrors  (boolean)
//      If true redraw form to display errors now. Otherwise errors can be displayed by calling
//      +link{DynamicForm.showErrors()}<br>
//      Note: When the errors are shown,
//      +link{dynamicForm.handleHiddenValidationErrors(), handleHiddenValidationErrors()} will
//      be fired for errors on hidden fields, or with no associated formItem.
//        @group    errors
//      @visibility external
//<
setErrors : function (errors, showErrors) {

    this.errors = isc.DynamicForm.formatValidationErrors(errors);
    var hasHiddenErrors = false,
        hiddenErrors = {};

    for (var fieldName in this.errors) {
        var item = this.getItem(fieldName);
        if (!item || !item.visible) {
            hiddenErrors[fieldName] = this.errors[fieldName];
            hasHiddenErrors = true;
        }
    }

    // pass in current set of hidden errors - we know they're up to date so no need to
    // call 'getHiddenErrors()' again
    if (showErrors) this.showErrors(this.errors, hiddenErrors);

},

//>    @method    dynamicForm.setError()  ([A])
//          Sets error message(s) for the specified itemName to the error string or array of
//          strings. You must call form.markForRedraw() to display the new error message(s).<br>
//          <b>Note:</b> you can call this multiple times for an individual itemName
//             which will result in an array of errors being remembered.
//
//        @param    itemName        (string)    name of the item to set
//        @param    errorMessage    (string|array)    error message string or array of strings
//        @group    errors
//      @visibility external
// @deprecated This method has been deprecated as of SmartClient release 5.7.
//  Use +link{DynamicForm.addFieldErrors()} or +link{DynamicForm.setFieldErrors()} instead
//<
setError : function (itemName, errorMessage) {
    var oldError = this.errors[itemName];
    if (!oldError) this.errors[itemName] = errorMessage;
    else {
        if (isc.isA.String(oldError)) this.errors[itemName] = [oldError, errorMessage];
        else this.errors[itemName].add(errorMessage);
    }
},





//>    @method    dynamicForm.addFieldErrors()
// Adds field validation error[s] to the specified field. Errors passed in will be added
// to any existing errors on the field caused by validation or a previous call to this method.
// <br>
// The errors parameter may be passed in as a string (a single error message), or an array of
// strings.<br>
// The showErrors parameter allows the errors to be displayed immediately. Alternatively, call
// +link{DynamicForm.showFieldErrors()} to display the errors for this field.
// @param fieldName (string) field to apply the new errors to
// @param errors (string | array of strings) errors to apply to the field in question
// @param show (boolean) If true this method will fall through to +link{dynamicForm.showFieldErrors}
// to update the display
// @group errors
// @visibility external
//<
// Not clear whether this is necessary in addition to 'setFieldErrors()', but this matches
// the previous 'setError()' method implementation, which was public in 5.6.
addFieldErrors : function (fieldName, errors, showErrors) {
    if (!this.errors) this.errors = {};

    this.addValidationError(this.errors, fieldName, errors);

    // Don't bother updating hiddenErrors - this will be updated by
    // showErrors() / showFieldErrors()
    if (showErrors) this.showFieldErrors(fieldName);
},

//>    @method    dynamicForm.setFieldErrors()
// Set field validation error[s] for some field.<br>
// The errors parameter may be passed in as a string (a single error message), or an array of
// strings.<br>
// The showErrors parameter allows the errors to be displayed immediately. Alternatively, an
// explicit call to +link{DynamicForm.showFieldErrors()} will display the errors for this field.
// @param fieldName (string) field to apply the new errors to
// @param errors (string | array of strings) errors to apply to the field in question
// @param show (boolean) If true this method will fall through to +link{dynamicForm.showFieldErrors}
// to update the display
// @group errors
// @visibility external
//<
setFieldErrors : function (fieldName, errors, showErrors) {
    if (this.errors == null) this.errors = {};
    this.errors[fieldName] = errors;

    // Don't bother updating hiddenErrors - this will be updated by
    // showErrors() / showFieldErrors()

    if (showErrors) this.showFieldErrors(fieldName);
},

//> @method dynamicForm.clearFieldErrors()
// Clear any validation errors on the field passed in.
// @param fieldName (string) field to clear errors from
// @param show (boolean) If true this method will fall through to +link{dynamicForm.showFieldErrors}
// to update the display
// @group errors
// @visibility external
//<
clearFieldErrors : function (fieldName, show, suppressAutoFocus) {
    if (this.errors == null) return;
    if (!this.errors[fieldName]) return;

    delete this.errors[fieldName];
    if (show) {
        this.showFieldErrors(fieldName, suppressAutoFocus);
    }
},

// Helper to clear a specific error message from a field's errors.
clearFieldError : function (fieldName, error, show) {
    if (this.errors == null || !this.errors[fieldName]) return;
    var fieldErrors = this.errors[fieldName];
    if (!isc.isAn.Array(fieldErrors)) {
        if (fieldErrors == error) {
            delete this.errors[fieldName];
        }
    } else {
        if (fieldErrors.contains(error)){
            fieldErrors.remove(error);
        }
        if (fieldErrors.length == 0) delete this.errors[fieldName];
    }
    if (show) this.showFieldErrors(fieldName);
},

//>    @method    dynamicForm.clearErrors()   ([])
//    Clears all errors for this DynamicForm.
// @param show (boolean) If true, redraw the form to clear any visible error messages.
// @group    errors
// @visibility external
//<
clearErrors : function (show) {
    this.setErrors({}, show);
},


//>    @method    dynamicForm.hasErrors()
// Return whether this form currently has any validation errors.<br>
// Validation errors are set up automatically by validation, or may be explicitly set via
// +link{dynamicForm.setErrors()} or +link{dynamicForm.setFieldErrors()}.
// @return    (Boolean)    true == form currently has validation errors.
// @group    errors
// @visibility external
//<
hasErrors : function () {
    var errors = this.errors;
    if (!errors) return false;
    for (var name in errors) {
        if (errors[name] != null) return true;
    }
    return false;
},

//> @method dynamicForm.hasFieldErrors()
// Does this form currently h ave any validation errors on the field passed in?<br>
// Validation errors are set up automatically by validation, or may be explicitly set via
// +link{dynamicForm.setErrors()} or +link{dynamicForm.setFieldErrors()}.
// @param fieldName (string) field to test for validation errors
// @return (Boolean) true if the form has outstanding errors for the field in question.
// @group errors
// @visibility external
//<
hasFieldErrors : function (fieldName) {
    var errors = this.errors;
    return (errors && errors[fieldName] != null);
},


// Drawing and redrawing
// --------------------------------------------------------------------------------------------

//>    @method    dynamicForm.draw()    (A)
// Focuses in the first form field on idle
//
//        @group    drawing
//
//        @param    [document]        (DOM document)    document to draw in
//
//        @return    ()
//<
_$_delayedSetValues:"_delayedSetValues",
_$_delayedSetValuesFocus:"_delayedSetValuesFocus",
draw : function (a,b,c,d) {
    if (isc._traceMarkers) arguments.__this = this;
    // draw the form as normal
    if (!this.readyToDraw()) return this;
    this.invokeSuper(isc.DynamicForm, this._$draw, a,b,c,d);

    // We've now written all our items into the DOM - notify them that they are drawn!
    this._itemsDrawn();


    var shouldFocus = this.autoFocus,
        functionName = (!shouldFocus ? this._$_delayedSetValues : this._$_delayedSetValuesFocus);
    this._setValuesPending = true;

    isc.Page.setEvent(isc.EH.IDLE, this, isc.Page.FIRE_ONCE, functionName);


    if (this.position == isc.Canvas.RELATIVE) {
        isc.Page.setEvent(isc.EH.LOAD, this, isc.Page.FIRE_ONCE, "_placeCanvasItems");
    }

    return this;
},

//>Safari

_adjustOverflowForPageLoad : function () {
    if (isc.Browser.isSafari) {
        var items = this.getItems();
        if (this.isDrawn() && items) {
            for (var i = 0; i < items.length; i++) {
                items[i]._updateHTMLForPageLoad();
                // If the item the form to redraw completely we don't need individual items to
                // sort out their sizes since they'll get wiped out and redrawn anyway.
                if (this.isDirty()) break;
            }
        }
    }
    return this.Super("_adjustOverflowForPageLoad", arguments);
},
//<Safari

// helper methods fired asynchronously after draw
_delayedSetValues : function () {
    this.setItemValues(null, true);
    this.rememberValues();
    delete this._setValuesPending;
},

_delayedSetValuesFocus : function () {
    this._delayedSetValues();

    this.delayCall("focus");
},

//>    @method    dynamicForm.redraw()
//        @group    drawing
//<
redraw : function () {
    this._itemsRedrawing();
    // make sure we're not focused in any element
    // Note: FormItem.storeFocusForRedraw / restoreFocusAfterRedraw handles silently refocusing
    // after the redraw completes


    this._blurFocusItemWithoutHandler();

    if (this.__suppressBlurHandler != null) delete this.__suppressBlurHandler;

    // call the superclass method to redraw the form
    this.Super("redraw", arguments);

    // notify our items that they've been redrawn in the DOM.
    this._itemsRedrawn();


    this.setItemValues(null, true);


    var scrollLeft, scrollTop, clipHandle;
    if (isc.Browser.isMoz) {
        clipHandle = this.getClipHandle();
        if (clipHandle) {
            scrollLeft = clipHandle.scrollLeft;
            scrollTop = clipHandle.scrollTop;
        }
    }

    if (isc.Browser.isMoz) {
        if (scrollLeft != null && clipHandle.scrollLeft != scrollLeft)
            clipHandle.scrollLeft = scrollLeft;
        if (scrollTop != null && clipHandle.scrollTop != scrollTop)
            clipHandle.scrollTop = scrollTop;
    }

    // Notify all our items that their positions may have been modified by the redraw.
    // This catches the many possible cases where the HTML written into the DF will have
    // changed, causing layout changes to visible form items.

    this.itemsMoved();


    if (this._specifiedNotifyAncestorsOnReflow != null) {
        this.notifyAncestorsOnReflow = this._specifiedNotifyAncestorsOnReflow;
        this._specifiedNotifyAncestorsOnReflow = null;
    }

},


// When we draw / redraw, we want to notify our items that their HTML is now present in the DOM

_itemsDrawn : function () {
    var items = this.items;
    for (var i = 0; i < items.length; i++) {
        if (items[i]) {
            if (items[i].visible) items[i].drawn();

            else if (isc.CanvasItem && isc.isA.CanvasItem(items[i])) {
                items[i].ensureCanvasCleared();
            }
        }
    }
},

_itemsRedrawn : function () {
    var items = this.items;
    for (var i = 0; i < items.length; i++) {
        var item = items[i];
        if (!item) continue;
        if (item.visible) {
            item.isDrawn() ? item.redrawn() : item.drawn();
        } else if (item.isDrawn()) {
            item.cleared();
        }
    }
    this.destroyOrphanedItems("Delayed destroy of removed items on form redraw");

},

_itemsCleared : function () {
    var items = this.items;
    for (var i = 0; i < items.length; i++) {
        // The function check here is because we sometimes end up in this function when
        // this.items is still a bunch of config, not a list of FormItems
        if (items[i].isDrawn && items[i].isDrawn()) items[i].cleared();
    }

    this.destroyOrphanedItems("Delayed destroy of removed items on clear");
},

destroyOrphanedItems : function (reason) {
    if (this._orphanedItems != null) {
        this._orphanedItems.map("destroy", [reason]);
        delete this._orphanedItems;
    }
},

// Notify items that are about to be redrawn BEFORE the redraw occurs as well as after

_itemsRedrawing : function () {
    var items = this.items;
    for (var i = 0; i < items.length; i++) {
        var item = items[i];
        if (!item) continue;
        if (item.visible && item.isDrawn()) item.redrawing();
    }
},

modifyContent : function () {
    // NOTE: we have to place Canvas items after the form's table has been redrawn, but before
    // adjustOverflow, so that CanvasItems do not force a shrinking form to stay full size
    this._placeCanvasItems();
},

_placeCanvasItems : function () {
    return this._notifyCanvasItems("placeCanvas");
},

// a utlity for making notification calls to all CanvasItems
_notifyCanvasItems : function (method) {
    // don't JS error if CanvasItem not included
    if (!isc.CanvasItem) return;

    for (var i = 0; i < this.items.length; i++) {
        var item = this.items[i];
        if (item && isc.isA.CanvasItem(item)) item[method]();
    }
},

//> @method    dynamicForm.redrawFormItem()
// Redraw the form item passed in.  This should handle re-evaluating showIf / visible property
// on the item, and width/height, as well as updating the HTML content of the item.
// Default implementation just marks the form for redraw.
//  @param  item    (FormItem)  Form item to be redrawn.
//<

redrawFormItem : function (item, reason) {

    var items = this.getItems();
    if (!item) return;
    while (item.parentItem) item = item.parentItem;
    if (!items.contains(item)) return;

    // Set this._itemsChanged so when we redraw we'll re-run the TableResizePolicy before
    // This is required for showing / hiding items or changing colSpan, etc.

    this._itemsChanged = true;
    this.markForRedraw(item.ID + ": " + (reason ? reason : "redrawFormItem"));
},

// for debugging purposes only
getElementValues : function () {
    var values = {};
    for (var i = 0; i < this.items.length; i++) {
        var item = this.items[i],
            value = item.getDataElement() ? item.getDataElement().value : "[no element]";

        values[item[this.fieldIdProperty]] = value;
    }
    return values;
},


setItemValues : function (values, onRedraw, initTime, items, validating) {
    // get the item values from the values object if it was not passed in.
    var setToExisting = (values == null);
    if (setToExisting) values = this.getValues();
    if (values == null) values = {};

    // If we're changing the set of items and setValuesAsCriteria has been
    // called, we may have advancedCriteria stored that didn't have an item but now
    // applies to an item that's been added.
    var extraCriteria;
    if (initTime) {
        extraCriteria = this._extraAdvancedCriteria ? this._extraAdvancedCriteria.criteria : null;
    }

    items = items || this.items;
    var undef;
    for (var itemNum = 0; itemNum < items.length; itemNum++) {
        var item = items[itemNum],
            fieldName = item.getFieldName(),
            dataPath = item.getTrimmedDataPath(),
            isSetToDefault = item.isSetToDefaultValue(),

            value = undef;

        if (values != null && !isc.isAn.emptyObject(values)) {
            if (dataPath) {
            //    var segments = dataPath.split(isc.slash),
            //        nestedValues = values;
            //    for (var i = 0; i < segments.length-1; i++) {
            //        nestedValues = nestedValues[segments[i]];
            //        if (nestedValues == null) break;
            //    }
            //    if (nestedValues != null) value = nestedValues[segments.last()];
                value = isc.DynamicForm._getFieldValue(dataPath,
                            (this.storeAtomicValues ? null : item), values, this, true);
            } else if (fieldName) {
                value = isc.DynamicForm._getFieldValue(fieldName,
                            (this.storeAtomicValues ? null : item), values, this, true);
            }
        }


        if (onRedraw && isc.CanvasItem && isc.isA.CanvasItem(item) &&

            !item._useHiddenDataElement())
        {
            continue;
        }

        var undefined,

            isUndefined = ((!fieldName && !dataPath) || value === undefined);

        var initValue = null;
        // support initializing form items with a specified 'value'
        if (initTime && isUndefined && item.value != null) {
            initValue = item.value;
            // Ignore the fact that item is set to default if the init-value (item.value)
            // doesn't match the value stored as item._value [which is derived from the default]
            if (initValue != item._value) isSetToDefault = false;
        }

        // If there's no value for the item in the simple values array,
        // but we have something in the 'extraCriteria' object
        // that applies to the item, use setCriteria to apply it

        var setToCriterion = null;
        if (isUndefined && extraCriteria != null) {

            for (var i = 0; i < extraCriteria.length; i++) {

                if (item.canEditCriterion(extraCriteria[i])) {

                    isUndefined = false;

                    if (setToCriterion == null) {
                        setToCriterion = extraCriteria[i];
                    } else {
                        var compositeCriterion = isc.DataSource.combineCriteria(
                            setToCriterion, extraCriteria[i],
                            this.operator, null, true);

                        if (!item.canEditCriterion(compositeCriterion)) {
                            this.logInfo("setItemValues(): current values include multiple extra criteria " +
                                "that could be applied to form item:" + item +
                                ". Criteria include:" +
                                this.echoFull(setToCriterion) + " and " +
                                this.echoFull(extraCriteria[i]) +
                                ". However, the item is unable to edit a composite criterion " +
                                "resulting from combining these criteria. Therefore " +
                                this.echoFull(extraCriteria[i]) + " will not be applied to this item",
                                "AdvancedCriteria");

                            // Don't clear the extraCriteria criterion- we'll see if another item can
                            // edit it, otherwise we'll leave it around as "extraAdvancedCriteria"
                            continue;

                        } else {
                            this.logInfo("setItemValues(): Combined multiple 'extra' criteria into " +
                                "composite criterion:" +
                                this.echoFull(compositeCriterion) + " and assigned to item:" + item,
                                "AdvancedCriteria");
                            setToCriterion = compositeCriterion;
                        }
                    }
                    // Arrays are passed around by reference in JS so this we're updating
                    // this._extraAdvancedCriteria here
                    extraCriteria.removeAt(i);
                    if (extraCriteria.length == 0) {
                        delete this._extraAdvancedCriteria;
                    } else {
                        // We've directly modified the array, so decrement the counter since
                        // we'll now be pointing at the next entry.
                        i--;
                    }

                    // Don't break - we may be able to apply more than one
                    // "extra" subcriterion to this item by combining them as a composite crit
                    //break;
                }
            }
        }

        if (item.shouldSaveValue == false) {
            if (!isUndefined) {
                // If the item is marked as shouldSaveValue false, but we've been passed a
                // value for it, assume the developer wants the item store a value in the
                // values array, so turn 'shouldSaveValue' back on for that item.
                //>DEBUG
                this.logInfo("DynamicForm.setValues() passed a value for '" + item[this.fieldIdProperty] + "'." +
                             " The corresponding form item was declared with 'shouldSaveValue' set to " +
                             " false to exclude its value from the form's values object." +
                             " Setting 'shouldSaveValue' to true for this item." +
                             "\n[To avoid seeing this message in the future, set 'shouldSaveValue'" +
                             " to true for any form items whose values are to be managed via " +
                             " form.setValues() / form.getValues().]")
                //<DEBUG
                item.shouldSaveValue = true;
            } else {

                var oldItemValue = (isSetToDefault ? null : item._value);
                if (initValue != null) oldItemValue = initValue;
                item.setValue(oldItemValue, (isSetToDefault ? false : onRedraw));
                continue;
            }

        }

        if (initValue != null) {
            isUndefined = false;
            value = initValue;
        }

        // If the value is undefined, we want to use 'item.clearValue()' to reset to the
        // default value.  Note that in order to cause defaultValues to be re-evaluated on a
        // redraw, if an item has it's default value we need to call clearValue() rather than
        // restoring the old default value.
        if ((isUndefined || (setToCriterion == null && setToExisting && isSetToDefault)) &&
            !validating)
        {

            var undef;
            if (!initTime) item.clearValue();
            else if (initTime && isSetToDefault && item._value !== undef) {
                item.saveValue(item._value, true);
            }

        } else {
            if (setToCriterion != null) {
                item.setCriterion(setToCriterion);


            } else if (!validating || !isUndefined) {

                item.setValue(value, true);
            }
        }
    }
},

// Drawing
// --------------------------------------------------------------------------------------------

_$absolute:"absolute",
_absPos : function () {
    //!DONTCOMBINE
    return this.itemLayout == this._$absolute;
},


setColWidths : function (colWidths) {
    if (colWidths == null) return;
    // handle a comma-separated String
    if (isc.isA.String(colWidths)) {
        var colWidthsArray = colWidths.split(/[, ]+/);
        if (colWidthsArray == null || colWidthsArray.length == 0) {
            this.logWarn("ignoring invalid colWidths string: " + colWidths);
            // wipe it out if it's the value we were created with
            if (colWidths == this.colWidths) this.colWidths = null;
            return;
        }
        colWidths = colWidthsArray;
    // handle an Array of one String where the string is comma-separated.  This happens when
    // coming from Component XML if colWidths is specified as an attribute - the colWidths
    // field needs to be declared multiple="true" to handle the normal XML format for an Array,
    // so the String attribute gets wrapped in an Array
    } else if (isc.isAn.Array(colWidths) && colWidths.length == 1 &&
               isc.isA.String(colWidths[0]))
    {
        var colWidthsArray = colWidths[0].split(/[, ]+/);
        if (colWidthsArray != null || colWidthsArray.length > 1) {
            colWidths = colWidthsArray;
        }
    }
    this.colWidths = colWidths;

    if (this.isDrawn()) this.markForRedraw();
},

//>    @method    dynamicForm.getInnerHTML()    (A)
//            Output the HTML for this form
//        @group    drawing
//
//        @return    (string)                HTML for the form
//<
_$showIf:"showIf",
_$showIfArgs:"item,value,form,values",
_$closeForm:"</FORM>",
_$tablePolicy:"tablePolicy",
_$colWidthEquals:"<COL WIDTH=",

_$topRowTag:(isc.Browser.isIE ? "<TR STYLE='position:absolute'>" : "<TR>"),

_$topRowCellEnd:(isc.Browser.isSafari || isc.Browser.isMoz ? "</div></TD>" : "</TD>"),
_$cellStart:"<TD>",
_$cellEnd:"</TD>",
_$rowStart:"<TR>",
_$rowEnd:"</TR>",
_$br:"<br>",
_$tableFormClose:"</TABLE></FORM>",
_$tableClose:"</TABLE>",

getInnerHTML : function (printCallback) {
    if (this.autoDupMethods) this.duplicateMethod("getInnerHTML");

    // get the values and items
    var values = this.values,
        items = this.items
    ;

    // Check Visibility / Disabled State
    // --------------------------------------------------------------------------------------------

    // iterate through the items, marking items as invisible if their .showIf is false
    // keep track if the visibility has changed or not
    var visibilityChanged = false;

    for (var itemNum = 0; itemNum < items.length; itemNum++) {
        var item = items[itemNum],
            visible = item.visible;

        // if the item has a showIf method
        //    evaluate that to see whether the item should be visible or not.
        //    We note if the visible states of any items changes so we can know to recalculate
        //        form layout if visibility of any items has changed.
        if (item.showIf) {
            // CALLBACK API:  available variables:  "item,value,form,values"
            // Convert a string callback to a function
            isc.Func.replaceWithMethod(item, this._$showIf, this._$showIfArgs);

            var value = item.getValue();

            visible = (item.showIf(item,value,this,values) == true);
        }
        if (visible && this.isPrinting) {
            // shouldPrint takes precedence over whether it's a control or not, etc

            if (item.shouldPrint != null) {
                visible = item.shouldPrint;
            } else if (visible && this.currentPrintProperties.omitControls) {
                var omitControls = this.currentPrintProperties.omitControls;
                for (var i = 0; i < omitControls.length; i++) {
                    var cName = omitControls[i];
                    if (isc.isA[cName] && isc.isA[cName](item)) {
                        visible = false;
                    }
                }
            }
        }

        if (visible != item.visible) {
            item.visible = visible;
            // If the item is marked to take up space even when it's hidden, don't reflow
            // on show/hide
            if (!item.alwaysTakeSpace) visibilityChanged = true;
        }
    }

    // if the dynamic visibility for any item(s) has changed, or the _itemsChanged flag has
    // been set, throw away any cached tableResizePolicy for the size of the form elements, etc.
    // We set the _itemsChanged flag when we modify the items array (adding/removing items)
    // or modify other things that invalidate the cache (like changing title orientation,
    // visibility of items, etc)
    if (visibilityChanged || this._itemsChanged) isc.Canvas.invalidateTableResizePolicy(items);
    this._itemsChanged = false;

    // set the required property of any fields that are conditionally required
    this.setRequiredIf();

    // Layout
    // --------------------------------------------------------------------------------------------

    // if flattenItems is set, summing columns, taking into account showTitle and colSpan
    // settings, as well as title orientation (titleOrientation:"top" means the title
    // doesn't take up a column)

    if (this.flattenItems) {
        var flatCols = null;

        for (var itemNum = 0; itemNum < items.length; itemNum++) {
            var item = items[itemNum];

            // if this field is not hidden or if it is and takes space
            // increment the total columns
            if (item.visible || item.alwaysTakeSpace) flatCols++;

            // if this field has a displayed title on the left,
            // increment the total columns
            if (item.showTitle && item.titleOrientation != "top")
                flatCols++;

            // if there is a colSpan set, make a copy of it and nullify it
            item._colSpan = item.colSpan || null;
            item.colSpan = null;
        }

        if (flatCols) {
            this.numCols = flatCols;
            this._itemsChanged = true;
            this.markForRedraw();
        }
    }


    // get a StringBuffer to hold the output
    var output = isc.StringBuffer.create();



    // start the form tag
    if (this.writeFormTag && !this.isPrinting) output.append(this.getFormTagStartHTML());

    if (this._absPos()) {
        output.append(this.getAbsPosHTML());

        // end the form
        output.append(this._$closeForm);

        return output.release()
    }

    // start the table
    output.append(this.getTableStartHTML());

    // generate evenly spaced colWidths if no explicit colWidths have been provided and
    // titleWidth is set to *
    if (this.titleWidth == this._$star && !this.colWidths) {
        this.colWidths = [];
        for (var i = 0; i < this.numCols; i++) this.colWidths[i] = this._$star;
    }

    // set up the colWidths array
    var colWidths;

    // if the form has colWidths defined, use those
    if (this.colWidths) {
        colWidths = this.colWidths;
        if (colWidths.length > this.numCols) {
            if (!this._suppressColWidthWarnings) {
                this.logWarn("colWidths Array longer than numCols, using only first " +
                             this.numCols + " column widths");
            }
            colWidths = colWidths.slice(0, this.numCols);
        } else if (colWidths.length < this.numCols) {
            if (!this._suppressColWidthWarnings) {
                this.logWarn("colWidths Array shorter than numCols, remaining columns get '*' size");
            }
            for (var i = colWidths.length; i < this.numCols; i++) colWidths[i] = isc.star;
        }
    } else {
        // otherwise create default column widths, based on the assumption that every other
        // column will be full of labels and so should have DF.titleWidth.
        // NOTE: We'll have a column full of labels by default because each item in the form
        // takes up two columns in the table: one for the label, the other for the native form
        // element itself.  We do it this way so that a series of textboxes will line up.
        colWidths = [];

        var totalWidth = this.getInnerContentWidth();

        // Take off cellBorder - this is actually the border of the native HTML <table>
        totalWidth -= (this.cellBorder != null ? this.cellBorder : 0);

        // NOTE: items that actually try to fit within the column width take into account
        // cellSpacing and cellPadding via FormItem.getInnerWidth()

        // if an odd number of columns is specified, assume the last column is an element
        // column, as a column of dangling labels is unlikely.  To produce reasonable layout,
        // a form with an odd number of columns will probably need to specify colWidths..
        var    titleCols = Math.floor(this.numCols/2),
            // total width for all label columns
            totalElementColWidth = totalWidth - (titleCols * this.titleWidth),
            // width of each form element column
            elementColWidth;
        if (this.isPrinting) {
            // When printing don't calculate element column widths based on
            // the DynamicForm size -- the printHTML may be written into a
            // different sized container
            elementColWidth = "*";
        } else {
            elementColWidth =  Math.floor(totalElementColWidth / (this.numCols-titleCols));
            // don't let it get too small
            elementColWidth = Math.max(this.minColWidth, elementColWidth);
        }

        for (var i = 0; i < titleCols; i++) {
            // add a column for the label
            colWidths.add(this.titleWidth);
            // add a column for the form element
            colWidths.add(elementColWidth);
        }
        // for an odd number of columns, take on another element column
        if ((this.numCols % 2) != 0) colWidths.add(elementColWidth);
        if (this.logIsInfoEnabled(this._$tablePolicy)) {
            this.logInfo("totalWidth: " + totalWidth + ", generated colWidths: " + colWidths,
                         this._$tablePolicy);
        }
    }
    // run the tableResizePolicy on the list to set up the table of form items
    //    this assigns sizes to dynamic items as well as populating the structure
    //    that maps items to particular rows/cols
    //   Note: This will set up the _size property on the items as a 2 element array, where
    //   the first element represents the desired width, and the the second the height.
    //   For some items getInnerHTML() will make use of this property to specify the elements
    //   drawn size, though if not available, the standard item.width, item.height will be used
    //   instead.

    var innerWidth = this.getInnerContentWidth(),
        innerHeight = this.getInnerContentHeight();


    if (this.cellSpacing != 0) {
        if (isc.Browser.isMoz) innerHeight -= 2*this.cellSpacing;
        else if (isc.Browser.isSafari) innerHeight -= this.cellSpacing;
    }

    items._defaultRowHeight = this.defaultRowHeight;
    isc.Canvas.applyTableResizePolicy(items, innerWidth, innerHeight,
                                  this.numCols, colWidths);



    var overflowed = false;
    if (isc.CanvasItem) {
        for (var i = 0; i < items.length; i++) {
            var item = items[i];
            if (isc.isA.CanvasItem(item) && item.checkCanvasOverflow()) {
                if (!overflowed && this.logIsInfoEnabled(this._$tablePolicy)) {
                    this.logInfo("CanvasItem: " + item + " overflowed, rerunning policy",
                                 this._$tablePolicy);
                }
                overflowed = true;
            }
        }
    }

    if (overflowed) {
        isc.Canvas.applyTableResizePolicy(items, innerWidth, innerHeight,
                                          this.numCols, colWidths, null, true);
    }

    if (!this.isPrinting) {
        colWidths = items._colWidths;
    }

    // output <COL> tags to set the sizes of the columns.

    for (var colNum = 0; colNum < colWidths.length; colNum++) {
        var colWidth = colWidths[colNum];
        // In printing mode we avoided the tableResizePolicy - we expect to see
        // colWidths specified as "*" and titleWidth
        // If "*" just omit writing out a width at all
        if (colWidth == "*") {
            output.append("<COL>");
        } else {
            output.append(this._$colWidthEquals, colWidth, this._$rightAngle);
        }
    }





    // if fixedColWidths is set, force column widths to be respected as minimums by writing
    // out a row of cells with spacers.  <COL> tags on their own won't enforce minimums.
    if (this.isPrinting) {

        output.append("<tr>");
    } else {
        output.append(this._$topRowTag);
    }

    var topRowCellStart = isc.DynamicForm._getTopRowCellStart();
    for (var colNum = 0; colNum < colWidths.length; colNum++) {
        if (!isc.isA.Number(colWidths[colNum])) {
            output.append(topRowCellStart.join(isc.emptyString), this._$topRowCellEnd);
        } else {
            var innerWidth = colWidths[colNum];
            // NOTE: correct for spacing, but *do not* correct for padding, because we write out
            // padding:0px on the cells
            innerWidth -= (this.cellSpacing!= null ? (2 * this.cellSpacing) : 0);


            if (isc.Browser.isIE8Strict) {
                innerWidth -= this.cellPadding != null ? (2* this.cellPadding) : 0;
            }
            // The top row has theoretically a height of zero px, but can actually be visible in IE
            // if it has a bg-color applied to it.
            // We've seen this occur with a stylesheet that globally sets td background-color.
            // handle this by applying standard form cell style

            topRowCellStart[3] = (isc.FormItem ? isc.FormItem.getPrototype().baseStyle : null);

            var spacerHeight = isc.Browser.isIE ? 1 : 0,
                cellStart = topRowCellStart.join(isc.emptyString);
            output.append(cellStart,
                          this.fixedColWidths ? isc.Canvas.spacerHTML(innerWidth,spacerHeight) : null,
                          this._$topRowCellEnd);
        }
    }
    output.append(this._$rowEnd);

    // if this.autoSendTarget is set, add a '__target__' hidden field so that the server knows the
    // name of the frame/window this form is being targeted at.
    if (this.autoSendTarget && this.target) output.append(this._getAutoSendTargetHTML());

    // Draw HTML for Items
    // --------------------------------------------------------------------------------------------



    var len = items.length,
        wentAsync = false;

    var self = this;
    var completeInnerHTMLFun = function completeInnerHTMLFun(htmlOutputs) {
        // append all item outputs
        if (htmlOutputs != null) {
            // since there may be more than 26 outputs, need to push onto output's stream directly.
            var outputStream = output.getArray();
            outputStream.push.apply(outputStream, htmlOutputs);
        }

        // end the current row
        if (len > 0) output.append(self._$rowEnd);

        // end the table and form
        if (self.writeFormTag && !self.isPrinting) output.append(self._$tableFormClose);
        // end just the table
        else output.append(self._$tableClose);

        var HTML = output.release();
        if (wentAsync) {
            self.fireCallback(printCallback, "HTML", [HTML]);
            return false;
        } else {
            return HTML;
        }
    };

    // for each item in the list, get HTML output for it and combine the output
    if (len > 0) {
        // Handle this by tracking items to include in the next cell in an array, to be updated
        // in the loop while writing cells out.
        var includeInNextCell = [],
            htmlOutputs = new Array(len),
            completedCount = 0;

        var itemCompletedFun = function itemCompletedFun() {
            if (++completedCount == len) {
                return completeInnerHTMLFun(htmlOutputs);
            }
        };

        var theHTML;
        for (var itemNum = 0; itemNum < len; ++itemNum) {


            var item = items[itemNum],
                itemOutput = isc.SB.create(),
                visible,
                column,
                error,
                value,
                titleOrientation,
                showErrors;

            // if a null item, skip it
            if (!item) {
                theHTML = itemCompletedFun();
                continue;
            }

            visible = item.visible;
            // note that the value of this item can't possibly be dirty anymore
            item._markValueAsNotDirty();

            //>DEBUG
            if (this.logIsDebugEnabled()) this.logDebug("Drawing FormItem: " + item); //<DEBUG

            // if the item has been marked as invisible, skip it unless it's marked to take space
            // even when hidden
            if (!item.alwaysTakeSpace && !visible) {
                theHTML = itemCompletedFun();
                continue;
            }

            // if this item should not take up a cell, we'll include it in the next cell's HTML
            // (Unless we're the last item, in which case, just take up a cell!)
            if ((item.rowSpan == 0 || item.colSpan == 0) && itemNum < len-1) {
                includeInNextCell.add(item);
                theHTML = itemCompletedFun();
                continue;
            }

            // get the error for this form element
            column = item.getFieldName();
            error = item.getErrors();
            value = item.getValue();
            titleOrientation = this.getTitleOrientation(item);

            // if the error is an empty string, null it out
            if (isc.is.emptyString(error)) error = null;

            // if the item should start its row or passes the name boundary
            // output the end and start row tag
            // Note: _startRow attribute set up via Canvas.applyTableResizePolicy()
            if (item._startRow || itemNum == 0) {
                if (itemNum != 0) {
                    itemOutput.append(this._$rowEnd);
                }
                if (item._emptyRows && item._emptyRows.length > 0) {
                    for (var i = 0; i < item._emptyRows.length; i++) {
                        itemOutput.append(this._$rowStart);

                        var numCells = this.numCols;
                        for (var ii = 0; ii < item._emptyRows[i]; ii++) {
                            itemOutput.append(this._$cellStart, "&nbsp;", this._$cellEnd);

                        }
                        itemOutput.append(this._$rowEnd);
                    }
                }
                itemOutput.append(this._$rowStart);
                if (item._emptyCells > 0) {
                    for (var i = 0; i < item._emptyCells; i++) itemOutput.append(this._$cellStart, this._$cellEnd);
                }
            }

            // place title on the left of the item, in its own cell
            if (titleOrientation == isc.Canvas.LEFT) {
                itemOutput.append(this.getTitleCellHTML(item, error));
            }

            // output the tag start for the item if it has a positive row and colSpan
            itemOutput.append(this.getCellStartHTML(item, error));

            // place title on top of the item, with no separate cell
            if (visible && titleOrientation == isc.Canvas.TOP) {
                if (this.shouldClipTitle(item)) {
                    itemOutput.append(this.getTitleCellInnerHTML(item, error));
                } else {
                    itemOutput.append(this.getTitleSpanHTML(item, error), this._$br);
                }
            }

            // if there is an error associated with the item, output that
            showErrors = (visible && error && this.showInlineErrors);
            if (showErrors && item.getErrorOrientation() == isc.Canvas.TOP) {
                itemOutput.append(this.getItemErrorHTML(item, error));
            }

            var completeIncludedInnerHTMLFun = (function (itemNum, item, itemOutput, visible, column, error, value, titleOrientation, showErrors) {
                var func = function func(HTML) {
                    itemOutput.append(HTML);

                    // Top and bottom orientation are handled by writing the error HTML out here -- left
                    // and right orientation will be handled as part of formItem.getInnerHTML
                    if (showErrors && item.getErrorOrientation() == isc.Canvas.BOTTOM) {
                        itemOutput.append(self.getItemErrorHTML(item, error));
                    }

                    // append the tag end for the item
                    itemOutput.append(self.getCellEndHTML(item, error));

                    // place title on right of item, in it's own cell
                    if (titleOrientation == isc.Canvas.RIGHT) {
                        itemOutput.append(self.getTitleCellHTML(item, error));
                    }

                    htmlOutputs[itemNum] = itemOutput.release();

                    return itemCompletedFun();
                };

                return function (includedHtmlOutputs) {
                    if (includedHtmlOutputs != null) {
                        // since there may be more than 26 included items, we need to push onto
                        // itemOutput's stream directly.
                        var itemOutputStream = itemOutput.getArray();
                        itemOutputStream.push.apply(itemOutputStream, includedHtmlOutputs);
                    }

                    // output the innerHTML for the item
                    if (visible) {
                        // pass in the parameter to write out the hint text and validation errors
                        // along with the form item
                        // Note if validation error orientation is top or bottom we write the error out
                        // as part of this method - otherwise we need to write the error out in the form
                        // item HTML (like the hint)
                        if (self.isPrinting) {
                            var printHTML = item.getPrintHTML(self.currentPrintProperties, func);
                            if (printHTML == null) {
                                return false;
                            } else {
                                return func(printHTML);
                            }
                        } else {
                            return func(item.getInnerHTML(value, true, self.showInlineErrors));
                        }

                    } else return func(isc.Canvas.spacerHTML(item.width, item.height));
                };
            })(itemNum, item, itemOutput, visible, column, error, value, titleOrientation, showErrors);

            // if any items are being 'piggy backed' into this item's cell, write them out now.
            var includedLen = includeInNextCell.length;
            if (includedLen > 0) {
                var includedHtmlOutputs = new Array(includedLen);

                var includedCompletedFun = (function (completeIncludedInnerHTMLFun, includedLen, includedHtmlOutputs) {
                    var includedCompletedCount = 0;
                    return function () {
                        if (++includedCompletedCount == includedLen) {
                            return completeIncludedInnerHTMLFun(includedHtmlOutputs);
                        }
                    };
                })(completeIncludedInnerHTMLFun, includedLen, includedHtmlOutputs);

                for (var m = 0; m < includedLen; ++m) {
                    var includedItem = includeInNextCell[m];

                    if (!includedItem.visible) {
                        includedCompletedFun();
                        continue;
                    }

                    var innerFunc = (function (includedHtmlOutputs, includedCompletedFun, m) {
                        return function (HTML) {
                            includedHtmlOutputs[m] = HTML;
                            return includedCompletedFun();
                        };
                    })(includedHtmlOutputs, includedCompletedFun, m);

                    if (this.isPrinting) {
                        var printHTML = includedItem.getPrintHTML(self.currentPrintProperties, innerFunc);
                        if (printHTML == null) {
                            wentAsync = true;
                        } else {
                            theHTML = innerFunc(printHTML);
                        }
                    } else {
                        theHTML = innerFunc(includedItem.getInnerHTML(includedItem.getValue()));
                    }
                }

                // drop the old 'includeInNextCell' array for the next item.
                includeInNextCell.length = 0;
            } else {
                theHTML = completeIncludedInnerHTMLFun();
            }

            if (theHTML === false) wentAsync = true;
        }

        if (wentAsync) {
            // indicate that we went asynchronous

            return false;
        } else {
            return theHTML;
        }
    } else {
        return completeInnerHTMLFun();
    }
},

// Any children of the form are likely to be canvasItems' canvii which are written out inline
// via code in CanvasItem.js
getPrintChildren : function () {
    return null;
},

// Method to return any canvasItems' canvases contained by this form.
getCanvasItemCanvii : function () {
    var items = this.items || [],
        canvii = [];
    for (var i = 0; i < items.length; i++) {
        if (items[i].isA("CanvasItem") && isc.isA.Canvas(items[i].canvas)) {
            canvii.add(items[i].canvas);
        }
    }
    return canvii;
},

createErrorItem : function () {
    var errorItem = isc.addProperties({cellStyle:this.errorItemCellStyle},
                                      this.errorItemDefaults,
                                      this.errorItemProperties);

    // Make the errorItem focusable in screen reader mode because then the user can tab to
    // the errorItem to have all error messages read at once.
    if (isc.screenReader) errorItem.canFocus = true;

    this.addItems([errorItem], 0);
    this._errorItem = this.getItem(0);
},

//> @method DynamicForm.getErrorsHTML()
// If +link{dynamicForm.showInlineErrors} is false, the form will render all errors in a list at
// the top of the form. This method returns the HTML for this list of errors.
// @param errors (object) Map of field names to error messages. Each field may contain a single
//                        error message (string) or an array of errors
// @return (HTML) error HTML.
// @group validation
// @visibility external
//<
getErrorsHTML : function (errors) {
    if (!errors || isc.isAn.emptyObject(errors)) return isc.emptyString;

    var SB = isc.SB.create(),
        sep = " : ";
    SB.append(this.errorsPreamble, "<ul>");
    for (var field in errors) {
        var item = this.getItem(field),
            message;
        if (item != null) {
            message = item.getErrorMessage(errors[field]);

            SB.append("<li>", item.getTitle(), sep, message, "</li>");

        // Field with no associated item (ds field?) Just display the error as normal
        } else {
            message = errors[field];
            if (isc.isAn.Array(message)) {
                message = "<ul><li>" + message.join("</li><li>") + "</li></ul>";
            }

            SB.append("<li>", field, sep, message, "</li>");
        }
    }
    SB.append("</ul>");
    return SB.release();
},

//> @method dynamicForm.getItemErrorHTML()
// If +link{dynamicForm.showInlineErrors} is true, this method is called for each item in the form
// and returns the error HTML to be written out next to the item.<br>
// Default implementation falls through to +link{FormItem.getErrorHTML()} on the item in question.
// @param item (FormItem) Form item for which the HTML should be retrieved
// @param error (string | array) Error message to display for the item, or array of error message
//                              strings.
// @group validation
// @visibility external
//<
getItemErrorHTML : function (item, error) {
    return item.getErrorHTML(error);
},

// Helper to generate the input required for the autoSendTarget feature
_$autoSendTargetTemplate:[
      "<INPUT TYPE=HIDDEN NAME='" ,
      , // target field name
      "' VALUE='" ,
      , // target
      "'>"
],
_getAutoSendTargetHTML : function () {
    this._$autoSendTargetTemplate[1] = this.autoSendTargetFieldName;
    this._$autoSendTargetTemplate[3] = this.target;
    return this._$autoSendTargetTemplate.join(isc.emptyString);
},


//>    @method    dynamicForm.getCellStartHTML()    (A)
//            Return the HTML for start tag of this item's cell.
//        @group    drawing
//
//        @param    item    (formItem)    item in question
//        @param    error    (string)    error for this item
//
//        @return    (HTML)    output for the start tag
//<
getCellStartHTML : function (item, error) {

    // get the colSpan for the item, which might be a "*"
    var colSpan = item.getColSpan(),

        rowSpan = item._rowSpan != null ? item._rowSpan : item.getRowSpan();

    // colSpan / rowSpan of zero is handled by writing the form item out into the next form
    // item's cell.
    // However if the last item in a form has rowSpan / colSpan of zero, we need to put it into its
    // own cell, so we should treat it as having rowSpan / colSpan of 1.
    if (colSpan == 0) colSpan = 1;
    if (rowSpan == 0) rowSpan = 1;

    // if the colSpan is a "*", set it appropriately
    if (colSpan == "*") {
        var startCol = (item._tablePlacement ? item._tablePlacement[0] : 0);
        colSpan = (this.numCols - startCol);
    }

    var className = item.getCellStyle();

    // Use the height calculated by tableResizePolicy rather than the specified size (may be
    // null, "*" or a percentage).

    var forceHeight = this.fixedRowHeights || item.shouldFixRowHeight();
    var height = item._size ? item._size[1] : null;

    if (isc.isA.Number(height) && this.cellSpacing != 0) height -= 2*this.cellSpacing;
    if (isc.Browser.isStrict && isc.isA.Number(height) && this.cellPadding != 0) {
        height -= 2*this.cellPadding;
    }
    return this._getCellStartHTML(
        (item.align ? item.align :
                       ((this.form? this.form.isRTL() : this.isRTL()) ? isc.Canvas.RIGHT : isc.Canvas.LEFT)),
        item.vAlign,

        className,
        rowSpan,
        colSpan,

        null,

        (forceHeight ? height : null),

        null,
        item.cssText,
        (this.form ? this.form.getID() : this.getID()),
        item.getItemID(),
        item.getFormCellID()
    );
},

_getCellStartHTML : function (align, vAlign, className, rowSpan, colSpan, width, height,
                              extraStuff, cssText, formID, itemID, cellID, nowrap)
{
    var output = isc.StringBuffer.create(),
        ns = isc._emptyString;

    output.append(
        "<TD ALIGN=", align,
            (vAlign == null ? ns : " VALIGN=" + vAlign),
            (className != null ? " CLASS='" + className + "'" : ns),
            " STYLE='", (cssText != null ? cssText : ns), "'",

           (rowSpan > 1 ? " ROWSPAN=" + rowSpan: ns),
           (colSpan > 1 ? " COLSPAN=" + colSpan : ns),
           (width != null ? " WIDTH=" + width : ns),
           (height != null ? " HEIGHT=" + height : ns),
           (extraStuff != null ? extraStuff : ns)
    );


    // If this is the containing cell for some item, write in ID and 'containsItem' attribute
    // for the item.
    // This method is used for cells containing things other than the form items, such as icons
    // in which case we'll avoid writing in these attributes.
    if (cellID) {
        output.append(" ID=", cellID, " ");
    }
    if (itemID && formID) {

        output.append(isc.DynamicForm._containsItem, "='",itemID,"'");

    }


    output.append(nowrap ? "><NOBR>" : ">");

    return output.release();

},

//>    @method    dynamicForm.getCellEndHTML()    (A)
//        @group    drawing
//            Return the HTML for start tag of this item's cell.
//
//        @param    item    (formItem)    item in question
//        @param    error    (string)    error for this item
//
//        @return    (HTML)    output for the start tag
//<
getCellEndHTML : function (item, error) {

    // otherwise return a simple end of cell
    return  this._getCellEndHTML();
},

_getCellEndHTML : function (nowrap) {
    return nowrap ? "</NOBR></TD>" : "</TD>";
},

//>    @method    dynamicForm.getTitleOrientation()    (A)
// Return the orientation of the title for a specific item or the default title orientation if
// no item is passed.
//
// @param [item] (FormItem) item to check
// @return (TitleOrientation) orientation of the title, or null if an item is passed and has no
//                            title
// @visibility external
//<
getTitleOrientation : function (item) {
    if (item && !item.shouldShowTitle()) return null;
    return (item ? item.titleOrientation : null) || this.titleOrientation || isc.Canvas.LEFT;
},

//> @attr dynamicForm.titleAlign (Alignment : null : IRW)
// Default alignment for item titles. If unset default alignment will be derived from
// +link{Page.isRTL(),text direction} as described in +link{dynamicForm.getTitleAlign()}
// @visibility external
//<

//>    @method    dynamicForm.getTitleAlign()    (A)
// Get the alignment for the title for some item. Default implementation is as follows:
// <ul><li>If +link{formItem.titleAlign} is specified, it will be respected</li>
//     <li>Otherwise if +link{dynamicForm.titleAlign,this.titleAlign} is set, it will be
//         respected</li>
//     <li>Otherwise titles will be aligned according to +link{Page.isRTL(),text direction},
//         with this method returning <code>"right"</code> if text direction is LTR,
//         or <code>"left"</code> if text direction is RTL.
// </ul>
// @param item (FormItem) item for which we're getting title alignment
// @return (Alignment) alignment for title
// @visibility external
//<
getTitleAlign : function (item) {
    var form = this.form || this; // for ContainerItem method-stealing hack
    return (item.titleAlign ? item.titleAlign :
            this.titleAlign ? this.titleAlign :
            // textDirection: set the direction of the titles according to the text direction
            // if not specified
            this.isRTL() ? isc.Canvas.LEFT : isc.Canvas.RIGHT);
},

//> @method dynamicForm.getTitleVAlign()  (A)
// Get the vertical alignment for the title for this item
//<

getTitleVAlign : function (item) {
    var valign = (item.titleVAlign ? item.titleVAlign :
                  this.titleVAlign ? this.titleVAlign :
                  isc.Canvas.CENTER);
    return (valign == isc.Canvas.CENTER ? isc.Canvas.MIDDLE : valign);
},

// titleHeight / getTitleHeight
// When calculating the size of items for tableResizePolicy, if the title is written into the
// items cell (for titleAlign:top), we need to take the height of the title into account
// so "*" sized items can take up the approprite amount of space.

titleHeight:15,
getTitleHeight : function (item) {
    var form = this.form || this; // for ContainerItem method-stealing hack
    return (item.titleHeight != null ? item.titleHeight : this.titleHeight);
},

//>    @method    dynamicForm.getTitleSpanHTML()    (A)
// Return the HTML for a FormItem's title, wrapping in SPAN rather than a table cell so that it
// doesn't affect the table used for Layout
//
//   @group    drawing
//        @param    item        (FormItem)    Item to show title of.
//        @param    error        (string)    error message for this item
//        @return    (HTML)    HTML output for this element
//<
getTitleSpanHTML : function (item, error) {
    var output = isc.StringBuffer.create();

    output.append("<SPAN ", this._containsItemTitleAttrHTML(item),
                  " CLASS='", item.getTitleStyle(),
                  "' ALIGN=", this.getTitleAlign(item),
                  ">");

    // get the actual title from the item
    output.append(this.getTitleHTML(item, error));

    // now end the title span
    output.append("</SPAN>");
    // and return the whole thing
    return output.release();
},

// Should a specific form item's title be clipped?
shouldClipTitle : function (item) {
    if (!item || !item.form == this) return false;
    return (item.clipTitle != null ? item.clipTitle : !!this.clipItemTitles);
},

//>    @method    dynamicForm.getTitleCellHTML()    (A)
//            Output a title cell for a FormItem.
//        @group    drawing
//
//        @param    item        (FormItem)    Item to show title of.
//        @param    error        (string)    error message for this item
//
//        @return    (HTML)    HTML output for this element
//<

_$heightColon:"height:", _$widthColon:"width:",_$maxWidthColon:"max-width:",
_$maxHeightColon:"max-height:",_$heightColon:"height:",
_$NOBR:"<NOBR>", _$innerTitleTableClose:"</td></tr></TABLE>", _$divClose:"</DIV>", _$tdClose:"</TD>",

_outerTitleCellTemplate:[
    "<TD ", // 0
    , // 1: this._containsItemTitleAttrHTML(item)
    " CLASS='", // 2
    , // 3: className
    "' ALIGN='", // 4
    , // 5: this.getTitleAlign(item)
    "' VALIGN='", // 6
    , // 7: this.getTitleVAlign(item)
    "'", // 8:
    , // 9: possible rowspan
      // NOTE: based on the titleOrientation, this may want to output colSpan OR rowSpan based
      // on the original item size. For now we just respect rowspan
    , // 10: possible colspan
    ">" // 11
],

// When clipping titles, a div is emitted which wraps the block having text-overflow:ellipsis.
// If emitOuterTextOverflow:true, then text-overflow:ellipsis is also applied to the wrapper
// div.
emitOuterTextOverflow: false,

getTitleCellHTML : function (item, error) {
    var output = isc.StringBuffer.create(),
        className = item.getTitleStyle(),
        titleAlign = this.getTitleAlign(item),
        titleVAlign = this.getTitleVAlign(item);

    // get the item title cell start
    var cellTemplate = this._outerTitleCellTemplate;
    cellTemplate[1] = this._containsItemTitleAttrHTML(item);
    cellTemplate[3] = className;
    cellTemplate[5] = titleAlign;
    cellTemplate[7] = titleVAlign;


    var rowSpan = item._rowSpan;
    if (rowSpan == null) rowSpan = item.getRowSpan();
    if (rowSpan > 1) cellTemplate[9] = " ROWSPAN=" + rowSpan;

    else cellTemplate[9] = null;
    if (item.getTitleColSpan() > 1) cellTemplate[10] = " COLSPAN=" + item.getTitleColSpan();
    else cellTemplate[10] = null;



    output.append(cellTemplate.join(isc.emptyString));
    output.append(this.getTitleCellInnerHTML(item, error));

    // now end the title cell
    output.append(this._$tdClose);

    // and return the whole thing
    return output.release();
},

_$top: "top",

// Content of the title cell
getTitleCellInnerHTML : function (item, error) {
    // Use the width / height calculated by TableResizePolicy rather than the specified
    // height / titleWidth properties.
    // Note that this is the total available space for the cell rather than the inner
    // space, so we need to adjust for styling.

    var output = isc.StringBuffer.create(),
        className = item.getTitleStyle(),
        titleAlign = this.getTitleAlign(item),
        titleOrientation = this.getTitleOrientation(item),
        titleWidth = item._titleWidth || null,
        height = item._size ? item._size[1] : null,
        clipTitle = this.shouldClipTitle(item),
        // Unless explicitly specified, wrap unclipped titles, but don't wrap clipped titles
        wrapTitle = (item.wrapTitle != null ? item.wrapTitle :
                    (this.wrapItemTitles != null ? this.wrapItemTitles : !clipTitle));

    if (titleOrientation == this._$top && item._size) {
        titleWidth = Math.max(item._size[0], titleWidth == null ? 0 : titleWidth);
    }



    // Adjust titleWidth/height for padding applied by this.cellPadding this.cellSpacing, &
    // the title class name
    if (height) {
        if (this.cellSpacing) height -= 2*this.cellSpacing;

        var tPadding, bPadding;
        if (className) {
            tPadding = isc.Element._getTopPadding(className, true);
            bPadding = isc.Element._getBottomPadding(className, true);
        }
        if (tPadding == null) tPadding = this.cellPadding || 0;
        if (bPadding == null) bPadding = this.cellPadding || 0;

        height -= (tPadding + bPadding)

        if (className) height -= isc.Element._getVBorderSize(className);
    }

    if (titleWidth) {
        if (this.cellSpacing) titleWidth -= 2*this.cellSpacing;
        var lPadding, rPadding;
        if (className) {
            lPadding = isc.Element._getLeftPadding(className, true);
            rPadding = isc.Element._getRightPadding(className, true);
        }
        if (lPadding == null) lPadding = this.cellPadding || 0;
        if (rPadding == null) rPadding = this.cellPadding || 0;

        titleWidth -= (lPadding + rPadding)
        titleWidth -= isc.Element._getHBorderSize(className);
    }

    var heightProperty = isc.Browser.isMoz ? this._$maxHeightColon : this._$heightColon,
        widthProperty = isc.Browser.isMoz ? this._$maxWidthColon : this._$widthColon;

    if (clipTitle) {
        if (this._titleClipDivTemplate == null) {
            this._titleClipDivTemplate = [
                "<DIV style='overflow:hidden;", // 0
                "white-space:nowrap;",          // 1
                ,                               // 2: possible width
                "' ",                           // 3
                isc.DynamicForm._itemPart,      // 4
                "='",                           // 5
                isc.DynamicForm._title,         // 6
                "' ",                           // 7
                isc.DynamicForm._containsItem,  // 8
                "='",                           // 9
                ,                               // 10: item ID
                "'>"                            // 11
            ];
            if (this.emitOuterTextOverflow) {
                this._titleClipDivTemplate[0] += isc.Browser._textOverflowPropertyName + ":ellipsis;";
            }
        }

        var divTemplate = this._titleClipDivTemplate;

        if (titleWidth != null) divTemplate[2] = widthProperty + titleWidth + "px;";
        else divTemplate[2] = null;

        divTemplate[10] = item.getID();

        output.append(divTemplate.join(isc.emptyString));

    // use NOBR to suppress wrapping. (white-space:nowrap inside a TD works in Moz but not IE)
    } else if (!wrapTitle) {
        output.append(this._$NOBR);
    }
    // get the actual title from the item
    output.append(this.getTitleHTML(item, error, clipTitle));

    if (clipTitle) {
        output.append(this._$divClose);
    }

    // and return the whole thing
    return output.release();
},

// Helper method for item title cell identifiers

_containsItemTitleAttrHTML : function (item) {
    if (!isc.DynamicForm._itemTitleAttrHTML) {
        isc.DynamicForm._itemTitleElementAttrHTML =  [
            " ", isc.DynamicForm._containsItem, "='",
            null,   // item ID
            "' ",
            isc.DynamicForm._itemPart, "='", isc.DynamicForm._title, "' ",
            // Also apply a unique ID so we can grab a pointer to the cell for re-styling
            // without redrawing the form as a whole.
            "ID="
             // title cell ID
        ];
    }
    isc.DynamicForm._itemTitleElementAttrHTML[3] = item.getItemID();
    // [Item ID is unique]
    isc.DynamicForm._itemTitleElementAttrHTML[10] = this._getTitleCellID(item);
    return isc.DynamicForm._itemTitleElementAttrHTML.join(isc.emptyString);
},

_$titleCell:"_titleCell",
_getTitleCellID : function (item) {
    return this._getDOMID(item.getID() + this._$titleCell);
},

getTitleCell : function (item) {
    if (!this.isDrawn()) return null;
    // Ensure we normalize name etc to an item object.
    item = this.getItem(item);
    if (!item) return null;
    return isc.Element.get(this._getTitleCellID(item));
},

// We support custom state-based styles for item titles.
// This method will apply the current style for the title item's title cell
updateTitleCellState : function (item) {
    var titleCell = this.getTitleCell(item);
    if (titleCell == null) return;
    item = this.getItem(item);

    // Apply the style to the cell, and also redraw the content of the cell.
    // This will handle things like:
    // - applying updated style to inner (clipping) table if necessary
    // - applying / clearing required title prefix / suffix
    // - picking up any custom state-based HTML returned by getTitleHTML()
    titleCell.className = item.getTitleStyle();
    titleCell.innerHTML = this.getTitleCellInnerHTML(item, item.getErrors());
},


_$titleClipper:"_titleClipper",
_getTitleClipperID : function (item) {
    return this._getDOMID(item.getID() + this._$titleClipper);
},

_getTitleClipper : function (item) {
    if (!this.isDrawn()) return null;
    item = this.getItem(item);
    if (!item) return null;
    return isc.Element.get(this._getTitleClipperID(item));
},

//> @method dynamicForm.titleClipped()
// Is the title for the given form item clipped? The form item must have title clipping enabled.
//
// @param item (FormItem) the form item.
// @return (boolean) true if the title is clipped; false otherwise.
// @see attr:dynamicForm.clipItemTitles
// @see attr:formItem.clipTitle
// @visibility external
//<
titleClipped : function (item) {
    var titleClipper = this._getTitleClipper(item);
    return (titleClipper != null &&
            isc.Element.getClientWidth(titleClipper) < titleClipper.scrollWidth);
},



_titleClipperTemplate: [
    ,                                          // 0: exclusive title prefix
    "<div style='float:right'>",               // 1
    ,                                          // 2: common title suffix
    "</div><div id='",                         // 3
    ,                                          // 4: "titleClipper" DOM ID
    "' style='overflow:hidden;",               // 5
    isc.Browser._textOverflowPropertyName,     // 6
    ":ellipsis",                               // 7
    (isc.Browser.isIE && !isc.Browser.isStrict ? ";width:100%" : ""), // 8
    "'>",                                      // 9 (note that white-space:nowrap is inherited)
    ,,                                         // 10 & 11: common title prefix, title HTML
    "</div>",                                  // 12
    null                                       // 13: exclusive title suffix
],

//>    @method    dynamicForm.getTitleHTML()    (A)
//    Output the HTML for a title for a FormItem.
//        @group    drawing
//
//        @param    item        (FormItem)    Item to show title of.
//        @param    error        (string)    error message for this item
//
//        @return    (HTML)    HTML output for this element
//<
getTitleHTML : function (item, error, clipTitle) {

    var output = isc.StringBuffer.create();

    // get the title to display

    var title = item.visible ? item.getTitleHTML() : null;
    if (title) {
        var required = this.isRequired(item),
            orientation = this.getTitleOrientation(item),
            leftPrefix = (orientation == isc.Canvas.LEFT || orientation == isc.Canvas.TOP);

        if (clipTitle) {
            var clipperTemplate = this._titleClipperTemplate;

            if (required && this.hiliteRequiredFields) {
                if (leftPrefix) {
                    var exclusiveRequiredTitlePrefix = this.exclusiveRequiredTitlePrefix,
                        exclusiveRequiredTitleSuffix = this.exclusiveRequiredTitleSuffix;
                    if (exclusiveRequiredTitlePrefix == null) {
                        if (this.requiredTitlePrefix.endsWith(this.titlePrefix)) {
                            exclusiveRequiredTitlePrefix = this.requiredTitlePrefix.substring(0, this.requiredTitlePrefix.length - this.titlePrefix.length);
                        } else {
                            exclusiveRequiredTitlePrefix = this.requiredTitlePrefix;
                        }
                    }
                    if (exclusiveRequiredTitleSuffix == null) {
                        if (this.requiredTitleSuffix.startsWith(this.titleSuffix)) {
                            exclusiveRequiredTitleSuffix = this.requiredTitleSuffix.substring(this.titleSuffix.length);
                        } else {
                            exclusiveRequiredTitleSuffix = this.requiredTitleSuffix;
                        }
                    }

                    clipperTemplate[0] = exclusiveRequiredTitlePrefix;
                    clipperTemplate[2] = this.requiredTitleSuffix.substring(0, this.requiredTitleSuffix.length - exclusiveRequiredTitleSuffix.length);
                    clipperTemplate[4] = this._getTitleClipperID(item);
                    clipperTemplate[10] = this.requiredTitlePrefix.substring(exclusiveRequiredTitlePrefix.length);
                    clipperTemplate[11] = title;
                    clipperTemplate[13] = exclusiveRequiredTitleSuffix;
                } else {
                    var exclusiveRequiredRightTitlePrefix = this.exclusiveRequiredRightTitlePrefix,
                        exclusiveRequiredRightTitleSuffix = this.exclusiveRequiredRightTitleSuffix;
                    if (exclusiveRequiredRightTitlePrefix == null) {
                        if (this.requiredRightTitlePrefix.endsWith(this.rightTitlePrefix)) {
                            exclusiveRequiredRightTitlePrefix = this.requiredRightTitlePrefix.substring(0, this.requiredRightTitlePrefix.length - this.rightTitlePrefix.length);
                        } else {
                            exclusiveRequiredRightTitlePrefix = this.requiredRightTitlePrefix;
                        }
                    }
                    if (exclusiveRequiredRightTitleSuffix == null) {
                        if (this.requiredRightTitleSuffix.startsWith(this.rightTitleSuffix)) {
                            exclusiveRequiredRightTitleSuffix = this.requiredRightTitleSuffix.substring(this.rightTitleSuffix.length);
                        } else {
                            exclusiveRequiredRightTitleSuffix = this.requiredRightTitleSuffix;
                        }
                    }

                    clipperTemplate[0] = exclusiveRequiredRightTitlePrefix;
                    clipperTemplate[2] = this.requiredRightTitleSuffix.substring(0, this.requiredRightTitleSuffix.length - exclusiveRequiredRightTitleSuffix.length);
                    clipperTemplate[4] = this._getTitleClipperID(item);
                    clipperTemplate[10] = this.requiredRightTitlePrefix.substring(exclusiveRequiredRightTitlePrefix.length);
                    clipperTemplate[11] = title;
                    clipperTemplate[13] = exclusiveRequiredRightTitleSuffix;
                }
            } else {
                if (leftPrefix) {
                    clipperTemplate[0] = this.exclusiveTitlePrefix;
                    clipperTemplate[2] = this.titleSuffix.substring(0, this.titleSuffix.length - this.exclusiveTitleSuffix.length);
                    clipperTemplate[4] = this._getTitleClipperID(item);
                    clipperTemplate[10] = this.titlePrefix.substring(this.exclusiveTitlePrefix.length);
                    clipperTemplate[11] = title;
                    clipperTemplate[13] = this.exclusiveTitleSuffix;
                } else {
                    clipperTemplate[0] = this.exclusiveRightTitlePrefix;
                    clipperTemplate[2] = this.rightTitleSuffix.substring(0, this.rightTitleSuffix.length - this.exclusiveRightTitleSuffix.length);
                    clipperTemplate[4] = this._getTitleClipperID(item);
                    clipperTemplate[10] = this.rightTitlePrefix.substring(this.exclusiveRightTitlePrefix.length);
                    clipperTemplate[11] = title;
                    clipperTemplate[13] = this.exclusiveRightTitleSuffix;
                }
            }

            output.append.apply(output, clipperTemplate);
        } else {
            // if the title is defined, output the titlePrefix + title + titleSuffix
            output.append(
                (required && this.hiliteRequiredFields ?
                    (leftPrefix ? this.requiredTitlePrefix : this.requiredRightTitlePrefix) :
                    (leftPrefix ? this.titlePrefix : this.rightTitlePrefix))
                , title
                , (required && this.hiliteRequiredFields ?
                    (leftPrefix ? this.requiredTitleSuffix : this.requiredRightTitleSuffix) :
                    (leftPrefix ? this.titleSuffix : this.rightTitleSuffix))
            );
        }
    } else {
        // otherwise just output a space
        //    this prevents us from putting colons next to an empty title item
        output.append("&nbsp;");
    }

    // and return the whole thing
    return output.release();
},


//>    @method    dynamicForm.getFormTagStartHTML()    (A)
//        @group    drawing
//            Return the HTML to start the form object itself.
//        @return    (string)                HTML for the start form tag
//<
_$formTagStartTemplate:[
    "<FORM " ,                              // 0
    "ID",                                   // 1
    "=" ,                                   // 2
    ,                                       // 3: this.getFormID()
    ,                                       // 4: absolute positioning, or null
    " METHOD=",                             // 5
    ,                                       // 6: this.method
    " ACTION='",                            // 7
    ,                                       // 8: this.action
    "' ENCTYPE=",                           // 9
    ,                                       // 10: multipart or normal encoding
    ,                                       // 11: Target= or null
    ,                                       // 12: target or null
    ,                                       // 13: close target quote or null

    " ONSUBMIT='return ",                   // 14
    ,                                       // 15: this.getID()
    "._handleNativeSubmit()' ONRESET='",    // 16
    ,                                       // 17: this.getID()

    // Do our proprietary reset rather than a real native reset.
    // There's no benefit to doing a native reset here, and it breaks certain items such
    // as date items.

    ".resetValues(); return false;'",       // 18


    " STYLE='margin-bottom:0px'",   // 19
    // This is required to send i18n data to server (which assumes UTF-8 encoding)
    " ACCEPT-CHARSET='UTF-8'", //20
    ">"           // 21
],
_$absPosStyle:" STYLE='position:absolute;left:0px;top:0px;'",
_$targetEquals:" TARGET='",
getFormTagStartHTML : function () {
    var template = this._$formTagStartTemplate,
        FormID = this.getFormID(),
        ID = this.getID();
    template[3] = FormID;
    // In order to get an absPos item placed at 0,0 in Moz (but not IE), it's necessary
    // to absolutely position the <FORM> element, or Moz generates an extra line box
    // with this simple structure.  (change font size to verify the extra space is due
    // to a line box)
    // <DIV STYLE='position:absolute;LEFT:0px;TOP:0px;WIDTH:500px;HEIGHT:500px;'
    // ><div style="position:relative;"><form><div
    // style="position: absolute; left: 0px; top: 0px;">foobar</div></form></div>
    if (this._absPos()) template[4] = this._$absPosStyle;
    else template[4] = null;

    template[6] = this.method;
    template[8] = this.action;

    if (this.isMultipart()) template[10] = isc.DynamicForm.MULTIPART_ENCODING;
    else template[10] = isc.DynamicForm.NORMAL_ENCODING;

    if (this.target != null) {
        template[11] = this._$targetEquals;
        template[12] = this.target;
        template[13] = this._$singleQuote;
    } else {
        template[11] = null;
        template[12] = null;
        template[13] = null;
    }


    template[15] = ID;
    template[17] = ID;

    return template.join(isc.emptyString);
},


writeWidthAttribute: false,

//>    @method    dynamicForm.getTableStartHTML()    (A)
//        @group    drawing
//            Return the HTML to start the table drawn around this form.
//        @return    (string)                HTML for the start table tag
//<
_$tableStartTemplate:[
    "<TABLE role='presentation' ID='",          // 0
    ,                       // 1:  this._getTableElementID()


    "' WIDTH='",            // 2
    ,                       // 3: innerContentWidth / innerWidth
    "' CELLSPACING='" ,     // 4
    ,                       // 5: this.cellSpacing
    "' CELLPADDING='" ,     // 6
    ,                       // 7: this.cellPadding
    "' BORDER='",           // 8
    ,                       // 9: this.cellBorder


    (isc.Browser.isMoz ? "'><TBODY>" : "'>") // 10
],
_$widthEquals: "' WIDTH='",
getTableStartHTML : function () {
    // This method is also applied to containerItems
    var isForm = isc.isA.DynamicForm(this),
        template = isForm ? this._$tableStartTemplate
                          : isc.DynamicForm.getPrototype()._$tableStartTemplate;
    template[1] = this._getTableElementID();
    if (this.isPrinting) {
        template[2] = isForm ? this._$widthEquals : isc.DynamicForm.getPrototype()._$widthEquals;
        template[3] = "100%";
    } else if (!!this.writeWidthAttribute) {
        template[2] = isForm ? this._$widthEquals : isc.DynamicForm.getPrototype()._$widthEquals;
        template[3] = (this.getInnerContentWidth != null
                       ? this.getInnerContentWidth()
                       : this.getInnerWidth());
    } else {
        template[3] = template[2] = null;
    }
    template[5] = this.cellSpacing;
    template[7] = this.cellPadding;
    template[9] = this.cellBorder;

    return template.join(isc.emptyString);
},

// Methods to access the table element for this form
_$table:"table",
_getTableElementID : function () {
    return this._getDOMID(this._$table);
},

_getTableElement : function () {
    return isc.Element.get(this._getTableElementID());
},


// Resizing:
// If we're showing any items whos sizes depend on the specified form size,
// redraw on resize to force them to be recalculated and redrawn
layoutChildren : function (a,b,c,d) {
    this.invokeSuper(isc.DynamicForm, "layoutChildren", a,b,c,d);
    var items = this.getItems();
    if (!items) return;
    for (var i = 0; i< items.length; i++) {
        // redraw for any percent sized / "*" width child
        var width = items[i].width, height = items[i].height;
        if (
            (isc.isA.String(width) && (width.contains("%") || width.contains("*"))) ||
             (isc.isA.String(height) && (height.contains("%") || height.contains("*"))) )
        {

            this.markForRedraw("size change with dynamic size children");
            break;
        }
    }
},

getAbsPosHTML : function () {
    var output = isc.SB.create();
    // for each item in the list, get HTML output for it and combine the output
    for (var itemNum = 0, len = this.items.length; itemNum < len; itemNum++) {

        // get a pointer to the item for that field
        var item = this.items[itemNum];
        // if a null item, skip it
        if (!item) continue;
        // note that the value of this item can't possibly be dirty anymore
        item._markValueAsNotDirty();

        // if the item has been marked as invisble, skip it
        if (!item.visible) continue;


        var includeHint = !item._getShowHintInField(),
            includeErrors = this.showInlineErrors
        ;
        output.append(item.getStandaloneItemHTML(item.getValue(), includeHint, includeErrors));
    }

    //this.logWarn("absPos HTML: " + output.toString());

    // Allow the SB to be reused
    return output.release()
},



getScrollWidth : function (recalculate) {
    if (this._deferredOverflow) {
        this._deferredOverflow = null;
        this.adjustOverflow("widthCheckWhileDeferred");
    }
    // reimplement caching code
    // Note: important to use the same cache field name because __adjustOverflow() invalidates it.
    if (!recalculate && this._scrollWidth != null) return this._scrollWidth;

    var width;
    // call super the fast way if we don't have absolutely positioned items
    if (!isc.Browser.isIE || !this._absPos() ||
        !(this.isDrawn() || this.handleDrawn()) || this.items == null)
    {
        width = isc.Canvas._instancePrototype.getScrollWidth.call(this, recalculate);
    } else {
        width = 0;
        for (var i = 0; i < this.items.length; i++) {
            var item = this.items[i];
            if (item.visible == false || !item.isDrawn()) continue;

            var handle = item.getAbsDiv();
            if (handle) {
                var itemRight = handle.scrollWidth + item._getPercentCoord(item.left);
                if (itemRight > width) width = itemRight;
            }
        }
    }
    this._scrollWidth = width;
    return width;
},

getScrollHeight : function (recalculate) {
    if (this._deferredOverflow) {
        this._deferredOverflow = null;
        this.adjustOverflow("heightCheckWhileDeferred");
    }
    // reimplement caching code
    // Note: important to use the same cache field name because __adjustOverflow() invalidates it.
    if (!recalculate && this._scrollHeight != null) return this._scrollHeight;

    var height;
    // call super the fast way if we don't have absolutely positioned items
    if (!isc.Browser.isIE || !this._absPos() ||
        !(this.isDrawn() || this.handleDrawn()) || this.items == null)
    {
        height = isc.Canvas._instancePrototype.getScrollHeight.call(this, recalculate);
    } else {
        height = 0;
        for (var i = 0; i < this.items.length; i++) {
            var item = this.items[i];
            if (item.visible == false || !item.isDrawn()) continue;

            var handle = item.getAbsDiv();
            if (handle) {
                var itemBottom = handle.scrollHeight + item._getPercentCoord(item.top, true);
                if (itemBottom > height) height = itemBottom;
            }
        }
    }
    this._scrollHeight = height;
    return height;

},

// Submitting
// --------------------------------------------------------------------------------------------

// _formWillSubmit() - will this form perform a direct submission
// If true we need to ensure we write out native elements for each form item
// (using hidden elements if necessary)
// Note that we need to consider 2 kinds of direct submission:
// - if this.canSubmit is true, and the user hits a submit button (or 'submit()'/ 'submitForm()'
//   are called, we're performing a completely standard HTML direct submission to the
//   action URL specified by the developer
// - We also in some cases use direct submission to convey RPC operations.
//   Cases where this occurs when saveData() is called:
//      - this.canSubmit is true
//      - this.action has been specified (differs from the class prototype value)
//      - isMultipart() is true
// In each of these cases return true to indicate a direct submission will occur
_formWillSubmit : function () {
    return this.canSubmit || this.isMultipart() ||
            (this.action != isc.DynamicForm.getPrototype().action);
},

//>    @method    dynamicForm.submitForm()    ([])
// Submits the form to the URL defined by +link{dynamicForm.action},
// identically to how a plain HTML &lt;form&gt; element would submit data,
// as either an HTTP GET or POST as specified by +link{dynamicForm.method}.
// <P>
// <b>Notes:</b>
// <ul>
// <li>this is used only in the very rare case that a form is used to submit data
// directly to a URL.  Normal server contact is through
// +link{group:dataBoundComponentMethods,DataBound Component Methods}.</li>
// <li>For this method to reliably include values for every field in the grid,
//      +link{DynamicForm.canSubmit} must be set to <code>true</code></li>
// <li>To submit values for fields that do not have an editor, use +link{HiddenItem}
// with a +link{formItem.defaultValue} set.  This is analogous to &lt;input type="hidden"&gt;
// in HTML forms.
// </ul>
//      @visibility external
//        @group    submitting
//<
submitForm : function () {
    if (!this._formWillSubmit()) {
        this.logWarn("Attempt to perform direct submission on DynamicForm where this.canSubmit " +
                     "is false. Please set this property to true, or use the standard databinding " +
                     "interfaces to send data to the server.");
    }

    // If we have a FileItem as an item in this form warn that we won't save its value and ignore
    // it. This is appropriate since FileItemForms are intended to be used with a SC Server backed
    // dataSource only and go through the saveData() codepath. We can't apply our values to the
    // FileItemForm and submit it directly since it doesn't have html form items for our various
    // values so will fail to commit them to the server.
    if (this.getFileItemForm() != null) {
        this.logWarn("Performing a direct submission on a DynamicForm containing a FileItem. " +
                    "Note: This item's value will not be submitted to the server.  FileItems " +
                    "are intended for use with databound forms backed by the SmartClient server " +
                    "only.  If you are not using the SmartClient Databinding subsystem, " +
                    "use an UploadItem rather than a FileItem to submit a file as part of a raw " +
                    "HTTP request. Otherwise use saveData() rather than a direct call to " +
                    "submitForm() to save the full set of values for the form.");
    }

    var form = this.getForm();
    if (!form) return;
    // Update the action laziliy if necessary - required for the case where it has been modified
    // after draw

    if (form.action != this.action) form.action = this.action;

    // In IE, having a partially populated uploadItem on a form, and then attempting to submit
    // the form via a call to form.submit() throws an Access Denied JS error
    // http://support.microsoft.com/kb/892442
    // Trap this case and log a warning

    try {
        return form.submit();
    } catch (e) {
        this.logWarn("Form submission was unsuccessful. In some browsers this can occur when " +
            "an upload item is present and has an invalid value.\n" + e.message);
        // We could fire a generic 'submission failed' handler here.
        // Developers can override this to warn the user in a way that makes sense for their
        // application.
        this.formSubmitFailed();
    }
},

// when implicitSave is true, this method is called by changed formItems at editorExit(), or
// after a pause in editing specified by implicitSaveDelay
performImplicitSave : function (item, onPause) {
    this.implicitSaveInProgress = true;

    if (item) {
        if (item._shouldUpdateParentItem) {
            item.parentItem.updateValue();
        }
        if (item._fireOnPauseTimer != null) isc.Timer.clear(item._fireOnPauseTimer);
    }

    if (this.awaitingImplicitSave) delete this.awaitingImplicitSave;
    this.logInfo("performImplicitSave called " +
        (!onPause ? "by editorExit()" : "after implicitSaveDelay (" + this.implicitSaveDelay + "ms)") +
        " for item " + item.name + ".");

    if (this.valuesManager) {
        // we have a valuesManager - since this is an implicitSave, we want a proper save to occur,
        // so trigger the VM to save, which causes it to gather changed values from all members,
        // including this one.
        this.valuesManager.saveData(this.getID()+"._implicitSaveCallback(data)", {showPrompt: false});
    } else {
        this.saveData(this.getID()+"._implicitSaveCallback(data)", {showPrompt: false});
    }
},

_addItemToImplicitSaveUpdateArray : function (item) {
    var storage = this.valuesManager ? this.valuesManager : this;
    if (!storage.itemsToUpdateState) storage.itemsToUpdateState = [];
    item.awaitingImplicitSave = true;
    storage.itemsToUpdateState.add(item);
    item.updateState();
},

_implicitSaveCallback : function (data) {
    delete this.implicitSaveInProgress;
    var storage = this.valuesManager ? this.valuesManager : this;
    if (storage.itemsToUpdateState) {
        for (var i=0; i< storage.itemsToUpdateState.length; i++) {
            var item = storage.itemsToUpdateState[i];
            delete item.awaitingImplicitSave;
            item.wasAwaitingImplicitSave = true;
            item.updateState();
        }
        delete storage.itemsToUpdateState;
    }
    this.implicitSaveCallback(data);
},



// default empty implementation in case devs switch implicitSave on without providing an override of this
implicitSaveCallback : function (data) {},

//> @attr DynamicForm.formSubmitFailedWarning (String : "Form was unable to be submitted. The most likely cause for this is an invalid value in an upload field." : IRWA)
// Warning to display to the user if an attempt to +link{dynamicForm.submitForm,natively submit} a
// form is unable to submit to the server. The most common cause for this failure is that the user
// has typed an invalid file-path into an upload type field.
// @visibility external
// @group i18nMessages
//<
formSubmitFailedWarning:"Form was unable to be submitted. The most likely cause for this is an " +
                        "invalid value in an upload field.",

//> @method DynamicForm.formSubmitFailed() [A]
// Method called when an attempt to +link{dynamicForm.submitForm,natively submit} a
// form is unable to submit to the server. Default behavior is to display the
// +link{formSubmitFailedWarning} in a warning dialog.
// The most common cause for this failure is that the user
// has typed an invalid file-path into an upload type field.
// @visibility external
// @group i18nMessages
//<
// Also cleans up pending RPCManager transactions if this form was doing a submit type transaction
formSubmitFailed : function () {
    isc.warn(this.formSubmitFailedWarning);
    // go a step further - if this was an attempt to commit an RPCManager transaction
    // we can cancel it so we don't hang with a prompt, or pop a timeout warning in a minute or 2
    var transactionText = this.getValues()._transaction;
    if (transactionText != null && isc.RPCManager && isc.XMLTools) {
        var doc = isc.XMLTools.parseXML(this.getValues()._transaction),
            transactionNum;
        if (doc) transactionNum = isc.XMLTools.selectNumber(doc, "//transactionNum");
        if (transactionNum != null) {

            isc.RPCManager.doClearPrompt(transactionNum);
            isc.RPCManager.clearTransaction(transactionNum);
        }

        var transactionItem = this.getItem("_transaction");
        if (transactionItem && isc.isA.HiddenItem(transactionItem)) {
            this.clearValue("_transaction");
        }
    }
},

//> @method DynamicForm.setAction()
// Sets the +link{DynamicForm.action,action} for this form.
// @param action (URL) New action URL
// @visibility external
//<
// @param autoGenerated (boolean) Was this action auto-generated by the SmartClient databinding
// system or explicitly specified by a developer?

setAction : function (action, autoGenerated) {
    this.action = action;
    var form = this.getForm();
    if (form) form.action = action;
    this._explicitAction = !autoGenerated;
},

//> @method DynamicForm.setTarget()
// Sets the +link{DynamicForm.target,target} for this form.
// @param target (string) New submission target
// @visibility external
//<
setTarget : function (target) {
    this.target = target;
    var form = this.getForm();
    if (form) form.target = target;
},


//> @method DynamicForm.setMethod()
// Sets the +link{DynamicForm.method,method} for this form.
// @param method (FormMethod) html form submission method (get or post)
// @visibility external
//<
setMethod : function (method) {
    this.method = method;
    var form = this.getForm();
    if (form) form.method = method;
},


// If we have a FileItem in this form, this helper method will return a pointer to its form
getFileItemForm : function () {
    if (!isc.FileItem) return null;
    var items = this.getItems() || [];
    for (var i = 0; i < items.length; i++) {
        if (isc.isA.FileItem(items[i])) {
            var fileItemCanvas = items[i].canvas;
            // Make sure that the FileItem's canvas is a DynamicForm before returning it because
            // there are cases where the canvas is not a form (for example, if the FileItem is
            // read-only).
            if (isc.isA.DynamicForm(fileItemCanvas)) return fileItemCanvas;
        }
    }
    return null;
},


_propagateOperationsToFileItem : function() {
    var form = this.getFileItemForm();
    if (form != null) {
        form.fetchOperation = this.fetchOperation;
        form.updateOperation = this.updateOperation;
        form.addOperation = this.addOperation;
        form.removeOperation = this.removeOperation;
    }
},


// _handleNativeSubmit.
// This method is fired from the onsubmit handler for the HTML form for this DynamicForm widget.
// The onsubmit handler will fire whenever a user action would normally trip a form submission
// These cases are:
// - If there's a submit element on the form and the user clicks it
// - If there's a submit element on the form and the user is focused in a Text item, and
//   hits enter.
// - If there's a single text element in the form only (even if there is no submit item) and
//   the user hits enter while focused in it
// We disallow native submission by returning false from this method in each of these cases
// because:
// - we never write out a native submit element (our submitItem is a buttonItem subclass)
// - we have our own more reliable handling for submitting on Enter keypress, explicitly handled
//   by our keypress handler.
// Note that onsubmit does NOT fire when form.submit() is called programmatically, so this has
// no effect except on the user interactions listed above.
// We can therefore always return false to suppress this event.
_handleNativeSubmit : function () {
    return false;
},



// Validation
// --------------------------------------------------------------------------------------------

//>    @method    dynamicForm.validate()  ([])
// Validates the form without submitting it, and redraws the form to display error messages
// if there are any validation errors. Returns true if validation succeeds, or false if
// validation fails.<br>
// For databound forms, any Datasource field validators will be run even if there is no
// associated item in the form.<br>
// Validators will also be run on hidden form items<br>
// In both these cases, validation failure can be handled via
// +link{DynamicForm.handleHiddenValidationErrors()}
// <P>
// If this form has any fields which require server-side validation
// (see +link{Validator.serverCondition}) this will also be initialized. Such validation will
// occur asynchronously.
// Developers can use +link{dynamicForm.isPendingAsyncValidation()} and
// +link{dynamicform.handleAsyncValidationReply()} to detect and respond to asynchronous validation.
//
// @param validateHiddenFields (boolean) Should validators be processed for non-visible fields
//         such as dataSource fields with no associated item or fields with visibility set to
//         <code>"hidden"</code>?
// @return (boolean) true if validation succeeds, or false if validation fails.
// @visibility external
// @group    validation
// @example validationType
//<



// checkValuesOnly parameter - if passed we're not going to store errors on the form or display
// them - simply pick up the error values and return them. Called by the 'valuesAreValid()' method



validate : function (validateHiddenFields, ignoreDSFields, typeValidationsOnly,
                    checkValuesOnly, skipServerValidation, suppressShowErrors)
{
    if (this.disableValidation) return true;

    // skip validation if we're databound and our datasource has validation disabled
    if (this.dataSource && this.dataSource.useLocalValidators != null &&
        this.useLocalValidators == false) return true;

    var hadErrorsBefore = this.hasErrors(),   // remember if we had errors before
                                              // so we'll redraw the form if this
                                              // validation pass finds no errors
        errorsFound = false,
        form = this.getForm(),
        hasChanges = false
    ;

    // We need to validate:
    // - form items with validators
    // - values that map to DS fields with validators.
    // (we don't need to worry about values with no associated field as there is no way to
    //  specify validators for such fields)
    var errors = {},
        hiddenErrors = {},
        values = this.getValues(),
        record = values,
        // fields are returned from ds in {fieldName:fieldObject} format
        dsFields = (validateHiddenFields && !ignoreDSFields && this.dataSource)
                        ? isc.addProperties({}, this.getDataSource().getFields())
                        : null
    ;

    // If we are embedded in a valuesManager, the record being edited may be split over
    // several forms. Pick up the "record" via a getValues() call on the valuesManager
    // so we can refer to other fields, primary key, etc.
    if (this.valuesManager != null) {
        record = this.valuesManager.getValues();
        if (this.dataPath != null) {
            record = isc.DynamicForm._getFieldValue(this.dataPath, null, record, this, true);

        }
    }

    // Validate each form item
    // Note that when validating ContainerItem (e.g. DateItem) form items, only the
    // ContainerItem itself is validated, and not any of its sub-items.
    var validationOptions = {unknownErrorMessage: this.unknownErrorMessage,
                             serverValidationMode: "full"};
    if (typeValidationsOnly)
        validationOptions.typeValidationsOnly = typeValidationsOnly;
    if (skipServerValidation)
        validationOptions.skipServerValidation = skipServerValidation;

    // Wrap field validation in a queue so that server validators are
    // sent as a single request.
    var wasAlreadyQueuing = isc.rpc ? isc.rpc.startQueue() : false;

    for (var itemNum = 0; itemNum < this.items.length; itemNum++) {
        var fieldErrorsFound = false,
            // get the field item
            item = this.items[itemNum],
            // get the name of this column in the values
            column = item.getFieldName(),
            // get the dataPath so we can perform validation with dataPath

            dp = item.getTrimmedDataPath() || item.getFieldName(),
            // get the value of this item
            value = item.getValue(),
            hidden = !item.visible || isc.isA.HiddenItem(item)
        ;

        if (hidden && !validateHiddenFields) continue;

        if (item.validators != null) {

            // normalize item.validators to an array.
            if (!isc.isAn.Array(item.validators)) {
                item.validators = [item.validators];
            }

            // Perform actual validation.
            var fieldResult = this.validateField(item, item.validators, value,
                                                 record, validationOptions);
            if (fieldResult != null) {
                if (fieldResult.errors != null) {
                    fieldErrorsFound = this.addValidationError(errors, column || dp,
                                                                fieldResult.errors);
                    if (fieldErrorsFound) errorsFound = true;
                }

                // if the validator returned a resultingValue, use that as the new value
                // whether the validator passed or failed.  This lets us transform data
                // (such as with the mask validator).
                if (fieldResult.resultingValue != null) {
                    // remember that value in the values list
                    value = fieldResult.resultingValue;
                    if (dp) {
                        isc.DynamicForm._saveFieldValue(dp, item, value, values, this, true);
                    } else if (column) {
                        values[column] = value;
                    }
                    hasChanges = true;
                }
            }
        }

        // If the item is not visible, copy the errors so we can run a method to let the
        // developer handle errors on hidden fields
        // Note that this includes 'hiddenItems' that are not marked as visible:false
        if (hidden && fieldErrorsFound) hiddenErrors[column || dp] = errors[column || dp];

        // Validators applied to an item are a superset of the validators applied to
        // a dataSource field - therefore no need to run DSField validators for this field

        if (dsFields) delete dsFields[column];
    }

    // If we are attached to a rules engine, notify it that we are performing validation.
    // This gives it a chance to re-run any validators it has in its rulesData that apply to
    // our specific fields
    if (this.rulesEngine != null) {
        var rulesErrors = this.rulesEngine.applyFieldValidators(errors, this);
        if (rulesErrors) errorsFound = true;
    }


    // Explicitly run through datasource field validators
    if (dsFields) {
        // Unless we're looking at a 'required' or 'requiredIf' field,
        // don't try to validate null values.
        validationOptions.dontValidateNullValues = true;
        // We want to process all validators
        delete validationOptions.typeValidationsOnly;

        for (var i in dsFields) {

            var fieldObject = dsFields[i],
                fieldName = i,
                validators = fieldObject.validators,
                value = values[fieldName]
            ;

            if (validators != null) {
                var value = values[fieldName];

                // Validate the dataSource field
                var fieldResult = this.validateField(fieldObject, validators, value,
                                                     values, validationOptions);
                if (fieldResult != null && fieldResult.errors != null) {
                    this.addValidationError(errors, fieldName, fieldResult.errors);
                }
            }

            if (errors[fieldName] != null) hiddenErrors[fieldName] = errors[fieldName];
        }
    }




    // Submit server validation requests queue
    if (!wasAlreadyQueuing && isc.rpc) isc.rpc.sendQueue();

    //>DEBUG
    if (errorsFound) this.logInfo("Validation errors: " + isc.Log.echoAll(errors));
    //<DEBUG

    if (checkValuesOnly) return (errorsFound ? errors : true);

    // set the error messages for the form whether any were found or not
    this.setErrors(errors);


    // if validation changes values, update the visible values in the form elements, which will
    // automatically update this.values
    if (hasChanges) {
        this.setItemValues(values, null, null, null, true);
        // directly save values for which there are no form elements
        for (var field in values) {
            if (this.getItem(field) == null) this._saveValue(field, values[field]);
        }
    }

    // redraw if we found new errors or if we previously had errors which must be cleared from view
    if (!suppressShowErrors && (errorsFound || hadErrorsBefore)) {
        this.showErrors(errors, hiddenErrors);
    }

    return !errorsFound;
},

//> @method DynamicForm.valuesAreValid()
// Method to determine whether the current form values would pass validation.
// This method will run validators on the form's values and return a value indicating whether
// validation was successful.
// <P>
// Unlike +link{DynamicForm.validate()} this method will not store the errors on the DynamicForm
// or display them to the user.
// @param validateHiddenFields (boolean) Should validators be processed for non-visible fields
//         such as dataSource fields with no associated item or fields with visibility set to
//         <code>"hidden"</code>?
// @param returnErrors (boolean) If unset, this method returns a simple boolean value indicating
// success or failure of validation. If this parameter is passed, this method will return
// an object mapping each field name to the errors(s) encountered on validation failure, or null
// if validation was successful.
// @return (boolean | object | null) Boolean value indicating validation success, or if
// <code>returnErrors</code> was specified, an object containing the generated errors mapped to
// their fieldNames.
// @visibility external
// @group validation
//<
valuesAreValid : function (validateHiddenFields, returnErrors) {
    var errors = this.validate(validateHiddenFields, null, null, true);

    if (errors === true) {
        return (returnErrors ? null : true);
    } else {
        return (returnErrors ? errors : false);
    }

},

//> @method DynamicForm.getValidatedValues()
// Call +link{dynamicForm.validate()} to check for validation errors. If no errors are found,
// return the current values for this form, otherwise return null.
// @return (object|null) current values or null if validation failed.
// @group errors
// @visibility external
//<
getValidatedValues : function () {
    // validate the form
    // This will cause the form to redraw automatically if it has new errors
    // (or it had errors before and doesn't now).

    if (!this.validate()) return null;
    return this.getValues();
},

//> @method DynamicForm.showErrors()
// If this form has any outstanding validation errors, show them now.<br>
// This method is called when the set of errors is changed by +link{dynamicForm.setErrors()} or
// +link{dynamicForm.validate()}.<br>
// Default implementation will redraw the form to display error messages and call
// +link{DynamicForm.handleHiddenValidationErrors(), handleHiddenValidationErrors()} to
// display errors with no visible field.<br>
// Note that this method may be overridden to perform custom display of validation errors.
// @group errors
// @visibility external
//<
// Additional 'errors' / 'hiddenErrors' parameters
// Used internally when we have just calculated the errors, as well as which fields are visible
// and which are hidden
// contains an object of fieldName to error mappings for fields that are not visible.
// Not public - if this method is being called by the user, always re-calculate which fields are
// visible /hidden. This is cleaner than tracking the hidden errors in a separate object as we'd
// have to update that each time fields were shown / hidden, etc.
showErrors : function (errors, hiddenErrors) {

    var suppressAutoFocus = !this.autoFocusOnError || this._suppressAutoFocusOnErrors;
    if (this._suppressAutoFocusOnErrors) delete this._suppressAutoFocusOnErrors;

    var undef;
    if (hiddenErrors === undef) hiddenErrors = this.getHiddenErrors();
    if (errors === undef) errors = this.getErrors();

    // If we have errors and we're not showing them inline, we need to auto-generate a blurb
    // item at the top of the form to display the errors.
    // Do this in showErrors only - this way if showInlineErrors is set to false, and this
    // method is overridden the developer will be suppressing this default approach.
    if (errors && !this.showInlineErrors &&
        (!this._errorItem || this._errorItem.destroyed || !this.items.contains(this._errorItem)))
    {
        this.createErrorItem();
    }

    // Redraw whether there are outstanding errors or not. This means that this method will
    // also clear visible errors that have been resolved.
    this.markForRedraw("Validation Errors Changed");

    if (errors && !isc.isAn.emptyObject(errors) && !suppressAutoFocus) {
        for (var fieldName in errors) {
            var item = this.getItem(fieldName);
            // if an error item was found, set the focus to that item

            if (item && item.isVisible() && item.isDrawn()) {
                this._focusInItemWithoutHandler(item);
                break;
            }
        }
    }
    // if we're showing the blurb at the top of the form scroll it into view.
    // Do this on a delay to allow IE to asynchronously complete focusing in the first error item
    if (!this.showInlineErrors) {
        this.delayCall("scrollIntoView", [0,0], 100);
    }

    if (hiddenErrors) {
        this._handleHiddenValidationErrors(hiddenErrors);
    }
},

getHiddenErrors : function () {
    if (!this.errors) return null;
    var hasHiddenErrors = false, hiddenErrors = {};

    for (var fieldName in this.errors) {
        var item = this.getItem(fieldName);
        if (!item || !item.visible) {
            hasHiddenErrors = true;
            hiddenErrors[fieldName] = this.errors[fieldName];
        }
    }
    return (hasHiddenErrors ? hiddenErrors : null);
},

//> @method DynamicForm.showFieldErrors ()
// If this form has any outstanding validation errors for the field passed in, show them now.
// Called when field errors are set directly via +link{dynamicForm.setFieldErrors()} /
// +link{dynamicForm.addFieldErrors} / +link{dynamicForm.clearFieldErrors()}.<br>
// Default implementation simply falls through to +link{dynamicForm.showErrors()}.
// @param fieldName (string) field to show errors for
// @group errors
// @visibility external
//<
// This can be called if errors are being updated individually on a per field basis.
// Note that calling handleHiddenVlaidationErrors will actually fire the handler and pass in
// the full set of hidden errors. We could have a more fine grained method
// like 'handleHiddenFieldValidationErrors()' instead.

showFieldErrors : function (fieldName, suppressAutoFocus) {

    // 'null' has meaning to showErrors so use explicit undefined instead
    var undef;
    if (suppressAutoFocus) this._suppressAutoFocusOnErrors = true;
    return this.showErrors();
},

// _handleHiddenValidationErrors()
// Internal method to display validation errors when we can't show them in a form.
// This is used to handle
// - errors coming from hidden form items
// - errors coming from a dataSource field for which we have no form item.
_handleHiddenValidationErrors : function (errors) {
    if (errors == null || isc.isAn.emptyObject(errors)) return;

    // If we have an implementation to handle the hidden validation errors, call it now.
    var returnVal;
    if (this.handleHiddenValidationErrors) {
        returnVal = this.handleHiddenValidationErrors(errors);
    }


    // returning false suppresses the log warn statement
    if (returnVal == false) return;

    var errorString = "Validation errors occurred for the following fields " +
                        "with no visible form items:";

    for (var fieldName in errors) {
        var fieldErrors = errors[fieldName];
        if (!isc.isAn.Array(fieldErrors)) fieldErrors = [fieldErrors];
        if (fieldErrors.length == 0) continue;

        errorString += "\n" + fieldName + ":";
        for (var i = 0; i < fieldErrors.length; i++) {
            errorString += (i == 0 ? "- " : "\n - ") + fieldErrors[i];
        }
    }

    this.logWarn(errorString, "validation");
},

isRequired : function (item) {
    return (isc.DynamicForm.canEditField(item, this) &&
            (item.required ||  // marked required is form or DS fields
             item._required || // currently required due to requiredIf
             // XML element is required and we are treating that as meaning required
             this.isXMLRequired(item))
           );
},

//>    @method    dynamicForm.setRequiredIf()    (A)
// Iterate through the items, setting the _required property of any item with a requiredIf
// to correspond to the evaluation that property
//
//            some fields may become required or not required
//        @group    validation
//<

_$requiredIf:"requiredIf",
_$required:"required",
setRequiredIf : function () {
    var values = this.getValues();

    // if any fields have 'requiredIf' set, set their required property now
    for (var itemNum = 0; itemNum < this.items.length; itemNum++) {
        var item = this.items[itemNum],
            validators = item.validators
        ;
        // Ensure if a 'required'/'requiredIf'
        // validator gets removed we don't keep stale "_required" flags around!
        delete item._required;
        // if item is not visible or it has no validators, skip it
        if (!item.visible || !validators || validators.length == 0) continue;

        for (var v = 0; v < validators.length; v++) {
            var validator = validators[v];
            if (!validator) continue;
            var type = isc.Validator.getValidatorType(validator);
            if (type == this._$requiredIf) {
                var value = item.getValue();
                // CALLBACK API:  available variables:  "item,validator,value"
                // Convert a string callback to a function
                if (validator.expression != null && !isc.isA.Function(validator.expression)) {
                    isc.Func.replaceWithMethod(validator, "expression",
                                                     "item,validator,value");
                }

                // set the hidden value for item._required according to the results of the
                // expression
                item._required = validator.expression.apply(this, [item, validator, value]);
            // if an explicit 'required' validator was specified but the field wasn't marked
            // as required:true, set the _required flag so we still show the required styling
            // on the title, etc.
            } else if (type == this._$required) {
                item._required = true;
            }
        }
    }
},



//>    @method    dynamicForm.setFocusItem()    (A)
//  Internal method used to track which form item last had focus.
//  The focusItem is updated with this method whenever an item receives focus.  When focus()
//  is called on the form, the focusItem will then be given focus.
//  Can be retrieved via 'getFocusSubItem()' [or 'getFocusItem()' if we don't want sub items
//  of containerItems], and cleared via 'clearFocusItem()'
//  Note that the focusItem may not currently have focus - focus could be on another widget.
//  Check formItem.hasFocus to see if an item currently has focus.
//
//        @group eventHandling, focus
//        @param    item (formItem)    item to focus in
//<
setFocusItem :  function (item) {
    // normalize the item passed in
    item = this.getItem(item);
    this._focusItem = item;
},

//>    @method    dynamicForm.getFocusItem()    (A)
// Return the current focus item for this form. If this form is drawn and has focus,
// this is the currently focused item. If the form does not have focus or is undrawn this
// is the item that last had focus, or would have focus if the item were drawn/given focus.
// Therefore note that this method can validly return an item which doesn't currently have focus.
// <P>
// May be null.
//
// @group eventHandling, focus
//
// @return (FormItem) returns the item that has the focus, or null if no item is currently focused
// @visibility external
//<
getFocusItem : function () {
    var item = this.getFocusSubItem();
    while (item && item.parentItem != null) {
        item = item.parentItem;
    }
    return item;
},

// For container items, we actually store the focusable sub item rather than
// the containerItem.
// This is what we typically use internally as this is where we'll explicitly put focus
// on redraw, etc.
getFocusSubItem : function () {
    return this._focusItem;
},

// Override _readyToFocus() -- if this DF is not drawn, it may still be appropriate to give it
// focus as it's items may be written into a container widget.
_readyToSetFocus : function () {

    return !this.isDisabled();


},

// Override 'setFocus()' to update item focus.
setFocus : function (hasFocus) {
    if (!this._readyToSetFocus()) return;
    var visible = this.isVisible();
    if (hasFocus) {

        // focus back in the last focus item if there is one.
        var item = this.getFocusSubItem();
        if (item == null) {
            var items = this.getItems();
            if (items != null) {
                for (var i = 0; i < items.length; i++) {
                    var testItem = items[i];
                    if (testItem._canFocus() && testItem.isDrawn() &&
                        testItem.isVisible() && !testItem.isDisabled())
                    {
                        item = testItem;
                        break;
                    }
                }
            }
        }

        // If we got a click on the form item background don't force focus into the current focus
        // item.

        var event = isc.EH.lastEvent;
        if (item != null && !(event.target == this && event.eventType == isc.EH.MOUSE_DOWN)) {
            // No need to call Super because focusing in the item will trigger the
            // elementFocus() method which updates this.hasFocus, etc.
            return this.focusInItem(item);
        }
    }
    this.Super("setFocus", arguments);
    // Override 'blur()' to take focus away from the focus item, as well as clear out
    // this.hasFocus.
    if (!hasFocus) {

        // Note we use the internal _blurItem() method to avoid clearing out this._focusItem.
        // This means a subsequent 'focus()' call on this form will restore focus to the same
        // item.
        this._blurItem(this.getFocusSubItem());

    }
},

// Override focusInNextTabElement() to put focus in the next form item if possible, before
// moving to the next widget on the page

_focusInNextTabElement : function (forward, mask, skipItems, item) {
    if (skipItems || !this.items || this.items.length == 0 ||
        (mask && isc.EH.targetIsMasked(this, mask)))
    {
        this.logInfo("DynamicForm - focusInNextTabElement() running. Delegating to Super()",
                     "syntheticTabIndex");
        return this.Super("_focusInNextTabElement", arguments);
    }

    // Determine the current focus item - if we don't have one, focus in the first item if
    // we're moving forward, or the last item if we're moving backwards
    var items = this.items;
    // Support being passed an explicit "item" param.

    if (item == null) item = this.getFocusSubItem();

    if (item == null) {
        this.logInfo("DynamicForm - focusInNextTabElement() running. Focusing at end.",
                     "syntheticTabIndex");

        this.focusAtEnd(forward);
        return;
    }


    // Allow the focus to be shifted WITHIN an item

    while (item.parentItem) {
        if (item._moveFocusWithinItem(forward)) {
            this.logInfo("DynamicForm - focusInNextTabElement() - allowed:" + item
                + " to shift focus internally.",
                        "syntheticTabIndex");

            return;
        }
        item = item.parentItem;
    }
    // one more check in case there was no parent item
    if (item._moveFocusWithinItem(forward)) {
        this.logInfo("DynamicForm - focusInNextTabElement() running. allowed:" + item
            + " to shift focus internally.",
                     "syntheticTabIndex");

        return;
    }

    item = this._getNextFocusItem(item, forward);
    this.logInfo("DynamicForm - focusInNextTabElement() moving to next item:" + item
                + ", forward?" + forward, "syntheticTabIndex");

    // either focus in the next item, or shift to the next widget.
    // The "focusAtEnd" parameter is used by multi-tab-stop items such as CanvasItems.
    // Ensure we focus at the start (or end) of the item as appropriate.
    if (item != null) {
        this.focusInItem(item, forward);
    } else {

        // In this case we've reached the end of our items.
        // We basically want to call this.Super() to continue to the next widget.
        // Exception: If this form is the only focusable thing on the page, that method will
        // call 'focus' in this form again (as it's both the first and last focusable widget
        // on the page!)... In this case, default focus behavior would mean focus would stay
        // in the current focus item, but we'd actually like to move back to the start of
        // our items. Explicitly catch and handle this case.
        if (isc.EH._firstTabWidget == this && isc.EH._lastTabWidget == this) {
            this.focusAtEnd(forward);
        } else {
            return this.Super("_focusInNextTabElement", arguments);
        }
    }
},

// _getNextFocusItem()
// Give a current item with focus - determine which item focus will next go to in response
// to Tab / shift+Tab

_getNextFocusItem : function (item, forward) {
    var items = this.items,
        originalItem = item,
        currentTabIndex = item.getGlobalTabIndex(),
        nextItem, nextTabIndex,
        index = items.indexOf(item);
    for (var i = 0; i < items.length; i++) {
        var otherItem = items[i];
        if (otherItem == item) continue;
        var gti = otherItem.getGlobalTabIndex();
        if (gti < 0) {
            continue;
        }
        if (!this._canFocusInItem(otherItem,true)) continue;
        if (forward) {
            // special case -- matching global tab index should go in the order in which
            // items are defined
            if (gti == currentTabIndex && i > index) {
                nextItem = otherItem;
                break;
            }
            if (gti > currentTabIndex &&
                (nextTabIndex == null || nextTabIndex > gti))
            {
                nextItem = otherItem;
                nextTabIndex = gti
            }
        } else {
            if ((gti < currentTabIndex || (gti == currentTabIndex && index > i)) &&
                (nextTabIndex == null || nextTabIndex <= gti))
            {
                nextItem = otherItem;
                nextTabIndex = gti;
            }
        }
    }
    return nextItem;
},

_getStartItemForFocusAtEnd : function (start) {
    if (!this.items) return;
    var startItem,
        index,
        items = this.items;

    for (var i = 0; i < items.length; i++) {
        var item = items[i],
            gti = item.getGlobalTabIndex();
        if (gti < 0 || !this._canFocusInItem(item,true)) continue;
        if ((index == null) ||
            (start && gti < index) ||
            (!start && gti >= index))
        {
            startItem = item;
            index = gti;
        }
    }

    if (startItem && this._canFocusInItem(startItem, true)) return startItem;
},

// Set the focus to the first or last focusable item
// start param indicates which end we want to focus in (if true go for the start of the items
// array)
focusAtEnd : function (start) {
    var startItem = this._getStartItemForFocusAtEnd(start);

    if (startItem) this.focusInItem(startItem, !!start);
    // Handle the case where we have no focusable items - in this case just shift on to the
    // next focusable widget instead.
    else {
        var mask,
            registry = isc.EH.clickMaskRegistry;
        if (registry) {
            for (var i = registry.length -1; i >= 0; i--) {
                if (isc.EH.isHardMask(registry[i])) {
                    mask = registry[i];
                    break;
                }
            }
        }
        this._focusInNextTabElement(start, mask, true);
    }
},




// Helper - can we currently call 'focus' on an item?
_canFocusInItem : function (item, tabStop) {
    if (isc.isA.String(item)) item = this.getItem(item);
    return item && item._canFocus() && item.isDrawn() && item.isVisible() && !item.isDisabled()
            && (!tabStop || item.tabIndex != -1);
},

//>    @method dynamicForm.focusInItem()
// Move the keyboard focus into a particular item.
// @group eventHandling, focus
// @param    itemName     (number|itemName|formItem)    Item (or reference to) item to focus in.
// @visibility external
//<

focusInItem : function (itemName, focusAtEnd) {
    // normalize the item in case it's a number or a string
    if (itemName != null) {
        var item = this.getItem(itemName);
    } else {
        var item = this.getFocusSubItem();
    }
    // if nothing was found to focus in, bail!
    if (!item) {
        if (itemName != null) this.logWarn("couldn't find focus item: " + itemName);
        return;
    }

    // if the item can accept focus
    if (item._canFocus()) {
        // focus in it
        item.focusInItem(focusAtEnd);
        // elementFocus will fire 'setFocusItem()' in any case, but do this here as well to
        // avoid problems with elementFocus being fired asynchronously
        this.setFocusItem(item);
        if (this._setValuesPending) {
            var theForm = this;
            isc.Page.setEvent("idle",
                              function () { if (!theForm.destroyed) theForm.focusInItem(); },
                              isc.Page.FIRE_ONCE);
        }
    } else {
        // otherwise complain
        this.logWarn("focusInItem: item cannot accept focus: " + item);
    }
},

// removes the form instance's knowledge of the currently focused element, but does not actually
// blur the element
clearFocusItem : function () {
    delete this._focusItem;
},


//>    @method    dynamicForm.blurFocusItem()    (A)
//  Fires the blurItem() command on the focused item
//  @group eventHandling, focus
//<

blurFocusItem : function () {
    var focusItem = this.getFocusSubItem();
    if (focusItem != null) {
        this._blurItem(focusItem);
        // clear out the remembered focus item - this is an explicit blur, so we don't want
        // focus to go to that item.
        this.clearFocusItem();
    }
},

// Internal '_blurItem' method fires the blur method on the item passed in, if it has focus.
// This does not update this._focusItem, so can be used to blur the form entirely without
// losing track of which item has focus
_blurItem : function (item) {
    if (item != null && item.hasFocus) item.blurItem();
},

// _blurFocusItemWithoutHandler
// Internal method to blur the focus item, without triggering its blur handler.
// Will not clear out this._focusItem.

_blurFocusItemWithoutHandler : function () {

    var focusItem = this.getFocusSubItem();
    if (focusItem != null && focusItem.hasFocus) {
        if (this.__suppressBlurHandler == null) this.__suppressBlurHandler = 0;
        else this.__suppressBlurHandler += 1;

        this._blurItem(focusItem);

    } else {
        this.logDebug("blur w/o handler: no item to blur");
    }
},

//_focusInItemWithoutHandler
// Internal method to focus in a form item without firing it's focus handler
_focusInItemWithoutHandler : function (item) {
    // If the item is non-focusable, no-op
    if (!item || !this._canFocusInItem(item)) {
        var parentItem;
        if (item && item.parentItem) {
            this._focusInItemWithoutHandler(item.parentItem);
            parentItem = true;
        }
        this.logInfo("_focusInItemWithoutHandler(" + item +
                     "): not calling focus as item not focusable or item already has focus" +
                     (parentItem ? ". Putting focus into containerItem instead." : ""),
                     "nativeFocus")
        return;
    }

    // If the item already has focus, no op
    // Note: In IE hasFocus is not a reliable check - it only gets updated on the asynchronous
    // onfocus handler - look directly at the document.activeElement to see where focus
    // currently is instead.

    var hasFocus = item.hasFocus;
    if (isc.Browser.isIE) {
        var focusItemInfo = isc.DynamicForm._getItemInfoFromElement(document.activeElement);
        hasFocus = (focusItemInfo && focusItemInfo.item == item);
    }
    if (hasFocus) return;

    this._suppressFocusHandlerForItem(item);

    this.focusInItem(item);
},

// _suppressFocusHandlerForItem()
// Sets a flag to avoid firing focus handlers when an item receives focus. This, together with
// _blurFocusItemWithoutHandler() allows us to silently blur and refocus in an item (EG on redraw)
// Note that this method should ALWAYS be followed by a call to focus in the item in question.
_suppressFocusHandlerForItem : function (item) {

    if (this.__suppressFocusHandler == null) this.__suppressFocusHandler = 0;
    else this.__suppressFocusHandler += 1;
    this.__suppressFocusItem = item;
},


setOpacity : function (newOpacity, animating, forceFilter, a,b,c) {
    var oldOp = this.opacity;
    this.invokeSuper(isc.DynamicForm, "setOpacity", newOpacity, animating, forceFilter, a,b,c);

    newOpacity = this.opacity;
    if (isc.Browser.isMoz && this.hasFocus &&
        (newOpacity != oldOp) &&
        (newOpacity == null || newOpacity == 100 || oldOp == null || oldOp == 100) )
    {
        var item = this.getFocusSubItem();
        if (item && item._willHandleInput()) {
            this._blurFocusItemWithoutHandler();
            this._focusInItemWithoutHandler(item);
        }
    }
},

// clearingElement
// When a form item is cleared or redrawn, its element will be removed from the DOM
// this is a notification for this.

clearingElement : function (item) {


    if (this.__suppressFocusHandler != null && this.__suppressFocusItem == item) {
        delete this.__suppressFocusHandler;
        delete this.__suppressFocusItem;
    }
    if (this.__suppressBlurHandler != null && (this.getFocusSubItem() == item)) {
        delete this.__suppressBlurHandler;
    }
},

hide : function () {

    if (isc.Browser.isMoz) this._blurItem(this.getFocusSubItem());
    this.Super("hide", arguments);
},

// Override setVisibility to ensure that 'visibilityChanged' notifications are fired on the
// items in this form.
setVisibility : function (newVisibility,a,b,c) {
    this.invokeSuper(isc.DynamicForm, "setVisibility", newVisibility,a,b,c);
    this.itemsVisibilityChanged();
    // If we are shown and we are auto-focus true, focus now
    if (this.isVisible() && this.isDrawn() && this.autoFocus) this.focus();
},

// override 'clear' to notify the form items that they have been hidden.

clear : function () {
    this.Super("clear", arguments);

    this.itemsVisibilityChanged()
    this._itemsCleared();
},

// If focus is taken from the form as a whole, ensure the focusItem's HTML element is blurred
_focusChanged : function (hasFocus) {
    this.Super("_focusChanged", arguments);

    if (!this.hasFocus) this._blurItem(this.getFocusSubItem());

},


parentVisibilityChanged : function (newVisibility) {
    //this.logWarn("parentVisibilityChanged, visible: " + this.isVisible());
    if (!this.isVisible() && isc.Browser.isMoz) this._blurItem(this.getFocusSubItem());
    this.Super("parentVisibilityChanged", arguments);
    this.itemsVisibilityChanged();

    // If we are shown due to a parent being shown, and we are auto-focus true, focus now.
    if (this.isVisible() && this.autoFocus) this.focus();
},

// Ensure we allow native text selection within form items.
_allowNativeTextSelection : function (event) {
    var itemInfo = this._getEventTargetItemInfo(event);
    // For now always allow text selection of form items' cells.
    if (itemInfo.item) {
        var rv = itemInfo.item._allowNativeTextSelection(event, itemInfo);
        if (rv != null) return rv;
    }
    return this.Super("_allowNativeTextSelection", arguments);
},

// Override prepareForDragging
// If the developer is dragging from inside one of our formItems, just disallow it
// This would be really odd UI - if a user drags across a text based item, you'd expect a
// selection to occur, taking precedence over this.canDragReposition.
prepareForDragging : function (a,b,c,d) {

    var EH = this.ns.EH;
    // this would indicate that a child has set itself as the dragTarget, and then
    // prepareForDragging bubbled to this Canvas.  By default, we leave this alone.
    if (EH.dragTarget) return;

    // If the event occurred over the text box / element / control-table of one of our items,
    // return false - We don't want to allow dragging of the form as a whole from within an
    // item - instead we'll support drag selection of the item. We also don't want to allow
    // 'prepareForDragging' to bubble up and allow dragging of a parent.
    var event = EH.lastEvent,
        itemInfo = this._getEventTargetItemInfo(event);
    if (itemInfo.item &&
        (itemInfo.overElement || itemInfo.overTextBox || itemInfo.overControlTable)) return false;

    return this.invokeSuper(isc.DynamicForm, "prepareForDragging", a,b,c,d);
},


// -------------------------------------------------------------------------------------------
// Event handling
// For events that get passed to form items, we will fire the event on the item where it
// occurred, then bubble it up through any parent items. For standard mouse and key evetns, we
// then allow the event to be fired on the DynamicForm, and bubbled up through the widget
// parent chain.

// -------------------------------------------------------------------------------------------



// Given an event, determine whether it occurred over one of our items.
// Note: we return an object of the following format:   {item:item, overTitle:boolean}
// - if the event occurred over the item's title rather than the item itself, overTitle will
// be true.
_getEventTargetItemInfo : function (event) {

    if (!event) event = isc.EH.lastEvent;



    var target = isc.EH.isMouseEvent(event.eventType) ? event.nativeTarget
                                                      : event.nativeKeyTarget;
    var info = isc.DynamicForm._getItemInfoFromElement(target, this);

    // Copy the item info onto the event object itself so handlers can check what part of the
    // item the event ocurred over directly
    event.itemInfo = info;
    return info;
},

//> @method dynamicForm.getEventItem ()
// If the current mouse event occurred over an item in this dynamicForm, returns that item.
// @return (FormItem) the current event target item
// @visibility external
//<
getEventItem : function () {
    var info = isc.EH.lastEvent.itemInfo;
    // skip events over titles or over "inactive" elements (EG placeholders in
    // alwaysShowEditors grids...)
    if (info != null && !info.inactiveContext && !info.overTitle) return info.item;
    return null;
},

//> @object FormItemEventInfo
// An object containing details for mouse events occurring over a FormItem.
// @visibility external
//<

//>@attr formItemEventInfo.item (FormItem : null : R)
// Item over which the event occurred.
// @visibility external
//<

//>@attr formItemEventInfo.overItem (Boolean : null : R)
// True if the event occurred over the main body of the item (for example the text-box), rather
// than over the title or within the form item's cell in the DynamicForm but outside the
// text box area.
// @visibility external
//<

//>@attr formItemEventInfo.overTitle (Boolean : null : R)
// True if the event occurred over the items title.
// @visibility external
//<

//>@attr formItemEventInfo.icon (String : null : IR)
// If this event occurred over a formItemIcon this attribute contains the
// +link{formItemIcon.name} for the icon.
//
// @visibility external
//<

//> @method dynamicForm.getEventItemInfo ()
// If the current mouse event occurred over an item, or the title of an item in this
// dynamicForm, return details about where the event occurred.
// @return (FormItemEventInfo) the current event target item details
// @visibility external
//<
getEventItemInfo : function () {
    var itemInfo = this._getEventTargetItemInfo();
    if (itemInfo == null || itemInfo.inactiveContext) return null;
    return {
        item:itemInfo.item,
        // simplify details of which part of the form item recieved the event
        // since the difference between (EG) textBox and element is implementation dependent
        // only
        overItem:(itemInfo.overElement || itemInfo.overTextBox || itemInfo.overControlTable),
        overTitle:itemInfo.overTitle,
        icon:itemInfo.overIcon
    }
},

// Have handleMouseStillDown send a 'mouseStillDown' event to items, if they have a handler
// for it.

handleMouseStillDown : function (event, eventInfo) {
    if (isc._traceMarkers) arguments.__this = this;

    var targetInfo = this._getEventTargetItemInfo(event),
        item = ((targetInfo.overTitle || targetInfo.inactiveContext) ? null : targetInfo.item);

    // avoid double delivery of events if there are nested DynamicForm elements all receiving
    // this event via bubbling - only deliver to item if it's one of ours
    if (item != null) {
        if (item.form != this) return;

        if (item.mouseStillDown) {
            if (item.handleMouseStillDown(event) == false) return false;
        }
    }

},
// also send 'mouseDown' to items

handleMouseDown : function (event, eventInfo) {
    var targetInfo = this._getEventTargetItemInfo(event),
        item = (targetInfo.overTitle ? null : targetInfo.item);

    // store off the mouseDownTarget so we can cancel handleClick if the mouseUpTarget is different
    this._mouseDownTarget = targetInfo;

    if (item != null) {
        // avoid double delivery of events if there are nested DynamicForm elements all receiving
        // this event via bubbling - only deliver to item if it's one of ours
        if (item.form != this) return;

        item.handleMouseDown(event);


        if (isc.Browser.isSafari && !targetInfo.inactiveContext && targetInfo.overElement
            && isc.isA.CheckboxItem(item))
        {
            item.focusInItem();
        }
    }
},

// Form item mouse event APIs:
// - FormItem.mouseOver(), mouseMove(), mouseOut()
//      Not currently exposed
// - FormItem.titleOver(), titleMove(), titleOut()
//      Not currently exposed - fired if the event occurred over the  title rather than item.
// - FormItem.itemHover(), titleHover()
//      fired after a delay - return false to cancel showing any Hover canvas for the item
// - FormItem.itemHoverHTML() / titleHoverHTML()
//      not implemented by default - returns the HTML to show in the Hover canvas for this
//      item (null will suppress hover canvas). Takes precedence over the equivalent form-level
//      item/titleHoverHTML() methods.
// - Form.itemHoverHTML(item) / titleHoverHTML(item)
//      returns the HTML to show for the Hover canvas for some item.  Default implementation
//      for both methods returns the 'prompt' for the Item.


// _itemMouseEvent - fired in response to mouseMove, mouseOver or mouseOut.
// Fires appropriate handlers on the item.
_itemMouseEvent : function (itemInfo, eventType) {

    var lastMoveItem = this._lastMoveItem,
        wasOverTitle = this._overItemTitle,
        wasOverTextBox = this._overItemTextBox,
        lastOverIcon = this._lastOverIconID,

        item = itemInfo.item,
        overTitle = itemInfo.overTitle,
        overTextBox = itemInfo.overTextBox,
        overIcon = itemInfo.overIcon,
        // this is the return value - used to prevent the form from showing its own hover after
        // an item has shown a hover
        allowBubbling = true
    ;
    // mirror FormItem._getTextBoxElement()
    if (!overTextBox && item && item.hasDataElement() && item._dataElementIsTextBox) {
        overTextBox = itemInfo.overElement;
    }

    // Don't fire mouse events on disabled items - set item to null so we fire mouseOut on
    // the previous item



    // If the event occurred over some 'inactiveEditorHTML' don't fire mouse-move based events
    // at all
    if (itemInfo.inactiveContext != null) {
        item = null;
        overTitle = null;
        overIcon = null;
    }

    // Don't attempt to fire events on items that have been destroyed

    if (lastMoveItem && lastMoveItem.destroyed) {
        lastMoveItem = null;
        this._lastMoveItem = null;
        this._lastOverIconID = null;
        this._overItemTitle = null;
        this._overItemTextBox = null;
    }
    if (item && item.destroyed) {
        item = null;
        overTitle = null;
        overTextBox = null;
        overIcon = null;
    }

    // Remember the information for the next mouse event
    this._lastMoveItem = item;
    this._overItemTitle = overTitle;
    this._overItemTextBox = overTextBox;
    this._lastOverIconID = overIcon;


    if (eventType == isc.EH.MOUSE_OVER) {
        if (item) {
            if (overTitle) item.handleTitleOver();
            else if (overTextBox) item.handleTextBoxOver();
            else {

                if (overIcon) this._lastOverIconID = null;

//                if (this.editMode) this.showRolloverControls(item);
                item.handleMouseOver();
                allowBubbling = false;
            }
        }
    } else if (eventType == isc.EH.MOUSE_OUT) {
        if (lastMoveItem) {
            if (wasOverTitle) lastMoveItem.handleTitleOut();
            else if (wasOverTextBox) lastMoveItem.handleTextBoxOut();
            else {
                if (lastOverIcon) lastMoveItem._iconMouseOut(lastOverIcon);
//                if (this.editMode) this.hideRolloverControls(item);
                lastMoveItem.handleMouseOut();
            }
        }

    // Mouse-Move case is more complex, as the user may have moved within an item, or be moving
    // between items, etc.
    } else {

        var changedItem = (lastMoveItem != item || wasOverTitle != overTitle ||
                           wasOverTextBox != overTextBox);

        // In this case the user has switched items.
        // We catch:    - moving between two items' cells (or title cells)
        //              - moving over a new item or title cell
        //              - moving out of an item or title cell
        //              - moving from an item's cell to title (or vice versa)
        if (changedItem) {
            if (lastMoveItem) {
                if (wasOverTitle) lastMoveItem.handleTitleOut();
                else if (wasOverTextBox) lastMoveItem.handleTextBoxOut();
                else {
                    if (lastOverIcon) lastMoveItem._iconMouseOut(lastOverIcon);
                    lastMoveItem.handleMouseOut();
                }
            }
            if (item) {
                if (overTitle) item.handleTitleOver();
                else if (overTextBox) item.handleTextBoxOver();
                else {
                    if (overIcon) item._iconMouseOver(overIcon);

                    // If the mouse is over an icon, then _iconMouseOver() was just called. If
                    // _lastPromptIcon was set by _iconMouseOver(), then Hover.setAction() was
                    // called to fire _handleIconHover() on a delay. We don't want to now call
                    // handleMouseOver() because that will reset the Hover action to call
                    // _handleHover() on a delay, thus canceling the icon's prompt.
                    // Note that the error icon is handled specially via _handleErrorIconMouseOver()
                    // and this does not set the _lastPromptIcon because there isn't a FormItemIcon
                    // object for the error icon.

                    if (!overIcon || (item._lastPromptIcon == null && overIcon != item.errorIconName)) {
                        item.handleMouseOver();
                    }

                    allowBubbling = false;
                }
            }

        // In this case we know the user has moved within an item's cell, title cell, or textBox.
        } else if (item) {

//            this.logWarn("overTitle:" + overTitle + ", overIcon: "+ overIcon);

            if (overTitle) item.handleTitleMove();
            else if (overTextBox) item.handleTextBoxMove();
            else {

                // we may have moved between icons within the item's cell.
                if (lastOverIcon != overIcon) {
                    if (lastOverIcon) item._iconMouseOut(lastOverIcon);
                    if (overIcon) item._iconMouseOver(overIcon);
                } else if (item) {
                    if (overIcon) item._iconMouseMove(overIcon);
                    item.handleMouseMove();
                }
            }
        }
    }

    return allowBubbling;
},

// Override 'handleMouseOver' / 'Out' / 'Move' to fire mouseOver / titleOver et al on
// form items.
handleMouseOver : function (event, eventInfo) {
    if (this.mouseOver && this.mouseOver(event, eventInfo) == false) return false;
    var result = this._itemMouseEvent(this._getEventTargetItemInfo(event), isc.EH.MOUSE_OVER);
    return result;
},

handleMouseMove : function (event, eventInfo) {
    // allow a form-level mouseMove handler to completely suppress item level handling.
    if (this.mouseMove && this.mouseMove(event,eventInfo) == false) return false;
    var result = this._itemMouseEvent(this._getEventTargetItemInfo(event), isc.EH.MOUSE_MOVE);
    return result;
},

handleMouseOut : function (event, eventInfo) {

    // We know if it's a mouseOut that there's no new item!

    this._itemMouseEvent({}, isc.EH.MOUSE_OUT);

    // If there's a form level mouseout handler, ensure we also fire it (and prevent bubbling
    // if appropriate)
    if (this.mouseOut && this.mouseOut(event,eventInfo) == false) return false;
},

//>    @method    dynamicForm.bubbleItemHandler()
//        Bubble an event up the nested item hierarchy for a particular item.
//        @group    event handling
//        @param    itemID            (number)            Global identifier for the item on which call the handler.
//        @param    handlerName        (string)            Name of the handler to call.
//        @param    [arg1]            (any)                Optional argument to the call.
//        @param    [arg2]            (any)                Optional argument to the call.
//        @param    [arg3]            (any)                Optional argument to the call.
//        @param    [arg4]            (any)                Optional argument to the call.
//<
bubbleItemHandler : function (itemID, handlerName, arg1, arg2, arg3, arg4) {

    var subItem = this.getItemById(itemID),
        result = null;

    for (; subItem != null; subItem = subItem.parentItem) {
        // if we don't directly hold this form item, don't attempt to send events to it

        if (subItem.form != this) continue;
        if (subItem[handlerName] != null && !isc.isA.Function(subItem[handlerName])) {
            isc.Func.replaceWithMethod(subItem, handlerName, "arg1,arg2,arg3,arg4");
        }

        if (subItem[handlerName] == null) {
            this.logWarn("handler:"+ handlerName + " is not present on itemID " + itemID);
            return false;
        }
        result = subItem[handlerName](arg1, arg2, arg3, arg4);

        // if result is false, bail from the handler!
        if (result == false) return result;
    }

    return result;
},

// helper for bubbling inactiveEditorEvents
// the item will handle actually firing the appropriate named event if it exists
bubbleInactiveEditorEvent : function (item, eventName, itemInfo) {
    return this.bubbleItemHandler(item, "_handleInactiveEditorEvent",
                                    eventName, itemInfo.inactiveContext, itemInfo);
},

//>    @method    dynamicForm.elementChanged()
// Handle a change event from an element.
// <p>
// May cause the form to redraw if the item (or sub-item) has redrawOnChange turned on
//
//        @group    event handling
//
//        @param    itemID            (itemID)    Reference to the (possibly nested) item that has changed.
//        @return    (boolean)            true == event should proceed normally, false == halt event
//<
elementChanged : function (itemID) {
    // bubble the elementChanged handler up through the item(s) specified.
    var result = this.bubbleItemHandler(itemID, "elementChanged", itemID);
    return (result != false);
},


// Override handleClick to fire click events on the item clicked.
handleClick : function (event, eventInfo) {
    var itemInfo =  this._getEventTargetItemInfo(event);

    //>EditMode
    // NOTE: Can't factor this into EditMode.js because we need to invoke a supercall and
    // this implementation gets in the way if we override
    // In edit mode, we need to be able to click on the DF itself, so we can select it; in
    // normal usage, clicks are passed up to parents because DynamicForms aren't considered
    // interesting in themselves, only their items
    if (this.editingOn) {
        if (!itemInfo || !itemInfo.item ||
            (!itemInfo.inactiveContext && !itemInfo.overTitle && !itemInfo.overIcon &&
             !itemInfo.overElement && !itemInfo.overTextBox && !itemInfo.overControlTable)) {

            // SpacerItem is a special case - we want to select it even though it has none of
            // the elements tested for in the previous condition
            var spacer = false;
            if (itemInfo && itemInfo.item && itemInfo.item.isA("SpacerItem")) {
                spacer = true;
            }

            this.logWarn("No item clicked upon, passing the click to the DF", "EventHandler");

            if (!spacer) return this.Super("handleClick", arguments);
        }
    }
    //<EditMode
    var returnVal;
    if (itemInfo && itemInfo.item) {
        var item = itemInfo.item;
        // If the mouse went down over a *different* item, don't fire click on this
        // item.

        var mouseDownInfo = this._mouseDownTarget || {},
            mouseDownItem = this._mouseDownTarget ? this._mouseDownTarget.item : null;
        if (mouseDownItem == itemInfo.item) {
            returnVal = this.handleItemClick(itemInfo, mouseDownInfo);
            // remember the "clickTarget" - we'll check this in double-click
            this._clickTarget = this._mouseDownTarget;
        }
    }
    delete this._mouseDownTarget;
    if (returnVal == false || returnVal == isc.EH.STOP_BUBBLING) return returnVal;
    return this.Super("handleClick", arguments);
},

handleItemClick : function (itemInfo, mouseDownInfo) {
    var returnVal;

    var item = itemInfo.item;

    if (itemInfo.inactiveContext) {
        this.logInfo("Bubbling inactive editor event for " + item.ID, "EventHandler");
        returnVal = this.bubbleInactiveEditorEvent("click", item, itemInfo);
    } else {
        if (this._mouseDownTarget.overTitle && itemInfo.overTitle) {
            this.logInfo("Bubbling handleTitleClick event for " + item.ID, "EventHandler");
            returnVal = this.bubbleItemHandler(item, "handleTitleClick", item);
        } else {

            // If we're over the item itself (essentially the element / text box, or picker),
            // fire click
            // SpacerItem is a special case...
            var isSpacer = item.isA("SpacerItem"),
                overItem = isSpacer || (itemInfo.overElement || itemInfo.overTextBox || itemInfo.overControlTable),
                wasOverItem = isSpacer || (mouseDownInfo.overElement || mouseDownInfo.overTextBox || mouseDownInfo.overControlTable)



            if (mouseDownInfo.overIcon && itemInfo.overIcon && (item.form == this)) {
                if (item._iconClick(itemInfo.overIcon) == false)
                    return false;
                // The picker is written into the main body of the item - other icons are not,
                // so don't fire the standard click handler for them.
                var icon = item.getIcon(itemInfo.overIcon);
                if (icon && icon.writeIntoItem) {
                    overItem = true;
                    wasOverItem = true;
                }
            }

            if (overItem && wasOverItem) {
                this.logInfo("Bubbling handleClick event for " + item.ID, "EventHandler");
                if (this.bubbleItemHandler(item, "handleClick", item) == false) {
                    returnVal = false;
                }
            }

            if (returnVal != false) {
                // fire cellClick (in addition to click where appropriate unless handleClick() returned
                // false).
                this.logInfo("Bubbling handleCellClick event for " + item.ID, "EventHandler");
                returnVal = this.bubbleItemHandler(item, "handleCellClick", item);
            }
        }
    }
    return returnVal;
},

// Override handleDoubleClick to fire doubleclick events on the item clicked.
handleDoubleClick : function (event, eventInfo) {
    var itemInfo =  this._getEventTargetItemInfo(event),
        mouseDownInfo = this._mouseDownTarget,
        clickInfo = this._clickTarget;
    var returnVal;
    if (itemInfo && itemInfo.item &&
        mouseDownInfo && (mouseDownInfo.item == itemInfo.item))
    {
        if (clickInfo && (clickInfo.item == itemInfo.item)) {
            var item = itemInfo.item;
            if (itemInfo.inactiveContext) {
                returnVal = this.bubbleInactiveEditorEvent(item, "doubleClick", itemInfo);
            } else if (itemInfo.overTitle && mouseDownInfo.overTitle) {
                returnVal = this.bubbleItemHandler(item, "handleTitleDoubleClick", item);
            } else {

                // If we're over the item itself (essentially the element / text box, or picker),
                // fire click
                var overItem = (itemInfo.overElement || itemInfo.overTextBox
                                 || itemInfo.overControlTable),
                    wasOverItem = (mouseDownInfo.overElement || mouseDownInfo.overTextBox
                                 || mouseDownInfo.overControlTable)


                if (itemInfo.overIcon && mouseDownInfo.overIcon) {
                    if (item._iconClick(itemInfo.overIcon) == false) return false;
                    // The picker is written into the main body of the item - other icons are not,
                    // so don't fire the standard click handler for them.
                    var icon = item.getIcon(itemInfo.overIcon);
                    if (icon && icon.writeIntoItem) {
                        overItem = true;
                        wasOverItem = true;
                    }
                }

                if (overItem && wasOverItem) {
                    if (this.bubbleItemHandler(item, "handleDoubleClick", item) == false) {
                        returnVal = false;
                    }
                }
                if (returnVal != false) {
                    // fire cellClick (in addition to click where appropriate unless handleClick() returned
                    // false).
                    returnVal = this.bubbleItemHandler(item, "handleCellDoubleClick", item);
                }
            }
        } else {
            // If the user double clicked with the first click landing in a different
            // item, fire a click on the second item here.
            returnVal = this.handleItemClick(itemInfo, mouseDownInfo || {});
        }
    }
    delete this._mouseDownTarget;
    delete this._clickTarget;

    if (returnVal == false || returnVal == isc.EH.STOP_BUBBLING) return returnVal;
    return this.Super("handleDoubleClick", arguments);
},

//>    @method    dynamicForm.elementFocus()    (A)
// Event fired when the keyboard focus goes to a particular item
// <P>
// Fired from the native focus event on form items.<br>
// This method fires the formItem.elementFocus handler, which will also fire any developer-
// specified focus handler on the appropraite item(s).
//
//        @group eventHandling, focus
//
//        @param    itemID     (itemID)    item that been focused.
//        @return    (boolean)  true == event should proceed normally, false == halt event
//<
elementFocus : function (element, itemID) {

    // Set the ISC focus element to this

    if (!this.hasFocus) isc.EventHandler.focusInCanvas(this);

    // call setFocusItem on the inner-most item that was focused

    var item = this.getItemById(itemID);
    this.setFocusItem(item);

    // bubble the "elementFocus" event up through the event handler(s) for the element
    var result = true,
        suppressHandler = false;

    if (this.__suppressFocusHandler != null) {
        // Catch the case where we get an onfocus handler from a different item to the one
        // on which we are suppressing elementFocus() - this can happen if when focus w/o
        // handler was fired the item already had focus, so its onfocus handler never fired.
        if (this.__suppressFocusItem != item) {

            delete this.__suppressFocusHandler;
            delete this.__suppressFocusItem;
        } else {
            suppressHandler = true;
            this.__suppressFocusHandler -=1;
            if (this.__suppressFocusHandler < 0) {
                delete this.__suppressFocusHandler;
                delete this.__suppressFocusItem;
            }
        }
    }

    result = this.bubbleItemHandler(itemID, "elementFocus", suppressHandler);

    return (result != false);
},

//>    @method    dynamicForm.elementBlur()    (A)
// Event fired when the keyboard blurs from a particular item
// <P>
// If the item has a "blur" handler, this will be fired automatically
//
// @group eventHandling, focus
//
//        @param    itemID    (itemID)  item that has blurred
//        @return    (boolean)           true == event should proceed normally, false == halt event
//<
elementBlur : function (element, itemID)  {
    if (!isc.isA.FormItem(this.getItemById(itemID))) return;

    // bubble the "elementBlur" event up through the event handler(s) for the element

    var result = true;
    if (this.__suppressBlurHandler == null) result = this.bubbleItemHandler(itemID, "elementBlur");
    else {
        this.__suppressBlurHandler -=1;
        if (this.__suppressBlurHandler < 0) delete this.__suppressBlurHandler;
    }

    // clear any prompt shown from the item
    this.clearPrompt();



    return (result != false);
},



// For keys that 'have meaning' to form items, we don't want to bubble them up past the form.
// For example - we don't want to bubble Arrow_Up / Arrow_Down and cause the form's parent
// element to scroll around when you're trying to move around inside a text area.
// return STOP_BUBBLING for those cases - this will prevent parent widgets from reacting to the
// events without cancelling them

_$Enter:"Enter",
_formItemKeys:{
    "Arrow_Up":true,
    "Arrow_Down":true,
    "Arrow_Right":true,
    "Arrow_Left":true,
    "Page_Up":true,
    "Page_Down":true,
    "Home":true,
    "End":true,
    "Backspace":true,
    "Delete":true,
    "Tab":true
},
handleKeyPress : function (event, eventInfo) {

    // Special case for Enter keypress: If this.saveOnEnter is true, and the enter keypress
    // occurred in a text item, auto-submit the form
    if (event.keyName == this._$Enter) {
        if (this.saveOnEnter) {
            var item = this.getFocusSubItem();
            // Note that this.submit() will call this.saveData() unless this.canSubmit is true
            if (item && item.shouldSaveOnEnter()) {
                this.submit();
            }
            // we always return STOP_BUBBLING on enter keypress (handled below) which is
            // appropriate.
        }
    }


    if (event.keyName == "Backspace" &&
        !isc.DynamicForm.canEditField(this.getFocusSubItem(), this))
    {
        return false;
    }
    // All character keys will have impact on the form item (this includes Enter, which is
    // appropriate)
    // Avoid this on "Escape" keypress since we do want this to bubble so we can (for example)
    // dismiss on outside click.
    if (event.characterValue != null && event.characterValue != 0 &&
        (event.keyName != "Escape"))
    {
        return isc.EventHandler.STOP_BUBBLING;
    }

    // The following non-character keys also 'have meaning' - so we don't want to bubble these.
    // cache the possible form item modifying keys to avoid lots of string instantiations
    if (this._formItemKeys[event.keyName] && event.keyTarget != this) {
        return isc.EventHandler.STOP_BUBBLING;
    }
    return this.Super("handleKeyPress", arguments);
},

// Item Hover HTML
// --------------------------------------------------------------------------------------------

//>@method  dynamicForm.itemHoverHTML()     (A)
//  Retrieves the HTML to display in a hover canvas when the user holds the mousepointer over
//  some item.  Return null to suppress the hover canvas altogether.<br>
//  Default implementation returns the prompt for the item if defined.<br>
//  Can be overridden via <code>item.itemHoverHTML()</code>
//
//  @group Hovers
//  @see FormItem.prompt
//  @see FormItem.itemHoverHTML()
//  @param item (FormItem)  Item the user is hovering over.
//  @visibility external
//<
itemHoverHTML : isc.DynamicForm._defaultItemHoverHTMLImpl,

//>@method  dynamicForm.titleHoverHTML()     (A)
//  Retrieves the HTML to display in a hover canvas when the user holds the mousepointer over
//  some item's title.  Return null to suppress the hover canvas altogether.<br>
//  Default implementation returns the prompt for the item if defined.  If no prompt is defined
//  and the item title is clipped, the item title will be shown in a hover by default.<br>
//  Can be overridden by +link{FormItem.titleHoverHTML()}.
//
//  @group Hovers
//  @see FormItem.prompt
//  @see FormItem.titleHoverHTML()
//  @param item (FormItem)  Item the user is hovering over.
//  @return (HTMLString) HTML to be displayed in the hover
//  @visibility external
//<
titleHoverHTML : function (item) {
    if (item.prompt) return item.prompt;
    if (this.shouldClipTitle(item)) return item.getTitle();
},

//>@method  dynamicForm.valueHoverHTML()     (A)
//  Retrieves the HTML to display in a hover canvas when the user holds the mousepointer over
//  some item's value.  Return null to suppress the hover canvas altogether.<br>
//  Can be overridden by +link{FormItem.valueHoverHTML()}.
//
//  @group Hovers
//  @see FormItem.valueHoverHTML()
//  @param item (FormItem)  Item the user is hovering over.
//  @visibility external
//<
valueHoverHTML : isc.DynamicForm._defaultValueHoverHTMLImpl,

// Method to actually show the Hover - called from the item when the user has hovered over
// the item.
_showItemHover : function (item, HTML) {
    if (HTML && !isc.is.emptyString(HTML) && item.showHover != false) {
        var properties = this._getHoverProperties(item);
        isc.Hover.show(HTML, properties, (item.hoverRect || this.itemHoverRect));
    } else isc.Hover.clear();
},

// Properties to apply to the hover shown for some item.
_getHoverProperties : function (item) {
    if (!isc.isA.FormItem(item)) item = this.getItem(item);

    var props = {};
    if (item) {
        props = isc.addProperties({}, {
            align: (item.hoverAlign != null ? item.hoverAlign : this.itemHoverAlign),
            hoverDelay: (item.hoverDelay != null ? item.hoverDelay : this.itemHoverDelay),
            height: (item.hoverHeight != null ? item.hoverHeight : this.itemHoverHeight),
            opacity: (item.hoverOpacity != null ? item.hoverOpacity : this.itemHoverOpacity),
            baseStyle: (item.hoverStyle != null ? item.hoverStyle : this.itemHoverStyle),
            showHover: (item.showHover != null ? item.showHover : this.showHover),
            valign: (item.hoverVAlign != null ? item.hoverVAlign : this.itemHoverVAlign),
            width: (item.hoverWidth != null ? item.hoverWidth : this.itemHoverWidth),
            wrap: (item.hoverWrap != null ? item.hoverWrap : this.itemHoverWrap)
        });
    } else {
        props = isc.addProperties({}, {
            align: this.hoverAlign,
            hoverDelay: this.hoverDelay,
            height: this.hoverHeight,
            opacity: this.hoverOpacity,
            baseStyle: this.hoverStyle,
            valign: this.hoverVAlign,
            width: this.hoverWidth
        });
    }

    props.moveWithMouse = this.hoverMoveWithMouse;

    return props;
},

// Item Prompts
// --------------------------------------------------------------------------------------------



//>    @method    dynamicForm.showPrompt()    (A)
//        @group    prompt
//            Show a prompt (as dictated by an item, say).
//
//        @param    prompt    (string)            Prompt to show.
//<
showPrompt : function (prompt) {
    window.status = prompt;
},

//>    @method    dynamicForm.clearPrompt()    (A)
//        @group    prompt
//            Clear any form prompt currently showing.
//
//<
clearPrompt : function () {
    window.status = "";
},

// Queries on form properties
// --------------------------------------------------------------------------------------------


// returns true if the form encoding is set to multipart, false otherwise
isMultipart : function () {
    // normal is the default setting; if encoding is set to a value other than this, assume
    // multipart encoding is desired
    return !(this.encoding == isc.DynamicForm.NORMAL ||
             this.encoding == isc.DynamicForm.NORMAL_ENCODING);
},

// Drag and drop
// ---------------------------------------------------------------------------------------

itemIsLastInRow : function (item, rowNum) {
    var rowTable=this.items._rowTable,
        row = rowTable[rowNum],
        index = this.getItems().indexOf(item);

    if (!row || index < 0) return false;

    if (row[this.numCols-1] == index) return true;
    return false;
},

getColumnWidths : function () {
    var rowTable=this.items._rowTable,
        widths = [];

    widths.length = this.numCols;
    // Init the widths array to zeroes to make the population loop simpler
    for (var j = 0; j < widths.length; j++) widths[j] = 0;

    for (var rowCount = 0; rowCount < rowTable.length; rowCount++) {
        var row = rowTable[rowCount];
        for (var i = 0; i < row.length; i++) {
            var item = this.items.get(row[i]);
            if (item.colSpan && item.colSpan > 1) continue;
            if (item.showTitle &&
                  (this.titleOrientation == "left" || !this.titleOrientation)) {
                if (item.getVisibleTitleWidth() > widths[i]) {
                    widths[i] = item.getVisibleTitleWidth();
                }
                i++;
            }
            if (item.width > widths[i]) widths[i] = item.width;
            if (item.showTitle && item.titleOrientation == "right" &&
                  item.getVisibleTitleWidth() > widths[i+1]) {
                widths[++i] = item.getVisibleTitleWidth();
            }
        }
    }
    return widths;
},

getItemTableOffsets : function (item, overrideRowTable) {
    var rowTable = overrideRowTable || this.items._rowTable,
        itemIndex = this.getItems().indexOf(item),
        result = {};

    result.itemIndex = itemIndex

    for (var rowCount = 0; rowCount < rowTable.length; rowCount++) {
        var row = rowTable[rowCount],
            start = row.indexOf(itemIndex),
            end = row.lastIndexOf(itemIndex);

        if (start > -1 && end > -1) {
            if (!result.left || start < result.left) result.left = start;
            if (!result.width || result.width < end - start) result.width = end - start+1;
            if (!result.top || rowCount < result.top) result.top = rowCount;
            if (!result.height || result.height < rowCount - result.top) {
                result.height = rowCount - result.top + 1;
            }
        }
    }

    return result;
},

getItemDropIndex : function (item, dropSide) {
    if (!item) return;
    if (!dropSide) dropSide = "L"; // by default, drop at item.itemIndex

    var offsets = this.getItemTableOffsets(item),
        rowTable = this.items._rowTable;

    if (dropSide == "L") return offsets.itemIndex;
    if (dropSide == "R") {
        if (this.itemIsLastInRow(item) && this.canAddColumns != true) {
            // This isn't really a special case in terms of item drop index - it might end up
            // in new column k rather than wrapping to old column j, but it will still be in
            // index position n.  Leaving in place in case it turns out that something special
            // *is* needed when we have the ability to auto-add columns
            return offsets.itemIndex+1;
        }
        return offsets.itemIndex+1;
    }
    if (dropSide == "T") {
        // if dropping above the top row, drop at the mouse location
        return this.getItemIndexAtTableLocation(
            offsets.top - (offsets.top==0 ? 0 : 1), offsets.left
            );
    }
    if (dropSide == "B") {
        var bottom = offsets.top + offsets.height - 1;
        var itemIndex = this.getItemIndexAtTableLocation(bottom + 1, offsets.left);
        if (itemIndex == null) {
            itemIndex = this.items.length;
        }
        return itemIndex;
    }
},

getItemIndexAtTableLocation : function (rowNum, colNum) {
    var rowTable=this.items._rowTable;

    if (!rowTable[rowNum]) return;
    return rowTable[rowNum][colNum];
},

getItemAtPageOffset : function (x, y) {
    // FIXME - should really cache this value as we're called from mouse movement events, but
    // the caching that was in place was hanging on to stale values
    this.items._currentColWidths = this.getColumnWidths();
    var rowTable=this.items._rowTable,
        widths=this.items._currentColWidths,
        heights=this.items._rowHeights;

    var colNum = this.inWhichPosition(widths,x-this.getPageLeft()),
        rowNum = this.inWhichPosition(heights,y-this.getPageTop());

    colNum = colNum == -1 ? 0 : colNum == -2 ? widths.length : colNum;
    rowNum = rowNum == -1 ? 0 : rowNum == -2 ? heights.length : rowNum;

    if (!rowTable[rowNum]) return null;

    var itemIndex = rowTable[rowNum][colNum],
        item = this.getItem(itemIndex);

    if (item!=null) {
        item._dragRowNum = rowNum;
        item._dragColNum = colNum;
        item._dragItemIndex = itemIndex;
    }

    return item;
},

getNearestItem : function (x, y) {

    var shortest = 9999999999,
        nearestItem;

    this.logDebug("Computing nearest item to (" + x + "," + y + ")", "formItemDragDrop");

    for (var i = 0; i < this.items.length; i++) {
        var item = this.items[i];
        var area = item.getPageRect(true),  // "true" = return a rect including the title
            left = area[0],
            top = area[1],
            width = area[2],
            height = area[3],
            xDelta = 0,
            yDelta = 0;
        if (x >= left && x <= left+width &&
            y >= top && y <= top+height)
        {
            // The cursor is inside this item, so it's obviously the nearest!
            return item;
        }
        if (x > left) {
            if (x > left+width) {
                xDelta = x - (left+width);
            }
        } else {
            xDelta = left - x;
        }
        if (y > top) {
            if (y > top+height) {
                yDelta = y - (top+height);
            }
        } else {
            yDelta = top - y;
        }

        // Compute the straight-line distance to the nearest point of the item's area
        var distance = Math.sqrt(xDelta*xDelta + yDelta*yDelta);

        this.logDebug("Item " + item.name + ": (l,t,w,h) = " + area, "formItemDragDrop");
        this.logDebug("XDelta: " + xDelta + ", yDelta: " + yDelta +
            ", straight line distance: " + distance, "formItemDragDrop");

        if (distance < shortest) {
            this.logDebug("Item " + item.name + ": distance is shorter than " + shortest +
                ", it is now the nearest item", "formItemDragDrop");
            shortest = distance;
            nearestItem = item;
        }
    }

    return nearestItem;
},

showDragLineForItem : function (item, mouseX, mouseY) {
    // make sure the drag line is set up
    this.makeDragLine();

    if (!item) {
        this._dragLine.hide();
        return;
    }

    var itemRect = item.getPageRect(),
        left = itemRect[0],
        top = itemRect[1],
        width = itemRect[2],
        height = item.getVisibleHeight(),
        titlesAt = this.titleOrientation || "left";

    if (item.showTitle!=false) {
        if (titlesAt == "left" || titlesAt == "right") width +=  item.getVisibleTitleWidth();
        if (titlesAt == "left") left -=  item.getVisibleTitleWidth();
    }

    // Dropping to the right of an item is a special case - we should always show the right-
    // hand dropLine
    var toRight;

    if (mouseX <= left) mouseX = left+1;
    else if (mouseX >= left+width) {
        mouseX = left+width-1;
        toRight = true;
    }

    // Favor top/bottom unless we are within a certain number of pixels of the left or right
    // edge.  This will be 20 pixels or a quarter of the widget width, whichever is the
    // smaller
    var sideExtent = width / 4;
    if (sideExtent > 20) sideExtent = 20;

    if (mouseY <= top) mouseY = top+1;
    else if (mouseY >= top+height) mouseY = top+height-1;

    var lOffset = mouseX - left, lPercent = Math.round(width / lOffset),
        tOffset = mouseY - top, tPercent = Math.round(height / tOffset),
        rOffset = (left+width)-mouseX, rPercent = Math.round(width / rOffset),
        bOffset = (top+height)-mouseY, bPercent = Math.round(height / bOffset),
        side = "R",
        lineHeight, lineWidth, lineLeft, lineTop;

    left--; top--;

    if (toRight || (Math.min(lPercent, rPercent) < Math.min(tPercent, bPercent) &&
                   ((lPercent > rPercent && lOffset < sideExtent) ||
                    (rPercent > lPercent && rOffset < sideExtent)))) {
        // it's left or right, so vertical line
        side = toRight ? "R" : lPercent > rPercent ? "L" : "R";
        lineWidth = 3;
        lineHeight = height;
        lineLeft = side == "L" ? left : left+width-1;
        lineTop = top;
    } else {
        // it's top or bottom, so horizontal line
        side = tPercent > bPercent ? "T" : "B";
        lineWidth = width;
        lineLeft = left;
        lineHeight = 3;
        lineTop = side == "T" ? top : top+height-1;
    }

    item.dropSide = side;

    if (this.itemIsLastInRow(item, item._dragRowNum) && !this.canAddColumns && item.dropSide == "R") {
        // if the item is the last in the row and this.canAddColumns is false, show the noDrop cursor
        this.hideDragLine();
        this.setNoDropIndicator();

        this._oldCursor = this.currentCursor;
        this.setCursor("not-allowed");
    }
    else {
        if (this._noDropIndicatorSet) {
            this.clearNoDropIndicator()
            this.setCursor(this._oldCursor);
        }

        var dims = {left: lineLeft, top: lineTop};
        this.adjustDragLinePosition(dims, item, side);
        lineLeft = dims.left;
        lineTop = dims.top;

        // resize and reposition the dragLine appropriately
        this._dragLine.resizeTo(lineWidth, lineHeight);
        this._dragLine.setPageRect(lineLeft, lineTop);
        // and stick it on top of everything else
        this._dragLine.bringToFront();
        this._dragLine.show();
    }
},

// Adjust the line position so it doesn't appear that we have two different drop positions (ie,
// to the right of item n and to the left of item n+1).  In fact we DO have these two distinct
// drop positions, but they result in the same thing happening
adjustDragLinePosition : function (dims, item, side) {
    var rowTable = this.items._rowTable,
        index = this.items.indexOf(item),
        row,
        colFrom, colTo;

    for (var i = 0; i < rowTable.length; i++) {
        if (rowTable[i].indexOf(index) != -1) {
            row = i;
            colFrom = rowTable[i].indexOf(index);
            colTo = rowTable[i].lastIndexOf(index);
            break;
        }
    }

    if (row == null || colFrom == null || colTo == null) return;

    if (side == "T") {
        if (row == 0) return;
        if (rowTable[row-1][colFrom] == rowTable[row-1][colTo] &&
            rowTable[row-1][colFrom-1] != rowTable[row-1][colFrom] &&
            rowTable[row-1][colTo+1] != rowTable[row-1][colFrom])
        {
            var rect = this.items[rowTable[row-1][colFrom]].getPageRect(true);
            var otherY = rect[1] + rect[3];
            dims.top -= Math.round((dims.top - otherY) / 2);
        }
    }

    if (side == "B") {
        if (row == rowTable.length - 1) return;
        if (rowTable[row+1][colFrom] == rowTable[row+1][colTo] &&
            rowTable[row+1][colFrom-1] != rowTable[row+1][colFrom] &&
            rowTable[row+1][colTo+1] != rowTable[row+1][colFrom])
        {
            var rect = this.items[rowTable[row+1][colFrom]].getPageRect(true);
            var otherY = rect[1];
            dims.top += Math.round((otherY - dims.top) / 2);
        }
    }

    if (side == "L") {
        if (colFrom == 0) return;
        // Need support for row-spanning columns here
        var rect = this.items[rowTable[row][colFrom-1]].getPageRect(true);
        var otherX = rect[0] + rect[2];
        dims.left -= Math.round((dims.left - otherX) / 2);
    }

    if (side == "R") {
        if (colTo == rowTable[row].length - 1) return;
        // Need support for row-spanning columns here
        var rect = this.items[rowTable[row][colTo+1]].getPageRect(true);
        var otherX = rect[0];
        dims.left += Math.round((otherX - dims.left) / 2);
    }
},

showDragLineForForm : function () {
    // make sure the drag line is set up
    this.makeDragLine();
    this._dragLine.resizeTo(3, this.getHeight());
    this._dragLine.setPageRect(this.getPageLeft(), this.getPageTop());
    this._dragLine.bringToFront();
    this._dragLine.show();
},

// Field hide/show, enable/disable
// ---------------------------------------------------------------------------------------
// The following enable/disable and show/hide methods are overrides of DBC

enableField : function (fieldName) {
    if (fieldName == null || isc.isAn.emptyString(fieldName)) return;

    var item = this.getItem(fieldName);
    if (item) item.enable();
},

disableField : function (fieldName) {
    if (fieldName == null || isc.isAn.emptyString(fieldName)) return;

    var item = this.getItem(fieldName);
    if (item) item.disable();
},

showField : function (fieldName) {
    if (fieldName == null || isc.isAn.emptyString(fieldName)) return;

    var item = this.getItem(fieldName);
    if (item) item.show();
},

hideField : function (fieldName) {
    if (fieldName == null || isc.isAn.emptyString(fieldName)) return;

    var item = this.getItem(fieldName);
    if (item) item.hide();
},

// A form's "selection chain" is the chain of selectionComponents that control what part of
// a complex nested structure the form is currently editing.  For a form that is editing a
// row from a nested list, this "chain" will consist of one component - it doesn't become a
// chain until we get to lists nested within lists, at which point we can only sensibly decide
// what data the form is editing if we know which record is selected in the outer list *as well
// as* which record is selected in the inner list
//
// This helper method actually returns an array consisting of the indices of selected records
// that describe this form's current position in the data hierarchy, from top to bottom.
getSelectionChain : function () {
    if (!this.selectionComponent) return [];
    var selComponents = [];
    var work = this;
    while (work.selectionComponent) {
        selComponents.add(work.selectionComponent);
        work = work.selectionComponent;
    }
    var indices = [];
    for (var i = selComponents.length - 1; i >= 0; i--) {
        indices.add(selComponents[i].getRecordIndex(selComponents[i].getSelectedRecord()));
    }
    return indices;
},

//> @method dynamicForm.setCanEdit
// Is this form editable or read-only? Setting the form to non-editable causes all
// form items to render as read-only unless a form item is specifically marked as editable
// (the item's +link{formItem.canEdit,canEdit} attribute is <code>true</code>).
//
// @param canEdit (boolean) Can this form be edited?
// @group readOnly
// @see dynamicForm.canEdit
// @visibility external
//<
setCanEdit : function (newValue) {
    this.canEdit = newValue;
    if (this.isDrawn()) this.markForRedraw("setCanEdit");
},

// Override setFieldCanEdit to setCanEdit on specific items.
setFieldCanEdit : function (fieldName, canEdit) {
    if (fieldName == null || isc.isAn.emptyString(fieldName)) return;

    var field = this.getField(fieldName);
    if (field) {
        if (field.setCanEdit) field.setCanEdit(canEdit);
        else {
            field.canEdit = canEdit;
            this.redraw();
        }
    }
},

//> @method dynamicForm.setReadOnlyDisplay()
// Setter for the +link{readOnlyDisplay} attribute.
// @param appearance (ReadOnlyDisplayAppearance) New read-only display appearance.
// @visibility external
//<
setReadOnlyDisplay : function (appearance, skipStorage) {
    if (!skipStorage) this.readOnlyDisplay = appearance;
    this.readOnlyDisplay = appearance;
    var items = this.getItems();
    if (items && items.length > 0) {
        for (var i=0; i<items.length; i++) {
            if (items[i].setReadOnlyDisplay)
                items[i].setReadOnlyDisplay(appearance, true);
        }
    }
    if (this.canEdit == false && this.isDrawn()) {
        this.redraw();
    }
}

});    // END isc.DynamicForm.addMethods()



// class methods
isc.DynamicForm.addClassMethods({

defaultFieldType:"text",

// Avoid re-instantiating strings every time this method is run
_$link:"link", _$text:"text", _$select:"select", _$checkbox:"checkbox",
_$staticText:"staticText", _$boolean:"boolean", _$integer:"integer",
_$binary:"binary", _$blob:"blob", _$multifile:"multifile", _$multiupload:"multiupload",
_$upload:"upload", _$file:"file",
_$base64Binary: "base64Binary", _$enum:"enum", _$CycleItem:"CycleItem", _$selectOther:"selectOther",
_$relation:"relation", _$nestedEditor:"NestedEditorItem", _$nestedListEditor:"NestedListEditorItem",
_$imageFile:"imageFile", _$viewFileItem:"ViewFileItem",
_$section:"section", _$sectionItem:"SectionItem",
_$button:"button", _$buttonItem:"ButtonItem",
getEditorType : function (field, widget) {
    // choosing which form item type to use:
    // Each field may consist of either entirely properties that were passed in, a mixture
    // of passed-in overrides and DataSource defaults, or entirely DataSource defaults.
    // - if "editorType" is present (or the legacy name "formItemType" for the same
    //   concept), use it regardless of whether it came from passed-in fields or from the
    //   DataSource defaults
    // - _constructor comes from XML translation.  When a field is specified as
    //      <TextItem name="foo" .../>
    //   .. _constructor will be "TextItem".  When a field is just specified as
    //      <field name="foo" type="text" .../>
    //   .. _constructor will have the value "FormItem", which we ignore because FormItem
    //   is an abstract base class, so we want to apply automatic item-choosing.
    if (field._constructor == isc.FormItem.Class) field._constructor = null;

    // Grab the DataSource (if any) for later use
    var ds = widget.getDataSource();

    var canEdit = this.canEditField(field,widget),
        defaultType = this.defaultFieldType,
        editorType = field.editorType
    ;

    if (isc.isA.Class(editorType)) {
        // we were passed a class, not a string - map to the class-name
        editorType = editorType.getClassName();
    }

    // NOTE: "formItemType" is a legacy synonym of "editorType"
    var type = (canEdit == false && field.readOnlyEditorType)
               || editorType || field.formItemType || field._constructor ||
               field.type || defaultType;



    if ((canEdit == false && field.readOnlyEditorType) || editorType ||
        field.formItemType || field._constructor)
    {
        return type;
    }

    var currentType = type;
    var returnType = null;

    var isFileType = (type == this._$binary || type == this._$file || type == this._$imageFile);

    while (currentType) {
        // .. otherwise, "type" has been specified on its own without the more specific
        // "editorType", and could refer either to a data type or form item type.
        // For certain known data types, pick appropriate editors.
        if (type == this._$link) {
            // NOTE: Looking at the canEdit property directly here, because the canEditField()
            // method returns true if there is no explicit setting t5o switch editability off,
            // but for links we need the opposite behavior (they should only be editable if
            // the user code explicitly sets canEdit:true)
            if (this.canEditField(field, widget) && field.canEdit) returnType = this._$text;
            else returnType = this._$link;
        // Default to using static text items for all canEdit:false fields regardless of data type
        // with the exception of links (which are already non editable)

        } else if (!canEdit && isFileType && field.canEdit == false) {



            if (type == this._$binary || type == this._$file || type == this._$imageFile)
                returnType = this._$viewFileItem;
            // a couple of common special-cases to avoid converting to staticText
            else if (type != this._$section && type != this._$sectionItem &&
                     type != this._$button && type != this._$buttonItem)
            {
                returnType = this._$staticText;
            }
        } else if (type == this._$boolean) {
            var map = field.valueMap;
            // assumption is that if a valueMap is provided, a boolean storage type
            // is being used for a field with two possible values but no obvious true/false
            // aspect, eg, Sex: Male/Female.  In this case, we should show a SelectItem rather
            // than eg a checkbox labeled "Sex"
            if (!isc.isAn.Array(map) && isc.isAn.Object(map)) returnType = this._$select;
            else returnType = this._$checkbox;
        } else if (type == this._$binary || type == this._$blob || type == this._$file ||
            type == this._$imageFile)
        {
            if (field.dataSource) returnType = this._$multifile
            else returnType = this._$file;
        } else if (type == this._$multiupload) {
            returnType = this._$multifile;
        } else if (type == this._$base64Binary) {
            returnType = this._$base64Binary;
        } else if (type == this._$enum) {
            // If we're just showing valueIcons and no type is specified, use a cycle-item rather
            // than a select.
            if (field.showValueIconOnly) returnType = this._$CycleItem
            else returnType = this._$select;
        } else if (isc.DataSource && isc.isA.DataSource(ds) && ds.fieldIsComplexType(field.name)) {
            // Note: if showComplexFields is false, fields of complexType declared in the
            // DataSource never make it to the form.
            returnType = field.multiple ? widget.nestedListEditorType : widget.nestedEditorType;
        } else {

            if (currentType && currentType != defaultType && currentType != this._$integer &&
                (currentType == this._$selectOther || (isc.FormItemFactory.getItemClass(currentType) != null)))
            {
                returnType = currentType;
            } else {
                currentType = isc.SimpleType.getType(currentType);
                if (returnType) {
                    break;
                } else if (currentType == null || currentType.inheritsFrom == null) {
                    // if field.type=="text" or field.type==null or field.type is not directly recognized by
                    // getItemClass():


                    // "text" is both a data type and a form item type.  We take it to mean the data
                    // type, and may pick a SelectItem or TextAreaItem instead of a TextItem.  This is
                    // the only case in which setting field.type to the short name of a FormItem type
                    // ("Item" suffix omitted) will not select that form item.  It can be avoided by
                    // setting editorType="text".
                    if (field.dataSource) {
                        // Use a relationItem for databound form items of unspecified type.
                        returnType = this._$relation;
                    } else if (field.valueMap || field.optionDataSource || field.displayField) {
                        // if a field has a valueMap, or an explicit optionDataSource / displayField
                        // [which is essentially a server-side valueMap]
                        // If we're showing valueIcons only, use CycleItem - otherwise default to "select"
                        returnType = (field.showValueIconOnly ? this._$CycleItem : this._$select);

                    } else if (widget &&
                               (field.length && field.length > widget.longTextEditorThreshold))
                    {
                        // for very large text fields, show a textArea.
                        returnType = widget.longTextEditorType;
                    } else {
                        // default anything else to text
                        returnType = defaultType;
                    }
                } else {
                    currentType = currentType.inheritsFrom;
                    type = currentType;
                    returnType = null;
                    continue;
                }
            }
        }
        break;
    }

    return returnType;
},

//> @attr dynamicForm.canEditFieldAttribute (string : "canEdit" : IRA)
// @include dataBoundComponent.canEditFieldAttribute
// @visibility external
//<

// canEditField - helper method to determine whether a field is editable
canEditField : function (field, widget) {
    if (!field) return true;

    if (widget && widget.canEditField) {
        //>DEBUG
        // This may seem mysterious since it overrides 'canEdit' settings on the item -
        // log a notification under the special 'canEditField' category.
        this.logDebug("Component " + widget + " calling 'canEditField()' method for field:" + field.name,
            "canEditField");
        //<DEBUG
        return widget.canEditField();
    }

    // Note field.canEdit is potentially set up via 'canEditFieldAttribute' or from 'canSave'
    // as part of dataBinding
    if (field.canEdit != null) return field.canEdit;
    if (widget && widget.canEdit != null) return widget.canEdit;

    return true;
},


// _getItemInfoFromElement - given some DOM element, determine which (if any) item the
// element is a part of.
// Returns an object of the following format:
//  {item:[formItem object], overTitle:boolean, overElement:boolean }

_$id:"id",
_getItemInfoFromElement : function (target, form) {


    var handle = form ? form.getClipHandle() : document,
        itemInfo = {},

        containsItem = isc.DynamicForm._containsItem,

        itemPart = isc.DynamicForm._itemPart,

        elementString = isc.DynamicForm._element,
        textBoxString = isc.DynamicForm._textBoxString,
        controlTableString = isc.DynamicForm._controlTableString,
        inlineErrorString = isc.DynamicForm._inlineErrorString,
        titleString = isc.DynamicForm._title;


    // We mark form items' HTML elements with a 'containsItem' parameter so we can determine
    // which item we're looking at.
    // Iterate up the DOM from the target checking for this attr
    while (target && target != handle && target != document) {

        var itemID = target.getAttribute ? target.getAttribute(containsItem) : null;
        if (itemID != null && !isc.isAn.emptyString(itemID)) {
            var item = window[itemID];
            if (item && !item.destroyed) {
                itemInfo.item = item;

                // catch the case where it's inactive itemHTML

                var inactiveContext = item._getInactiveContextFromElement(target);
                if (inactiveContext != null) {
                    if (this.logIsDebugEnabled("inactiveEditorHTML")) {
                        this.logDebug("Event occurred over inactive HTML for item:" + item +
                                " inactiveContext:" + this.echo(inactiveContext),
                                "inactiveEditorHTML");
                    }
                    itemInfo.inactiveContext = inactiveContext;
                }

                // We also hang an attribute describing which part of the item an element is
                // so we can determine whether we're looking at the item's title, element or
                // one of it's icons.
                // Options are:
                //  "element" - over a native element like an <input> box
                //  "title" - over the title cell
                //  "textbox" - over the textBox
                //  "controlTable" - control table
                //  Anything else assumed to be an icon ID

                var eventItemPart = target.getAttribute(itemPart);
                if (eventItemPart == elementString) itemInfo.overElement = true;
                else if (eventItemPart == titleString) itemInfo.overTitle = true;
                else if (eventItemPart == textBoxString) itemInfo.overTextBox = true;
                else if (eventItemPart == controlTableString) itemInfo.overControlTable = true;
                else if (eventItemPart == inlineErrorString) itemInfo.overInlineError = true;
                else if (eventItemPart && !isc.isAn.emptyString(eventItemPart))
                    itemInfo.overIcon = eventItemPart;

                // quit the loop so we can return the item info.
                break;
            }
        }

        target = target.parentNode;
    }

    return itemInfo;
},


// Callable either on server-formatted errors or editor component format errors.
// Response:
//     { fieldName : {errorMessage: value, otherProp: value},
//       anotherFieldName : {errorMessage: value, otherProp: value},
//       ...
//     }
//   Note that error object {} can also be an array of error objects [{}, ...]
getSimpleErrors : function (errors) {
    // If error is in server format, transform the server error report format to the error
    // report expected by an editor component.  Server errors are formatted as:
    // [{ "recordPath" : pathString,
    //    fieldName : errors,
    //    anotherFieldName : errors,
    //  }]
    // Where pathString is a string representing the record (used for flat or hierarchical data
    // on the server).
    // And where the errors for each field have the format
    // { errorMessage : msg, resultingValue : value }
    // or
    // [{ errorMessage : msg, resultingValue : value },
    //  { errorMessage : msg, otherProp : value },  ... ]
    //
    // Editor components expect just { fieldName : errorMessage } - we drop
    // the resultingValue and other properties
    //
    var errorObjects = {};
    // note we support errors for only one row
    if (isc.isAn.Array(errors)) errors = errors[0];

    for (var fieldName in errors) {
        var fieldErrors = errors[fieldName];
        if (fieldName == "recordPath" && !isc.isAn.Object(fieldErrors)) continue;

        if (isc.isAn.Array(fieldErrors)) {
            errorObjects[fieldName] = [];
            for(var i = 0; i < fieldErrors.length; i++) {
                var error = fieldErrors[i];
                errorObjects[fieldName][i] = isc.isAn.Object(error)
                                                ? isc.shallowClone(error)
                                                : {errorMessage: error};
            }
        } else {
            errorObjects[fieldName] = isc.isAn.Object(fieldErrors)
                                          ? isc.shallowClone(fieldErrors)
                                          : {errorMessage: fieldErrors};
        }
    }
    return errorObjects;
},

// Callable either on server-formatted errors or editor component format errors.

formatValidationErrors : function (errors) {
    // If error is in server format, transform the server error report format to the error
    // report expected by an editor component.  Each server error is:
    // { fieldName : errors },
    //   anotherFieldName : errors },
    //   ...
    // }
    // where the errors for each field have the format
    // { errorMessage : msg, resultingValue : value }
    // or
    // [{ errorMessage : msg, resultingValue : value },
    //  { errorMessage : msg, otherProp : value },  ... ]
    //
    // Editor components expect just { fieldName : errorMessage } - we drop
    // the resultingValue and possible other properties
    //

    var errorMessages = {};
    // note we support errors for only one row
    if (isc.isAn.Array(errors)) errors = errors[0];

    for (var fieldName in errors) {
        var fieldErrors = errors[fieldName];
        if (fieldName == "recordPath" && !isc.isAn.Object(fieldErrors)) continue;

        if (isc.isAn.Array(fieldErrors)) {
            errorMessages[fieldName] = [];
            for(var i = 0; i < fieldErrors.length; i++) {
                var error = fieldErrors[i];
                if(isc.isAn.Object(error)) error = error.errorMessage;
                errorMessages[fieldName][i] = error;
            }
        } else {
            errorMessages[fieldName] = isc.isAn.Object(fieldErrors) ? fieldErrors.errorMessage
                                                                    : fieldErrors;
        }
    }
    return errorMessages;
},


// compareValues
// Do 2 field values match? Used wherever we need to compare field values.
// Handles all expected data types.
// Used to detect changes to values (eg; 'valuesHaveChanged()')

compareValues : function (value1, value2) {

    if (value1 == value2) return true;

    if (isc.isA.Date(value1) && isc.isA.Date(value2))
        return (Date.compareDates(value1, value2) == 0);

    else if (isc.isAn.Array(value1) && isc.isAn.Array(value2)) {
        if (value1.length != value2.length) return false;
        for (var i = 0; i < value1.length; i++) {

            if (!isc.DynamicForm.compareValues(value1[i], value2[i])) return false;
        }
        return true;
    } else {
        // handle having values set to Number, String etc instance
        // IE var foo = new Number(2); rather than just var foo = 2;
        // This returns true for isA.Object()
        if (isc.isA.Number(value1) || isc.isA.String(value1) || isc.isA.Boolean(value1)) {
            value1 = value1.valueOf();
        }
        if (isc.isA.Number(value2) || isc.isA.String(value2) || isc.isA.Boolean(value2)) {
            value2 = value2.valueOf();
        }
        if (value1 == value2) return true;
        if (isc.isAn.Object(value1) && isc.isAn.Object(value2)) {
            var tempObj = isc.addProperties({}, value2);
            for (var attr in value1) {
                // We're doing a simple == comparison here - won't handle
                // complex (Object, Date, Array) attribute values
                if (value2[attr] != value1[attr]) return false;
                delete tempObj[attr];
            }
            // tempObj should now be empty if they match
            for (var attr in tempObj) {
                return false;
            }
            return true;
        }
    }
    return false;
},

// valuesHaveChanged - recursively compares newValues with oldValues, allowing formItem
// compareValues() to run for values with an associated item and handling data paths.
//
// Implemented as a classMethod and used by DynamicForm.valuesHaveChanged
// and ValuesManager.valuesHaveChanged [so the form parameter may be a ValuesManager rather than
// a DynamicForm].
valuesHaveChanged : function (form, returnChangedVals, values, oldValues, rootPath) {

    // A value may have been cleared and the property deleted from `values'. To ensure that we
    // detect the clearing of a value as a change, we need to make sure that `values' is
    // augmented with any properties that exist in `oldValues'.

    var augmentedValues = values,
        undef;
    for (var oldProp in oldValues) {
        if (!(oldProp in values)) {
            // Lazily create a copy of `values' the first time a property is found in `oldValues'
            // that is not in `values'.
            if (augmentedValues === values) augmentedValues = isc.addProperties({}, values);

            augmentedValues[oldProp] = undef;
        }
    }
    values = augmentedValues;

    var changed = false,
        changedVals = {};
    for (var prop in values) {
        // ignore functions
        if (isc.isA.Function(values[prop])) continue;


        if (prop == isc.gwtRef || prop == isc.gwtModule) continue;

        // Skip instances and classes

        if (isc.isAn.Instance(values[prop]) || isc.isA.Class(values[prop])) continue;

        var fullPath = rootPath == null ? prop : rootPath + "/" + prop;

        // Use compareValues to compare old and new values
        // This will catch cases such as Dates where an '==' comparison is
        // not sufficient.
        // Note: If we have a form item use item.compareValues() in case it has been overridden
        var item = form.getItem(fullPath);
        if (item != null) {
            changed = !item.compareValues(values[prop], oldValues[prop]);
            if (changed && returnChangedVals) changedVals[prop] = values[prop];

        } else {
            var value = values[prop],
                oldValue = oldValues[prop];

            var valIsObj = isc.isA.Object(value),
                oldValIsObj = isc.isAn.Object(oldValue);
            // handle having values set to Number, String etc instance
            // IE var foo = new Number(2); rather than just var foo = 2;
            // This returns true for isA.Object()
            if (valIsObj &&
                (isc.isA.Number(value) || isc.isA.String(value) || isc.isA.Boolean(value)))
            {
                value = value.valueOf();
                valIsObj = false;
            }

            if (oldValIsObj &&
                (isc.isA.Number(oldValue) || isc.isA.String(oldValue) || isc.isA.Boolean(oldValue)))
            {
                oldValue = oldValue.valueOf();
                oldValIsObj = false;
            }

            if (valIsObj &&
                !isc.isAn.Array(value) && !isc.isA.Date(value) &&
                oldValIsObj && !isc.isAn.Array(oldValue) && !isc.isA.Date(oldValue))
            {
                var innerChanged = this.valuesHaveChanged(
                                    form, returnChangedVals, values[prop], oldValues[prop], fullPath);
                if (!returnChangedVals && innerChanged) {
                    changed = true;
                    break;
                } else if (!isc.isAn.emptyObject(innerChanged)) {
                    if (changedVals[prop] == null) changedVals[prop] = {};
                    isc.addProperties(changedVals[prop], innerChanged);
                }
            } else {
                changed = !isc.DynamicForm.compareValues(value, oldValue);
                if (changed && returnChangedVals) changedVals[prop] = value;
            }
        }
        // no need to keep going once we've found a difference
        // unless we've been asked to return the changed values
        if (changed && !returnChangedVals) {
            return true;
        }
    }

    return (returnChangedVals ? changedVals : changed);
},

// get filter criteria for a list of filter components (passed as arguments)
getFilterCriteria : function () {
    var criteria = {};
    for (var i = 0; i < arguments.length; i++) {
        var arg = arguments[i];
        if (arg == null) continue;
        isc.addProperties(criteria, arg.getFilterCriteria());
    }
    return criteria;
},

// HTML template generation
_getTopRowCellStart : function () {
     if (!this._observingDoublingStrings) {
        isc.Canvas._doublingStringObservers.add({
            target:this,
            methodName:"_doublingStringsChanged"
        });
        this._observingDoublingStrings = true;
    }
    if (this._$topRowCellStart == null) {

        this._$topRowCellStart = [
            "<TD style='",
            isc.Canvas._$noStyleDoublingCSS,
            "height:0px;overflow:hidden;padding:0px;' class='",
            null,
            "'>",

            (isc.Browser.isSafari || isc.Browser.isMoz ? "<div style='overflow:hidden;height:0px'>" : "")
        ]
    }
    return this._$topRowCellStart;
},
_getTitleInnerTableTemplate : function () {
    if (!this._observingDoublingStrings) {
        isc.Canvas._doublingStringObservers.add({
            target:this,
            methodName:"_doublingStringsChanged"
        });
        this._observingDoublingStrings = true;
    }
    if (this._titleInnerTableTemplate == null) {
        this._titleInnerTableTemplate = [
            "<TABLE height=",   // 0
            , // 1: height
            " border=0 cellspacing=0 cellpadding=0><tr><td class='", // 2
            , // 3: className
            // Override any style attributes that would look wrong double-applied by the className
            "' style='" + isc.Canvas._$noStyleDoublingCSS + "' ALIGN='", // 4
            , // 5: this.getTitleAlign(item)
            "'>",   // 6
            null    // 7: <NOBR>
        ];
    }
    return this._titleInnerTableTemplate;
},

_doublingStringsChanged:function () {
    this._$topRowCellStart = null;
    this._titleInnerTableTemplate = null;
}


    //> @attr dynamicForm.allowExpressions (boolean : null : IRW)
    // For a form that produces filter criteria
    // (see +link{dynamicForm.getValuesAsCriteria,form.getValuesAsCriteria()}), allows the user to
    // enter simple expressions in any field in this form that takes text input.
    // <P>
    // See +link{formItem.allowExpressions} for details.
    //
    // @group advancedFilter
    // @visibility external
    //<


});
// InlineForms: embedding SmartClient FormItems into native HTML forms.
// See QA/DynamicForm/inlineForms.jsp
// ---------------------------------------------------------------------------------------

isc.defineClass("InlineFormItem", "DynamicForm").addProperties({
    position:"relative",

    // don't write a form tag, so that form items written out join a surrounding HTML
    // form.  Note if we did not set this flag, IE will JS error if you try to insert a form
    // inside a form.  Firefox doesn't mind and the values show up within the outer form.
    // Safari untested.
    writeFormTag:false,

    // write native form fields to carry values for synthetic items, just as with direct submit
    canSubmit:true,

    // only one item, with no title
    numCols: 1,

    // in case the default is switched at the Canvas level
    autoDraw: true


    //redraw : function (a,b,c,d) {
    //    this.invokeSuper(isc.InlineFormItem, this._$redraw, a,b,c,d);
    //    this.getItem(0).getDataElement().form.offsetHeight;
    //}
});

isc.InlineFormItem.addClassMethods({
    // This override of create() does create a form, but applies properties to the (singular)
    // FormItem, so that it's possible to use inline items from XML like so:
    //     <InlineItem name="name" type="type">
    //       <valueMap> ... </valueMap>
    //     </InlineItem>
    // NOTE: it's ordinarily not a good idea to override create to return some kind of
    // "wrapper" component, because in order to be used inline in eg a Layout.members array,
    // create() must return the wrapper component, however in other usage (eg subcomponent
    // creation) the expectation is that create will return an instance of whatever was
    // created.
    create : function (A,B,C,D,E,F,G,H,I,J,K,L,M) {

        var itemProps = isc.addProperties({
            showTitle:false,
            validate : function () { this.form.validate(); },
            destroy : function () { this.form.destroy(); this.Super("destroy", arguments); }
        }, A,B,C,D,E,F,G,H,I,J,K,L,M);

        var theForm = this.createRaw().completeCreation({
            fields : [ itemProps ],
            valuesManager : itemProps.valuesManager
        }, itemProps.formProperties );

        return theForm.getItem(0);
    }
});

isc.DynamicForm.addClassMethods({
    //> @classMethod DynamicForm.makeInlineItem()
    // Return a SmartClient form item suitable for embeddeding into a normal HTML form.
    // <P>
    // For example, embedding a +link{ComboBoxItem}:
    // <pre>
    // &lt;form name="contactForm" action="/makeContact.jsp"&gt;
    //    &lt;input type="text" name="name"&gt;
    //    &lt;script&gt;isc.DynamicForm.makeInlineItem("title", "comboBox",
    //                       { valueMap:["CEO", "CTO", "CIO", "COO"] })&lt;/script&gt;
    // &lt;/form&gt;
    // </pre>
    // The value managed by the SmartClient form item is then available for direct DOM access
    // just like ordinary HTML &lt;INPUT&gt; elements, and will be submitted normally with the
    // form.
    // <P>
    // This is an advanced API for use in incremental upgrade of older applications, or for
    // unusual form layouts that can't be accommodated by any combination of
    // +link{group:formLayout,form layout}, +link{ValuesManager} and +link{Layout,H/VLayouts}.
    //
    // @param name (String) name of the form field
    // @param type (String) type of the form field, same as +link{FormItem.type}
    // @param props (FormItem) other properties for the created FormItem
    //
    // @group inlineFormItems
    // @visibility inlineFormItems
    //<
    makeInlineItem : function (name, type, props, formProps) {
        return isc.InlineFormItem.create({
            name: name,
            type: type,
            formProperties : formProps
        }, props)
    },

    //> @classMethod DynamicForm.getFormValues()
    // Return the values of a native HTML &lt;form&gt; element as JavaScript object.
    // <P>
    // Each property in the returned object represents a native form element value.  Select
    // multiple items are represented as an Array of the selected values.
    //
    // @param formId (String) DOM ID of the form
    //
    // @group inlineFormItems
    // @visibility inlineFormItems
    //<
    getFormValues : function (formId) {
        return isc.Canvas.getFormValues(formId);
    }

});


isc.DynamicForm.registerStringMethods({

    //> @method dynamicForm.valuesChanged()
    // Handler fired when the entire set of values is replaced, as by a call to
    // +link{setValues}, +link{resetValues} or +link{editRecord}.
    // <P>
    // Note that it is invalid to call such methods from this handler because doing so would
    // result in an infinite loop.
    //
    // @visibility external
    //<
    valuesChanged : "",

    //> @method dynamicForm.itemChanged()
    // Handler fired when there is a changed() event fired on a FormItem within this form.
    // <P>
    // Fires after the change() handler on the FormItem itself, and only if the item did not
    // cancel the change event and chooses to allow it to propagate to the form as a whole.
    //
    // @param    item    (FormItem)    the FormItem where the change event occurred
    // @param    newValue (any)    new value for the FormItem
    // @visibility external
    //<
    itemChanged : "item,newValue",

    //> @method dynamicForm.itemChange()
    // Handler fired when there is a change() event fired on a FormItem within this form.
    // <P>
    // Fires after the change() handler on the FormItem itself, and only if the item did not
    // cancel the change event and chooses to allow it to propagate to the form as a whole.
    //
    // @param    item    (FormItem)    the FormItem where the change event occurred
    // @param    newValue (any)    new value for the FormItem
    // @param    oldValue (any)    value the FormItem had previous to this change() event
    // @return (boolean) return false to cancel the change, or true to allow it
    // @visibility external
    //<
    itemChange : "item,newValue,oldValue",

    //>    @method dynamicForm.itemKeyPress()
    // Handler fired when a FormItem within this form receives a keypress event.
    // <P>
    // Fires after the keyPress handler on the FormItem itself, and only if the item did not
    // cancel the event and chooses to allow it to propagate to the form as a whole.
    //
    // @param    item    (FormItem)    the FormItem where the change event occurred
    // @param    keyName (string)      name of the key that was pressed (EG: "A", "Space")
    // @param   characterValue  (number)    numeric character value of the pressed key.
    // @return (boolean) return false to cancel the keyPress, or true to allow it
    //
    // @visibility external
    //<
    itemKeyPress : "item,keyName,characterValue",

    //>    @method dynamicForm.submitValues()
    // Triggered when a SubmitItem is included in the form is submitted and gets pressed.
    //
    // @param    values    (object)        the form values
    // @param    form      (DynamicForm)   the form being submitted
    // @group submitting
    // @see method:dynamicForm.submit()
    // @visibility external
    //<
    submitValues : "values,form",

    //> @method dynamicForm.handleHiddenValidationErrors (A)
    // Method to display validation error messages for fields that are not currently visible
    // in this form.<br>
    // This will be called when validation fails for<br>
    // - a hidden field in this form<br>
    // - if this form is databound, a datasource field with specified validators, for which we
    //   have no specified form item.<br>
    // Implement this to provide custom validation error handling for these fields.<br>
    // By default hidden validation errors will be logged as warnings in the developerConsole.
    // Return false from this method to suppress that behavior.
    // @param   errors (object) The set of errors returned - this is an object of the form<br>
    //                      &nbsp;&nbsp;<code>{fieldName:errors}</code><br>
    //                      Where the 'errors' object is either a single string or an array
    //                      of strings containing the error messages for the field.
    // @return (boolean) false from this method to suppress that behavior
    // @visibility external
    //<
    handleHiddenValidationErrors:"errors"
});







//> @class FormItem
//
// An Item that can participate in a DynamicForm, managing some value.
// <P>
// FormItems are never directly created, instead, FormItem descriptors are passed to a
// DynamicForm.  See the +link{DynamicForm} documentation for details.
//
// @treeLocation Client Reference/Forms/Form Items
// @visibility external
//<
// XXX this should be a subclass of "TableItem" or something else that understands width,
// height, rowSpan, colSpan et al, but is not necessary a form item, to allow the form layout
// code to be factored out and used elsewhere.
isc.ClassFactory.defineClass("FormItem");



// Copy across the canvas method to generate DOM IDs for the various elements we will be
// creating
isc.FormItem.addMethods({
    // we use getDOMID to generate our elements' unique dom ids
    // If we're writing out 'inactiveHTML' we may be rendering multiple elements on the page
    // with the same 'partName' but we want them to have separate unique IDs. In this
    // case we'll modify the partName to ensure unique IDs for all the inactive elements.
    _inactiveTemplate:[null, "_inactiveContext", null],
    _getDOMID : function (partName, dontCache, dontReuse, inactiveContext) {

        // If we're in the process of writing out inactive HTML, pick up the current
        // inactiveContext ID, or lazily create a new one if we haven't got one yet.

        if (inactiveContext == null && this.isInactiveHTML()) {
            inactiveContext = this._currentInactiveContext;
        }
        // see if this becomes expensive (string concat)
        if (inactiveContext != null) {
            this._inactiveTemplate[0] = partName;
            this._inactiveTemplate[2] = inactiveContext;
            partName = this._inactiveTemplate.join(isc.emptyString);
            if (this.logIsDebugEnabled("inactiveEditorHTML")) {
                this.logDebug("_getDOMID called for inactive HTML -- generated partName:"
                    + partName, "inactiveEditorHTML");
            }

            // ignore 'dontCache' if we're writing out inactive context.

            dontCache = false;

        }
        return isc.Canvas.getPrototype()._getDOMID.apply(this, [partName,dontCache,dontReuse]);
    },

    _getDOMPartName:isc.Canvas.getPrototype()._getDOMPartName,


    _releaseDOMIDs:isc.Canvas.getPrototype()._releaseDOMIDs,
    reuseDOMIDs:false
});

isc.FormItem.addClassMethods({

    //> @classMethod FormItem.create()
    // FormItem.create() should never be called directly, instead, create a +link{DynamicForm}
    // and specify form items via +link{DynamicForm.items,form.items}.
    //
    // @visibility external
    //<
    // Log a warning if called directly
    create : function (A,B,C,D,E,F,G,H,I,J,K,L,M) {
        this.logWarn(
            "Unsupported call to " + this.getClassName() + ".create(). FormItems must be created " +
            "by their containing form. To create form items, use the 'items' property of a DynamicForm " +
            "instance. See documentation for more details."
        );
        // If we're passed properties combine them into a single raw object - if this is then
        // assigned to a form's "items" attribute the developer will likely get the expected
        // behavior.
        // (No need to call Super)
        return isc.addProperties({}, A,B,C,D,E,F,G,H,I,J,K,L,M);
    },

    // getNewTagID() -- a method to broker out IDs for the form element tags, if no name is
    // specified for the form element
    // (If a name is specified we'll use that instead)
    getNewTagID : function () {
        if (this._currentTagIDNumber == null) this._currentTagIDNumber = 0;
        this._currentTagIDNumber += 1;
        return "isc_FormItemElement_ID_" + this._currentTagIDNumber;
    },

    // setElementTabIndex()
    // Given a DOM element (a form item element), and a tabIndex, update the tabIndex on
    // the appropriate element.
    setElementTabIndex : function (element, tabIndex) {
        // Set the tabIndex property on the element
        element.tabIndex = tabIndex;

        // In mozilla setting a tabIndex to -1 is not sufficient to remove it from the
        // page's tab order -- update the 'mozUserFocus' property as well to achieve this
        // if we're passed a desired tabIndex less than zero (or revert this property if
        // necessary from a previous exclusion from the page's tab order)

        if (isc.Browser.isMoz) {
            element.style.MozUserFocus = (tabIndex < 0 ? "ignore" : "normal");
        }
    },



    _aboutToFireNativeElementFocus : function (item) {
        if (!isc.Browser.isIE) return;
        var activeElement = this.getActiveElement();

        if (activeElement && activeElement.tagName == null) activeElement = null;

        // Note: this will work for elements in the DOM that are not part of ISC form items.
        if (activeElement &&
            ((activeElement.tagName.toLowerCase() == this._inputElementTagName &&
              activeElement.type.toLowerCase() == this._textElementType) ||
              activeElement.tagName.toLowerCase() == this._textAreaElementTagName))
        {
            // IE proprietary API
            var range = activeElement.createTextRange();
            range.execCommand("Unselect");
        }
    },


    // Helper method to determine if the item passed in is text based
    _textBasedItem : function (item, checkForPopUp) {
        if (isc.isA.FormItem(item)) item = item.getClassName();

        if (!this._textClassNames) {
            this._textClassNames = {
                text:true,
                TextItem:true,
                textItem:true,
                textArea:true,
                TextAreaItem:true,
                textAreaItem:true
            }
            this._popUpClassNames = {
                popUpTextArea:true,
                PopUpTextAreaItem:true,
                popUpTextAreaItem:true
            }
        }

        return this._textClassNames[item] || (!checkForPopUp || this._popUpClassNames[item]);
    },

    // Native handlers to be applied to elements written into the DOM
    // --------------------------------------------------------------------------------------

    // Focus/blur handelers to be applied to Form item elements.
    // Applied directly to the element, so we need to determine which item we are a part of
    // and call the appropriate focus/blur handler method on that item.
    _nativeFocusHandler : function () {
        if (!window.isc || !isc.DynamicForm) return;

        isc.EH._setThread("IFCS");

        var result;
        if (isc.Log.supportsOnError) {
            result = isc.FormItem.__nativeFocusHandler(this);
        } else {
            try {
                result = isc.FormItem.__nativeFocusHandler(this);
            } catch (e) {
                isc.Log._reportJSError(e);
            }
        }
        isc.EH._clearThread();
        return result;
    },
    __nativeFocusHandler : function (element) {

        //!DONTCOMBINE
        var itemInfo = isc.DynamicForm._getItemInfoFromElement(element),
            item = itemInfo.item;


        if (item && item.isDisabled()) {
            element.blur();
            return;
        }

        if (item) {
            return item._nativeElementFocus(element, item);
        }
        isc.EH._clearThread();
    },

    _nativeBlurHandler : function () {
        // Check for blur being fired on page unload (when the isc object is out of scope)
        if (!window.isc || !isc.DynamicForm) return;

        isc.EH._setThread("IBLR");
        var result;
        if (isc.Log.supportsOnError) {
            result = isc.FormItem.__nativeBlurHandler(this);
        } else {
            try {
                result = isc.FormItem.__nativeBlurHandler(this);
            } catch (e) {
                isc.Log._reportJSError(e);
            }
        }
        isc.EH._clearThread();
        return result;
    },
    __nativeBlurHandler : function (element) {
        //!DONTCOMBINE
        var itemInfo = isc.DynamicForm._getItemInfoFromElement(element),
            item = itemInfo.item;
        if (item && item.hasFocus) {
            return item._nativeElementBlur(element, item);
        }
    },

    // IE specific handler for oncut / onpaste
    _nativeCutPaste : function () {
        if (!window.isc) return;
        var element = this,
            itemInfo = isc.DynamicForm._getItemInfoFromElement(element),
            item = itemInfo.item;
        if (item && item.hasFocus) {
            return item._nativeCutPaste(element, item);
        }
    },

    // For some form items we make use of the native onchange handler.
    // This is a single function that will be applied directly to elements as a change handler
    // Currently used by the nativeSelectItem class and the checkboxItem class (and UploadItem)
    _nativeChangeHandler : function () {

        //!DONTCOMBINE
        if (!window.isc || !isc.DynamicForm) return;

        var element = this,
            itemInfo = isc.DynamicForm._getItemInfoFromElement(element),
            item = itemInfo.item;
        if (item) return item._handleElementChanged();
    },

    // Focus / blur handlers applied directly to icons
    _nativeIconFocus : function () {
        //!DONTCOMBINE

        var element = this,
            itemInfo = isc.DynamicForm._getItemInfoFromElement(element),
            item = itemInfo.item,
            iconID = itemInfo.overIcon;
        if (item) {

            if (item.iconIsDisabled(iconID)) element.blur();
            else return item._iconFocus(iconID, element);
        }
    },

    _nativeIconBlur : function () {
        //!DONTCOMBINE
        if (!window.isc) return;

        var element = this,
            itemInfo = isc.DynamicForm._getItemInfoFromElement(element),
            item = itemInfo.item,
            iconID = itemInfo.overIcon;
        if (item && !item.iconIsDisabled(iconID)) return item._iconBlur(iconID, element);
    },

    // Native click handler for icons can just return false. This will cancel navigation.
    // We will fire icon.click() via the standard DynamicForm.handleClick method
    _nativeIconClick : function () {
        return false;
    },


    _testStuckSelectionAfterRedraw : function (formItem) {
        if (!isc.Browser.isIE) return;
        this._testFocusAfterRedrawItem = formItem;
        this.fireOnPause("testStuckSelection", {target:this, methodName:"_testStuckSelection"});
    },
    _testStuckSelection : function () {
        var item = this._testFocusAfterRedrawItem;
        // Focus may have moved elsewhere etc since the refocusAfterRedraw
        if (item == null ||
            item.destroyed ||
            !item.isDrawn() ||
            !item.isVisible() ||
            !item.hasFocus)
        {
            return;
        }
        if (item._IESelectionStuck()) {

            item.focusInItem();
        }
    },

    // Helper method to return a prompt string to show in hovers over error icons
    getErrorPromptString : function (errors) {
        var errorString = "";
        if (!isc.isAn.Array(errors)) errors = [errors];
        for (var i =0; i< errors.length; i++) {
            errorString += (i > 0 ? "<br>" : "") + errors[i].asHTML();
        };
        return errorString;
    },


    // HTML templating involving no-style-doubling string [which may change at runtime]
    _getOuterTableStartTemplate : function () {
        if (!this._observingDoublingStrings) {
            isc.Canvas._doublingStringObservers.add({
                target:this,
                methodName:"_doublingStringsChanged"
            });
            this._observingDoublingStrings = true;
        }
        if (this._$outerTableStartTemplate == null) {
            this._$outerTableStartTemplate = [
                "<TABLE role='presentation' CELLSPACING=0 CELLPADDING=0 BORDER=0 ID='",         // 0
                ,                                                           // 1 [ID for outer table]
                // We'll apply the 'cellStyle' for the item to the outer table as styles won't
                // be inherited by sub elements of the table.
                // Explicitly avoid getting doubled borders etc.
                "' STYLE='" + isc.Canvas._$noStyleDoublingCSS,              // 2
                ,                                                           // 3 [css to override class attrs]
                "' CLASS='",                                                // 4
                ,                                                           // 5 [pick up the cellStyle css class]

                "'><TR>",                                                   // 6
                ,                                                           // 7 Potential first cell for
                                                                            //   error on left...
                // Main cell - If we're showing a picker this will contain the 'control' table
                // If we're not showing a picker, this wll contain the 'text box'
                "<TD style='",                                              // 8
                ,                                                           // 9 [possibly css for text box cell]
                "' VALIGN=",                                                // 10

                ,                                                           // 11   [v align]
                ">"                                                         // 12
                // Either the text box element (returned by getElementHTML()) or an inner control table

            ];
        }
        return this._$outerTableStartTemplate;
    },

    _getIconsCellTemplate : function () {
        if (!this._observingDoublingStrings) {
            isc.Canvas._doublingStringObservers.add({
                target:this,
                methodName:"_doublingStringsChanged"
            });
            this._observingDoublingStrings = true;
        }

        if (this._$iconsCellTemplate == null) {
            this._$iconsCellTemplate = [
                "</TD><TD VALIGN=",     // 0
                ,                       // 1 [v align property for icons]

                " WIDTH=",              // 2
                ,                       // 3 [total icons width]
                " style='" + isc.Canvas._$noStyleDoublingCSS + "line-height:",
                ,                       // 5 iconHeight
                "px' class='",          // 6
                ,                       // 7 Apply standard cell style to the item
                "' ID='",               // 8
                ,                       // 9 ID for cell
                                        //  (allows us to show/hide icons by writing into the cell)
                "'>",                   // 10
                null                    // 11 [icons HTML]
            ];
        }
        return this._$iconsCellTemplate;
    },
    _doublingStringsChanged : function () {
        this._$outerTableStartTemplate = null;
        this._$iconsCellTemplate = null;
    }

});

isc.FormItem.addClassProperties({

    _inputElementTagName : "input",
    _textElementType : "text",
    _textAreaElementTagName : "textarea",
    _cellStyleCache: {},
    _rtlCellStyleCache: {}
});

isc.FormItem.addProperties({

    // Basics
    // ---------------------------------------------------------------------------------------

    //> @type FormItemType
    // DynamicForms automatically choose the FormItem type for a field based on the
    // <code>type</code> property of the field.  The table below describes the default FormItem
    // chosen for various values of the <code>type</code> property.
    // <P>
    // You can also set +link{FormItem.editorType,field.editorType} to the classname of a
    // +link{FormItem} to override this default mapping.  You can alternatively override
    // +link{dynamicForm.getEditorType()} to create a form with different rules for which
    // FormItems are chosen.
    // <P>
    // @value "text"    Rendered as a +link{class:TextItem}, unless the length of the field (as
    // specified by +link{attr:dataSourceField.length} attribute) is larger than the value
    // specified by +link{attr:dynamicForm.longTextEditorThreshold}, a
    // +link{class:TextAreaItem} is shown.
    //
    // @value "boolean"   Rendered as a +link{class:CheckboxItem}
    //
    // @value "integer"   Same as <code>text</code> by default.
    //                    Consider setting editorType:+link{SpinnerItem}.
    // @value "float"     Same as <code>text</code> by default.
    //                    Consider setting editorType:+link{SpinnerItem}.
    // @value "date"      Rendered as a +link{class:DateItem}
    // @value "time"      Rendered as a +link{class:TimeItem}
    // @value "enum"      Rendered as a +link{class:SelectItem}.  Also true for any field that
    //                    specifies a +link{formItem.valueMap}.
    //                    Consider setting editorType:+link{ComboBoxItem}.
    // @value "sequence"  Same as <code>text</code>
    // @value "link"      If +link{dataSourceField.canEdit}<code>:false</code> is set on the field,
    //                    the value is rendered as a +link{class:LinkItem}.  Otherwise the field
    //                    is rendered as a +link{class:TextItem}.
    // @value "image"     Rendered as an image if not editable, or as a +link{TextItem} to edit
    //                    the URL or partial URL if editable
    // @value "imageFile" Rendered as a +link{class:FileItem}, or a +link{ViewFileItem} if not editable
    // @value "binary"    Rendered as a +link{class:FileItem}, or a +link{ViewFileItem} if not editable
    //
    // @see attr:FormItem.type
    // @see type:FieldType
    // @visibility external
    //<

    //> @attr formItem.type (FormItemType : "text" : [IR])
    // The DynamicForm picks a field renderer based on the type of the field (and sometimes other
    // attributes of the field).
    //
    // @see type:FormItemType
    // @see type:FieldType
    // @group appearance
    // @visibility external
    //<

    //> @attr formItem.editorType (FormItem class : null : [IR])
    // Name of the FormItem to use for editing, eg "TextItem" or "SelectItem".
    // <P>
    // The type of FormItem to use for editing is normally derived automatically from
    // +link{formItem.type,field.type}, which is the data type of the field, by the rules
    // explained +link{type:FormItemType,here}.
    //
    // @see type:FormItemType
    // @see type:FieldType
    // @group appearance
    // @visibility external
    //<

    getReadOnlyDisplay : function () {
        if (this.readOnlyDisplay != null) return this.readOnlyDisplay;

        var form = this.form;
        if (form != null) {
            return form.readOnlyDisplay;
        }

        return isc.DynamicForm._instancePrototype.readOnlyDisplay;
    },

    //> @method formItem.setReadOnlyDisplay()
    // Setter for +link{FormItem.readOnlyDisplay}.
    // @param appearance (ReadOnlyDisplayAppearance) new <code>readOnlyDisplay</code> value.
    // @visibility external
    //<
    setReadOnlyDisplay : function (appearance, skipStorage) {
        if (!skipStorage) {
            this.readOnlyDisplay = appearance;
            if (isc.isA.CanvasItem(this) && isc.isA.DynamicForm(this.canvas)) {
                this.canvas.setReadOnlyDisplay(appearance);
            }
            if (this.items) {
                for (var i=0; i<this.items.length; i++) {
                    this.items[i].setReadOnlyDisplay(appearance, skipStorage);
                }
            }
        }
        if (!this.getCanEdit() && this.isDrawn()) {
            if (this.readOnlyDisplayChanged) this.readOnlyDisplayChanged(appearance);
            this.redraw();
        }
    },

    getReadOnlyTextBoxStyle : function () {
        return this.readOnlyTextBoxStyle ||
                    (this.form ? this.form.readOnlyTextBoxStyle : "staticTextItem");
    },

    _getClipStaticValue : function () {
        var item = this;
        do {
            var clipStaticValue = item.clipStaticValue;
            if (clipStaticValue != null) return clipStaticValue;
            item = item.parentElement;
        } while (item != null);

        var form = this.form;
        if (form != null) {
            return !!form.clipStaticValue;
        }

        return !!isc.DynamicForm._instancePrototype.clipStaticValue;
    },

    //> @attr formItem.name (identifier : null : IR)
    // Name for this form field.
    // <P>
    // The FormItem's name determines the name of the property it edits within the form. Must be
    // unique within the form as well as a valid JavaScript identifier, as specified by ECMA-262
    // Section 7.6 (the <var class="smartclient">+link{String.isValidID()}</var><var class="smartgwt">StringUtil.isValidID()</var>
    // function can be used to test whether a name is a valid JavaScript identifier).
    //
    // @group basics
    // @visibility external
    //<

    //> @attr formItem.dataPath (DataPath : null : IR)
    // dataPath for this item. Allows the user to edit details nested data structures in a
    // flat set of form fields
    // @visibility external
    //<

    //> @attr formItem.title             (String : null : IRW)
    // User visible title for this form item.
    //
    // @group basics
    // @visibility external
    //<

    //> @attr formItem.defaultValue       (any : null : IRW)
    // Value used when no value is provided for this item. Note that whenever this item's value
    // is cleared programmatically (for example via <code>item.setValue(null)</code>), it will be
    // reverted to the <code>defaultValue</code>. Developers should use the
    // +link{DynamicForm.values} object if their intention is to provide an initial value for a
    // field in a form rather than a value to use in place of <code>null</code>.
    //
    // @see method:defaultDynamicValue
    // @group basics
    // @visibility external
    // @example fieldEnableDisable
    //<


    //> @attr formItem.value (any : null : IR)
    // Value for this form item.
    // <var class="smartclient">This value may be set directly on the form item initialization
    // block but is not updated on live items and should not be directly accessed.
    // Once a form item has been created by the dynamicForm use +link{FormItem.setValue()} and
    // +link{FormItem.getValue()} directly.</var>
    // @group basics
    // @visibility external
    //<

    //> @attr formItem.ID (identifier : null : IRW)
    // Global identifier for referring to the formItem in JavaScript.  The ID property is
    // optional if you do not need to refer to the widget from JavaScript, or can refer to it
    // indirectly (for example, via <code>form.getItem("<i>itemName</i>")</code>).
    // <P>
    // An internal, unique ID will automatically be created upon instantiation for any formItem
    // where one is not provided.
    //
    // @group basics
    // @visibility external
    //<

    //> @attr formItem.emptyDisplayValue (string : "" : IRW)
    // Text to display when this form item has a null or undefined value.
    // <P>
    // If the formItem has a databound pickList, and its +link{formItem.displayField} or
    // +link{formItem.valueField} (if the former isn't set) has an undefined
    // +link{DataSourceField.emptyCellValue,emptyCellValue} setting, that field's
    // <code>emptyCellValue</code> will automatically be set to the <code>emptyDisplayValue</code>.
    //
    // @group display_values
    // @visibility external
    //<
    emptyDisplayValue:"",

    // ValueMap
    // -----------------------------------------------------------------------------------------

    //> @attr formItem.valueMap (Array or Object: null : IRW)
    // In a form, valueMaps are used for FormItem types that allow the user to pick from a
    // limited set of values, such as a SelectItem.  The valueMap can be either an Array of
    // legal values or an Object where each property maps a stored value to a user-displayable
    // value.
    // <P>
    // To set the initial selection for a form item with a valueMap, use
    // +link{formItem.defaultValue}.
    // <P>
    // See also +link{dataSourceField.valueMap}.
    //
    // @group valueMap
    // @visibility external
    //<

    // optionDataSource
    // ----------------------------------------------------------------------------------------

    //> @attr formItem.optionDataSource        (DataSource | String : null : IR)
    // If set, this FormItem will map stored values to display values as though a
    // +link{valueMap} were specified, by fetching records from the
    // specified <code>optionDataSource</code> and extracting the
    // +link{formItem.valueField,valueField} and
    // +link{formItem.displayField,displayField} in loaded records, to derive one
    // valueMap entry per record loaded from the optionDataSource.
    // <P>
    // With the default setting of +link{fetchMissingValues}, fetches will be initiated against
    // the optionDataSource any time the FormItem has a non-null value and no corresponding
    // display value is available.  This includes when the form is first initialized, as well
    // as any subsequent calls to +link{formItem.setValue()}, such as may happen when
    // +link{DynamicForm.editRecord()} is called.  Retrieved values are automatically cached by
    // the FormItem.
    // <P>
    // Note that if a normal, static +link{formItem.valueMap,valueMap} is <b>also</b> specified for
    // the field (either directly in the form item or as part of the field definition in the
    // dataSource), it will be preferred to the data derived from the optionDataSource for
    // whatever mappings are present.
    // <P>
    // In a databound form, if +link{FormItem.displayField} is specified for a FormItem and
    // <code>optionDataSource</code> is unset, <code>optionDataSource</code> will default to
    // the form's current DataSource
    //
    // @see invalidateDisplayValueCache()
    // @group display_values
    // @visibility external
    // @getter FormItem.getOptionDataSource()
    // @example listComboBox
    //<

    //> @attr FormItem.optionFilterContext     (RPCRequest Properties : null : IRA)
    // If this item has a specified <code>optionDataSource</code>, and this property is
    // not null, this will be passed to the datasource as +link{rpcRequest} properties when
    // performing the fetch operation on the dataSource to obtain a data-value to display-value
    // mapping
    // @visibility external
    //<

    //> @attr FormItem.optionCriteria     (criteria : null : IRA)
    // If this item has a specified <code>optionDataSource</code>, and this property may be used
    // to specify criteria to pass to the datasource when
    // performing the fetch operation on the dataSource to obtain a data-value to display-value
    // mapping
    // @visibility external
    //<

    //> @attr FormItem.optionOperationId     (string : null : IRA)
    // If this item has a specified <code>optionDataSource</code>, this attribute may be set
    // to specify an explicit +link{DSRequest.operationId} when performing a fetch against the
    // option dataSource to pick up display value mapping.
    // @visibility external
    //<

    //> @attr formItem.valueField  (string : null : IR)
    // If this form item maps data values to display values by retrieving the
    // +link{FormItem.displayField} values from an
    // +link{FormItem.optionDataSource,optionDataSource}, this property
    // denotes the the field to use as the underlying data value in records from the
    // optionDataSource.<br>
    // If unset, assumed to be the +link{FormItem.name} of this form item.
    // @group display_values
    // @visibility external
    // @getter FormItem.getValueFieldName()
    //<

    //> @attr formItem.displayField   (string : null : IR)
    // Specifies an alternative field from which display values should be retrieved for this
    // item.
    // <P>
    // The display field can be either another field value in the same record or a field that
    // must be retrieved from a related +link{formItem.optionDataSource,optionDataSource}.
    // <P>
    // If this item is not databound (+link{FormItem.optionDataSource} is unset), or bound
    // to the same dataSource as the form as a whole, this item will call
    // +link{dynamicForm.getValue,form.getValue()}
    // the form named after is implemented by picking up the
    // value of the specified field from the Form's values object.
    // <P>
    // Otherwise this item will attempt to map its underlying value to a display value
    // by retrieving a record from the +link{FormItem.optionDataSource} where the
    // +link{FormItem.valueField} matches this item's value, and displaying the
    // <code>displayField</code> value from that record.
    // Note that if <code>optionDataSource</code> is set and this value is not
    // set, +link{formItem.getDisplayFieldName()} will return the dataSource title field by default.
    // <P>
    // This essentially enables the specified <code>optionDataSource</code> to be used as
    // a server based +link{group:valueMap}.
    //
    // @see invalidateDisplayValueCache()
    // @group display_values
    // @visibility external
    // @getter FormItem.getDisplayFieldName()
    //<

    //> @attr formItem.multipleValueSeparator   (string : ', ' : IR)
    // If this item is displaying multiple values, this property will be the
    // string that separates those values for display purposes.
    //
    // @group display_values
    // @visibility external
    //<
    multipleValueSeparator: ", ",

    //> @attr formItem.fetchMissingValues   (Boolean : true : IRWA)
    // If this form item has a specified +link{FormItem.optionDataSource}, should the
    // item ever perform a fetch against this dataSource to retrieve the related record.
    // <P>
    // The fetch occurs if the item value is non null on initial draw of the form
    // or whenever setValue() is called. Once the fetch completes, the returned record
    // is available via the +link{FormItem.getSelectedRecord()} api.
    // <P>
    // By default, a fetch will only occur if +link{formItem.displayField} is specified, and
    // the item does not have an explicit +link{formItem.valueMap} containing the
    // data value as a key.<br>
    // However you can also set +link{formItem.alwaysMissingFetchValues} to have a fetch occur
    // even if no <code>displayField</code> is specified. This ensures
    // +link{formItem.getSelectedRecord()} will return a record if possible - useful for
    // custom formatter functions, etc.
    // <P>
    // Note - for efficiency we cache the associated record once a fetch has been performed, meaning
    // if the value changes, then reverts to a previously seen value, we do not kick
    // off an additional fetch to pick up the display value for the previously seen data value.
    // If necessary this cache may be explicitly invalidated via a call to
    // +link{formItem.invalidateDisplayValueCache()}
    //
    // @group display_values
    // @see formItem.optionDataSource
    // @see formItem.getSelectedRecord()
    // @see formItem.filterLocally
    // @visibility external
    //<
    fetchMissingValues:true,

    //> @attr formItem.alwaysFetchMissingValues (Boolean : false : IRWA)
    // If this form item has a specified +link{FormItem.optionDataSource} and
    // +link{formItem.fetchMissingValues} is true, when the item value changes, a fetch will be
    // performed against the optionDataSource to retrieve the related record
    // if +link{formItem.displayField} is specified and the new item value is not present in any
    // valueMap explicitly specified on the item.
    // <P>
    // Setting this property to true means that a fetch will occur against the optionDataSource
    // to retrieve the related record even if +link{formItem.displayField} is unset, or the
    // item has a valueMap which explicitly contains this field's value.
    // <P>
    // An example of a use case where this might be set would be if +link{formItem.formatValue}
    // or +link{formItem.formatEditorValue} were written to display properties from the
    // +link{formItem.getSelectedRecord(),selected record}.
    // <P>
    // Note - for efficiency we cache the associated record once a fetch has been performed, meaning
    // if the value changes, then reverts to a previously seen value, we do not kick
    // off an additional fetch even if this property is true. If necessary this cache may be
    // explicitly invalidated via a call to +link{formItem.invalidateDisplayValueCache()}
    //
    // @group display_values
    // @visibility external
    //<
    alwaysFetchMissingValues:false,


    //> @attr formItem.filterLocally (boolean : null : IRA)
    // If this form item is mapping data values to a display value by fetching records from a
    // dataSource (see +link{FormItem.optionDataSource}, +link{FormItem.displayField}
    // and +link{FormItem.fetchMissingValues}), setting this property to true ensures that when
    // the form item value is set, entire data-set from the dataSource is loaded at once and
    // used as a valueMap, rather than just loading the display value for the current value.
    // This avoids the need to perform fetches each time setValue() is called with a new value.
    // <P>
    // See also +link{PickList.filterLocally} for behavior on form items such as SelectItems
    // that show pick-lists.
    //
    // @group display_values
    // @visibility external
    //<

    // Data Type Formatters
    // ---------------------------------------------------------------------------------------
    // Note: dateFormatter and timeFormatter provide a way to control format of date or
    // time data in a generic form item such as a static text item.
    // Consistent name with ListGrid.dateFormatter / timeFormatter

    //> @attr formItem.dateFormatter (DateDisplayFormat : null : [IRWA])
    // Display format to use for date type values within this formItem.
    // <P>
    // Note that Fields of type <code>"date"</code>, <code>"datetime"</code> or
    // <code>"time"</code> will be edited using a +link{DateItem} or +link{TimeItem} by
    // default, but this can be overridden - for <code>canEdit:false</code> fields, a
    // +link{StaticTextItem} is used by default, and the developer can always specify
    // a custom +link{formItem.editorType} as well as +link{formItem.type,data type}.
    // <P>
    // The +link{formItem.timeFormatter} may also be used to format underlying Date values as
    // times (ommitting the date part entirely). If both <code>dateFormatter</code> and
    // <code>timeFormatter</code> are specified on an item, for
    // fields specified as +link{formItem.type,type "time"} the
    // <code>timeFormatter</code> will be used, otherwise the <code>dateFormatter</code>
    // <P>
    // If <code>item.dateFormatter</code> and <code>item.timeFormatter</code> is unspecified,
    // date display format may be defined at the component level via
    // +link{DynamicForm.dateFormatter}, or for fields of type <code>"datetime"</code>
    // +link{DynamicForm.datetimeFormatter}. Otherwise the
    // default is to use the system-wide default short date format, configured via
    // +link{Date.setShortDisplayFormat()}.  Specify any valid +link{type:DateDisplayFormat} to
    // change the format used by this item.
    // <P>
    // Note that if this is a freeform editable field, such a +link{TextItem}, with type
    // specified as <code>"date"</code> or <code>"datetime"</code> the system will automatically
    // attempt to parse user entered values back to a Date value, assuming the entered string
    // matches the date format for the field. Developers may further customize this via an
    // explicit +link{formItem.inputFormat} or via entirely custom
    // <var class="smartclient">
    // +link{formItem.formatEditorValue} and +link{formItem.parseEditorValue} methods.
    // </var>
    // <var class="smartgwt">
    // <code>setEditorValueFormatter</code> and <code>setEditorValueParser</code> methods.
    // </var>
    //
    // @see formItem.timeFormatter
    //
    // @group appearance
    // @visibility external
    //<
    //dateFormatter:null

    // Undocumented flag -- if no formatter is explicitly specified and we're looking at
    // a js date value should we use "normal" or "short" formatter by default.
    // Won't effect fields of type "date" since we never want to show the time (which is
    // always displayed in the "normal" format.
    useShortDateFormat:true,

    //> @attr formItem.timeFormatter (TimeDisplayFormat : null : [IRWA])
    // Time-format to apply to date type values within this formItem.  If specified, any
    // dates displayed in this item will be formatted as times using the appropriate format.
    // This is most commonly only applied to fields specified as type <code>"time"</code> though
    // if no explicit +link{formItem.dateFormatter} is specified it will be respected for other
    // fields as well.
    // <P>
    // If unspecified, a timeFormatter may be defined
    // +link{DynamicForm.timeFormatter,at the component level} and will be respected by fields
    // of type <code>"time"</code>.
    //
    // @group appearance
    // @visibility external
    //<
    //timeFormatter:null

    //> @attr formItem.displayFormat (varies : null : [IRWA])
    // Fields of type <code>"date"</code> or <code>"time"</code> will be edited using
    // a +link{DateItem} or +link{TimeItem} by default.
    // <P>
    // However this can be overridden - for <code>canEdit:false</code> fields, a
    // +link{StaticTextItem} is used by default, and the developer can always specify
    // a custom +link{formItem.editorType} as well as +link{formItem.type,data type}.
    // <P>
    // For fields of type <code>"date"</code>, set this property to a valid
    // +link{dateDisplayFormat} to specify how the date should be formatted.
    // <br>
    // For fields of type <code>"time"</code>, set this property to a valid
    // +link{type:TimeDisplayFormat, TimeDisplayFormat} to specify how the time should be formatted.
    // <br>
    // Note that if +link{formItem.dateFormatter} or +link{formItem.timeFormatter} are specified
    // they will take precedence over this setting.
    // <P>
    // If this field is of type <code>"date"</code> and is editable, the
    // +link{formItem.inputFormat} may be used to specify how user-edited date strings will
    // be parsed.
    //
    // @deprecated in favor of +link{formItem.dateFormatter} and +link{formItem.timeFormatter}
    // @see formItem.inputFormat
    // @see formItem.dateFormatter
    // @see formItem.timeFormatter
    // @visibility external
    //<

    //> @attr formItem.inputFormat (DateInputFormat : null : [IRWA])
    // For fields of type <code>"date"</code>, if this is an editable field such as a
    // +link{TextItem}, this property
    // allows you to specify the +link{DateItem.inputFormat, inputFormat} applied to the item.
    // @see formItem.dateFormatter
    // @visibility external
    //<

    //> @attr formItem.decimalPrecision (number : null : [IRW])
    // @include dataSourceField.decimalPrecision
    //
    // @group appearance
    // @serverDS allowed
    // @visibility external
    //<

    //> @attr formItem.decimalPad (number : null : [IRW])
    // @include dataSourceField.decimalPad
    //
    // @group appearance
    // @serverDS allowed
    // @visibility external
    //<




    // ValueIcons
    // ---------------------------------------------------------------------------------------
    //> @attr formItem.valueIcons   (Object : null : IRW)
    // A mapping of logical form item values to URLs.
    // If specified, when the form item is set to the value in question, an icon will be
    // displayed with the appropriate source URL.
    // @group   valueIcons
    // @setter  formItem.setValueIcons()
    // @see     formItem.getValueIcon()
    // @visibility external
    //<

    //> @attr formItem.emptyValueIcon (string : null : IRW)
    // This property allows the developer to specify an icon to display when this item has
    // no value. It is configured in the same way as any other valueIcon
    // (see +link{formItem.valueIcons})
    // @group valueIcons
    // @visibility external
    //<

    //> @attr formItem.showValueIconOnly (boolean : null : IRWA)
    // If +link{FormItem.valueIcons} is set, this property may be set to show the valueIcon
    // only and prevent the standard form item element or text from displaying
    // @group valueIcons
    // @visibility external
    //<
    //> @attr formItem.suppressValueIcon (boolean : null : IRWA)
    // If +link{FormItem.valueIcons} is set, this property may be set to prevent the value
    // icons from showing up next to the form items value
    // @group valueIcons
    // @visibility external
    //<

    //> @attr formItem.valueIconWidth (number : null : IRW)
    // If +link{formItem.valueIcons} is specified, use this property to specify a width for
    // the value icon written out.
    // @see FormItem.valueIconHeight
    // @see FormItem.valueIconSize
    // @group valueIcons
    // @visibility external
    //<

    //> @attr formItem.valueIconHeight (number : null : IRW)
    // If +link{formItem.valueIcons} is specified, use this property to specify a height for the
    // value icon written out.
    // @see FormItem.valueIconWidth
    // @see FormItem.valueIconSize
    // @group valueIcons
    // @visibility external
    //<

    //> @attr formItem.valueIconSize (number : 16 : IRW)
    // If +link{formItem.valueIcons} is specified, this property may be used to specify both
    // the width and height of the icon written out.
    // Note that +link{FormItem.valueIconWidth} and +link{formItem.valueIconHeight} take
    // precedence over this value, if specified.
    // @see FormItem.valueIconWidth
    // @see FormItem.valueIconHeight
    // @group valueIcons
    // @visibility external
    //<
    valueIconSize:16,

    //> @attr formItem.valueIconLeftPadding (number : 0 :  IRW)
    // If we're showing a value icon, this attribute governs the amount of space between the
    // icon and the start edge of the form item cell.
    // <p>
    // <b>NOTE:</b> In RTL mode, the valueIconLeftPadding is applied to the <em>right</em> of
    // the value icon.
    // @see FormItem.valueIcons
    // @visibility external
    // @group valueIcons
    //<
    valueIconLeftPadding:0,

    //> @attr formItem.valueIconRightPadding (number : 3 :  IRW)
    // If we're showing a value icon, this attribute governs the amount of space between the
    // icon and the value text.
    // <p>
    // <b>NOTE:</b> In RTL mode, the valueIconRightPadding is applied to the <em>left</em> of
    // the value icon.
    // @see FormItem.valueIcons
    // @visibility external
    // @group valueIcons
    //<
    valueIconRightPadding:3,

    //> @attr formItem.imageURLPrefix (string : null : IRWA)
    // Prefix to apply to the beginning of any +link{FormItem.valueIcons} when determining the
    // URL for the image.
    // Will not be applied if the <code>valueIcon</code> URL is absolute.
    // @group valueIcons
    // @visibility external
    //<

    //> @attr formItem.imageURLSuffix (string : null : IRWA)
    // Suffix to apply to the end of any +link{FormItem.valueIcons} when determining the URL for
    // the image. A common usage would be to specify a suffix of <code>".gif"</code> in which
    // case the <code>valueIcons</code> property would map values to the names of images without
    // the <code>".gif"</code> extension.
    // @group valueIcons
    // @visibility external
    //<

    // Internal
    // ---------------------------------------------------------------------------------------

    //> @attr formItem.form     (DynamicForm : null : R)
    // A Read-Only pointer to this formItem's DynamicForm widget.
    // @visibility external
    //<
    // Handles values for the form item.  Also handles writing the item's HTML by default.

    //> @attr formItem.containerWidget  (Canvas : null : RA)
    // A Read-Only pointer to the SmartClient canvas that holds this form item. In most cases this
    // will be the +link{formItem.form,DynamicForm} containing the item but in some cases
    // editable components handle writing out form items directly. An example of this
    // is +link{group:editing,Grid Editing} - when a listGrid shows per-field editors, the
    // <code>containerWidget</code> for each item will be the listGrid body.
    // <P>
    // Note that even if the <code>containerWidget</code> is not a DynamicForm, a DynamicForm
    // will still exist for the item (available as +link{formItem.form}), allowing access
    // to standard APIs such as +link{dynamicForm.getValues()}
    // @visibility external
    //<


    // RelationItem
    // ---------------------------------------------------------------------------------------

    //> @attr formItem.dataSource (DataSource or String : null : [IRWA])
    //
    // If this FormItem represents a foreignKey relationship into the dataSource of the form
    // containing this item, specify it here.
    //
    //  @visibility experimental
    //<


    // Picker Icon
    // -----------------------------------------------------------------------------------------

    //> @attr formItem.showPickerIcon (Boolean : null : IRW)
    // Should we show a special 'picker' +link{FormItemIcon,icon} for this form item? Picker
    // icons are customizable via +link{pickerIconProperties,pickerIconProperties}. By default
    // they will be rendered inside the form item's "control box" area, and will call
    // +link{FormItem.showPicker()} when clicked.
    // @group pickerIcon
    // @visibility external
    //<

    //> @attr formItem.showFocusedPickerIcon (Boolean : false : [IRW])
    // If +link{FormItem.showPickerIcon} is true for this item, should the picker icon show
    // a focused image when the form item has focus?
    // @group pickerIcon
    // @visibility external
    //<
    showFocusedPickerIcon:false,

    // We draw the icon into an exactly sized table cell - don't draw with any margin

    pickerIconHSpace:0,

    //> @attr formItem.pickerIconDefaults (FormItemIcon Properties : : IRWA)
    // Block of default properties to apply to the pickerIcon for this widget.
    // Intended for class-level customization: To modify this value we recommend using
    // +link{Class.changeDefaults()} rather than directly assigning a value to the property.
    // @group pickerIcon
    // @visibility external
    //<
    pickerIconDefaults: {
        click : function (form, item, icon) {
            item.showPicker();
        }
    },

    //> @attr formItem.pickerIconProperties (FormItemIcon Properties : null : IRWA)
    // If +link{showPickerIcon,showPickerIcon} is true for this item, this block of properties will
    // be applied to the pickerIcon. Allows for advanced customization of this icon.
    // @group pickerIcon
    // @visibility external
    //<

    //> @attr formItem.pickerIconName (identifier : "picker" : IRA)
    // If +link{showPickerIcon,showPickerIcon} is true, this attribute specifies the
    // +link{formItemIcon.name} applied to the picker icon
    // @group pickerIcon
    // @visibility external
    //<
    pickerIconName:"picker",

    //> @attr formItem.pickerIconSrc (SCImgURL : "" : IRWA)
    // If +link{showPickerIcon,showPickerIcon} is true for this item, this property governs the
    // +link{FormItemIcon.src,src} of the picker icon image to be displayed.
    // @group pickerIcon
    // @visibility external
    //<
    pickerIconSrc:"",

    //> @attr formItem.pickerIconWidth (int : null : IRWA)
    // If +link{showPickerIcon,showPickerIcon} is true for this item, this property governs the
    // size of the picker icon. If unset, the picker icon will be sized as a square to fit in the
    // available height for the icon.
    // <p>
    // It is not recommended to change the pickerIconWidth from the default value if +link{group:skinning,spriting}
    // is enabled because the image sprites are set up assuming specific, fixed dimensions of the picker
    // icon. If the pickerIconWidth must be changed, then the +link{pickerIconStyle,pickerIconStyle}
    // should be changed to a custom CSS style name.
    // @group pickerIcon
    // @visibility external
    //<

    //> @attr formItem.pickerIconHeight (int : null : IRWA)
    // If +link{showPickerIcon,showPickerIcon} is true for this item, this property governs the
    // size of the picker icon. If unset, the picker icon will be sized as a square to fit in the
    // available height for the icon.
    // <p>
    // It is not recommended to change the pickerIconHeight from the default value if +link{group:skinning,spriting}
    // is enabled because the image sprites are set up assuming specific, fixed dimensions of the picker
    // icon. If the pickerIconHeight must be changed, then the +link{pickerIconStyle,pickerIconStyle}
    // should be changed to a custom CSS style name.
    // @group pickerIcon
    // @visibility external
    //<

    //> @attr formItem.pickerIconPrompt (HTMLString : null : IR)
    // Prompt to show when the user hovers the mouse over the picker icon.
    // @group pickerIcon
    // @group i18nMessages
    // @visibility external
    //<

    // Picker Widget (pop-up launched by picker icon)
    // -----------------------------------------------------------------------------------------

    //> @attr formItem.picker (AutoChild Canvas : null : [IRW])
    // The component that will be displayed when formItem.showPicker() is called due to a click
    // on the +link{showPickerIcon,pickerIcon}.
    // <P>
    // Can be specified directly as a Canvas, or created automatically via the
    // +link{type:AutoChild} pattern.
    // <P>
    // Note that the picker is not automatically destroyed with the FormItem that uses it, in
    // order to allow possibly recycling of picker components.  To destroy a single-use picker,
    // override +link{Canvas.destroy()}.
    //
    // @visibility external
    //<

    //> @attr formItem.pickerConstructor (SCClassName : null : [IRW])
    // Class name of the picker to be created.
    //
    // @visibility external
    //<

    //> @attr formItem.pickerProperties (Canvas Properties : {} : [IRW])
    // Default properties for the picker.
    //
    // @visibility external
    //<


    // Validation
    // -----------------------------------------------------------------------------------------

    //> @attr formItem.validators     (Array of Validator : null : IR)
    // Validators for this form item.
    // <P>
    // <b>Note:</b> these validators will only be run on the client; to
    // do real client-server validation, validators must be specified on the DataSource.
    // @visibility external
    //<

    //> @attr formItem.required       (boolean : null : [IR])
    // Whether a non-empty value is required for this field to pass validation.
    // <P>
    // If the user does not fill in the required field, the error message to be shown will
    // be taken from these properties in the following order: +link{FormItem.requiredMessage},
    // +link{DynamicForm.requiredMessage}, +link{DataSource.requiredMessage},
    // +link{Validator.requiredField}.
    // <P>
    // <b>Note:</b> if specified on a FormItem, <code>required</code> is only enforced on the
    // client.  <code>required</code> should generally be specified on a
    // +link{class:DataSourceField}.
    //
    // @group validation
    // @visibility external
    // @example formShowAndHide
    //<

    //> @attr   formItem.requiredMessage     (string : null : [IRW])
    // The required message for required field errors.
    // @group validation
    // @visibility external
    //<

    // Status
    // -----------------------------------------------------------------------------------------

    //> @attr formItem.visible (Boolean : true : IRW)
    // Whether this item is currently visible.
    // <P>
    // <code>visible</code> can only be set on creation.  After creation, use
    // +link{formItem.show()} and +link{formItem.hide()} to manipulate visibility.
    //
    // @group appearance
    // @visibility external
    //<
    visible:true,

    //>    @attr    formItem.alwaysTakeSpace    (boolean : false : IRW)
    // If this form item is not visible, should they form it is contained in re-layout to
    // take advantage of the additional space, or should it continue to flow as if the
    // item were visible.  Set to true to have the form continue to flow around the item
    // even if it's not written out.
    //
    // @group appearance
    // @visibility internal
    //<

    //> @attr formItem.canEdit  (boolean : null : IRW)
    // Is this form item editable (canEdit:true) or read-only (canEdit:false)? Setting the
    // form item to non-editable causes it to render as read-only. Can be updated at runtime via
    // the +link{formItem.setCanEdit(),setCanEdit()} method.
    // <P>
    // Read-only appearance may be specified via +link{formItem.readOnlyDisplay}.
    // The default setting for this value (<code>"readOnly"</code>) differs from
    // the disabled state in that the form item is not rendered with disabled styling and
    // most form items will allow copying of the contents while read-only but do not while
    // disabled.
    // <P>
    // Note that for forms bound to a +link{DataSource}, if this property is not explicitly
    // set at the item level, its default value will match the
    // +link{DynamicForm.canEditFieldAttribute} on the associated dataSource field.
    // <P>
    // Developers should also be aware that the +link{readOnlyDisplay} attribute is
    // unrelated to the +link{dataSourceField.readOnlyEditorType} attribute. When a
    // DynamicForm is first bound to a dataSource, for
    // +link{dataSourceField.canEdit,canEdit:false} DataSourceFields,
    // +link{dataSourceField.readOnlyEditorType} will determine what +link{FormItemType}
    // should be created for the field. Once created, a FormItem's type can not be changed.
    // Setting +link{formItem.canEdit} at runtime will simply change the appearance
    // of the item to allow or disallow editing of the item.
    //
    // @setter FormItem.setCanEdit()
    // @group readOnly
    // @see FormItem.setCanEdit()
    // @see DynamicForm.setCanEdit()
    // @visibility external
    //<
    //canEdit: null,

    //> @attr formItem.readOnlyDisplay (ReadOnlyDisplayAppearance : null : IRW)
    // If this item is +link{FormItem.getCanEdit(),read-only}, how should this item be displayed
    // to the user? If set, overrides the form-level +link{DynamicForm.readOnlyDisplay} default.
    // @see DynamicForm.readOnlyDisplay
    // @visibility external
    //<
    //readOnlyDisplay: null,

    //> @attr formItem.readOnlyTextBoxStyle (FormItemBaseStyle : null : IRW)
    // Base text box style to apply when this item is +link{FormItem.getCanEdit(),read-only} and
    // is using +link{FormItem.readOnlyDisplay,readOnlyDisplay}
    // <var class="smartclient">"static".</var>
    // <var class="smartgwt">{@link com.smartgwt.client.types.ReadOnlyDisplayAppearance#STATIC}.</var>
    // If set, overrides the form-level +link{DynamicForm.readOnlyTextBoxStyle} default.
    // @see DynamicForm.readOnlyTextBoxStyle
    // @visibility external
    //<
    //readOnlyTextBoxStyle: null,

    //> @attr formItem.clipStaticValue (Boolean : null : IR)
    // If this item is +link{FormItem.getCanEdit(),read-only} and is using
    // +link{FormItem.readOnlyDisplay,readOnlyDisplay}
    // <var class="smartclient">"static",</var>
    // <var class="smartgwt">{@link com.smartgwt.client.types.ReadOnlyDisplayAppearance#STATIC},</var>
    // should the item value be clipped if it overflows the specified size of the item?
    // If set, overrides the form-level +link{DynamicForm.clipStaticValue} default.
    // @see DynamicForm.clipStaticValue
    // @visibility external
    //<
    //clipStaticValue: null,

    //>    @attr    formItem.disabled        (Boolean : false : IRW)
    // Whether this item is disabled.  Can be updated at runtime via the <code>setDisabled()</code>
    // method.  Note that if the widget containing this formItem is disabled, the formItem will
    // behave in a disabled manner regardless of the setting of the item.disabled property.
    // <p>
    // Note that not all items can be disabled, and not all browsers show an obvious disabled style
    // for native form elements.
    // @group appearance
    // @setter FormItem.setDisabled()
    // @see FormItem.setDisabled()
    // @visibility external
    // @example fieldEnableDisable
    //<

    //> @attr formItem.disableIconsOnReadOnly (Boolean : true : IRW)
    // If +link{formItem.canEdit} is set to false, should +link{formItem.icons,icons} be disabled
    // by default?
    // <P>
    // This may also be specified at the icon level. See +link{formItemIcon.disableOnReadOnly}.
    // @group formIcons
    // @visibility external
    //<
    disableIconsOnReadOnly:true,

    // Keyboard handling
    // -----------------------------------------------------------------------------------------

    //>@attr formItem.canFocus  (boolean : null : IRA)
    // Is this form item focusable? Setting this property to true on an otherwise
    // non-focusable element such as a +link{StaticTextItem} will cause the item to be
    // included in the page's tab order and respond to keyboard events.
    // @group focus
    // @visibility external
    //<
    // If not set focusability is determined by whether this item has a data element by default.
    // If set to true, and this item has no data element we write out HTML to allow focus
    // on the item's text-box.

    //> @attr formItem.accessKey  (keyChar : null : IRW)
    //      AccessKey - a keyboard shortcut to trigger a form item's default behavior.<br>
    //      Note: Alt+ (or in Moz Firefox 2.0 and above, Shift+Alt+) the specified key will
    //      trigger the form item's default behavior.
    // @group focus
    // @visibility external
    //<

    accessKey:null,

    //> @attr formItem.tabIndex (integer : null : IRW)
    // TabIndex for the form item within the form, which controls the order in which controls
    // are visited when the user hits the tab or shift-tab keys to navigate between items.
    // <P>
    // tabIndex is automatically assigned as the order that items appear in the
    // +link{dynamicForm.items} list.
    // <P>
    // To specify the tabindex of an item within the page as a whole (not just this form), use
    // +link{globalTabIndex} instead.
    //
    //  @visibility external
    //  @group focus
    //<
    //tabIndex:null,

    //> @attr formItem.globalTabIndex (integer : null : IRWA)
    // TabIndex for the form item within the page. Takes precedence over any local tab index
    // specified as +link{tabIndex,item.tabIndex}.
    // <P>
    // Use of this API is <b>extremely</b> advanced and essentially implies taking over
    // management of tab index assignment for all components on the page.
    //
    // @group focus
    // @visibility external
    //<
    //globalTabIndex:null,

    //> @attr   formItem.selectOnFocus  (boolean : null : IRW)
    // Allows the +link{dynamicForm.selectOnFocus,selectOnFocus} behavior to be configured on a
    // per-FormItem basis.  Normally all items in a form default to the value of
    // +link{dynamicForm.selectOnFocus}.
    //
    // @group focus
    // @visibility external
    //<
    // Exposed on TextItem and TextAreaItem directly since it won't apply to other items.

    //> @attr   formItem.selectOnClick  (boolean : null : IRW)
    // Allows the +link{dynamicForm.selectOnClick,selectOnClick} behavior to be configured on a
    // per-FormItem basis.  Normally all items in a form default to the value of
    // +link{dynamicForm.selectOnClick}.
    //
    // @group focus
    // @visibility external
    //<
    // Exposed on TextItem and TextAreaItem directly since it won't apply to other items.


    //> @attr formItem.changeOnKeypress (boolean : true : IRW)
    // Should this form item fire its +link{formItem.change(),change} handler (and store its
    // value in the form) on every keypress? Set to <code>false</code> to suppress the 'change'
    // handler firing (and the value stored) on every keypress.
    // <p>
    // Note: If <code>false</code>, the value returned by +link{formItem.getValue(),getValue}
    // will not reflect the value displayed in the form item element as long as focus is in
    // the form item element.
    //
    // @group  eventHandling, values
    //<
    changeOnKeypress:true,

    // maintainSelectionOnTransform
    // Internal, but non obfuscated property.
    // Ensure selection is maintained if:
    // - the value is reverted due to a change handler returning false during typing
    // - the value is modified during typing, but either the value is unchanged except for
    //   case shifting, or the entire value was selected before typing.
    // If this causes a performance hit in any cases, we can disable it.
    maintainSelectionOnTransform:true,


    //> @attr   formItem.dirtyOnKeyDown (boolean : true : RW)
    //  Should this form item get marked as dirty on every keyDown?
    //  @group  eventHandling, values
    //<
    dirtyOnKeyDown:true,

    // Titles
    // --------------------------------------------------------------------------------------------
    //> @attr formItem.showTitle (Boolean : true : IRW)
    // Should we show a title cell for this formItem?
    // <p>
    // Note: the default value of this attribute is overridden by some subclasses.
    //
    // @group title
    // @visibility external
    //<
    showTitle:true,

    //> @attr formItem.titleOrientation (TitleOrientation : isc.Canvas.LEFT : IRW)
    // On which side of this item should the title be placed.  +link{type:TitleOrientation}
    // lists valid options.
    // <P>
    // Note that titles on the left or right take up a cell in tabular
    // +link{group:formLayout,form layouts}, but titles on top do not.
    //
    // @group title
    // @see DynamicForm.titleOrientation
    // @visibility external
    //<
// titleOrientation:isc.Canvas.LEFT, // dynamically defaulted in DynamicForm

    //> @attr formItem.titleAlign (Alignment : null : IRW)
    // Alignment of this item's title in its cell.
    // <p>
    // If null, dynamically set according to text direction.
    // @group title
    // @visibility external
    //<

    //> @attr formItem.titleVAlign (VerticalAlignment : isc.Canvas.CENTER : IRW)
    // Vertical alignment of this item's title in its cell. Only applies when
    // +link{formItem.titleOrientation} is <code>"left"</code> or <code>"right"</code>.
    // @group title
    // @visibility external
    //<
//    titleVAlign:isc.Canvas.CENTER, // dynamically defaulted in getTitleVAlign

    //> @attr formItem.clipTitle (boolean : null : [IRW])
    // If the title for this form item is showing, and is too large for the available space
    // should the title be clipped?
    // <p>
    // Null by default - if set to true or false, overrides +link{DynamicForm.clipItemTitles}.
    // @group title
    // @visibility external
    //<
    clipTitle:null,

    //> @attr formItem.wrapTitle (boolean : null : [IRW])
    // If specified determines whether this items title should wrap.
    // Overrides +link{DynamicForm.wrapItemTitles,wrapItemTitles} at the DynamicForm level.
    // @group title
    // @visibility external
    //<
//    wrapTitle:null,

    // Change handling
    // --------------------------------------------------------------------------------------------

    //> @attr formItem.redrawOnChange (Boolean : false : IRW)
    // If true, this item will cause the entire form to be redrawn
    // when the item's "elementChanged" event is done firing
    // @group changeHandling
    // @visibility external
    //<

    //> @attr formItem.validateOnChange (Boolean : false : IRW)
    // If true, form items will be validated when each item's "change" handler is fired
    // as well as when the entire form is submitted or validated.
    // <p>
    // Note that this property can also be set at the form level or on each validator;
    // If true at the form or field level, validators not explicitly set with
    // <code>validateOnChange:false</code> will be fired on change - displaying errors and
    // rejecting the change on validation failure.
    // @group changeHandling
    // @visibility external
    // @see DynamicForm.validateOnChange
    //<


    //> @attr formItem.validateOnExit (Boolean : false : IRW)
    // If true, form items will be validated when each item's "editorExit" handler is fired
    // as well as when the entire form is submitted or validated.
    // <p>
    // Note that this property can also be set at the form level.
    // If true at either level the validator will be fired on editorExit.
    // @visibility external
    // @see dynamicForm.validateOnExit
    //<

    //> @attr formItem.stopOnError (boolean : null : IR)
    // Indicates that if validation fails, the user should not be allowed to exit
    // the field - focus will be forced back into the field until the error is corrected.
    // <p>
    // This property defaults to +link{DynamicForm.stopOnError} if unset.
    // <p>
    // Enabling this property also implies +link{FormItem.validateOnExit} is automatically
    // enabled. If there are server-based validators on this item, setting this property
    // also implies that +link{FormItem.synchronousValidation} is forced on.
    //
    // @visibility external
    //<

    //> @attr formItem.rejectInvalidValueOnChange (Boolean : false : IRWA)
    // If validateOnChange is true, and validation fails for this item on change, with no suggested
    // value, should we revert to the previous value, or continue to display the bad value entered
    // by the user. May be set at the item or form level.
    // @visibility external
    //<
    // Introduced for back-compat: pre 7.0beta2 this was the default behavior, so enable this flag
    // at the item or form level if required for backcompat.
    //rejectInvalidValueOnChange:null,


    //>    @attr formItem.changeOnBlur (boolean : false : IRWA)
    // If true, this item will fire it's elementChanged message on BLUR in a field (eg: when the
    // user tabs through without making changes as well as when they actually change something), if
    // false, the message will only fire when the field is actually changed.  This is useful for
    // fields that do validation/value setting on change (such as the TimeItem), to work around a
    // bug in IE where the change event doesn't always fire correctly when you manually set the
    // value of the field in its ONCHANGE handler.  Note that it is not recommended that you set
    // both changeOnBlur==true AND redrawOnChange==true, as this will cause the form to redraw every
    // time you tab through that item.
    // @group changeHandling
    //<


    //> @attr  formItem.synchronousValidation (boolean : null : IR)
    // If enabled, whenever validation is triggered and a request to the server is required,
    // user interactivity will be blocked until the request returns. Can be set for the entire
    // form or individual FormItems.
    // <p>
    // If false, the form will try to avoid blocking user interaction until it is strictly
    // required. That is until the user attempts to use a FormItem whose state could be
    // affected by a server request that has not yet returned.
    //
    // @visibility external
    //<

    // Size and Layout
    // --------------------------------------------------------------------------------------------
    //> @attr formItem.width (number : "*" : IRW)
    // Width of the FormItem.  Can be either a number indicating a fixed width in pixels, or
    // "*" indicating the FormItem fills the space allocated to it's column (or columns, for a
    // +link{colSpan,column spanning} item).
    // <P>
    // See the +link{group:formLayout} overview for details.
    //
    // @group formLayout
    // @visibility external
    // @example columnSpanning
    //<
    width:"*",

    //> @attr formItem.height (number : 20 : IRW)
    // Height of the FormItem.  Can be either a number indicating a fixed height in pixels, a
    // percentage indicating a percentage of the overall form's height, or "*" indicating take
    // whatever remaining space is available. See the +link{group:formLayout} overview for details.
    // <p>
    // For form items having a +link{showPickerIcon,picker icon} (e.g. +link{SelectItem}, +link{ComboBoxItem})
    // and +link{SpinnerItem}s, if +link{group:skinning,spriting} is enabled, it is not recommended
    // to change the height of the form item from the default because the image sprites are set up
    // assuming a specific, fixed height of the item. If the item height must be changed, then
    // the +link{pickerIconStyle,pickerIconStyle} should be changed to a custom CSS style name.
    // Or, in the case of +link{SpinnerItem}s, the +link{FormItemIcon.baseStyle,baseStyle} and
    // +link{FormItemIcon.src,src} of SpinnerItem.INCREASE_ICON and SpinnerItem.DECREASE_ICON
    // should be customized.
    //
    // @group formLayout
    // @visibility external
    // @example formLayoutFilling
    //<

    height:20,

    //> @attr formItem.cellHeight (number : null : IRW)
    // If specified, this property will govern the height of the cell in which this form
    // item is rendered.
    // Will not apply when the containing DynamicForm sets <code>itemLayout:"absolute"</code>.
    // @visibility external
    //<


    //> @attr formItem.titleColSpan  (number : 1 : IRW)
    // Number of columns that this item's title spans.
    // <P>
    // This setting only applies for items that are showing a title and whose
    // +link{titleOrientation} is either "left" or "right".
    //
    // @group formLayout
    // @visibility external
    //<
    titleColSpan:1,

    //> @attr formItem.colSpan (number : 1 : IRW)
    // Number of columns that this item spans.
    // <P>
    // The <code>colSpan</code> setting does not include the title shown for items with
    // +link{showTitle}:true, so the effective <code>colSpan</code> is one higher than this
    // setting for items that are showing a title and whose +link{titleOrientation} is either
    // "left" or "right".
    //
    // @group formLayout
    // @visibility external
    //<
    colSpan:1,

    //> @attr formItem.rowSpan (number : 1 : IRW)
    // Number of rows that this item spans
    // @group formLayout
    // @visibility external
    //<
    rowSpan:1,

    //> @attr formItem.startRow (Boolean : false : IRW)
    // Whether this item should always start a new row in the form layout.
    // @group formLayout
    // @visibility external
    //<

    //> @attr formItem.endRow (Boolean : false : IRW)
    // Whether this item should end the row it's in in the form layout
    // @group formLayout
    // @visibility external
    //<

    // The align property is used by the dynamic form to align the item as a whole (control table
    // and all) in its cell.
    // In addition to this we support textAlign to align the contents within the text-box


    //> @attr formItem.align (Alignment : isc.Canvas.LEFT : IRW)
    // Alignment of this item in its cell. Note that the alignment of content within this item
    // can be separately controlled via +link{formItem.textAlign} (typically only applies to items
    // showing a "textBox", such as +link{textItem,textItems} or +link{selectItem,selectItems}).
    // @group appearance
    // @visibility external
    //<

    //> @attr formItem.vAlign (VerticalAlignment : isc.Canvas.CENTER : IRW)
    // Vertical alignment of this item within its cell. This property governs the position
    // of the item's text box within the cell (not the content within the text box).
    // If +link{shouldApplyHeightToTextBox()} is true, for this to have a visible effect,
    // the cell height must exceed the specified height of the item, either due to
    // an explicit +link{cellHeight} specification, or due to the row being expanded
    // by another taller item.
    // <P>
    // Has no effect if +link{dynamicForm.itemLayout} is set to <code>"absolute"</code> for the
    // form.
    //
    // @group title
    // @visibility external
    //<

    //> @attr   formItem.textAlign  (Alignment : isc.Canvas.LEFT : IRW)
    // Alignment of the text / content within this form item. Note that +link{formItem.align} may
    // be used to control alignment of the entire form item within its cell. May not apply to all
    // form item types.
    // @group appearance
    // @visibility external
    //<

    //> @attr formItem.left (int : 0 : IRWA)
    // Left coordinate of this item in pixels.  Applies only when the containing DynamicForm
    // sets <code>itemLayout:"absolute"</code>.
    // @visibility absForm
    //<

    //> @attr formItem.top (int : 0 : IRWA)
    // Top coordinate of this item in pixels.  Applies only when the containing DynamicForm
    // sets <code>itemLayout:"absolute"</code>.
    // @visibility absForm
    //<

    //> @attr formItem.wrap (boolean : null : IRW)
    // If true, item contents can wrap. If false, all the contents should appear on a single line.
    // @visibility internal
    //<

    //> @attr formItem.clipValue  (Boolean : null : IRW)
    // If true, text that exceeds the specified size of the form item will be clipped.
    // @visibility internal
    //<



    // AutoComplete
    // --------------------------------------------------------------------------------------------
    //> @attr formItem.autoComplete   (AutoComplete : null : IRW)
    // Whether to do inline autoComplete.
    // <p>
    // If unset, defaults to form.autoComplete
    //
    // @see dynamicForm.autoComplete
    // @visibility autoComplete
    //<

    //> @attr formItem.uniqueMatch    (boolean : null : IRW)
    // When autoComplete is enabled, whether to offer only unique matches to the user.
    // <p>
    // If unset, defaults to form.uniqueMatch.
    //
    // @see dynamicForm.uniqueMatch
    // @visibility autoComplete
    //<

    //> @attr formItem.autoCompleteCandidates (Array : null : IRW)
    // Optional set of candidate completions.
    // <p>
    // If candidates are not specified, the values in the valueMap, if any, will be used, or
    // within a DataBound form, the other values currently present in the loaded portion of the
    // dataset.
    // @visibility autoComplete
    //<


    //>@attr    formItem.browserSpellCheck  (boolean : null : IRWA)
    // If this browser supports spell-checking of text editing elements, do we want this
    // enabled for this item? If unset the property will be inherited from the containing form.
    // <P>
    // Notes:<br>
    // - this property only applies to text based items such as TextItem and TextAreaItem.<br>
    // - this property is not supported on all browsers.
    // @see dynamicForm.browserSpellCheck
    // @visibility external
    //<

    // In addition to spell-check Safari on iPhone / iPad supports the following features on
    // text items:
    // - auto capitalizing
    //>@attr formItem.browserAutoCapitalize (boolean : null : IRWA)
    // This property may be set to <code>false</code> to explicitly
    // set the native HTML <code>"autocapitalize"</code> attribute to <code>"off"</code>
    // in the data element of this item.
    // <P>
    // This attribute is used by some browsers- typically on mobile platforms - as
    // a way to disable default auto-capitalize behavior within the item.
    //

    // @visibility internal
    //<

    // - auto correct
    //>@attr formItem.browserAutoCorrect (boolean : null : IRWA)
    // This property may be set to <code>false</code> to explicitly
    // set the native HTML <code>"autocorrect"</code> attribute to <code>"off"</code>
    // in the data element of this item.
    // <P>
    // This attribute is used by some browsers- typically on mobile platforms - as
    // a way to disable default auto-correct behavior within the item.
    //
    // @visibility internal
    //<

    // - which keyboard to display (text, phone, url, email, zip)
    //>@attr formItem.browserInputType (String : null : IRWA)
    // Form item input type - governs which keyboard should be displayed for
    // mobile devices (supported on iPhone / iPad)
    // @visibility internal
    //<
    browserInputTypeMap:{
        "digits": "number",

        // likely synonym
        "phone": "tel"
    },

    // Icons
    // --------------------------------------------------------------------------------------------

    //>@attr    formItem.icons      (Array of FormItemIcon Properties : null  : IRW)
    //  An array of descriptor objects for icons to display in a line after this form item.
    //  These icons are clickable images, often used to display some kind of helper for
    //  populating a form item.
    //  @group formIcons
    //  @see    class:FormItemIcon
    //  @visibility external
    //  @example formIcons
    //<

    //> @attr   formItem.defaultIconSrc      (SCImgURL : "[SKIN]/DynamicForm/default_formItem_icon.gif" : IRWA)
    // Default icon image source.
    // Specify as the partial URL to an image, relative to the imgDir of this component.
    // To specify image source for a specific icon use the <code>icon.src</code> property.<br>
    // If this item is drawn in the disabled state, the url will be modified by adding
    // "_Disabled" to get a disabled state image for the icon.
    // If <code>icon.showOver</code> is true, this url will be modified by adding "_Over" to get
    // an over state image for the icon.
    //  @group  formIcons
    //  @visibility external
    //<
    defaultIconSrc:"[SKIN]/DynamicForm/default_formItem_icon.gif",

    //> @attr   formItem.showOverIcons  (boolean : null : IRWA)
    //  If we're showing icons, should we change their image source to the appropriate <i>over</i>
    //  source when the user rolls over (or puts focus onto) them?  Can be overridden on a per
    //  icon basis by the formItemIcon <code>showOver</code> property.
    //  @group formIcons
    //  @visibility external
    //<

    //> @attr   formItem.showFocusedIcons (boolean : null : IRWA)
    //  If we're showing icons, should we change their image source to the appropriate
    //  <i>focused</i>  source when this item has focus?  Can be overridden on a per
    //  icon basis by the formItemIcon <code>showFocused</code> property.
    //  @group formIcons
    //  @visibility external
    //<

    //> @attr   formItem.iconHSpace (integer : 3 : IR)
    // Horizontal space (in px) to leave between form item icons. The space
    // appears to the left of each icon. May be overridden at the icon level via
    // +link{formItemIcon.hspace}.
    //  @group  formIcons
    // @visibility external
    //<
    iconHSpace:3,

    //> @attr   formItem.iconVAlign (VerticalAlignment: "bottom" : [IRWA])
    //      How should icons be aligned vertically for this form item.
    //  @group  formIcons
    //  @visibility external
    //<
    // Options are "top", "center", "bottom" - Implemented via the css 'vertical-alignment'
    // property
    iconVAlign:isc.Canvas.BOTTOM,

    //> @attr   formItem.iconHeight (number : 20 : IRWA)
    //      Default height for form item icons
    //  @group  formIcons
    //  @visibility external
    //<
    iconHeight:20,

    //> @attr   formItem.iconWidth (number : 20 : IRWA)
    //      Default width for form item icons
    //  @visibility external
    //  @group  formIcons
    //<
    iconWidth:20,

    //> @attr formItem.prompt (string : null : IRW)
    //
    // This text is shown as a tooltip prompt when the cursor hovers over this item.
    //
    // @group basics
    // @visibility external
    //<

    // FormItem.implementsPromptNatively:
    // By default we show prompts in an ISC hover. However for some form items we'll write
    // out HTML that will cause a native hover prompt to show up instead. In these cases
    // we suppress the ISC hover

    //implementsPromptNatively:false,

    //> @attr formItem.iconPrompt (string : "" : IRWA)
    // Default prompt (and tooltip-text) for icons.
    // @group formIcons
    // @visibility external
    //<

    iconPrompt:"",

    //> @attr   formItem.showIcons (Boolean : true : IRWA)
    // Set to false to suppress writing out any +link{formItem.icons} for this item.
    //  @group  formIcons
    //  @visibility external
    //<

    showIcons:true,



    //> @attr   formItem.redrawOnShowIcon (boolean : true : IRWA)
    //      When dynamically showing/hiding icons for this form  item, should we force a
    //      redraw of the entire form?
    //  @group  formIcons
    //<

    redrawOnShowIcon:false,

    //> @attr   formItem.canTabToIcons  (boolean : null : IRWA)
    // If set, this property determines if this form item's icons should be included in
    // the page's tab order, with the same tabIndex as this form item?
    // <P>
    // Note that if this form item has tabIndex -1, neither the form item nor the icons
    // will be included in the page's tab order.
    //
    // @group  formIcons
    // @visibility advancedIcons
    //<
    //canTabToIcons:null,

    // Validation Error Icon
    // We write out a special icon to indicate validation errors. This will not be part of the
    // icons array but will be rendered using some of the same code.

    //> @attr   formItem.errorIconHeight    (number : 16 : IRW)
    // Height of the error icon, if we're showing icons when validation errors occur.
    // @group  errorIcon
    // @see FormItem.showErrorIcon
    // @visibility external
    //<
    errorIconHeight: 16,

    //> @attr   formItem.errorIconWidth    (number : 16 : IRW)
    // Height of the error icon, if we're showing icons when validation errors occur.
    // @group  errorIcon
    // @see FormItem.showErrorIcon
    // @visibility external
    //<
    errorIconWidth: 16,

    //> @attr   formItem.errorIconSrc    (SCImgURL : "[SKIN]/DynamicForm/validation_error_icon.png" : IRW)
    // URL of the image to show as an error icon, if we're showing icons when validation
    // errors occur.
    // @group  errorIcon
    // @see FormItem.showErrorIcon
    // @visibility external
    //<
    errorIconSrc: "[SKIN]/DynamicForm/validation_error_icon.png",

    //> @attr   formItem.showErrorIcon (boolean : null : IRW)
    // @include dynamicForm.showErrorIcons
    //  @group  errorIcon, validation, appearance
    //  @visibility external
    //<
    // Note Actually writing this item (and the error text) into the DOM is handled by the
    // Form (or containerWidget) - but this property governs whether we include the icon in the
    // errorHTML we return.
    //showErrorIcon: null,

    //> @attr   formItem.showErrorText (boolean : null : IRW)
    // @include dynamicForm.showErrorIcons
    // @group  validation, appearance
    // @visibility external
    //<
    //showErrorText: null,

    //> @attr formItem.showErrorStyle     (boolean : null : IRW)
    // @include dynamicForm.showErrorIcons
    // @group validation, appearance
    // @visibility external
    // @see FormItem.cellStyle
    //<
    //showErrorStyle: null,

    //> @attr formItem.errorOrientation (align : null : IRW)
    // If +link{dynamicForm.showInlineErrors} is true, where should the error icon and text appear
    // relative to the form item itself. Valid options are <code>"top"</code>,
    // <code>"bottom"</code>, <code>"left"</code> or <code>"right"</code>.<br>
    // If unset the orientation will be derived from +link{dynamicForm.errorOrientation}.
    // @group validation, appearance
    // @visibility external
    //<
    //errorOrientation: null,

    // Define a jsdoc pseudo-class for form item icon descriptor objects.
    // ------

        //> @object FormItemIcon
        //
        //  Form item icon descriptor objects used by Form Items to specify the appearance and
        //  behavior of icons displayed after the item in the page flow.
        //
        //  @treeLocation   Client Reference/Forms/Form Items/FormItem
        //  @see attr:FormItem.icons
        //  @group formIcons
        //  @visibility external
        //  @example formIcons
        //<

        //> @attr formItemIcon.baseStyle (CSSStyleName : null : IRWA)
        // Base CSS style. If set, as the component changes state and/or is focused, suffixes
        // will be added to the base style. Possible suffixes include "Over" if the user mouses
        // over the icon and +link{FormItemIcon.showOver,this.showOver} is true, "Disabled" if
        // the icon is disabled, and "Focused". In addition, if +link{FormItemIcon.showRTL,showRTL}
        // is enabled, then an "RTL" suffix will be added.
        // @visibility external
        //<

        //> @attr formItemIcon.name (identifier : null : IR)
        // Identifier for this form item icon. This identifier (if set) should be unique
        // within this form item and may be used to get a pointer to the icon object
        // via +link{FormItem.getIcon()}.
        // @visibility external
        //<
        // Note: Also used by the AutoTest subsystem - if the name is autoGenerated, we can't
        // guarantee it won't change as the application changes (specifically the order of
        // icons for this form item changes).
        // For auto-generated icons, such as the picker we should always provide a reliable
        // standard name

        //> @attr formItemIcon.src (SCImgURL : null : IRW)
        // If set, this property determines this icon's image source.
        // If unset the form item's <code>defaultIconSrc</code> property will be used
        // instead.<br>
        // As with <code>defaultIconSrc</code> this URL will be modified by adding
        // "_Over" or "_Disabled" if appropriate to show the icon's over or disabled state.
        // <p>
        // If +link{FormItemIcon.showRTL,showRTL} is enabled, then "_rtl" will be added to the
        // source URL before the extension.
        // <p>
        // The special value "blank" means that no image will be shown for this icon. This
        // is particularly useful together with +link{FormItemIcon.baseStyle} to implement
        // spriting of the different icon states.
        //
        // @group formIcons
        // @see attr:formItem.defaultIconSrc
        // @example formIcons
        // @visibility external
        //<

        //> @attr formItemIcon.showOver (boolean : null : IRWA)
        // Should this icon's image and/or +link{FormItemIcon.baseStyle,baseStyle} switch to the
        // appropriate "Over" value when the user rolls over or focuses on the icon?
        // @group  formIcons
        // @visibility external
        // @see attr:formItem.showOverIcons
        //<

        //> @attr formItemIcon.showFocused (Boolean : null : IRWA)
        // Should this icon's image and/or +link{FormItemIcon.baseStyle,baseStyle} switch to the
        // appropriate "Focused" value when the user puts focus on the form item or icon?
        // @see formItem.showFocusedIcons
        // @see formItemIcon.showFocusedWithItem
        // @group  formIcons
        // @visibility external
        //<

        //> @attr formItemIcon.showRTL (Boolean : null : IRA)
        // Should this icon's +link{FormItemIcon.src,src} and/or +link{FormItemIcon.baseStyle,baseStyle}
        // switch to the appropriate RTL value when the FormItem is in RTL mode? If true, then
        // the image URL for all states will have "_rtl" added before the extension. Also, if
        // baseStyle is set, all style names will have an "RTL" suffix. This should only be
        // enabled if RTL media is available.
        // <p>
        // For example, if an icon's src is "[SKINIMG]formItemIcons/myFormIcon.png" and the baseStyle
        // is "myFormIcon", then in the "Down" state, SmartClient will use "[SKINIMG]formItemIcons/myFormIcon_Down_rtl.png"
        // for the image source and "myFormIconDownRTL" for the style name.
        // @group RTL
        // @group formIcons
        // @visibility external
        //<

        //> @attr formItemIcon.showFocusedWithItem (boolean : null : IRWA)
        // If this icon will be updated to show focus (see +link{formItemIcon.showFocused},
        // +link{formItem.showFocusedIcons}), this property governs whether the focused state should
        // be shown when the item as a whole receives focus or just if the icon receives focus.
        // If this property is unset, default behavior is to show focused state when the item
        // receives focus.
        // @see formItem.showFocusedIcons
        // @see formItemIcon.showFocused
        // @group  formIcons
        // @visibility external
        //<

        //> @attr formItemIcon.neverDisable (boolean : null : IRWA)
        // If <code>icon.neverDisable</code> is true, when this form item is disabled, the
        // icon will remain enabled.
        // Note that disabling the entire form will disable all items, together with their
        // icons including those marked as neverDisable - this property only has an effect
        // if the form is enabled and a specific item is disabled within it.
        // <P>
        // If this property is true, the icons will also remain enabled if a form item
        // is marked as +link{formItem.canEdit,canEdit:false}. For finer grained control over
        // whether icons are enabled for read-only icons see +link{formItem.disableIconsOnReadOnly}
        // and +link{formItemIcon.disableOnReadOnly}
        //
        // @group  formIcons
        // @visibility external
        //<

        //> @attr formItemIcon.disableOnReadOnly (boolean : null : IRWA)
        // If +link{formItem.canEdit} is set to false, should this icon be disabled.
        // If unset this is determined by +link{formItem.disableIconsOnReadOnly}.
        // Note that if +link{formItemIcon.neverDisable} is set to true, the icons will
        // be rendered enabled regardless of this setting and whether the item is editable.
        // @group formIcons
        // @visibility external
        //<

        //> @attr formItemIcon.tabIndex (int : null : IRA)
        // TabIndex for this formItemIcon.
        // <P>
        // Set to -1 to remove the icon from the tab order, but be cautious doing so: if the
        // icon triggers important application functionality that cannot otherwise be accessed
        // via the keyboard, it would be a violation of accessibility standard to remove the
        // icon from the tab order.
        // <P>
        // Any usage other than setting to -1 is extremely advanced in the same way as using
        // +link{formItem.globalTabIndex}.
        //
        // @group formIcons
        // @visibility external
        //<

        //> @method formItemIcon.click()
        //      StringMethod action to fire when this icon is clicked
        //      If unset the form item's <code>iconClick</code> method will be fired instead (if
        //      specified).
        //  @param  form (DynamicForm)  The Dynamic Form to which this icon's item belongs.
        //  @param  item (FormItem)     The Form Item containing this icon
        //  @param  icon (FormItemIcon) A pointer to the form item icon clicked
        //  @group  formIcons
        //  @visibility external
        //  @example formIcons
        //<

        //> @method formItemIcon.keyPress()
        //      StringMethod action to fire when this icon has focus and receives a keypress
        //      event.
        //      If unset the form item's <code>iconKeyPress</code> method will be fired instead
        //      (if specified).
        //  @param  keyName (string)    Name of the key pressed
        //  @param  character (character) character produced by the keypress
        //  @param  form (DynamicForm)  The Dynamic Form to which this icon's item belongs.
        //  @param  item (FormItem)     The Form Item containing this icon
        //  @param  icon (FormItemIcon) A pointer to the form item icon
        //  @group  formIcons
        //  @visibility external
        //<

        //> @attr   formItemIcon.width (number : null : IRW)
        //      If set, this property determines the width of this icon in px.
        //      If unset the form item's <code>iconWidth</code> property will be used instead.
        //  @group  formIcons
        //  @visibility external
        //  @see    attr:formItem.iconWidth
        //<

        //> @attr   formItemIcon.height (number : null : IRW)
        //      If set, this property determines the height of this icon in px.
        //      If unset the form item's <code>iconHeight</code> property will be used instead.
        //  @group  formIcons
        //  @visibility external
        //  @see    attr:formItem.iconHeight
        //<

        //> @attr   formItemIcon.prompt (string : null : IRWA)
        // If set, this property will be displayed as a prompt (and tooltip text) for this form
        // item icon.
        // <P>
        // If unset the form item's <code>iconPrompt</code> property will be used instead.
        //
        //  @group  formIcons
        //  @visibility external
        //  @see    attr:formItem.iconPrompt
        //<

        //> @attr   formItemIcon.hspace (integer : null : IR)
        //      If set, this property determines the number of pixels space to be displayed on
        //      the left of this form item icon.<br>
        //      If unset the form item's <code>iconHSpace</code> property will be used instead.
        //  @group  formIcons
        //  @see    attr:formItem.iconHSpace
        // @visibility external
        //<

        //> @method formItemIcon.showIf ()
        // If specified, <code>icon.showIf</code> will be evaluated when the form item is
        // drawn or redrawn. Return true if the icon should be visible, or false if it
        // should be hidden. Note that if +link{formItem.showIcon()} or +link{formItem.hideIcon()}
        // is called, this method will be overridden.
        // @param form (DynamicForm) the DynamicForm in which the icon is embedded
        // @param item (FormItem) the item to which this icon is attached.
        // @return (boolean) Return true if the icon should be visible, false otherwise.
        // @visibility external
        //<


    // Hints
    // --------------------------------------------------------------------------------------------

    //> @attr formItem.hint (HTMLString : null : IRW)
    // Specifies "hint" string to show next to the form item to indicate something to the user.
    // This string generally appears to the right of the form item.
    //
    // @see hintStyle
    // @group appearance
    // @visibility external
    // @example formHints
    //<

    //> @attr formItem.showHint (Boolean : true : IRWA)
    // If a hint is defined for this form item, should it be shown?
    //
    // @group appearance
    // @visibility external
    //<
    showHint:true,

    // Styles
    // --------------------------------------------------------------------------------------------

    //> @attr formItem.showFocused     (Boolean : false : IRWA)
    // When this item receives focus, should it be re-styled to indicate it has focus?
    //
    // @group appearance
    // @visibility external
    // @see formItem.cellStyle
    //<
    showFocused:false,

    //> @attr formItem.showDisabled (Boolean : true : IRWA)
    // When this item is disabled, should it be re-styled to indicate its disabled state?
    //
    // @group appearance
    // @visibility external
    // @see cellStyle
    //<
    showDisabled:true,

    //> @attr formItem.showRTL (boolean : false : IRA)
    // When this item is in RTL mode, should its style name include an "RTL" suffix?
    // @group RTL
    // @group appearance
    // @visibility external
    // @see cellStyle
    //<
    showRTL:false,

    // Discussion on compound item / skinning for jsdoc. This is referred to by other JSDoc
    // entries but doesn't need to show up in the doc-tree.
    //> @groupDef CompoundFormItem_skinning
    // When skinning basic FormItems like SelectItem and TextItem, consider that compound form
    // items like DateItem and ComboBox reuse simpler items like SelectItem and TextItem, so adding
    // a border to SelectItem would also apply a border to each select item within DateItem.<br>
    // To avoid such side-effects, if you want to add styling to all SelectItems used in your
    // application, you can create an application-specific subclass like MySelectItem and apply
    // properties there.<br>
    // @visibility external
    //<

    //> @type FormItemBaseStyle
    // This string is the base CSS class name applied to a FormItem (or some part of a form item).
    // The style name will be modified as the 'state' of the form item changes. Specifically:<ul>
    // <li>If +link{FormItem.showFocused} is true, when the form item receives focus, this
    //     style will have the suffix "Focused" appended to it.</li>
    // <li>If +link{FormItem.showErrorStyle} is true, if the form item has errors, this
    //     style will have the suffix "Error" appended to it.</li>
    // <li>If +link{FormItem.showDisabled} is true, when the form item is disabled, this
    //     style will have the suffix "Disabled" appended to it.</li>
    // <li>Finally, if +link{FormItem.showRTL} is true, when the form item is in RTL mode, this
    //     style will have the suffix "RTL" appended to it.</ul>
    // So for example if the cellStyle for some form item is set to "formCell" and showFocused
    // is true, when the form item receives focus, the form item's cell will have the "formCellFocused"
    // style applied to it.
    // @visibility external
    // @group appearance
    //<

    //> @attr formItem.cellStyle  (FormItemBaseStyle : "formCell" : IRW)
    // CSS style applied to the form item as a whole, including the text element, any icons, and
    // any hint text for the item. Applied to the cell containing the form item.
    // <P>
    // NOTE: See the +link{group:CompoundFormItem_skinning} discussion for special skinning considerations.
    // @visibility external
    // @group appearance
    //<
    cellStyle:"formCell",

    //> @attr formItem.hintStyle      (string : "formHint" : IRW)
    // CSS class for the "hint" string.
    //
    // @see hint
    // @group appearance
    // @visibility external
    //<

    hintStyle:"formHint",

    //> @attr formItem.titleStyle (FormItemBaseStyle : "formTitle" : IRW)
    // Base CSS class name for a form item's title. Note that this is a +link{FormItemBaseStyle} so
    // will pick up stateful suffixes on focus, disabled state change etc. by default.
    // <p>
    // Note the appearance of the title is also affected by
    // +link{dynamicForm.titlePrefix}/+link{dynamicForm.titleSuffix,titleSuffix} and
    // +link{dynamicForm.requiredTitlePrefix}/+link{dynamicForm.requiredTitleSuffix,requiredTitleSuffix}.
    //
    // @visibility external
    // @see formItem.cellStyle
    //<
    titleStyle:"formTitle",

    //> @attr formItem.printTitleStyle (FormItemBaseStyle : null : IRW)
    // Base CSS stylename for a form item's title when generating print HTML for the item.
    // If unset +link{formItem.titleStyle} will be used instead.
    // @group printing
    // @visibility external
    //<

    //> @attr formItem.textBoxStyle (FormItemBaseStyle : null : IRW)
    // Base CSS class name for a form item's text box element.
    // <P>
    // NOTE: See the +link{group:CompoundFormItem_skinning} discussion for special skinning considerations.
    // @group appearance
    // @visibility external
    // @see formItem.cellStyle
    //<
    //textBoxStyle:null,

    //> @attr formItem.printTextBoxStyle (FormItemBaseStyle : null : IRW)
    // Base CSS class name for a form item's text box element when getting printable HTML for the
    // form. If unset +link{formItem.textBoxStyle} will be used instead. Note that focused styling
    // will never be displayed while printing, though error and disabled styling will.
    //
    // @group printing
    // @visibility external
    //<
    //printTextBoxStyle:null,

    //> @attr formItem.pickerIconStyle (FormItemBaseStyle : null : IRW)
    // Base CSS class name for a form item's picker icon cell. If unset, inherits from
    // this item's +link{controlStyle,controlStyle}.
    // @group pickerIcon
    // @group appearance
    // @see formItem.cellStyle
    // @visibility external
    //<
    //pickerIconStyle:null,

    //> @attr formItem.controlStyle (FormItemBaseStyle : null : IRW)
    // Base CSS class name for a form item's control box (surrounds text box and picker).
    // <P>
    // NOTE: See the +link{group:CompoundFormItem_skinning} discussion for special skinning considerations.
    // @group appearance
    // @visibility pickerIcon
    // @see formItem.cellStyle
    //<
    //controlStyle:null,

    //> @attr formItem.editPendingCSSText (string : "color:#0066CC;" : [IRWA])
    // Custom CSS text to be applied to cells with pending edits that have not yet been
    // submitted.
    // @visibility external
    // @group appearance
    //<
    editPendingCSSText:"color:#0066CC;",

    //> @attr formItem.showFocusedErrorState (Boolean : false : IRWA)
    // If set to true, when an item has errors and is focused, an "ErrorFocused" suffix
    // will appear on the stylename.
    //
    // @group appearance
    // @visibility external
    // @see formItem.cellStyle
    //<
    showFocusedErrorState:false,

    // -------------------------------
    // Deprecated styling properties:

    //> @attr formItem.cellClassName (CSSStyleName : "formCell" : IRW)
    // CSS class for a form item's cell in the form layout
    //
    // @group appearance
    // @visibility external
    // @deprecated As of SmartClient version 5.5, deprecated in favor of +link{formItem.cellStyle}
    //<

    //> @attr formItem.errorCellClassName (CSSStyleName : "formError" : IRW)
    // CSS class for a form item's cell when a validation error is showing.
    //
    // @group appearance
    // @visibility external
    // @deprecated As of SmartClient version 5.5 deprecated in favor of +link{formItem.cellStyle}
    //<

    //>    @attr formItem.titleClassName (CSSStyleName : "formTitle" : IRW)
    // CSS class for the form item's title.
    // @group title
    // @visibility external
    // @deprecated As of SmartClient Version 5.5, use +link{formItem.titleStyle} instead
    //<
    //titleClassName : "formTitle",

    //>    @attr formItem.titleErrorClassName (CSSStyleName : "formTitleError" : IRW)
    // CSS class for a form item's title when a validation error is showing.
    // @group title
    // @visibility external
    // @deprecated As of SmartClient Version 5.5, use +link{formItem.titleStyle} instead
    //<
    //titleErrorClassName : "formTitleError",

    //>    @attr formItem.hintClassName (CSSStyleName : "formHint" : IRW)
    // CSS class for the "hint" string.
    //
    // @see hint
    // @group appearance
    // @visibility external
    // @deprecated As of SmartClient version 5.5, deprecated in favor of +link{FormItem.hintStyle}
    //<

    // Internal flag designating whether this element type has a data element
    // (an actual HTML form element, holding a value).  Accessed via the 'hasDataElement()'
    // method.
    _hasDataElement:false,

    // Hovers
    // -----------------------------------------------------------------------------------------
    //> @attr formItem.hoverDelay (number : null : IRWA)
    // If specified, this is the number of milliseconds to wait between the user rolling over
    // this form item, and triggering any hover action for it.<br>
    // If not specified <code>this.form.itemHoverDelay</code> will be used instead.
    // @group Hovers
    // @visibility external
    //<
    //,hoverDelay:null

    //> @attr formItem.hoverWidth (measure : null : [IRW])
    // Option to specify a width for any hover shown for this item.
    // @see DynamicForm.itemHoverWidth
    // @group Hovers
    // @visibility external
    //<

    //> @attr FormItem.hoverHeight  (measure : null : [IRW])
    // Option to specify a height for any hover shown for this item.
    // @see DynamicForm.itemHoverHeight
    // @group Hovers
    // @visibility external
    //<

    //> @attr FormItem.hoverAlign (Alignment  : null : [IRW])
    // Text alignment  for text displayed in this item's hover canvas, if shown.
    // @see DynamicForm.itemHoverAlign
    // @group Hovers
    // @visibility external
    //<

    //> @attr FormItem.hoverVAlign (VerticalAlignment : null : [IRW])
    // Vertical text alignment  for text displayed in this item's hover canvas, if shown.
    // @see DynamicForm.itemHoverVAlign
    // @group Hovers
    // @visibility external
    //<

    //> @attr FormItem.hoverStyle (CSSStyleName  : null : [IRW])
    // Explicit CSS Style for any hover shown for this item.
    // @see DynamicForm.itemHoverStyle
    // @group Hovers
    // @visibility external
    //<

    //> @attr FormItem.hoverOpacity (number : null : [IRW])
    // Opacity for any hover shown for this item
    // @see DynamicForm.itemHoverOpacity
    // @group Hovers
    // @visibility external
    //<

    //> @attr FormItem.hoverRect (object : null : [IRWA])
    // Explicit placement information for any hover shown for this item.
    // Should be specified as an object of the form <br>
    // <code>{left:[value], top:[value], width:[value], height:[value]}</code>
    // @see DynamicForm.itemHoverRect
    // @group Hovers
    // @visibility internal
    //<


    //> @attr formItem.showClippedTitleOnHover (boolean : true : [IRW])
    // If true and the title is clipped, then a hover containing the full title of this item
    // is enabled.
    // <p>
    // <var class="smartclient">The +link{formItem.titleHover()} method is called before the
    // hover is displayed, allowing the hover to be canceled if desired. The HTML shown in the
    // hover can be customized by overriding +link{formItem.titleHoverHTML()}.</var>
    // <var class="smartgwt">A <code>TitleHoverEvent</code> is fired before the hover is displayed,
    // allowing the hover to be canceled if desired. The HTML shown in the hover can be customized
    // by setting a <code>FormItemHoverFormatter</code> on either this <code>FormItem</code>
    // or the <code>DynamicForm</code>. See <code>setItemTitleHoverFormatter()</code>.</var>
    // @group Hovers
    // @visibility external
    //<
    showClippedTitleOnHover:true,

    //> @attr FormItem.showClippedValueOnHover (Boolean : true : [IRW])
    // If true and the value is clipped, then a hover containing the full value of this item
    // is enabled.
    // <p>
    // <var class="smartclient">The +link{FormItem.valueHover()} method is called before the
    // hover is displayed, allowing the hover to be canceled if desired. The HTML shown in the
    // hover can be customized by overriding +link{FormItem.valueHoverHTML()}.</var>
    // <var class="smartgwt">A <code>ValueHoverEvent</code> is fired before the hover is displayed,
    // allowing the hover to be canceled if desired. The HTML shown in the hover can be customized
    // by setting a <code>FormItemHoverFormatter</code> on either this <code>FormItem</code>
    // or the <code>DynamicForm</code>. See <code>setItemValueHoverFormatter()</code>.</var>
    // @group Hovers
    // @visibility external
    //<
    showClippedValueOnHover:true

    // Criteria and Operators
    // -----------------------------------------------------------------------------------------

    //> @attr formItem.operator (OperatorId : null : IR)
    // +link{OperatorId} to be used when +link{dynamicForm.getValuesAsCriteria()} is called.
    // <P>
    // <code>item.operator</code> can be used to create a form that offers search functions such
    // as date range filtering, without the more advanced user interface of the +link{FilterBuilder}.
    // <P>
    // When <code>item.operator</code> is set for any FormItem in a form,
    // <code>form.getValuesAsCriteria()</code> will return an +link{AdvancedCriteria} object
    // instead of a normal +link{Criteria} object.  Each FormItem will produce one
    // +link{Criterion} affecting the DataSource field specified by +link{formItem.criteriaField}.
    // The criteria produced by the FormItems will be grouped under the logical operator
    // provided by +link{dynamicForm.operator}.
    // <P>
    // if <code>operator</code> is set for some fields but not others, the default operator is
    // "equals" for fields with a valueMap or an optionDataSource, and for fields of type "enum"
    // (or of a type that inherits from "enum").  The default operator for all other fields is
    // "iContains" (case-insensitive match)
    // <P>
    // <b>Note:</b> Advanced criteria will only be created for items in a Databound form. If this
    // item is part of a form with no +link{dataBoundComponent.dataSource,dataSource}, the
    // <code>operator</code> attribute will have no effect.
    //
    // @group criteriaEditing
    // @visibility external
    //<

    //> @attr formItem.criteriaField (identifier : null : IR)
    // When using +link{formItem.operator}, the name of the DataSource field for the
    // +link{Criterion} this FormItem generates.  If not specified, defaults to
    // +link{FormItem.name}.
    // <P>
    // Generally, because <code>criteriaField</code> defaults to <code>item.name</code>, you don't
    // need to specify it.  However, if more than one FormItem specifies criteria for the same
    // DataSource field, they will need unique values for +link{formItem.name} but should set
    // +link{formItem.criteriaField} to the name of DataSource field they both target.
    // <P>
    // For example, if two DateItems are used to provide a min and max date for a single field called
    // "joinDate", set +link{formItem.criteriaField} to "joinDate" on both fields but give the fields
    // distinct names (eg "minDate" and "maxDate") and use those names for any programmatic access,
    // such as +link{dynamicForm.setValue()}.
    //
    // @visibility external
    //<

    // -----------------------------------------------------------------------------------------

    //> @attr formItem.saveOnEnter (Boolean : null : IRW)
    // Set this to true to allow the parent form to save it's data when 'Enter' is pressed on
    // this formItem and +link{DynamicForm.saveOnEnter,saveOnEnter} is true on the parent form.
    // @visibility external
    //<
    //saveOnEnter:false,

});

isc.FormItem.addMethods({
    //>    @method    FormItem.init()    (A)
    //            initialize the formItem object
    //
    //        @param    [all arguments]    (object)    objects with properties to override from default
    //<
    _$height:"height", _$width:"width",
    _$colSpan:"colSpan", _$rowSpan:"rowSpan",
    init : function () {
        if (isc._traceMarkers) arguments.__this = this;

        // get a global ID so we can be called in the global scope
        // If getID() is called before this (typically only likely in an override of init),
        // we will already have a global ID - in this case avoid clobbering it.
        if (this.ID == null || window[this.ID] != this) {
            isc.ClassFactory.addGlobalID(this);
        }

        // if "options" was specified, switch to "valueMap"
        if (this.options && !this.valueMap) {
            this.valueMap = this.options;
            delete this.options;
        }

        // Make sure that any 'measure' properties are in the correct format

        this._convertRawToMeasure(this._$height);
        this._convertRawToMeasure(this._$width);
        this._convertRawToMeasure(this._$colSpan);
        this._convertRawToMeasure(this._$rowSpan);

        // Start with our default value
        // - we do this rather than calling 'this.setValue(this.getDefaultValue())' for a
        //   couple of reasons:
        //   a) At this point the form's "values" object may not be initialized
        //   b) In subclass overrides, such as the container item, setValue() makes use of
        //      properties that get set up after this (for them Superclass) init()
        //      implementation
        // - setValue() would also call form.saveItemValue() - it's ok to skip this at this
        //   stage as after the form item has been created this call would be made after form
        //   item creation via 'setValues()' for any items where 'shouldSaveValue' is true.
        // - setValue() would also call setElementValue() - ok to skip as our elements haven't
        //   been set up until draw().
        this._value = this.getDefaultValue();
        // Note that this is the default value.
        this._setToDefault = true;

        this._setUpIcons();

        // If any validators have stopOnError set, this form item must be marked
        // validateOnExit:true. SynchronousValidation is also enabled.

        if ((!this.validateOnExit || !this.synchronousValidation) &&
            this.validators && this.validators.length > 0)
        {
            for (var i = 0; i < this.validators.length; i++) {
                if (this.validators[i].stopOnError) {
                    this.validateOnExit = true;
                    this.synchronousValidation = true;
                    break;
                }
            }
        }
        // If any form or form item has stopOnError set, this form item must be marked
        // validateOnExit:true. SynchronousValidation is also enabled.
        if ((!this.validateOnExit || !this.synchronousValidation) &&
            ((this.stopOnError == null && this.form && this.form.stopOnError) || this.stopOnError))
        {
            this.validateOnExit = true;
            this.synchronousValidation = true;
        }

        this.onInit(this);
    },

    // onInit() - notification method fired on initialization

    onInit:function (item) {
    },

    isRTL : function () {
        return this.containerWidget == null ? isc.Page.isRTL() : this.containerWidget.isRTL();
    },


    _$star:"*",
    _convertRawToMeasure : function (property) {
        var value = this[property];
        if (value == null || isc.isA.Number(value) || value == this._$star) return value;
        var numericVal = parseInt(value);
        if (numericVal == value) {
            this[property] = numericVal;
            return value;
        }
        return value;
    },

    destroy : function (a,b,c,d,e) {


        if (isc.FormItem._pendingEditorExitCheck == this) {
            isc.FormItem._pendingEditorExitCheck.checkForEditorExit(true, true);
        }

        this.invalidateDisplayValueCache(true);

        if (this.isDrawn()) this.cleared();

        // If this is a form item that shows a unique pickList, destroy it too
        var pickList = this.pickList;
        this.pickList = null;
        if (pickList != null) {
            if (pickList.formItem == this) delete pickList.formItem;
            if (pickList.isVisible()) pickList.hide();
            if (!this.reusePickList()) pickList.destroy();
            else if (pickList.body) pickList.body._reused = true;
        }

        this.destroyed = true;
        this.form = null;
        this._dataElement = null;
        var undef;

        isc.ClassFactory.dereferenceGlobalID(this);
        this._releaseDOMIDs();

        // NOTE: we assume picker recycling as a default

        if (isc.EH._focusTarget == this) isc.EH._focusTarget = null;
        this.invokeSuper(isc.FormItem, "destroy", a,b,c,d,e);
    },

    clear : function () {
        if (this.picker) this.picker.clear();
    },

    // Override toString() so form items show their names rather than their IDs by default
    toString : function () {
        var name = this.getFieldName(),
            ID = this.ID,

            string = "[" + this.Class + " instance " +
                        (name != null ? " name ='" + name + "', " : "") +
                        "global ID=" + ID + "]";
        return string;

    },

    getDataSource : function () {
        if (isc.isA.String(this.dataSource)) return isc.DS.get(this.dataSource);
        return this.dataSource;
    },


    registerWithDataView : function (dataView) {
        if (!this.inputDataPath) return;

        if (!dataView) {
            dataView = this.form;
            while (dataView && !isc.isA.DataView(dataView)) dataView = dataView.parentElement;
        }

        if (!dataView) {
            this.logWarn("Component initialized with an inputDataPath property, but no DataView " +
                         "was found in the parent hierarchy. inputDataPath is only applicable to " +
                         "DataBoundComponents and FormItems being managed by a DataView");
            return;
        }

        dataView.registerItem(this);
    },


    // IDs and names
    // --------------------------------------------------------------------------------------------

    //>    @method    formItem.getFieldName()    (A)
    //            Return the name for the this formItem.
    //        @group    drawing
    //
    //        @return    (string)    name for this form item
    // @visibility external
    //<
    getFieldName : function () {
        return this.name;
    },

    //>    @method    formItem.getDataPath() (A)
    // Return the dataPath for the this formItem.
    // @return (dataPath) dataPath for this form item
    // @visibility external
    //<
    getDataPath : function () {
        return this.dataPath;
    },

    // returns this.datapath, but if the path is absolute and includes
    // this form's dataPath, trims that off (so it's the datapath relative to the containing form)
    getTrimmedDataPath : function () {
        var dp = this.getDataPath();
        if (dp && this.form && this.form.dataPath) {
            dp = this.form._trimDataPath(dp);
        }
        if (dp && dp.endsWith("/")) dp = dp.substring(0, dp.length-1);
        return dp;
    },

    //>    @method    formItem.getFullDataPath() (A)
    // Return the fully-qualified dataPath for the this formItem (ie, the dataPath expressed
    // in absolute terms from the root of the hierarchy, rather than relative to the item's
    // parent form).  Note that the item's name is substituted into the full dataPath if the
    // item does not specify an explicit dataPath.  For example, if we have a field called
    // <code>name</code> that specifies no dataPath, on a form that specifies a dataPath of
    // <code>/order/items</code>, this method will return <code>/order/items/name</code>
    // @return (DataPath) Fully-qualified dataPath for this form item
    // @visibility external
    //<
    getFullDataPath : function () {
        var localDP =  this.getDataPath() || this.getFieldName();
        if (!localDP) {
            if (this.shouldSaveValue) {
                this.logWarn("Encountered field with neither name nor dataPath: " +
                                this.echo(this));
            }
            localDP = "";
        }
        // convert numbers - historically it was allowed to have field names that are numbers...
        if (!isc.isA.String(localDP)) localDP = localDP+"";
        if (localDP.startsWith(isc.Canvas._$slash)) return localDP;
        var parentDP = this.form.getFullDataPath();
        if (parentDP && parentDP != isc.Canvas._$slash) {
            return parentDP + isc.Canvas._$slash + localDP;
        }
        return localDP;
    },

    //>    @method    formItem.shouldSaveOnEnter() (A)
    // Returns true if 'Enter' key presses in this formItem should allow a saveOnEnter: true
    // parent form to save it's data.  The default implementation returns the value of
    // +link{FormItem.saveOnEnter} or false if that property is unset.
    // @return (Boolean) boolean indicating whether saving should be allowed to proceed
    // @visibility external
    //<
    shouldSaveOnEnter : function () {
        var result = this.saveOnEnter != null ? this.saveOnEnter : false;
        return result;
    },


    //>    @method    formItem.getItemName()    (A)
    //            Return the name for the this formItem.  Synonym for getFieldName()
    //        @group    drawing
    //
    //        @return    (string)    name for this form item
    // @visibility internal
    //<
    getItemName : function () {
        return this.getFieldName();
    },


    //>    @method    formItem.getElementName()    (A)
    //            Return the name to be written into this form item's HTML element.
    //          This will not necessarily match the value returned by this.getFieldName().
    //        @group    drawing
    //
    //        @return    (string)    name of the form element
    //<

    _$underscore:"_",
    _$value:"value",
    getElementName : function () {


        if (this.isInactiveHTML()) return "";
        var name = this.getFieldName();

        // if this item has a parentItem, prepend the name of the parentItem
        // This is just for uniqueness - we will not be trying to parse this string to determine
        // what an items' parent element is
        if (this.parentItem) {
            var masterName = this.parentItem.getElementName();
            if (name == isc.emptyString) name = masterName;
            else name = [masterName, this._$underscore, name].join(isc.emptyString);
        }

        // If we still don't have a name, or the name matches the ID for the item,
        // return a unique 'name' string for this item.
        // - Note: we can't actually use the ID of the item, because when we write handlers
        //   into the form items, we want to be able to refer to the item by it's ID. Because
        //   the handlers are executed in the scope of the native form object in the
        //   document.forms array, if the native name of the Form Element matches the ID of
        //   the item, the ID would give us a pointer to the Form Element rather than the item.
        if (name == null || name == this.getID() || name == isc.emptyString) {
            name = this._getDOMID(this._$value);
        }

        return name;
    },

    //>    @method    formItem.getDataElementId()    (A)
    // Return the ID for this form item's data element.
    //        @group    drawing
    //        @return    (string)    name of the form element
    // @visibility testAutomation
    //<

    _$dataElement:"dataElement",
    getDataElementId : function () {
        // inactiveHTML depends on context so we can't simply cache...
        if (this.isInactiveHTML()) return this._getDOMID(this._$dataElement);

        if (this.__tagId == null) {
            this.__tagId = this._getDOMID(this._$dataElement, true);
        }
        // This will be a unique ID that can be written into the element tag.
        // It's used for getting a reference to the form item element to be used by the
        // "FOR" property written into the label for the form item.
        // Doesn't necessarily reflect anything about the information carried by the form item.
        return this.__tagId;

    },

    //>    @method    formItem.getItemID()    (A)
    //        Return the unique global ID for this form item instance.
    //      The item is available in the global scope via this ID as window[itemID].
    //      Synonym for getID().
    //        @group    drawing
    //
    //        @return    (string)    ID of the form item.
    //<
    getItemID : function () {
        return this.getID();
    },

    //>    @method    formItem.getID()    (A)
    //        Return the unique global ID for this form item instance.
    //      The item is available in the global scope via this ID as window[itemID].
    //        @group    drawing
    //
    //        @return    (string)    ID of the item.
    //<
    getID : function () {

        if (this.ID == null) {
            isc.ClassFactory.addGlobalID(this);
        }
        return this.ID;
    },



    // Titles
    // --------------------------------------------------------------------------------------------

       //>    @method    formItem.shouldShowTitle()    (A)
    //    Draw a cell for this item title?
    //        @group    drawing
    //
    //        @return    (boolean)    true if item title cell should be drawn
    //<
    shouldShowTitle : function () {
        return this.showTitle;
    },

    _$label:"label",
    _getLabelElementID : function () {
        return this._getDOMID(this._$label);
    },

    //>    @method    formItem.getTitleHTML()    (A)
    //    Return the HTML for the title of this formItem
    //        @group    drawing
    //
    //        @return    (HTMLString)    title for the formItem
    //<
    _$forEquals: " for='",
    _$accesskeyEquals: " accesskey='",
    _$titleHTMLTemplate: [
        "<label id='",      // [0]
        ,                   // [1] this._getLabelElementID()
        "'",                // [2]

        // FOR="" attribute
        ,                   // [3] this._$forEquals or null
        ,                   // [4] this.getDataElementId() or null
        ,                   // [5] isc.apos or null

        // ACCESSKEY="" attribute
        ,                   // [6] this._$accesskeyEquals or null
        ,                   // [7] this.accessKey or null
        ,                   // [8] isc.apos or null

        ">",                // [9]
        ,                   // [10] title
        "</label>"          // [11]
    ],
    getTitleHTML : function () {
        var template = this._$titleHTMLTemplate,
            title = this.getTitle(),
            focusElementId = null,
            accessKey = null;
        if (this._canFocus()) {
            accessKey = this.accessKey;
            if (accessKey != null) {
                // underline the accessKey char within the title
                title = isc.Canvas.hiliteCharacter(title, accessKey);
            }

            // Note: the <LABEL> tag allows us to set an accessKey on the element without writing it
            // directly into the element's HTML.  It also improves on (for example) screen reader
            // support. It also means clicking the title will put focus into the target form item.

            if (this.hasDataElement()) focusElementId = this.getDataElementId();


        }

        template[1] = this._getLabelElementID();

        if (focusElementId != null) {
            template[3] = this._$forEquals
            template[4] = focusElementId;
            template[5] = isc.apos;
        } else {
            template[5] = template[4] = template[3] = null;
        }

        if (accessKey != null) {
            template[6] = this._$accesskeyEquals
            template[7] = accessKey;
            template[8] = isc.apos;
        } else {
            template[8] = template[7] = template[6] = null;
        }

        template[10] = title;

        return template.join(isc._emptyString);
    },

    //>    @method    formItem.getTitle()    (A)
    //    Return the title of this formItem
    //        @group    drawing
    //
    //        @return    (HTML)    title for the formItem
    // @visibility external
    //<
    getTitle : function () {
        var undef;
        if (!this.form) return;
        // allow a developer to actually specify a null title, but showTitle true as an obvious
        // way to leave alignment all the same but not show annoying ":" next to the title cell.
        if (this[this.form.titleField] !== undef) return this[this.form.titleField];
        return this[this.form.fieldIdProperty];
    },

    // Defer to DF to pick up the form's default titleOrientation
    getTitleOrientation : function () { return this.form.getTitleOrientation(this); },

    // Layout
    // --------------------------------------------------------------------------------------------

    //> @method formItem.isVisible()    ()
    // Return true if the form item is currently visible. Note that like the similar
    // +link{canvas.isVisible(),Canvas API}, it indicates visibility settings only and so
    // will return true for an item that is not drawn.
    //
    //  @group  visibility
    //  @return (Boolean)   true if the form item is visible
    // @visibility external
    //<
    isVisible : function () {
        if (!this.containerWidget.isVisible()) return false;

        // If we have a showIf(), which evaluated to false, we will have been marked as
        // visibility false (done in DynamicForm.getInnerHTML()).
        if (this.visible == false) return false;
        // If we're a child of a container item, check whether the container item has been
        // marked as not-visible.
        if (this.parentItem && !this.parentItem.isVisible()) return false;
        return true;
    },

       //>    @method    formItem.getRowSpan()    (A)
    // Return the rowSpan for this item
    //        @group    formLayout
    //
    //        @return    (number)    rowSpan
    //<
    getRowSpan: function () {
        return this.rowSpan;
    },

       //>    @method    formItem.getColSpan()    (A)
    // Return the colSpan for this item
    //        @group    formLayout
    //
    //        @return    (number)    colSpan
    //<
    getColSpan : function () {
        // disallow colSpan of zero
        if (this.colSpan == 0) this.colSpan = 1;
        return this.colSpan;
    },

    //> @method formItem.getTitleColSpan() (A)
    // Return the titleColSpan for this item
    // @group formLayout
    //
    // @return (number) titleColSpan
    //<
    getTitleColSpan : function () {
        // disallow titleColSpan of zero
        if (this.titleColSpan == 0) this.titleColSpan = 1;
        return this.titleColSpan;
    },

    //> @method formItem.isStartRow()   (A)
    // Should this item be drawn on a new row?
    //      @group tableLayout
    //      @return (boolean) true if a new row should start for this item
    //<
    isStartRow : function () {
        return this.startRow
    },

    //> @method formItem.isEndRow()   (A)
    // Should this be the last item on a row?
    //      @group tableLayout
    //      @return (boolean) true if a new row should start after this item
    //<
    isEndRow : function () {
        return this.endRow
    },

    //>    @method    formItem.getRect()
    // Return the coordinates of this object as a 4 element array.
    //        @group    positioning, sizing
    //
    //        @return    (array)        [left, top, width, height]
    // @visibility external
    //<
    getRect : function () {
        return [this.getLeft(), this.getTop(), this.getVisibleWidth(), this.getVisibleHeight()];
    },

    //>    @method    formItem.getPageRect()
    // Return the page-level coordinates of this object as a 4 element array.
    //        @group    positioning, sizing
    //
    //        @return    (array)        [left, top, width, height]
    // @visibility external
    //<
    getPageRect : function (includeTitle) {
        if (includeTitle) return this.getPageRectIncludingTitle();
        return [this.getPageLeft(), this.getPageTop(),
                this.getVisibleWidth(), this.getVisibleHeight()];
    },

    getPeerRect : function () {
        return this.getPageRect();
    },

    getPageRectIncludingTitle : function () {
        var left = this.getPageLeft(),
            top = this.getPageTop(),
            width = this.getVisibleWidth(),
            height = this.getVisibleHeight();

        if (this.showTitle) {
            var titleLeft = this.getTitlePageLeft(),
                titleTop = this.getTitlePageTop(),
                titleWidth = this.getVisibleTitleWidth(),
                titleHeight = this.form.getTitleHeight(this);;
            if (this.titleOrientation == "left" || this.titleOrientation == "left" ||
                this.titleOrientation == null)
            {
                left = left < titleLeft ? left : titleLeft;
                width += titleWidth;
            } else {
                left = left < titleLeft ? left : titleLeft;
                width = width > titleWidth ? width : titleWidth;
                if (isc.isA.Number(titleHeight)) height += titleHeight;
            }
        }
        return [left, top, width, height];
    },


    getCellHeight : function (reportOverflowedSize) {
        if (isc._traceMarkers) arguments.__this = this;

        if (this.cellHeight != null) {
            return this.cellHeight;
        }

        var height = this.getHeight(reportOverflowedSize);
        if (!isc.isA.Number(height)) return height;

        // never report a height lower than that required by our visible icons
        // (these are our external icons - not our picker icon)
        var iconsHeight = this.getIconsHeight();
        if (height < iconsHeight) {
            height = iconsHeight;
        }

        // If we are showing a picker icon, and it has a specified height, that may also cause
        // our height to be larger than expected.
        // If no specified height, sized to fit in available space, so won't expand the item.
        if (this._shouldShowPickerIcon() && this.pickerIconHeight) {
            var pickerIconHeight = this.pickerIconHeight + this._getPickerIconVPad();
            if (pickerIconHeight > height) height = pickerIconHeight;
        }

        var form = this.containerWidget;
        if (this._absPos() || !isc.isA.DynamicForm(form)) return height;

        height += this._getCellVBorderPadSpacing();

        // If titleOrientation is TOP, and we're showing a title, add our title height to our
        // reported cellHeight, so tableLayoutPolicy code will take it into account

        if (this.showTitle && this.form.getTitleOrientation(this) == isc.Canvas.TOP) {
            height += this.form.getTitleHeight(this);
        }
        return height;
    },
    // Forms only write a height into the cell containing the form item if shouldFixRowHeight
    // is true.
    // If we have an explicit cellHeight specified, consider the height of this item "fixed"!
    // If we have an explicit height but are not applying it to the text box, also apply
    // the height to the cell.
    shouldFixRowHeight : function () {
        return this.cellHeight != null ||
            (!this.shouldApplyHeightToTextBox() && this.getHeight() != null);
    },

    // Returns the space taken up around this form item by the cell (determined from
    // cellSpacing, border and padding).
    _getCellVBorderPadSpacing : function () {

        var height = 0,
            form = this.form,
            cellStyle = this.getCellStyle();

        // For items written into containerIems, cellSpacing/padding will be defined on the
        // parent item, not the form.
        if (this.parentItem) form = this.parentItem;

        // Spacing around cells (above and below)
        height += 2*form.cellSpacing;


        var cellPadding = isc.isA.Number(form.cellPadding) ? form.cellPadding : 0,
            paddingTop = isc.Element._getTopPadding(cellStyle, true);
        if (paddingTop == null) paddingTop = cellPadding

        var paddingBottom = isc.Element._getBottomPadding(cellStyle, true);
        if (paddingBottom == null) paddingBottom = cellPadding;

        height += paddingTop;
        height += paddingBottom;
        height += isc.Element._getVBorderSize(cellStyle);

        return height;
    },
    _getCellHBorderPadSpacing : function () {

        var height = 0,
            form = this.form,
            cellStyle = this.getCellStyle();

        // For items written into containerIems, cellSpacing/padding will be defined on the
        // parent item, not the form.
        if (this.parentItem) form = this.parentItem;

        // Spacing around cells (above and below)
        if (isc.isA.Number(form.cellSpacing)) height += 2*form.cellSpacing;


        // We have seen a case where a developer set form.cellPadding to a string
        // ("2" rather than 2), which led to us assembling an inappropriate string like "0220"
        // in this method. If cellPadding is specified as a string just ignore it.
        var formCellPadding = isc.isA.Number(form.cellPadding) ? form.cellPadding : 0,
            paddingLeft = isc.Element._getLeftPadding(cellStyle, true);
        if (paddingLeft == null) paddingLeft = formCellPadding;

        var paddingRight = isc.Element._getRightPadding(cellStyle, true);
        if (paddingRight == null) paddingRight = formCellPadding;

        height += paddingLeft;
        height += paddingRight;
        height += isc.Element._getHBorderSize(cellStyle);

        return height;
    },

    //>@method  FormItem.getInnerHeight()
    // Returns the available space for content of this FormItem, based on the specified
    // height for the item, and any styling.
    // @return (number) height in px.
    //<
    // This method returns the space within the cell derived either from item.cellHeight
    // (less padding etc), or item.height.
    // This means if you have both item.height and item.cellHeight specified, the
    // cellHeight is used.
    // Implementation note: We actually look at item._size in this method - that's set
    // up by logic at the DynamicForm level which feeds the items into the
    // TableResizePolicy which in turn calls item.getCellHeight() to figure out
    // the heights.


    getInnerHeight : function () {
        var form = this.containerWidget;

        if (this._absPos()) return this._getPercentCoord(this.height, true);

        // If we've never run through stretch-resize-policy, this.height/width may be
        // specified as a string.
        // If we're being written out as standalone item HTML in a non-form containerWidget,
        // give that widget a chance to size the item (resolving "*" etc sizes)

        if (this._size == null && this.height != null && isc.isA.String(this.height) &&
            this.containerWidget && !isc.isA.DynamicForm(this.containerWidget) &&
            this.containerWidget.sizeFormItem != null)
        {
            this.containerWidget.sizeFormItem(this);
        }


        if (this._size) {
            var height = this._size[1];
            if (!isc.isA.Number(height)) return height;


            if (this._writtenIntoCell()) {
                height -= this._getCellVBorderPadSpacing();
            }
            return height;
        }
        return this.getHeight();
    },


    getInnerWidth : function (adjustForIcons) {
        var form = this.containerWidget;

        if (this._absPos()) return this._getPercentCoord(this.width);

        // If we've never run through stretch-resize-policy, this.height/width may be
        // specified as a string.
        // If we're being written out as standalone item HTML in a non-form containerWidget,
        // give that widget a chance to size the item (resolving "*" etc sizes)

        if (this._size == null && this.width != null && isc.isA.String(this.width) &&
            this.containerWidget && !isc.isA.DynamicForm(this.containerWidget) &&
            this.containerWidget.sizeFormItem != null)
        {
            this.containerWidget.sizeFormItem(this);
        }

        var width = this._size ? this._size[0] : this.width;

        // happens if StretchResize hasn't been run and size is specified as eg "*".  In this
        // case the FormItem may not handle the size in string form anyway, but we shouldn't
        // try to do math on it.
        if (!isc.isA.Number(width)) {
            return width;
        }
        // _size refers to the total area taken up by this items cell - to get the innerWidth
        // (available for the item and it's icons), deduct the border / padding / spacing
        // of the cell)

        if (this._writtenIntoCell()) {
            width -= this._getCellHBorderPadSpacing();
        }
        return width;
    },

    // getColWidth()
    // If this item is being written into a standard dynamic form cell, determine the width for
    // the column this item is written into.

    getColWidth : function () {
        var items = this.form ? this.form.items : null;
        if (items && items._colWidths != null && this._tablePlacement != null) {
            // this._tablePlacement stored as [startCol, startRow, endCol, endRow]
            var startCol = this._tablePlacement[0],
                endCol = this._tablePlacement[2];
            if (this.showTitle) {
                var titleOrientation = this.getTitleOrientation();
                if (titleOrientation == isc.Canvas.LEFT) startCol += 1;
                else if (titleOrientation == isc.Canvas.RIGHT) endCol -= 1;
            }
            var width = 0;
            for (var c = startCol; c < endCol; c++) {
                width += items._colWidths[c];
            }
            return width;
        }
        return null;
    },



    _absPos : function () {
        return (this.containerWidget._absPos && this.containerWidget._absPos());
    },

    _writtenIntoCell : function () {
        return (this.containerItem != null ||
                (this.form == this.containerWidget && !this._absPos()));
    },
    // percent coordinate interpretation for absPos forms
    _$percent:"%",
    _getPercentCoord : function (coord, vertical) {
        // decided against since this re-interprets the default size of "*" for many items
        //if (coord == "*") coord = "100%";
        if (isc.isA.String(coord) && isc.endsWith(coord, this._$percent)) {
            var parent = this.containerWidget,
                parentSize = vertical ? parent.getInnerHeight() : parent.getInnerWidth();
            return Math.round((parseInt(coord, 10) / 100) * parentSize);
        }
        return coord;
    },


    getElementWidth : function () {
        var width = this.getInnerWidth();

        if (!isc.isA.Number(width)) return null;
        width -= this.getTotalIconsWidth();

        return (isc.isA.Number(width) ? Math.max(width, 1) : null);
    },


    // getTextBoxWidth() / height()
    // returns the size of the text box (used for writing out HTML - not retrieved by looking at
    // the DOM element in question).


    getTextBoxWidth : function (value) {
        var basicWidth = this.getElementWidth();
        if (!isc.isA.Number(basicWidth)) return basicWidth;

        if (this.textBoxStyle) {
            var className = this.getTextBoxStyle();
            basicWidth -= (isc.Element._getLeftMargin(className) + isc.Element._getRightMargin(className));
            if (this._sizeTextBoxAsContentBox()) {
                basicWidth -= isc.Element._getHBorderPad(className);
            }
        }
        if (this._shouldShowPickerIcon()) {
            basicWidth -= this.getPickerIconWidth();
            var iconProps = this.getPickerIcon();
            if (iconProps.hspace != null) basicWidth -= iconProps.hspace;
            if (this.pickerIconStyle)
                basicWidth -= isc.Element._getHBorderPad(this.getPickerIconStyle());
            if (this.controlStyle)
                basicWidth -= isc.Element._getHBorderPad(this.getControlStyle());
        }


        if (this.hasDataElement() && this._getValueIcon(value)) {
            basicWidth -= ((this.getValueIconWidth() || 0) +
                                    (this.valueIconLeftPadding + this.valueIconRightPadding));
        }

        // reduce by error width for left or right-oriented errors
        return basicWidth - this._getErrorWidthAdjustment();
    },

    // anticipated width of the error message, if we are showing errors on the left or right
    getErrorWidth : function () {
        // If we are showing errors on the left/right we should adjust the textBox size to account
        // for them. We don't know how long the error strings will be and it's ok for them to wrap
        // so make the default space we leave for them configurable at the item level
        var errorWidth = 0;
        if (this.form.showInlineErrors && this.hasErrors()) {
            var orientation = this.getErrorOrientation();
            if (orientation == isc.Canvas.LEFT || orientation == isc.Canvas.RIGHT) {
                if (this.shouldShowErrorText()) {
                    errorWidth += this.errorMessageWidth;
                } else if (this.shouldShowErrorIcon()) {

                    errorWidth += this.errorIconWidth + this.iconHSpace;
                }
            }
        }
        return errorWidth;
    },

    // _getErrorWidthAdjustment
    // If we're showing horizontal-orientated error text/icon - how much do we need to reduce
    // the text box's rendered size by to leave space for the error text
    _getErrorWidthAdjustment : function () {
        var errorWidth = this.getErrorWidth();
        if (errorWidth != 0 && this.expandHintAndErrors && (this.getColWidth() != null)) {
            var additionalColSpace = this.getColWidth() - this.getInnerWidth();
            if (additionalColSpace > 0) errorWidth -= additionalColSpace;
            // don't allow the value to go negative
            if (errorWidth < 0) errorWidth = 0;
        }
        return errorWidth;
    },




    //> @attr formItem.errorMessageWidth (int : 80 : IRW)
    // When +link{dynamicForm.showInlineErrors} and +link{showErrorText} are both true and
    // +link{errorOrientation} is "left" or "right", errorMessageWidth is the amount to reduce
    // the width of the editor to accommodate the error message and icon.
    // @group validation
    // @visibility external
    //<
    errorMessageWidth:80,

    // Helpers to avoid code duplication
    getValueIconHeight : function () {
        var height = this.valueIconHeight;
        if (height == null) height = this.valueIconSize;
        return height;
    },

    getValueIconWidth : function () {
        var width = this.valueIconWidth;
        if (width == null) width = this.valueIconSize;
        return width;
    },

    //> @attr formItem.applyHeightToTextBox (Boolean : null : IRA)
    // If +link{formItem.height} is specified, should it be applied to the
    // item's text box element?
    // <P>
    // If unset, behavior is determined as described in +link{shouldApplyHeightToTextBox()}
    // @visibility external
    //<

    //> @method formItem.shouldApplyHeightToTextBox() [A]
    // If +link{formItem.height} is specified, should it be applied to the
    // item's text box element? If this method returns false, the
    // text box will not have an explicit height applied, though the containing cell
    // will be sized to accomodiate any specified height.
    // <P>
    // This is used in cases where the text box does not have distinctive styling
    // (for example in standard +link{StaticTextItem}s). As the textBox has no explicit
    // height, it fits the content. Since the text box is not visually distinct to
    // the user, this makes +link{formItem.vAlign} behave as expected with the
    // text value of the item being vertically aligned within the cell.
    // <P>
    // Default implementation will return +link{applyHeightToTextBox} if explicitly set
    // otherwise <code>false</code> if +link{readOnlyDisplay} is set to
    // <code>"static"</code> and the item is +link{getCanEdit(),not editable}, otherwise
    // true.
    // @return (boolean) true if the height should be written into the items' text box.
    // @visibility external
    //<
    shouldApplyHeightToTextBox : function () {
        if (this.applyHeightToTextBox != null) return this.applyHeightToTextBox;
        // xxx paul
        if (this.renderAsStatic()) return false;
        return true;
    },

    getTextBoxHeight : function () {

        if (!this.shouldApplyHeightToTextBox()) return null;


        var basicHeight = this.getHeight();
        if (!isc.isA.Number(basicHeight)) {
            var innerHeight = this.getInnerHeight();
            if (this.cellHeight != null && isc.isA.String(basicHeight) &&
                basicHeight.endsWith("%"))
            {
                var percentHeight = parseInt(basicHeight);
                if (isc.isA.Number(innerHeight)) {
                    basicHeight = Math.round(innerHeight * (percentHeight/100));
                } else {
                    basicHeight = innerHeight;
                }
            } else {
                basicHeight = innerHeight;
            }
        }
        if (!isc.isA.Number(basicHeight)) return basicHeight;

        // If we're showing a valueIcon, adjust the textBox height to accommodate it if necessary
        if (this.valueIcons != null || this.getValueIcon != null) {
            var valueIconHeight = this.getValueIconHeight();
            if (valueIconHeight > basicHeight) basicHeight = valueIconHeight;
        }


        if (this.textBoxStyle) {
            var className = this.getTextBoxStyle();

            basicHeight -= (isc.Element._getTopMargin(className) +
                            isc.Element._getBottomMargin(className));
            if (this._sizeTextBoxAsContentBox()) {
                basicHeight -= isc.Element._getVBorderPad(className);
            }
        }
        // If we're writing out a control box we also have to adjust the height for the control
        // box's styling
        if (this._shouldShowPickerIcon() && this.controlStyle) {
            basicHeight -= isc.Element._getVBorderPad(this.getControlStyle());
        }


        if (this.showTitle && this.form.getTitleOrientation(this) == isc.Canvas.TOP &&
            !isc.isA.Number(this.getCellHeight()))
        {
            basicHeight -= this.form.getTitleHeight(this);
        }

        return basicHeight;
    },


    _sizeTextBoxAsContentBox : function () {
        return !isc.Browser.isBorderBox;
    },


    // getTextPickerIconWidth() / height()
    // returns the size of the picker icon's cell (used for writing out HTML - not retrieved by looking at
    // the DOM element in question).
    getPickerIconWidth : function () {
        return (this.pickerIconWidth != null ? this.pickerIconWidth : this.getPickerIconHeight());
    },

    getPickerIconHeight : function () {
        if (this.pickerIconHeight != null) return this.pickerIconHeight;
        else {
            var height = (isc.isA.Number(this.getHeight()) ? this.getHeight() : this.getInnerHeight());
            if (!isc.isA.Number(height)) return null;

            height -= this._getPickerIconVPad();

            this.pickerIconHeight = height;
            return height;
        }
    },

    // Vertical padding between the picker icon and the outer table
    _getPickerIconVPad : function () {

        var pad = 0;
        if (this.controlStyle){
            pad += isc.Element._getVBorderPad(this.controlStyle);
        }
        if (this.pickerIconStyle) {
            pad += isc.Element._getVBorderPad(this.pickerIconStyle);
        }
        return pad;
    },

    //>    @method    formItem.getHeight()    (A)
    //    Output the height for this element
    //        @group    sizing
    //        @return    (number)    height of the form element
    //<
    getHeight : function () {
        return this.height;
    },


    //>    @method    formItem.getVisibleHeight()    (A)
    //    Output the drawn height for this item in pixels.
    //  Note: this is only reliable after this item has been written out into the DOM.
    //        @group    sizing
    //        @return    (integer)    height of the form item
    // @visibility external
    //<
    // this returns the height of the outer table for the item
    getVisibleHeight : function () {
        var element = this.isDrawn() ? this.getOuterElement() : null;
        if (element == null) {
            this.logInfo("getVisibleHeight() - unable to determine drawn height for this item -" +
                         " returning pixel height from specified height", "sizing");
            if (isc.isA.Number(this.height)) {
                return this.height;
            }

            this.logWarn("getVisibleHeight() unable to determine height - returning zero",
                         "sizing");
            return 0;
        }

        return element.offsetHeight;
    },

    //>    @method    formItem.getIconHeight()    (A)
    //    Takes an icon definition object, and returns the height for that icon in px.
    //        @group    sizing
    //      @param  icon (object)   icon definition object for this item.
    //        @return    (number)    height of the form item icon in px
    //      @visibility external
    //<
    getIconHeight : function (icon) {
        // default to the first icon, if possible
        if (icon == null && this.icons != null && this.icons.getLength() > 0) icon = this.icons[0];
        else if (!this._isValidIcon(icon)) {
            this.logWarn("getIconHeight() passed invalid icon:" + isc.Log.echoAll(icon));
            return null;
        }

        // Note: we could actually look at the icon element in the DOM, (if it's drawn)
        // but we have full control over the HTML written into form item icons, so this value
        // should always match the specified size for the icon.
        return (icon.height != null ? icon.height : this.iconHeight);

    },

    getTitleVisibleHeight : function () {
        var titleElement = this.isDrawn() && this.form
                                          ? isc.Element.get(this.form._getTitleCellID(this))
                                          : null;
        if (titleElement == null) {
            var warning = "getTitleHeight() Unable to determine position for " +
                          (this.name == null ? "this item " : this.name) + ". ";
            if (this.isDrawn()) {
                warning += "This method is not supported by items of type " + this.getClass();
            } else {
                warning += "Position cannot be determined before the element is drawn"
            }
            warning += " - returning zero.";

            this.form.logWarn(warning);
            return 0;
        }
        return isc.Element.getVisibleHeight(titleElement);
    },


    //>    @method    formItem.getWidth()    (A)
    //    Output the width for this element. Note this returns the specified width for the
    //  element, which may be "*" or a percentage value. Use 'getVisibleWidth()' to get the
    //  drawn width in pixels.
    //        @group    sizing
    //        @return    (number)    width of the form element
    // @visibility external
    //<
    getWidth : function () {
        return this.width
    },

    //>    @method    formItem.getVisibleWidth()    (A)
    //    Output the drawn width for this item in pixels. This method is only reliable after
    //  the item has been drawn into the page.
    //        @group    sizing
    //        @return    (integer)    width of the form item
    // @visibility external
    //<

    getVisibleWidth : function () {
        var element = this.isDrawn() ? this.getOuterElement() : null;
        if (element == null) {
            this.logInfo("getVisibleWidth() - unable to determine drawn width for this item -" +
                         " returning pixel width from specified width", "sizing");
            if (isc.isA.Number(this.width)) {
                return this.width;
            // HACK: stretchResizePolicy is run on the form when writing out items into the DOM
            // this will resolve non numeric (* and percentage) sizes to pixel widths, and store
            // the specified column sizes on the items object as _colWidths.  If this is present
            // return the appropriate numeric value.
            } else if (this.form && this.form.items._colWidths != null) {

                return this.form.items._colWidths[this.form.getItems().indexOf(this)];
            }

            this.logWarn("getVisibleWidth() unable to determine width - returning zero",
                         "sizing");
            return 0;
        }

        return element.offsetWidth;

    },

    getVisibleTitleWidth : function () {
        var element = this.isDrawn() && this.form
                                     ? isc.Element.get(this.form._getTitleCellID(this))
                                     : null;
        if (element == null) {
            this.logInfo("getVisibleTitleWidth() - unable to determine drawn width for this " +
                         "item - returning 0", "sizing");
            return 0;
        }

        return element.offsetWidth;
    },

    //>    @method    formItem.getIconWidth()    (A)
    //    Takes an icon definition object, and returns the width for that icon in px.
    //        @group    sizing
    //      @param  icon (object)   icon definition object for this item.
    //        @return    (number)    width of the form item icon in px
    //      @visibility external
    //<
    getIconWidth : function (icon) {
        // default to the first icon, if possible
        if (icon == null && this.icons != null && this.icons.getLength() > 0) icon = this.icons[0];
        else if (!this._isValidIcon(icon)) {
            this.logWarn("getIconWidth() passed invalid icon:" + isc.Log.echoAll(icon));
            return null;
        }

        // Note: we could actually look at the icon element in the DOM, (if it's drawn)
        // but we have full control over the HTML written into form item icons, so this value
        // should always match the specified size for the icon.
        return (icon.width != null ? icon.width : this.iconWidth);

    },

    //> @method formItem.setHeight()    (A)
    // Set the height for this element
    //      @group  sizing
    //        @param    (number)    new height for the form element
    //<
    setHeight : function (height) {
        this.height = height;
        // redraw the item (default implementation notifies the container widget that the item
        // needs redrawing)
        this.redraw();
    },

    //> @method formItem.setWidth()    (A)
    // Set the width for this element
    //      @group  sizing
    //        @param    (number)    new width for the form element
    //<
    setWidth : function (width) {
        this.width = width;
        this.redraw();
    },

    //> @method formItem.setLeft()    (A)
    // For a form with +link{DynamicForm.itemLayout,itemLayout}:"absolute" only, set the left
    // coordinate of this form item.
    // <P>
    // Causes the form to redraw.
    // @visibility absForm
    //<
    setLeft : function (left) {
        this.left = left;
        this.redraw();
    },
    //> @method formItem.setTop()    (A)
    // For a form with +link{DynamicForm.itemLayout,itemLayout}:"absolute" only, set the top
    // coordinate of this form item.
    // <P>
    // Causes the form to redraw.
    // @visibility absForm
    //<
    setTop : function (top) {
        this.top = top;
        this.redraw();
    },

    //> @method formItem.moved()    (A)
    // The container widget is responsible for writing the HTML for form items into the DOM.
    // This is a notification function fired by the container items on form items when their
    // global position changes.
    //<
    moved : function () {
        // No default implementation - can be overridden / observed as required.
    },

    //> @method formItem.visibilityChanged()    (A)
    // The container widget is responsible for writing the HTML for form items into the DOM.
    // This is a notification function fired by the container items on form items when they are
    // hidden or shown on the page.  May be caused by parent show() / hide(), or calls to
    // showItem / hideItem.
    //<

    visibilityChanged : function () {
        // No default implementation - can be overridden / observed as required.
    },

    //> @method formItem.zIndexChanged()    (A)
    // The container widget is responsible for writing the HTML for form items into the DOM.
    // This is a notification function fired by the container items on form items when their
    // zIndex is modified on the page.
    //<
    zIndexChanged : function () {
        // No default implementation - can be overridden / observed as required.
    },

    // HTML generation: element, errors, icons and hints
    // --------------------------------------------------------------------------------------------


    //> @method formItem.getInactiveEditorHTML()
    // This method returns a non-interactive HTML representation of this formItem
    // The HTML may be rendered multiple times on the same page and will not include
    // standard unique DOM identifiers or error handling.
    // Used by ListGrids for +link{ListGrid.alwaysShowEditors} type functionality.
    // @param includeHint (boolean) passed through to getStandaloneItemHTML
    // @param includeErrrs (boolean ) passed through to getStandaloneItemHTML
    // @param [context] (any) optional arbitrary context for the inactive HTML. This will
    //  allow us to associate a chunk of HTML with information about the calling code such
    //  as which cell this inactive HTML was written out into in a grid, etc.
    //<
    getInactiveEditorHTML : function (value, includeHint, includeErrors, context) {
        this._retrievingInactiveHTML = true;

        // call 'setupInactiveContext()' to generate a new 'inactiveContext' ID and
        // associate any passed in context with it.

        this._currentInactiveContext = this.setupInactiveContext(context);
        if (this.logIsDebugEnabled("inactiveEditorHTML")) {
            this.logDebug("getInactiveEditorHTML() called - context passed in:" + this.echo(context) +
                    " generated context ID:" + this._currentInactiveContext, "inactiveEditorHTML");
        }

        var HTML = this.getStandaloneItemHTML(value, includeHint, includeErrors);
        delete this._currentInactiveContext;
        delete this._retrievingInactiveHTML;
        return HTML;
    },

    // creates a directory of the 'inactiveHTML' contexts we were passed.
    // Returns a unique identifier by which the context was indexed. Also used
    // to create unique DOM IDs for inactive elements

    // Use an object rather than an array for the directory - easier to delete spots that
    // are no longer required.

    //_inactiveDirectory:null,
    _currentInactiveContextIDCount:1,
    setupInactiveContext : function (context) {

        if (context == null) context = {};
        if (this._isPrinting()) context.isPrintHTML = true;

        var ID = this._currentInactiveContextIDCount++;

        // store the ID directly on the context object so we can manage the directory with just
        // the context object to refer us back to where it's stored!
        context.inactiveContextID = ID;
        context.formItem = this;

        // This is important: Don't share a single object across form items
        // Also this._inactiveDirectory == null is a quick check for never
        // having rendered any inactive items
        if (!this._inactiveDirectory) this._inactiveDirectory = {};
        this._inactiveDirectory[ID] = context;
        return ID;
    },

    // helper to delete inactive context?
    clearAllInactiveEditorContexts : function () {
        delete this._inactiveDirectory;
    },

    clearInactiveEditorContext : function (context) {
        if (isc.isAn.Object(context)) context = context.inactiveContextID;
        if (this._inactiveDirectory) delete this._inactiveDirectory[context];

    },

    // based on a live element in the DOM, determine which (if any) inactiveContext
    // it's associated with by looking at the ID

    _$inactiveContextParsingRegex:new RegExp(".*_inactiveContext(.*)$"),
    _getInactiveContextFromElement : function (element) {
        if (element && element.id != null && this._inactiveDirectory != null) {
            var id = element.id,
                partName = this._getDOMPartName(id);

            if (partName) {
                var inactiveContext = partName.match(this._$inactiveContextParsingRegex);
                if (inactiveContext) {
                    return this._inactiveDirectory[inactiveContext[1]];
                }
            }
        }
        return null;
    },
    // Are we retrieving inactive HTML?
    // This includes the HTML we'll render out into the print window, and
    // the [what?]
    isInactiveHTML : function () {

        if (this.parentItem && this.parentItem.isInactiveHTML()) return true;
        return this._isPrinting() || this._retrievingInactiveHTML;
    },

    _isPrinting : function () {
        return this.containerWidget && this.containerWidget.isPrinting;
    },
    //> @method formItem.getStandaloneItemHTML()   (A)
    // This method returns the HTML for any form item not being written into a standard
    // DynamicForm's table. It allows other widgets to embed form items in their HTML without
    // having to have a DynamicForm as a child.
    //      @group drawing
    //      @return         (HTML)      HTML output for this standalone item's element
    //      @visibility internal
    //<
    // For a widget (other than a DynamicForm) to embed form items, the other widget must
    // - Define a form for it's "standalone" items to belong to.
    // - set the 'containerWidget' property on the form item[s] it wants to write out
    // - use this method to get the HTML for the form item
    // - call 'drawn()' when the HTML for the item has been written out
    // - call 'cleared()' when the HTML for the item is removed from the DOM
    // - call 'redrawn()' when the HTML for the item is re-written in the DOM
    // This is used by the ListGrid to show the edit form items within cells.

    _$absDivStart:"<DIV STYLE='position:absolute;left:",
    _$pxSemiTopColon:"px;top:",
    _$pxSemiWidthColon:"px;width:",
    _$pxSemiHeightColon:"px;height:",
    _$pxSemiIDEquals:"px;' ID='",
    _$quoteRightAngle:"'>",
    _$absDivEnd:"</DIV>",

    _$standaloneStartTemplate:[
        "<SPAN style='white-space:nowrap;' eventProxy=",        // [0]
        ,                                                       // [1] formID
            // this 'containsItem' property may be used to determine which
            // form item events (passed to the form) occurred over.

        " " + isc.DynamicForm._containsItem + "='",                                      // [2]
        ,                                                       // [3] itemID
        "' ID='",                                               // [4]
        ,                                                       // [5] ID for span
        "'>"                                                    // [6]
    ],
    _$standaloneEnd:"</SPAN>",
    _$standaloneSpan:"_standaloneSpan",
    getStandaloneItemHTML : function (value, includeHint, includeErrors) {
        // Write a span around the item with this form's ID as the eventProxy -- this ensures
        // that events are handled by the form rather than whatever the next parent canvas is
        var output = isc.SB.create(),
            form = this.form;

        // output a <SPAN> so the EventHandler recognizes which form this item belongs to
        if (form) {
            if (this._absPos()) {
                var left = this._getPercentCoord(this.left),
                    top = this._getPercentCoord(this.top, true),
                    width = this.getInnerWidth(),
                    height = this.getInnerHeight();
                if (!isc.isA.Number(left)) left = 0;
                if (!isc.isA.Number(top)) top = 0;
                output.append(this._$absDivStart);
                output.appendNumber(left);
                output.append(this._$pxSemiTopColon);
                output.appendNumber(top);

                if (isc.isA.Number(width)) {
                    output.append(this._$pxSemiWidthColon);
                    output.appendNumber(width);
                }
                if (isc.isA.Number(height)) {
                    output.append(this._$pxSemiHeightColon);
                    output.appendNumber(height);
                }
                output.append(this._$pxSemiIDEquals, this._getAbsDivID(), this._$quoteRightAngle);
            }

            var template = this._$standaloneStartTemplate,
                formID = form.getID(),
                itemID = this.getID();

            template[1] = formID;
            template[3] = itemID;

            template[5] = this._getDOMID(this._$standaloneSpan);

            output.append(template);



            output.append(this.getInnerHTML(value, includeHint, includeErrors, true));
            output.append(this._$standaloneEnd);

            if (this._absPos()) {
                output.append(this._$absDivEnd);
            }
        }
        // return and relese the buffer so it can be reused
        return output.release();
    },

    _$absDiv:"_absDiv",
    _getAbsDivID : function () {
        return this._getDOMID(this._$absDiv);
    },

    // cache the absolute div (when 'cleared()' fires this will get cleared)
    getAbsDiv : function () {
        if (this._absDiv) return this._absDiv;
        if (!this.isDrawn()) return;
        this._absDiv = isc.Element.get(this._getAbsDivID());
        return this._absDiv;
    },

    _hasExternalIcons : function () {
        var icons = this.icons;
        if (!icons) return false;
        for (var i = 0; i < icons.length; i++) {
            if (!icons[i].writeIntoItem) return true; // found external icon
        }
        return false; // all icons internal
    },

    // -- Disabled item event mask --

    // In some browsers (seen in Moz), native mouse events (such as mousemove) are not generated
    // when the user moves over disabled native form item elements.
    // If this form item is disabled, we write out a div floating over the native form item
    // so we can still get native mouse events and respond by showing hovers, etc.
    //
    // In IE, we get bogus native events (event.srcElement is an object that can't be
    // enumerated - crashes browser and all event properties produce error if poked) when the
    // mouse is over disabled text in textitems that can be fixed with this workaround
    useDisabledEventMask : function () {
        return ((isc.Browser.isMoz && this.hasDataElement()) ||
                (isc.Browser.isIE && isc.isA.TextItem(this))) &&
               this.getHeight() != null;
    },


    _eventMaskTemplate:[
        "<DIV isDisabledEventMask='true' style='overflow:hidden;position:absolute;width:",
        null,   // 1 width
        "px;height:",
        null,   // 3 height
        //"px;border:1px solid red;' containsItem='",
        "px' " + isc.DynamicForm._containsItem + "='",
        null,   // 5 item id
        "' " + isc.DynamicForm._itemPart + "='" + isc.DynamicForm._element + "' ID='",
        ,       // 7 ID for the element - so we can easily clear it from the DOM
        "'>",
        null,   // 9 spacerHTML - we'll lazily add a spacer here, otherwise a &nbsp; -
                // this needs to be lazy to avoiding trying to load blank.gif before the skin
                // is loaded

        "</DIV>"
    ],
    _getEventMaskHTML : function () {
        var template = this._eventMaskTemplate;
        template[1] = this._getEventMaskWidth();
        template[3] = this.getHeight();
        template[5] = this.getItemID();
        template[7] = this._getDOMID("eventMask");
        template[9] = this._getEventMaskSpacerHTML();

        return template.join(isc.emptyString);
    },

    _getEventMaskSpacerHTML : function () {
        return isc.Canvas.spacerHTML(1600, 100)
    },

    _getEventMaskElement : function () {
        return isc.Element.get(this._getDOMID("eventMask"));
    },




    _getEventMaskWidth : function () {
        var width = this.getElementWidth();
        if (width == null) {
            if (isc.RadioItem && isc.isA.RadioItem(this) && this.parentItem != null) {
                width = this.parentItem.getElementWidth();
            }

            if (width == null) return 0;

        } else {
            // Shrink to account for error icons if necessary
            if (this.form.showInlineErrors && this.hasErrors()
                     && this.getErrorOrientation() == isc.Canvas.LEFT)
            {
                width -= this.getErrorWidth();
            }
        }
        return width;
    },

    // Browser spell checking
    // Supported on
    //  Moz Firefox 2.0 beta2 and above
    //  Safari (tested on 5.0)
    //  Chrome (tested on 5.0.375)
    //  Safari/iPad and Safari/iPhone
    // Untested on IE
    // Note: if browserSpellCheck is unset, we pick it up from the containing form item

    getBrowserSpellCheck : function () {
        if (this.browserSpellCheck != null) return this.browserSpellCheck;
        return this.form.browserSpellCheck;
    },

    // -- Hidden data element --

    // If this is an item with no native form element, but this form's value is being
    // submitted directly to the server, we are going to need a hidden item in the form to
    // represent its value.
    _useHiddenDataElement : function () {
        return (this.shouldSaveValue && !this.hasDataElement() && this.shouldSubmitValue());
    },

    // HTML for the hidden data element
    _$hiddenDataElement:"hiddenDataElement",
    _getHiddenDataElementID : function () {
        return this._getDOMID(this._$hiddenDataElement);
    },
    _getHiddenDataElement : function () {
        return this._getHTMLPartHandle(this._$hiddenDataElement);
    },

    _getHTMLPartHandle : function (part) {
        if (!this.isDrawn()) return null;

        if (!this._htmlPartHandles) this._htmlPartHandles = {};

        // Note: we free up this cache on 'cleared()' / 'redrawn()'
        var handle = this._htmlPartHandles[part];
        if (handle == null) {
            handle = isc.Element.get(this._getDOMID(part));
            if (handle != null) this._htmlPartHandles[part] = handle;
        }
        return handle;
    },

    _$control:"control",
    _getControlTableID : function () {
        return this._getDOMID(this._$control);
    },
    _getControlTableElement : function () {
        return this._getHTMLPartHandle(this._$control);
    },

    _$textBox:"textBox",
    _getTextBoxID : function () {
        return this._getDOMID(this._$textBox);
    },
    _getTextBoxElement : function () {
        if (this.hasDataElement() && this._dataElementIsTextBox && !this.renderAsStatic()) {
            return this.getDataElement();
        }
        return this._getHTMLPartHandle(this._$textBox);
    },


    _$pickerIconCell:"pickerIconCell",
    _getPickerIconCellID : function () {
        return this._getDOMID(this._$pickerIconCell);
    },
    _getPickerIconCellElement : function () {
        return this._getHTMLPartHandle(this._$pickerIconCell);
    },


    _getHiddenDataElementHTML : function () {
        return "<INPUT type='hidden' name='" +
                this.getFieldName() + "' ID='" + this._getHiddenDataElementID() + "'>";
    },

    _$hintCell:"hintCell",
    _getHintCellID : function () {
        return this._getDOMID(this._$hintCell);
    },
    _getHintCellElement : function () {
        return this._getHTMLPartHandle(this._$hintCell);
    },

    //> @method formItem.updateState() [A]
    // Update the visual state of a FormItem to reflect any changes in state or any changes in
    // style settings (eg +link{formItem.textBoxStyle}).
    // <P>
    // Calls to <code>updateState()</code> normally occur automatically as a consequence of
    // focus changes, items becoming disabled, etc.  This method is advanced and intended only
    // for use in workarounds.
    //
    // @visibility external
    //<

    _$FormItemStyling:"FormItemStyling",
    updateState : function () {

        if (!this.isDrawn()) return;

        var showDebugLogs = this.logIsDebugEnabled(this._$FormItemStyling);

        // elements to style:
        // - cell

        if (this.containerWidget == this.form && !this._absPos()) {
            var cellStyle = this.getCellStyle();
            if (showDebugLogs) this.logDebug("About to apply basic cell style:"+ cellStyle, "FormItemStyling");

            // We'll either have a form cell around us, or we'll have written out an abolutely positioned
            // div
            var formCell = this.getFormCell();
            if (formCell) formCell.className = cellStyle;
            // If we have an outer table element we also apply the overall cellstyle to that
            var outerTable = this.getOuterTableElement();
            if (outerTable) outerTable.className = cellStyle;

            // Tell the form to update our title cell's state too.
            if (this.showTitle) this.form.updateTitleCellState(this);
        }

        if (this._shouldShowPickerIcon()) {
            var controlStyle = this.getControlStyle(),
                pickerIconStyle = this.getPickerIconStyle();

                if (showDebugLogs) {
                    this.logDebug("About to apply cell styles to control box and picker icon cell:"+
                                    [controlStyle, pickerIconStyle], "FormItemStyling");

                }

            // - inner table (control style)
            var controlHandle = this._getControlTableElement();
            if (controlHandle) controlHandle.className = controlStyle;
            // - pickerIconBox
            var pickerIconHandle = this._getPickerIconCellElement();
            if (pickerIconHandle) pickerIconHandle.className = pickerIconStyle;
        }

        // - text box
        var textBoxStyle = !this._showingInFieldHint ?
                                this.getTextBoxStyle() : this._getInFieldHintStyle();
        if (showDebugLogs) {
            this.logDebug("About to apply text box style:"+ textBoxStyle,
             "FormItemStyling");
        }

        var textBoxHandle = this._getTextBoxElement();
        if (textBoxHandle) {
            textBoxHandle.className = textBoxStyle;
            if (this.getImplicitSave()) {
                var cssObj = textBoxHandle.style;
                if (this.awaitingImplicitSave) {
                    if (cssObj && this._implicitSaveCSS != true) {
                        this._implicitSaveCSS = true;
                        this._oldCssText = "" + cssObj.cssText;
                        cssObj.cssText = "" + cssObj.cssText + this.editPendingCSSText;
                    }
                } else {
                    if (this.wasAwaitingImplicitSave == true && this._oldCssText) {
                        delete this._implicitSaveCSS;
                        delete this.wasAwaitingImplicitSave;
                        cssObj.cssText = "" + this._oldCssText;
                        delete this._oldCssText;
                    }
                }
            }
        }


        if (this._writeOutFocusProxy() && textBoxHandle) {
            if (!this._focusOutline) {
                // Size the focus outline to match this item's text box size, adjusted for
                // the fact that we always write out a 1px border
                var width = this.getTextBoxWidth(), height = this.getTextBoxHeight();
                width += isc.Element.getHBorderSize(textBoxHandle) -2;
                if (height != null) height += isc.Element.getVBorderSize(textBoxHandle) -2;
                var focusOutlineID = this._getDOMID("focusOutline");
                isc.Element.insertAdjacentHTML(
                    textBoxHandle,
                    "beforeBegin",
                    "<DIV ID='" +  focusOutlineID +
                    (this.textBoxStyle ? "' CLASS='" + this.textBoxStyle +  "Focused'" : "'") +
                    " STYLE='background-image:none;background-color:transparent;position:absolute;width:"
                    +
                    width +
                    (height == null ? "px;" : "px;height:" + height) +
                    "px;visibility:hidden;border:1px dotted white;z-index:100;'>&nbsp;</DIV>"
                );
                this._focusOutline = isc.Element.get(focusOutlineID);
            }

            if (this.hasFocus) this._focusOutline.style.visibility = "inherit";
            else this._focusOutline.style.visibility = "hidden";
        }


    },

    // We have a number of deprecated classNames as of 5.5 - helper method to log warnings
    _$deprecated:"deprecated",
    _warnDeprecated : function (oldPropertyName, newPropertyName, version) {
        if (!this.logIsInfoEnabled(this._$deprecated)) return;
        // Keep track of which property names we've already warned about on this item.
        if (!this._warnedDeprecated) this._warnedDeprecated = {};
        if (this._warnedDeprecated[oldPropertyName] == true) return;

        if (version == null) version = "5.5";
        var logString = isc.SB.create();
        logString.append(
            "Using '", oldPropertyName, "': ", this[oldPropertyName],
            " to style this form item.  This property is deprecated as of SmartClient Version ",
            version, " - we recommend removing this property and using '", newPropertyName, "' instead.");
        this.logInfo(logString.release(), "deprecated");

        this._warnedDeprecated[oldPropertyName] = true;
    },

    //>    @method    formItem.getInnerHTML()        (A)
    //    Return the HTML for this formItem's element(s) and icons.
    //        @group    drawing
    //
    //        @param    value    (string)    Value of the element.
    //        @return            (HTML)    HTML output for this element
    //<

    getInnerHTML : function (value, includeHint, includeErrors, returnArray) {
        // Inactive content: such as printHTML:
        // If we're marked as inactive while getting innerHTML set the _currentInactiveContext
        // flag if it hasn't been set already
        // This ensures we generate unique DOMIDs for inactive content which
        // is separate from the default DOMIDs for our active HTML elements on the page.
        // Note: may have already been set up / mapped to an explicit 'context' object via
        // setupInactiveContext() - we do this in getInactiveHTML(). In this case respect the
        // existing context / contextID
        var clearInactiveContext, clearPInactiveContext;
        if (this.isInactiveHTML() && this._currentInactiveContext == null) {
            clearInactiveContext = true;
            // If our parent is inactive pick up the same 'inactiveContext' object.

            var parentContext, parentItem = this.parentItem;
            if (parentItem != null && parentItem.isInactiveHTML()) {
                if (parentItem._currentInactiveContext == null) {

                    parentItem.setupInactiveContext();
                    clearPInactiveContext = true;
                }
                parentContext = parentItem._inactiveDirectory[parentItem._currentInactiveContext];
            }
            this._currentInactiveContext = this.setupInactiveContext(parentContext);

            if (this.logIsDebugEnabled("inactiveEditorHTML")) {
                this.logDebug("getInnerHTML(): Item is marked as inactive - set up " +
                    "new inactive context ID:" + this._currentInactiveContext,
                    "inactiveEditorHTML");
            }
        }


        this._gotHintHTML = includeHint && !this._getShowHintInField();

        var output;

        // If we need to write out a hidden native data element, do so now.
        if (this._useHiddenDataElement()) {
            if (!output) output = isc.SB.create();
            output.append(this._getHiddenDataElementHTML());
        }

        // If displaying hint in-field, suppress displaying hint in surrounding table.
        if (this._getShowHintInField()) includeHint = false;

        // Note that the tableHTML is an array
        var tableHTML = this._getTableHTML(value, includeHint, includeErrors);


        var returnVal;

        if (output != null) {
            output.append(tableHTML);
            if (returnArray) {

                returnVal = output.getArray().duplicate();
                // pass true here to suppress the normal "toString()" / return from
                // StringBuffer.release, which is a small optimization
                output.release(true);
            } else {
                returnVal = output.release();
            }
        } else {
            returnVal = (returnArray ? tableHTML : tableHTML.join(isc.emptyString));
        }

        // If we set the _currentInactiveContext flag, clear it now.
        if (clearInactiveContext) delete this._currentInactiveContext;
        if (this.parentItem && clearPInactiveContext)
            delete this.parentItem._currentInactiveContext;
        return returnVal;
    },

    _writeOuterTable : function (includeHint, hasLeftRightErrors) {
        if (hasLeftRightErrors) return true;

        if (includeHint && this.getHint() != null) return true;

        if (this.icons && this.icons.length > 0) return true;
    },

    // _getValueIcon()
    // Returns the URL for the value icon to show for this cell, or null if there is none.
    // Checks for the presence of this.getValueIcon, or this.valueIcons
     _$Over:"Over", _$Down:"Down", _$Disabled:"Disabled",
    _getValueIcon : function (value) {

        if (this.suppressValueIcon) return null;

        var icon,
            undef;
        if (value === undef) value = this.getValue();
        if (this.getValueIcon) icon = this.getValueIcon(value);
        // Default behavior
        else {
            if (value == null) icon = this.emptyValueIcon;
            else if (this.valueIcons != null) icon = this.valueIcons[value];
        }

        // We may (and commonly do) just not have a valueIcon
        if (icon == null) return null;

        // We need to be able to show over, disabled, focused and 'mouseDown' state
        // Required for the CheckboxItem
        // This is done independently of the cell style applied to the item's text.

        var newState = ((this.isDisabled() || this.isReadOnly()) &&
                        this.showValueIconDisabled ? this._$Disabled : this._iconState);
        if (newState != null) {

            // Use caching to speed up image-name generation
            if (!isc.CheckboxItem._valueIconStateCache) isc.CheckboxItem._valueIconStateCache = {};
            var cacheObject = isc.CheckboxItem._valueIconStateCache[icon];

            if (!cacheObject) {
                cacheObject = {};
                cacheObject.Over = isc.Img.urlForState(icon, false, false, this._$Over);
                cacheObject.Down = isc.Img.urlForState(icon, false, false, this._$Down);
                cacheObject.Disabled = isc.Img.urlForState(icon, false, false, this._$Disabled);

                isc.CheckboxItem._valueIconStateCache[icon] = cacheObject;
            }

            icon = cacheObject[newState];
        }

        return icon;
    },

    // _getValueIconHTML - returns the IMG tag to write out as our valueIcon
    // or null if we're not showing a valueIcon
    _$valueIcon:"valueIcon",
    _getValueIconHTML : function (value) {
        var valueIcon = this._getValueIcon(value);
        if (valueIcon == null) {
            return isc.emptyString;
        }

        var prefix = this.imageURLPrefix || this.baseURL || this.imgDir,
            suffix = this.imageURLSuffix;
        if (suffix) valueIcon = valueIcon + suffix;

        var valueIconWidth = this.getValueIconWidth();
        var valueIconHeight = this.getValueIconHeight();

        var isRTL = this.containerWidget != null && this.containerWidget.isRTL(),
            valueIconLeftPadding = isRTL ? this.valueIconRightPadding : this.valueIconLeftPadding,
            valueIconRightPadding = isRTL ? this.valueIconLeftPadding : this.valueIconRightPadding;


        return isc.Canvas._getValueIconHTML(valueIcon, prefix, valueIconWidth, valueIconHeight,
                                            valueIconLeftPadding, valueIconRightPadding,
                                            this._getDOMID(this._$valueIcon));
    },

    // method to get a pointer to the valueIcon img element
    _getValueIconHandle : function () {
        if (!this.isDrawn()) return null;
        var img = isc.Element.get(this._getDOMID(this._$valueIcon));
        return img;
    },

    _$outerTableEnd:"</TD></TR></TABLE>",

    _$controlTableTemplate:[
       // Control table
       "<TABLE role='presentation' ID='",        // 0
       ,                     // 1 [ID for the table] this._getControlTablID()

       // By marking the control table with the 'itemPart' attributes we simplify determining
       // what "part" of the item received the event.
       "' " + isc.DynamicForm._containsItem + "='",                // 2
       ,                                                           // 3 [formItem ID]
       "' " + isc.DynamicForm._itemPart + "='" + isc.DynamicForm._controlTableString,       // 4
       "' CELLPADDING=0 CELLSPACING=0 STYLE='",                     // 5
       ,                    // 6 [css text for the control table]
       "' CLASS='",         // 7
       ,                    // 8 [control table className]

       // Text box cell
       "'><TR><TD style='", // 9
       ,                    // 10 [css text for textBox cell]
       "'>",                // 11

       // Text
       ,                    // 12 [textBox html]

       "</TD><TD ID='",     // 13
       ,                    // 14 [picker icon cell ID]
       "' CLASS='",         // 15
       ,                    // 16 [Picker Icon className]
       "' STYLE='",         // 17
       ,                    // 18 [picker icon css]
       "'>",                // 19
       ,                    // 20 [Picker Icon HTML]
       "</TD></TR></TABLE>"
    ],

    _$hintCellTemplate : ["</TD><TD ID='", // 0
                            ,              // 1 this._getHintCellID()
                            "' CLASS='",   // 2
                            ,              // 3 this.getHintStyle()
                            "'>"           // 4
                                           // 4 hint
    ],

    // helper method to return the HTML for the form item's outer element.


    // Returns an array of strings that form the appropriate HTML.



    _getTableHTML : function (value, includeHint, includeErrors) {
        var errorOrientation = this.getErrorOrientation(),
            showErrors,
            errorOnLeft = errorOrientation == isc.Canvas.LEFT,
            errorHTML,
            readOnly = this.isReadOnly()
        ;
        if (includeErrors &&
            (errorOnLeft || errorOrientation == isc.Canvas.RIGHT))
        {
            var errors = this.getErrors();
            if (errors) {
                showErrors = true;
                errorHTML = this.getErrorHTML(errors);
            }
        }

        var vAlign = this.iconVAlign,
            displayValue = this.mapValueToDisplay(value),
            writeOuterTable = this._writeOuterTable(includeHint, showErrors),
            writeControlTable = this._shouldShowPickerIcon();
        ;

        var template = writeOuterTable ? isc.FormItem._getOuterTableStartTemplate() : [];
        if (writeOuterTable) {


            template.length = 13;

            template[1] = this._getOuterTableID();
            template[3] = this.getOuterTableCSS();
            // Apply the cell style to the outer table so (EG) font color / weight get inherited
            // Note that we don't write out the 'cellStyle' at all unless the item is
            // written into a DF cell
            if (this.containerWidget == this.form && !this._absPos()) {
                template[5] = this.getCellStyle();
            } else {
                template[5] = null;
            }

            // If we show the error on the left this is where we output it...
            if (showErrors && errorOnLeft) {
                template[7] = isc.StringBuffer.concat("<TD STYLE='",
                                isc.Canvas._$noStyleDoublingCSS, "' CLASS='",
                                this.getCellStyle(), "'>", errorHTML, "</TD>");
            } else template[7] = null;

            // If the first cell of the outer table contains the text box, write out the
            // appropriate css text
            if (!writeControlTable) template[9] = this.getTextBoxCellCSS();
            else template[9] = isc.Canvas._$noStyleDoublingCSS;
            // First cell
            template[11] = vAlign;
        }

        // If the element is disabled, in some browsers we write an event mask over it
        // to capture mouse events.
        // This is required because we don't get any mouse events (at a native level) over
        // disabled form item elements.
        if ((this.isInactiveHTML() || this.isDisabled()) && this.useDisabledEventMask()) {
            template[template.length] = this._getEventMaskHTML();
        }

        // Logic is quite different for showing a picker icon vs not showing a picker icon.
        if (!writeControlTable) {
            // write the element HTML (text box) directly into the outer table's first cell

            // Note - if we are showing a valueIcon, it will be included in the HTML returned
            // from getElementHTML()
            template[template.length] = (readOnly ? this.getReadOnlyHTML(displayValue, value)
                                                  : this.getElementHTML(displayValue, value));
        } else {

            var pickerIconStyle = this.getPickerIconStyle(),
                itemID = this.getID(),
                controlStyle = this.getControlStyle(),
                controlTemplate = this._$controlTableTemplate,
                controlHandleID = this._getControlTableID(),
                textBoxID = this._getTextBoxID(),
                pickerCellID = this._getPickerIconCellID()
            ;

            controlTemplate[1] = controlHandleID;
            controlTemplate[3] = itemID;
            controlTemplate[6] = this.getControlTableCSS();
            // If no control table style was explicitly specified, pick up the style for the
            // DF cell containing this item (as it will not cascade up through the table element

            if (controlStyle == null && this.containerWidget == this.form && !this._absPos()) {
                controlTemplate[8] = this.getCellStyle();
                controlTemplate[6] += isc.Canvas._$noStyleDoublingCSS;
            } else {
                controlTemplate[8] = controlStyle
            }
            controlTemplate[10] = this.getTextBoxCellCSS();
            controlTemplate[12] = (readOnly ? this.getReadOnlyHTML(displayValue, value)
                                            : this.getElementHTML(displayValue, value));
            controlTemplate[14] = pickerCellID;
            controlTemplate[16] = pickerIconStyle;
            controlTemplate[18] = this.getPickerIconCellCSS();
            var PI = this.getPickerIcon(),
                showPIFocus = PI && this.hasFocus && this.showFocusedPickerIcon &&
                                (PI.showFocusedWithItem != false);
            controlTemplate[20] = this.getIconHTML(this.getPickerIcon(), showPIFocus);

            // Actually write out the control table in the cell
            for (var i = 0; i < controlTemplate.length; i++) {
                template[template.length] = controlTemplate[i];
            }
        }


        if (writeOuterTable) {
            if (this._hasExternalIcons()) {

                var iconsTemplate = isc.FormItem._getIconsCellTemplate();
                iconsTemplate[1] = vAlign;
                iconsTemplate[3] = this.getTotalIconsWidth();
                iconsTemplate[5] = this.iconHeight;
                iconsTemplate[7] = this.getCellStyle();
                iconsTemplate[9] = this.getIconCellID();
                // that actually gives back the HTML for each icon.
                iconsTemplate[11] = this.getIconsHTML();

                for (var i = 0; i < iconsTemplate.length; i++) {
                    template[template.length] = iconsTemplate[i];
                }
            }

            var showRightError = (showErrors && !errorOnLeft);
            var hint;
            if (includeHint) {
                hint = this.getHint();
                if (isc.isA.emptyString(hint)) hint = null;
            }
            if (hint || showRightError) {
                var hintCellTemplate = this._$hintCellTemplate;
                hintCellTemplate[1] = this._getHintCellID();
                hintCellTemplate[3] = hint ? this.getHintStyle() : null;
                hintCellTemplate[5] = (hint || "") + (showRightError ? errorHTML || "" : "");
                for (var i = 0; i < hintCellTemplate.length; i++) {
                    template[template.length] = this._$hintCellTemplate[i];
                }
            }
            // close the table
            template[template.length] = this._$outerTableEnd;
        }

        return template;
    },

    _$iconCell:"iconCell",
    getIconCellID : function () {
        return this._getDOMID(this._$iconCell);
    },

    _$outerTable:"_outerTable",
    _getOuterTableID : function () {
        return this._getDOMID(this._$outerTable);
    },

    // Retrieving Stylenames
    // --------------------------------------

    // Helper to get style name from base style based on current state
    _getCellStyle : function (baseStyle) {
        var hasErrors = this.hasErrors(),
            rtl = this.showRTL && this.containerWidget != null && this.containerWidget.isRTL();

        // Use caching to speed up style-name generation
        var cacheObject;
        if (rtl) {
            cacheObject = isc.FormItem._rtlCellStyleCache[baseStyle];
            if (!cacheObject) {
                cacheObject = isc.FormItem._rtlCellStyleCache[baseStyle] = {
                    Normal: baseStyle + "RTL",
                    Error: baseStyle + "ErrorRTL",
                    ErrorFocused: baseStyle + "ErrorFocusedRTL",
                    Focused: baseStyle + "FocusedRTL",
                    Disabled: baseStyle + "DisabledRTL"
                };
            }
        } else {
            cacheObject = isc.FormItem._cellStyleCache[baseStyle];
            if (!cacheObject) {
                cacheObject = isc.FormItem._cellStyleCache[baseStyle] = {
                    Normal: baseStyle,
                    Error: baseStyle + "Error",
                    ErrorFocused: baseStyle + "ErrorFocused",
                    Focused: baseStyle + "Focused",
                    Disabled: baseStyle + "Disabled"
                };
            }
        }

        // if we have an error always just return the error state
        if (hasErrors && this.shouldShowErrorStyle() && this.form.showInlineErrors) {
            return this.showFocusedErrorState && this.hasFocus && !this.isInactiveHTML() ?
                cacheObject.ErrorFocused : cacheObject.Error;
        } else {
            // suppress focused styling when inactive
            if (this.showFocused && this.hasFocus && !this.isInactiveHTML())
                return cacheObject.Focused;
            if (this.showDisabled && this.isDisabled()) return cacheObject.Disabled;
            // Otherwise "normal" state.
            return cacheObject.Normal;
        }
    },

    //> @method formItem.getCellStyle() (A)
    // Function to retrieve the style name to apply to this form item's cell.
    // Derives the style name from +link{FormItem.cellStyle,this.cellStyle}.
    //
    // @return (CSSStyleName) style to apply to the cell
    //<
    // In some cases we apply the base cells tyle to sub items within the cell. In this case
    // avoid logging warnings if the deprecated styling property attributes are set, so we
    // don't warn repeatedly per rendered item.
    getCellStyle : function () {
        // For items written into a container item, allow the container item to override the
        // cellStyle, so it can re-skin it's child items effectively
        if (this.parentItem != null) {
            if (this.parentItem.itemCellStyle) return this._getCellStyle(this.parentItem.itemCellStyle);
        }

        var className = this._getCellStyle(this.cellStyle);
        //>!BackCompat 2006.3.9
        // If the  old styling properties are set have them take precedence over the
        // new  style names since new names will typically be present from the skin files,
        // but old app code will not know about the new names
        if (!this.hasErrors()) {
            // If the deprecated 'cellClassName' property is set, use that
            if (this.cellClassName != null) {
                this._warnDeprecated("cellClassName", "cellStyle");
                className = this.cellClassName;
            }
        } else {
            // If the deprecated 'errorCellClassName' proeprty is set, use that
            if (this.errorCellClassName != null) {
                this._warnDeprecated("errorCellClassname", "cellStyle");
                className = this.errorCellClassName;
            }
        }
        //<!BackCompat
        return className;
    },

    //> @method formItem.setCellStyle()
    // Setter for +link{FormItem.cellStyle}.
    //
    // @param newCellStyle (FormItemBaseStyle) the new <code>cellStyle</code> value.
    // @group appearance
    // @visibility external
    //<
    setCellStyle : function (newCellStyle) {
        var oldCellStyle = this.cellStyle;
        this.cellStyle = newCellStyle;
        if (oldCellStyle != newCellStyle) this.updateState();
    },

    //>@method  FormItem.getTitleStyle() (A)
    // Function to retrieve the css style class name to apply to this form item's title cell.
    // Derives the style name from <code>this.titleStyle</code>
    // @return CSSStyleName css class to apply to the cell
    //<
    getTitleStyle : function () {
        // If we are printing default to this.printTitleStyle if specified

        if (this._isPrinting() && this.printTitleStyle) {
            return this._getCellStyle(this.printTitleStyle);
        }
        var error = this.getErrors();
        if (error == isc.emptyString) error = null;
        var className = this._getCellStyle(this.titleStyle);
        //>!BackCompat 2006.3.9
        if (!error) {
            // If the deprecated 'titleClassName' property is set, use that
            if (this.titleClassName != null) {
                this._warnDeprecated("titleClassName", "titleStyle");
                className = this.titleClassName;
            }
        } else {
            // If the deprecated 'titleErrorClassName' proeprty is set, use that
            if (this.titleErrorClassName != null) {
                this._warnDeprecated("titleErrorClassName", "titleStyle");
                className = this.titleErrorClassName
            }
        }
        //<!BackCompat
        return className;

    },

    //>@method  FormItem.getHintStyle() (A)
    // Function to retrieve the css style class name to apply to this form item's hint text
    // Derives the style name from <code>this.hintStyle</code>
    // @return CSSStyleName css class to apply to the cell
    //<
    getHintStyle : function () {

        //>!BackCompat 2006.3.9
        if (this.hintClassName != null) {
            this._warnDeprecated("hintClassName", "hintStyle");
            return this.hintClassName;
        }
        //<!BackCompat
        if (this.hintStyle != null) return this.hintStyle;

    },

    // The text box is the element that we write into the first cell of the table control
    // table which contains the textual value of the form item.
    // This is written out by this.getElementHTML and by default is a DIV.

    getTextBoxStyle : function () {
        if (this._isPrinting() && this.printTextBoxStyle) {
            return this._getCellStyle(this.printTextBoxStyle);
        }

        // use the readOnlyTextBoxStyle with canEdit: false and readOnlyDisplay: "static"
        var tbStyle = (this.getCanEdit() == false && this.renderAsStatic() ?
                this.getReadOnlyTextBoxStyle() : this.textBoxStyle),
            styleName = this._getCellStyle(tbStyle)
        ;

        //>!BackCompat 2006.3.9
        // deprecated input element style
        if (this.elementClassName != null) {
            this._warnDeprecated("elementClassName", "textBoxStyle");
            styleName = this.elementClassName;
        }
        //<!BackCompat
        return styleName;
    },

    // Styling applied to the table cell containing the picker icon (if we're showing one)
    getPickerIconStyle : function () {
        if (this.pickerIconStyle != null) return this._getCellStyle(this.pickerIconStyle);
        // allow styling to be inherited from our parent table
        return null;
    },

    // Styling applied to the 'control' table - only rendered if we're showing a picker icon -
    // contains the main text box and the picker icon.
    getControlStyle : function () {
        if (this.controlStyle != null) return this._getCellStyle(this.controlStyle);
        return null;
    },

    // CSS Generation
    // -----------------
    // Method to return custom CSS styling for various parts of the form item

    _$wrapCSS:"white-space:normal;",_$nowrapCSS:"white-space:nowrap;",
    _$minWidthColon:"min-width:", _$minHeightColon:"min-height:",
    _$widthColon:"width:", _$heightColon:"height:", _$pxSemi:"px;", _$semi:";",

    _$cachedOuterTableCSS:{},
    getOuterTableCSS : function () {
        var cacheObject = this._$cachedOuterTableCSS;




        var width,
            expandOuterTable = this.expandHintAndErrors;

        // In printing mode, if this.width is explicitly specified as a number,
        // respect it -- but don't write out a calculated width based on the
        // dyanmicForm's drawn size since printHTML may be written into another component of
        // different width.
        if (!this._isPrinting() || isc.isA.Number(this.width)) {
            // only actually expand if we have a hint or errors.
            if (expandOuterTable && this.getHint() == null) {
                var errorOrientation = this.getErrorOrientation();
                    expandOuterTable = (errorOrientation == isc.Canvas.LEFT ||
                                        errorOrientation == isc.Canvas.RIGHT);
            }

            if (expandOuterTable && (this.getColWidth() != null)) {
                width = Math.max(this.getInnerWidth(), this.getColWidth())
            } else {
                width = this.getInnerWidth();
            }
        }


        if (!isc.isA.Number(width)) {
            if (!this._$nowidthOuterTableCSS)
                this._$nowidthOuterTableCSS = this._$wrapCSS;
            return this._$nowidthOuterTableCSS;
        } else if (cacheObject[width] != null) {
            return cacheObject[width];
        }

        var output = isc.SB.create();
        output.append(this._$wrapCSS);

        output.append(this._$widthColon);
        output.appendNumber(width, 5);
        output.append(this._$pxSemi);

        cacheObject[width] = output.release();

        return cacheObject[width];
    },

    // Control table

    // Retrieve style text to apply to the controlbox table, if we're writing one out.
    _$defaultCursor:"cursor:default;",
    getControlTableCSS : function () {
        var output = isc.SB.create();
        output.append(this._$defaultCursor);

        // The control-table should be sized to the 'innerWidth', minus the size of any
        // external icons. This is currently available as this.getElementWidth()
        var width = this.getElementWidth() - this._getErrorWidthAdjustment();
        if (isc.isA.Number(width)) output.append(this._$widthColon, width, this._$pxSemi);

        // no need to specify height - we will pick this up from the text box element

        return output.release();
    },

    // Text Box Cell

    // Apply no-style-doubling css to the cell containing the text box. This will prevent
    // globally applied "td" styles from showing up around items with hints / checkboxes etc
    // May be overridden by subclasses

    getTextBoxCellCSS : function () {
        return this.textBoxCellCSS != null ? this.textBoxCellCSS : isc.Canvas._$noStyleDoublingCSS;
    },

    // Retrieve style text to apply directly to the text box
    _$textOverflowEllipsisCSS:"overflow:hidden;" + isc.Browser._textOverflowPropertyName + ":ellipsis;",
    _$textAlignColon:"text-align:",
    _$lineHeightColon:"line-height:",
    _$borderBox:"border-box",
    _$mozBoxSizingColon:"-moz-box-sizing:",
    _$webkitBoxSizingColon:"-webkit-box-sizing:",
    _$boxSizingColon:"box-sizing:",
    getTextBoxCSS : function () {
        var output = isc.SB.create(),
            needTextBoxTable = this._needTextBoxTable(),
            clipValue = this._getClipValue();


        if (!needTextBoxTable) {
            var isPrinting = this._isPrinting();


            if (!isPrinting || isc.isA.Number(this.width)) {
                var elementWidth = this.getTextBoxWidth();
                if (isc.isA.Number(elementWidth)) {
                    if ((isc.Browser.isOpera
                         || isc.Browser.isMoz
                         || isc.Browser.isSafari
                         || isc.Browser.isIE9) && !clipValue) {
                        output.append(this._$minWidthColon, elementWidth, this._$pxSemi);
                    } else {
                        output.append(this._$widthColon, elementWidth, this._$pxSemi);
                    }
                }
            }

            var height = this.getTextBoxHeight(),
                heightIsNumber = isc.isA.Number(height);
            if (heightIsNumber) {
                if (!isPrinting && isc.Browser.isMoz && !clipValue) {
                    output.append(this._$minHeightColon, height, this._$pxSemi);
                } else {
                    output.append(this._$heightColon, height, this._$pxSemi);


                    if (isPrinting) output.append(this._$lineHeightColon, height, this._$pxSemi);
                }
            }
        }

        // Don't allow overflow if clipValue is true.
        if (clipValue) output.append(this._$textOverflowEllipsisCSS);

        if (this.wrap) {
            output.append(this._$wrapCSS);
        } else {
            output.append(this._$nowrapCSS);


            if (this._shouldVerticallyCenterTextBox() && !needTextBoxTable) {
                if (isc.Browser.isMoz) output.append(this._$lineHeightColon, "-moz-block-height;");
                else if (heightIsNumber) output.append(this._$lineHeightColon, height, this._$pxSemi);
            }
        }

        if (this.textAlign != null) {
            output.append(this._$textAlignColon, this.textAlign, this._$semi);
        }

        if (isc.Browser.isBorderBox) {
            if (isc.Browser.isMoz) {
                output.append(this._$mozBoxSizingColon, this._$borderBox, this._$semi);
            } else {
                if (isc.Browser.isWebKit) {
                    output.append(this._$webkitBoxSizingColon, this._$borderBox, this._$semi);
                }
                output.append(this._$boxSizingColon, this._$borderBox, this._$semi);
            }
        }

        return output.release();
    },

    // custom styling for picker icon cell

    _$fontSize:"font-size:",
    getPickerIconCellCSS : function () {
        // Not required in IE
        if (isc.Browser.isIE) return isc.emptyString;

        var height = this.getPickerIconHeight();
        if (isc.isA.Number(height) && height < this.getInnerHeight()) {
            return this._$fontSize + height + this._$pxSemi;
        }
        return isc.emptyString;
    },

    // Helper method to get the properties this item's picker icon if 'showPickerIcon' is true.

    getPickerIcon : function () {
        if (this._pickerIcon == null) {
            var pickerIconWidth = this.getPickerIconWidth(),
                pickerIconHeight = this.getPickerIconHeight();

            var props = isc.addProperties({}, this.pickerIconDefaults, this.pickerIconProperties, {
                // Flag this as the picker icon to simplify any special manipulation
                pickerIcon:true,

                writeIntoItem:true,
                showFocused:this.showFocusedPickerIcon,
                hspace:this.pickerIconHSpace,

                // Customizable properties:
                width:pickerIconWidth,
                height:pickerIconHeight,
                src:this.pickerIconSrc,
                prompt: this.pickerIconPrompt
            });

            // apply a name to it to make it a 'valid' icon type object - allows us to get
            // a pointer to its HTML element
            this._setupIconName(props, this.pickerIconName);

            this._pickerIcon = props;

            // We need to have the _disabled flag be set from when the picker icon is
            // first drawn so subsequent enable() / disable()s will update it.
            if (this.iconIsDisabled(props)) props._disabled = true;
        }
        return this._pickerIcon;
    },

    // getElementHTML() - writes out the valueIcon (if present) and text box for the form item
    // For form items using a native HTML Form element such as <input>, this method returns
    // that element's HTML


    _$accessKeyEquals:" ACCESSKEY='",
    _$tabIndexEquals:" TABINDEX='",
    _$singleQuote:"'",
    _$textBoxTemplate:[ "<DIV ID='", // 0
                        ,            // 1: ID for text box
                        // By marking the textBox with the 'itemPart' attributes we simplify
                        // determining what "part" of the item received the event.
                        "' " + isc.DynamicForm._containsItem + "='",                // 2
                        ,            // 3: [formItem ID]
                        "' " + isc.DynamicForm._itemPart + "='" + isc.DynamicForm._textBoxString, // 4
                        "' CLASS='", // 5
                        ,            // 6: this.getTextBoxStyle()
                        "' STYLE='", // 7
                        ,            // 8: this.getTextBoxCSS()
                        "'",         // 9
                        ,            // 10: textBoxFocusAttributes
                        ">",         // 11
                        ,            // 12: valueIcon HTML (if required)
                        ,            // 13: actual value
                        "</DIV>"     // 14
                      ],
    _$textBoxTableTemplate: [
                        "<table role='presentation' cellpadding='0' cellspacing='0' " +
                        "style='table-layout:fixed;overflow:hidden' width='", // 0
                        ,            // 1:
                        "'",         // 2
                        ,            // 3: " height='...'" or null
                        "><tbody><tr><td", // 4
                        ,            // 5: nowrap
                        " id='",     // 6 + 0
                        ,            // 6 + 1: ID for text box
                        "' " + isc.DynamicForm._containsItem + "='", // 6 + 2
                        ,            // 6 + 3: [formItem ID]
                        "' " + isc.DynamicForm._itemPart + "='" + isc.DynamicForm._textBoxString, // 6 + 4
                        "' class='", // 6 + 5
                        ,            // 6 + 6: this.getTextBoxStyle()
                        "' style='", // 6 + 7
                        ,            // 6 + 8: this.getTextBoxCSS()
                        ";overflow:hidden' valign='middle'", // 6 + 9
                        ,            // 6 + 10: textBoxFocusAttributes
                        ">",         // 6 + 11
                        ,            // 6 + 12: valueIcon HTML (if required)
                        ,            // 6 + 13: actual value
                        "</td></tr></tbody></table>" // 6 + 14
                      ],
    _nowrapTrue: " nowrap='true'",
    _shouldVerticallyCenterTextBox : function () {
        return false;
    },

    _needTextBoxTable : function () {
        return (this._shouldVerticallyCenterTextBox() &&
                !this._isPrinting() &&
                isc.Browser.isIE &&
                ((!isc.Browser.isStrict && 7 <= isc.Browser.version && isc.Browser.version <= 8) ||
                 isc.Browser.version <= 6));
    },
    getElementHTML : function (value, dataValue) {

        var output = isc.SB.create(),
            useFocusProxy = this._writeOutFocusProxy();


        var canFocus = this._canFocusInTextBox(),
            textBoxFocusAttributes,
            focusProxyString,
            textBoxStyle = this.getTextBoxStyle();

        if (canFocus) {
            // If we're disabled tabIndex will currently be -1. However we don't clear
            // this.accessKey, so do an explicit check to avoid writing out an accessKey on
            // a disabled form item
            var tabIndex = this._getElementTabIndex(),
                accessKey = this.isDisabled() ? null : this.accessKey;
            if (useFocusProxy) {
                focusProxyString = isc.Canvas.getFocusProxyString(
                                        this.getID(),
                                        // position the focus proxy at 0,0 in the appropriate
                                        // table cell

                                        false, 0, 0,

                                        this.getTextBoxWidth(), this.getTextBoxHeight(),
                                        this.isVisible(), !this.isDisabled(),
                                        tabIndex, accessKey,
                                        // Events on this focus proxy will be handled by the ISC
                                        // eventHandling system

                                        false
                            );
            } else {
                var attrs = isc.SB.create();
                if (accessKey != null) attrs.append(this._$accessKeyEquals, accessKey, this._$singleQuote);
                attrs.append(this._$tabIndexEquals, tabIndex, this._$singleQuote);
                textBoxFocusAttributes = attrs.release();
            }
        }

        if (focusProxyString != null) output.append(focusProxyString);

        var tbTemplate,
            tbOffset;
        if (this._needTextBoxTable()) {
            tbTemplate = this._$textBoxTableTemplate;
            tbOffset = 6;

            var width = this.getTextBoxWidth(dataValue);
            if (isc.Browser.isIE6 && isc.Browser.isStrict) {
                width += isc.Element._getHBorderPad(textBoxStyle);
            }
            tbTemplate[1] = width;

            var height = this.getTextBoxHeight(dataValue);
            if (isc.isA.Number(height)) {
                if (isc.Browser.isIE6 && isc.Browser.isStrict) {
                    height += isc.Element._getVBorderPad(textBoxStyle);
                }
                tbTemplate[3] = " height='" + height + "'";
            } else {
                tbTemplate[3] = null;
            }

            tbTemplate[5] = this.wrap ? null : this._nowrapTrue;

        } else {
            tbTemplate = this._$textBoxTemplate;
            tbOffset = 0;
        }

        tbTemplate[tbOffset + 1] = this._getTextBoxID();
        tbTemplate[tbOffset + 3] = this.getID();
        tbTemplate[tbOffset + 6] = String.asAttValue(textBoxStyle);
        tbTemplate[tbOffset + 8] = this.getTextBoxCSS();
        tbTemplate[tbOffset + 10] = textBoxFocusAttributes; // Will be null if appropriate

        // Pre-pend the value with the valueIconHTML [will be null if appropriate]
        tbTemplate[tbOffset + 12] = this._getValueIconHTML(dataValue);
        tbTemplate[tbOffset + 13] = (this.showValueIconOnly ? null : value);

        output.append(tbTemplate);

        //this.logWarn("element HTML:"+ output.toString());
        return output.release();
    },

    // By default, getElementHTML returns a static display field so we use that.
    // This also allows subclasses that have read-only support inline in getElementHTML()
    // to not require a simple getReadOnlyHTML() override just to call it.
    getReadOnlyHTML : function (value, dataValue) {
        return this.getElementHTML(value, dataValue);
    },

    //> @method formItem.getPrintHTML() (A)
    // @param [printProperties] (PrintProperties)
    // @param [callback] (Callback)
    // @return (HTMLString) print HTML for this item
    // @group printing
    // @visibility internal
    //<
    getPrintHTML : function (printProperties, callback) {
        var value = this.getValue();
        var HTML = this[this.isReadOnly() ? "getReadOnlyHTML" : "getElementHTML"](this.mapValueToDisplay(value), value);
        if (HTML == null) HTML = isc.emptyString;
        return HTML;
    },

    // If we are focusable and not flagged as having an 'inputElement' use a focus proxy
    // wherever we can't make a DIV natively focusable

    _writeOutFocusProxy : function () {


        if (this.useFocusProxy != null) return this.useFocusProxy;

        // Focus proxies were required for older versions of Safari and Chrome
        // This is no longer the case with the latest versions
        // Tested on:
        // - Chrome 13.0.782.215 on Mac OSX and Windows 7 (reports as safariVersion 535.1).
        // - Safari 5.1 on Mac OSX and Windows 7 (reports as safariVersion 534.5)

        return (isc.Browser.isMoz && isc.Browser.geckoVersion < 20051111) &&
                this._canFocus() && !this.hasDataElement();
    },

    // Helper method for HTML parts:

    _getItemElementAttributeHTML : function () {
        if (!isc.FormItem._itemElementAttrHTML) {
            isc.FormItem._itemElementAttrHTML =  [
                " ", isc.DynamicForm._containsItem, "='",
                null,   // item ID
                "' ",
                isc.DynamicForm._itemPart, "='", isc.DynamicForm._element, "'"
            ];
        }
        isc.FormItem._itemElementAttrHTML[3] = this.getItemID();
        return isc.FormItem._itemElementAttrHTML.join(isc.emptyString);
    },

    //> @method formItem.isValid()
    // Returns true if this FormItem has no validation errors.
    // @return (Boolean)
    //<
    isValid : function () {
        var errors = this.getErrors();
        if (errors == null || isc.isAn.emptyObject(errors)) {
            return true;
        }
        return false;
    },

    //>    @method    formItem.getErrors()    (A)
    // Return the validation errors in the form associated with this item, if any.
    // Errors will be returned as either a string (single error message) or an array of strings
    // (multiple error messages).
    //  @return    (string | array) Error message(s) for this item.
    // @group errors
    //<
    getErrors : function () {
        if (this.form) return this.form.getFieldErrors(this);
    },

    // getError() synonym for getErrors() for backcompat

    getError : function () {
        //>DEBUG
        this.logWarn("call to deprecated method FormItem.getError()." +
                     " Use FormItem.getErrors() instead."

                     );
        //<DEBUG
        return this.getErrors();
    },

    // getErrorMessage - given an error string or array of errors - return it formatted as HTML for
    // display
    getErrorMessage : function (error) {
        var errorMessageID = this._getErrorMessageID();
        return (isc.isAn.Array(error) ? "<ul id='" + errorMessageID + "'><li>" + error.join("</li><li>") + "</li></ul>"
                                      : "<span id='" + errorMessageID + "'>" + error + "</span>");
    },

    _getErrorMessageID : function () {
        return this._getDOMID("errorMessage");
    },

    // shouldShowErrorIcon / text / style helpers
    // Allows for form level control of whether error icons/text shows up inline
    shouldShowErrorIcon : function () {
        return this.showErrorIcon != null ? this.showErrorIcon : this.form.showErrorIcons;
    },
    shouldShowErrorText : function () {
        return this.showErrorText != null ? this.showErrorText : this.form.showErrorText;
    },
    shouldShowErrorStyle : function () {
        return this.showErrorStyle != null ? this.showErrorStyle : this.form.showErrorStyle;
    },
    // by default show hover prompts on the icon if we're showing the icon but not the message
    shouldShowErrorIconPrompt : function () {
        return this.shouldShowErrorIcon && !this.shouldShowErrorText();
    },

    // should the error show up above / below / left/right of the item?
    getErrorOrientation : function () {
        return this.errorOrientation != null ? this.errorOrientation : this.form.errorOrientation;
    },


    //>    @method    formItem.getErrorHTML()    (A)
    // Output the HTML for an error message in a form element. Default behavior respects
    // +link{FormItem.showErrorIcon} and +link{FormItem.showErrorText} as described in the
    // documentation for those attributes.
    // @param error (string | array of string) error message string or array of error messages
    // @return (HTML) HTML to display the error
    // @visibility external
    //<

    getErrorHTML : function (error) {
        var showErrorText = this.shouldShowErrorText(),
            showErrorIcon = this.shouldShowErrorIcon();

        if (!showErrorText && !showErrorIcon) return isc.emptyString;

        var form = this.form,
            // If we are writing out an error icon, use a table to insure:
            // - if we're showing a single error message that wraps it doesn't
            //   wrap UNDERNEATH the error icon
            // - if we're showing multiple error messages in a bulleted list the icon
            //   appears to the left of the list rather than appearing above it on a
            //   separate line
            // - note in strict mode (inc HTML5 mode), an img followed by a nbsp char
            //   will wrap the nbsp char, causing the image to appear essentially misaligned
            //   handle this by always writing the table if we're writing out the
            //   error icon in strict mode

            writeTable = (isc.Browser.isStrict ? showErrorIcon :
                          showErrorIcon && showErrorText),


            writeDivInline = !writeTable && showErrorIcon &&
                            ((this.getErrorOrientation() == isc.Canvas.LEFT) ||
                             (this.getErrorOrientation() == isc.Canvas.RIGHT)),

                        // We may want to make this setting hierarchical - so it can be set at
                        // the item or validator level as well
            titleText = (showErrorText && this.form.showTitlesWithErrorMessages &&
                         this.getTitle() != null ? this.getTitle() + ": " : null),
            output,

            messageString = showErrorText ? this.getErrorMessage(error) : null;


        // Write out containsItem / itemPart - this allows us to
        // use partwise events to identify the inline error text.
        // Could be used for custom events, but more importantly, this
        // is helpful for the AutoTest subsystem.
        if (!writeTable) {
            output = isc.SB.concat("<DIV ",
                    this._getInlineErrorHandleAttributes(),
                    (writeDivInline ? "style='display:inline;'" : null)
                    ," CLASS='", this.getCellStyle() , "'>"
                    , (showErrorIcon ? this.getErrorIconHTML(error) + "&nbsp;" : null)
                    , titleText
                    , messageString
                    , "</DIV>"
            );
        } else {

            output = isc.SB.concat("<TABLE ",
                    this._getInlineErrorHandleAttributes(),
                    "' role='presentation' WIDTH=100% CELLSPACING=0 CELLPADDING=0><TR>",
                    "<TD WIDTH=",this.errorIconWidth,">"
                    // If we're writing a table we know we're always writing out the icon
                    , this.getErrorIconHTML(error)
                    , "</TD><TD STYLE='", isc.Canvas._$noStyleDoublingCSS, "' CLASS='" ,
                        this.getCellStyle() , "'>&nbsp;"
                    , titleText
                    , messageString
                    , "</TD></TR></TABLE>"
            );
        }
        return output;
    },

    _getInlineErrorHandleID : function () {
        return this._getDOMID("inlineErrorHandle");
    },

    _getInlineErrorHandleAttributes : function () {
        if (this._inlineErrorAttributeString == null) {
            this._inlineErrorAttributeString = isc.SB.concat(
                "ID='", this._getInlineErrorHandleID(), "' ",
                isc.DynamicForm._containsItem, "='", this.getID(), "' ",
                isc.DynamicForm._itemPart, "='", isc.DynamicForm._inlineErrorString, "' "
            );
        }
        return this._inlineErrorAttributeString;
    },

    getInlineErrorHandle : function () {
        return this.getDocument().getElementById(this._getInlineErrorHandleID());
    },

    getErrorIconHTML : function (error) {

        this._currentIconError = error;

        var id = this.getErrorIconId();

        var errorString = "";
        // add the error as an aria-label so that we can point to this as an "aria-describedby"
        // element.  This is added as part of the "extraStuff" parameter below
        if (error != null && isc.Canvas.ariaEnabled() && !isc.Canvas.useLiteAria()) {
            if (isc.isAn.Array(error)) error = error.join(",");
            errorString = " aria-label='" + String.asAttValue(error) + "'";
        }


        return this._getIconImgHTML(
                // unique ID for the img
                id,
                this.errorIconWidth, this.errorIconHeight,
                //vAlign for the icon
                "top",
                0,  // vMargin
                // No left margin for the icon, no background-color for this icon
                null,
                null,

                // Src

                this.form.getImgURL(this.errorIconSrc),
                // special flag to avoid 'display:block' in standards mode
                true,
                // extraStuff for error icon info for event (This will cause error text
                // to show up in a hover)
                // getIconImgHTML doesn't handle this directly since we usually
                // don't have img-only icons be interactive.
                isc.DynamicForm._containsItem + "='" + this.getID() + "' " +

                // Don't use the same ID for the icon part name (used for event handling)
                // as for the element in the DOM - the 'errorIconId' is retrieved via
                // _getDOMId which guarantees
                // a unique ID within the page (required for img name / dom element ID etc),
                // but doesn't guarantee consistency across page reloads etc.
                // We want the eventPart type ID to be consistent so the autoTest subsystem
                // can reliably identify error icons.
                isc.DynamicForm._itemPart + "='" + this.errorIconName + "'" +
                errorString
        );
    },

    getErrorIconId : function () {
        return this._getDOMID("error");
    },
    errorIconName:"isc_errorIcon",

    //>    @method    textItem.getHint()    (A)
    //    Returns the hint text for this item. Default implementation returns +link{FormItem.hint}, or
    //  null if there is no hint to show.
    //
    // @group    appearance
    // @return    (HTML)        HTML to show as the hint for the item
    // @visibility external
    //<
    getHint : function () {
        if (!this.showHint || !this.hint) return null
        return this.hint;
    },

    // Drawing
    // ----------------------------------------------------------------------------------------
    // Form items don't write themselves into the DOM - this is typically handled by their
    // dynamicForm, or for 'standalone items', this is handled by the items' containerWidget.
    // The code to write the items out into the DOM should also notify the form items that they
    // have been written into the DOM, to allow us to perform 'isDrawn()' checks and perform
    // any necessary manipulations of the items' data element.


    //> @method formItem.drawn()
    // Notification function to be fired on the form item when the item has been written into
    // the DOM by some container widget.
    //
    // @group drawing
    // @visibility internal
    //<
    _$drawing:"drawing",
    drawn : function () {
        //>DEBUG
        if (this.logIsInfoEnabled(this._$drawing)) {
            this.logInfo("Form item drawn " +
                         (this.containerWidget == this.form ?
                                "in form " + this.form.getID() :
                                "in container widget " + this.containerWidget.getID()) +
                         (this.logIsDebugEnabled("drawing") ? this.getStackTrace() : ""),
                         "drawing");
        }
        //<DEBUG

        this._drawn = true;
        if (this._gotHintHTML) this._wroteOutHint = true;
        this._gotHintHTML = null;
        this._applyHandlersToElement();

        if (isc.screenReader) this.addContentRoles();
    },

    // fired when this item is about to be redrawn

    redrawing : function () {
        if (this._hasRedrawFocus(true)) {

            this._storeFocusForRedraw();
        }
        this.form.clearingElement(this);
        this._absDiv = null;
    },

    //> @method formItem.redrawn()
    // Notification function to be fired on the form item when the items HTML has been redrawn
    // by some container widget.
    //
    // @group drawing
    // @visibility internal
    //<
    redrawn : function () {
        //>DEBUG
        if (this.logIsInfoEnabled("drawing")) {
            this.logInfo("Form item redrawn " +
                         (this.containerWidget == this.form ?
                                "in form " + this.form.getID() :
                                "in container widget " + this.containerWidget.getID()) +
                         (this.logIsDebugEnabled("drawing") ? this.getStackTrace() : ""),
                         "drawing");
        }
        //<DEBUG

        // clear pointer to data element
        this._clearCachedHandles();

        this._applyHandlersToElement();

        if (isc.screenReader) this.addContentRoles();

        if (this._hasRedrawFocus(true)) {
            this._refocusAfterRedraw();
        }
    },

    // _storeFocusForRedraw()
    // When a dynamicForm is redrawn, if an item has focus, we want it to continue to have the same
    // focus / selection as before the redraw.
    // This method stores the selection / where the focus is (text box vs icons etc), so we
    // can restore it after focus

    _storeFocusForRedraw : function () {
        this._hadFocusBeforeRedraw = true;

        this.rememberSelection();

        if (this.items) {
            for (var i = 0; i < this.items.length; i++) {
                if (this.items[i].hasFocus) {
                    return this.items[i]._storeFocusForRedraw();
                }
            }
        }

        var element = this._getCurrentFocusElement();

        if (element != null && element != this.getFocusElement()) {
            var picker = this.getPickerIcon();
            if (picker != null && this._getIconLinkElement(picker) == element) {
                this._redrawFocusIcon = picker;
            } else if (this.icons) {
                for (var i = 0; i < this.icons.length; i++) {
                    if (this._getIconLinkElement(this.icons[i]) == element) {
                        this._redrawFocusIcon = this.icons[i];
                        break;
                    }
                }
            }
        }
    },

    // _refocusAfterRedraw - fired in response to item.redrawn()
    // If the item had focus when the redraw occurred, put focus back in it (without firing any
    // focus handlers) now.
    _refocusAfterRedraw : function () {

        // Sanity checks - don't refocus if
        // - we're not visible or drawn
        // - focus is marked as being elsewhere on the page
        // - focus is marked as being elsewhere on the form
        //  (focus shift implies this was delayed and focus has subsequently moved)
        var shouldRefocus = this.isDrawn() && this.isVisible();
        if (shouldRefocus) {
            var focusCanvas = isc.EH.getFocusCanvas();
            if (focusCanvas != null && focusCanvas != this.form) {
                shouldRefocus = false;
            } else {
                var focusItem = this.form.getFocusSubItem();
                if (focusItem != this && focusItem != this.parentItem &&
                    (!this.items || !this.items.contains(focusItem)))
                {

                    shouldRefocus = false;
                }
            }
        }
        delete this._hadFocusBeforeRedraw;


        if (shouldRefocus && isc.Browser.isIE) {
            isc.FormItem._testStuckSelectionAfterRedraw(this);
        }

        if (this.items) {
            for (var i = 0; i < this.items.length; i++) {
                if (this.items[i]._hasRedrawFocus()) {
                    return this.items[i]._refocusAfterRedraw();
                }
            }
        }


        // we want our refocus to be silent - call the uber-advanced method to suppress
        // developer-specified focus handlers from firing when focus gets restored
        if (shouldRefocus) {
            this.form._suppressFocusHandlerForItem(this);
            // If the focus item was scrolled out of view, it'll natively jump into
            // view when we refocus on it, scrolling the parent.
            // Use the notifyAncestorsAboutToReflow / notifyAncestorsReflowComplete mechanism
            // to avoid a scroll jump on the containerWidget (even though the redraw has
            // already occurred at this point)

            this.containerWidget.notifyAncestorsAboutToReflow();
        }

        // If appropriate stick focus back into the icon which previously had it
        var focussed = false;
        if (this._redrawFocusIcon) {
            var icon = this.getIcon(this._redrawFocusIcon);
            delete this._redrawFocusIcon;
            if (icon) {
                if (shouldRefocus) {
                    this.focusInIcon(icon);
                }
                focussed = true;
            }
        }

        // still going - we weren't focused in a sub icon or an item - just focus in
        // our standard focus element
        if (shouldRefocus && !focussed) {
            // set a flag noting that we're currently refocussing after redraw
            // on element focus (which may be async) we'll clear this flag
            // This allows us to suppress 'selectOnFocus' behavior.
            this._refocussingAfterRedraw = true;
            this.focusInItem();
        }
        if (shouldRefocus) {
            this.containerWidget.notifyAncestorsReflowComplete();
        }

    },


    _applyHandlersToElement : function () {
        //!DONTCOMBINE

        if (this._canFocus()) {
            var element = this.getFocusElement();
            if (!element) {
                if (this._canFocusInTextBox()) {
                    this.logWarn("Attempting to apply event handlers to this item. " +
                        "Unable to get a pointer to this item's focus element");
                    return;
                }
            } else {

                if (this._propagateMultiple) {
                    // _propagateMultiple is set on items that can support multiple-files
                    if (this.multiple) {
                        element.multiple = this.multiple;
                    } else {
                        element.multiple = false;
                    }
                }

                if (this.accept) {
                    // FileItem and UploadItem use this to supply filters to the file picker
                    element.accept = this.accept;
                }

                // Apply focus/blur handlers to the focus element. These fall through to
                // formItem._nativeElementFocus() / formItem._nativeElementBlur()
                element.onfocus = isc.FormItem._nativeFocusHandler;
                element.onblur = isc.FormItem._nativeBlurHandler;

                // IE fires proprietary oncut / onpaste events. Set up handlers for these so we
                // can detect changes due to paste triggered from a menu option as well as from
                // keypresses.

                if (isc.Browser.isIE) {
                    element.onpaste = isc.FormItem._nativeCutPaste;
                    element.oncut= isc.FormItem._nativeCutPaste;
                }

                // Support a generic way to apply native event handlers to the element without
                // overriding this method.
                //  this._nativeEventHandlers is expected to be an object of the format
                //   {nativeHandlerName:function}
                // [Don't apply these handlers to icons!]
                if (this._nativeEventHandlers) {
                    for (var handler in this._nativeEventHandlers) {
                        if (this._nativeEventHandlers[handler] == null) continue;
                        element[handler] = this._nativeEventHandlers[handler];
                    }
                }
            }
        }

        this._setUpIconEventHandlers();
    },

    _setUpIconEventHandlers : function () {
        // If we have any icons, we need to apply focus/blur handlers to them as well.
        // Note that we may draw/clear icons independently of redrawing the form item, so we
        // need a separate method to handle them being drawn
        if (this._shouldShowPickerIcon()) this._iconDrawn(this.getPickerIcon());
        if (this.showIcons && this.icons && this.icons.length > 0) {

            for (var i = 0; i < this.icons.length; i++) {
                var icon = this.icons[i];
                if (icon && (this._writeIconIntoItem(icon) || this._shouldShowIcon(icon)))
                    this._iconDrawn(icon);
            }
        }
    },

    // Notification function fired whenever an icons is written into the DOM.
    // Allows us to apply event handlers directly to the icon rather than writing them out
    // as part of the icon's HTML
    _$hash:"#",
    _useIconLinkElements:

        (!isc.Browser.isSafari) &&
        (!isc.Browser.isMoz),
    _iconDrawn : function (icon) {
        if (!icon.imgOnly) {
            var link = this._getIconLinkElement(icon);

            if (link) {
                link.onfocus = isc.FormItem._nativeIconFocus
                link.onblur = isc.FormItem._nativeIconBlur

                if (this._useIconLinkElements) {
                    // The link needs an HREF or it will not be focus-able
                    link.href = this._$hash;

                    // Write out an onclick handler that simply stops us navigating to the href
                    // for the icon's link.  We will fire the icon's click action via
                    // standard form item click handling
                    link.onclick = isc.FormItem._nativeIconClick;
                }
            }

        }



    },

    //> @method formItem.cleared()
    // Notification function to be fired on the form item when the items HTML has been removed
    // from the DOM by some container widget.
    //
    // @group drawing
    // @visibility internal
    //<
    cleared : function () {
        //>DEBUG
        if (this.logIsInfoEnabled("drawing")) {
            this.logInfo("Form item cleared " +
                         (this.containerWidget == this.form ?
                                "from within form " + this.form.getID() :
                                "from within container widget " + this.containerWidget.getID()) +
                         (this.logIsDebugEnabled("drawing") ? this.getStackTrace() : ""),
                         "drawing");
        }
        //<DEBUG
        this.form.clearingElement(this);

        this._clearCachedHandles();
        this._wroteOutHint = false;
        this._gotHintHTML = false;
        this._drawn = false;
    },

    _clearCachedHandles : function () {
        this._dataElement = null;
        this._absDiv = null;
        this._focusProxyHandle = null;
        this._htmlPartHandles = {};
    },

    //> @method formItem.isDrawn()
    // Returns true if this item has been written out into the DOM.
    //
    // @return (Boolean) whether this item is drawn
    // @group drawing
    // @visibility external
    //<
    isDrawn : function () {
        return this._drawn;
    },

    // Icons
    // -----------------------------------------------------------------------------------------

    // _setUpIcons called at init time.  This should apply default properties to icons as
    // required.
    _setUpIcons : function () {
        var icons = this.icons;
        if (icons == null) return;

        for (var i = 0; i < icons.length; i++) {
            var icon = icons[i];

            this._setUpIcon(icon);
        }

    },

    // _setUpIcon - run by setUpIcon() on each specified icon object to apply required
    // properties such as ID.
    // Split into a separate method so this can be called separately if icons are applied after
    // setUpIcons has been run (See ExpressionItem for an example of this)
    _setUpIcon : function (icon) {
        // apply an identifier to the icon (to be written into the DOM as an attribute)
        // to ensure that the
        // appropriate click action is fired on a click, and allow us to get a pointer
        // back to the icon image / link elements in the DOM
        this._setupIconName(icon);

        // Set the '_disabled' flag on the icon. We use this to track whether we need to
        // update HTML when the icon gets enabled / disabled
        if (this.iconIsDisabled(icon)) icon._disabled = true;

    },


    //> @method    formItem.getIconsHTML()  (A)
    //  Return the HTML to draw any icons to be displayed after the form item
    //  @group  appearance
    //
    //      @return (HTML)      HTML for the icons
    //<
    _iconsTableStart:"<table role='presentation' cellpadding=0 cellspacing=0 margin=0><tr>",
    getIconsHTML : function (includePicker) {
        if (!this.showIcons ||
            (this.icons == null && (!includePicker || !this._shouldShowPickerIcon())))
        {
            return "";
        }
        var hasFocus = this._hasRedrawFocus(true);

        if (this.showIconsOnFocus && !hasFocus) {
            this.hideAllIcons();
            return "";
        }

        var output = isc.SB.create(),
            showingIcons = false;

        // Write the icons out into a table with one cell per icon.
        // This will ensure they reliably show up in a row horizontally without
        // relying on <nobr> or css white-space:nowrap;

        var icons = this.icons;
        if (includePicker && this._shouldShowPickerIcon()) {
            icons = [this.getPickerIcon()];
            icons.addList(this.icons);
        }

        for (var i = 0; i < icons.length; i++) {


            var icon = icons[i];
            // don't write out the icon if it specified a showIf, which returns false
            if (!this._shouldShowIcon(icon) || this._writeIconIntoItem(icon)) continue;

            if (showingIcons == false) {
                showingIcons = true;
                output.append(this._iconsTableStart);
            }

            output.append("<td>");

            var showFocused = hasFocus && this._iconShouldShowFocused(icon, true);
            output.append(this.getIconHTML(icon, null, this.iconIsDisabled(icon), !!showFocused));

            output.append("</td>");
        }
        if (showingIcons) output.append("</table>");

        return output.release();
    },

    // Helper method to determine if an item (or one of it's subItems) has focus before redraw
    _hasRedrawFocus : function (checkSubItems) {
        var hasFocus = this.hasFocus ||  this._hadFocusBeforeRedraw;
        // If we have sub items, check for whether one of those has focus
        if (checkSubItems && !hasFocus && this.items != null) {
            for (var i = 0; i < this.items.length; i++) {
                if (this.items[i].hasFocus || this.items[i]._hadFocusBeforeRedraw) hasFocus = true;
                break;
            }
        }

        // Exception: If we're scrolled out of the containerWidget's viewport, don't refocus or
        // we'll natively jump scroll into view.


        return hasFocus;
    },

    // setupIconName
    // Given an icon object, ensure it has a unique name, autoGenerating one if appropriate
    // This method is called on init and on setIcons so should be able to test for
    // uniqueness here.

    _setupIconName : function (icon, name) {
        if (name == null) name = icon.name;
        // Backcompat: We used to use icon._id.
        // This was never exposed so developers shouldn't have been setting this
        // attribute but for safety, if this is set, respect it
        if (name == null && icon._id != null) {
            this.logWarn("Attempting to use '_id' property as icon name - this property has been deprecated in favor of 'name'");
            name = icon._id;
        }
        if (name != null) {
            // test for uniqueness - names must be unique or getIcon / updateIconSrc etc will fail
            var collisions = this.icons ? this.icons.findAll("name", name) : [];
            if (collisions != null && collisions.length > 0 &&
                (collisions.length > 1 || collisions[0] != icon))
            {
                this.logWarn("This form item has more than one icon with the same specified name:"
                    + name + ". Ignoring this name and using an auto-generated one instead.");
                name = null;
            } else {
                icon.name = name;
                return icon;
            }
        }
        if (this._nextIconId == null) this._nextIconId = 0;
        icon.name =  "_" + this._nextIconId++;
        return icon;
    },


    _getIconVAlign : function (icon) {
        // Don't write out a vertical-align css property for the picker icon
        if (this._pickerIcon && (icon == this._pickerIcon)) return null;

        var alignment = this.iconVAlign;

        if (alignment == isc.Canvas.TOP) {
            return "top";
        } else if (alignment == isc.Canvas.BOTTOM) {
            return (isc.Browser.isSafari ? "bottom" : "text-bottom");
        } else if (alignment == isc.Canvas.CENTER) {
            return "middle"
        }

        // if we don't recognize the alignment, just return it.
        return alignment;
    },

    // _getIconVMargin - return a value to write as a top / bottom margin onto the icons' img
    // tag

    _getIconVMargin : function () {
        return 0;
    },

    // Helper to get the prompt for the icon, if there is one.
    getIconPrompt : function (icon) {
        // don't show icon-specific prompt on hover over disabled icon (or disabled item with
        // icon, which is obviously also disabled).
        // We still show item level prompt for disabled items - this is useful for
        // telling the user why an item is disabled, for example.
        if (this.iconIsDisabled(icon)) return null;
        return icon.prompt || this.iconPrompt;
    },

    // Gets the src for an icon's image.
    _$blank: "blank",
    _$rtl: "rtl",
    getIconURL : function (icon, over, disabled, focused) {
        var src = icon.src || this.defaultIconSrc;
        if (src == this._$blank) return isc.Canvas._blankImgURL;

        var state = (this.showDisabled && (disabled || this.iconIsDisabled(icon)))
                            ? isc.StatefulCanvas.STATE_DISABLED
                            : over ? isc.StatefulCanvas.STATE_OVER : null,
            customState = (icon.showRTL && this.containerWidget != null && this.containerWidget.isRTL())
                          ? this._$rtl : null;

        src = isc.Img.urlForState(src, false, focused, state, null, customState);
        return src;
    },

    _$RTL: "RTL",
    getIconStyle : function (icon, over, disabled, focused) {
        if (!icon || icon.baseStyle == null) return null;
        var retValue = icon.baseStyle;
        if (this.showDisabled && (disabled || this.iconIsDisabled(icon))) {
            retValue += isc.StatefulCanvas.STATE_DISABLED;
        } else {
            if (focused) retValue += isc.StatefulCanvas.FOCUSED;
            if (over) retValue += isc.StatefulCanvas.STATE_OVER;
        }
        if (icon.showRTL && this.containerWidget != null && this.containerWidget.isRTL()) {
            retValue += this._$RTL;
        }
        return retValue;
    },

    // getIconHTML() retrieves the HTML for icons.
    // By default icons are written into the DOM after the form item. However we also use this
    // method for icons written directly into the form item's HTML (see the SelectItem for
    // an example of this).
    getIconHTML : function (icon, over, disabled, focused) {
        var iconSrc = this.getIconURL(icon, over,disabled,focused),
            width = this.getIconWidth(icon),
            height = this.getIconHeight(icon),
            hspace = (icon.hspace != null ? icon.hspace : this.iconHSpace),
            backgroundColor = icon.backgroundColor,
            formID = this.form.getID(),
            // Remember - this is a global ID for this Form Item Instance, so can be used
            // as window[itemID].foo(), as well as being passed to the 'bubbleEvent()' method
            // on the Form.
            itemID = this.getItemID(),
            iconID = icon.name,
            iconStyle = this.getIconStyle(icon, over, disabled, focused),
            classText = (iconStyle == null ? isc.emptyString : " class='" + iconStyle + this._$singleQuote);
        // If the icon is marked as 'imgOnly', just return the img tag - event handling should
        // be handled by the Form Item itself
        if (icon.imgOnly) {

            return this._getIconImgHTML(
                                this._getIconImgId(iconID),
                                width,
                                height,
                                this._getIconVAlign(icon),
                                this._getIconVMargin(icon),
                                // If it's just an image we always put hspace onto the image tag
                                // as a left margin
                                hspace,
                                backgroundColor,
                                iconSrc,
                                null,
                                classText
                    );

        // We embed the icon in a link to make it focusable

        } else {


            if (isc.FormItem._iconTemplate == null) {
                isc.FormItem._iconHSpacePrefix = " style='margin-left:";
                isc.FormItem._iconHSpaceRTLPrefix = " style='margin-right:";

                isc.FormItem._iconTemplate = [
                    (this._useIconLinkElements
                        ? "<a role='button' ID='" : "<span role='button' ID='"),    // 0
                    ,                           // 1: link elementID: this._getIconLinkId(icon.name)
                    "'",                        // 2


                    isc.FormItem._iconHSpacePrefix,  // 3
                    ,                           // 4: icon h-space: hspace
                    "px;"
                    + (isc.Browser.isMoz ? "-moz-user-focus:" : ""),    // 5
                    ,                           // 6: normal / ignore (MOZ ONLY)
                    ,                           // 7: cursor:default if disabled, otherwise "hand"

                    "' tabIndex=",               // 8
                    ,                           // 9: Tab index

                    // Identifiers for the form item event handling system
                    " ",                        // 10
                    isc.DynamicForm._containsItem,  // 11
                    "='",                       // 12
                    ,                           // 13: itemID
                    "' ",                       // 14
                    isc.DynamicForm._itemPart,  // 15
                    "='",                       // 16
                    ,                           // 17: iconID

                    // Allow the ISC event handling system to handle events occurring over
                    // this link.
                    "' handleNativeEvents=false>", // 18
                    ,                           // 19: this._getIconImgHTML()
                    (this._useIconLinkElements ? "</a>" : "</span>") // 20


                ]
            }

            var template = isc.FormItem._iconTemplate;


            var disabled = this.iconIsDisabled(icon),
                tabIndex = (disabled || this.canTabToIcons == false) ? -1
                                              : this._getIconTabIndex(icon);


            template[1] = this._getIconLinkId(iconID);

            var hspaceToLink = this._applyIconHSpaceToLink(icon);
            if (hspaceToLink) {
                if (this.containerWidget.isRTL()) {
                    template[3] = isc.FormItem._iconHSpaceRTLPrefix;
                } else {
                    template[3] = isc.FormItem._iconHSpacePrefix;
                }
                template[4] = hspace;
            } else {
                template[4] = "0"
            }


            //In Moz we need to set -moz-user-focus to disable focus if tabIndex < 0
            if (isc.Browser.isMoz) template[6] = (tabIndex < 0 ? "ignore;" : "normal;");

            template[7] = disabled ? "cursor:default;" : "cursor:" + isc.Canvas.HAND;

            template[9] = tabIndex;

            if (isc.Canvas.ariaEnabled() && !isc.Canvas.useLiteAria()) {
                template[10] = " ";
                // we use window.status to show the prompt, this won't work for a screenReader
                if (icon.prompt) {

                    template[10] = " aria-label='" + icon.prompt.replaceAll("'", "&apos;") + "' ";
                }
                // advertise disabled state as well
                if (disabled) template[10] += " aria-disabled='true' ";
            }

            template[13] = itemID;
            template[17] = iconID;
            template[19] = this._getIconImgHTML(
                                this._getIconImgId(iconID),
                                width,
                                height,
                                this._getIconVAlign(icon),
                                this._getIconVMargin(icon),
                                (!hspaceToLink ? hspace : null),
                                backgroundColor,
                                iconSrc,
                                null,
                                classText
                           );

            return template.join(isc.emptyString);

        }
    },

    // Helper method - Wherever possibly we apply icon hspace as margin-left on the Link item
    // around an icon rather than on the img tag. This avoids the dotted focus outline extending
    // to the left of the image when the icon has focus.
    // However
    // - this doesn't work in IE
    // - in Safari / Chrome we've seen it introduce styling problems

    // - In Moz strict mode it also introduces styling problems, causing (for example)
    //   the date picker icon to appear vertically misaligned with other icons.
    // - in some cases we don't write out a link element
    // So we can't always use this approach

    _applyIconHSpaceToLink : function (icon) {
        return (!isc.Browser.isIE && !isc.Browser.isSafari && !icon.imgOnly && !isc.Browser.isStrict);
    },

    // Use _getIconImgHTML() to get the HTML for the image, without the link tag
    // Called from _getIconHTML(), and also used for the error icon

    _$vAlignColon:"vertical-align:",
    _iconImgHTMLExtraCSSTextTemplate: [
        // Align the icon vertically as specified.

        ,                           // 0 vertical-align:, or null
        ,                           // 1 valign or null (this._getIconVAlign(icon))
        ";margin-top:",             // 2
        ,                           // 3: this._getIconVMargin(icon)
        "px;margin-bottom:",        // 4
        ,                           // 5: this._getIconVMargin(icon)
        "px;",                      // 6
        // Optional left margin for the icon
        ,                           // 7
        // Optional background-color for the icon
        ,                           // 8: background-color='xxx'
        null                        // 9: optional display:block for strict mode
    ],
    _getIconImgHTML : function (imgID, width, height, vAlign, vMargin, lMargin, backgroundColor,
                                src, errorIcon, extraStuff) {
        // Get the icon Img HTML from the Canvas 'imgHTML()' method.  This handles displaying
        // PNG type files as well as other img files.


        var template = this._iconImgHTMLExtraCSSTextTemplate;

        if (vAlign != null) {
            template[0] = this._$vAlignColon;
            template[1] = vAlign;
        } else {
            template[0] = null;
            template[1] = null;
        }

        // apply any top / bottom margin to the icon

        template[3] = vMargin;
        template[5] = vMargin;

        if (lMargin != null) {

            template[7] = (this.containerWidget.isRTL() ? "margin-right:" : "margin-left:") + lMargin + "px;";
        } else {
            template[7] = null;
        }

        template[8] = (backgroundColor != null ? "background-color:" + backgroundColor + ";" : null);

        // display:block - required for strict mode IF the icon is being rendered into a table
        // (which is the default)
        // Avoid this if we're not in strict mode, or this is the error icon
        if (isc.Browser.isStrict && !isc.Browser.isTransitional && !errorIcon) {
            template[9] = "display:block;";
        } else {
            template[9] = null;
        }

        var extraCSSText = template.join(isc._emptyString);

        if (extraStuff == null) {
            extraStuff = " id='" + imgID + "'";
        } else {
            extraStuff += " id='" + imgID + "'";
        }


        var imgParams = isc.FormItem._imgParams = isc.FormItem._imgParams ||
                { align:isc.Browser.isSafari ? "absmiddle" : "TEXTTOP" };
        imgParams.src = src;
        imgParams.width = width;
        imgParams.height = height;
        imgParams.extraCSSText = extraCSSText;
        imgParams.extraStuff = extraStuff;
        return isc.Canvas.imgHTML(imgParams);
    },

    // -------------------------
    // icons methods
    //

    // Icons consist of 2 elements - an image surrounded by a link
    // Internal methods _getIconLinkId() and _geticonImgId() return a unique identifier for
    // these elements based on some icon's ID
    _ImgIDCache:{},
    _$_iLink_:"_iLink_",
    _$_iImg_:"_iImg_",
    _getIconLinkId : function (id) {
        // inactiveHTML - avoid caching the IDs here - we'll not be needing to get at the generated
        // link/img elements directly
        if (this.isInactiveHTML()) {
            return this._getDOMID(this._$_iLink_ + id);
        }
        if (!this._iLinkIDCache) this._iLinkIDCache = {};
        var cache = this._iLinkIDCache;
        if (!cache[id]) {
            // doing our own cacheing so don't have _getDOMID also cache the result
            cache[id] = this._getDOMID(this._$_iLink_ + id, true);
        }
        return cache[id];
    },
    _getIconImgId : function (id) {
        // inactiveHTML - avoid caching the IDs here - we'll not be needing to get at the generated
        // link/img elements directly
        if (this.isInactiveHTML()) {
            return this._getDOMID(this._$_iImg_ + id);
        }
        if (!this._iImgIDCache) this._iImgIDCache = {};
        var cache = this._iImgIDCache;
        if (!cache[id]) {
            // doing our own cacheing so don't have _getDOMID also cache the result
            cache[id] = this._getDOMID(this._$_iImg_ + id, true);
        }
        return cache[id];
    },


    // Internal methods to get a pointer to Icon's HTML elements in the DOM

    _getIconLinkElement : function (icon) {
        icon = this.getIcon(icon);
        if (icon == null || icon.imgOnly) return null;
        var elementID = this._getIconLinkId(icon.name);
        return isc.Element.get(elementID);
    },

    _getIconImgElement : function (iconName) {
        var icon = this.getIcon(iconName);
        if (icon == null) {
            if (iconName == this.errorIconName) {
                return isc.Element.get(this.getErrorIconId());
            }
            return null;
        }

        var elementID = this._getIconImgId(icon.name);
        return isc.Element.get(elementID);
    },

    // Helper method - determines whether the some event occurred over one of our icons based
    // on the native target element for the event.

    _getTargetIcon  : function (element) {
        if (!element || !this.icons) return null;

        var itemInfo = isc.DynamicForm._getItemInfoFromElement(element);
        if (!itemInfo || itemInfo.item != this) return null;
        return itemInfo.icon;
    },

    isPickerIcon : function (icon) {
        // return true if the passed icon is the pickerIcon
        if (isc.isAn.Object(icon)) return icon.pickerIcon;
        var pickerIcon = this.getPickerIcon();
        return (pickerIcon && pickerIcon.name == icon);
    },

    // _shouldShowIcon() helper method to evaluate 'showIf' property on form item icons
    _$true:"true",
    _$false:"false",
    _shouldShowIcon : function (icon) {
        // if printing, or if canEdit is false and readOnlyDisplay is "static", show no icon

        if (this._isPrinting() || (this.renderAsStatic() && this.isPickerIcon(icon))) return false;
        if (icon.showIf == null) return true;
        // If specified as a boolean or true/false string, we don't need to build a function, etc
        if (icon.showIf === true || icon.showIf == this._$true) return true;
        if (icon.showIf === false || icon.showIf == this._$false) return false;
        // Note - icons are simple objects and have no stringMethodRegistry, so we must
        // use replaceWithMethod() to convert to a method (if it's currently a string)
        isc.Func.replaceWithMethod(icon, "showIf", "form,item");
        return !!icon.showIf(this.form, this);
    },

    _shouldShowPickerIcon : function () {
        return this.showPickerIcon && this._shouldShowIcon(this.getPickerIcon())
            && !this._isPrinting();
    },


    _writeIconIntoItem : function (icon) {
        if (icon.writeIntoItem) return true;
        return false;
    },

    _mayShowIcons : function () {
        if (!this.showIcons || this.icons == null ||
            (this.showIconsOnFocus && !this.hasFocus)) return false;
        return true;
    },

    // getTotalIconsWidth()
    // Method to return the horizontal drawn space taken up by all this form item's icons.
    // This enables us to size the form's HTML element appropriately.
    getTotalIconsWidth : function () {
        if (!this._mayShowIcons()) return 0;

        var width = 0;
        for (var i = 0; i < this.icons.length; i++) {
            var icon = this.icons[i];
            if (!this._shouldShowIcon(icon) || this._writeIconIntoItem(icon)) continue;

            width += (icon.width != null ? icon.width : this.iconWidth) +
                        (icon.hspace != null ? icon.hspace : this.iconHSpace);
        }
        return width;
    },

    getIconsHeight : function () {
        if (!this._mayShowIcons()) return 0;

        var maxHeight = 0;
        for (var i = 0; i < this.icons.length; i++) {
            var icon = this.icons[i];
            if (!this._shouldShowIcon(icon) || this._writeIconIntoItem(icon)) continue;
            var iconHeight = (icon.height != null ? icon.height : this.iconHeight);
            // If we're writing margins out, the icons will take up more space
            iconHeight += this._getIconVMargin() *2;
            if (iconHeight > maxHeight) maxHeight = iconHeight;
        }

        return maxHeight;
    },

    //>@method setIcons()
    //  Programmatically update the icons for this Form Item.  Will redraw the form item to show
    //  the new icons
    //  @param  icons   (Array) Array of icon definition objects
    //<

    setIcons : function (icons) {
        this.icons = icons;
        this._setUpIcons();
        this.redraw();
    },

    addIcon : function (icon) {
        if (!this.icons) this.icons = [];
        this.icons.add(icon);
        this.setIcons(this.icons);
        return icon;
    },

    getIconByProperty : function (key, value) {
        if (this.icons) return this.icons.find(key, value);
    },

    // enable / disable icons at runtime
    // (Used by 'setDisabled')
    setIconEnabled : function (icon) {

        icon = this.getIcon(icon);
        if (!icon) return;

        // Track the enabled / disabled state on the icon. This avoids us updating the
        // HTML if we don't have to.
        var enabled = !this.iconIsDisabled(icon);
        if (!!icon._disabled != enabled) return;
        if (!enabled) icon._disabled = true;
        else delete icon._disabled;

        if (!this.isDrawn()) return;

        var linkElement = this._getIconLinkElement(icon),
            imgElement = this._getIconImgElement(icon);

        // Enabling / disabling an icon will modify:
        // - tabIndex (can't tab to disabled icons);
        //   - focus altogether
        // - click action should no-op
        // - disabled image should be shown if there is one.
        if (linkElement) {
            // Note - if we did a 'this.setElementTabIndex(-1)' on 'setDisabled(true)' there
            // would be no need for this, as that will also update the tabIndex of icons.
            // However we don't by default because applying the native 'disabled' state to
            // the native HTML form elements will already remove them from the page's tab order.
            if (!enabled) {
                isc.FormItem.setElementTabIndex(linkElement, -1);
                linkElement.style.cursor = "default"
            } else {
                isc.FormItem.setElementTabIndex(linkElement, this._getIconTabIndex(icon))
                linkElement.style.cursor = "";
            }
        }
        if (imgElement) {
            var src = this.getIconURL(icon, null, !enabled);
            isc.Canvas._setImageURL(imgElement, src);
            var iconStyle = this.getIconStyle(icon, null, !enabled);
            if (iconStyle != null) imgElement.className = iconStyle;
        }
    },


    //> @method  formItem.showIcon()
    // This method will show some icon in this item's +link{formItem.icons} array, if it is not
    // already visible. Note that once this method has been called, andy previously specified
    // +link{formItemIcon.showIf} will be discarded.
    // <P>
    // Note that if the form item's showIcons property is set to false, no icons will be displayed
    // for the item. In this case this method will not cause the icon to be displayed.
    //
    // @param icon (String) +link{FormItemIcon.name,Name} of the icon to be shown.
    // @visibility external
    //<

    showIcon : function (icon, focused) {
        // all icons are no longer hidden!
        delete this._allIconsHidden;

        // icon param doc'd as being icon name but support index or raw icon object too.
        if (isc.isA.String(icon) || isc.isA.Number(icon)) icon = this.getIcon(icon);
        if (!isc.isAn.Object(icon)) return;

        // If the icon's ID hasn't been set yet, set it now

        if (icon.name == null) {
            this._setupIconName(icon);
        }

        var currentlyVisible = this._shouldShowIcon(icon);

        // even if the icon is currently visible, set showIf to ensure it is always visible
        // from this point on.
        // For icons written into the form item, the 'getElementHTML()' method should handle
        // this as appropraite.
        icon.showIf = function () {return true;}

        // Only force a redraw / insert into the DOM if the icon wasn't previously visible
        if (!currentlyVisible && this.showIcons && this.containerWidget.isDrawn() &&
            this.isVisible())
        {
            // If the redrawOnShowIcon property is set, simply mark the form for redraw
            // If writeIntoItem is true we have to redraw since we will be changing the HTML
            // of the whole form item.
            if (this.redrawOnShowIcon || icon.writeIntoItem) {
                this.redraw();

            // Otherwise we're going to show/hide the icon without redrawing the whole form
            } else {
                var iconCellElement = isc.Element.get(this.getIconCellID());

                if (iconCellElement != null) {

                    // If no icons are visible just get getIconsHTML to get full HTML, including
                    // an outer table we write out to ensure we don't wrap icons
                    if (iconCellElement.childNodes.length == 0) {


                        // Note that in some cases we write the pickerIcon into the item, in others
                        // we dont, so getIconsHTML() can include the picker.
                        // In this case we're always writing out exactly one icon, so we only want
                        // getIconsHTML() to include the picker HTML if it is the picker icon.
                        iconCellElement.innerHTML = this.getIconsHTML(icon == this.getPickerIcon());

                    } else {
                        var iconHTML = this.getIconHTML(icon, null, this.isDisabled(), focused),
                            // We write icons into separate cells of a table...
                            cellHTML = "<td>" + iconHTML + "</td>",
                            iconTable = iconCellElement.firstChild,
                            index = 0;
                        for (var i = 0; i < this.icons.length; i++) {
                            if (this.icons[i] == icon) break;
                            if (this._shouldShowIcon(this.icons[i])) {
                                index ++;
                            }
                        }
                        if (index == 0) {
                            isc.Element.insertAdjacentHTML(iconTable.rows[0], "afterBegin", cellHTML);
                        } else {
                            isc.Element.insertAdjacentHTML(iconTable.rows[0].cells[index-1], "beforeEnd", cellHTML);
                        }
                    }

                    // Fire _iconVisibilityChanged().  This method will handle resizing the form
                    // item element to accommodate the space taken up by the newly shown icon.
                    this._iconVisibilityChanged();
                    // notify the icon that it has been written into the DOM so we can set u
                    // eventHandlers for it.

                    this._iconDrawn(icon);


                // No icon cell element - must redraw.
                // This could happen if this.icons was null so we didn't write an outer-table
                // at all
                } else {

                    this.logInfo("showIcon(): Unable to dynamically update icon visibility - " +
                                 "redrawing the form", "formItemIcons");
                    return this.redraw();
                }
            }
        }
    },

    //> @method  formItem.hideIcon()
    // This method will hide some icon in this item's +link{formItem.icons} array, if it is
    // currently visible. Note that once this method has been called, andy previously specified
    // +link{formItemIcon.showIf} will be discarded.
    //
    // @param icon (String) +link{FormItemIcon.name,Name} of the icon to be hidden.
    // @visibility external
    //<
    hideIcon : function (icon) {
        if (isc.isA.String(icon) || isc.isA.Number(icon)) icon = this.getIcon(icon);
        if (!isc.isAn.Object(icon)) return;
        var currentlyVisible = this._shouldShowIcon(icon);
        icon.showIf = function () {return false;}

        // Only force a redraw / remove from the DOM if the widget was previously visible
        if (currentlyVisible && this.showIcons && this.containerWidget.isDrawn() &&
            this.isVisible())
        {
            // If the redrawOnShowIcon property is set, simply mark the form for redraw
            if (this.redrawOnShowIcon || icon.writeIntoItem) {
                this.redraw();
            }
            // Otherwise we're going to show/hide the icon without redrawing the whole form
            else {
                var element = icon.imgOnly  ? this._getIconImgElement(icon)
                                            : this._getIconLinkElement(icon);

                if (element == null) {
                    this.logInfo("hideIcon(): Unable to dynamically update icon visibility - " +
                                 "redrawing the form");
                    return this.redraw();
                }

                //this.logWarn("would remove element: " + this.echo(element) +
                //             " from parentNode: " + this.echo(element.parentNode));
                var cell = element.parentNode;
                // sanity check - the external icons are all written into a table - verify
                // that the parent element *is* a td element
                if (cell.tagName != "TD") {
                    isc.Element.clear(element);
                } else {

                    cell.parentNode.removeChild(cell);
                }

                // Fire _iconVisibilityChanged().  This method will handle resizing the form
                // item element to accommodate the space taken up by the newly shown icon.
                this._iconVisibilityChanged();
            }
        }
    },

    // _iconVisibilityChanged()
    // Notification fired when showIcon() or hideIcon() succussfully completes having
    // manipulated the DOM to show/hide an icon.
    // Default implementation will resize the form item element to accommodate the space
    // taken up by it's visible icons.
    // Will not be fired if showIcon() or hideIcon() fell through to form.markForRedraw().
    _iconVisibilityChanged : function () {
        this._resetWidths();
    },

    // showAllIcons / hideAllIcons:
    // Used by 'showIconsOnFocus' / 'hideIconsOnKeypress' behavior.
    showAllIcons : function (focused) {

        if (this._hideAllIconsEvent != null) {
            isc.Timer.clear(this._hideAllIconsEvent);
            delete this._hideAllIconsEvent;
        }
        this._showIcons(this.icons, focused);
    },

    hideAllIcons : function () {
        if (this._hideAllIconsEvent != null) delete this._hideAllIconsEvent;
        this._hideIcons(this.icons);
        this._allIconsHidden = true;
    },

    // _showIcons / _hideIcons -- helper functions to show/hide multiple icons at a time.
    _showIcons : function (icons, focused) {
        if (icons == null || icons.length == 0) return;
        for (var i = 0; i < icons.length; i++) {
            focused = focused && this._iconShouldShowFocused(icons[i], true);
            this.showIcon(icons[i], focused);
        }
    },


    _hideIcons : function (icons) {
        if (icons == null || icons.length == 0) return;

        for (var i = 0; i < icons.length; i++) {
            this.hideIcon(icons[i]);
        }
    },

    //> @method FormItem.getIcon()
    // Given an +link{formItemIcon.name} return a pointer to the icon definition
    // @param name (string) specified +link{formItemIcon.name}
    // @return (FormItemIcon) form item icon matching the specified name
    // @visibility external
    //<
    getIcon : function (name) {
        if (name == null) return;

        var icon;
        if (this.icons) {
            if (isc.isA.Number(name)) {
                return this.icons[name];
            }
            for (var i = 0; i < this.icons.length; i++) {
                // make sure we fire the click action of the appropriate object in the 'icons' array
                if (this.icons[i] == name || this.icons[i].name == name) icon = this.icons[i];
            }
        }
        if (!icon && this.showPickerIcon) {
            if (this.isPickerIcon(name)) icon = this.getPickerIcon();
        }
        if (!icon) {
            this.logInfo("FormItem unable to get pointer to icon with name:"+ name +
                         " - Invalid name, or icons array has been inappropriately modified." +
                         " To update icon[s] for some form item, use the method 'setIcons()'.")
        }
        return icon;
    },


    // _setIconImgState() - an internal method to show the 'over' / 'focused' image for an icon.
    _setIconImgState : function (icon, over, focused) {
        // We should already be showing the disabled image if we're marked as disabled.
        if (this.iconIsDisabled(icon)) return;

        // If we weren't explicitly passed 'focused', look at this.hasFocus
        if (focused == null) focused = this.hasFocus && this._iconShouldShowFocused(icon, true)

        var iconImg = this._getIconImgElement(icon);
        if (iconImg != null) {

            var src = this.getIconURL(icon, over, null, focused);
            isc.Canvas._setImageURL(iconImg, src);
            var iconStyle = this.getIconStyle(icon, over, null, focused);
            if (iconStyle != null) iconImg.className = iconStyle;
        }
    },

    _iconShouldShowOver : function (icon) {
        if (!icon || this.iconIsDisabled(icon)) return false;
        if (icon.showOver != null) return icon.showOver;
        return this.showOverIcons;
    },

    _iconShouldShowFocused : function (icon, itemFocus) {
        if (!icon || this.iconIsDisabled(icon)) return false;
        if (itemFocus && icon.showFocusedWithItem == false) return false;
        if (icon.showFocused != null) return icon.showFocused;
        return this.showFocusedIcons;
    },

    // setIconBackgroundColor() - change the backgroundColor of an icon
    // (used by the colorItem).
    setIconBackgroundColor: function (icon, color) {

        icon.backgroundColor = color;
        var iconImg = this._getIconImgElement(icon);
        if (iconImg != null) {

            try {iconImg.style.backgroundColor = color;} catch (e) {}
        }
    },

    // Picker
    // -----------------------------------------------------------------------------------------

    //> @method formItem.showPicker() (A)
    // Method to show a picker for this item. By default this method is called if the user
    // clicks on a +link{showPickerIcon,pickerIcon}.  May also be called programmatically.
    // <P>
    // Default implementation lazily creates and shows the +link{FormItem.picker,Picker Autochild}.
    // May be overridden to implement some custom picker for this item.
    //
    // @visibility external
    //<


    showPicker : function (modal, icon, pickerProperties, rect) {

        var picker = this.picker;

        pickerProperties = isc.addProperties(pickerProperties || {}, {
            callingForm: this.form,
            callingFormItem: this
        });

        // support being passsed the global ID of a picker
        if (isc.isA.String(picker) && isc.isA.Canvas(window[picker])) {
            picker = this.picker = window[picker];
        }

        // lazily create the picker
        if (!picker) {
            picker = this.picker = this.createPicker(pickerProperties);

            // provide observeable dataChanged function to all pickers
            if (!isc.isA.Function(picker.dataChanged)) {
                picker.dataChanged = new Function ();
            }

            // make sure the picker doesn't drift too far away from the original coordinates or
            // off the screen by resizing (items being added or removed)
            picker.observe(picker, "resized",
                                   "observed.placeNear(observed.lastShowRect)");

            // observe dataChanged

            if (this.pickerDataChanged && picker.dataChanged) {
                this.observe(picker, "dataChanged", "observer.pickerDataChanged(observed)");
            }
        } else {
            isc.addProperties(picker, pickerProperties);
        }
        var pickerID = picker.getID();

        //this.logWarn("showPicker with rect: " + this.echo(rect) +
        //             ", getPickerRect: " + this.echoLeaf(this.getPickerRect) +
        //             ", icon: " + this.echo(icon));

        // if no position was specified, use either the top left of the (if it
        // exists) or the last mouse position
        if (!rect) {
            if (this.getPickerRect) {
                rect = this.getPickerRect();
            } else if (icon) {
                var iconRect = this.getIconPageRect(icon);
                rect = [iconRect[0],iconRect[1]]
            }
            else rect = [isc.EH.getX(), isc.EH.getY()];
        }
        // storing the lastShowRect allows the picker to reposition itself if it resizes
        picker.lastShowRect = rect;

        picker.setRect(rect);
        // draw the picker offscreen to get a size before placing it
        if (!picker.isDrawn()) {
            picker.moveTo(null, -9999);
            picker.draw();
        }
        // use placeNear so we don't get clipped by the window.
        this.picker.placeNear(rect);

        // set the picker data.  A picker advertises its desire to have data set on it by
        // defining a setData method.  If the formItem defines the special getPickerData()
        // function, call that - otherwise call getValue() which works for all formItems
        if (isc.isA.Function(picker.setData)) {
            if (picker._ignorePickerSetData) {
                // flag set by, eg, RelativeDateItem, which has already called setData()
                delete picker._ignorePickerSetData;
            } else {
                if (isc.isA.Function(this.getPickerData)) {
                    picker.setData(this.getPickerData(picker));
                } else picker.setData(this.getValue(picker));
            }
        }

        // show a clickmask.  When the clickmask is clicked notify the picker if it has the
        // clickMaskClicked method defined.  If we're asked to open a modal picker, the picker
        // needs to take care of hiding itself and clearing the clickMask.
        var clickAction = modal ? null : pickerID+".hide()";
        if (modal && isc.isA.Function(picker.clickMaskClicked))
            clickAction = pickerID+".clickMaskClicked()";

        picker.showClickMask(clickAction, !modal, picker);
        if (modal != null && picker.isModal == null) picker.isModal = modal;
        picker.show();
        picker.bringToFront();
        picker.focus();

        // Return false to suppress default click handling from firing
        return false;
    },

    createPicker : function (pickerProperties) {
        return this.createAutoChild("picker", pickerProperties);
    },

    hidePicker : function () {
        if (!this.picker) return;
        this.picker.hideClickMask();
        this.picker.hide();
    },

    // ----------------------------------------------------------------------------------------



    //> @method    formItem.redraw()  (I)
    // Redraw this form item.  Default implementation will notify containerWidget by calling
    // containerWidget.redrawFormItem() (if it's present), otherwise just mark the containerWidget
    // for redraw.
    //<
    // DynamicForm 'redrawFormItem()' simply marks the form for redraw.

    redraw : function (reason) {
        // we can get redraw() attempts during init before we're actually drawn, which have no
        // effect on a DynamicForm, but will affect inline editing by redrawing the grid body
        if (!this.isDrawn()) return;

        // Note - We record whether we had focus before the redraw.
        // This is required because the form will blur the focus item during redraw, and then
        // refocus when redraw is complete.
        // In some cases getInnerHTML() will return different HTML for an item with focus
        // so we need this flag as the item will not have real focus until after redraw is
        // complete.
        // This flag is cleared by DynamicForm._refocusAfterRedraw()


        if (this.hasFocus) this._hadFocusBeforeRedraw = true;
        if (!this.hasFocus && this.items != null) {
            for (var i = 0; i < this.items.length; i++) {
                if (this.items[i].hasFocus) this._hadFocusBeforeRedraw = true;
            }
        }
        if (this.containerWidget.redrawFormItem) {
            this.containerWidget.redrawFormItem(this, reason);
        } else {
            this.containerWidget.markForRedraw("Form item redrawn"+ (reason ? ": " + reason : isc.emptyString));
        }
    },

    // adjustOverflow
    // Called when content changes (which may cause size change)
    // By default calls adjustOverflow on DynamicForm

    adjustOverflow : function (reason) {
        if (!this._adjustOverflowReason) {
            this._adjustOverflowReason = [this.getID(), "  overflow changed: "]
        }
        if (reason == null) this._adjustOverflowReason[2] = "No Reason Specified.";
        else this._adjustOverflowReason[2] = reason;

        if (isc.isA.DynamicForm(this.containerWidget)) {
            // shift canvasItems around if necessary
            this.containerWidget._placeCanvasItems();
            this.containerWidget.adjustOverflow(this._adjustOverflowReason.join(isc.emptyString));
        }

    },

    //> @method    formItem.show()  (I)
    // Show this form item.
    // <BR><BR>
    // This will cause the form to redraw.  If this item had an item.showIf expression, it will
    // be destroyed.
    // @visibility external
    //<
    // If the container widget has a redrawFormItem method we use that. This is currently only
    // implemented on the DynamicForm class where it is used to invalidate cached
    // tableResizePolicy information.
    // If there is no redrawFormItem method we just mark the container widget for redraw

    show : function (preserveShowIf) {
        if (this.visible == true) return;
        this.visible = true;
        if (!preserveShowIf) this.showIf = null;
        if (this.containerWidget.redrawFormItem) this.containerWidget.redrawFormItem(this, "showing form item");
        else this.containerWidget.markForRedraw("showing form item");

        this.visibilityChanged(true);
    },

    //> @method    formItem.hide()  (I)
    // Hide this form item.
    // <BR><BR>
    // This will cause the form to redraw.  If this item had an item.showIf expression, it will
    // be destroyed.
    // @visibility external
    //<
    hide : function (preserveShowIf) {
        if (this.visible == false) return;
        this.visible = false;
        if (!preserveShowIf) this.showIf = null;
        if (this.containerWidget.redrawFormItem) this.containerWidget.redrawFormItem(this, "hiding form item");
        else this.containerWidget.markForRedraw("hiding form item");

        this.visibilityChanged(true);
    },

    //>Safari

    _updateHTMLForPageLoad : function () {
        if (!isc.Browser.isSafari || !this.isDrawn()) return;

        this._resetWidths();
    },

    _resetWidths : function () {
        if (!this.isDrawn()) return;
        var shouldClip = this._getClipValue();

        var outerTable = this.getOuterTableElement();
        if (outerTable) outerTable.style.width = this.getInnerWidth();

        if (this._shouldShowPickerIcon()) {
            var controlTable = this._getControlTableElement();
            if (controlTable) controlTable.style.width = this.getElementWidth();

            var iconDef = this.getPickerIcon(),
                img = this._getIconImgElement(iconDef);
                if (img) {
                    img.style.height = this.getPickerIconHeight();
                    img.style.width = this.getPickerIconWidth();
                }
        }

        var textBoxWidth = this.getTextBoxWidth(),
            widthCSSText = (textBoxWidth == null ? isc.emptyString : textBoxWidth + isc.px),
            textBoxHeight = this.getTextBoxHeight(),
            heightCSSText = (textBoxHeight == null ? isc.emptyString : textBoxHeight + isc.px),
            textBox = this._getTextBoxElement();
        if (textBox) {

            if (shouldClip) textBox.style.width = widthCSSText;
            else textBox.style.minWidth = widthCSSText;
            textBox.style.height = heightCSSText;
        }
        if (this._writeOutFocusProxy()) {
            var focusProxy = this.getFocusElement()
            if (focusProxy) {
                focusProxy.style.width = widthCSSText;
                focusProxy.style.height = heightCSSText;
            }
        }
    },
    //<Safari

    // Element management
    // --------------------------------------------------------------------------------------------


    //>    @method    formItem.hasElement()
    //    Deprecated form of hasDataElement() - kept for backwards compat.
    //        @group    elements
    //        @return    (boolean)        true == item has a form element containing a value for the item
    //      @see    hasDataElement()
    //      @deprecated As of SmartClient 5.5, use +link{formItem.hasDataElement}.
    //<
    hasElement : function () {
        return this.hasDataElement();
    },

    //> @method     formItem.hasDataElement()
    // Does this form item type have an associated form element in the DOM, containing a value?
    // Note - if hasDataElement() returns true, this implies that this data element type
    // has a data element - it doesn't imply that the form is drawn, or that the data element
    // is currently written into the DOM.
    // <P>
    // Use 'getDataElement()' to get a pointer to the data element (will return null if the
    // data element is not found).
    //
    // @group formValues
    // @visibility   internal
    // @see     method:FormItem.getDataElement
    //<

    hasDataElement : function () {
        // Most FormItems either always have or always do not have an element, however we make
        // this a function rather than accessing the _hasDataElement flag directly because
        // subclasses such as the ContainerItem class may do more complicated things which
        // make this method's return value vary.
        if (this.showValueIconOnly) return false;
        return this._hasDataElement;
    },

    //>    @method    formItem.getElement()
    //  Deprecated form of getDataElement() - kept for backwards compatability.
    //        @group    elements
    //
    //        @param    [itemName]     (string)    Item to get the element for.  If null, use this item.
    //        @return    (Element)        DOM element subclass
    //      @deprecated As of SmartClient 5.5, use +link{formItem.getDataElement}.
    //<
    getElement : function (itemName) {
        return this.getDataElement(itemName);
    },

    //> @method formItem.getFocusElement()
    // Returns the HTML element that should receive focus when 'focusInItem()' is called on this
    // form item.
    // Default implementation returns the data element for the form item. May be overridden by
    // subclasses
    //  @group events
    //  @return (Element) DOM element to receive native focus
    //  @visibility internal
    //<
    getFocusElement : function () {
        if (!this.isDrawn() || !this._canFocus()) return null;
        if (this.hasDataElement()) return this.getDataElement();
        if (this._writeOutFocusProxy()) {
            if (!this._focusProxyHandle) {
                // this ID is created by the Canvas-level focusProxy string generation
                this._focusProxyHandle = isc.Element.get(this.getID() + "__focusProxy");
            }
            return this._focusProxyHandle;
        }
        return this._canFocusInTextBox() ? this._getTextBoxElement() : null;
    },

    // _getCurrentFocusElement()
    // Since form items can consist of multiple focusable HTML elements (most commonly an input
    // element and a number of icons (defined as <A> tags), we need a way to determine
    // which DOM element has native focus when formItem.hasFocus is true.
    // In IE we could rely on 'document.activeElement', but there is no equivalent in the
    // other browsers, so instead we hang a flag onto the form item on element focus,
    // (via the 'nativeFocusHandler()' and 'iconFocus()' methods) and clear it on blur (or
    // update on focus to a different element).
    _getCurrentFocusElement : function () {
        if (this.hasFocus == null && !isc.EH._lastFocusTarget == this) {
            return null;
        }
        var element = this._currentFocusElement;
        // double check for IE using the native document.activeElement - should not be
        // necessary

        if (isc.Browser.isIE && element != this.getActiveElement()) {
            this.logInfo("not returning focus element " + this.echoLeaf(element) +
                         " since it's not active" + isc.EH._getActiveElementText(),
                         "nativeFocus");
            if (this.hasFocus) {
                this.hasFocus = false;
                this.elementBlur();
            }
            this._currentFocusElement = null;
            return null;
        }

        return element;
    },

    //>    @method    formItem.getDataElement()
    //      Return a pointer to the form element containing the value for this form item, or
    //      null if it doesn't currently exist.
    //      Will always return null if this form item type does not have an associated data
    //      element which can be determined by formItem.hasDataElement()
    //
    //        @group    elements
    //
    //        @param    [itemName]    (String)    Optional form item name - if passed will return that
    //                                      item's element.  (Item should be a member of the same
    //                                      form)
    //        @return    (Element)        DOM element subclass (or null)
    //<
    getDataElement : function (itemName) {
        // if no itemName was specified, assume they mean us!
        if (itemName == null) {
            var item = this;
        } else {
            // otherwise have the form get a pointer to the item
            var item = this.form.getItem(itemName);
        }

        // If the item does not have a data element, return null.
        if (!item.hasDataElement()) return null;

        // If the item is not marked as drawn() bail.

        if (!this.isDrawn()) return;



        // cache the result of getElementById, cleared in redrawn()/cleared()/destroy()
        var dataElement = this._dataElement;
        if (dataElement == null) {
            dataElement = (this._dataElement = isc.Element.get(this.getDataElementId()));
        }
        return dataElement
    },

    // This method returns a pointer to the outer element of the form item

    getOuterElement : function () {
        if (!this.isDrawn()) return null;

        // If the "includeHint" parameter was passed to getInnerHTML() when we were written out
        // we pass this on to the method determining whether we wrote an outer table.
        var hasHint = this._wroteOutHint;
        if (this._writeOuterTable(hasHint)) {
            return this.getOuterTableElement();
        }
        if (this._shouldShowPickerIcon()) {
            return this._getControlTableElement();
        }
        var element = this._getTextBoxElement();
        // If all else fails (possible due to custom innerHTML) back off to the
        // containing element for the entire item
        if (element == null) {
            element = this.getHandle();
        }
        return element;
    },

    // getHandle() returns a pointer to the element that contains this form item.
    // One of:
    // - form cell
    // - abs div
    // - standalone 'span' element
    getHandle : function () {
        if (!this.isDrawn()) return null;
        if (this._absPos()) return this.getAbsDiv();
        if (this.containerWidget == this.form) return this.getFormCell();
        return isc.Element.get(this._getDOMID(this._$standaloneSpan));
    },

    // pointer to the table around this form item's content
    getOuterTableElement : function () {
        return this._getHTMLPartHandle(this._$outerTable);
    },

    // Which part of the form item did the event occur over?
    _overElement : function (event) {
        if (!event) event = isc.EH.lastEvent;
        var itemInfo = event.itemInfo;
        return (itemInfo && itemInfo.overElement);
    },

    _overTextBox : function (event) {
        if (!event) event = isc.EH.lastEvent;
        var itemInfo = event.itemInfo;

        return (itemInfo && (itemInfo.overTextBox || itemInfo.overElement));
    },

    // control table comprises the text box, picker icon and surrounding table
    _overControlTable : function (event) {
        if (!event) event = isc.EH.lastEvent;
        var itemInfo = event.itemInfo;
        return (itemInfo &&
                (itemInfo.overControlTable || this._overTextBox(event) ||
                 (itemInfo.overIcon && this.getIcon(itemInfo.overIcon) == this.getPickerIcon())
                )
               );
    },

    _$cell:"cell",
    getFormCellID : function () {
        return this._getDOMID(this._$cell);
    },
    getFormCell : function () {
        return isc.Element.get(this.getFormCellID());
    },

    // ValueMaps
    // --------------------------------------------------------------------------------------------

    //>    @method    formItem.getDisplayValue()
    // Returns this item's value with any valueMap applied to it - the value as currently
    // displayed to the user.
    // @param [value] optional stored value to be mapped to a display value.  Default is to
    //                use the form's current value
    // @return (any) value displayed to the user
    // @group valueMap
    // @visibility external
    //<

    getDisplayValue : function (value) {
        var undef;
        if (this.multiple) {
            var useCurrentValue = false;

            if (value === undef) {
                value = this.getValue();
                useCurrentValue = true;
            }

            if (!(value == null || isc.isAn.Array(value))) {
                if (useCurrentValue) {
                    this.logWarn(
                            "getDisplayValue - this is a multiple FormItem but the value obtained " +
                            "from getValue() was not null and was not an array.");
                    value = [value];
                } else {
                    // The form item is `multiple: true` and the caller passed in a value that
                    // is not null and not an array.  Assume that the caller is seeking the
                    // display value for the single value that was passed in.
                    return this.mapValueToDisplay(value);
                }
            }

            if (value != null) {
                var displayValue = [];
                for (var i = 0, len = value.length; i < len; ++i) {
                    displayValue.push(this.mapValueToDisplay(value[i]));
                }
                return displayValue;
            }
        }

        return this.mapValueToDisplay(value !== undef ? value : this.getValue());
    },

    //>    @method    formItem.mapValueToDisplay()
    //  Given a value for this form item, return the value to be displayed.
    //  Default implementation will apply valueMap if there is one.  May be overridden for
    //  other implementations by subclasses
    //    @param  value  (any) value to be mapped to a display value
    //  @return (any)        value to display
    //<

    _$nbsp:"&nbsp;",
    mapValueToDisplay : function (value) {

        // escapeHTML is not doc'd at the formItem level. It doesn't make sense for
        // all form item types, such as those with a native HTML input element, so will
        // be enabled via a flag where we need it.
        var asHTML = this.canEscapeHTML &&
                    // outputAsHTML / asHTML are old and deprecated
                    (this.escapeHTML || this.outputAsHTML || this.asHTML);

        var displayValue;

        if (this.multiple && isc.isAn.Array(value)) {
            // For multi-select items, value might be an array of selected values.
            // Display them as a list of display values, separated by the multipleValueSeparator.
            displayValue = "";
            for (var i = 0, length = value.length; i < length; i++) {
                var key = value[i],
                    valueIconHTML = this._getValueIconHTML(key);

                if (valueIconHTML != null && length > 1) {
                    displayValue += valueIconHTML;
                }

                displayValue += this.mapValueToDisplay(key);

                if (i != length - 1) {
                    displayValue += this.multipleValueSeparator;
                }
            }
        } else {
            displayValue = this._mapKey(value, true);

            var displayFieldName = this.getDisplayFieldName();
            if (displayFieldName != null) {
                var ods = this.getOptionDataSource();

                // Check whether the displayField has escapeHTML:true.
                var displayField = (ods == null ? null : ods.getField(displayFieldName));
                if (displayField != null && this.canEscapeHTML && displayField.escapeHTML) {
                    asHTML = true;
                }

                if (displayValue == null) {
                    // Try looking in the option data source's cache data.
                    var odsCacheData = (ods == null ? null : ods.getCacheData());
                    if (odsCacheData != null) {
                        var optionRecord = odsCacheData.find(this.getValueFieldName(), value);
                        if (optionRecord != null) displayValue = optionRecord[displayFieldName];
                    }
                }
            }
            displayValue = this._formatDataType(displayValue != null ? displayValue : value);

            // Don't escape &nbsp; unless that's actually the data value!

            if (asHTML && (value == null || value == isc.emptyString) &&
                (displayValue == this._$nbsp || displayValue == this.emptyDisplayValue))
            {
                asHTML = false;
            }
            if (asHTML) {
                displayValue = (displayValue == null ? null : String(displayValue).asHTML());
            }
        }
        return displayValue;
    },
    canEscapeHTML:false,

    //> @method formItem.formatValue()
    // Allows customization of how the FormItem's stored value is formatted for display.
    // <p>
    // By default, this formatter will only be applied to static displays such
    // as +link{StaticTextItem} or +link{SelectItem}, and does not apply to values
    // displayed in a freely editable text entry field
    // (such as a +link{TextItem} or +link{TextAreaItem}).
    // <p>
    // To define formatting logic for editable text, developers may:
    // <ul>
    // <li>set +link{textItem.formatOnBlur} to true, which causes the static formatter
    // to be applied while the item does not have focus, and then be cleared when the user
    // moves focus to the text field</li>
    // <li>use +link{formatEditorValue} and supply a
    // corresponding +link{parseEditorValue} that can convert a formatted and subsequently
    // user-edited value back to a stored value.</li>
    //
    // @param value (any) Underlying data value to format. May be null.
    // @param record (ListGridRecord) The record currently being edited by this form.
    //      Essentially the form's current values object.
    // @param form (DynamicForm) pointer to the DynamicForm
    // @param item (FormItem) pointer to the FormItem
    // @return (string) Display value to show.
    //
    // @example formatRelatedValue
    // @visibility external
    //<

    //> @method formItem.formatEditorValue()
    // Allows customization of how the FormItem's stored value is formatted for display
    // in an editable text entry area, such as a +link{TextItem} +link{TextAreaItem}.  For
    // display values which will not be directly editable by the user, use
    // +link{formItem.formatValue()} instead.
    // <p>
    // When customizing how values are displayed during editing, it is almost always necessary
    // to provide a +link{formItem.parseEditorValue()} as well, in order to convert a formatted
    // and subsequently user-edited value back to a stored value.
    //
    // @param value (any) Underlying data value to format. May be null.
    // @param record (ListGridRecord) The record currently being edited by this form.
    //      Essentially the form's current values object.
    // @param form (DynamicForm) pointer to the DynamicForm
    // @param item (FormItem) pointer to the FormItem
    // @return (string) display value to show in the editor.
    //
    // @visibility external
    //<

    //> @method formItem.parseEditorValue()
    // Allows customization of how a used-entered text value is converted to the FormItem's
    // logical stored value (the value available from +link{getValue()}).
    // <p>
    // This method only applies to form items which show an editable text entry area, such at
    // the +link{TextItem} or +link{TextAreaItem}.
    // <p>
    // See also +link{formItem.formatEditorValue()}
    //
    // @param value (string) value as entered by the user
    // @param form (DynamicForm) pointer to the dynamicForm containing this item
    // @param item (FormItem) pointer to this item
    // @return (any) Data value to store for this item.
    //
    // @visibility external
    //<

    //> @method formItem.formValuesChanged()
    // Notification that fires when the parent form's values are changed by a
    // +link{ValuesManager}.
    // @visibility internal
    //<

    // should we apply static formatters to the display value
    // We typically want to do this for readOnly fields.
    // For fields with a freeform text entry area (text / textArea) we don't want to use the
    // static formatter -- instead we'll use the special "edit value" formatters which should
    // have a corollary parser method.
    // Note that for some formItems, such as LinkItem, this is flipped based on the
    // 'canEdit' setting for the item
    // If formatOnBlur is true, we always apply the static format while the item is unfocussed
    // This gives developers an easy way to specify a formatter without needing a
    // corollary parser method.
    applyStaticTypeFormat:true,
    shouldApplyStaticTypeFormat : function () {
        if (this.applyStaticTypeFormat) return true;
        if (this.formatOnBlur) {
            var hasFocus = this.hasFocus;
            // If we're blurred, apply the staticTypeFormat
            return !hasFocus;
        }
        return false;
    },

    // If we have a non-string value, use the appropriate formatter to display it as a string.

    _formatDataType : function (value, applyStaticTypeFormat) {
        if (applyStaticTypeFormat == null) {
            applyStaticTypeFormat = this.shouldApplyStaticTypeFormat();
        }
        if (applyStaticTypeFormat) {
            if (this.formatValue != null) {

                var form = this.form,
                    record = this.form ? this.form.values : {};
                return this.formatValue(value,record,form,this);

            }
        } else if (this.formatEditorValue != null) {
            var form = this.form,
                record = this.form ? this.form.values : {};
            return this.formatEditorValue(value,record,form,this);

        } else if (this._editFormatter != null) {

            var form = this.form,
                record = this.form ? this.form.values : {};
            // if we have a _simpleType - editFormatter was presumably derived from it.
            // However don't crash if _simpleType.editFormatter is undefined.

            if (this._simpleType && this._simpleType.editFormatter) {
                return this._simpleType.editFormatter(value, this, form, record);
            } else {
                return this._editFormatter(value, this, form, record);
            }
        }

        // If the value is a native Date object format it according to the following rules:
        // - if this.dateFormatter or this.timeFormatter is specified, respect it (If both are
        //   specified, favor 'dateFormatter' unless field is explicitly of type "time")
        // - if this.displayFormat is specified respect it as either a dateFormatter or timeFormatter
        //   depending on specified field type.
        // - otherwise check for form.timeFormatter for time fields, form.datetimeFormatter for
        //   datetime fields, or form.dateFormatter for all other field types.
        if (isc.isA.Date(value)) {
            if (this._formatAsTime()) {
                var formatter = this._getTimeFormatter();
                var isLogicalTime = isc.SimpleType.inheritsFrom(this.getType(), "time");
                return isc.Time.toTime(value, formatter, isLogicalTime);
            } else {

                var formatter = this._getDateFormatter();
                var type = this.getType(),
                    dateField = isc.SimpleType.inheritsFrom(type, "date"),
                    datetimeField = isc.SimpleType.inheritsFrom(type, "datetime");
                // Logical date fields -- always use short format (time is meaningless) and pass
                // in the "logicalDate" parameter so we ignore any custom timezone.
                if (dateField && !datetimeField) {
                    return value.toShortDate(formatter, false);

                // Otherwise, if showing short format, use toShortDate or toShortDatetime for
                // explicit datetime fields -- or for long format use default "normal" formatter.

                } else {
                    if (this.useShortDateFormat) {
                        return datetimeField ? value.toShortDatetime(formatter, true)
                                            : value.toShortDate(formatter, true);
                    } else {
                        return value.toNormalDate(formatter);
                    }
                }
            }
        }

        // _normalDisplayFormatter and _shortDisplayFormatter is picked up from SimpleType
        // logic.

        //
        // Note: if the simpleType with the formatter exists on the field, use that in
        // preference to the method patched from the simpleType because use of 'this' within
        // said formatter then correctly references the simpleType and not the field.
        if (this._simpleType && isc.isA.Function(this._simpleType.normalDisplayFormatter) &&
            applyStaticTypeFormat)
        {

            value = this._simpleType.normalDisplayFormatter(value, this, this.form, this.form.values);

        } else if (this._normalDisplayFormatter && applyStaticTypeFormat) {

            value = this._normalDisplayFormatter(value, this, this.form, this.form.values);

        } else if (value != null) {

            value = isc.iscToLocaleString(value);
            // map "" to our 'emptyDisplayValue' - allows subclasses such as selectItems
            // to style the content properly by writing out "&nbsp;" rather than ""
            if (this.emptyDisplayValue != isc.emptyString && value == isc.emptyString)
                value = this.emptyDisplayValue;

        }
        if (value == null) value = this.emptyDisplayValue;
        return value;
    },

    // Helper methods to determine how to format date values.
    _formatAsTime : function () {
        var type = this.getType(),
            isTime = isc.SimpleType.inheritsFrom(type, "time"),
            formatAsTime = isTime;

        // at the item level, the presence of timeFormatter but no date, or vice-versa
        // implies we should use the formatter regardless of type.
        if (this.timeFormatter == null && this.dateFormatter != null) formatAsTime = false;
        if (this.dateFormatter == null && this.timeFormatter != null) formatAsTime = true;
        // If neither are set, rely on type inheriting from time - anything will format as a date.
        return formatAsTime;
    },
    _getDateFormatter : function () {

        if (this.dateFormatter != null) return this.dateFormatter;
        var type = this.getType(),
            isDate = isc.SimpleType.inheritsFrom(type, "date"),
            isDatetime = isc.SimpleType.inheritsFrom(type, "datetime");

        // 'displayFormat' may also be specified as either a date or time formatter
        // this expects the field to have a specified "type".

        if (isDate && this.displayFormat != null) return this.displayFormat;

        if (isDatetime && this.form.datetimeFormatter != null) return this.form.datetimeFormatter;
        return this.form.dateFormatter;
    },
    _getTimeFormatter : function () {
        if (this.timeFormatter != null) return this.timeFormatter;
        // 'displayFormat' may also be specified as either a date or time formatter
        // this expects the field to have a specified "type".

        if (this.displayFormat != null && isc.SimpleType.inheritsFrom(this.type, "time")) {
            return this.displayFormat;
        }
        return this.form.timeFormatter;
    },


    //>    @method    formItem.mapDisplayToValue()
    //  Converts a display value for this item to a value to be saved out.
    //  Default implementation will map backwards based on the valueMap specified if there is
    //  one.
    //    @param  value  (any) display value
    //  @return (any)        value re-mapped for storing
    //<


    mapDisplayToValue : function (value) {
        value = this._parseDisplayValue(value);
        return this._unmapKey(value);
    },

    _parseDisplayValue : function (value) {
        var applyStaticTypeFormat = this.shouldApplyStaticTypeFormat();
        if (!applyStaticTypeFormat) {
            if (this.parseEditorValue != null) {
                value = this.parseEditorValue(value, this.form, this);
            } else if (this._parseInput != null) {
                var form = this.form,
                    record = form ? form.values : {};
                // fire it in the scope of the simpleType
                if (this._simpleType && this._simpleType.parseInput) {
                    value = this._simpleType.parseInput(value, this, form, record);
                } else {
                    value = this._parseInput(value, this, form, record);
                }
            }
            // Handle parsing values to Dates for fields of type "date"
            // This is rarely going to be required but would handle something special like
            // the developer showing a date type field with 'editorType' explicitly set to
            // "TextItem"

            if (value != null && isc.isA.String(value)) {
                var type = this.getType();
                var isDate = isc.SimpleType.inheritsFrom(type,"date"),
                    isTime = isc.SimpleType.inheritsFrom(type,"time"),
                    isDatetime = isDate && isc.SimpleType.inheritsFrom(type, "datetime"),
                    isEmptyString = (value == "")
                ;

                if (isDate || isTime) {
                    if (this._formatAsTime()) {
                        if (isEmptyString && this.allowEmptyValue) {
                            // handle empty time-values
                            value = null;
                        } else {

                            var baseDate;
                            if (!isTime && isc.isA.Date(this._value)) {
                                baseDate = this._value;
                            }
                            var timeVal = isc.Time.parseInput(value, false, false, !isTime, baseDate);
                            if (isc.isA.Date(timeVal)) value = timeVal;
                        }
                    } else {
                        var inputFormat = this.inputFormat;
                        if (inputFormat == null) {
                            inputFormat = Date.mapDisplayFormatToInputFormat(this._getDateFormatter());
                        }
                        var logicalDate = isDate && !isDatetime;
                        var dateVal = Date.parseInput(value, inputFormat, this.centuryThreshold,
                                        false, !logicalDate);
                        if (isc.isA.Date(dateVal)) value = dateVal;
                    }
                }
            }
        }
        return value;

    },

    // getType() - returns the specified 'type' for this item
    // If this.criteriaField is specified, type will be picked up from that field

    getType : function () {
        if (this.type != null) return this.type;
        if (this.criteriaField && this.form && this.form.dataSource) {
            var ds = isc.DataSource.get(this.form.dataSource);
            var criteriaField = ds.getField(this.criteriaField);
            if (criteriaField) return criteriaField.type;
        }
        return null;
    },

    // Helper to set the time on a date to zero for a datetime

    setToZeroTime : function (date) {
        Date.setToZeroTime(date);
    },

    //>    @method    formItem._mapKey() (A)
    // Map a key value through the item.valueMap, if defined,
    // to return the display value that we should show to the user.
    // By default returns the key if no mapping was found. 2nd parameter allows the developer
    // to suppress this behavior, and return null if no mapping was found.
    //<
    _mapKey : function (key, dontReturnKey) {
        // assert !isc.isAn.Array(key)

        var defaultValue = dontReturnKey ? null : key;

        var map = this.getValueMap();
        if (!map) return defaultValue;
        if (isc.isA.String(map)) map = this.getGlobalReference(map);

        // If it's an array, just return the key.  It's either in the array or not - no need
        // to transform.
        if (isc.isAn.Array(map)) return defaultValue;

        return isc.getValueForKey(key, map, defaultValue);
    },

    //>    @method    formItem._unmapKey() (A)
    //        Map a display value through the item.valueMap, if defined,
    //        to return the key value used internally.
    //<
    _unmapKey : function (value) {
//JMD: handle null value in isc.getKeyForValue instead?
        var map = this.getValueMap();
        if (!map) return value;
        if (isc.isA.String(map)) map = this.getGlobalReference(map);

        // if it's an array, just return the value, it's either in the array or not - no need
        // to transform.
        if (isc.isAn.Array(map)) return value;

        var result = isc.getKeyForValue(value, map);
        // if getKeyForValue returns the same value it was passed, and that happens to also
        // be the emptyDisplayValue for this item, don't allow the emptyDisplayValue to be
        // promoted to the internal value
        if (result == value && result == this.emptyDisplayValue) {
            result = "";

            var valueField = this.getValueFieldName();
            if (valueField != null) {
                // Try looking in the option data source's cache data.
                var ods = this.getOptionDataSource();
                var odsCacheData = (ods == null ? null : ods.getCacheData());
                if (odsCacheData != null) {
                    var optionRecord = odsCacheData.find(this.getDisplayFieldName(), value);
                    if (optionRecord != null) result = optionRecord[valueField];
                }
            }
        }
        return result;
    },

    //>    @method    formItem.setValueMap()    (A)
    // Set the valueMap for this item.
    // @group    valueMap
    // @param    valueMap (Array or Object) new valueMap
    // @see attr:valueMap
    // @visibility external
    //<
    setValueMap : function (valueMap) {
        this.valueMap = valueMap;

        this.updateValueMap();
    },

    //> @method formItem.setOptionDataSource() [A]
    // Method to set the +link{formItem.optionDataSource} at runtime
    // @param dataSource (DataSource) new optionDatasource
    // @visibility external
    //<
    setOptionDataSource : function (dataSource) {
        if (isc.isA.String(dataSource)) dataSource = isc.DataSource.get(dataSource);
        if (this.getOptionDataSource() == dataSource) {
            return;
        }
        this.ignoreOptionDataSource();
        this.optionDataSource = dataSource;
        // This in turn calls updateValueMap
        this.invalidateDisplayValueCache();
    },

    //> @method formItem.setValueIcons ()
    // Set the valueIcons for this item
    // @param map (object) mapping of logical values for this item to icon src URLs
    // @group valueIcons
    // @visibility external
    //<
    setValueIcons : function (map) {
        this.valueIcons = map;
        if (this.isDrawn()) this.redraw();
    },

    //>    @method    formItem.setOptions()    (A)
    // Set the options for this item (a select or a radioGroup, etc.).  Synonymous with
    // setValueMap().
    //        @group    valueMap
    //        @param    valueMap (Array or Object) new valueMap
    //<
    setOptions : function (valueMap) {
        return this.setValueMap(valueMap);
    },

    //> @method formItem.updateValueMap()
    // Helper method fired whenever the valueMap is modified.
    // Will refresh the displayed value if appropriate.
    // @param   refreshDisplay  (boolean)   Can be passed to explicitly indicate that the new
    //                                      valueMap effects the currently displayed value so
    //                                      a refresh is required, or vice versa. If not passed
    //                                      we always refresh.
    //<
    updateValueMap : function (refreshDisplay) {
        if (refreshDisplay != false && !this._showingInFieldHint) {
            this._setElementValue(this.getDisplayValue());
        }
        if (this.hasElement()) this.setElementValueMap(this.getValueMap());
    },

    //>    @method    formItem.setElementValueMap()    (A)
    // Set the valueMap in the form representation for this object.<p>
    //
    // Default implementation does nothing -- override in a subclass to actually manipulate the
    // form.
    //        @group    valueMap
    //        @param    valueMap (Array or Object) new valueMap
    //<
    setElementValueMap : function (valueMap) {
        // no default implementation
    },

    //>    @method    formItem.getValueMap()    (A)
    // Internal method to compute the actual valueMap from the author-specified valueMap and
    // other properties.
    //        @group    valueMap
    //        @return    (Object) the valueMap
    //<
    getValueMap : function () {

        // get the valueMap from the item
        var valueMap = this.valueMap;

        // if valueMap are specified as a string, treat it as a global reference to the actual
        // list
        if (isc.isA.String(valueMap)) {
            valueMap = this.getGlobalReference(valueMap);
        }

        // for FormItems with displayFields, this._displayFieldValueMap is a special map between
        // data field values and display field values in the items' optionDataSource.
        // Set up in 2 ways:
        // - if the optionDataSource matches the dataSource for the form, this is picked up
        //   from a call to setValues() on the form as a whole (EG editing records)
        // - if the value for the item is set to an unrecognized value as part of
        //   item.setValue(), mapValueToDisplay will perform an explicit fetch against the
        //   dataSource to retrieve the displayValue for the value passed in.
        // Combine this special map with the explicitly specified valueMap.
        var displayMap = this._displayFieldValueMap;
        if (displayMap != null) {
            if (valueMap == null) valueMap = displayMap;

            else {
                // if the explicit map is an array, convert it to an object
                if (isc.isAn.Array(valueMap)) {
                    var explicitMap = valueMap;
                    valueMap = {};
                    for (var i = 0; i < explicitMap.length; i++) {
                        valueMap[explicitMap[i]] = explicitMap[i];
                    }
                }
                // Add entries for the special displayFieldValueMap
                // Note that the explicitly specified entries should take precedence
                valueMap = isc.addProperties({}, valueMap);
                var undef;
                for (var prop in displayMap) {
                    if (valueMap[prop] === undef) valueMap[prop] = displayMap[prop];
                }
            }
        }

        return valueMap;
    },

    //> @method FormItem.getValueFieldName()
    // Getter method to retrieve the +link{FormItem.valueField} for this item.
    // If unset, default behavior will return the +link{FormItem.name} of this field.
    // @group display_values
    // @return (string) fieldName to use a "value field" in records from this items
    //              +link{FormItem.optionDataSource}
    // @visibility external
    //<

    getValueFieldName : function () {
        if (this.valueField) return this.valueField;

        if (this.form && this.form.dataSource && this.foreignKey)
            return isc.DS.getForeignFieldName(this, this.form.dataSource);

        var fieldName = this.getFieldName();




        return fieldName || "name";
    },

    //> @method   FormItem.getDisplayFieldName()
    // Returns the <code>displayField</code> for this item. This will typically be
    // specified explicitly via the +link{formItem.displayField} attribute. However, if
    // that property is unset, and the +link{formItem.valueField} for this item is
    // hidden in the +link{formItem.optionDataSource}, this method
    // will return the title field for the <code>optionDataSource</code>.
    //
    // @return (String) display field name, or null if there is no separate display field to use.
    // @visibility external
    //<
    getDisplayFieldName : function () {
        if (this.displayField) return this.displayField;
        var optionDataSource = this.getOptionDataSource();

        var valueFieldName = this.getValueFieldName();

        if (optionDataSource &&
            optionDataSource != isc.DataSource.getDataSource(this.form.dataSource) &&

            optionDataSource.getField(valueFieldName) &&
            optionDataSource.getField(valueFieldName).hidden == true) {
                return optionDataSource.getTitleField();
        }
    },


    // If this item has a specified displayField, and no specified optionDataSource
    // we can pick up the display value for the field from the displayField value of the form's
    // values object
    // (This is based on the assumption that we are editing a 'record' - similar behavior
    // to the ListGrid).
    // See DynamicForm._useDisplayFieldValueFromRecord()

    _displayFieldValueFromFormValues : function () {
        // for items with an option dataSource and a specified displayField, display the
        // form's displayField value by default

        if (this.displayField != null) {
            var vals = this.form.getValues(),
                dataVal = vals[this.getFieldName()],
                displayVal = vals[this.displayField];
            if (displayVal != null) {
                var valueMap = {};
                valueMap[dataVal] = displayVal;
            }
            this._displayFieldValueMap = valueMap;
        }
    },

    //>    @method    formItem.getOptions()    (A)
    // Return the valueMap for this item.  Synonymous with getValueMap()
    //        @group    valueMap
    //
    //        @return    (Object) the valueMap
    //<
    getOptions : function () {
        return this.getValueMap()
    },


    //> @method FormItem.getOptionDataSource()
    // Returns the +link{FormItem.optionDataSource} for this item.
    // <p>
    // Always uses <code>item.optionDataSource</code> if specified.  Otherwise, if
    // +link{dataSourceField.foreignKey} was specified, uses the target DataSource.  Otherwise,
    // uses the DataSource of this item's form (if one is configured).
    //
    // @return (DataSource) the optionDataSource, or null if none is configured
    // @group display_values
    // @visibility external
    //<
    getOptionDataSource : function () {
        var ods = this.optionDataSource;

        if (ods == null) {
            var formDS = this.form ? this.form.dataSource : null;

            // use foreignKey if specified
            if (this.foreignKey) ods = isc.DS.getForeignDSName(this, formDS);

            // otherwise fall back to DataSource for form as a whole
            if (ods == null && formDS) ods = formDS;
        }
        // convert identifiers to an actual datasource object
        if (isc.isA.String(ods)) ods = isc.DataSource.getDataSource(ods);

        return ods;
    },

    //>    @method    formItem.getValueMapTitle()    (A)
    // Return the title associated with a particular value
    //        @group    valueMap
    //        @return    (string)    title of the option in question
    //<
    getValueMapTitle : function (value) {
        var valueMap = this.getValueMap();
        // return the value as the title if it exists in the valueMap array
        if (isc.isAn.Array(valueMap)) return (valueMap.contains(value) ? value : "");
        return valueMap[value];
    },

    // --------------------------------------------------------------------------------------------

    //>    @method    formItem.saveValue()
    // Store a value for this form item internally, and at the form level.<br>
    // This method will update our internal "_value" property and the value stored in the form's
    // "values" array.
    // It is used in 'setValue()', and in  'elementChanged()', and 'handleKeyPress()' to ensure the
    // stored values for this item reflect the value displayed in this form item's element.
    //      @visibility internal
    //        @group formValues
    //
    // @param    value     (any)                value to save for this item
    // @param [isDefault] (boolean) Indicates that this value was derived from the default
    //  value for this item (allowing us to re eval dynamic defaults in setItemValues())
    //<
    saveValue : function (value, isDefault) {

        //this.logWarn("saving value: " + value + this.getStackTrace());

        var undef;
        this._value = value;
        // set or clear the flag indicating whether this is a default value.
        this._setToDefault = isDefault;

        // This value is going to be saved on the form itself under form.values.
        // If we have a hidden data element (for direct submission), update it now so that
        // when the form gets submitted the element value is present.
        if (this.isDrawn()) {
            if (this._useHiddenDataElement()) this._setHiddenDataElementValue(value);
        }

        if (this.form == null) return;

        if (value == undef && this._clearingValue) {
            this.form.clearItemValue(this);
        } else {
            this.form.saveItemValue(this, value);
        }
    },

    // If we're using a hidden data element, this method will set its value, so when the form
    // is natively submitted the value is available to the server.
    _setHiddenDataElementValue : function (value) {
        var hde = this._getHiddenDataElement();
        if (hde) hde.value = value;
    },

    //>    @method    formItem.setValue()
    // Set the value of the form item to the value passed in
    // <p>
    // NOTE: for valueMap'd items, newValue should be data value not displayed value
    // @visibility external
    // @param    newValue     (any)                value to set the element to
    //<
    // @param   [allowNullValue]   (boolean) Internal parameter to avoid setting to default when
    // passed a null value. Used when redrawing a form item that the user has explicitly set
    // to null as opposed to a call to 'setValue(null)' which will reset to default.
    _$smart:"smart",
    setValue : function (newValue, allowNullValue, timeCritical, dontResetCursor) {


        this._setValueCalled = true;

        // If we have focus, remember the selection so we can retain the cursor insertion point
        // - useful for the case where this is a simple data transform, such as case-shifting
        var resetCursor = !dontResetCursor && (this.maintainSelectionOnTransform && this.hasFocus &&
                           (this._getAutoCompleteSetting() != this._$smart));


        if (resetCursor && isc.Browser.isIE) {
            if (!this._hasNativeFocus()) {
                resetCursor = false;
            }
        }

        if (resetCursor) this.rememberSelection(timeCritical);

        // since we're being set to an explicit value, cancel delayed save on keyPress
        if (this._pendingUpdate != null) {
            isc.Timer.clearTimeout(this._pendingUpdate);
            this._pendingUpdate = null;
        }

        // use the default value if necessary.

        var isDefault;

        if (newValue == null && !allowNullValue) {
            var defaultVal = this.getDefaultValue();
            // don't apply the default value if it's not set - this allows for the distinction
            // between setting the value to 'null' vs 'undefined'
            if (defaultVal != null) {
                isDefault = true;
                newValue = defaultVal;
            }
        }
        // If the form item is `multiple` then the value of the form must be an array.
        if (this.multiple && newValue != null && !isc.isAn.Array(newValue)) {
            newValue = [newValue];
        }
        // truncate newValue to the length of the field, if specified
        if (this.length != null && newValue != null && isc.isA.String(newValue) &&
            newValue.length > this.length)
        {
            newValue = newValue.substring(0, this.length);
        }
        // saveValue will store the value as this._value, and will save the value in the form
        // if this.shouldSaveValue is true
        this.saveValue(newValue, isDefault);

        // shouldFetchMissingValue() tests for whether we should fetch values at all
        // (option dataSource, fetchMissingValues etc) and whether we already have the
        // value cached.
        if (newValue != null) {
            if (this.multiple) {
                // assert isc.isAn.Array(newValue) // enforced above
                var shouldFetchValues = [];
                for (var i = 0, len = newValue.length; i < len; ++i) {
                    var val = newValue[i];
                    if (val != null && this.shouldFetchMissingValue(val)) {
                        shouldFetchValues.push(val);
                    }
                }
                this._clearSelectedRecord();
                this._checkForDisplayFieldValue(shouldFetchValues);
            } else if (this.shouldFetchMissingValue(newValue)) {
                // _checkForDisplayFieldValue() will kick off a fetch (Unless we're already pending a
                // response for this value).
                // Drop the current selected record - it's invalid right now and will be
                // repopulated when the fetch completes
                this._clearSelectedRecord();
                this._checkForDisplayFieldValue(newValue);
            }
        } else {
            // update the selected record from cache unless we already have it set up correctly.
            if (this._selectedRecordValue == null ||
                !this.compareValues(this._selectedRecordValue, this._value))
            {
                this._updateSelectedRecord();
            }
        }

        // map the value passed to the visible value as necessary
        var displayValue = this.getDisplayValue(newValue);

        // set the value of the item

        this._setElementValue(displayValue, newValue);

        // On simple data transforms (currently case shifting only), we will retain the
        // cursor positon across setValue() calls if the item has focus
        if (resetCursor) this.resetToLastSelection(true);

        return newValue
    },

    //>@method formItem.shouldFetchMissingValue()
    // If this field has a specified +link{optionDataSource}, should we perform a fetch against
    // that dataSource to find the record that matches this field's value?
    // <P>
    // If the value is non-null, this method is called when the item is first rendered
    // or whenever the value is changed via a call to +link{setValue()}. If it returns
    // true, a fetch will be dispatched against the optionDataSource to get the record
    // matching the value
    // <P>
    // When the fetch completes, if a record was found that matches the
    // data value (and the form item value has not subsequently changed again),
    // the item will be re-rendered to reflect any changes to the display value,
    // and the record matching the value
    // will be available via +link{this.getSelectedRecord()}.
    // <P>
    // Default behavior will return false if +link{this.fetchMissingValues} is
    // set to false. Otherwise it will return true if +link{this.alwaysFetchMissingValues} is
    // set to true, or if a +link{displayField} is specified for this item and the item
    // value is not already present in the item's valueMap.
    //
    // @param newValue (any) The new data value of the item.
    // @return (Boolean) should we fetch the record matching the new value from the
    //   item's optionDataSource?
    // @visibility external
    //<
    shouldFetchMissingValue : function (newValue) {
        if (this.fetchMissingValues == false) return false;
        if (this.getOptionDataSource() == null) return false;
        // If we already saw this data value and performed a fetch against it, don't kick off another
        // fetch even if alwaysFetchValues is true.
        var inCache = false;
        if (this._displayFieldCache != null &&
            // _gotAllOptions basically indicates that filterLocally was true when we
            // populated our cache, so even if we can't find the record we don't need to
            // fetch again.
            (this._gotAllOptions ||
            this._displayFieldCache.find(this.getValueFieldName(), newValue) != null))
        {
            inCache = true;
        }
        if (inCache) return false;
        // Fetch missing value if the flag to always fetch is true, or if we have
        // a displayField specified and don't have the value explicitly in our valueMap already.
        if (this.alwaysFetchMissingValues) return true;

        // return true if we have a displayField set and we don't have the
        // value in our valueMap
        if (this.getDisplayFieldName() == null) return false;
        var inValueMap = (this._mapKey(newValue, true) != null);
        return !inValueMap;
    },

    // used by Visual ISC only
    setDefaultValue : function (newValue) {
        var prevDefaultValue = this.defaultValue, undef;
        this.defaultValue = newValue;
        if (this.isSetToDefaultValue() || (this._value == null && prevDefaultValue === undef))
            this.clearValue();
    },

    _checkForDisplayFieldValue : function (newValue) {
        if (!this._fetchingMissingValues) this._fetchingMissingValues = {};

        // Flag to indicate we're currently getting this missing value from the server
        // so we don't kick off another fetch for the same value.
        // This will be cleared when we get the display value back (at which point the
        // display value will show up in the result of this.getValueMap())
        var needFetch = false;
        if (isc.isAn.Array(newValue)) {
            for (var i = 0, len = newValue.length; i < len; ++i) {
                var val = newValue[i];
                if (!this._fetchingMissingValues[val]) {
                    this._fetchingMissingValues[val] = needFetch = true;
                }
            }
        } else if (!this._fetchingMissingValues[newValue]) {
            this._fetchingMissingValues[newValue] = needFetch = true;
        }

        if (needFetch) {

            // when deriving a valueMap from a DataSource, respect optionCriteria,
            // optionFetchContext etc as we do in ListGrid fields and PickList based items
            var recordCrit = isc.addProperties({}, this.optionCriteria);
            if (!this.filterLocally) {
                var valueCriterion = {};
                valueCriterion[this.getValueFieldName()] = newValue;
                recordCrit = isc.DataSource.combineCriteria(recordCrit, valueCriterion);
            }

            var context = isc.addProperties(
                {},
                this.optionFilterContext,
                {showPrompt:false,
                 internalClientContext:{dataValue:newValue, filterLocally:this.filterLocally},
                 componentId:this.containerWidget.getID(),
                 componentContext:this.getFieldName() }
            );

            var undef;
            if (this.optionOperationId !== undef) {
                context.operationId = this.optionOperationId;
            }
            this.getOptionDataSource().fetchData(
                recordCrit,
                {
                    target:this,
                    methodName:"fetchMissingValueReply"
                },
                context
            );
        }
    },

    // Callback method fired when the server returns with the display value from
    // our optionDataSource.
    // Fold this new value into our valueMap, and if necessary refresh to display it.
    fetchMissingValueReply : function (response, data, request) {

        // If we fetched all the values in the data-set, use array.find to find the appropriate
        // one
        var dataVal = response.internalClientContext.dataValue,
            // Look at filterLocally as it was set when the fetch was initialized as that
            // governs what the criteria were - could have been subsequently changed.
            filterLocally = response.internalClientContext.filterLocally,
            displayField = this.getDisplayFieldName(),
            valueField = this.getValueFieldName();

        if (!isc.isAn.Array(dataVal)) dataVal = [dataVal];

        var filteredData;
        if (!filterLocally) {
            filteredData = [];
        }

        var notFoundCount = 0;
        for (var i = 0, len = dataVal.length; i < len; ++i) {
            // Clean up the _fetchingMissingValues object
            delete this._fetchingMissingValues[dataVal[i]];

            var record = data ? data.find(valueField, dataVal[i]) : null;
            if (!record) {
                //>DEBUG

                this.logInfo("Unable to retrieve display value for data value:" + dataVal[i] +
                             " from dataSource " + this.getOptionDataSource());
                //<DEBUG

                ++notFoundCount;
            } else if (!filterLocally) {
                filteredData.push(record);
            }
        }

        var dataLength = data ? data.getLength() : 0,
            dataValLength = dataVal.getLength();
        if (!filterLocally && (dataLength > (dataValLength - notFoundCount))) {
            this.logWarn("FetchMissingValues - filterLocally is false yet optionDataSource " +
                         "fetch included records that do not match our current data value. Ignoring " +
                         "these values.", "fetchMissingValues");
            this.logDebug("Data returned:" + this.echoAll(data), "fetchMissingValues");

            data = filteredData;
        }

        // Cache the returned results in our 'displayFieldCache' array. This has 2 advantages:
        // - on 'setValue()' to a value we've already seen we can update the selected record
        //   without requiring an additional fetch
        // - We can maintain cache-synch with the dataSource by observing dataChanged
        //   [like resultsets]. Caching the entire record rather than just the valueMap means we
        //   can handle sparse updates which refer only to primaryKeys [just deletion, probably].

        var needsRefresh = this._addDataToDisplayFieldCache(data) &&
                this._refreshForDisplayValueChange();

        // If we retrieved the entire dataSet, set a flag to avoid future fetches that
        // would otherwise occur if 'setValue()' was called passing in a value that's
        // not present in this valueMap
        if (filterLocally) this._gotAllOptions = true;

        // We need to refresh our displayed value if we're still showing the
        // data value

        this.updateDisplayValueMap(needsRefresh);
    },

    _addRecordToDisplayFieldCache : function (record) {
        if (record != null) {
            return this._addDataToDisplayFieldCache([record]);
        } else {
            return false;
        }
    },

    _addDataToDisplayFieldCache : function (data) {
        if (data != null) {
            return this._modifyDataInDisplayFieldCache(data, true, true, false, true);
        } else {
            return false;
        }
    },

    _removeValueFromDisplayFieldCache : function (value) {
        var cache = this._displayFieldCache;
        if (cache) {
            var valueField = this.getValueFieldName(),
                record = cache.find(valueField, value);

            if (record != null) {
                return this._modifyDataInDisplayFieldCache([record], false, false, true, true);
            }
        }
        return false;
    },

    // Add a list of records to the displayValue cache.  The `add`, `update`, and `remove`
    // arguments are flags that determine the action taken.  If `returnNeedsRefresh` is true
    // then this method also returns whether or not the changes to the cache affect the
    // the displayField values of the current value of the form item.
    _modifyDataInDisplayFieldCache : function (data, add, update, remove, returnNeedsRefresh) {

        if (this._displayFieldCache == null) {
            this._displayFieldCache = [];
        }

        var cache = this._displayFieldCache,
            valueField = this.getValueFieldName(),
            displayField = this.getDisplayFieldName(),
            addOnly = add && !(update || remove);

        if (returnNeedsRefresh) {
            var value = this.getValue(),
                needsRefresh = false;

            if (!isc.isAn.Array(value)) value = [value];
        }

        for (var i = 0; i < data.length; i++) {
            var record = data[i],
                recordValue = record[valueField],
                j = cache.findIndex(valueField, recordValue),
                maybeNeedsRefresh = false;

            if (j == -1) {
                if (add) {
                    cache.push(record);
                    maybeNeedsRefresh = true;
                }
            } else if (update || remove) {
                var cachedRecord = cache[j],
                    changed = (record[displayField] != cachedRecord[displayField]);

                if (update && changed) {
                    cache[j] = record;
                    maybeNeedsRefresh = true;
                } else if (remove) {
                    cache.splice(j, 1);
                    maybeNeedsRefresh = true;
                }
            }

            if (returnNeedsRefresh && maybeNeedsRefresh && !needsRefresh) {
                needsRefresh = (value.indexOf(recordValue) != -1);
            }
        }

        // As with ResultSets, observe dataChanged on the dataSource so we can update our
        // cache automatically when records cached in our displayFieldCache are modified.
        var dataSource = this.getOptionDataSource();
        if (!this.isObserving(dataSource, "dataChanged")) {
            this.observe(dataSource,
                "dataChanged", "observer.dataSourceDataChanged(observed,dsRequest,dsResponse)");
        }

        if (returnNeedsRefresh) return needsRefresh;
    },

    _refreshForDisplayValueChange : function () {
        return true;
    },

    updateDisplayValueMap : function (needsRefresh) {
        // update this._selectedRecord from the _displayFieldCache
        this._updateSelectedRecord();

        var data = this._displayFieldCache,
            displayField = this.getDisplayFieldName(),
            valueField = this.getValueFieldName();

        // Add to the special 'displayFieldValueMap'
        // This is combined with any explicitly specified valueMap by 'getValueMap()'

        var valueMap = this._displayFieldValueMap = {};

        var undef;
        for (var i = 0; i < data.length; i++) {
            var record = data[i];
            var value = record[valueField], display = record[displayField];

            // Note: We assume uniqueness here - if multiple records are returned with the same
            // data value, we'd expect them to have the same display value (and we can ignore
            // the later rows).
            if (!value || valueMap[value] !== undef) {
                if (valueMap[value] != display) {
                    // Log a warning if we hit duplicate entries with non duplicate display
                    // values
                    this.logWarn("Deriving valueMap for '" + valueField +
                                    "' from dataSource based on displayField '" + displayField +
                                    "'. This dataSource contains more than one record with " + valueField
                                    + " set to " + value + " with differing " + displayField + " values."
                                    + " Derived valueMap is therefore unpredictable.",
                                "fetchMissingValues");
                }
                continue;
            }

            valueMap[record[valueField]] = displayField != null ? display : value;
        }
        // UpdateValueMap actually combines the displayFieldValueMap with any user-specified VM.
        this.updateValueMap(needsRefresh);
    },

    //> @method formItem.invalidateDisplayValueCache()
    // If this item has a specified +link{formItem.displayField}, the value displayed to the
    // user for this item may be derived from another field.
    // <P>
    // The display field can be either another field value in the same record or a field that
    // must be retrieved from a related +link{formItem.optionDataSource,optionDataSource} if
    // +link{FormItem.fetchMissingValues} is true. In this latter case, we perform a fetch against
    // the option dataSource when the item value changes in order to determine the
    // display value to show (and we make the associated record available via
    // +link{formItem.getSelectedRecord()}).
    // <P>
    // We cache this data on the form item, so if the item value changes to a new value, then reverts
    // to a previously-seen value, the display value and selected record are already available
    // without the need for an additional fetch. The cached values will also be kept in synch with
    // the dataSource data assuming it is modified via standard add, update or delete operations.
    // <P>
    // This method explicitly invalidates this cache of optionDataSource data, and if the item value
    // is non null and fetchMissingValues is still true, re-fetches the data.
    //
    // @group display_values
    // @visibility external
    //<
    // Internal destroying parameter allows us to clean up optionDataSources / missingValues type
    // stuff without instantiating a new fetch.
    invalidateDisplayValueCache : function (destroying) {
        // drop the generated 'displayFieldValueMap' / 'displayFieldCache'
        this._displayFieldValueMap = null;
        this._displayFieldCache = null;
        this._clearSelectedRecord();
        this._gotAllOptions = false;

        this.ignoreOptionDataSource();
        if (destroying) return;

        // If we are just showing values from the form as a whole, regenerate
        if (this.form._useDisplayFieldValue(this)) {
            this._displayFieldValueFromFormValues();
        // Otherwise call _checkForDisplayFieldValue which will re-fetch against the OptionDataSource
        // unless fetchMissingValues is false, etc.
        } else if (this._value != null && this.shouldFetchMissingValue(this._value)) {
            this._clearSelectedRecord();
            this._checkForDisplayFieldValue(this._value);
        }
        // updateValueMap should reset our display value - of course if an asynch fetch occurred
        // we'll temporarily show the data value, until the fetch completes.
        this.updateValueMap();
    },

    ignoreOptionDataSource : function () {
        // Drop optionDataSource observation. We'll re-set it up in fetchMissingValueReply if
        // fetchMissingValues is true

        var ODS = this.getOptionDataSource();
        if (ODS != null && this.isObserving(ODS, "dataChanged")) {
            this.ignore(ODS, "dataChanged");
        }
    },

    // dataSourceDataChanged
    // if optionDataSource and fetchMissingValues is specified we pick up DataSource records
    // and build a valueMap from them.
    // As with ResultSets, we observe dataChanged on the dataSource so we can keep these cached
    // records / valueMap synched with the dataSource.
    dataSourceDataChanged : function (dataSource,dsRequest,dsResponse) {
        var logCacheSynch = this.logIsDebugEnabled("fetchMissingValues");
        if (logCacheSynch) {
            this.logDebug("dataSourceDataChanged is firing for request:" + this.echo(dsRequest),
             "fetchMissingValues");
        }
        var cache = this._displayFieldCache;
        if (cache == null) return;

        if (dsResponse.invalidateCache) {
            if (logCacheSynch) {
                this.logDebug("Request had invalidateCache set, dropping cached display values",
                    "fetchMissingValues");
            }
            this.invalidateDisplayValueCache();
        } else {

            var displayField = this.getDisplayFieldName(),
                valueField = this.getValueFieldName();

            var updateData = dataSource.getUpdatedData(dsRequest, dsResponse, true),
                isAdd = dsRequest.operationType == "add",
                isUpdate = dsRequest.operationType == "update",
                isRemove = dsRequest.operationType == "remove";

            if (logCacheSynch) {
                this.logDebug("Operation type:" + dsRequest.operationType + ", updateData:" +
                        this.echoAll(updateData), "fetchMissingValues");
            }

            // Bail if no change was actually made or we don't understand the operation
            // in question

            if (updateData == null || (!isAdd && !isRemove && !isUpdate)) return;

            if (!isc.isAn.Array(updateData)) {
                updateData = [updateData];
            }
            var dataValueModified = false,
                valueField = this.getValueFieldName();

            if (isAdd) {
                cache.addList(updateData);
                if (this.multiple) {
                    var this_value = this._value;
                    if (!(this_value == null || isc.isAn.Array(this_value))) {
                        this.logInfo(
                                "dataSourceDataChanged - this is a multiple FormItem but this._value " +
                                "is not null and is not an array.");
                        this_value = [this_value];
                    }

                    if (this_value != null) {
                       var len = this_value.getLength();
                       for (var i = 0; !dataValueModified && i < len; ++i) {
                           dataValueModified = updateData.find(valueField, this_value[i]) != null;
                       }
                   }
                } else {
                    dataValueModified = updateData.find(valueField, this._value) != null;
                }
            } else {
                var keyColumns = dataSource.getPrimaryKeyFields();
                for (var i = 0; i < updateData.length; i++) {
                    var updateRow = updateData[i],
                        keyValues = isc.applyMask(updateRow, keyColumns);

                    // find the index of the old row
                    var index = dataSource.findByKeys(keyValues, cache);
                    if (index == -1) {
                        if (isRemove) continue;
                        // else - update, if we didn't have the record add it

                        cache.add(updateRow);
                    } else {
                        var recordValue = cache[index][valueField];

                        if (this.multiple) {
                            var this_value = this._value;
                            if (!(this_value == null || isc.isAn.Array(this_value))) {
                                this.logWarn(
                                        "dataSourceDataChanged - this is a multiple FormItem but " +
                                        "this._value is not null and is not an array.");
                                this_value = [this_value];
                            }

                            if (this_value != null) {
                                var len = this_value.getLength();
                                for (var k = 0; !dataValueModified && k < len; ++k) {
                                    dataValueModified = recordValue == this_value[k];
                                }
                            }
                        } else if (recordValue == this._value) {
                            dataValueModified = true;
                        }
                        if (isRemove) {
                            cache.removeAt(index);
                        } else {
                            cache[index] = updateRow;
                        }
                    }
                }
            }

            // Now rebuild the valueMap from the new set of cache data, and the 'selectedRecord'
            // if necessary.
            this.updateDisplayValueMap(dataValueModified && this._refreshForDisplayValueChange());
        }
    },

    //> @method formItem.getSelectedRecord()
    // Get the record returned from the +link{optionDataSource} when +link{fetchMissingValues}
    // is true, and the missing value is fetched.
    // <P>
    // +link{formItem.fetchMissingValues} kicks off the fetch when the form item is initialized
    // with a non null value or when setValue() is called on the item. Note that this method
    // will return null before the fetch completes, or if no record is found in the
    // optionDataSource matching the underlying value.
    // @return (ListGridRecord) selected record
    // @group display_values
    // @visibility external
    //<
    getSelectedRecord : function () {
        if (this._selectedRecordValue != null) {
            if (!this.compareValues(this._selectedRecordValue, this._value)) {
                this.logInfo("getSelectedRecord - cached record doesn't match new value - dropping",
                            "fetchMissingValues");
                this._clearSelectedRecord();
            }
        }
        return this._selectedRecord;
    },

    _updateSelectedRecord : function () {
        if (this._value == null || this._displayFieldCache == null) {
            this._clearSelectedRecord();
        } else {
            var valueField = this.getValueFieldName();
            this._selectedRecordValue = this._value;
            if (this.multiple) {
                var this_value = this._value;
                if (!(this_value == null || isc.isAn.Array(this_value))) {
                    this.logWarn(
                            "_updateSelectedRecord - this is a multiple FormItem but this._value " +
                            "is not null and is not an array");
                    this_value = [this_value];
                }

                if (this_value == null) {
                    this._selectedRecord = null;
                } else {
                    // assert isc.isAn.Array(this._value)
                    this._selectedRecord = [];
                    for (var i = 0, len = this_value.length; i < len; ++i) {
                        this._selectedRecord.push(
                                this._displayFieldCache.find(valueField, this_value[i]));
                    }
                }
            } else {
                this._selectedRecord = this._displayFieldCache.find(valueField, this._value);
            }
        }
    },
    _clearSelectedRecord : function () {
         delete this._selectedRecord;
         delete this._selectedRecordValue;
    },

    //>    @method    formItem.clearValue()
    // Clear the value for this form item.
    // <P>
    // Note that if a default value is specified, value will be set to that default value,
    // otherwise value will be cleared, (and removed from the containing form's values).
    // @visibility external
    //<

    clearValue : function () {

        this._clearingValue = true;
        this.setValue();
        delete this._clearingValue;
    },

    // Update the form item to display the value passed in.  This method basically calls
    // setElementValue().  In the case of `multiple: true` FormItems this method
    // converts the input value (assumed to be an array of strings) to a string using the
    // multipleValueSeparator to join the values.
    // Note that the <code>newValue</code> passed in is expected to be the display value
    // (which is, in the case of a multiple FormItem, an array of display values),
    // rather than a raw value.

    _setElementValue : function (newValue, dataValue) {
        if (this.multiple && newValue != null && isc.isAn.Array(newValue)) {
            var newValueStr = "";
            for (var i = 0, len = newValue.length; i < len; i++) {
                if (newValueStr != "") newValueStr += this.multipleValueSeparator;
                newValueStr += newValue[i];
            }
            newValue = newValueStr;
        }
        return this.setElementValue(newValue, dataValue);
    },

    //>    @method    formItem.setElementValue()
    // Update the form item to display the value passed in.  If this item has a true form data
    // element (text box, checkbox, etc), this method will set the value of that element.
    // Otherwise updates the necessary HTML for the form item to display the new value.
    // Note that the <code>newValue</code> passed in is expected to be the display value,
    // rather than the raw value (should have  already been passed through
    // <code>this.mapValueToDisplay()</code>).
    //
    //        @group    elements
    //        @param    newValue     (any)    value to set the element to
    //<
    // Note this method also update any valueIcon to display the appropriate value for the
    // current form item value

    setElementValue : function (newValue, dataValue) {
        if (!this.isDrawn()) return;
        var undef;
        if (dataValue === undef) {
            dataValue = this._value;
        }

        // If we hae a data element we always set element.value
        if (this.hasDataElement()) {



            // get a pointer to the native form element for this item
            var element = this.getDataElement();
            if (element != null) {
                this._updateValueIcon(dataValue);
                // If the element.value already matches the new value don't explicitly
                // reassign


                if (element.value !== newValue) {
                    var scrollLeft = element.scrollLeft,
                        scrollTop = element.scrollTop;

                    var mapToString = isc.Browser.isIE && isc.isA.TextItem(this);
                    element.value = (mapToString && newValue == null) ? isc.emptyString
                                                                      : newValue;

                    if (isc.Browser.isIE && isc.Browser.version >= 10) {
                        element.scrollLeft = scrollLeft;
                        element.scrollTop = scrollTop;
                    }
                }
                if (newValue === undef || newValue == null || isc.isAn.emptyString(newValue)) {

                    this._showingInFieldHint = false;
                }
                return newValue;
            }
        }
        // otherwise if we have no data element, just redraw the content of our text box
        var textBox = this._getTextBoxElement();
        if (textBox != null) {
            if (this.showValueIconOnly) newValue = isc.emptyString;
            var valueIconHTML = this._getValueIconHTML(dataValue);
            if (valueIconHTML != null)
                newValue = valueIconHTML + (newValue != null ? newValue :  isc.emptyString);

            if (isc.Browser.isIE) {
                if (newValue && newValue.startsWith("<nobr>"))
                    newValue = newValue.substring(6);
                if (newValue && newValue.endsWith("</nobr>"))
                    newValue = newValue.substring(0,newValue.length-7);
                try {
                    textBox.innerHTML = newValue;
                } catch (e) {
                    var newSpan = document.createElement("span");
                    newSpan.innerHTML = newValue;
                    textBox.innerHTML = "";
                    textBox.appendChild(newSpan);
                }
            } else {
                textBox.innerHTML = newValue;
            }
            if (!this._getClipValue() || this.height == null || this.width == null) {
                this.adjustOverflow("textBox value changed");
            }
        }

        // If we didn't get a pointer to our text box, we would expect the sub item to
        // implement an appropriate override to setElementValue()
    },

    // _updateValueIcon
    // Explicitly updates the valueIcon image src based on the data value passed in.
    _updateValueIcon : function (value) {
        if (this.suppressValueIcon || !this.isDrawn()) return;

        var src = this._getValueIcon(value),
            valueIconHandle = this._getValueIconHandle();
        if (src != null) {
            if (this.imageURLSuffix != null) src += this.imageURLSuffix;
            var imgDir = this.imageURLPrefix || this.baseURL || this.imgDir;

            // If the image is already written out, just update its src
            if (valueIconHandle != null) {
                isc.Canvas._setImageURL(valueIconHandle, src, imgDir);

            // In this case the valueIcon has never been written out.
            // Positioning of the valueIcon will vary by form item.
            // - for data element based items, such as text items, we write the icon out before
            //   the data element
            // - for non data element based items, such as (synthetic) selects, we write the
            //   icon out inside the text box

            } else {
                src = isc.Canvas.getImgURL(src, imgDir);

                var inserted = false;
                if (this.hasDataElement()) {
                    var element = this.getDataElement();
                    if (element != null) {
                        isc.Element.insertAdjacentHTML(
                            element, "beforeBegin", this._getValueIconHTML(value)
                        );
                        element.style.width = this.getTextBoxWidth(value);
                        inserted = true;
                    }
                } else {
                    var textBox = this._getTextBoxElement();
                    if (textBox != null) {
                        isc.Element.insertAdjacentHTML(
                            textBox, "afterBegin", this._getValueIconHTML(value)

                        );
                        inserted = true;
                    }
                }
                // sanity check - if we failed to insert the icon, redraw
                if (!inserted) this.redraw();
            }

        // If we have no current value icon, clear the handle if its present.

        } else if (valueIconHandle != null && !(isc.isAn.Array(value) && value.length > 1) ) {
            isc.Element.clear(valueIconHandle);
            if (this.hasDataElement()) {
                var element = this.getDataElement();
                element.style.width = this.getTextBoxWidth(value);
            }
        }
    },

    //>@method formItem.setPrompt()
    // Set the +link{formItem.prompt} for this item
    // @param prompt (string) new prompt for the item.
    // @visibility external
    //<
    setPrompt : function (prompt) {
        this.prompt = prompt;
        // no need for a redraw - we don't rely on native HTML tooltips, but react to hover events to
        // show prompts
    },

    //>@method formItem.setHint()
    // Set the hint text for this item
    // @param hintText (string) new hint for the item
    // @visibility external
    //<
    setHint : function (hintText) {
        this.hint = hintText;
        if (this.showHint) this.redraw();
    },

    //>@method formItem.setHintStyle()
    // Set the hintStyle for this item
    // @param hintStyle (CSSStyleName) new style for hint text
    // @visibility external
    //<
    setHintStyle : function (style) {
        if (!this._getShowHintInField() && this.getHint()) {
            var hintHandle = this._getHintCellElement();
            if (hintHandle) hintHandle.className = style;
        }
    },

    // Internal methods to show/hide hints within field
    // _showingInFieldHint maintains the visibility state of hint within field

    _showInFieldHint : function () {
        if (!this._showingInFieldHint) {
            // Set field class to our hint style
            var element = this.getDataElement();
            if (element) {
                element.className = this._getInFieldHintStyle();
                // switch password items to plain-text while hint is showing
                if (this.isA.TextItem) {
                     this._preHintElementType = element.type;
                     element.type = "text";
                 }
            } else {
                var textBox = this._getTextBoxElement();
                if (textBox != null) {
                    textBox.className = this._getInFieldHintStyle();
                }
            }
            // Show the hint in the field
            // Note that hint is HTML which may not display correctly within the field.
            // To improve the situation, unescape common HTML sequences first.
            var hint = this.getHint();
            if (hint) hint = hint.unescapeHTML();
            this.setElementValue(hint);
            this._showingInFieldHint = true;
        }
    },
    _hideInFieldHint : function (clearStyleOnly) {
        if (this._showingInFieldHint) {
            // Reset field class to the default style
            var element = this.getDataElement();
            if (element) {
                element.className = this.getTextBoxStyle();
                if (this._preHintElementType) {
                    element.type = this._preHintElementType;
                    delete this._preHintElementType;
                }
            } else {
                var textBox = this._getTextBoxElement();
                if (textBox != null) {
                    textBox.className = this.getTextBoxStyle();
                }
            }
            // Clear the hint text from the field
            if (!clearStyleOnly) this.setElementValue(isc.emptyString);
            this._showingInFieldHint = false;
        }
    },

    // Internal method to define hint style
    _getInFieldHintStyle : function() {
        var rtl = this.showRTL && this.containerWidget != null && this.containerWidget.isRTL();
        if (this.showDisabled && this.isDisabled()) {
            return this.textBoxStyle + (rtl ? "DisabledHintRTL" : "DisabledHint");
        } else {
            return this.textBoxStyle + (rtl ? "HintRTL" : "Hint");
        }
    },

    // Does field support in-field hints and are these hints enabled?
    _getShowHintInField : function() {
        if (this.showHint && this.showHintInField) {
            // applies only to text items (and subclasses)
            if (isc.isA.TextItem(this)
                || isc.isA.TextAreaItem(this)
                || isc.isA.SelectItem(this))
            {
                return true;
            }
        }
        return false;
    },

    // Value Management
    // --------------------------------------------------------------------------------------------

    //>    @method    formItem.getDefaultValue()
    // Return the default value for this item
    //        @group    elements
    //
    //        @return    (any)        value of this element
    //<
    getDefaultValue : function () {
        if (this.defaultDynamicValue) {
            // CALLBACK API:  available variables:  "item,form,values"
            // Convert a string callback to a function
            this.convertToMethod("defaultDynamicValue");
            var item = this,
                form = this.form,
                values = this.form.getValues()
            ;
            return this.defaultDynamicValue(item,form,values);
        }
        // Return this.defaultValue - note that this will return null (technically 'undef') if no
        // default value has been set, which is appropriate - allows null values in form items.
        return this.defaultValue;
    },

    //>    @method    formItem.setToDefaultValue()
    // Set the value for this item to the default value stored in the item
    //        @group    elements
    //        @return    (any)        value of this element
    //<
    // Since a defaultValue means we don't support setting to null, this is really just a
    // synonym for clearValue(), which itself calls 'setValue(null)' and lets setValue figure
    // out the defaultValue.
    setToDefaultValue : function () {
        return this.clearValue();
    },

    //> @method formItem.isSetToDefaultValue()
    // Is the current value displayed by the form item derived from the default value for the
    // item.
    // @return (boolean) True if this item's value is derived from the default
    //<
    isSetToDefaultValue : function () {
        return (this._setToDefault == true);
    },

    _completionAcceptKeys : {
        "Tab":true,
        "Arrow_Left":true,
        "Arrow_Right":true,
        "Arrow_Up":true,
        "Arrow_Down":true,
        "Home":true,
        "End":true,
        "Page_Up":true,
        "Page_Down":true,
        "Enter":true
    },

    //> @method formItem.updateValue()
    // Update the stored value for this form item based on the current display value.
    //
    //  @see saveValue()
    //  @see handleChange()
    //  @see mapDisplayToValue()
    //  @visibility internal
    //<
    // Performs the following steps:
    // - takes the current value of this item's form element
    // - maps it to the appropriate value for storage using 'mapDisplayToValue()'
    // - perform validation of the form item [if validateOnChange is true]
    //      - if the resulting value from the validator differs from the value passed in,
    //        update the display and stored value to reflect this.
    // - fire any 'transformInput()' method on the value passed in. If the value is changed,
    //   store and display this new value
    // - fire the change handler for the field.
    //      - if the change handler returns false, revert to the previous value
    // - return false if the change was "rejected". IE:
    //      - a validator failed
    //      - a change handler returned false.

    updateValue : function () {
        // for the case where we're changeOnKeypress false, throw away the
        // remembered value from the last "minimalUpdate"
        if (this._lastMinimalUpdateValue != null) delete this._lastMinimalUpdateValue;

        // this is effectively meaningless if we have no element (override for special cases
        // like container items)
        if (!this.hasElement() || this.getDataElement() == null) return;

        var newValue = this.getElementValue();
        return this._updateValue(newValue);
    },
    _updateValue : function (newValue) {

        // avoid spurious changes with auto-completion
        if (this._pendingCompletion) {
            newValue = this._handleChangeWithCompletion(newValue);
        }

        // unmap the value if necessary
        newValue = this.mapDisplayToValue(newValue);

        return this.storeValue(newValue);
    },

    // storeValue() - given a new value, fires change handlers, and stores out the new
    // (or resulting value) such that a call to 'getValue()' will return the new value.
    // Exposed (as an override point) in CanvasItem
    storeValue : function (newValue) {
        // Bail if we have already saved the value (avoids firing change on arrow keypresses,
        // etc.)
        if (this.compareValues(newValue, this._value)) {
            //this.logWarn("FI._updateValue: not saving, value unchanged: " + this._value);
            return true;
        }

        // This method may have been tripped by the developer's change handler somehow
        // (most common example - causing formItem to blur() when changeOnBlur is true)
        // If this is the case, bail unless the value passed in differs from the value we're
        // about to save (stored as this._changeValue)
        if (this._changingValue) {
            if (this.compareValues(newValue, this._changeValue)) {
                //this.logWarn("FI._updateValue: bailing on redundant change: " + this._changeValue);
                return true;
            }

        }

        // fire the change handler, (handles validation etc)
        // Notes:
        // - handleChange may modify the value to be saved (due to validator.suggestedValue,
        //   change handler returning false, etc).
        //   In this case:
        //   - it will also actually save the value / reset the elementValue via a call to
        //      setValue()
        //   - It will store the resulting value from the change handler
        //     (whether modified and saved or not) as this._changeValue. We can then save
        //     this value out iff it hasn't already been saved (!= this._value).
        // - We consider some interactions a change "failure"- such as the change handler
        //   explicitly returning false. In these cases the handleChange() method will return
        //   false. We simply return this to our calling method in case there is any special
        //   handling to be performed.
        var returnVal = this.handleChange(newValue, this._value);

        // bail if handleChange() returned false
        if (!returnVal) return false;

        // The change handler may call 'setItems' on the form (particularly likely in LG editing)
        // in which case we'll be destroyed

        if (this.destroyed) return;

        // Ensure we have the latest value (stored as this._changeValue)
        newValue = this._changeValue;
        // We may need to perform some visual updates based on the new value - do this here
        this.updateAppearance(newValue);

        // save the value
        //this.logWarn("FI._updateValue: old value: " + this._value + ", newValue: " + newValue +
        //             ", will save: " + (!this.compareValues(newValue, this._value)));
        if (!this.compareValues(newValue, this._value)) {
            // if the saved value is null and newValue is the emptyDisplayValue,
            // don't store it as the new saved value
            if (!(this._value == null && newValue && newValue == this.emptyDisplayValue))
                this.saveValue(newValue);
        }

        delete this._changeValue;

        // fire any specified 'changed' handler for this item.

        this.handleChanged(this._value);
        return returnVal;
    },

    //> @attr formItem.implicitSave (Boolean : false : IRW)
    // When true, indicates that changes to this item will cause an automatic save on a
    // +link{dynamicForm.implicitSaveDelay, delay}, as well as when the entire form is
    // submitted.  Unless implicitSaveOnBlur is set to false on either this
    // +link{formItem.implicitSaveOnBlur, formItem} or it's
    // +link{dynamicForm.implicitSaveOnBlur, form} changes will also be automatically saved on
    // editorExit.
    // @visibility external
    //<

    //> @attr formItem.implicitSaveOnBlur (Boolean : false : IRW)
    // If not set to false, form item values will be saved when this item's "editorExit"
    // handler is fired as well as on a delay and when the entire form is submitted.
    // @visibility external
    //<

    // handleChanged() - helper to fire any user-specified "changed" handler on this item.
    handleChanged : function (value) {
        // Give any Rules associated via a rulesEngine a chance to fire.
        if (this.form.rulesEngine != null) {
           this.form.rulesEngine.processChanged(this.form, this);
        }

        if (this.changed) this.changed(this.form, this, value);
        if (this.form) {
            if (!this.suppressItemChanged && this.form.itemChanged != null)
                this.form.itemChanged(this, value);

            this.checkForImplicitSave();
        }
    },

    checkForImplicitSave : function () {
        if (this.getImplicitSave()) {
            var _this = this;
            this.form._addItemToImplicitSaveUpdateArray(this);
            this.form.awaitingImplicitSave = true;
            this.form.fireOnPause("fiImplicitSave",
                function () {
                    if (_this.form.awaitingImplicitSave) {
                        _this.form.performImplicitSave(_this, true);
                    }
                }, this.form.implicitSaveDelay
            );
        }
    },

    // updateAppearance() - helper method fired in response to updateValue when we have
    // a new value (entered by the user)
    // Default implementation will just rewrite any valueIcon's URL.

    updateAppearance : function (newValue) {
        if (this.valueIcons || this.getValueIcon) {
            this._updateValueIcon(newValue);
        }
    },


    //>    @method    formItem.getValue()
    // Return the value tracked by this form item.
    // <P>
    // Note that for FormItems that have a +link{ValueMap} or where a
    // +link{formatValue(),formatter} has been defined, <code>getValue()</code> returns the
    // underlying value of the FormItem, not the displayed value.
    //
    // @group formValues
    // @return (any) value of this element
    // @visibility external
    //<
    getValue : function () {
        if (this.destroyed || this.destroying) return;

        // We return this._value, rather than looking at the form item's element and deriving
        // the value from there.
        // This is appropriate for a number of reasons.
        //  - this._value may be of a type not supported by the form element, which usually only
        //    supports strings.  We need to track booleans, null (as distinct from null
        //    string), or a number (as opposed to a number in String form).
        //    The value set by user interaction in the form may need to be processed before it
        //    can be stored as this._value.
        //  - We want to ensure that a change handler is fired before the value is updated
        //    (allowing users to cancel a change).
        //    If getValue() were to return the value derived from the element, in some cases we
        //    would not have received a change notification, but the value returned would be
        //    different from the last stored value.
        //
        // We keep this._value up to date via the 'updateValue()' method, which will fire
        // change handlers and validators for the item, then store the value via 'saveValue()'
        // updateValue() is called whenever the value may have changed (depending on the form
        // item type this may be a result of native onchange, keypress or other event[s]).
        var undef;
        if (this._value !== undef) {
            return this._value;
        }

        // If no value has been stored for this element, return the value the form has for this
        // element.

        return this.form.getSavedItemValue(this);
    },

    //> @method    formItem.getElementValue()
    // Return the value stored in the form element(s) for this item without modification
    //  @group    elements
    //
    //  @return    (any)        value of this element
    //<

    getElementValue : function () {


        // If showing an in-field hint, simulate an element value of undefined
        if (this._showingInFieldHint) return null;

        // get a pointer to the element for this item
        var element = this.getDataElement();

        // if no element was found, bail
        if (!element) return null;

        var value = "";


        if (this._propagateMultiple && element.files && element.files.length > 1) {
            for (var i=0; i < element.files.length; i++) {
                if (value != "") value += this.multipleValueSeparator;
                value += element.files[i].name;
            }
        } else {
            value = element.value;
        }

        return value;
    },


    //>    @method    formItem.resetValue()
    // Reset the value for this item to the value stored in the last save point for the form
    //        @group    elements
    //<
    resetValue : function () {
        var oldValue = this.form._oldValues[this.getFieldName()];
        this.setValue(oldValue);
    },

    //>    @attr formItem.shouldSaveValue (Boolean : true : IR)
    // Should this item's value be saved in the form's values and hence returned from
    // +link{dynamicForm.getValues,form.getValues()}?
    // <p>
    // <code>shouldSaveValue:false</code> is used to mark formItems which do not correspond to
    // the underlying data model and should not save a value into the form's
    // +link{dynamicForm.values,values}.  Example includes visual separators, password re-type fields,
    // or checkboxes used to show/hide other form items.
    // <p>
    // A <code>shouldSaveValue:false</code> item should be given a value either via
    // +link{formItem.defaultValue} or by calling
    // +link{dynamicForm.setValue,form.setValue(item, value)} or
    // +link{formItem.setValue,formItem.setValue(value)}.  Providing a value via
    // +link{dynamicForm.values,form.values} or +link{dynamicForm.setValues,form.setValues()}
    // will automatically switch the item to <code>shouldSaveValue:true</code>.
    // <P>
    // Note that <ul>
    // <li>if an item is shouldSaveValue true, but has no name, a warning is logged, and
    //     shouldSaveValue will be set to false.
    // </li></ul>
    //
    // @group formValues
    // @visibility external
    //<
    shouldSaveValue:true,

    // Will this form item's value be submitted directly to the server via a native form submit?
    shouldSubmitValue : function () {
        return this.form._formWillSubmit();
    },

    //> @method formItem.setCanEdit()
    // Is this form item editable (canEdit:true) or read-only (canEdit:false)?
    // Setting the form item to non-editable causes it to render as read-only,
    // using the appearance specified via +link{formItem.readOnlyDisplay}.
    // <P>
    // The default appearance for canEdit:false items
    // (<code>readOnlyDisplay:"readOnly"</code>) differs from the disabled state in that
    // the form item is not rendered with disabled styling and
    // most form items will allow copying of the contents while read-only but do not while
    // disabled.
    //
    // @param canEdit (boolean) Can this form item be edited?
    // @group readOnly
    // @see attr:FormItem.canEdit
    // @see setDisabled()
    // @visibility external
    //<
    setCanEdit : function (canEdit) {
        var wasEditable = !this.isReadOnly();
        this.canEdit = canEdit;
        var isEditable = !this.isReadOnly();
        if (wasEditable != isEditable) {
            this.updateCanEdit();
            if (this.canEditChanged) this.canEditChanged(canEdit);

            this.redraw();
        }
    },

    renderAsStatic : function () {
        return this.getCanEdit() == false && this.getReadOnlyDisplay() == "static";
    },
    renderAsReadOnly : function () {
        return this.getCanEdit() == false && this.getReadOnlyDisplay() == "readOnly";
    },
    renderAsDisabled : function () {
        return this.getCanEdit() == false && this.getReadOnlyDisplay() == "disabled";
    },

    //> @method formItem.getCanEdit()
    // Is this form item editable or read-only?
    // <P>
    // This setting differs from the enabled/disabled state in that most form items will
    // allow copying of the contents while read-only but do not while disabled.
    // @return (boolean) true if editable
    // @group readOnly
    // @visibility external
    //<
    getCanEdit : function () {
        return !this.isReadOnly();
    },


    // updateCanEdit - helper method to update the form item to reflect it's read-only state
    updateCanEdit : function () {
        // Disabled state takes precedence over read-only.
        if (this.isDisabled()) return;

        var isReadOnly = this.isReadOnly();
        this.setElementReadOnly(isReadOnly);
        this._setIconsEnabled();
        // update the valueIcon if we have one
        this._updateValueIcon(this.getValue());
        // UpdateState is a catch-all method that updates the css classes applied to our elements
        // to reflect the 'disabled' versions
        this.updateState();

        if (this.canEditChanged) this.canEditChanged(this.canEdit);
    },

    //> @method formItem.setElementReadOnly()
    // Change the read-only state of the form element immediately.
    //<
    // Unless the specific form item overrides this method and can change the state directly,
    // redraw the item using the new state.
    setElementReadOnly : function (readOnly) {
        if (this.hasDataElement()) this.redraw();
    },

    // _setElementReadOnly()
    // Actually update the HTML to mark the data element as enabled / disabled

    _setElementReadOnly : function (readOnly) {
        if (this.hasDataElement()) {
            var element = this.getDataElement();
            if (element) {
                if (!readOnly && !element.readOnly) {
                    // must be using disabled state
                    element.disabled = readOnly;
                } else {
                    element.readOnly = readOnly;
                    if (isc.screenReader) {
                        if (element.setAttribute) element.setAttribute("aria-readonly", "true");
                    }
                }
                element.tabIndex = this._getElementTabIndex();
            }
        } else if (this._canFocus()) {
            var element = this.getFocusElement();
            if (element) element.tabIndex = this._getElementTabIndex();
        }
    },

    // isReadOnly - helper method to determine whether a field is editable
    // the public 'getCanEdit()' method falls through to this.
    isReadOnly : function () {

        var widget = this.form;

        // Check container(s)
        var item = this;
        while (item.parentItem) {
            if (item.canEdit != null) return !item.canEdit;
            item = item.parentItem;
        }
        return !isc.DynamicForm.canEditField(item, widget);
    },

    //> @method formItem.isEditable()
    // Can this items value be edited by the user
    // @return (boolean)    true if the user can edit this item's value.
    //<
    // Most form items are editable so return true by default an override for the cases where
    // items are just there to display static values, even though technically an instance of
    // the FormItem class would not be editable.

    isEditable : function () {
        return true;
    },

    //> @method formItem.getCriteriaFieldName()
    // Returns the name of the field to use in a Criteria object for this item
    //
    // @return (String)    the name of the field to use in a Criteria object for this item
    // @group criteriaEditing
    //<
    // The main current use for this method is when filtering a field that has a displayField,
    // using a ComboBoxItem. Depending on whether the user selected from the picklist or just
    // typed some partial filter into the combo box, we will want to filter on either the
    // underlying field or the display field.  Done this way to make it extensible.

    getCriteriaFieldName : function () {
        var fieldName = this.criteriaField || this.includeFrom || this.getDataPath() || this.getFieldName();
        if (this.form.dataPath != null) {
            fieldName = this.form._trimDataPath(fieldName);
            if (fieldName.endsWith("/")) fieldName = fieldName.substring(0, fieldName.length-1);
        }
        return fieldName;
    },

    //> @method formItem.getCriteriaValue()
    // Returns the value of the field to use in a Criteria object for this item
    //
    // @return (String)    the value of the field to use in a Criteria object for this item
    // @group criteriaEditing
    //<
    getCriteriaValue : function () {
        return this.getValue();
    },

    //> @method formItem.hasAdvancedCriteria()
    // Does this form item produce an +link{AdvancedCriteria} sub criterion object?
    // If this method returns true, +link{dynamicForm.getValuesAsCriteria()} on the
    // form containing this item will always return an +link{AdvancedCriteria} object, calling
    // <var class="smartclient">+link{formItem.getCriterion()}</var>
    // <var class="smartgwt"><code>FormItemCriterionGetter.getCriterion()</code></var>
    // on each item to retrieve the individual criteria.
    // <P>
    // Default implementation will return <code>true</code> if +link{formItem.operator} is
    // explicitly specified.
    //
    // @return (Boolean) true if this item will return an AdvancedCriteria sub-criterion.
    //
    // @group criteriaEditing
    // @visibility external
    //<
    hasAdvancedCriteria : function () {
        var value = [null, undefined].contains(this._value) ? null : this._value;
        return value != null && (this.operator != null || this._shouldAllowExpressions());
    },

    _shouldAllowExpressions : function () {
        // by default, text, textArea, canvas, container and date (which calls through to this
        // function from it's own override) Items allow expressions
        var allow = isc.isA.TextItem(this) || isc.isA.TextAreaItem(this) ||
                isc.isA.CanvasItem(this) || isc.isA.ContainerItem(this) ||
                isc.isA.DateItem(this)
        ;

        if (!allow) return false;

        allow = this.allowExpressions;
        if (allow == null) allow = this.form.allowExpressions;
        return allow;
    },

    // getOperator() returns the operator for this form item
    // will return this.operator if specified, otherwise the default operator for the type
    // being edited by this item.
    getOperator : function (textMatchStyle, isMultiValued) {
        var operator;
        if (this.operator) {
            operator = this.operator;
        } else if (isMultiValued) {
            operator = "inSet"
        } else {
            var type = this.getType();
            if (this.valueMap || this.optionDataSource ||
                isc.SimpleType.inheritsFrom(type, "enum") ||
                isc.SimpleType.inheritsFrom(type, "boolean") ||
                isc.SimpleType.inheritsFrom(type, "float") ||
                isc.SimpleType.inheritsFrom(type, "integer") ||
                isc.SimpleType.inheritsFrom(type, "time"))
            {
                operator = "equals";
            } else {
                if (textMatchStyle == null) textMatchStyle = "substring";
                // Don't pass in a value - this is appropriate for text-based items
                // we'll override for other items if necessary.

                operator = isc.DataSource.getCriteriaOperator(null, textMatchStyle);
            }
        }

        return operator;
    },

    //> @method formItem.canEditCriterion() [A]
    // When a dynamic form is editing an advanced criteria
    // object via +link{DynamicForm.setValuesAsCriteria()}, this method is used to determine
    // which sub-criteria apply to which form item(s).
    // <P>
    // This method will be called on each item, and passed the sub-criterion of the
    // AdvancedCriteria object. It should return true if the item can edit the criterion,
    // otherwise false. If it returns true, setValuesAsCriteria() will call
    // +link{formItem.setCriterion()} to actually apply the criterion to the form item, and
    // +link{dynamicForm.getValuesAsCriteria()} can subsequently retrieve the edited criterion
    // by calling +link{formItem.getCriterion()}.
    // <P>
    // Default implementation will return true if the criterion <code>fieldName</code> and
    // <code>operator</code> match the fieldName and operator (or default operator) for
    // this item.
    //
    // @param criterion (Criterion) sub-criterion from an AdvancedCriteria object
    // @return (boolean) return true if this item can edit the criterion in question.
    //
    // @group criteriaEditing
    // @visibility external
    //<

    canEditCriterion : function (criterion, warnOnField) {
        var thisOperator = this.getOperator(null, isc.isAn.Array(criterion.value));
        if (criterion.fieldName != null && criterion.fieldName == this.getCriteriaFieldName()
            && criterion.operator == thisOperator)
        {
            return true;
        }

        if (this._shouldAllowExpressions()) {
            var fieldNames = isc.DS.getCriteriaFields(criterion,
                this.form.expressionDataSource || this.form.dataSource,
                true);
            return fieldNames.contains(this.getCriteriaFieldName());
        } else {
            var isAdvanced = isc.DS.isAdvancedCriteria(criterion);
            if (this.multiple && isAdvanced && criterion.operator == "or") {
                return true;
            }
        }

        return false;
    },

    //> @method formItem.canEditSimpleCriterion() [A]
    // Is this FormItem responsible for editing the simple criterion value for the specified
    // fieldName? Default implementation will return true if the fieldName matches the specified
    // +link{criteriaField} if there is one, otherwise the +link{name} for this item.
    // @return (boolean) true if this item can edit the specified fieldName
    //<
    canEditSimpleCriterion : function (fieldName) {
        var cField = this.getCriteriaFieldName();
        return cField == fieldName;
    },

    // method called from setValuesAsCriteria to actually apply a simple criterion to this item.

    setSimpleCriterion : function (value, fieldName) {
        this.setValue(value);
    },

    //> @method formItem.getCriterion() (A)
    // Override this method if you need to provide a specialized criterion from this formItem
    // when creating an AdvancedCriteria via +link{dynamicForm.getValuesAsCriteria()}.
    // <P>
    // This API is provided to allow you to specify a more complex criterion than the
    // "field-operator-value" criterions that are built-in.  Note that the built-in behavior is
    // generally quite flexible and powerful enough for most requirements.  An example of a case
    // where you might want to override this method is if you wanted to implement a date range
    // selection (ie, date &gt; x AND date &lt; y) on a form that was combining its other criteria
    // fields with an "or" operator.
    // <P>
    // Note that this method is part of the criteria editing subsystem: if overridden, it
    // is likely that you will want to also override +link{formItem.hasAdvancedCriteria()} to
    // ensure this method is called by the form, and to support editing of existing advanced
    // criteria you may also need to override +link{formItem.canEditCriterion()} and
    // +link{formItem.setCriterion()}.
    // <P>
    // The default implementation will return a criterion including the form item value, fieldName
    // and specified +link{formItem.operator}, or a default operator derived from the
    // form item data type if no explicit operator is specified.
    //
    // @param [textMatchStyle] (TextMatchStyle) If passed assume the textMatchStyle
    //   will be used when performing a fetch operation with these criteria. This may impact
    //   the criterion's operator property.
    // @return (Criterion) criterion object based on this fields current edited value(s).
    // @group criteriaEditing
    // @visibility external
    //<
    getCriterion : function (textMatchStyle) {
        var value = this.getCriteriaValue();

        if (value == null || isc.is.emptyString(value)) return;
        // multi-selects are returned as an array.
        if (isc.isAn.Array(value)) {
             // If nothing is selected, or if blank is selected, no criteria
            var uniqueItems = value.getUniqueItems();
            if (value.length == 0 || (uniqueItems.length == 1 && isc.isA.String(value[0])
                && isc.is.emptyString(value[0]))) return;
        }

        var operator = this.getOperator(textMatchStyle, isc.isAn.Array(value)),
            fieldName = this.getCriteriaFieldName();

        var result = {
            fieldName: fieldName,
            operator: operator,
            value: value
        };

        if (this._shouldAllowExpressions()) {
            var crit = this.parseValueExpressions(value, fieldName, operator);
            if (crit != null) result = crit;
        }

        return result;
    },

    //> @method formitem.setCriterion() [A]
    // Update this form item to reflect a criterion object from within an AdvancedCriteria.
    // Called by +link{DynamicForm.setValuesAsCriteria()} when +link{formItem.canEditCriterion()}
    // returns true for this item.
    // <P>
    // Default implementation simply calls +link{formItem.setValue} with the <code>value</code>
    // of the criterion passed in
    // @param criterion (Criterion) criterion to edit
    // @group criteriaEditing
    // @visibility external
    //<
    setCriterion : function (criterion) {
        var allowEx = this._shouldAllowExpressions(),
            value = criterion ? criterion.value : null
        ;

        if (allowEx) {
            value = this.buildValueExpressions(criterion);
        } else {
            if (this.multiple) {
                var isAdvanced = isc.DS.isAdvancedCriteria(criterion);
                if (isAdvanced && criterion.operator == "or") {
                    // if the passed advancedCriteria is a flat list of ORs with the same
                    // operator and field (this one), build an array of values from them and
                    // pass that to setValue
                    var newValue = [],
                        critArray = criterion.criteria,
                        fieldName = this.getCriteriaFieldName(),
                        firstOp = critArray[0] ? critArray[0].operator : null,
                        failed = false
                    ;
                    for (var i=0; i<critArray.length; i++) {
                        var subCrit = critArray[i];
                        if (subCrit.criteria || subCrit.fieldName != fieldName ||
                                subCrit.operator != firstOp)
                        {
                            failed = true;
                            break;
                        }
                        newValue.add(subCrit.value);
                    }
                    if (!failed) value = newValue;
                }
            }
        }
        this.setValue(value);
    },

    _getTextBoxScrollWidth : function (textBoxHandle) {
        return textBoxHandle.scrollWidth;
    },

    _getClipValue : function () {
        return (this.renderAsStatic() ? this._getClipStaticValue() : !!this.clipValue);
    },

    //> @method formItem.valueClipped()
    // Is the value clipped?
    // <p>
    // The form item must have value clipping enabled. If a form item type supports the
    // clipValue attribute, then clipValue must be true. +link{TextItem}s and derivatives
    // (e.g. +link{SpinnerItem}) automatically clip their values.
    //
    // @return (boolean) true if the value is clipped; false otherwise.
    // @visibility external
    //<
    valueClipped : function () {
        var clipValue = this._getClipValue(),
            textBoxHandle;
        return (clipValue &&
                (textBoxHandle = this._getTextBoxElement()) != null &&
                isc.Element.getClientWidth(textBoxHandle) < this._getTextBoxScrollWidth(textBoxHandle));
    },

    // Errors
    // --------------------------------------------------------------------------------------------

    //>    @method    formItem.clearErrors()
    //            Clear all error messages for this item
    //        @group    errorHandling
    //<
    clearErrors : function (suppressAutoFocus) {
        var name = this.getFieldName();
        if (name) this.form.clearFieldErrors(name, true, suppressAutoFocus);
    },

    //>    @method    formItem.setError()
    // Set the error message for this item
    //        @group    errorHandling
    //        @param    message    (string) error message
    //<
    setError : function (message) {
        var name = this.getFieldName();
        if (name) this.form.setError(name, message);
    },

    //>    @method    formItem.hasErrors()
    //        Return whether this item currently has any validation errors as
    //         a result of a previous validation pass.
    //        @group    errorHandling
    //    @return    (boolean)    true == item currently has validation errors.
    //<
    hasErrors : function () {
        // recurse up parent tree to find the root form item and get the correct error status
        if (this.parentItem != null) return this.parentItem.hasErrors();
        var name = this.getFieldName();

        if (name && this.form) return this.form.hasFieldErrors(name);
        var dp = this.getDataPath();
        if (dp && this.form) return this.form.hasFieldErrors(dp);
        return false;
    },

    //> @method formItem.validate()
    // Validate this item.
    //
    // @return (Boolean) returns true if validation was successful (no errors encountered), false
    //                   otherwise.
    // @visibility external
    //<
    validate : function () {
        var hadErrorsBefore = this.hasErrors(),
            fieldErrors = [],
            allErrors = null,
            stopOnError = false
        ;

        // Wrap field validation in a queue so that server validators are
        // sent as a single request.
        var wasAlreadyQueuing = isc.rpc.startQueue();

        // Process all validators on field that are applicable.
        // Note that validateFieldAndDependencies may modify the record so we pass
        // a copy of our current values.
        var value = this.getValue(),
            record = isc.addProperties({}, this.form.getValues()),
            validationOptions = {unknownErrorMessage: this.form.unknownErrorMessage,
                                typeValidationsOnly:this.form.validateTypeOnly}
        ;
        var fieldResult = this.form.validateFieldAndDependencies(this, this.validators, value,
                                                                 record, validationOptions);

        var storeErrorAs = this.name;
        if (storeErrorAs == null) storeErrorAs = this.getDataPath();
        if (storeErrorAs == null) {
            this.logWarn("item has no specified name or dataPath - " +
                "unable to meaningfully store validation errors.");
        }

        // Submit server validation requests queue
        if (!wasAlreadyQueuing) isc.rpc.sendQueue();

        if (fieldResult != null) {
            // if the validator returned a resultingValue, use that as the new value
            // whether the validator passed or failed.  This lets us transform data
            // (such as with the mask validator).
            if (fieldResult.resultingValue != null) {
                // Update field value
                this.setValue(fieldResult.resultingValue);
            }
            if (!fieldResult.valid) {
                fieldErrors = fieldResult.errors[storeErrorAs];
                if (fieldErrors == null) fieldErrors = [];
            }
            stopOnError = fieldResult.stopOnError;

            // Even though the changed field may be valid, there may be other fields
            // that are no longer valid because of a dependency. These errors should
            // be shown on the form.
            allErrors = fieldResult.errors;
        }

        // If any errors are set or cleared, mark that a redraw is needed.
        var redrawRequired = false;

        // If we failed validation or validation is clearing previous errors,
        // update the errors on the field
        if (fieldErrors.length > 0 || hadErrorsBefore) {
            if (fieldErrors.length > 0) {
                this.form.setFieldErrors(storeErrorAs, fieldErrors, false);
            // otherwise clear old errors if there were any
            } else {
                this.form.clearFieldErrors(storeErrorAs, false);
            }
            redrawRequired = true;

            // If validation failed and we shouldn't leave field, force focus back.
            if (stopOnError) this.focusInItem();
        }

        // If other fields on the form have been validated, show/clear their error(s)

        if (allErrors) {
            for (var errorFieldName in allErrors) {
                if (errorFieldName != storeErrorAs) {
                    var errors = allErrors[errorFieldName];
                    if ((errors != null && !isc.isAn.emptyObject(errors)) ||
                        this.form.hasFieldErrors(errorFieldName))
                    {
                        this.form.setFieldErrors(errorFieldName, errors, false);
                        redrawRequired = true;
                    }
                }
            }
        }
        if (redrawRequired) {

            this.redraw();
        }

        return (fieldErrors.length == 0);
    },

    //> @method formItem.setRequired()
    // Setter to mark this formItem as +link{formItem.required}, or not required at runtime.
    // Note that an alternative approach to updating the <code>required</code> flag directly
    // would be to simply use a +link{ValidatorType,requiredIf} type validator.
    // <P>
    // Note that this method will not re-validate this item by default or clear any
    // existing validation errors. If desired, this may be achieved by calling
    // +link{formItem.validate()} or +link{dynamicForm.clearErrors()}.
    // @param required (boolean) new +link{formItem.required} value.
    // @visibility external
    //<
    setRequired : function (required) {
        if (required == this.required) return;
        this.required = required;
        if (this.form == null) return;

        if (required) {
            // getRequiredValidator defined in dataBoundComponent
            var requiredValidator = this.form.getRequiredValidator(this);
            this.addValidator(requiredValidator);
        } else {
            this.removeValidator({type:"required"});
        }
        // redraw the item - this'll refresh the form / refreshing the item title to
        // show / hide the bold prefix / suffix...
        this.redraw();
    },


    addValidator : function (validator) {
        if (this.validators == null) this.validators = [];
        else if (!isc.isAn.Array(this.validators)) this.validators = [this.validators];

        if (this.validators._typeValidators) {
            this.validators = this.validators.duplicate();
        }
        this.validators.add(validator);
    },

    removeValidator : function (validator) {
        if (this.validators == null) return;
        if (!isc.isAn.Array(this.validators)) this.validators = [this.validators];
        if (this.validators._typeValidators) {
            this.validators = this.validators.duplicate();
        }

        // Handle being passed a properties block rather than a pointer to the
        // live object...
        var liveVal = this.validators.find(validator);
        this.validators.remove(liveVal);
    },

    // AutoComplete
    // -----------------------------------------------------------------------------------------

    // change fires on keyPresses that change value

    // intended key behaviors: no changes needed to accomplish these, generally
    // - accept match:
    //   - Navigate away from field: Tab/Shift-Tab
    //     - inline editing: Arrow Up/Down, Enter
    //   - Enter (w/o inline editing)
    // - remove match:
    //   - Delete/Backspace

    //> @method formItem.setAutoComplete()
    // Change the autoCompletion mode for this form field.
    //
    // @param   newSetting (AutoComplete)  new setting
    // @visibility autoComplete
    //<
    setAutoComplete : function (newSetting) {
        this.autoComplete = newSetting;
        this._handleAutoCompleteChange();
    },

    _handleAutoCompleteChange : function () {
        // get cascaded setting
        var setting = this._getAutoCompleteSetting();
        // toggle setting on native element
        if (isc.Browser.isIE && this.hasDataElement()) {
            var element = this.getDataElement();
            if (element) element.autoComplete = (setting == "native" ? "" : "off");
        }
    },

    // get cascaded autoComplete setting
    _getAutoCompleteSetting : function () {
        if (this.autoComplete != null) return this.autoComplete;
        return this.form.autoComplete;
    },

    // whether ISC auto complete is enabled
    autoCompleteEnabled : function () {
        // unsupportable at the moment
        if (isc.Browser.isSafari) return false;

        return this._getAutoCompleteSetting() == "smart";
    },

    // whether we're set to show unique matches only
    uniqueMatchOnly : function () {
        if (this.uniqueMatch != null) return this.uniqueMatch;
        return this.form.uniqueMatch;
    },

    // get candidates for autoCompletion
    getCandidates : function () {
        var candidates = this.autoCompleteCandidates;

        if (candidates == null) {
            var valueMap = this.getValueMap();
            if (valueMap != null) {
                if (isc.isAn.Array(valueMap)) candidates = valueMap;
                else candidates = isc.getValues(valueMap);
            // return values from adjacent records in the dataset, if available
            } else if (this.form.grid) {
                var data = this.form.grid.data;

                // return all values that happen to be cached
                if (isc.isA.ResultSet!=null && isc.isA.ResultSet(data)) candidates = data.getValuesList(this.name);
                // return all values for the column
                else candidates = data.getProperty(this.name);
            }
        }

        // Clear out duplicates from the candidates - if we have a ListGrid with multiple
        // instances of some string in the results, we want to allow autoCompletion to that
        // string.
        // (Note this will NOT clear out strings that are identical except for case, even
        // though they don't autoComplete differently, which is appropriate)
        if (candidates != null) candidates = candidates.getUniqueItems();
        return candidates;
    },

    // get the completion, if any, for this value
    getCompletion : function (base) {
        if (base == null) return;
        var candidates = this.getCandidates();
        if (candidates == null || candidates.length == 0) return;

        var upperBase = base.toUpperCase(),
            uniqueMatchOnly = this.uniqueMatchOnly(),
            firstMatch;
        for (var i = 0; i < candidates.length; i++) {
            var candidate = candidates[i],
                upperCandidate = candidate != null ? candidate.toUpperCase() : null;
            // if the user has exactly typed one of our auto-complete options, don't show
            // any completions

            if (upperCandidate == upperBase) return null;
            if (isc.startsWith(upperCandidate, upperBase)) {
                // return the first match
                if (!uniqueMatchOnly) return candidate;

                // only return a unique match
                if (firstMatch != null) return null;
                firstMatch = candidate;
            }
        }
        return firstMatch;
    },

    // show an autoComplete value, if there's a valid match
    showCompletion : function (value) {

        // drop any existing completion
        this.clearCompletion();

        // check for whether autoCompletion makes sense for this type of FormItem, whether we
        // currently have an element
        if (!this.canAutoComplete || !this.hasDataElement() ||
            !this.autoCompleteEnabled()) return;

        // don't autoComplete on backspace or delete, despite change to form value
        var keyName = isc.EH.lastEvent.keyName;

        if (keyName == "Backspace" || keyName == "Delete") return;

        var completion = this.getCompletion(value);
        if (completion == null) {

            return;
        }

        // set the autocompletion value
        // NOTE: preserve upper/lowercase of typed-in value; we only convert to the
        // completion's casing when the user accepts the completion

        // Blur, and re-focus in the form item (without firing handlers).
        // This is required if the Input Method Editor (IME) is active to get out of the IME
        // mode, so that additional keypresses will overwrite the (selected) completion
        // characters
        // (The IME is used to enter multibyte chars, such as Japanese, using a western
        // keyboard, with multiple keystrokes returning a single character)


        this.form._blurFocusItemWithoutHandler();
        this.form._focusInItemWithoutHandler(this);


        this.setElementValue(value + completion.substring(value.length));
        this._baseValue = value;
        this._pendingCompletion = completion;

        // select the completion
        if (this._isSetSelectionRangeSupported) {
            this.setSelectionRange(value.length, completion.length);
        }
    },


    _handleChangeWithCompletion : function (newValue) {
        var completion = this._pendingCompletion,
            keyName = isc.EH.lastEvent.keyName;

        if (this._completionAcceptKeys[keyName] == true) {
            // if the completion is accepted, switch value to the exact letter case
            // of the completion value.
            // Note that with the exception of the "Enter" key, all completionAcceptKeys are
            // navigation keys that will modify the selection / text insertion point in the
            // text box.
            // If the user hit Enter, always put focus at the end of the word so the user can
            // continue typing.
            // Otherwise, respect wherever the browser natively put the cursor.

            this.acceptCompletion(keyName == this._$Enter);
            return completion;
        }

        var offeredText = completion.substring(this._baseValue.length);


        // if the field value doesn't end with the completion, the user must have typed
        // something over the completion, or deleted some characters, etc - handle as a
        // normal change.
        if (!newValue.endsWith(offeredText)) {
            this.clearCompletion();
            return newValue;
        }



        // if the completion is still selected, override the value in the field, which contains
        // the completion, returning instead the text as it was when the completion was offered
        if (this.getSelectedText() == offeredText) {

            return this._baseValue;
        }

        // otherwise the completion is no longer selected, use the value in the field

        this.clearCompletion();
        return newValue;

        // alternate approach:
        //var charValue = isc.EH.lastEvent.characterValue;
        //if (charValue != null) return newValue;
        //this.logWarn("no change, trimmed value to: " + newValue);
        //return this._baseValue;
    },

    clearCompletion : function () {
        delete this._pendingCompletion;
        delete this._baseValue;
    },

    // accept any pending autoCompletion
    acceptCompletion : function (cursorAtEnd) {
        var completion = this._pendingCompletion;
        if (!completion) return;

        if (this.autoCompleteEnabled()) {


            // Cursor insertion position:
            // The user can accept completion in several ways. Depending on what interaction
            // occurred we may need to change the cursor insertion point after setting the
            // form item element value.
            // Completion tripped by:
            // - taking focus from the field.
            //      No need to worry about cursor insertion position
            // - via various 'navigation' type keypresses (arrow left, home, etc)
            //      In this case rely on native browser behavior to 'do the right thing'
            //      Observed behavior: On right arrow focus goes to the end of the completion.
            //      On Left arrow, behavior varies by browser
            //      Remember the position before changing the element value so we can re-set
            //      to that position.
            // - enter keypress
            //      Explicitly put focus at the end of the field so the user can continue typing
            //      We achieve this by passing in a special 'focusAtEnd' parameter if the
            //      completion was accepted via an enter keypress.
            var selectionRange = cursorAtEnd ? [completion.length, completion.length] : null;
            if (this.getElementValue() != completion) {
                if (!cursorAtEnd && this._isGetSelectionRangeSupported) {
                    selectionRange = this.getSelectionRange();
                }
                this.setElementValue(completion);
            }

            if (this.hasFocus && selectionRange && this._isSetSelectionRangeSupported) {
                this.setSelectionRange(selectionRange[0], selectionRange[1]);
            }
        }
        this.clearCompletion();
    },

    // Text Selection
    // ----------------------------------------------------------------------------------------

    //> @method formItem.setSelectionRange()
    // Puts focus into this form item and selects characters between the given indices.
    // Only applies to drawn text based items.
    // @param start (int) selection starting character index
    // @param end (int) end of selection character index
    // @visibility internal
    //<
    // exposed on textItem / textAreaItem
    _isSetSelectionRangeSupported: true,
    _$character:"character",
    setSelectionRange : function (start, end) {
        // applies only to text items (and subclasses)
        if (!isc.isA.TextItem(this) && !isc.isA.TextAreaItem(this)) return;

        // bail if undrawn
        if (!this.isDrawn()) return;

        if (!isc.isA.Number(start)) start = 0;
        if (!isc.isA.Number(end)) end = 0;
        if (start > end) {
            var newStart = end;
            end = start;
            start = newStart;
        }

        var element = this.getDataElement();
        if (element == null) return;

        if (this.logIsInfoEnabled("nativeFocus") && !this._hasNativeFocus()) {
            this.logInfo("setSelectionRange() about to change focus " + isc.EH._getActiveElementText() +
                          (this.logIsDebugEnabled("traceFocus") ? this.getStackTrace() : ""),
                         "nativeFocus");
        }

        if (isc.Browser.isIE) {
            // IE proprietary API


            isc.EH._settingTextSelection = true;
            var range = element.createTextRange();
            range.collapse(true);
            range.moveStart(this._$character, start);
            range.moveEnd(this._$character, (end-start));
            range.select();
            delete isc.EH._settingTextSelection;
        } else {
            // DOM API, known to be supported by Moz and Safari (as-of circa 2.0 (2006?))
            element.focus();
            element.setSelectionRange(start, end);
        }


        if (end > start) this._lastSelectRange = [start, end];
    },

    //> @method formItem.selectValue()
    // Put focus in this item and select the entire value.
    // Only applies to text based items
    // @visibility internal
    //<
    selectValue : function () {
        var val = this.getElementValue(),
            end = isc.isA.String(val) ? val.length : 0;
        this.setSelectionRange(0,end);
    },

    //> @method formItem.deselectValue()
    // If this item currently has focus, clear the current selection. leaving focus in the item.
    // Has no effect if the item is undrawn or unfocused.
    // Only applies to text-based items.
    // @param [start] (Boolean) By default the text insertion cursor will be moved to the end of the
    //   current value - pass in this parameter to move to the start instead
    // @visibility internal
    //<
    deselectValue : function (start) {
        if (!this.hasFocus) return;
        if (start) this.setSelectionRange(0,0);
        else {
            var val = this.getElementValue(),
                end = isc.isA.String(val) ? val.length : 0;
            this.setSelectionRange(end,end);
        }
    },

    //> @method formItem.getSelectionRange()
    // For text-based items, this method returns the indices of the start/end of the current
    // selection if the item currently has the focus. In browsers other than Internet Explorer 6-9,
    // if this item does not have focus, then this method returns the indices of the start/end
    // of the selection the last time that this item had focus. In IE 6-9, returns null if the
    // item does not have focus.
    // <P>
    // In all browsers, clicking anywhere outside of the item causes the item to lose focus;
    // hence, in IE 6-9, this method will not work in other components' event handlers for
    // certain events. For example, within the +link{Canvas.click(),click()} handler of a button,
    // this item will have already lost focus, so in IE 6-9, this method will return null
    // if called within the button's click() handler. One cross-browser solution to this issue
    // is to save the selection range for later in a +link{Canvas.mouseDown(),mouseDown()} or
    // +link{Canvas.mouseOver(),mouseOver()} handler.
    // <P>
    // Notes:
    // <UL>
    //   <LI>In browsers other than IE 6-9, calling +link{setValue(),setValue()}
    // or otherwise changing the +link{getEnteredValue(),entered value} invalidates the past
    // selection range.</LI>
    //   <LI>The returned indices are indices within the entered value rather than the item's
    // value as returned by +link{FormItem.getValue(),getValue()}.
    // The distinction is particularly important for +link{TextAreaItem}s because browsers
    // normalize the line endings in the <code>&lt;textarea&gt;</code> element's value.
    // Internet Explorer 6, 7, and 8 convert line endings to "\r\n" while other browsers
    // convert line endings to "\n"
    // +externalLink{http://www.w3.org/TR/html5/forms.html#concept-textarea-api-value,as specified by the HTML5 standard}.</LI>
    // </UL>
    // @return (array | null) 2 element array showing character index of the current or past
    // selection's start and end points within this item's +link{getEnteredValue(),entered value}.
    // In IE 6-9, returns null if the item does not have focus.
    // @visibility internal
    //<
    // Exposed on TextItem / TextAreaItem
    _isGetSelectionRangeSupported: true,
    _$EndToEnd:"EndToEnd", _$EndToStart:"EndToStart",
    _$StartToEnd: "StartToEnd", _$StartToStart: "StartToStart",
    _$character:"character",
    getSelectionRange : function (timeCritical) {
        // applies only to text items (and subclasses)
        if (!isc.isA.TextItem(this) && !isc.isA.TextAreaItem(this)) return;

        if (isc.isA.UploadItem(this)) return;

        var element = this.getDataElement();
        if (element == null) return;
        if (isc.Browser.isIE && isc.Browser.version < 10) {
            if (!this._hasNativeFocus()) return null;

            var selectedRange = this._getIESelectionRange();
            if (selectedRange == null) return null;

            if (isc.isA.TextAreaItem(this)) {
                if (!this.supportsSelectionRange) return null;


                var testRange = selectedRange.duplicate();
                testRange.moveToElementText(element);
                var length = testRange.text.length;
                testRange.setEndPoint(this._$StartToStart, selectedRange);
                var i = length - testRange.text.length;
                return [i, i + selectedRange.text.length];
            } else {
                var rangeArray = [],
                    testRange = element.createTextRange();
                if (testRange == null) return null;
                // does the selection end at the end of the input?
                if (testRange.compareEndPoints(this._$EndToEnd, selectedRange) == 0) {

                    rangeArray[1] = testRange.text.length;
                } else {
                    testRange.setEndPoint(this._$EndToEnd, selectedRange);
                    rangeArray[1] = testRange.text.length;
                }

                testRange.setEndPoint(this._$EndToStart, selectedRange);
                rangeArray[0] = testRange.text.length;
                return rangeArray;
            }

        } else if (isc.Browser.isMoz || isc.Browser.isSafari || isc.Browser.isOpera || isc.Browser.isIE) {
            return [element.selectionStart, element.selectionEnd];
        }

    },

    // return true if this item has focus as reported by the browser natively

    _hasNativeFocus : function () {
        var focusElement = this.getFocusElement(),
            activeElement = this.getActiveElement()
        ;
        return (focusElement == activeElement);
    },

    // return the selected text within the form item
    getSelectedText : function () {
        // Only support getting selected text of a textual edit field
        if (!isc.isA.TextItem(this) && !isc.isA.TextAreaItem(this)) {
            return;
        }


        if (isc.Browser.isIE) {

            var range = this._getIESelectionRange();
            if (range) return range.text;
        } else if (isc.Browser.isMoz || isc.Browser.isSafari) {

            var element = this.getElement();
            if (element != null) {
                return element.value.substring(element.selectionStart, element.selectionEnd);
            }
        }
    },


    _IESelectionStuck : function () {
        if (!isc.Browser.isIE) return false;
        try {
            var typeDetail = document.selection ? document.selection.typeDetail : null;
        } catch (e) {
            this.logDebug("Internet explorer native 'stuck focus' state detected");
            return true;
        }
        return false;
    },

    // Helper method for determining form item selection in IE
    _getIESelectionRange : function () {
        if (!isc.Browser.isIE) return;

        if (isc.isA.TextAreaItem(this) && !this.supportsSelectionRange) return null;



        var selection = this.getDocument().selection,
            range = (selection != null ? selection.createRange() : null);

        if (range != null && range.parentElement().id == this.getDataElementId()) return range;
        return null;
    },

    // helper methods to remember the current text selection / text insertion point within
    // some form item.

    rememberSelection : function (timeCritical) {
        // No op if we're not drawn or getSelectionRange() is unsupported
        if (!this.isDrawn() || !this._isGetSelectionRangeSupported) return;

        // applies only to text items (and subclasses)
        if (!isc.isA.TextItem(this) && !isc.isA.TextAreaItem(this)) return;

        // If the field is empty we can skip remembering the insertion point!
        var elementValue = this.getElementValue();
        if (elementValue == isc.emptyString) return;

        this._valueAtLastSelection = elementValue;

        var range = this.getSelectionRange(timeCritical);
        if (range) {
            this._lastSelectionStart = range[0];
            this._lastSelectionEnd = range[1];
        }
    },

    // Reset the selection to whatever was selected when 'rememberSelection' last ran
    resetToLastSelection : function (dataTransform) {
        // If we aren't drawn, or don't have a remembered selection we can't reset it!
        if (!this.isDrawn() || this._lastSelectionStart == null) return;
        var shouldReset,
            elementValue = this.getElementValue(),
            // valueAtLastSelection will always be a string.
            oldValue = this._valueAtLastSelection;
        if (!dataTransform) shouldReset = (elementValue == oldValue);
        else {
                shouldReset = true;
            // if everything was selected pre-change, select everything again, regardless
            // of how the value changed.
            if (this._lastSelectionStart == 0 && this._lastSelectionEnd == oldValue.length) {
                // Shift the end to ensure we select the entire new value
                this._lastSelectionEnd = elementValue.length;

            // Otherwise, if we're just shifting case retain the remembered selection.

            } else {
                if (elementValue.toLowerCase() != oldValue.toLowerCase()) {
                    this._lastSelectionStart = this._lastSelectionEnd = elementValue.length;
                }
            }
        }

        if (shouldReset && this._isSetSelectionRangeSupported) {
            this.setSelectionRange(this._lastSelectionStart, this._lastSelectionEnd);
        }
        delete this._lastSelectionStart;
        delete this._lastSelectionEnd;
        delete this._valueAtLastSelection;
    },


    // Event handling
    // ----------------------------------------------------------------------------------------


    //> @method formItem.handleChange()
    //      Internal method called whenever this item value is modified by user interaction
    //      (Called from 'updateValue()').<br>
    //      Calls call validators on this item if this.validateOnChange is true<br>
    //      Calls any 'change' handler specified for this item.<br>
    //      If validation fails or the change handler returns false, this method will reset the
    //      element to display the current item value, or validator suggested value (rejecting the
    //      change).
    //  @return (boolean)   false if the change was rejected
    //  @see updateValue()
    //  @see change()
    //  @see validateOnChange
    //  @visibility internal
    //<

    handleChange : function (value, oldValue) {

        if (this._changingValue && this.compareValues(value, this._changeValue)) return true;

        // Set the flag to indicate that we're performing a change
        this._changingValue = true;
        // By default we will not modify the value passed in.
        this._changeValue = value;

        // If the value changes due to a validator, etc. we have to know about it so we can
        // call this.setValue()
        var originalValue = value;
        // Handle the kinds of data that get passed around by reference
        if (isc.isA.Date(originalValue)) originalValue = originalValue.duplicate();
        else if (isc.isAn.Array(originalValue)) originalValue = originalValue.duplicate();
        else if (isc.isAn.Object(originalValue)) originalValue = isc.addProperties({}, originalValue);

        // If there's a transformInput method specified, allow it to update the value before
        // we proceed with validation, etc.
        if (this.transformInput) {
            value = this.transformInput(this.form, this, value, oldValue);
        }

        var hadErrorsBefore = this.hasErrors(),
            cancelSave = false,
            fieldErrors = [],
            allErrors = null,
            suggestedValue,
            fieldResult
        ;

        // Set the "setValueCalled" flag to false - we want to detect whether
        // a user calls 'setValue' directly from a change handler (in which case we
        // won't reset to something else)
        this._setValueCalled = false; // setValue(), if called, sets this to true


        // Refuse the new value if it exceeds the specified length for the field if
        // appropriate
        var trimmedValue = this._enforceLengthOnEdit(value, oldValue);
        if (trimmedValue != value) {
            value = trimmedValue;
            cancelSave = (value == oldValue);
        }

        // If we simply reject the change because it exceeds the specified length,
        // no need to run through validation / change notifications.
        // Otherwise do carry on and validate the new (truncated) string.
        if (!cancelSave) {

        // Process all validators on field that are applicable for validateOnChange
        // along with any dependent fields. Note that validateFieldAndDependencies may
        // modify the record so we pass a copy of our current values.

        // Wrap field validation in a queue so that server validators are
        // sent as a single request.
        var wasAlreadyQueuing = isc.rpc.startQueue();

        // NOTE: Do not call this.form.getValues(). If this field has both a edit parser
        // and formatter to handle a value that is an object, the returned object from
        // the parser is likely to be a new object each time even if the entered value
        // did not change. In that case, _updateValue() does not see the redundant update
        // and stop an infinite recursion via: DF.getValues() -> DF.updateFocusItemValue
        // -> FI.updateValue -> FI._updateValue -> FI.mapDisplayToValue -> FI.handleChange.
        var record = isc.addProperties({}, this.form.values),
            validationOptions = {unknownErrorMessage: this.form.unknownErrorMessage,
                                 changing: true},
            fieldResult = this.form.validateFieldAndDependencies (this, this.validators, value,
                                                                   record, validationOptions)
        ;

        // Submit server validation requests queue
        if (!wasAlreadyQueuing) isc.rpc.sendQueue();

        var validationFailed = false;

        if (fieldResult != null) {
            validationFailed = !fieldResult.valid;
            // if the validator returned a resultingValue, use that as the new value
            // whether the validator passed or failed.  This lets us transform data
            // (such as with the mask validator).
            if (fieldResult.resultingValue != null) {
                // remember that value in the values list
                suggestedValue = fieldResult.resultingValue;
            }
            if (!fieldResult.valid) {
                fieldErrors = fieldResult.errors[this.name];
                if (fieldErrors == null) fieldErrors = [];
            }

            // Even though the changed field may be valid, there may be other fields
            // that are no longer valid because of a dependency. These errors should
            // be shown on the form.
            allErrors = fieldResult.errors;
        }


        var undef;
        if (validationFailed && suggestedValue === undef && this._rejectInvalidValueOnChange())
        {
            cancelSave = true;
            suggestedValue = oldValue;

            if (oldValue == null) suggestedValue = null;
        }

        // hang onto any suggested value as our working value (so it gets passed to any
        // change handler we have)
        if (suggestedValue !== undef) value = suggestedValue;

        // If we failed validation, update the errors object on the form


        if (validationFailed) {
            this.clearErrors(true);
            this.setError(fieldErrors);
        // otherwise clear old errors if there were any
        } else if (fieldResult != null && hadErrorsBefore) {
            this.clearErrors(true);
        }

        // Fire the change handler if
        // - we passed validation
        // - or we're supposed to fire the change handler whether an error was found or not
        this._setValueCalled = false; // setValue(), if called, sets this to true
        if ((!validationFailed || this.changeOnError)) {

            // if either change handler returns false, we'll reset to the old value
            if (this.change != null) {
                if (this.change(this.form, this, value, oldValue) == false) {
                    value = oldValue;
                    cancelSave = true;
                }
            }
            // the change handler can do anything including setItems on the form.
            // therefore check whether we got destroyed before continuing with this thread
            if (this.destroyed) return;

            if (!cancelSave && this.form && this.form.itemChange != null) {
                if (this.form.itemChange(this, value, oldValue) == false) {
                    value = oldValue;
                    cancelSave = true;
                }
            }
        }
        }


        var userCalledSetValue = this._setValueCalled;
        // if an error was found, or another value was suggested, set the value now

        var valueChanged = !this.compareValues(value, originalValue);
        if ((cancelSave || valueChanged) && !userCalledSetValue)
        {
            // If we cancelSave, avoid having setValue attempt to reset the cursor
            // - we revert to the pre change selection below.
            this.setValue(value, null, true, cancelSave);
            // Reset the selection to whatever it was BEFORE the change occurred if change /
            // validators reset to the old value
            // Note that "cancelSave" specifically indicates we're rejecting the
            // change (and resetting to the original value).
            if (cancelSave && this.maintainSelectionOnTransform) {
                this._revertToPreChangeSelection();
            }
        }

        if (this._setValueCalled) this._changeValue = this._value;

        // if this item wants to redraw the form when it's changed,
        // or an error was found in validator, redraw the form
        // or the item had errors before but they are now cleared, redraw the form
        if ((this.redrawOnChange || fieldErrors.length > 0 ||
            (fieldErrors.length == 0 && fieldResult != null && hadErrorsBefore)))
        {
            this.redraw();
        }

        // If other fields on the form have been validated, show/clear their error(s)

        if (allErrors) {
            for (var errorFieldName in allErrors) {
                if (errorFieldName != this.name) {
                    this.form.setFieldErrors(errorFieldName, allErrors[errorFieldName], true);
                }
            }
        }

        // Avoid showing completion if focus is being taken from this item
        if (!cancelSave && this.hasFocus) this.showCompletion(value);
        // Clear out this._changingValue - we're done with our change handler
        // Leave this._changeValue in place, this is used by the calling method to determine
        // the result of the change handler.
        delete this._changingValue;


        return (!cancelSave);
    },

    _enforceLengthOnEdit : function (value, oldValue) {
        if (this.length != null &&
            value != null && isc.isA.String(value) &&
            value.length > this.length)
        {
            // We don't want to just trim the new value as the user may put the cursor at
            // the front of a "full" text item, and press a key, and in this case we want
            // to reject the change and reset to the old string (not show a changed string
            // with the new character at the front).
            // However if the existing value was shorter than the specified length we don't
            // want to reject the change entirely - for example a user might paste
            // 11 characters into an empty field with a 10-char limit and it's better to
            // show the truncated string than to refuse the change entirely.
            // Therefore reset to the old value iff it exactly matches the specified length
            // otherwise trim the new string.

            var resetToOldValue = oldValue != null && isc.isA.String(oldValue) &&
                                    oldValue.length == this.length;
            if (resetToOldValue) {
                value = oldValue;
            } else {
                value = value.substring(0, this.length);
            }
        }
        return value;
    },

    // if this.validateOnChange is true, and validation fails with no suggested value, should
    // we revert to the previous value, or allow the bad value to be displayed along with the
    // validation error?
    _rejectInvalidValueOnChange : function () {
        return (this.rejectInvalidValueOnChange != null) ? this.rejectInvalidValueOnChange
                                                           : this.form.rejectInvalidValueOnChange;
    },

    // compareValues - undocumented (non obfuscated) helper method: do 2 possible values for
    // this item match

    compareValues : function (value1, value2) {
        // comparison implemented on the DynamicForm class directly
        var compareValues = isc.DynamicForm.compareValues;
        if (this.multiple) {
            if (!(value1 == null || isc.isAn.Array(value1))) {
                if (!this._propagateMultiple) this.logWarn(
                        "compareValues - this is a multiple FormItem but compareValues was " +
                        "called with a non-null first argument `value1` that is not an array.");
                value1 = [value1];
            }
            if (!(value2 == null || isc.isAn.Array(value2))) {
                if (!this._propagateMultiple) this.logWarn(
                        "compareValues - this is a multiple FormItem but compareValues was " +
                        "called with a non-null second argument `value2` that is not an array.");
                value2 = [value2];
            }

            if (value1 == null && value2 == null) {
                return true;
            } else if (value1 == null || value2 == null) {
                return false;
            }

            if (value1.length != value2.length) {
                return false;
            }
            var i = 0, len = value1.length;
            while (i < len && compareValues(value1[i], value2[i])) {
                ++i;
            }
            return (i == len);
        }
        return compareValues(value1, value2);
    },


    // Handle a change event from an element.  Called directly by handlers on the native HTML
    // element
    elementChanged : function () {
        // we sometimes call this method synthetically
        var inThread = (isc.EH._thread != null);
        if (!inThread) isc.EH._setThread("ICHG");

        this.logDebug("native change");
        // updateValue() will handle firing any validators, validate-on-change change handlers,
        // and will save the value.
        if (isc.Log.supportsOnError) {
            this.updateValue();
        } else {
            try {
                this.updateValue();
            } catch (e) {
                isc.Log._reportJSError(e);
            }
        }

        if (!inThread) isc.EH._clearThread();

        // return true so the event terminates normally (and the user can leave the field)

        return true;
    },

    // Inactive Editor events
    // We sometimes write out "inactive" editor HTML for items - these match the live item
    // elements appearance-wise but are non editable and have limited interactivity
    // [used for printing / ListGrid.alwaysShowEditors]
    // All interactivity on these items will go through handleInactiveEditorEvent -
    // Given an event type such as 'click' check for the presence of a handler
    // named "inactiveEditor" + event type - EG "inactiveEditorClick" - if present fire it
    // passing in the inactiveContext set up when the HTML was generated and the standard
    // itemInfo which will include where over the inactive content the event occurred
    // [giving us potential support for inactive clicks on icons, etc]

    // Lazily created map of standard event names to 'inactiveEditor' event names
    _inactiveEditorEvents:{
    },
    _handleInactiveEditorEvent : function (eventType, inactiveContext, itemInfo) {

        if (this.logIsDebugEnabled("inactiveEditorHTML")) {
            this.logDebug("handling inactive editor event:" + eventType + ", inactive context:" +
                   this.echo(inactiveContext), "inactiveEditorHTML");
        }
        var eventName = this._inactiveEditorEvents[eventType];
        if (eventName == null) {
            eventName = this._inactiveEditorEvents[eventType] =
                "inactiveEditor" + eventType.substring(0,1).toUpperCase() + eventType.substring(1);
        }

        if (this[eventName] != null) {
            return this[eventName](inactiveContext, itemInfo);
        }
    },

    // Most of our handlers are stringMethods which take 2 params, form and item.
    // To avoid code duplication have a 'fireStandardHandler' method to handle this pattern.
    _fireStandardHandler : function (handlerName) {
        this.convertToMethod(handlerName);
        return this[handlerName](this.form, this, isc.EH.lastEvent);
    },


    //>    @method    formItem.handleTitleClick()
    // Handle a click event from this items title cell.
    //        @group    event handling
    //<
    handleTitleClick : function () {
        if (this.isDisabled()) return;
        //>EditMode
        if (this.editingOn) {
            this.editClick();
            // Also fire normal click event in EditMode, so we know to hilite the item
            this.handleClick();
            return false;
        }
        //<EditMode
        return this._fireStandardHandler("titleClick");
    },

    //>    @method    formItem.handleTitleDoubleClick()
    // Handle a double click event from this items title cell.
    //        @group    event handling
    //<
    handleTitleDoubleClick : function () {
        if (this.isDisabled()) return;
        return this._fireStandardHandler("titleDoubleClick");
    },

    //>    @method    formItem.handleClick()
    // Handle a click event over this form item
    //        @group    event handling
    //<
    handleClick : function () {
        //>EditMode
        if (this.editingOn) {
            isc.EditContext.selectCanvasOrFormItem(this, true);
            return false;
        }
        //<EditMode
        if (this.isDisabled()) return;
        return this._fireStandardHandler("click");
    },

    //>    @method    formItem.handleDoubleClick()
    // Handle a double click event over this form item
    //        @group    event handling
    //<
    handleDoubleClick : function () {
        if (this.isDisabled()) return;
        return this._fireStandardHandler("doubleClick");
    },

    //>    @method    formItem.handleCellClick()
    // Handle a click event from an enclosing cell
    //        @group    event handling
    //<
    handleCellClick : function () {
        if (this.isDisabled()) return;
        return this._fireStandardHandler("cellClick");
    },

    //>    @method    formItem.handleCellDoubleClick()
    // Handle a double click event from an enclosing cell
    //        @group    event handling
    //<
    handleCellDoubleClick : function () {
        if (this.isDisabled()) return;
        return this._fireStandardHandler("cellDoubleClick");
    },


    _handleElementChanged : function () {
        return this.form.elementChanged(this.getID());
    },

    // Handlers for mouseOver/Move/Out events (sent from the Form)
    // Fires developer specified mouseOver/move/out and titleOver/move/out handlers if present.
    // handleMouseMove also handles showing icon prompts in the Hover canvas.

    handleMouseMove : function () {

        if (!this.isDisabled() && (this.showValueIconOver || this.showValueIconDown)) {
            var itemInfo = isc.EH.lastEvent.itemInfo,
                overItem = (itemInfo.overElement || itemInfo.overTextBox ||
                            itemInfo.overControlTable),
                iconState = this._iconState;

            if (overItem) {
                // If appropriate show the 'over' version of the valueIcon
                // _mouseIsDown is a flag set when the user does a mouseDown over the item and
                // cleared on mouseUp. If this flag is set, the user did a mouseDown on the
                // item, moved the mouse off, and back on without releasing the mouse, so we
                // want to show down rather than over state
                if (this._mouseIsDown && this.showValueIconDown) {
                    if (iconState != this._$Down) {
                        this._iconState = this._$Down;
                        this._updateValueIcon(this.getValue());
                    }
                } else if (this.showValueIconOver && iconState != this._$Over) {
                    this._iconState = this._$Over;
                    this._updateValueIcon(this.getValue());
                }
            } else {

                var expectedState = (this.showValueIconFocused && this.showValueIconOver
                                     && this.hasFocus) ? this._$Over : null;
                if (iconState != expectedState) {
                    this._iconState = expectedState;
                    this._updateValueIcon(this.getValue());
                }
            }
        }

        if (this._fireStandardHandler("mouseMove") == false) return false;
    },

    handleMouseOver : function () {
        // set up the hover to show a prompt for this cell if appropriate
        isc.Hover.setAction(this, this._handleHover, null, this._getHoverDelay());

        return this._fireStandardHandler("mouseOver");
    },
    handleMouseOut : function () {
        // Clear any valueIcon 'over' state when the user moves off the item


        var expectedState = (this.showValueIconFocused && this.showValueIconOver
                              && this.hasFocus) ? this._$Over : null;
        if (this._iconState != expectedState) {
            this._iconState = expectedState;
            this._updateValueIcon(this.getValue());
        }

        // Clear the hover set up for this item

        this.stopHover();
        return this._fireStandardHandler("mouseOut");
    },

    handleMouseDown : function () {
        this._mouseIsDown = true;
        isc.Page.setEvent(isc.EH.MOUSE_UP, this, isc.Page.FIRE_ONCE, "_clearMouseDown");

        var itemInfo = isc.EH.lastEvent.itemInfo,
            inactiveContext = itemInfo.inactiveContext;
        if (inactiveContext != null) {
            return this.form.bubbleInactiveEditorEvent(this, "mouseDown", itemInfo);
        }

        if (!this.isDisabled() && this.showValueIconDown) {
            var overItem = (itemInfo.overElement || itemInfo.overTextBox || itemInfo.overControlTable);
            if (overItem) {
                this._iconState = this._$Down;

                this._updateValueIcon(this.getValue());
            }
        }
        if (this.mouseDown) return this._fireStandardHandler("mouseDown");
    },

    //> @method FormItem.stopHover()    [A]
    // This method is fired when the user rolls off this item (or the title for this item) and
    // will clear any hover canvas shown by the item.
    // @group Hovers
    // @visibility external
    //<
    stopHover : function () {
        isc.Hover.clear();
    },

    // _clearMouseDown fired on mouseUp to clear valueIcon mouseDown state.
    // (Fires whether the mouse is over this icon or not - this is how we track the case of
    // the user doing a mouseDown over us, moving off, then back on without releasing the mouse)
    _clearMouseDown : function () {
        this._mouseIsDown = null;
        // If the mouse is over us, we will be in state "Down" - in this case reset to Over
        if (this._iconState == this._$Down) {
            this._iconState = this.showValueIconOver ? this._$Over : null;
            this._updateValueIcon(this.getValue());
        }
    },

    handleMouseStillDown : function (event) {
        if (this.mouseStillDown) {
            return this._fireStandardHandler("mouseStillDown");
        }
    },

    // Helper method - how long should we delay before showing hovers?
    _getHoverDelay : function () {
        return this.hoverDelay != null ? this.hoverDelay : this.form.itemHoverDelay;
    },

    handleTitleMove : function () {
        return this._fireStandardHandler("titleMove");
    },

    handleTitleOver : function () {
        // set up the hover to show a prompt for this cell if appropriate
        isc.Hover.setAction(this, this._handleTitleHover, null, this._getHoverDelay());

        return this._fireStandardHandler("titleOver");
    },
    handleTitleOut : function () {
        // clear the hover event set up on this item title
        this.stopHover();
        return this._fireStandardHandler("titleOut");
    },

    handleTextBoxMove : function () {
        return this._fireStandardHandler("textBoxMove");
    },

    handleTextBoxOver : function () {
        isc.Hover.setAction(this, this._handleTextBoxHover, null, this._getHoverDelay());
        return this._fireStandardHandler("textBoxOver");
    },
    handleTextBoxOut : function () {
        this.stopHover();
        return this._fireStandardHandler("textBoxOut");
    },

    // Icon events:

    // On icon focus and blur, update the icon appearance (showing the over style), if showOver
    // is true, and show the icon prompt in the window's status bar (rather than the href of
    // the link).
    // Fire _nativeElementFocus() on the form item to handle default focus behavior
    // Note that Safari doesn't fully support focus/blur on icons - see comments in
    // getIconHTML() for further details
    _iconFocus : function (id, element) {
        var icon = this.getIcon(id);
        if (icon != null) {

            var prompt = (icon.prompt != null ? icon.prompt : this.iconPrompt)
            window.status=prompt;

            if (this._iconShouldShowFocused(icon)) {
                this._setIconImgState(icon, false, true);
            }
            else if (this._iconShouldShowOver(icon)) this._setIconImgState(icon, true);
        }
        return this._nativeElementFocus(element,this);
    },

    _iconBlur : function (id, element) {
        var icon = this.getIcon(id);
        if (icon != null) {

            window.status="";

            // If we're showing the icon's focused state, clear it if showFocusedWithItem is false
            // If showFocusedWithItem is true, clear if this.showIconsOnFocus is false since
            // if focus goes to another element within the item, the focus styling will be
            // reapplied, and if showIconsOnFocuse is true and focus goes outside the item the
            // icon will be hidden in any case.
            var showFocused = this._iconShouldShowFocused(icon),
                showFocusedWithItem = icon.showFocusedWithItem != false,
                showOnFocus = this.showIconsOnFocus;
            if (showFocused && (!showOnFocus || !showFocusedWithItem)) {
                this._setIconImgState(icon, false, false);
            }
            if (this._iconShouldShowOver(icon)) this._setIconImgState(icon, false, false);
        }
        return this._nativeElementBlur(element,this);
    },



    // On icon mouseOver / mouseOut,
    // - update the icon's appearance if showOver is true.
    // - show the prompt in a hover if there is a prompt defined for the icon
    // - update window.status to also show the prompt.
    _iconMouseOver : function (id) {
        // We use the standard icon code to write out our validation error icon - if this is
        // where the event occurred, pass that through to a separate handler.
        if (id == this.errorIconName) return this._handleErrorIconMouseOver();
        var icon = this.getIcon(id);
        if (icon != null) {

            // If appropriate set the 'over' state for the icon img
            if (this._iconShouldShowOver(icon)) this._setIconImgState(icon, true);

            // Set up the hover action on this item:
            // Remember which icon we're over, then set hover action (will fire instantly if
            // the hover is already up)
            this._lastPromptIcon = icon;
            isc.Hover.setAction(this, this._handleIconHover, null, this._getHoverDelay());


            var prompt = (icon.prompt != null ? icon.prompt : this.iconPrompt)
            window.status=prompt;
            return true;
        }
    },

    _iconMouseOut : function (id) {
        if (id == this.errorIconName) return this._handleErrorIconMouseOut();
        var icon = this.getIcon(id);
        if (icon != null) {
            window.status = "";

            if (this._iconShouldShowOver(icon)) this._setIconImgState(icon, false);

            // Reset the hover action to show the hover for the item as a whole
            delete this._lastPromptIcon;
            isc.Hover.setAction(this, this._handleHover, null, this._getHoverDelay());

            return true;
        }
    },


    _iconMouseMove : isc.Class.NO_OP,

    //> @method    formItem._iconClick()  (I)
    // Handle a click on a form item icon.  Fires the click action defined for the icon.
    //  @group  appearance, events
    //
    //<
    _iconClick : function (id) {
        var icon = this.getIcon(id);
        if (icon == null) return;
        if (this.iconIsDisabled(icon)) return;
        if (icon.click != null) {
            // Note - can't use 'convertToMethod' on the icon object, as it has no registry of
            // stringMethods.  Must use 'replaceWithMethod' instead.
            if (!isc.isA.Function(icon.click)) {
                isc.Func.replaceWithMethod(icon, "click", "form,item,icon");
            }
            if (icon.click(this.form, this, icon) == false) return false;
        }

        if (this.iconClick) this.iconClick(this.form, this, icon);
    },


    _$Enter:"Enter",
    _$Space:"Space",
    iconClickOnEnter:true,
    iconClickOnSpace:true,
    _iconKeyPress : function (id) {
        var icon = this.getIcon(id);
        if (icon) {
            var keyName = isc.EH.getKey(),
                character = isc.EH.getKeyEventCharacter();
            if (icon.keyPress) {
                // Note - can't use 'convertToMethod' on the icon object, as it has no registry
                // of  stringMethods.  Must use 'replaceWithMethod' instead.
                if (!isc.isA.Function(icon.keyPress)) {
                    isc.Func.replaceWithMethod(icon, "keyPress", "keyName, character,form,item,icon");
                }
                if (icon.keyPress(keyName, character, this.form, this, icon) == false)
                    return false;
            }
            if (this.iconKeyPress) this.iconKeyPress(keyName, character, this.form, this, icon);

            // by default we always have "enter" or "space" fire the icon's click action
            if ((this.iconClickOnEnter && keyName == this._$Enter) ||
                (this.iconClickOnSpace && keyName == this._$Space))
            {
                if (this._iconClick(icon) == false) return false;
            }

        }
    },

    // error icon events

    _handleErrorIconMouseOver : function () {
        isc.Hover.setAction(this, this._handleErrorIconHover, null, this._getHoverDelay());
    },

    _handleErrorIconMouseOut : function () {
        isc.Hover.setAction(this, this._handleHover, null, this._getHoverDelay());

    },

    _handleErrorIconHover : function () {
        //!DONTCOMBINE
        if (this.itemHover && this.itemHover(this, this.form) == false) return false;


        var promptString = this.shouldShowErrorIconPrompt()
                            ? isc.FormItem.getErrorPromptString(this._currentIconError)
                            : isc.emptyString;

        if (promptString && !isc.is.emptyString(promptString))
            isc.Hover.show(promptString, this.form._getHoverProperties(this));
        else isc.Hover.setAction(this, this._handleHover, null, this._getHoverDelay());
    },


    // Hover events ---------------------------------------------------------------------------

    //_handleHover / _handleTitleHover fired when the user hovers over this item/title.
    //
    // Fire any custom hover-handler for the item.
    // If the custom handler does not return false, show a hover canvas for this item.
    // contents for the hover derived from item.itemHoverHTML() or form.itemHoverHTML()
    // (default implementation at the form level shows item prompt)
    _handleHover : function (event) {
        // Note itemHover / titleHover registered as stringMethods
        if (this.itemHover && this.itemHover(this, this.form) == false) return false;

        var HTML;
        if (this.itemHoverHTML) HTML = this.itemHoverHTML(this, this.form);
        else HTML = this.form.itemHoverHTML(this, this.form);

        this.form._showItemHover(this, HTML);
    },

    _handleTitleHover : function (event) {
        //!DONTCOMBINE
        if (!this.showClippedTitleOnHover || !this.form.titleClipped(this)) return;

        if (this.titleHover && this.titleHover(this, this.form) == false) return false;

        var HTML;
        if (this.titleHoverHTML) HTML = this.titleHoverHTML(this, this.form);
        else HTML = this.form.titleHoverHTML(this, this.form);

        this.form._showItemHover(this, HTML);
    },

    _handleTextBoxHover : function (event) {
        //!DONTCOMBINE


        if ((this.itemHoverHTML ||
             (this.form.itemHoverHTML !== isc.DynamicForm._defaultItemHoverHTMLImpl)) &&
            !this.valueHover &&
            this.form.valueHoverHTML === isc.DynamicForm._defaultValueHoverHTMLImpl)
        {
            return this._handleHover(event);
        }

        if (!this.showClippedValueOnHover || !this.valueClipped()) return this._handleHover(event);

        if (this.valueHover && this.valueHover(this, this.form) == false) return false;

        var HTML;
        if (this.valueHoverHTML) HTML = this.valueHoverHTML(this, this.form);
        else HTML = this.form.valueHoverHTML(this, this.form);

        this.form._showItemHover(this, HTML);
    },

    // _handleIconHover: helper method fired when the user hovers over an icon.  Only fired if
    // the hovered-over icon has a prompt to show.
    // Call 'itemHover()' if defined *(allows the user to suppress the prompt), and then show
    // the icon prompt.
    _handleIconHover : function () {
        // note: we don't want to show the icon prompt if the item level 'itemHover' method
        // returns false.

        //!DONTCOMBINE
        if (this.itemHover && this.itemHover(this, this.form) == false) return false;
        var icon = this._lastPromptIcon,
            prompt = this.getIconPrompt(icon);
        if (prompt && !isc.is.emptyString(prompt))
            isc.Hover.show(prompt, this.form._getHoverProperties(this));
        // If there's no prompt, the standard item hover to show the appropriate HTML
        // (will get shown synchronously since the hover's already up)
        else isc.Hover.setAction(this, this._handleHover, null, this._getHoverDelay());
    },

    //> @method formItem.itemHover()     (A)
    // Optional stringMethod to fire when the user hovers over this item.
    // Return false to suppress default behavior of showing a hover canvas containing the
    // HTML returned by <code>formItem.itemHoverHTML()</code> /
    // <code>form.itemHoverHTML()</code>.
    //
    // @param  item (FormItem)     Pointer to this item
    // @param  form (DynamicForm)  This items form
    // @group Hovers
    // @see FormItem.titleHover()
    // @see FormItem.itemHoverHTML()
    // @visibility external
    //<

    //> @method formItem.titleHover()     (A)
    // Optional stringMethod to fire when the user hovers over this item's title.
    // Return false to suppress default behavior of showing a hover canvas containing the
    // HTML returned by <code>formItem.titleHoverHTML()</code> /
    // <code>form.titleHoverHTML()</code>.
    //
    // @param  item (FormItem)     Pointer to this item
    // @param  form (DynamicForm)  This items form
    // @group Hovers
    // @see FormItem.itemHover()
    // @see FormItem.titleHoverHTML()
    // @visibility external
    //<

    //> @method formItem.valueHover()     (A)
    // Optional stringMethod to fire when the user hovers over this item's value.
    // Return false to suppress default behavior of showing a hover canvas containing the
    // HTML returned by +link{FormItem.valueHoverHTML()} / +link{DynamicForm.valueHoverHTML()}.
    //
    // @param  item (FormItem)     Pointer to this item
    // @param  form (DynamicForm)  This items form
    // @group Hovers
    // @see FormItem.itemHover()
    // @visibility external
    //<


    //> @method formItem.titleHoverHTML()     (A)
    // If defined, this method should return the HTML to display in a hover canvas when the
    // user holds the mousepointer over this item's title.  Return null to suppress the hover
    // canvas altogether.
    // <P>
    // If not defined, +link{DynamicForm.titleHoverHTML()} will be evaluated to determine
    // hover content instead.
    //
    // @param  item (FormItem)     Pointer to this item
    // @param  form (DynamicForm)  This items form
    // @return (HTMLString) HTML to be displayed in the hover
    // @group Hovers
    // @see FormItem.prompt
    // @see FormItem.titleHover()
    // @see FormItem.itemHoverHTML()
    // @see FormItem.showClippedTitleOnHover
    // @visibility external
    //<

    //> @method formItem.valueHoverHTML()     (A)
    // If defined, this method should return the HTML to display in a hover canvas when the
    // user holds the mousepointer over this item's value.  Return null to suppress the hover
    // canvas altogether.
    // <p>
    // If not defined, +link{DynamicForm.valueHoverHTML()} will be evaluated to determine
    // hover content instead.
    //
    // @param  item (FormItem)     Pointer to this item
    // @param  form (DynamicForm)  This items form
    // @return (HTMLString) HTML to be displayed in the hover
    // @group Hovers
    // @see FormItem.showClippedValueOnHover
    // @visibility external
    //<

    //> @method formItem.itemHoverHTML()     (A)
    // If defined, this method should return the HTML to display in a hover canvas when the
    // user holds the mousepointer over this item.  Return null to suppress the hover
    // canvas altogether.
    // <P>
    // If not defined, <code>dynamicForm.itemHoverHTML()</code> will be evaluated to
    // determine hover content instead.
    //
    // @param  item (FormItem)     Pointer to this item
    // @param  form (DynamicForm)  This items form
    // @return (HTMLString) HTML to be displayed in the hover
    // @group Hovers
    // @see FormItem.prompt
    // @see FormItem.itemHover()
    // @see FormItem.titleHoverHTML()
    // @example itemHoverHTML
    // @visibility external
    //<



    getGlobalTabIndex : function () {
        if (this.globalTabIndex == null) {
            if (this.tabIndex == -1) this.globalTabIndex = -1;
            else {
                var formIndex = this.form.getTabIndex(),
                    localTabIndex = this.getTabIndex();
                if (formIndex == -1) return -1;
                return (formIndex + localTabIndex);
            }
        }
        return this.globalTabIndex;
    },

    // getTabIndex() - returns the local tabIndex for this item.
    // Probably only to be used by internally
    getTabIndex : function () {
        if (this.tabIndex != null) return this.tabIndex;
        if (this.globalTabIndex || !this._canFocus()) return null;
        if (this._localTabIndex == null) {
            this.form._assignTabIndices();
        }
        return this._localTabIndex;
    },

    // setTabIndex() / setGlobalTabIndex()
    // force the form to redraw so the HTML is updated to reflect the changes in tabIndex
    setGlobalTabIndex : function (index) {
        this.globalTabIndex = index;
        this._setElementTabIndex(index);

    },
    setTabIndex : function (tabIndex) {
        this.globalTabIndex = null;
        this.tabIndex = tabIndex;

        this._setElementTabIndex(tabIndex);
    },

    // _getElementTabIndex() returns the tab index to actually write into the element.
    // This may differ from the result of this.getGlobalTabIndex() to allow (for example)
    // taking form items out of the page's tab order without forgetting their global tab index.

    _getElementTabIndex : function (ignoreDisabled) {
        //!DONTCOMBINE
        if (this.isInactiveHTML() ||
            (!ignoreDisabled && this.isDisabled()))
        {
            return -1;
        }
        if (this._elementTabIndex != null) return this._elementTabIndex;
        return this.getGlobalTabIndex();
    },

    // _setElementTabIndex() - update the tab index written into the HTML element for this
    // form item.

    _setElementTabIndex : function (tabIndex) {

        // remember the tabIndex passed in.
        this._elementTabIndex = tabIndex;

        // If we can't accept focus, or aren't drawn/visible just bail
        if (!this._canFocus() || !this.isDrawn()) return;

        // Default implementation will set the tabIndex on whatever element is returned by
        // this.getFocusElement().
        // Note that this may not work for all items - for example items with multiple elements
        // in the DOM.
        if (this.getFocusElement() != null) {
            isc.FormItem.setElementTabIndex(this.getFocusElement(), tabIndex);

            // Also update any form item icons.
            // Note that we are only doing this if we have an element, because if we do not
            // the redraw (below) is required in any case, and will cause the icons' tab index
            // to be updated.
            this._updateIconTabIndices();

        } else {
            // Make the default implementation for form items with no 'focusElement' to redraw
            // the form - this should reset the innerHTML of the element to whatever is
            // required for an updated tabIndex in most cases.

            this.redraw("set tab index");
        }
    },

    // Our element tab index is typically derived based on a local offset from the form's tab-index
    // This notification is fired when the form's tabIndex changes, and allows us to
    // update our element tab index if appropriate
    updateTabIndex : function () {
        if (!this._canFocus() || !this.isDrawn() || this.isDisabled()) return;

        var gti = this.getGlobalTabIndex();
        if (this._elementTabIndex != gti) this._setElementTabIndex(gti);
    },

    // returns the tab index for some icon
    _getIconTabIndex : function (icon) {
        // We want the developer to be able to specify tabIndex -1 on icons

        if (icon.tabIndex == -1 || this.iconIsDisabled(icon)) return -1;
        // Pass in the param to avoid returning -1 if the item is disabled - this allows us to
        // leave 'neverDisable' icons in the tab-order for the page.
        return this._getElementTabIndex(true);
    },

    // Helper method to iterate through this item's icons, and update all their tab indices.
    _updateIconTabIndices : function () {
        var icons = [];
        icons.addList(this.icons);
        if (this._shouldShowPickerIcon()) icons.add(this.getPickerIcon());

        for (var i = 0; i < icons.length; i++) {
            var icon = icons[i];
            if (!icon || icon.imgOnly) continue;
            var iconElement = this._getIconLinkElement(icon);

            if (iconElement != null) {
                isc.FormItem.setElementTabIndex(iconElement, this._getIconTabIndex(icon));
            }

        }
    },

    //> @method formItem.setDisabled()    (A)
    //  Set this item to be enabled or disabled at runtime.
    //  @param  disabled (boolean)   true if this item should be disabled
    //  @see    attr:FormItem.disabled
    // @see setCanEdit()
    // @group enable
    //  @visibility external
    //<
    setDisabled : function (disabled) {
        var wasDisabled = this.isDisabled();
        this.disabled = disabled;
        var isDisabled = this.isDisabled();
        if (wasDisabled != isDisabled) this.updateDisabled();
    },

    //> @method formItem.setShowDisabled()
    // Setter method for +link{formItem.showDisabled}
    // @param showDisabled (boolean) new showDisabled setting
    // @visibility external
    //<
    setShowDisabled : function (showDisabled) {
        this.showDisabled = showDisabled;
        this.updateDisabled();
    },

    // updateDisabled - helper method to update the form item to reflect it's enabled/disabled
    // state

    updateDisabled : function () {
        var disabled = this.isDisabled();
        this._setElementEnabled(!disabled);
        this._setIconsEnabled();
        // update the valueIcon if we have one
        this._updateValueIcon(this.getValue());
        // UpdateState is a catch-all method that updates the css classes applied to our elements
        // to reflect the 'disabled' versions

        if (this.showDisabled) this.updateState();

    },

    // deprecated corollary to setEnabled()
    setEnabled : function (enabled) {
        return this.setDisabled(!enabled);
    },

    //> @method formItem.isDisabled()    (A)
    //  Is this item disabled?
    //  @return disabled (Boolean)   true if this item is be disabled
    //  @see    attr:FormItem.disabled
    // @group enable
    //  @visibility external
    //<
    isDisabled : function () {

        if (this.form == null) return this.disabled;

        var disabled = this.disabled || this.renderAsDisabled();
        // For members of containerItems, inherit the disabled-ness of the parent item
        if (!disabled) {
            if (this.parentItem != null) disabled = this.parentItem.isDisabled();
            else {
                disabled = this.form.isDisabled();

                // Allow disabled-ness to be inherited from either the form or the containerWidget
                if (!disabled && this.containerWidget != this.form) disabled = this.containerWidget.isDisabled();
            }
        }
        return disabled;

    },

    //>@method formItem.enable()
    // Set this item to be enabled at runtime.
    // @see attr:FormItem.disabled
    // @group enable
    // @visibility external
    //<
    enable : function () {
        this.setDisabled(false);
    },

    //>@method formItem.disable()
    // Set this item to be disabled at runtime.
    // @see attr:FormItem.disabled
    // @group enable
    // @visibility external
    //<
    disable : function () {
        this.setDisabled(true);
    },

    // _setElementEnabled()
    // Actually update the HTML to mark the data element as enabled / disabled
    // (Overridden where appropriate by subclasses)

    _setElementEnabled : function (enabled) {
        if (this.hasDataElement()) {
            var element = this.getDataElement();
            if (element) {
                element.disabled = !enabled;
                element.tabIndex = this._getElementTabIndex();
                // If we use an 'eventMask' clear it out if we're being enabled, or write it
                // over the native form item element if we're being disabled.
                if (this.useDisabledEventMask()) {
                    var maskElement = this._getEventMaskElement();
                    if (maskElement && (!maskElement.getAttribute ||
                        maskElement.getAttribute("isDisabledEventMask") != "true"))
                    {
                        maskElement = null;
                    }

                    if (enabled && maskElement) {
                        isc.Element.clear(maskElement);
                    } else if (!enabled && !maskElement) {
                        isc.Element.insertAdjacentHTML(element, "beforeBegin", this._getEventMaskHTML());
                    }
                }
            }
        } else if (this._canFocus()) {
            var element = this.getFocusElement();
            if (element) element.tabIndex = this._getElementTabIndex();
        }
    },
    // _setIconsEnabled()
    // Update all icons' html to match a new enabled state
    _setIconsEnabled : function () {
        if (this.showPickerIcon) {
            var pickerIcon = this.getPickerIcon();
            this.setIconEnabled(pickerIcon);
        }
        if (!this.icons || this.icons.length < 1) return;
        for (var i = 0; i< this.icons.length; i++) {
            this.setIconEnabled(this.icons[i]);
        }
    },

    iconIsDisabled : function (icon) {
        icon = this.getIcon(icon);
        if (!icon) return;
        // if we're in a disabled container that trumps 'neverDisable'
        if (this.containerWidget && this.containerWidget.isDisabled()) return true;
        if (icon.neverDisable) return false;
        // disabled at the item level - trumps read-only
        if (this.isDisabled()) return true;
        // Check for flag to see if the icon shoudl be disabled if the item is readOnly
        var disableOnReadOnly = icon.disableOnReadOnly;
        if (disableOnReadOnly == null) {
            disableOnReadOnly = this.disableIconsOnReadOnly;
        }
        if (disableOnReadOnly) return this.isReadOnly();
        return false;
    },

    //> @method formItem._canFocus()    ()
    //  Return true if the form item can accept keyboard focus
    //  @group  visibility
    //  @return (boolean)   true if the form item is visible
    //<

    _canFocus : function () {
        // If there's an explicit 'canFocus' property, respect it.
        if (this.canFocus != null) return this.canFocus;
        return this.hasDataElement();
    },

    // Should we write out focus-attributes on the textBox [or if required, a focusProxy]
    _canFocusInTextBox : function () {
        return this._canFocus();
    },

    //> @method formItem.getCanFocus()
    // Returns true for items that can accept keyboard focus such as data items
    // (+link{TextItem,TextItems}, +link{TextAreaItem,TextAreaItems}, etc),
    // +link{CanvasItem,CanvasItems} with a focusable canvas, or items where +link{canFocus}
    // was explicitly set to true.
    //
    // @return (boolean)   true if the form item is visible
    // @group focus
    // @visibility external
    //<
    getCanFocus : function () {
        return this._canFocus();
    },



    // _shouldSelectOnFocus() - should we select the entire text string on (this) focus event?
    // Checks this.selectOnFocus but also whether focus came from a mouse click in the item
    // (in which case we want to suppress the selectOnFocus behavior so the user doesn't have to
    // click twice).
    _shouldSelectOnFocus : function () {
        var selectOnFocus = this.selectOnFocus;
        if (selectOnFocus == null && this.form) selectOnFocus = this.form.selectOnFocus;

        var selectOnClick = this.selectOnClick;
        if (selectOnClick == null && this.form) selectOnClick = this.form.selectOnClick;

        // a click is a focus, force selectOnFocus on if selectOnClick is set
        if (selectOnClick) selectOnFocus = true;

        if (selectOnFocus) {
            var isMouseEvent = isc.EH.isMouseEvent();
            if (isMouseEvent && !selectOnClick) selectOnFocus = false;
        }
        return selectOnFocus;
    },

    //>    @method formItem.focusInItem()
    //            Move the keyboard focus into this item's focusable element
    //        @group eventHandling, focus
    // @visibility external
    //<
    focusInItem : function () {
        // Verify that the form is visible (Script errors occur if you attempt to focus
        // on a hidden item)
        var canFocus = this.isVisible() && this._canFocus() && !this.isDisabled(),
            element = canFocus ? this.getFocusElement() : null;

        if (!canFocus || !element) {

            // This method will return null if we don't have an HTML element, or the
            // element is currently not drawn into the DOM
            return;
        }
        if (element.focus) {

            var suppressFocus = false;

            if (this._IESelectionStuck()) {

                try {
                    document.selection.clear();
                } catch (e) {
                    this.logInfo("Internet explorer error calling document.selection.clear()."
                        + e.message);
                }

                //>DEBUG
                this.logDebug("focusInItem(): Internet Explorer selection is currently " +
                    "wedged due to a native bug tripped by synchronous focus manipulation " +
                    "and redraw. Explicitly clearing selection before resetting native focus.",
                     "nativeFocus");
                //<DEBUG

            // In IE - avoid calling 'focus' on an element that already has native focus

            } else if (isc.Browser.isIE && element == this.getActiveElement()) {
                suppressFocus = true;
            }
            if (!suppressFocus) {
                //>DEBUG
                this.logInfo("about to call element.focus() " + isc.EH._getActiveElementText() +
                             (this.logIsDebugEnabled("traceFocus") ? this.getStackTrace() : ""),
                             "nativeFocus");
                //<DEBUG
                // Fire a notification function centrally so we know a programmatic focus
                // change has been triggered
                isc.FormItem._aboutToFireNativeElementFocus(this);
                isc.EventHandler._unconfirmedFocus = this;
                element.focus();

                //if (isc.Browser.isIE) {
                //    this.logDebug("called element focus" + isc.EH._getActiveElementText(),
                //                  "nativeFocus");
                //}
            } else {
                this.logInfo("element already focused, not focus()ing", "nativeFocus");
            }



            if (isc.Browser.isIE) {
                isc.EH._lastFocusTarget = this;
                this._currentFocusElement = element;
            }
        } else {
            //>DEBUG
            this.logInfo("can't call element focus, no element", "nativeFocus");
            //<DEBUG
        }
        var selectOnFocus = this._shouldSelectOnFocus();
        // if we're just refocussing after redraw, don't selectOnFocus -- we want to retain
        // whatever selection was before focus

        if (this._refocussingAfterRedraw) selectOnFocus = false;
        if (selectOnFocus && element.select) element.select();

    },

    //>    @method formItem.blurItem()
    //            Takes focus from this form item's focusable element.
    //        @group eventHandling, focus
    // @visibility external
    //<
    blurItem : function () {
        if (!this.isVisible() || !(this.hasFocus || isc.EH._lastFocusTarget == this)) return;

        // Call 'blur()' on whatever element has been recorded as having native focus.
        // We record the current focus element in '_nativeElementFocus()'

        var element = this._getCurrentFocusElement();

        if (element && element.blur) {
            //>DEBUG
            this.logInfo("about to call element blur" + isc.EH._getActiveElementText() +
                         (this.logIsDebugEnabled("traceBlur") ? this.getStackTrace() : ""),
                         "nativeFocus");
            //<DEBUG
            isc.EH._unconfirmedBlur = this;


            if (isc.Browser.isIE) {
                try {
                    element.blur();
                } catch (e) {

                }
            } else {
                element.blur();
            }

            //if (isc.Browser.isIE) {
            //    this.logDebug("called element blur, active element now: " +
            //              document.activeElement.id, "nativeFocus");
            //}
        } else {
            //>DEBUG
            this.logInfo("can't call element blur, no element", "nativeFocus");
            //<DEBUG
            // Note: if this item was marked as having focus, _getCurrentFocusElement()
            // ensures that flag gets cleared
        }


    },

    // focusInIcon()
    // - explicitly puts focus into an icon
    focusInIcon : function (icon) {
        icon = this.getIcon(icon);
        if (icon == null || icon.imgOnly) return;
        var element = this._getIconLinkElement(icon);
        if (element != null) element.focus();
    },

    // blurIcon()
    // - take focus from an icon
    blurIcon : function (icon) {
        if (isc.isA.String(icon)) icon = this.getIcon(icon);
        if (icon == null || !this.icons || !this.icons.contains(icon) || icon.imgOnly) return;
        var element = this._getIconLinkElement(icon);

        // Note - we are checking for the icon being present and drawn, but not whether it
        // actually has focus in this method - it is relying on the fact that this should only
        // be called if the passed in icon has focus.
        if (element != null) element.blur();
    },

    // _nativeElementFocus
    // Internal method fired when an element of this form item receives focus.
    // (Fired from focus on the data-element, or icons for most form item types)
    // Sets up formItem.hasFocus, and remembers which native element has focus, before firing
    // Form.elementFocus() to handle bubbling the focus event through the Form item hierarchy.

    _nativeElementFocus : function (element, itemID) {
        if (this._refocussingAfterRedraw) delete this._refocussingAfterRedraw;
        if (isc.EH._unconfirmedFocus == this) delete isc.EH._unconfirmedFocus
        isc.EH._logFocus(this, true);


        if (isc.Browser.isMoz && !this.isVisible()) {
            this.logWarn("calling element.blur() to correct focus in hidden item: " + this,
                         "nativeFocus");
            element.blur();
            return;
        }

        // set this.hasFocus:
        this.hasFocus = true;

        // remember which element got focus:
        this._currentFocusElement = element;

        var result = this.form.elementFocus(element, itemID);

        return result;
    },

    _nativeElementBlur : function (element, itemID) {
        if (isc.EH._unconfirmedBlur == this) delete isc.EH._unconfirmedBlur

        // If we're pending an update, and we've lost focus, update now
        if (this._pendingUpdate != null) {
            isc.Timer.clearTimeout(this._pendingUpdate);
            this._delayedUpdate();
        }

        isc.EH._logFocus(this);
        this.hasFocus = false;
        delete this._currentFocusElement;

        var result = this.form.elementBlur(element, itemID);
        return result;
    },

    //>    @method    formItem.elementFocus()
    // Handle a focus event from an element
    // @param suppressHandlers (boolean) If passed, don't fire any developer-visible focus
    //                                  handler functions.
    //        @group    event handling
    //<
    // Note: currently our onfocus handler will call 'elementFocus' at the form level, which
    // will then
    // - mark "hasFocus" as true on this item
    // - call this method.
    // We may want to shift the 'hasFocus' into this file to make it clearer when we get rid of
    // the 'standalone' behavior.

    elementFocus : function (suppressHandlers) {
        // if the item specifies a prompt, show it in the status bar

        if (this.prompt) this.form.showPrompt(this.prompt);

        // If 'showIconsOnFocus' is set, show the icons
        if (this.showIconsOnFocus && this.showIcons) {
            this.showAllIcons(true);
        } else {
            // if the icons are already visible update their appearance to show "Focused" image
            if (this.icons) this.updateIconsForFocus(this.icons, true);
        }

        // if formatOnBlur is true, update the element value to get rid
        // of the static formatter
        if (this.formatOnBlur) {
            var displayValue = this.getDisplayValue();
            this._setElementValue(displayValue, this._value);
        }

        if (this.showFocusedPickerIcon && this._shouldShowPickerIcon()) {
            var iconDef = this.getPickerIcon();
            if (iconDef) this.updateIconsForFocus(iconDef, true);
        }
        // Update the className of our various bits of HTML to show focused state
        if (this.showFocused) this.updateState();
        // If we're showing a valueIcon, put it into 'over' state if necessary

        if (this.showValueIconFocused && this.showValueIconOver && this._iconState == null) {
            this._iconState = this._$Over;
            this._updateValueIcon(this.getValue());
        }

        if (suppressHandlers) return;


        // If there are pending server validations that could affect this field,
        // block UI interaction until they complete. Skip any further handlers as well.

        if ((this.grid && this.grid.blockOnFieldBusy(this))
            || (!this.grid && this.form.blockOnFieldBusy(this)))
        {
            return false;
        }

        // If necessary fire this.editorEnter
        this.handleEditorEnter()

        // if the item has a "focus" handler
        if (this.focus) {
            // CALLBACK API:  available variables:  "form,item"
            // Convert a string callback to a function
            this.convertToMethod("focus");
            return this.focus(this.form, this);
        }

        return true;
    },

    updateIconsForFocus : function (icons, hasFocus) {

        if (icons == null) return;
        // force hasFocus to a boolean
        hasFocus = !!hasFocus;
        if (!isc.isAn.Array(icons)) icons = [icons];
        for (var i = 0 ; i < icons.length; i++) {
            if (this._iconShouldShowFocused(icons[i], true)) {
                var img = this._getIconImgElement(icons[i]);
                if (img != null) {
                    isc.Canvas._setImageURL(img,
                                    this.getIconURL(icons[i], false, null, hasFocus));
                    var iconStyle = this.getIconStyle(icons[i], false, null, hasFocus);
                    if (iconStyle != null) img.className = iconStyle;
                }
            }
        }
    },

    //>    @method    formItem.elementBlur()
    // Handle a blur event from an element
    //        @group    event handling
    //<
    elementBlur : function () {
        // if the item specified a prompt, clear it from the status bar since it no longer
        // applies
        if (this.prompt) this.form.clearPrompt();

        // If we're showing icons on focus, we should hide them on blur.

        if (this.showIconsOnFocus && this.showIcons) {

            //this.logWarn("setting icon hide timer")
            if (this._hideAllIconsEvent == null) {
                this._hideAllIconsEvent = this.delayCall("hideAllIcons", [], 0);
            }
        // If we're hiding icons on keypress, and not showing on focus, we want to have them
        // re-show when the element gets a blur event.
        } else if (this.hideIconsOnKeypress && this.showIcons) {
            this.showAllIcons();

        // If we just hid, or showed the icons, no need to update state for blur - otherwise
        // we may need to clear "focused" state
        } else {
            if (this.icons) this.updateIconsForFocus(this.icons, false);
        }

        // if formatOnBlur is true, update the element value to apply
        // the static formatter
        if (this.formatOnBlur) {
            var displayValue = this.getDisplayValue();
            this._setElementValue(displayValue, this._value);
        }



        if (this.showFocusedPickerIcon && this._shouldShowPickerIcon()) {
            var iconDef = this.getPickerIcon();
            if (iconDef) this.updateIconsForFocus(iconDef, false);
        }

        // Update the className of our various bits of HTML to show focused state
        if (this.showFocused) {
            this.updateState();
        }

        // If we're showing a valueIcon, we use the "Over" state to indicate focus -
        // clear this if appropriate

        if (this._iconState == this._$Over) {
            this._iconState = null
            this._updateValueIcon(this.getValue());
        }


        // if there's a pending autoCompletion, accept it now
        // No need for 'focusAtEnd()' - the element will no longer have focus
        this.acceptCompletion();

        // If necessary fire this.editorExit();
        this.checkForEditorExit();

        // if the item has a "blur" handler
        if (this.blur) {
            // CALLBACK API:  available variables:  "form,item"
            // Convert a string callback to a function
            this.convertToMethod("blur");
            return this.blur(this.form, this);
        }

        return true;
    },

    // Helper method - we got an onblur but focus may have moved to a sub-element within this
    // item (EG going to an icon).
    // Only fire editorExit if focus has actually gone elsewhere on the page.
    checkForEditorExit : function (delayed, fromFocusEvent) {

        if (!delayed && !isc.Browser.isIE) {
            isc.FormItem._pendingEditorExitCheck = this;
            this._delayedEditorExitCheck = this.delayCall("checkForEditorExit", [true]);
            return;
        }

        if (fromFocusEvent && this._delayedEditorExitCheck != null) {
            isc.Timer.clearTimeout(this._delayedEditorExitCheck);
        }
        // If we got this far, any delayedEditorExitCheck is no longer applicable.
        this._delayedEditorExitCheck = null;
        if (isc.FormItem._pendingEditorExitCheck == this) {
            isc.FormItem._pendingEditorExitCheck = null;
        }

        var activeElement = this.getActiveElement();
        if (activeElement != null) {

            var itemInfo = isc.DynamicForm._getItemInfoFromElement(activeElement);
            if (itemInfo != null) {
                var item = itemInfo.item;
                while (item) {
                    if (item == this) return;
                    // Check if focus moved to a sub-item of a container item.
                    item = item.parentItem;
                }
            }
        }
        this.handleEditorExit();
    },

    // _moveFocusWithinItem() - helper method to simulate a tab / shift tab while the
    // user is focused in this form item.
    // Shifts focus to the next focusable element (may be an icon, or for containerItems a
    // sub element)
    // Returns true if focus was succesfully shifted within this item

    _moveFocusWithinItem : function (forward) {
        var items = this.items,
            icons = this.icons;
        if (this._pickerIcon != null) {
            icons = [this._pickerIcon];
            icons.addList(this.icons);
        }

        // catch the common case where we have only one natively focusable element
        if ((items == null || items.length == 0) && (icons == null || icons.length == 0)) {
            return false;
        }

        var iconIndex = this.getFocusIconIndex(true),
            itemIndex;
        if (iconIndex == null) {
            var targetItem = isc.EventHandler.lastEvent.keyTarget;
            if (targetItem == this) itemIndex = 0;
            else if (items) {
                itemIndex = items.indexOf(targetItem);
            }
        }

        // If we don't have focus, no-op
        if ((itemIndex == null || itemIndex == -1) && iconIndex == null) {
            return false;
        }

        // Now determine where focus should go, based on whether this is a tab or shift
        // tab, and where the event occurred:
        if (forward) {

            // We're moving forwards, so start with finding the next sub-item, if there
            // is one
            if (itemIndex != null && items != null) {
                while (itemIndex < items.length-1) {
                    itemIndex += 1
                    var focusItem = items[itemIndex];

                    if (focusItem._canFocus()) {
                        this.logInfo("FormItem.moveFocusWithinItem(" + forward +
                                 "): Moving to item:" + focusItem, "syntheticTabIndex");
                        // Found another focusable item after the one that had focus
                        focusItem.focusInItem();

                        // return true to indicate that we shifted the focus
                        return true;
                    }
                }
            }

            // at this point we know that focus will have to go the next focusable
            // icon if there is one.
            if (iconIndex == null) iconIndex = -1;

            if (this.canTabToIcons != false && this.showIcons && icons != null) {

                while (iconIndex < icons.length -1) {
                    iconIndex += 1;
                    var icon = icons[iconIndex];
                    if (this._shouldShowIcon(icon) && !icon.imgOnly && icon.tabIndex != -1) {
                        this.logInfo("FormItem.moveFocusWithinItem(" + forward +
                                 "): Moving to icon:" + this.echo(icon), "syntheticTabIndex");

                        // Found a visible icon after the last focus element - focus
                        // in it and return.
                        this.focusInIcon(icon);
                        return true;
                    }
                }
            }

            // If we are here, the user has hit tab on the last focusable sub item or
            // icon in this form item.
            return false;

        } else {
            // No need to check for this.icons == null or this.showIcons, as iconIndex
            // will only be set if we are currently focused on an icon
            // We do need to check for 'canTabToIcons', since we are explicitly putting
            // focus onto the form item icons, rather than relying on their tabindex
            if (this.canTabToIcons != false && iconIndex != null) {
                while (iconIndex > 0) {
                    iconIndex -= 1;
                    // This icon should get focus - focus on it and return.
                    var icon = icons[iconIndex]
                    if (this._shouldShowIcon(icon) && !this.imgOnly && icon.tabIndex != -1) {
                        this.logInfo("FormItem.moveFocusWithinItem(" + forward +
                                 "): Moving to icon:" + this.echo(icon), "syntheticTabIndex");
                        this.focusInIcon(icon);
                        return true;
                    }
                }
            }

            // If itemIndex is null, the event was on the first visible icon - start
            // checking the last visible sub item
            if (itemIndex == null)
                itemIndex = items != null ? items.length : 1;

            // If we got here we have a valid itemIndex (may be 1 higher than the
            // number of subItems).
            while (itemIndex > 0) {
                var focusItem;
                itemIndex -= 1

                if (items == null) {
                    focusItem = this;
                } else {
                    focusItem = items[itemIndex]
                }

                if (focusItem._canFocus()) {
                    this.logInfo("FormItem.moveFocusWithinItem(" + forward +
                             "): Moving to item:" + focusItem, "syntheticTabIndex");

                    // This sub-item (or this item, if we have no sub items) should get
                    // focus - focus on it and return false to cancel the event.
                    focusItem.focusInItem();
                    return true;
                }
            }

            return false
        }
    },

    // Helper method to determine the index of the icon with focus (or null if no icon has
    // focus) based on the focus'd HTML element

    getFocusIconIndex : function (includePickerIcon) {
        var currentFocusElement = this._getCurrentFocusElement();
        var icons;
        if (includePickerIcon && this._pickerIcon != null) {
            icons = [this._pickerIcon];
            icons.addList(this.icons);
        } else {
            icons = this.icons;
        }
        if (currentFocusElement == null || icons == null || icons.length == 0) return null;
        for (var i = 0; i < icons.length; i++) {
            if (this._getIconLinkElement(icons[i]) == currentFocusElement) return i;
        }
        return null;
    },

    _$img:"img",
    _allowNativeTextSelection : function (event, itemInfo) {
        if (itemInfo.overTitle) return;
        // Suppress native dragging of icons
        if (itemInfo.overIcon) return false;
        // Note that imgOnly icons and valueIcons won't set the "overIcon" attribute.
        // Explicitly check for the user attempting to select or drag an IMG tag
        if (event == null) event = isc.EH.lastEvent;
        if (event.nativeTarget && (event.nativeTarget.tagName.toLowerCase() == this._$img))
            return false;

        return this.canSelectText != false;
    },


    handleEditorExit : function () {
        // Don't crash if this is tripped post destroy somehow.
        if (!this.form) return;

        if (!this._hasEditorFocus) return;
        this._hasEditorFocus = null;

        var value = this.getValue();

        // validateOnExit if necessary.
        // _suppressValidateOnEditorExit flag introduced for ComboBoxItem where we have
        // 'completeOnTab' behavior, which completes based on teh values in the pickList for
        // the item. These values may be being fetched asynchronously meaning the completion can
        // occur after focus has truly left the field. In this case it doesn't make sense to
        // validate the temp value that's entered while the fetch is occuring.
        // Instead validate when the fetch completes and we perform tab auto-completion
        if (!this._suppressValidateOnEditorExit) this._performValidateOnEditorExit(value);

        // If implicitSaving and value in editor changed, call the parent form to save
        if (this.getImplicitSave() && this.form && this.form.awaitingImplicitSave
                && !this.form.implicitSaveInProgress && this._itemValueIsDirty()
                && this.getImplicitSaveOnBlur() != false)
        {
            this.form.performImplicitSave(this, false);
        }

        if (this.editorExit) this.editorExit(this.form, this, value);
    },

    getImplicitSave : function () {
        // Just bail if we've already been removed from our form

        if (this.form == null) return false;
        return (this.implicitSave != null ? this.implicitSave : this.form.implicitSave);
    },

    getImplicitSaveOnBlur : function () {
        if (this.getImplicitSave() == false) return false;
        return (this.implicitSaveOnBlur != null ?
            this.implicitSaveOnBlur : this.form.implicitSaveOnBlur);
    },

    // helper to validate() on editor exit if necessary.
    // Checks for this form not being in update mode or the value in the editor having changed since
    // editorEnter.
    // Fired from handleEditorExit
    // In ComboBoxItem we also fire this from 'fireTabCompletion()' in the case where we
    // had a delayed tab completion due to a pending fetch.
    _performValidateOnEditorExit : function (value) {
        if (this.form == null) return;
        if (this.validateOnExit || this.form.validateOnExit) {
            // _forceValidateOnExit allows items to force a re-validation. We use this in
            // dateItem where if the user picks a date from the picker we actually change values
            // then focus in the item so wouldn't catch the _editorEnterValue change.
            if (this._forceValidateOnExit ||
                (this.form.getSaveOperationType && this.form.getSaveOperationType() != "update") ||
                !this.compareValues(value, this._editorEnterValue))
            {
                this.validate();
            }
            this._forceValidateOnExit = null;
            this._editorEnterValue = null;
        }
        var rulesEngine = this.form.rulesEngine;
        if (rulesEngine != null) {
            rulesEngine.processEditorExit(this.form, this);
        }
    },


    // Documented in actionMethods
    handleEditorEnter : function () {
        if (isc.FormItem._pendingEditorExitCheck != null) {
            isc.FormItem._pendingEditorExitCheck.checkForEditorExit(true, true);
        }
        if (this._hasEditorFocus) return;
        this._hasEditorFocus = true;

        var value = this.getValue();
        // Save starting value for validateOnExit
        if (this.validateOnExit || this.form.validateOnExit) this._editorEnterValue = value;

        if (this.editorEnter) this.editorEnter(this.form, this, value);

        if (this.form.rulesEngine != null) {
            this.form.rulesEngine.processEditorEnter(this.form, this);
        }
    },


    _setupFocusCheck : function () {
        var formItem = this;
        this._nativeFocusCheckEvent = isc.Page.setEvent(
                isc.EH.MOUSE_UP, function () {
                    if (!formItem.destroyed) formItem._checkNativeFocus();
                });

    },
    _checkNativeFocus : function () {
        // clear out the event so we don't fire on future clicks
        isc.Page.clearEvent(isc.EH.MOUSE_UP, this._nativeFocusCheckEvent);
        delete this._nativeFocusCheckEvent;

        if (this.getActiveElement() == document.body) {
            //this.logWarn("Catching native focus issue");
            this.focusInItem()
        }
    },

    // _willHandleInput / _handleInput

    _willHandleInput : function () {
        // Only returns true for supported browsers, and for items where we explicitly write out
        // an oninput handler.
        return false;
    },
    _handleInput : function () {
        isc.EH._setThread("INP");
        this.__handleInput();
        isc.EH._clearThread();
    },
    __handleInput : function () {
        // If masked edit is enabled, changeOnKeypress is handled there.
        if (!this.mask) {
            if (this.changeOnKeypress) {
                if (isc.Log.supportsOnError) {
                    this.updateValue();
                } else {
                    try {
                        this.updateValue();
                    } catch (e) {
                        isc.Log._reportJSError(e);
                    }
                }
            } else {
                var elementValue = this.getElementValue();
                this._minimalUpdateValue(elementValue);
            }
        }
    },

    // Called when changeOnKeypress is false to minimally handle an 'input' event (i.e. no
    // change event is fired, but this gives us a chance to apply some behaviors that are
    // otherwise only done in the full updateValue() path).
    _minimalUpdateValue : function (elementValue) {
        var oldValue = this._lastMinimalUpdateValue;
        if (oldValue == null) oldValue = this._value;
        var trimmedValue = this._enforceLengthOnEdit(elementValue, oldValue);
        if (trimmedValue != elementValue) {
            this.setElementValue(trimmedValue);
            if (trimmedValue == oldValue) this._revertToPreChangeSelection();
        }
        // We need to be able to reset to the last element value, but
        // since we're not logically changing, we're not storing this as this._value
        this._lastMinimalUpdateValue = trimmedValue;
    },

    _handleSelect : function () {
        isc.EH._setThread("SEL");
        this.__handleSelect();
        isc.EH._clearThread();
    },
    __handleSelect : isc.Class.NO_OP,

    // Native oncut / onpaste handlers for IE
    // Fires before the value is pasted into the form item, so returning false would cancel the
    // paste.
    // Perform update on a delay so we have the new value available from the form item element.
    _nativeCutPaste : function (element, item) {

        // Fire change handlers on paste.

        if (this.changeOnKeypress) this._queueForUpdate();
    },




    // This is called by EventHandler.bubbleEvent - we make use of it to give the special
    // form item specific parameters to the keyPress string method, and to call 'itemKeyPress'
    // on the form.
    _$Tab:"Tab",
    handleKeyPress : function (event, eventInfo) {

        // if we don't have a form - we've presumably been removed from our form via a
        // 'setItems()' call since the native keyPress event

        if (!this.form) return;

        var targetInfo = this.form._getEventTargetItemInfo(event),
            keyName = event.keyName;

        // Fire iconKeyPress if approrpriate
        if (targetInfo.overIcon) {
            if (this._iconKeyPress(targetInfo.overIcon) == false) return false;

        // Only update value if focus was on the item itself (not on an icon)
        // Similarly only hideIconsOnKeypress if we were not focused on an icon
        } else {

            // If we're changing on every keypress, set this up to happen asynchronously so
            // the new value is available

            if (!this._willHandleInput()) {
                if (this.changeOnKeypress) this._queueForUpdate();
                else this._queueForMinimalUpdate();

            // If our change handler will be tripped from 'handleInput' we still need
            // to remember the "pre change" selection here as by the time that method
            // fires, the element value, and cursor insertion point, has already changed.
            } else {
                this.rememberSelectionForUpdate();
            }

            // If hideIconsOnKeypress is true, we want to hide all the icons (gives the user more
            // space to type).
            // Only do this if this is not the Tab key (in which case the user is just navigating
            // through the field), and the user is not currently focused on an icon's link element.

            if (this.hideIconsOnKeypress && !this._allIconsHidden && keyName != this._$Tab) {
                this.hideAllIcons();
            }
        }

        // Fire this.keyPress and this.form.itemKeyPress
        return this._fireKeyPressHandlers(this,this.form,keyName,event.characterValue);
    },

    // _fireKeyPressHandlers - will fire item.keyPress and form.itemKeypress
    _fireKeyPressHandlers : function (item, form, keyName, characterValue) {
        if (this.keyPress != null && this.keyPress(item, form, keyName, characterValue) == false) {
            return false;
        }

        // it's possible for the keyPress handler to not return false, but to destroy the form
        // - for example an implementation that uses arrow_up/down to move the inline editor in
        // a ListGrid.
        if (!this.form) return false;

        // Let masked field handle keyPress

        if (this._maskKeyPress != null
            && this._maskKeyPress(item, form, keyName, characterValue) == false)
        {
            return false;
        }

        // itemKeyPress is a method fired on the form when an item receives a keypress.
        // Differs from "keyPress" on the form in that:
        // - the event is guaranteed to have come from a keypress in an item (not just a
        //   keypress on the form itself)
        // - it tells the form which item generated the event.
        // - itemKeyPress will not bubble up from the form to parent widgets as we're
        //   calling it directly.

        // Don't fire itemKeypress directly from a sub item of a container item. The Container
        // is the logical form item as far as the form is concerned, and the developer can
        // get back to the sub item via event.keyTarget anyway, so wait for the keyPress to
        // bubble to the container and have that fire itemKeyPress at the form level.

        if (this.parentItem == null && this.form.itemKeyPress != null) {
            return this.form.itemKeyPress(item , keyName , characterValue);
        }
    },

    // If we're firing change on every keypress, we actually do this asynchronously on a timer
    // so that the value is available in the form item when change fires.

    _$delayedUpdate:"_delayedUpdate",
    _queueForUpdate : function () {
        if (this._pendingUpdate != null) {
            isc.Timer.clearTimeout(this._pendingUpdate);
            this._delayedUpdate();
        }
        // If we're changing on keypress, remember the current insertion point, so that if
        // the change handler fires and returns FALSE, we can reset the cursor insertion point.

        this.rememberSelectionForUpdate();

        this._pendingUpdate = isc.Timer.setTimeout({target:this, methodName:this._$delayedUpdate},
                                                   0);
    },
    // Similar logic for changeOnKeypress:false items
    _$delayedMinimalUpdate:"_delayedMinimalUpdate",
    _queueForMinimalUpdate : function () {
        if (this._pendingUpdate != null) {
            isc.Timer.clearTimeout(this._pendingUpdate);
            this._delayedUpdate();
        }
        if (!this._pendingMinimalUpdate) {
            this.rememberSelectionForUpdate();
            this._pendingMinimalUpdate =
                isc.Timer.setTimeout({target:this, methodName:this._$delayedMinimalUpdate},
                                                   0);
        }
    },
    _delayedMinimalUpdate : function () {
        delete this._pendingMinimalUpdate;
        this._minimalUpdateValue(this.getElementValue());
        this._clearPreChangeSelection();
    },


    rememberSelectionForUpdate : function () {
        // If we're changing on keypress, remember the current insertion point, so that if
        // the change handler fires and returns FALSE, we can reset the cursor insertion point.

        if (this.maintainSelectionOnTransform &&
            (this._getAutoCompleteSetting() != this._$smart))
        {
            this._rememberPreChangeSelection();
        }
    },

    _delayedUpdate : function () {
        delete this._pendingUpdate;
        this.updateValue();
        this._clearPreChangeSelection();
    },

    // Helper methods for resetting the selection if a change handler triggered by a keyPress
    // returns false.
    _rememberPreChangeSelection : function () {
        if (!this._isGetSelectionRangeSupported) return;


        var preChangeRange = this.getSelectionRange(true);
        if (preChangeRange) {
            this._preChangeStart = preChangeRange[0];
            this._preChangeEnd = preChangeRange[1];
        }
    },

    _revertToPreChangeSelection : function () {
        // No op if this change didn't come from a keypress
        if (this._preChangeStart == null) return;
        if (this._isSetSelectionRangeSupported) {
            this.setSelectionRange(this._preChangeStart, this._preChangeEnd);
        }
    },
    _clearPreChangeSelection : function () {
        delete this._preChangeStart;
        delete this._preChangeEnd;
    },


    // HandleKeyDown overridden to mark the item dirty.
    // Also calls formItem.keyDown with appropriate parameters
    handleKeyDown : function (event, eventInfo) {

        // Mark the value as dirty if appropriate
        if (this.dirtyOnKeyDown) this._markValueAsDirty();

        var item = this,
            form = this.form,
            keyName = event.keyName;

        // fire keyDown stringMethod
        if (this.keyDown != null && this.keyDown(item, form, keyName) == false) return false
    },

    //>    @method    formItem._itemValueIsDirty()    (IA)
    //      Is this form item marked as having a dirty value?
    //
    //      @return (boolean)   true if the value is marked as being dirty via the _valueIsDirty
    //                          flag
    //<
    // Return this value from a method to allow overriding by container items
    _itemValueIsDirty : function () {
        return this._valueIsDirty == true;
    },

    _markValueAsDirty : function () {
        this._valueIsDirty = true;
    },

    _markValueAsNotDirty : function () {
        this._valueIsDirty = false;
    },

    // handleKeyUp
    // Overridden to call this.keyUp with appropriate parameters.
    handleKeyUp : function (event, eventInfo) {
        // if we don't have a form - we've presumably been removed from our form via a
        // 'setItems()' call since the native keyUp event

        if (!this.form) return;

        var item = this,
            form = this.form,
            keyName = event.keyName;

        // Fire keyUp stringMethod
        if (this.keyUp != null && this.keyUp(item, form, keyName) == false) return false;
    },

    // Serialization
    // ----------------------------------------------------------------------------------------

    getSerializeableFields : function(removeFields, keepFields) {
        removeFields = removeFields || [];

        // form is a backref
        removeFields.addList(["form"]);
        return this.Super("getSerializeableFields", [removeFields, keepFields], arguments);
    },

    // Element coordinates
    // ----------------------------------------------------------------------------------------

    // These are needed by elements that create Canvii that float in the vicinity of the item,
    // such as the ComboBox.
    //


    //>@method formItem.getLeft()
    // Returns the left coordinate of this form item in pixels. Note that this method
    // is only reliable after the item has been drawn.
    // @return (integer) left coordinate within the form in pixels\
    // @group positioning,sizing
    // @visibility external
    //<
     getLeft : function () {
        var tableElement = this.isDrawn() ? this.getOuterElement() : null;
        if (tableElement == null) {
            var warning = "getLeft() Unable to determine position for " +
                          (this.name == null ? "this item " : this.name) + ". ";
            if (this.isDrawn()) {
                warning += "This method is not supported by items of type " + this.getClass();
            } else {
                warning += "Position cannot be determined before the element is drawn"
            }
            warning += " - returning zero.";

            this.form.logWarn(warning);
            return 0;
        }
        var left = this._getElementLeft(tableElement);
        return left;
        return this._getElementLeft(tableElement);
    },

    getTitleLeft : function () {
        var titleElement = this.isDrawn() && this.form
                                          ? isc.Element.get(this.form._getTitleCellID(this))
                                          : null;
        if (titleElement == null) {
            var warning = "getTitleLeft() Unable to determine position for " +
                          (this.name == null ? "this item " : this.name) + ". ";
            if (this.isDrawn()) {
                warning += "This method is not supported by items of type " + this.getClass();
            } else {
                warning += "Position cannot be determined before the element is drawn"
            }
            warning += " - returning zero.";

            this.form.logWarn(warning);
            return 0;
        }
        return this._getElementLeft(titleElement);
    },

    // Separate out the method to get the position based on an HTML element.  This simplifies
    // overriding 'getLeft()' to look at something other than the result of this.getElement()
    // for items with no data element.
    _getElementLeft : function (element) {
        var currentNode = element.offsetParent,
            formElement = this.containerWidget.getClipHandle(),
            formParent = formElement.offsetParent,
            left = isc.Element.getOffsetLeft(element);

        // iterate up until we reach the targetElement, or the targetElement's offsetParent
        // We could also check for documentBody to avoid crashing in the case where we were
        // passed bad params.
        while (currentNode && currentNode != formElement && currentNode != formParent) {

            // Add the currentNode's offsetLeft - left w.r.t. its offsetParent
            left += isc.Element.getOffsetLeft(currentNode)

            // Deduct the scrollLeft
            left -= (currentNode.scrollLeft || 0);
            // Add the border thickness
            // (last offsetLeft relative to inside of border, this one relative to outside of border)
            var borderLeftWidth = (isc.Browser.isIE ?
                                    parseInt(currentNode.currentStyle.borderLeftWidth) :
                                    parseInt(isc.Element.getComputedStyleAttribute(currentNode,
                                                                                "borderLeftWidth")));

            if (isc.isA.Number(borderLeftWidth)) left += borderLeftWidth;

            // getOffsetLeft() will give the distance from the outside of this element's margin
            // to the parent element -- we want the distance from the inside of the margin, so
            // add the margin thickness
            var marginLeftWidth = (isc.Browser.isIE ?
                                    parseInt(currentNode.currentStyle.marginLeft) :
                                    parseInt(isc.Element.getComputedStyleAttribute(currentNode, "marginLeft")));

            if (isc.isA.Number(marginLeftWidth)) left += marginLeftWidth;

            // Move up the DOM chain
            currentNode = currentNode.offsetParent;
        }

        // OffsetLeft from the last iteration was relative to the content of the offsetParent
        if (currentNode == formParent) {
            // deduct the targetElement's offsetLeft
            // No need to adjust for border / padding in this case
            left -= isc.Element.getOffsetLeft(formElement)
        }

        return left;
    },

    _isValidIcon : function (icon) {
        return (icon != null &&
                (this.icons && this.icons.contains(icon) ||
                 this.showPickerIcon && this.getPickerIcon() == icon));
    },

    //>@method  getIconLeft()    (A)
    //  Returns the (offset) left-coordinate of an icon within its containing widget.
    //  @param  icon    (object)    icon definition
    //  @return (number)    icon left position in px
    //  @visibility external
    //<
    getIconLeft : function (icon) {
        // default to the first icon, if possible
        if (icon == null && this.icons != null && this.icons.getLength() > 0) icon = this.icons[0];
        else  if (!this._isValidIcon(icon)) {
            this.logWarn("getIconLeft() passed invalid icon:" + isc.Log.echoAll(icon));
            return null;
        }
        var iconElement = this._getIconImgElement(icon);
        if (iconElement == null) {
            this.logWarn("getIconLeft() unable to return position of icon - " +
                         "this icon is not currently drawn into the page. Returning null");
            return null;
        }

        // Determine offsetLeft wrt containing widget
        return isc.Element.getLeftOffset(iconElement, this.containerWidget.getClipHandle());
    },

    // Methods to get the rendered position of the form item.
    // Note that these rely on the standard nested table element format - if getInnerHTML() is
    // overridden these may need to also be overridden
    //>@method formItem.getTop()
    // Returns the top coordinate of the form item in pixels. Note that this method is only
    // reliable after the item has been drawn out.
    // @return (integer) top position in pixels
    // @group positioning,sizing
    // @visibility external
    //<
    getTop : function () {
        var element = this.isDrawn() ? this.getOuterElement() : null;
        if (element == null) {
            // We will not find an element if we are not drawn into the DOM, or if we don't
            // have a data element.
            // In either case, bail with an appropriate warning.
            var warning = "getTop() Unable to determine position for " +
                          (this.name == null ? "this item " : this.name) + ". ";
            if (this.isDrawn()) {
                warning += "This method is not supported by items of type " + this.getClass();
            } else {
                warning += "Position cannot be determined before the element is drawn"
            }
            warning += " - returning zero.";

            this.form.logWarn(warning);
            return 0;
        }
        var top = this._getElementTop(element);
        return top;
    },

    getTitleTop : function () {
        var titleElement = this.isDrawn() && this.form
                                          ? isc.Element.get(this.form._getTitleCellID(this))
                                          : null;
        if (titleElement == null) {
            var warning = "getTitleTop() Unable to determine position for " +
                          (this.name == null ? "this item " : this.name) + ". ";
            if (this.isDrawn()) {
                warning += "This method is not supported by items of type " + this.getClass();
            } else {
                warning += "Position cannot be determined before the element is drawn"
            }
            warning += " - returning zero.";

            this.form.logWarn(warning);
            return 0;
        }
        return this._getElementTop(titleElement);
    },
    _getElementTop : function (element) {
        var formElement = this.containerWidget.getClipHandle(),
            formParent = formElement.offsetParent,
            currentNode = element.offsetParent,
            top = isc.Element.getOffsetTop(element)

        ;

        // iterate up until we reach the targetElement, or the targetElement's offsetParent
        // We could also check for documentBody to avoid crashing in the case where we were
        // passed bad params.
        while (currentNode && currentNode != formElement && currentNode != formParent) {
            // Add the currentNode's offsetTop - top w.r.t. its offsetParent
            top += isc.Element.getOffsetTop(currentNode)
            // Deduct the scroll top
            top -= (currentNode.scrollTop || 0);
            // Add the border thickness
            // (last offsetTop relative to inside of border, this one relative to outside of border)

            var borderTopWidth = (isc.Browser.isMoz ? 0 :
                                  (isc.Browser.isIE ?
                                    parseInt(currentNode.currentStyle.borderTopWidth) :
                                    parseInt(isc.Element.getComputedStyleAttribute(currentNode,
                                                                                "borderTopWidth"))
                                  )
                                 );

            if (isc.isA.Number(borderTopWidth)) top += borderTopWidth;

            // getOffsetTop() will give the distance from the outside of this element's margin
            // to the parent element -- we want the distance from the inside of the margin, so
            // add the margin thickness

            var marginTopWidth = (isc.Browser.isIE ?
                                    parseInt(currentNode.currentStyle.marginTop) :
                                    parseInt(isc.Element.getComputedStyleAttribute(currentNode, "marginTop")));
            if (isc.isA.Number(marginTopWidth)) top += marginTopWidth;

            // Move up the DOM chain
            currentNode = currentNode.offsetParent;
        }

        // OffsetTop from the last iteration was relative to the content of the offsetParent
        if (currentNode == formParent) {
            // deduct the targetElement's offsetTop
            // No need to adjust for border / padding in this case
            top -= isc.Element.getOffsetTop(formElement)
        }

        return top;

    },

    //>@method  getIconTop()    (A)
    //  Returns the (offset) top-coordinate of an icon within its containing widget.
    //  @param  icon    (object)    icon definition
    //  @return (number)    icon top position in px
    //  @visibility external
    //<
    getIconTop : function (icon) {
        // default to the first icon, if possible
        if (icon == null && this.icons != null && this.icons.getLength() > 0) icon = this.icons[0];
        else if (!this._isValidIcon(icon)) {
            this.logWarn("getIconTop() passed invalid icon:" + isc.Log.echoAll(icon));
            return null;
        }
        // Note we're interested in the position of the img rather than the actual link element
        var iconElement = this._getIconImgElement(icon);
        if (iconElement == null) {
            this.logWarn("getIconTop() unable to return position of icon - " +
                         "this icon is not currently drawn into the page. Returning null");
            return null;
        }

        // Determine offsetTop wrt containing widget
        return isc.Element.getTopOffset(iconElement, this.containerWidget.getClipHandle());
    },

    //> @method formItem.getPageLeft()
    // Returns the drawn page-left coordinate of this form item in pixels.
    // @return (int) page-left coordinate in px
    // @group positioning
    // @visibility external
    //<

    getPageLeft : function () {
        var cw = this.containerWidget;
        var adjustForRTL = (cw.isRTL() && cw.overflow != isc.Canvas.VISIBLE);
        if (!adjustForRTL) {
            return this.getLeft() +
                   ((this.containerWidget.getPageLeft()
                        + this.containerWidget.getLeftMargin()
                        + this.containerWidget.getLeftBorderSize())
                   - this.containerWidget.getScrollLeft());
        } else {
            var maxScroll = cw.getScrollWidth()-cw.getViewportWidth();
            return this.getLeft() +
                (this.containerWidget.getPageLeft()
                        + this.containerWidget.getLeftMargin()
                        + this.containerWidget.getLeftBorderSize()
                        + (cw.vscrollOn ? cw.getScrollbarSize() : 0))
                - (cw.getScrollLeft() - maxScroll);
        }
    },

    //> @method formItem.getPageTop()
    // Returns the drawn page-top coordinate of this form item in pixels.
    // @return (int) page-top coordinate in px
    // @group positioning
    // @visibility external
    //<
    getPageTop : function () {
        return this.getTop() +
                ((this.containerWidget.getPageTop()
                    + this.containerWidget.getTopMargin()
                    + this.containerWidget.getTopBorderSize())
                - this.containerWidget.getScrollTop());
    },

    getTitlePageLeft : function () {
        return this.getTitleLeft() +
               ((this.containerWidget.getPageLeft()
                    + this.containerWidget.getLeftMargin()
                    + this.containerWidget.getLeftBorderSize())
                - this.containerWidget.getScrollLeft());
    },
    getTitlePageTop : function () {
        return this.getTitleTop() +
                ((this.containerWidget.getPageTop()
                    + this.containerWidget.getTopMargin()
                    + this.containerWidget.getTopBorderSize())
                - this.containerWidget.getScrollTop());
    },

    //>@method  getIconRect()    (A)
    //  Returns the size / position of an icon with respect to the widget rendering out the
    //  form item as an array of coordinates.
    //  @param  icon    (object)    icon definition for the icon you want to determine the
    //                              position of (defaults to first icon in this.icons).
    //
    //  @return (array)    four element array representing the Left, Top, Width, and Height of
    //                      the icon in px.
    //  @visibility external
    //<
    getIconRect : function (icon) {
        // if the icon param is empty, it will be defaulted to the first icon by each of the
        // getIcon...() methods.
        return [this.getIconLeft(icon),
                this.getIconTop(icon),
                this.getIconWidth(icon),
                this.getIconHeight(icon)];
    },

    //>@method  getIconPageRect()    (A)
    //  Returns the size / position of an icon on the page as an array of coordinates.
    //  @param  icon    (object)    icon definition for the icon you want to determine the
    //                              position of (defaults to first icon in this.icons).
    //
    //  @return (array)    four element array representing the Left, Top, Width, and Height of
    //                      the icon in px.
    //  @visibility external
    //<
    getIconPageRect : function (icon) {
        var rect = this.getIconRect(icon);
        rect[0] += this.containerWidget.getPageLeft();
        rect[1] += this.containerWidget.getPageTop();
        return rect;
    },


    // Mark the form for redraw if 'setProperties()' is called passing in
    // any properties that would effect the layout of the form.
    // Call 'updateState()' if setProperties modifies any properties that would effect the
    // styling of the form item elements
    _relayoutProperties : {
        colSpan:true,
        rowSpan:true,
        startRow:true,
        endRow:true,
        showTitle:true,
        showHint:true
    },
    _stylingProperties:{
        baseStyle:true,
        showErrorStyle:true,
        //showDisabled:true // handled by explicit setShowDisabled method
        showFocused:true,
        showErrorStyle:true,
        controlStyle:true,
        pickerIconStyle:true,
        textBoxStyle:true
    },
    _$itemCellStyle:"itemCellStyle",
    propertyChanged : function (prop, val) {
        if (this._relayoutProperties[prop] == true) this._requiresFormRedraw = true;
        if (this._stylingProperties[prop] == true) this.updateState();
        if (prop == this._$itemCellStyle && this.items) {
            for (var i = 0; i< this.items.length; i++) {
                this.items[i].updateState();
            }
        }
    },

    doneSettingProperties : function () {
        if (this._requiresFormRedraw) {
            var form = this.form, items = form.items;

            items._rowTable = null;
            form.markForRedraw();
        }
        delete this._requiresFormRedraw;
    },

    //>EditMode
    // ---------------------------------------------------------------------------------------
    // EditMode.  Ideally, these methods would be defined in EditMode.js, but it introduces
    // dependency issues and causes JS errors when loading some SmartClient apps (not the
    // DocViewer or Visual Builder, for some reason)

    setEditMode : function (editingOn, editContext, editNode) {
        if (editingOn == null) editingOn = true;
        if (this.editingOn == editingOn) return;
        this.editingOn = editingOn;

        if (this.editingOn) {
            this.editContext = editContext;
        }

        this.editNode = editNode;
    },

    editClick : function () {
        var left = this.getTitlePageLeft(),
            width = this.getVisibleTitleWidth(),
            top, titleTop,
            height, titleHeight;

        titleTop = this.getTitlePageTop();
        titleHeight = this.getTitleVisibleHeight();
        height = this.getVisibleHeight();
        top = (titleHeight == height) ? titleTop : titleTop + ((titleHeight - height) / 2);

        isc.EditContext.manageTitleEditor(this, left, width, top, null);
    },
    //<EditMode

    // Expression parsing

    //> @attr formItem.allowExpressions (boolean : null : IRW)
    // For a form that produces filter criteria
    // (see +link{dynamicForm.getValuesAsCriteria,form.getValuesAsCriteria()}), allows the user
    // to type in simple expressions to cause filtering with that operator.  For
    // example, entering ">5" means values greater than 5, and ">0 and <5" means values between
    // 0 and 5.
    // <P>
    // The following table lists character sequences that can be entered as a prefix to a value,
    // and the corresponding +link{type:OperatorID,operator} that will be used.  Note that in the
    // absence of a prefix, the operator specified via +link{formItem.operator} is used. Also note
    // that whatever you enter will be used literally, including any whitespace characters. For
    // example if you input '== China ' then ' China ' will be the value.
    // <P>
    // <table style='font-size:14;'>
    // <tr><td><b>Prefix</b></td><td><b>Operator</b></td></tr>
    // <tr><td>&lt;</td><td>lessThan</td></tr>
    // <tr><td>&gt;</td><td>greaterThan</td></tr>
    // <tr><td>&lt;=</td><td>lessThanOrEqual</td></tr>
    // <tr><td>&gt;=</td><td>greaterThanOrEqual</td></tr>
    // <tr><td>someValue...someValue</td><td>betweenInclusive</td></tr>
    // <tr><td>!</td><td>notEqual</td></tr>
    // <tr><td>^</td><td>startsWith</td></tr>
    // <tr><td>|</td><td>endsWith</td></tr>
    // <tr><td>!^</td><td>notStartsWith plus logical not</td></tr>
    // <tr><td>!@</td><td>notEndsWith plus logical not</td></tr>
    // <tr><td>~</td><td>contains</td></tr>
    // <tr><td>!~</td><td>notContains</td></tr>
    // <tr><td>#</td><td>isNull</td></tr>
    // <tr><td>!#</td><td>isNotNull</td></tr>
    // <tr><td>==</td><td>exact match (for fields where 'contains' is the default)</td></tr>
    // </table>
    // <P>
    // Two further special notations are allowed:
    // <ul>
    // <li> /<i>regex</i>/ means the value is taken as a regular expression and applied via the
    // "regexp" operator
    // <li> =.<i>fieldName</i> means the value should match the value of another field.  Either the
    // user-visible title of the field (field.title) or the field's name (field.name) may be used.
    // </ul>
    // <P>
    // In all cases, if an operator is disallowed for the field (via
    // +link{dataSourceField.validOperators,field.validOperators} at either the dataSource or field
    // level), the operator character is ignored (treated as part of a literal value).
    // <P>
    // By default, the case-insensitive version of the operator is used (eg, startsWith will
    // actually use "iStartsWith").  To avoid this, explicitly set item.operator (the default
    // operator) to any case sensitive operator (eg "equals" or "contains") and case sensitive
    // operators will be used for user-entered expressions.
    // <P>
    // Compound expressions (including "and" and "or") are allowed only for numeric or date/time
    // types.
    // <P>
    // To enable this behavior for all fields in a form that allow text entry, see
    // +link{dynamicForm.allowExpressions}.
    // <P>
    // <P>
    // Note that, like +link{formItem.operator}, enabling <code>allowExpressions:true</code>
    // causes +link{dynamicForm.getValuesAsCriteria,form.getValuesAsCriteria()}) to return
    // +link{AdvancedCriteria}.
    //
    // @group advancedFilter
    // @visibility external
    //<


    //> @attr formItem.validOperators (Array of OperatorId : null : IR)
    // Array of valid filtering operators (eg "greaterThan") that are legal for this FormItem.
    // <P>
    // Applies only to form/formItem when +link{formItem.allowExpressions} is true, allowing the
    // user to input expressions.
    // @group advancedFilter
    // @visibility external
    //<


    parseValueExpressions : function (value, fieldName) {
        var type = this.getType(),
            typeInheritsFromTime = isc.SimpleType.inheritsFrom(type, "time"),
            isValidLogicType = (isc.SimpleType.inheritsFrom(type, "integer") ||
                isc.SimpleType.inheritsFrom(type, "float") ||
                isc.SimpleType.inheritsFrom(type, "date") ||
                typeInheritsFromTime
            ),
            opIndex = isc.DynamicForm.getOperatorIndex(),
            validOps = isc.getKeys(opIndex),
            result = { operator: "and", criteria: [] },
            crit = result.criteria,
            valueParts = [],
            allowEx = this._shouldAllowExpressions(),
            ds = isc.DS.get(this.form.expressionDataSource || this.form.dataSource)
        ;

        if (!value) value = this.getValue();
        if (!value) return;

        if (!isc.isA.String(value)) value += "";

        if (typeInheritsFromTime) {
            value = isc.Time._prepForParseValueExpressions(value);
        }

        var defOpName = this.getOperator();
        if (defOpName) validOps.add(defOpName);

        var defOp = ds ? ds.getSearchOperator(defOpName) : { id: defOpName };

        var insensitive = defOp.caseInsensitive;

        if (isValidLogicType && value.contains(" and ")) {
            valueParts = value.split(" and ");
        } else if (isValidLogicType && value.contains(" or ")) {
            valueParts = value.split(" or ");
            result.operator = "or";
        } else if (value.contains("...")) {
            valueParts = value.split("...");
            if (valueParts.length == 2) {
                var tempOps = opIndex["..."],
                    tempOp;

                if (tempOps) tempOp = (insensitive ? tempOps.find("caseInsensitive", true) : tempOps[0]);

                var field = ds ? ds.getField(fieldName) : null;

                if (field) {
                    if (isc.SimpleType.inheritsFrom(field.type, "date")) {
                        valueParts[0] = new Date(Date.parse(valueParts[0]));
                        valueParts[0].logicalDate = true;
                        valueParts[1] = new Date(Date.parse(valueParts[1]));
                        valueParts[1].logicalDate = true;
                    } else if (isc.SimpleType.inheritsFrom(field.type, "time")) {
                        var baseDatetime = isc.Time.createLogicalTime(0, 0, 0, 0);
                        valueParts[0] = isc.Time.parseInput(valueParts[0], false, false, false, baseDatetime);
                        baseDatetime.setSeconds(59, 999);
                        valueParts[1] = isc.Time.parseInput(valueParts[1], false, false, false, baseDatetime);
                    } else if (field.type == "text") {

                        if (!valueParts[1].endsWith(this._betweenInclusiveEndCrit)) {
                            valueParts[1] += this._betweenInclusiveEndCrit;
                        }
                    }
                }

                return { fieldName: fieldName, operator: tempOp.ID,
                    start: valueParts[0], end: valueParts[1] };
            }
        } else {
            valueParts = [value];
        }

        var skipTheseOps = [ " and ", " or " ];

        for (var i=0; i<valueParts.length; i++) {
            var valuePart = valueParts[i],
                subCrit = { fieldName: fieldName }
                field = ds ? ds.getField(fieldName) : null,
                isDateField = (field ? field && isc.SimpleType.inheritsFrom(field.type, "date") : false),
                isTimeField = (field ? field && isc.SimpleType.inheritsFrom(field.type, "time") : false),
                valueHasExpression = false
            ;

            for (var key in opIndex) {
                if (!key) continue;

                var ops = opIndex[key],
                    wildCard = false,
                    op
                ;

                if (key == "==" && isc.isA.String(valuePart) && valuePart.startsWith("=") &&
                        !valuePart.startsWith("==") && !valuePart.startsWith("=("))
                {
                    wildCard = true;
                }

                if (ops && ops.length) {
                    var finalOps = ops.findAll("caseInsensitive", insensitive);
                    if (finalOps == null || finalOps.length == 0) finalOps = ops;
                    if (finalOps.length > 1) {

                    }
                    op = finalOps[0];
                }

                if (!op || !op.symbol || skipTheseOps.contains(op.symbol)) {
                    continue;
                }

                if (validOps.contains(op.symbol) && (
                        (isc.isA.String(valuePart) && (valuePart.startsWith(op.symbol) ||

                            (op.symbol == "..." && valuePart.contains(op.symbol)))
                        )
                        || wildCard))
                {
                    valueHasExpression = true;

                    if (valuePart.startsWith(op.symbol)) {
                        valuePart = valuePart.substring(op.symbol.length - (wildCard ? 1 : 0));
                    }

                    if (op.closingSymbol) {
                        // this is a containing operator (inSet, notInSet), with opening and
                        // closing symbols...  check that the value endsWith the correct
                        // closing symbol and strip it off - op.processValue() will split
                        // the string for us later
                        if (valuePart.endsWith(op.closingSymbol)) {
                            valuePart = valuePart.substring(0, valuePart.length - op.closingSymbol.length);
                        }
                    }

                    if (valuePart.contains("...")) {
                        // allow range operators as well as conjunctives
                        var rangeValueParts = valuePart.split("...");
                        if (rangeValueParts.length == 2) {
                            var tempOps = opIndex["..."],
                                tempOp;

                            if (tempOps) tempOp = (insensitive ? tempOps.find("caseInsensitive", true) : tempOps[0]);

                            var field = ds ? ds.getField(fieldName) : null;

                            if (field) {
                                if (isc.SimpleType.inheritsFrom(field.type, "date")) {
                                    rangeValueParts[0] = new Date(Date.parse(rangeValueParts[0]));
                                    rangeValueParts[0].logicalDate = true;
                                    rangeValueParts[1] = new Date(Date.parse(rangeValueParts[1]));
                                    rangeValueParts[1].logicalDate = true;
                                } else if (isc.SimpleType.inheritsFrom(field.type, "time")) {
                                    var baseDatetime = isc.Time.createLogicalTime(0, 0, 0, 0);
                                    rangeValueParts[0] = isc.Time.parseInput(rangeValueParts[0], false, false, false, baseDatetime);
                                    baseDatetime.setSeconds(59, 999);
                                    rangeValueParts[1] = isc.Time.parseInput(rangeValueParts[1], false, false, false, baseDatetime);
                                } else if (field.type == "text") {

                                    if (!rangeValueParts[1].endsWith(this._betweenInclusiveEndCrit)) {
                                        rangeValueParts[1] += this._betweenInclusiveEndCrit;
                                    }
                                }
                            }

                            result.criteria.add({ fieldName: fieldName, operator: tempOp.ID,
                                start: rangeValueParts[0], end: rangeValueParts[1]
                            });

                            continue;
                        }
                    }

                    if (isDateField) {
                        valuePart = new Date(Date.parse(valuePart));
                        valuePart.logicalDate = true;
                    } else if (isTimeField) {
                        var baseDatetime = null;

                        // lessThan, lessOrEqual
                        if (op.upperBounds) {
                            if (op.inclusive) baseDatetime = isc.Time.createLogicalTime(0, 0, 59, 999);
                            else baseDatetime = isc.Time.createLogicalTime(0, 0, 0, 0);

                        // greaterThan, greaterOrEqual
                        } else if (op.lowerBounds) {
                            if (op.inclusive) baseDatetime = isc.Time.createLogicalTime(0, 0, 0, 0);
                            else baseDatetime = isc.Time.createLogicalTime(0, 0, 59, 999);
                        }

                        valuePart = isc.Time.parseInput(valuePart, false, false, false, baseDatetime);
                    }

                    subCrit.operator = op.ID;

                    if (op.processValue) {
                        valuePart = op.processValue(valuePart, ds);
                    }

                    if (op.wildCard && isc.isA.String(valuePart) && valuePart.contains(op.wildCard)) {
                        // this is an operator that supports wildCards (equals, notEquals)...

                        var parts = valuePart.split(op.wildCard),
                            partCrit;

                        if (parts.length > 1) {
                            for (var partIndex=0; partIndex<parts.length; partIndex++) {
                                var part = parts[partIndex];

                                if (!part || part.length == 0) continue;

                                partCrit = { fieldName: fieldName, value: part };

                                var hasPrefix = partIndex > 0,
                                    hasSuffix = parts.length - 1 > partIndex
                                ;

                                if (hasPrefix && hasSuffix) {
                                    // this is a contains criteria
                                    partCrit.operator = insensitive ? "iContains" : "contains";
                                } else if (hasPrefix) {
                                    // this is an endsWith criteria
                                    partCrit.operator = insensitive ? "iEndsWith" : "endsWith";
                                } else if (hasSuffix) {
                                    // this is a startsWith criteria
                                    partCrit.operator = insensitive ? "iStartsWith" : "startsWith";
                                }

                                result.criteria.add(partCrit);
                            }

                            // we'll include a check for this attribute when rebuilding the
                            // value later
                            this._lastValueHadWildCards = true;

                            // clear out the sub-crit's operator - this will prevent it being
                            // added to the result criteria below (we've already added
                            // everything we need above
                            subCrit.operator = null;
                        }
                    } else {
                        // set the value if one is required for the op
                        if (op.valueType != "none") subCrit.value = valuePart;
                    }

                    break;
                }

            }
            if (!valueHasExpression) {
                // this was a straight expression like "10"
                subCrit.operator = defOpName;
                subCrit.value = valuePart;
            }
            if (subCrit.operator) result.criteria.add(subCrit);
        }
//        this.logWarn("Parsed expression:" + value + " to criterion:" + this.echo(result));
        if (result.criteria.length == 1) result = result.criteria[0];
        if (result.criteria && result.criteria.length == 0) result = null;

        return result;
    },

    flattenExpressionCriteria : function (crit) {
        var result = [];

        for (var i=0; i<crit.length; i++) {
            var subCrit = crit[i];

            if (!subCrit.criteria) {
                result.add(subCrit);
            } else {
                result.addList(this.flattenExpressionCriteria(subCrit.criteria))
            }
        }

        return result;
    },



    useWildCardsByDefault: true,
    _betweenInclusiveEndCrit: "ZZZZZZZZZZ",
    buildValueExpressions : function (advancedCriteria) {
        var fullCrit = advancedCriteria,
            crit = isc.shallowClone(fullCrit),
            defaultConjunctive = " " + crit.operator + " ",
            // we're going to support more than one identical conjunctive (like 10 or 20 or 30)
            // and also multiple "between" operators simultaneaously (like 10...20 and 20...30)
            conjunctives = [defaultConjunctive],
            values = [],
            result = "",
            ds = isc.DS.get(this.form.expressionDataSource || this.form.dataSource)
        ;

        if (fullCrit.criteria) {
            crit.criteria = this.flattenExpressionCriteria(fullCrit.criteria);
        }

        var opIndex = isc.DynamicForm.getOperatorIndex(),
            opList = isc.getKeys(opIndex),
            validOps = this.validOperators
        ;

        if (!validOps) {
            validOps = [];
            for (var j=0; j< opList.length; j++) {
                var opSymbol = opIndex[opList[j]];
                validOps.addList(opSymbol.getProperty("ID"));
            }
        }

        var defOpName = this.getOperator();
        if (defOpName) validOps.add(defOpName);

        var defOp = ds ? ds.getSearchOperator(defOpName) : { id: defOpName };

        var insensitive = defOp.caseInsensitive,
            hasWildCards = false,
            wildCard
        ;

        if (!crit.criteria) {
            var critArray = [ crit ];
            crit = { criteria: critArray };
        }

        var opsWithWildCards = ["startsWith", "iStartsWith", "contains", "iContains",
                "endsWith", "iEndsWith"
        ];

        if (this.useWildCardsByDefault && this.type == "text" &&
                (crit.criteria.length > 1 ||
                (crit.criteria.length == 1 &&
                    opsWithWildCards.contains(crit.criteria[0].operator) &&
                    crit.criteria[0].value && crit.criteria[0].value.startsWith("=") &&
                    !crit.criteria[0].value.startsWith("==") && !crit.criteria[0].value.startsWith("=(")
                ) || this._lastValueHadWildCards

            ))
        {

            hasWildCards = true;
            var opSymbol = opIndex["=="];
            var equalsOp = opSymbol.find({ "ID": "equals" });
            wildCard = equalsOp.wildCard;
            conjunctives[0] = "";
        }

        var conjunctiveOffset=0;

        for (var i=0; i < crit.criteria.length; i++) {
            var subCrit = crit.criteria[i],
                subOp = subCrit.operator,
                value = subCrit.value,
                field = ds ? ds.getField(subCrit.fieldName) : null
            ;

            for (var j=0; j< opList.length; j++) {
                var opSymbol = opIndex[opList[j]];
                var tempOp = opSymbol.find({ "ID": subOp });
                if (tempOp) {
                    subOp = tempOp;
                    break;
                }
            }

            if (i>0) {
                conjunctives.add(defaultConjunctive);
            }

            if (isc.isA.String(subOp)) {
                this.logWarn("Unknown filter-expression operator: '" + subOp + "'");
            } else if (hasWildCards) {
                // we have wildCards
                if (subOp.ID == "contains" || subOp.ID == "iContains") {
                    if (values[values.length-1] != wildCard) values.add(wildCard);
                    values.add(subCrit.value);
                    values.add(wildCard);
                } else if (subOp.ID == "startsWith" || subOp.ID == "iStartsWith") {
                    values.add(subCrit.value);
                    values.add(wildCard);
                } else if (subOp.ID == "endsWith" || subOp.ID == "iEndsWith") {
                    if (values[values.length-1] != wildCard) values.add(wildCard);
                    values.add(subCrit.value);
                }
            } else if (subOp.ID == defOpName) {
                values.add(this._formatCriterionValue(subCrit.value));
            } else if (subOp.ID == "betweenInclusive" || subOp.ID == "iBetweenInclusive") {
                if (crit.criteria.length > 1) conjunctives.addAt(subOp.symbol, conjunctiveOffset);
                else conjunctives[conjunctiveOffset] = subOp.symbol
                // make sure the ... conjunctive is in the correct place
                conjunctiveOffset++;

                var endVal = subCrit.end;
                if (field && field.type == "text") {
                    if (endVal && endVal.endsWith(this._betweenInclusiveEndCrit)) {
                        endVal = endVal.replace(this._betweenInclusiveEndCrit, "");
                    }
                }
                var startVal = this._formatCriterionValue(subCrit.start);
                endVal = this._formatCriterionValue(endVal);
                if (startVal != endVal) values.addList([ startVal, endVal ]);
                else values.add(startVal);
            } else if (subOp.ID == "isNull" || subOp.ID == "notNull") {
                values.add(subOp.symbol);
            } else if (validOps.contains(subOp.ID)) {
                var op = subOp;
                if (isc.isAn.Array(value)) value = value.join(subOp.valueSeparator);
                if (op.ID != defOp) {
                    value = (op && op.symbol ? op.symbol : "") + this._formatCriterionValue(value);
                    if (op.closingSymbol) value += op.closingSymbol;
                }
                values.add(value);
            } else if (subOp.ID.startsWith("i")) {
                var otherOp = subOp.ID.substring(1),
                    initial = otherOp.charAt(0)
                ;
                otherOp = initial.toLowerCase() + otherOp.substring(1)
                if (validOps.contains(otherOp)) {
                    var op2 = opList.find("ID", otherOp);
                    if (op2.ID != defOp) {
                        value = (op && op.symbol ? op.symbol : "") + this._formatCriterionValue(value);
                        if (op.closingSymbol) value += op.closingSymbol;
                    }
                    values.add(value);
                }
            }


            conjunctiveOffset++;
        }

        if (hasWildCards) values.addAt("=", 0);

        if (conjunctives.length > 1) {
            // if the expression includes multiple expressions, the individual ones may also
            // contain a between conjunctive - build the string up manually
            for (var i=0; i<values.length; i++) {
                result += values[i];
                if (i<values.length-1) result += conjunctives[i];
            }
        } else {
            result = values.join((values.length > 1 ? conjunctives[0] : ""));
        }

        delete this._lastValueHadWildCards;

        return result.length > 0 ? result : null;
    },

    _formatCriterionValue : function (value) {
        return String(value);
    }
});

isc.FormItem.registerStringMethods({
    //>    @method formItem.showIf() (A)
    // Expression that's evaluated to see if an item should be dynamically hidden.
    // <p>
    // <code>showIf()</code> is evaluated whenever the form draws or redraws.
    // <P>
    // Note that explicit calls to +link{formItem.show()} or +link{formItem.hide()} will
    // will wipe out the <code>showIf</code> expression.
    //
    // @param    item    (FormItem)    the form item itself (also available as "this")
    // @param    value   (any)         current value of the form item
    // @param    form    (DynamicForm) the managing DynamicForm instance
    // @param   values  (Object)      the current set of values for the form as a whole
    // @return (boolean) whether the item should be shown
    //
    // @example formShowAndHide
    // @visibility external
    //<
    showIf : "item,value,form,values",

    //> @method formItem.defaultDynamicValue() (A)
    // Expression evaluated to determine the +link{FormItem.defaultValue} when no value is
    // provided for this item.
    // <P>
    // If you don't need dynamic evaluation, you can just use <code>item.defaultValue</code>.
    //
    // @param    item    (FormItem)    the form item itself (also available as "this")
    // @param    form    (DynamicForm) the managing DynamicForm instance
    // @param   values  (Object)      the current set of values for the form as a whole
    //
    // @see attr:defaultValue
    // @group formValues
    // @visibility external
    //<
    defaultDynamicValue : "item,form,values",

    //> @method formItem.focus
    // Called when this FormItem receives focus.
    //
    // @param    form    (DynamicForm) the managing DynamicForm instance
    // @param    item    (FormItem)    the form item itself (also available as "this")
    // @group eventHandling
    // @visibility external
    //<
    focus : "form,item",

    //> @method formItem.blur
    // Called when this FormItem loses focus.
    //
    // @param    form    (DynamicForm) the managing DynamicForm instance
    // @param    item    (FormItem)    the form item itself (also available as "this")
    // @group eventHandling
    // @visibility external
    //<
    blur : "form,item",



    //> @method formItem.editorEnter()
    // Notification method fired when the user enters this formItem.
    // Differs from +link{formItem.focus()} in that while <code>focus</code> and <code>blur</code>
    // may fire multiple as the user navigates sub elements of an item (such as interacting
    // with a pick list), <code>editorEnter</code> will typically fire once when the user
    // starts to edit this item as a whole, and once when the user moves onto a different
    // item or component
    // @param form (DynamicForm) form containing this item
    // @param item (FormItem) form item recieving focus
    // @param value (Any) current item value.
    // @group eventHandling
    // @visibility external
    //<
    editorEnter : "form,item,value",

    //> @method formItem.editorExit
    // Notification method fired when the user leaves this formItem.
    // Differs from +link{formItem.blur()} in that while <code>focus</code> and <code>blur</code>
    // may fire multiple as the user navigates sub elements of an item (such as interacting
    // with a pick list), <code>editorEnter</code> will typically fire once when the user
    // starts to edit this item as a whole, and <code>editorExit</code> fires once when the
    // user moves onto a different item or component
    // @param form (DynamicForm) form managing this form item
    // @param item (FormItem) pointer to the form item being managed
    // @param value (any) current value of the form item
    // @group eventHandling
    // @visibility external
    //<
    editorExit : "form,item,value",

    //> @method formItem.click
    // Called when this FormItem is clicked on.
    // <P>
    // Note: <code>click()</code> is available on StaticTextItem, BlurbItems, ButtonItem, and
    // derivatives.  Other form items (such as HiddenItem) do not support <code>click()</code>.
    //
    // @param    form    (DynamicForm) the managing DynamicForm instance
    // @param    item    (FormItem)    the form item itself (also available as "this")
    // @return (boolean) Return false to cancel the click event. This will prevent the event from
    //   bubbling up, suppressing
    //   +link{canvas.click,click} on the form containing this item.
    // @group eventHandling
    // @visibility external
    //<
    click : "form,item",

    //> @method formItem.doubleClick
    // Called when this FormItem is double-clicked.
    //
    // @param    form    (DynamicForm) the managing DynamicForm instance
    // @param    item    (FormItem)    the form item itself (also available as "this")
    // @return (boolean) Return false to cancel the doubleClick event. This will prevent the event from
    //   bubbling up, suppressing
    //   +link{canvas.doubleClick,doubleClick} on the form containing this item.
    // @group eventHandling
    // @visibility external
    //<
    doubleClick : "form,item",

    //> @method formItem.iconClick()
    //      StringMethod.
    //      Default action to fire when the user clicks on a form item icon. May be overridden
    //      by setting <code>click</code> on the form item icon directly.
    //  @group  formIcons
    //  @visibility external
    //  @param form (DynamicForm)   a pointer to this item's form
    //  @param  item    (FormItem)  a pointer to this form item
    //  @param  icon    (FormItemIcon)  a pointer to the icon that received the click event.
    //<
    // Note - developers would be more likely to set a click handler on each icon.
    iconClick : "form,item,icon",

    //> @method formItem.iconKeyPress()
    //      StringMethod.
    //      Default action to fire when an icon has keyboard focus and the user types a key.
    //      May be overridden by setting <code>keyPress</code> on the form item icon directly.
    //  @group  formIcons
    //  @visibility external
    //  @param keyName (string) name of the key pressed
    //  @param character (character) character produced by the keypress
    //  @param form (DynamicForm)   a pointer to this item's form
    //  @param  item    (FormItem)  a pointer to this form item
    //  @param  icon    (FormItemIcon)  a pointer to the icon that received the click event.
    //<
    iconKeyPress : "keyName,character,form,item,icon",

    //> @method formItem.change()
    // Called when a FormItem's value is about to change as the result of user interaction.  This
    // method fires after the user performed an action that would change the value of this field,
    // but before the element itself is changed.
    // <P>
    // Returning false cancels the change.  Note that if what you want to do is
    // <b>transform</b> the user's input, for example, automatically change separator
    // characters to a standard separator character, you should implement
    // +link{formItem.transformInput,transformInput} rather than using a combination of
    // change() and setValue() to accomplish the same thing.  Returning false from
    // <code>change</code> is intended for rejecting input entirely, such as typing invalid
    // characters.
    // <p>
    // Note that if you ask the form for the current value in this handler, you will get the old
    // value because the change has not yet been committed.  The new value is available as a
    // parameter to this method.
    //
    // @param    form    (DynamicForm) the managing DynamicForm instance
    // @param    item    (FormItem)    the form item itself (also available as "this")
    // @param   value   (any)         The new value of the form item
    // @param   oldValue    (any)     The previous value of the form item
    //
    // @return  (boolean) In your handler, return false to cancel the change, true to allow the change
    // @group eventHandling
    // @visibility external
    // @example fieldEnableDisable
    //<
    change : "form,item,value,oldValue",

    //> @method formItem.changed()
    // Called when a FormItem's value has been changed as the result of user interaction.  This
    // method fires after the newly specified value has been stored.
    // @param    form    (DynamicForm) the managing DynamicForm instance
    // @param    item    (FormItem)    the form item itself (also available as "this")
    // @param   value   (any)         The current value (after the change).
    // @group eventHandling
    // @visibility external
    //<
    changed : "form,item,value",



    //> @method formItem.transformInput()
    // Called when a FormItem's value is about to change as the result of user interaction.  This
    // method fires after the user performed an action that would change the value of this field,
    // and allows the developer to modify / reformat the value before it gets validated / saved.
    // Fires before +link{formItem.change}.
    // <P>
    // Return the reformatted value.
    //
    // @param    form    (DynamicForm) the managing DynamicForm instance
    // @param    item    (FormItem)    the form item itself (also available as "this")
    // @param   value   (any)         The new value of the form item
    // @param   oldValue    (any)     The previous (current) value of the form item
    //
    // @return  (any) The desired new value for the form item
    // @visibility external
    //<
    transformInput : "form,item,value,oldValue",


    cellClick : "form,item",
    cellDoubleClick : "form,item",

    //> @method formItem.titleClick()
    // Notification method fired when the user clicks the title for this item
    // @param form (DynamicForm) the managing DynamicForm instance
    // @param item (FormItem) the form item whose title was clicked
    // @return (boolean) Return false to cancel the click event. This will prevent the event from
    //   bubbling up, suppressing
    //   +link{canvas.click,click} on the form containing this item.
    // @visibility external
    //<
    titleClick : "form,item",
    //> @method formItem.titleDoubleClick()
    // Notification method fired when the user double-clicks the title for this item
    // @param form (DynamicForm) the managing DynamicForm instance
    // @param item (FormItem) the form item whose title was double-clicked
    // @return (boolean) Return false to cancel the doubleclick event. This will prevent the event from
    //   bubbling up, suppressing
    //   +link{canvas.doubleClick,doubleClick} on the form containing this item.
    // @visibility external
    //<
    titleDoubleClick : "form,item",

    mouseMove : "form,item",
    mouseOver : "form,item",
    mouseOut : "form,item",
    titleMove : "form,item",
    titleOver : "form,item",
    titleOut : "form,item",
    textBoxMove : "form,item",
    textBoxOver : "form,item",
    textBoxOut : "form,item",


    itemHover : "item,form",
    titleHover : "item,form",
    valueHover : "item,form",

    //> @method formItem.keyPress()
    // StringMethod fired when the user presses a key while focused in this form item.
    //
    // @param item (FormItem) Item over which the keypress occurred
    // @param form (DynamicForm) Pointer to the item's form
    // @param keyName (KeyName) Name of the key pressed (Example: <code>"A"</code>, <code>"Enter"</code>)
    // @param characterValue (number) If this was a character key, this is the numeric value
    //        for the character
    //
    // @return (boolean) return false to attempt to cancel the event.  Note for general purpose
    //                   APIs for managing whether user input is allowed, use +link{change()}
    //                   or +link{transformInput()} instead.
    //
    //
    // @group eventHandling
    // @visibility external
    //<
    keyPress : "item, form, keyName, characterValue",  // was keyNum, form, item

    // NOTE: characterValue not passed to keyDown/keyUp because it's not guaranteed to be
    // available for these events

    //> @method formItem.keyDown()
    // StringMethod fired in response to a keydown while focused in this form item.
    //
    // @param item (FormItem) Item over which the keydown occurred
    // @param form (DynamicForm) Pointer to the item's form
    // @param keyName (KeyName) Name of the key pressed (Example: <code>"A"</code>, <code>"Enter"</code>)
    // @return (boolean) return false to attempt to cancel the event.  Note for general purpose
    //                   APIs for managing whether user input is allowed, use +link{change()}
    //                   or +link{transformInput()} instead.
    //
    // @group eventHandling
    // @visibility external
    //<
    keyDown : "item,form,keyName",

    //> @method formItem.keyUp()
    // StringMethod fired in response to a keyup while focused in this form item.
    //
    // @param item (FormItem) Item over which the keyup occurred
    // @param form (DynamicForm) Pointer to the item's form
    // @param keyName (KeyName) Name of the key pressed (Example: <code>"A"</code>, <code>"Enter"</code>)
    // @return (boolean) return false to attempt to cancel the event.  Note for general purpose
    //                   APIs for managing whether user input is allowed, use +link{change()}
    //                   or +link{transformInput()} instead.
    //
    // @group eventHandling
    // @visibility external
    //<
    keyUp : "item,form,keyName",

    //> @method formItem.getValueIcon()
    // If specified this stringMethod allows the developer to specify the image source for an
    // icon to be displayed for the current form item value.
    // <P>
    // Takes precedence over +link{FormItem.valueIcons}
    //
    // @param value (any) value of the item for which an item should be returned.
    //
    // @group valueIcons
    // @visibility external
    //<
    getValueIcon : "value",

    // called via DF.saveData() callback.  Return false from this method to perform async
    // processing before saveData() callback is called.  Then call form.saveDataComplete() to
    // tell the form to proceed.
    formSaved: "request,response,data",


    // Custom formatters and parsers, documented above
    formatValue:"value,record,form,item",
    formatEditorValue:"value,record,form,item",
    parseEditorValue:"value,form,item"

});

isc.FormItem.getPrototype().toString = function () {
    return "[" + this.Class + " ID:" + this.ID +
            (this.name != null ? " name:" + this.name : "") + "]";
};







//>    @class    FormItemFactory
//
//    Singleton class to create FormItems for you from object literals
//
//    TODO: consider making this a static method on FormItem.
//
//<
isc.ClassFactory.defineClass("FormItemFactory");
isc.FormItemFactory.addClassMethods({

    // given a form item instantiation object, return the name of the formItem class to use
    getItemClassName : function (object, className, form, dontChangeObject) {
        if (className == null) className = object.editorType || object.formItemType ||
                                                    object.type;

        // Special case SelectOtherItems and SelectItems -
        //  look back at the form's useNativeSelectItems property.
        if (isc.isA.String(className)) {
            var lccn = className.toLowerCase();

            if (lccn.contains(isc.DynamicForm._$select)) {
                // make 'selectOther's into 'selects' with the isSelectOther property set to
                // true.

                if (lccn == "selectother" || lccn == "selectotheritem") {
                    lccn = "select";
                    if (!dontChangeObject) object.isSelectOther = true;
                }

                if (lccn == "select" || lccn == "selectitem") {
                    form = object.form || form;
                    var useNativeSelect = (!isc.ListGrid ||
                                  (object.multiple && object.multipleAppearance == "grid") ||
                                  (form ? form.useNativeSelectItems : false));
                    if (useNativeSelect) className = "NativeSelectItem"
                    else className = "SelectItem";
                }
            }
            if (lccn == isc.DynamicForm._$multifile) className = "MultiFileItem";
            else if (lccn == isc.DynamicForm._$multiupload) className = "MultiUploadItem";
            else if (lccn == isc.DynamicForm._$base64Binary.toLowerCase()) className = "SOAPUploadItem";
        }
        return className;
    },

    // get the form item class that className seems to indicate, or null if there's no match
    _$text : "text",
    _$Item : "Item",
    _$TextareaItem : "TextareaItem",
    _$TextAreaItem : "TextAreaItem",
    _$DatetimeItem : "DatetimeItem",
    _$DateTimeItem : "DateTimeItem",
    _classTable : {},
    getItemClass : function (className) {

        var classObject = isc.ClassFactory.getClass(className);

        // if the className was not the literal class name of a FormItem subclass
        if (!classObject || !isc.isA.FormItem(classObject)) {
            // Assume that an SGWTFactory is a FormItem subclass if supplied here.
            // We don't have an easy way to actually check at this point.
            if (isc.SGWTFactory && isc.isA.SGWTFactory(classObject)) return classObject;

            // catch ToolSkin subclasses of form items, like TTextAreaItem here
            if (className != null && className.startsWith("T")) {
                var normalClassName = className.substring(1),
                    classObject = isc.ClassFactory.getClass(normalClassName);
                if (isc.isA.FormItem(classObject)) return classObject;
            }

            if (className == null) className = this._$text;
            var table = this._classTable,
                officialName = table[className];
            if (!officialName) {
                officialName = table[className] =
                    // assume the short name of the type was used (eg text -> TextItem)
                    className.substring(0,1).toUpperCase() +
                            className.substring(1) + this._$Item;
            }
            // synonym
            if (officialName == this._$TextareaItem) officialName = this._$TextAreaItem;
            if (officialName == this._$DatetimeItem) officialName = this._$DateTimeItem;
            classObject = isc.ClassFactory.getClass(officialName);
        }
        return classObject; // may still be null
    },

    //>    @method    FormItemFactory.makeItem()    (A)
    //        @group    creation
    //            given an object literal, convert it to an appropriate FormItem type
    //
    //        @param    object        (object)    properties for the new object
    //    XXX we may want to pass other defaults here...
    //        @return    (FormItem)        an appropriate FormItem
    //<
    makeItem : function (object) {

        if (object == null) return null;

        if (isc.isA.FormItem(object)) {
            //>DEBUG
            //this.logDebug("Returning formItem " + object);
               //<DEBUG
            return object;
        }

        var className = this.getItemClassName(object),
            classObject = this.getItemClass(className);

        // if no class was found, use a generic TextItem
        if (!classObject) {
            //>DEBUG
            this.logWarn("makeItem(): type " + object.type + " not recognized, using TextItem");
            //<DEBUG
            classObject = isc.TextItem;
        }

        //>DEBUG
        //this.logDebug("Making item of class " + classObject.Class);
        //<DEBUG
        return isc.ClassFactory.newInstance(classObject, object);
    }
});







//-------------------------------
// Field Validation functions
//-------------------------------

// Define Validators class for docs
//> @class Validator
// A validator describes a check that should be performed on a value the user is trying to
// save.
// <p>
// Validators are specified for DataSource fields via the +link{attr:DataSourceField.validators}
// property.  Validators that need not be run on the server can also be specified for a
// specific +link{class:FormItem} or +link{class:ListGridField}.
// <p>
// SmartClient supports a powerful library of +link{type:ValidatorType,ValidatorTypes} which
// have identical behavior on both the client and the server.
// <p>
// Beyond this, custom validators can be defined on the client and custom validation logic
// added on the server.  Note that the <code>regexp</code> and <code>mask</code> validator
// types are very flexible and can be used to perform virtually any kind of formatting check
// that doesn't involve some large external dataset.
// <p>
// Custom validators can be reused on the client by adding them to the global validator list,
// via the +link{classMethod:Validator.addValidator()} method.
//
// @serverDS allowed
// @visibility external
// @see ValidatorType
// @treeLocation Client Reference/Forms
//<

//> @attr validator.type (ValidatorType : null : IR)
// Type of the validator.
// <p>
// This can be one of the built-in +link{type:ValidatorType}, the string "custom" to define
// a custom validator, or the string "serverCustom" to define a server-only custom validator.
//
// @serverDS allowed
// @visibility external
//<

//> @attr validator.name (String : null : IR)
// Descriptive name for a validator. Useful when displaying or editing rules in
// a +link{RuleEditor}.
// @visibility rules
//<

//> @attr validator.description (String : null : IR)
// Optional description of this validator, typically used to explain the purpose behind the
// validator. Useful when displaying or editing validators in a +link{RuleEditor}
// @visibility rules
//<

//> @attr validator.applyWhen (AdvancedCriteria : null : IRA)
// Used to create a conditional validator based on +link{AdvancedCriteria,criteria}.
// The criteria defines when the validator applies. The form current values or ListGrid record
// is used as reference for the criteria. If the criteria matches the validator will be
// processed. Otherwise the validator is skipped and assumed valid.
// <p>
// To use an <code>applyWhen</code> criteria the form or grid must use a +link{DataSource}.
// @serverDS allowed
// @visibility external
//<

//> @attr validator.dependentFields (Array of String : null : IRA)
// User-defined list of fields on which this validator depends. Primarily used for validators
// of type "custom" but can also be used to supplement +link{validator.applyWhen} criteria.
// @serverDS allow
// @visibility external
// @see validator.applyWhen
//<

//> @method validator.condition()
// For a validator that is not a built-in +link{type:ValidatorType}, a function or
// String expression to evaluate to see if this validator passes or fails.
// <p>
// Because the validator declaration itself is passed as a parameter to
// <code>condition()</code>, you can effectively parameterize the validator.  For example, to
// create a validator that checks that the value is after a certain date:<pre>
//     { type:"custom", afterDate:new Date(),
//       condition:"value.getTime() > validator.afterDate.getTime()" }
// </pre>
// Reusable validators, like the above, can be registered as a standard validatorType by
// calling +link{Validator.addValidator()}.
// <P>
// Note that, if a field is declared with a builtin +link{type:FieldType}, the value passed in
// will already have been converted to the specified type, if possible.
//
// @param item (DataSourceField or FormItem) FormItem or DataSourceField on which this
//                                           validator was declared.  NOTE: FormItem will not
//                                           be available during a save performed without a
//                                           form (eg programmatic save) or if the field
//                                           is not available in the form.
// @param validator (Validator) Validator declaration from eg
//                              +link{DataSourceField.validators}.
// @param value     (any)       value to validate
// @param record (object) Field values for record being validated.
// @return (boolean) whether the value passed validation.  True for passed, false for fail.
//
//
// @serverDS allowed
// @visibility external
//<


//> @attr validator.serverCondition (String : null : IR)
// For validators of type "serverCustom" only: a scriptlet in any supported JSR223 scripting
// language which is run in order to see if validation passes.  For example:
// <P>
// <pre>
//     &lt;validator type="serverCustom"&gt;
//         &lt;serverCondition language="groovy"&gt;&lt;![CDATA[
//             value &lt; dataSources.StockItem.fetchById(record.itemId).quantity
//         ]]&gt;&lt;/serverCondition&gt;
//     &lt;/validator&gt;
// </pre>
// The scriptlet should return a boolean true or false value - failing to return a value will
// be considered a false result (validator failed).  If your expression is syntactically
// invalid, an exception is thrown and the error message is displayed in the client.
// <P>
// See +link{group:serverScript} for general information on Server Scripting and JSR223, and
// +link{group:velocitySupport} for general information on Velocity support, and also see below
// for special rules for Velocity.
// <P>
// <b>Available variables</b>
// The following variables are available in a <code>serverCondition</code>:
// <ul>
// <li><b>dataSource</b> - The current DataSource</li>
// <li><b>record</b> - Other submitted values part of the same update</li>
// <li><b>value</b> - The value of the field</li>
// <li><b>validator</b> - The config of this validator, including all attributes declared on
//                        the &lt;validator&gt; tag, presented as a <code>Map</code></li>
// <li><b>field</b> - The field (as a <code>DSField</code> object)</li>
// </ul>
// Note that "record" will contain only other values submitted at the same time, not the
// complete DataSource record.  For most types of cross-field validation, you should fetch the
// current saved record using the server-side API DataSource.fetchById().  For example, in
// Velocity:
// <pre>
//     $dataSource.fetchById($record.<i>primaryKeyField</i>).otherFieldName
// </pre>
// Note that, while a DSRequest provides dsRequest.oldValues, these values cannot be relied
// upon for a security check since they could be faked.
// <P>
// Server-side custom validators also have access to the standard set of context variables that
// come from the Servlet API.  However, be aware that if you write conditions that depend upon
// these variables, you preclude your Validator from being used in the widest possible variety
// of circumstances; for example, in a command-line process.  Rather, it will be tied to
// operating in the context of, say, an <code>HttpSession</code>.
// <P>
// Given the above caveat, the following context variables are also available:
// <ul>
// <li><b>servletRequest</b> - The associated <code>HttpServletRequest</code></li>
// <li><b>session</b> - The associated <code>HttpSession</code></li>
// <li><b>httpParameters</b> - This variable gives you access to the parameters <code>Map</code>
//         of the associated <code>HttpServletRequest</code>; it is an alternate form of
//         <code>$servletRequest.getParameter</code></li>
// <li><b>requestAttributes</b> - This variable gives you access to the attributes <code>Map</code>
//         of the associated <code>HttpServletRequest</code>; it is an alternate form of
//         <code>$servletRequest.getAttribute</code></li>
// <li><b>sessionAttributes</b> - This variable gives you access to the attributes <code>Map</code>
//         of the associated <code>HttpSession</code>; it is an alternate form of
//         <code>$session.getAttribute</code></li>
// </ul>
// <P>
// <b>Special considerations for Velocity</b>
// <P>
// To return a true or false value in Velocity, you script can either be just an expression
// that returns a boolean value, or the result of evaluating the Velocity template can result
// in output of "true" or "false".  All of the following are valid forms:
// <p><code>
// &nbsp;&nbsp;$value &lt; 100<br>
// &nbsp;&nbsp;$util.contains($value, "some string")<br>
// &nbsp;&nbsp;$record.someField</code>(assuming that "someField" contains a boolean value)<code><br>
// &nbsp;&nbsp;$value &gt; $record.otherField</code>
// <P>
// For additional troubleshooting information when Velocity expressions aren't working as
// expected, set the log category org.apache.Velocity to DEBUG in log4j.isc.config.xml.
// <P>
// Because it's tricky to call arbitrary Java methods in Velocity, the following special
// objects are passed to Velocity for convenience:
// <ul>
// <li><b>dataSources</b> - The list of all DataSources, accessible by name (so, for example,
//     <code>$dataSources.supplyItem</code> refers to the <code>supplyItem</code> DataSource
//     object).</li>
// <li><b>util</b> - A <code>com.isomorphic.util.DataTools</code> object, giving you access to
//               all of that class's useful helper functions</li>
// </ul>
//
// @serverDS only
// @example scriptingValidation
// @example velocityValidation
// @visibility external
//<

//> @attr validator.serverObject (ServerObject : null : IR)
// For validators of type "serverCustom" only, a +link{ServerObject} declaration that allows
// the SmartClient Server to find a Java class via a variety of possible approaches, and call a
// method on it to perform validation.
// <P>
// The target object must implement a method whose first 4 arguments are:
// <code>
//    Object value, Validator validator, String fieldName, Map record
// </code><p>
// (<code>com.isomorphic.datasource.Validator</code> is a subclass of <code>Map</code> that
// represents a validator's configuration, and also provides APIs for implementing templated
// error messages).<p>
// You provide the name of the method to call by specifying
// +link{serverObject.methodName,methodName}
// as part of the serverObject declaration.  If you do not specify a methodName, SmartClient
// expects to find a compliant method called "condition".
// <P>
// Additional arguments may be declared and are automatically supplied based on the declared
// argument type, via +link{dmiOverview,DMI}.  Available objects include:
// <ul>
// <li><b>DataSource</b> - the DataSource where this validator is declared, an instance of
//                         com.isomorphic.datasource.DataSource or a subclass</li>
// <li><b>HttpServletRequest</b> - from standard Java servlets API</li>
// <li><b>HttpServletResponse</b> - from standard Java servlets API</li>
// <li><b>ServletContext</b> - from standard Java servlets API</li>
// <li><b>HttpSession</b> - from standard Java servlets API</li>
// <li><b>RequestContext</b> - an instance of com.isomorphic.servlet.RequestContext</li>
// <li><b>RPCManager</b> - the RPCManager associated with the transaction this validation is
//                         part of; an instance of com.isomorphic.rpc.RPCManager</li>
// </ul>
// Note that any servlet-related objects will not be available if your validator is run outside
// of the scope of an HTTP servlet request, such as a command-line process.
//
// @serverDS only
// @visibility external
// @example dmiValidation
//<

//> @attr validator.resultingValue (Object : null : IR)
// To transform the incoming value that is validated into a different value or format set this
// property from +link{validator.condition()} to the desired value.
// @serverDS allowed
// @visibility external
//<

//> @attr validator.errorMessage (String : null : IR)
// Text to display if the value does not pass this validation check.
// <P>
// If unspecified, default error messages exist for all built-in validators, and a generic
// message will be used for a custom validator that is not passed.
// @serverDS allowed
// @visibility external
// @example conditionallyRequired
//<

//> @attr validator.stopIfFalse (Boolean : false : IR)
// Normally, all validators defined for a field will be run even if one of the validators has
// already failed.  However, if <code>stopIfFalse</code> is set, validation will not proceed
// beyond this validator if the check fails.
// <P>
// This is useful to prevent expensive validators from being run unnecessarily, or to allow
// custom validators that don't need to be robust about handling every conceivable type of
// value.
//
// @serverDS allowed
// @visibility external
//<

//> @attr validator.stopOnError (boolean : null : IR)
// Indicates that if this validator is not passed, the user should not be allowed to exit
// the field - focus will be forced back into the field until the error is corrected.
// <p>
// This property defaults to +link{FormItem.stopOnError} if unset.
// <p>
// Enabling this property also implies +link{FormItem.validateOnExit} is automatically
// enabled. If this is a server-based validator, setting this property also implies that
// +link{FormItem.synchronousValidation} is forced on.
//
// @serverDS allowed
// @visibility external
//<

//> @attr validator.clientOnly (Boolean : false : IR)
// Indicates this validator runs on the client only.
// <p>
// Normally, if the server is trying to run validators and finds a validator that it can't
// execute, for safety reasons validation is considered to have failed.  Use this flag to
// explicitly mark a validator that only needs to run on the client.
//
// @serverDS allowed
// @visibility external
//<

//> @attr validator.validateOnChange (boolean : null : IRW)
// If true, validator will be validated when each item's "change" handler is fired
// as well as when the entire form is submitted or validated. If false, this validator
// will not fire on the item's "change" handler.
// <p>
// Note that this property can also be set at the form/grid or field level;
// If true at any level and not explicitly false on the validator, the validator will be
// fired on change - displaying errors and rejecting the change on validation failure.
//
// @serverDS allowed
// @visibility external
//<




//> @object validatorDefinition
// Validator definition for a built-in +link{Validator.type}.
//
// @treeLocation Client Reference/Forms/Validator
// @visibility external
//<
// Unexposed properties
// - valueType: similar to Operator.valueType -- basically what kind of condition is this - is it
//   a value (like equals, substring, etc), a range, a list of values, none or something custom.
//   Used by the RuleEditor
// - dataType: Many validators only apply to specific data types -- For example integerRange,
//   floatLimit, etc. Sepecifying a dataType indicates that a validator should apply to a data-source
//   field of the specified type only.
//   May be specified as a single type, an array of types, or "none" (or unset) meaning it's not
//   type-specific at all (EG "required").
//   Again - used by the RuleEditor.

//> @attr validatorDefinition.type (string : null : IR)
// Type of the validator unique in +link{type:ValidatorType}.
//
// @visibility external
//<
//> @attr validatorDefinition.title (string : null : IR)
// Descriptive name of the validator.
//
// @visibility external
//<
//> @attr validatorDefinition.requiresServer (boolean : false : IR)
// Does this validator only run server-side?
//
// @visibility external
//<

//> @attr validatorDefinition.defaultErrorMessage (string : null : IR)
// Default error message to be shown when validator fails validation. Can be overridden
// for an individual validator by setting +link{validator.errorMessage}.
//
// @visibility external
//<

//> @method validatorDefinition.condition()
// Method invoked to perform the actual validation of a value.
// <p>
// Because the validator itself is passed as a parameter to
// <code>condition()</code>, you can effectively parameterize the validator.  For example, to
// create a validator that checks that the value is after a certain date:<pre>
//     { type:"custom", afterDate:new Date(),
//       condition:"value.getTime() > validator.afterDate.getTime()" }
// </pre>
// Note that, if a field is declared with a builtin +link{type:FieldType}, the value passed in
// will already have been converted to the specified type, if possible.
//
// @param item (DataSourceField or FormItem) FormItem or DataSourceField on which this
//                                           validator was declared.  NOTE: FormItem will not
//                                           be available during a save performed without a
//                                           form (eg programmatic save) or if the field
//                                           is not available in the form.
// @param validator (Validator) Validator declaration from eg
//                              +link{DataSourceField.validators}.
// @param value     (any)       value to validate
// @param record    (object)    Field values for record being validated.
// @return (boolean) whether the value passed validation.  True for passed, false for fail.
//
// @serverDS allowed
// @visibility external
//<


//> @method validatorDefinition.action
// This method is called after every validation (i.e. call to
// +link{validatorDefinition.condition}) whether it passed or failed. This allows the
// validator perform an operation on the field based on the validation outcome.
// <p>
// An <code>action()</code> method is not needed to report an error message only.
//
// @param result    (boolean)   The result of the validator. The value will be null if
//                              the validator was skipped because of conditional criteria.
// @param item (DataSourceField or FormItem) FormItem or DataSourceField on which this
//                                           validator was declared.  NOTE: FormItem will not
//                                           be available during a save performed without a
//                                           form (eg programmatic save) or if the field
//                                           is not available in the form.
// @param validator (Validator) Validator declaration from eg
//                              +link{DataSourceField.validators}.
// @param record (Record) Record that was validated
// @param component (DataBoundComponent) The DataBoundComponent holding the item such
//                                       DynamicForm or ListGrid.
//
// @visibility external
//<


//> @type ValidatorType
// Used to name a validator or reference a standard, built-in +link{validator} - see list below.
// <p>
// To make use of a standard validator type for a field in a DataSource, or
// DynamicForm instance, specify the <code>validators</code> property to an array
// containing a validator definition where the <code>type</code> property is set to
// the appropriate type.
// <p>
// A custom error message can be specified for any validator type by setting the
// <code>errorMessage</code> property on the validator definition object, and some
// validator types make use of additional properties on the validator definition
// object such as <code>max</code> or <code>min</code>.
// <p>
// For example, to use the <code>integerRange</code> validator type:<br><br><code>
// &nbsp;&nbsp;field:{<br>
// &nbsp;&nbsp;&nbsp;&nbsp;validators:[<br>
// &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{type:"integerRange", min:1, max:100}<br>
// &nbsp;&nbsp;&nbsp;&nbsp;]<br>
// &nbsp;&nbsp;}
// </code>
// <p>
// Custom validators can be reused on the client by adding them to the global validator list,
// via the +link{classMethod:Validator.addValidatorDefinition()} method.
//
// @value isBoolean
// Validation will fail if this field is non-empty and has a non-boolean value.
// @value isString
// Validation will fail if the value is not a string value.
// @value isInteger
// Tests whether the value for this field is a whole number.  If
// <code>validator.convertToInteger</code> is true, float values will be converted
// into integers and validation will succeed.
// @value isFloat
// Tests whether the value for this field is a valid floating point number.
// @value isFunction
// Tests whether the value for this field is a valid expression or function; if it is
// valid, creates a +link{group:stringMethods,stringMethod} object with the value
// and set the resultingValue to the StringMethod.
// @value requiredIf
// RequiredIf type validators should be specified with an <code>expression</code>
// property set to a +link{group:stringMethods,stringMethod}, which takes three
// parameters:<ul>
// <li>item - the DynamicForm item on which the error occurred (may be null)
// <li>validator - a pointer to the validator object
// <li>value - the value of the field in question
// <li>record - the "record" object - the set of values being edited by the widget
// </ul>
// When validation is performed, the expression will be evaluated (or executed) - if it
// returns <code>true</code>, the field will be treated as a required field, so validation
// will fail if the field has no value.
// <p>To allow server-side enforcement, a <code>required</code> validator can be used instead.
// Conditional criteria can be specified with the <code>applyWhen</code> property.
// <p>See +explorerExample{conditionallyRequired}.
// @value matchesField
// Tests whether the value for this field matches the value of some other field.
// The field to compare against is specified via the <code>otherField</code> property
// on the validator object (should be set to a field name).
// <p>See +explorerExample{matchValue}.
// @value isOneOf
// Tests whether the value for this field matches any value from an arbitrary
// list of acceptable values.  The set of acceptable values is specified via
// the <code>list</code> property on the validator, which should be set to an array of
// values. If validator.list is not supplied, the valueMap for the field will be used.
// If there is no valueMap, not providing validator.list is an error.
// @value integerRange
// Tests whether the value for this field is a whole number within the range
// specified.  The <code>max</code> and <code>min</code> properties on the validator
// are used to determine the acceptable range, inclusive. To specify the range as
// exclusive of the min/mix values, set <code>exclusive</code> to <code>true</code>.
// <p>See +explorerExample{validationBuiltins}.
// @value lengthRange
// This validator type applies to string values only.  If the value is a string value
// validation will fail if the string's length falls outside the range specified by
// <code>validator.max</code> and <code>validator.min</code>.
// <p>
// Note that non-string values will always pass validation by this validator type.
// <p>
// Note that the <code>errorMessage</code> for this validator will be evaluated as
// a dynamicString - text within <code>\${...}</code> will be evaluated as JS code
// when the message is displayed, with <code>max</code> and <code>min</code> available as
// variables mapped to <code>validator.max</code> and <code>validator.min</code>.
// @value contains
// Determine whether a string value contains some substring specified via
// <code>validator.substring</code>.
// @value doesntContain
// Determine whether a string value does <b>not</b> contain some substring specified via
// <code>validator.substring</code>.
// @value substringCount
// Determine whether a string value contains some substring multiple times.
// The substring to check for is specified via <code>validator.substring</code>.
// The <code>validator.operator</code> property allows you to specify how to test
// the number of substring occurrences. Valid values for this property are
// <code>==</code>, <code>!=</code>, <code>&lt;</code>, <code>&lt;=</code>,
// <code>&gt;</code>, <code>&gt;=</code>.
// <p>
// The number of matches to check for is specified via <code>validator.count</code>.
// @value regexp
// <code>regexp</code> type validators will determine whether the value specified
// matches a given regular expression.  The expression should be specified on the
// <code>validator</code> object as the <code>expression</code> property.
// <p>See +explorerExample{regularExpression}.
// @value mask
// Validate against a regular expression mask, specified as <code>validator.mask</code>.
// If validation is successful a transformation can also be specified via the
// <code>validator.transformTo</code> property. This should be set to a string in the
// standard format for string replacement via the native JavaScript <code>replace()</code>
// method.
// <p>See +explorerExample{valueTransform}.
// @value dateRange
// Tests whether the value for a date field is within the range specified.
// Range is inclusive, and is specified via <code>validator.min</code> and
// <code>validator.max</code>, which should be specified in
// <a target=_blank href="http://www.w3.org/TR/xmlschema-2/#dateTime">XML Schema
// date format</a> or as a live JavaScript Date object (for client-only validators only).
// To specify the range as exclusive of the min/mix values, set <code>exclusive</code>
// to <code>true</code>.
// <p>
// Note that the <code>errorMessage</code> for this validator will be evaluated as
// a dynamicString - text within <code>\${...}</code> will be evaluated as JS code
// when the message is displayed, with <code>max</code> and <code>min</code> available as
// variables mapped to <code>validator.max</code> and <code>validator.min</code>.
// @value floatLimit
// Validate a field as a valid floating point value within a value range.
// Range is specified via <code>validator.min</code> and <code>validator.max</code>.
// Also checks precision, specified as number of decimal places in
// <code>validator.precision</code>. If <code>validator.roundToPrecision</code> is set
// a value that doesn't match the specified number of decimal places will be rounded
// to the nearest value that does.
// <p>
// For backwards compatibility only. Use "floatRange" and/or "floatPrecision" instead.
// @value floatRange
// Tests whether the value for this field is a floating point number within the range
// specified.  The <code>max</code> and <code>min</code> properties on the validator
// are used to determine the acceptable range, inclusive. To specify the range as
// exclusive of the min/mix values, set <code>exclusive</code> to <code>true</code>.
// <p>
// Note that the <code>errorMessage</code> for this validator will be evaluated as
// a dynamicString - text within <code>\${...}</code> will be evaluated as JS code
// when the message is displayed, with <code>max</code> and <code>min</code> available as
// variables mapped to <code>validator.max</code> and <code>validator.min</code>.
// @value floatPrecision
// Tests whether the value for this field is a floating point number with the
// appropriate number of decimal places - specified in <code>validator.precision</code>
// If the value is of higher precision and <code>validator.roundToPrecision</code>
// is specified, the value will be rounded to the specified number of decimal places
// and validation will pass, otherwise validation will fail.
// @value required
// A non-empty value is required for this field to pass validation.
// @value readOnly
// Change the state/appearance of this field. Desired appearance is specified via
// the <code>fieldAppearance</code> property on the validator object. See
// +link{type:FieldAppearance} type for choices.
// <p>
// If <code>fieldAppearance</code> is not specified, the default is "readOnly".
// @value isUnique
// Returns true if the value for this field is unique.  The uniqueness check is performed across
// the whole DataSource unless you specify property <code>validator.criteriaFields</code> as a
// comma-separated string of field names; in that case, the uniqueness check is done in the
// context of those extra criteria, allowing you to check, for example, whether an employee
// number is unique for the department and location found on the record being validated.
// <p>
// Validators of this type have +link{attr:ValidatorDefinition.requiresServer,requiresServer}
// set to <code>true</code> and do not run on the client.
// <p>See +explorerExample{uniqueCheckValidation}.
// @value hasRelatedRecord
// Returns true if the record implied by a relation exists.  The relation can be
// derived automatically from the +link{attr:DataSourceField.foreignKey} attribute of
// the field being validated, or you can specify it manually via
// <code>validator.relatedDataSource</code> and <code>validator.relatedField</code>.
// <p>
// You can specify at DataSource level that this validator should be automatically
// applied to all fields that specify a +link{attr:DataSourceField.foreignKey,foreignKey} -
// see +link{attr:DataSource.validateRelatedRecords}.
// <p>
// Validators of this type have +link{attr:ValidatorDefinition.requiresServer,requiresServer}
// set to <code>true</code> and do not run on the client.
// <p>
// Note that this validation is generally unnecessary for data coming from a UI.  The
// typical UI uses a +link{class:SelectItem} or +link{class:ComboBoxItem} with an
// +link{FormItem.optionDataSource,optionDataSource} for user entry, such that the user
// can't accidentally enter a related record if that doesn't exist, and a typical SQL
// schema will include constraints that prevent a bad insert if the user attempts to
// circumvent the UI.  The primary purpose of declaring this validation explicitly is
// to provide clear, friendly error messages for use cases such as +link{class:BatchUploader},
// where values aren't individually chosen by the user. See also the example
// +explorerExample{hasRelatedValidation,Related Records}.
// @value serverCustom
// Custom server-side validator that either evaluates the Velocity expression provided in
// +link{Validator.serverCondition,serverCondition} (see +explorerExample{velocityValidation})
// or makes DMI call to +link{Validator.serverObject,serverObject} to evaluate condition
// (see +explorerExample{dmiValidation}).
// <p>
// Validators of this type have +link{attr:ValidatorDefinition.requiresServer,requiresServer}
// set to <code>true</code> and do not run on the client.
//
// @visibility external
//<

// NOTE ON DEFAULT ERROR MESSAGES:
// If the validator doesn't have an error message, set the defaultErrorMessage property on the
// object to distinguish it from an error message set by the user (errorMessage property).
// It's unnecessary to do this on the server because the error message is returned as part of
// the validation result, and the validator parameters aren't modified.

isc.ClassFactory.defineClass("Validator");

isc.Validator.addProperties({

//> @attr validator.serverOnly (boolean : null : IR)
// Indicates this validator runs on the server only.
//
// @serverDS only
// @visibility external
//<

});

// These need to be constants to allow the built-in validators to be i18n'd.  NOTE: it would be
// nice to move these definitions closer to the relevant validator, but note that some
// validators have more than one error message, so we can't adopt a simple convention of naming
// the errors after the validator.
isc.Validator.addClassProperties({
    //>@classAttr   Validator.notABoolean (string : "Must be a true/false value" : [IRA])
    //  Default error message to display when standard <code>isBoolean</code> type validator
    //  returns false.
    // @visibility external
    // @group i18nMessages
    //<
    notABoolean:"Must be a true/false value",
    //>@classAttr   Validator.notAString (string : "Must be a string." : [IRA])
    //  Default error message to display when standard <code>isString</code> type validator
    //  returns false.
    // @visibility external
    // @group i18nMessages
    //<
    notAString:"Must be a string.",
    //>@classAttr   Validator.notAnInteger (string : "Must be a whole number." : [IRA])
    //  Default error message to display when standard <code>isInteger</code> type validator
    //  returns false.
    // @visibility external
    // @group i18nMessages
    //<
    notAnInteger:"Must be a whole number.",
    //>@classAttr   Validator.notADecimal (string : "Must be a valid decimal." : [IRA])
    //  Default error message to display when standard <code>isFloat</code> type validator
    //  returns false.
    // @visibility external
    // @group i18nMessages
    //<
    notADecimal:"Must be a valid decimal.",
    //>@classAttr   Validator.notADate (string : "Must be a date." : [IRA])
    //  Default error message to display when standard <code>isDate</code> type validator
    //  returns false.
    // @visibility external
    // @group i18nMessages
    //<
    notADate:"Must be a date.",


    //>@classAttr   Validator.notATime (string : "Must be a time." : [IRA])
    //  Default error message to display when standard <code>isTime</code> type validator
    //  returns false.
    // @group i18nMessages
    //<
    notATime: "Must be a time.",

    //>@classAttr   Validator.notAnIdentifier (string : "Identifiers must start with a letter, underscore or $ character, and may contain only letters, numbers, underscores or $ characters." : [IRA])
    //  Default error message to display when standard <code>isIdentifier</code> type validator
    //  returns false.
    // @group i18nMessages
    //<
    notAnIdentifier: "Identifiers must start with a letter, underscore or $ character, " +
                    "and may contain only letters, numbers, underscores or $ characters.",

    //>@classAttr   Validator.notARegex (string : "Must be a valid regular expression." : [IRA])
    //  Default error message to display when standard <code>isRegex</code> type validator
    //  returns false.
    // @group i18nMessages
    //<
    notARegex:"Must be a valid regular expression.",

    //>@classAttr   Validator.notAColor (string : "Must be a CSS color identifier." : [IRA])
    //  Default error message to display when standard <code>isColor</code> type validator
    //  returns false.
    // @group i18nMessages
    //<
    notAColor:"Must be a CSS color identifier.",

    //>@classAttr   Validator.mustBeLessThan (string : "Must be no more than \${max}" : [IRA])
    //  Default error message to display when standard <code>integerRange</code> type validator
    //  returns false because the value passed in is greater than the specified maximum.
    // This is a dynamic string - text within <code>\${...}</code> will be evaluated as JS code
    // when the message is displayed, with <code>max</code> and <code>min</code> available as
    // variables mapped to <code>validator.max</code> and <code>validator.min</code>.
    // @visibility external
    // @group i18nMessages
    //<
    mustBeLessThan:"Must be no more than ${max}",

    //>@classAttr   Validator.mustBeGreaterThan (string : "Must be at least \${min}" : [IRA])
    //  Default error message to display when standard <code>integerRange</code> type validator
    //  returns false because the value passed in is less than the specified minimum.
    // <p>This is a dynamic string - text within <code>\${...}</code> will be evaluated as JS code
    // when the message is displayed, with <code>max</code> and <code>min</code> available as
    // variables mapped to <code>validator.max</code> and <code>validator.min</code>.
    // @visibility external
    // @group i18nMessages
    //<
    mustBeGreaterThan:"Must be at least ${min}",

    //>@classAttr   Validator.mustBeLaterThan (string : "Must be later than \${min}" : [IRA])
    // Default error message to display when standard <code>dateRange</code> type validator
    // returns false because the value passed in is greater than the specified maximum date.
    // <p>This is a dynamic string - text within <code>\${...}</code> will be evaluated as JS code
    // when the message is displayed, with <code>max</code> and <code>min</code> available as
    // variables mapped to <code>validator.max</code> and <code>validator.min</code>.
    // @visibility external
    // @group i18nMessages
    //<

    mustBeLaterThan:"Must be later than ${min.toShortDate()}",

    //>@classAttr Validator.mustBeLaterThanTime (string : "Must be later than \${isc.Time.toShortTime(min)}" : [IRA])
    // Default error message to display when standard <code>timeRange</code> type validator
    // returns false because the time-portion of the date-value passed in is less than the
    // specified minimum time.
    // <p> This is a dynamic string - text within <code>\${...}</code> will be evaluated as JS code
    // when the message is displayed, with <code>max</code> and <code>min</code> available as
    // variables mapped to <code>validator.max</code> and <code>validator.min</code>.
    // @visibility external
    // @group i18nMessages
    //<
    mustBeLaterThanTime:"Must be later than ${isc.Time.toShortTime(min)}",

    //>@classAttr   Validator.mustBeEarlierThan (string : "Must be earlier than \${max}" : [IRA])
    //  Default error message to display when standard <code>dateRange</code> type validator
    //  returns false because the value passed in is less than the specified maximum date.
    // This is a dynamic string - text within <code>\${...}</code> will be evaluated as JS code
    // when the message is displayed, with <code>max</code> and <code>min</code> available as
    // variables mapped to <code>validator.max</code> and <code>validator.min</code>.
    // @visibility external
    // @group i18nMessages
    //<
    mustBeEarlierThan:"Must be earlier than ${max.toShortDate()}",

    //>@classAttr Validator.mustBeEarlierThanTime (string : "Must be earlier than \${isc.Time.toShortTime(max)}" : [IRA])
    // Default error message to display when standard <code>timeRange</code> type validator
    // returns false because the time-portion of the date-value passed in is greater than the
    // specified minimum time.
    // <p> This is a dynamic string - text within <code>\${...}</code> will be evaluated as JS code
    // when the message is displayed, with <code>max</code> and <code>min</code> available as
    // variables mapped to <code>validator.max</code> and <code>validator.min</code>.
    // @visibility external
    // @group i18nMessages
    //<
    mustBeEarlierThanTime:"Must be earlier than ${isc.Time.toShortTime(max)}",

    //> @classAttr Validator.mustBeShorterThan (string : "Must be no more than \${max} characters" : IRA)
    // Default error message to display when standard <code>lengthRange</code> type validator
    // returns false because the value passed in has more than <code>validator.max</code> characters.
    // This is a dynamic string - text within <code>\${...}</code> will be evaluated as JS code
    // when the message is displayed, with <code>max</code> and <code>min</code> available as
    // variables mapped to <code>validator.max</code> and <code>validator.min</code>.
    // @visibility external
    // @group i18nMessages
    //<
    mustBeShorterThan:"Must be no more than ${max} characters",

    //> @classAttr Validator.mustBeLongerThan (string : "Must be at least \${min} characters" : IRA)
    // Default error message to display when standard <code>lengthRange</code> type validator
    // returns false because the value passed in has fewer than <code>validator.min</code> characters.
    // This is a dynamic string - text within <code>\${...}</code> will be evaluated as JS code
    // when the message is displayed, with <code>max</code> and <code>min</code> available as
    // variables mapped to <code>validator.max</code> and <code>validator.min</code>.
    // @visibility external
    // @group i18nMessages
    //<
    mustBeLongerThan:"Must be at least ${min} characters",

    //>@classAttr   Validator.mustBeExactLength (string : "Must be exactly \${max} characters" : [IRA])
    // Default error message to display when standard <code>lengthRange</code> type validator
    // has <code>validator.max</code> and <code>validator.min</code> set to the same value,
    // and returns false because the value passed is not the same length as these limits.<br>
    // This is a dynamic string - text within <code>\${...}</code> will be evaluated as JS code
    // when the message is displayed, with <code>max</code> and <code>min</code> available as
    // variables mapped to <code>validator.max</code> and <code>validator.min</code>.
    // @visibility external
    // @group i18nMessages
    //<
    mustBeExactLength:"Must be exactly ${max} characters",


    //>@classAttr   Validator.notAMeasure (string : 'Must be a whole number, percentage, "*" or "auto"' : [IRA])
    //  Default error message to display when standard <code>isMeasure</code> type validator
    //  returns false.
    // @group i18nMessages
    //<
    notAMeasure:'Must be a whole number, percentage, "*" or "auto"',

    //>@classAttr   Validator.requiredField (string : 'Field is required' : [IRA])
    // Default error message to display when validation fails for a field marked as required
    // or with a standard <code>required</code> type validator.
    // The message is also displayed for a field with a standard <code>requiredIf</code> type
    // validator whose condition evaluates to true, because the field has no value.
    // @visibility external
    // @group i18nMessages
    //<
    requiredField:"Field is required",

    //>@classAttr   Validator.notOneOf (string : 'Not a valid option' : [IRA])
    // Default error message to display when standard <code>isOneOf</code> type validator
    // is not passed.
    // @visibility external
    // @group i18nMessages
    //<
    notOneOf:"Not a valid option",

    //>@classAttr   Validator.notAFunction (string : 'Must be a function.' : [IRA])
    //  Default error message to display when standard <code>isFunction</code> type validator
    //  returns false.
    // @group i18nMessages
    //<
    notAFunction:'Must be a function.',


    _$true : "true",
    _$false : "false",
    _$dot:".",

    //> @type FieldAppearance
    READONLY:"readOnly",   // @value isc.Validator.READONLY Show in read-only appearance
    HIDDEN:"hidden",       // @value isc.Validator.HIDDEN   Hide field
    DISABLED:"disabled",   // @value isc.Validator.DISABLED Disable field
    // @visibility external
    //<

    _validatorDefinitions : {


        // isType validators
        // ------------------------------------------------------------------------------------

        // Validation will fail if this field is non-empty and has a non-boolean value.
        isBoolean: {
            type:"isBoolean",
            title:"Value is boolean",
            valueType:"none",
            dataType:"none",
            condition : function (item, validator, value) {
                // skip empty fields
                if (value == null || isc.is.emptyString(value)) return true;
                if (isc.isA.Boolean(value)) return true;

                if (!validator.errorMessage) {
                    validator.defaultErrorMessage = isc.Validator.notABoolean;
                }

                if (isc.isA.String(value)) {
                    var Validator = isc.Validator;
                    validator.resultingValue = (value == Validator._$true);
                    // "true" and "false" is the valid String representation of a boolean
                    return (value == Validator._$true || value == Validator._$false);
                } else if (isc.isA.Number(value)) {
                    validator.resultingValue = (value != 0);
                    // 0 and 1 is the valid numeric representation of a boolean
                    return (value == 0 || value == 1);
                }
                // anything else is a failure, but we still tell you it's boolean value
                validator.resultingValue = !!value;
                return false;
            }
        },

        // Validation will fail if the value is not a string value.

        isString: {
            type:"isString",
            title:"Value is a string",
            valueType:"none",
            dataType:"none",
            condition : function (item, validator, value) {
                if (value == null || isc.isA.String(value)) return true;
                if (!validator.errorMessage) validator.defaultErrorMessage = isc.Validator.notAString;
                validator.resultingValue = isc.iscToLocaleString(value);
                return true;
            }
        },

        // Tests whether the value for this field is a whole number.  If
        // validator.convertToInteger is true, float values will be converted
        // into integers and validation will succeed.
        isInteger: {
            type:"isInteger",
            title:"Value is a whole number",
            valueType:"none",
            dataType:"none",
            condition : function (item, validator, value) {
                // skip empty fields
                if (value == null || isc.is.emptyString(value)) return true;
                if (!validator.errorMessage) validator.defaultErrorMessage = isc.Validator.notAnInteger;

                // if the value can't be resolved to a number, return false
                if (isNaN(value)) return false;    //Not a number or a string that resolves to a number.

                // Note: this routine will be subject to JavaScript's rounding errors for extremely
                // large numbers (16+ digits)
                var intValue = parseInt(value,10),
                    isInteger = (value == intValue);

                if (validator.convertToInteger) {

                    // Parse as float and round instead of parseInt() because parseInt() is
                    // basically Math.floor().  We want 1.5 to become 2, etc.
                    var floatValue = parseFloat(value),
                        intValue = Math.round(floatValue);

                    // reset suggested value (no change if already an integer)
                    validator.resultingValue = intValue;

                    // return true - if we're doing the conversion allow validation to succeed
                    return true;
                } else {
                    // If we were passed an integer, still update the resulting value - this
                    // will ensure that 1.0 is stored as just 1.
                    if (isInteger) {
                        validator.resultingValue = intValue;
                        return true;
                    } else return false;
                }
            }
        },

        // Tests whether the value for this field is a valid floating point number.
        isFloat: {
            type:"isFloat",
            title:"Value is a floating point number",
            valueType:"none",
            dataType:"none",
            condition : function (item, validator, value) {
                if (value == null || isc.is.emptyString(value)) return true;
                if (!validator.errorMessage) validator.defaultErrorMessage = isc.Validator.notADecimal;

                // is the value a valid float?
                var floatValue;
                // treat "." as zero - this ensures that if the user is typing ".3", and we're
                // validating on change, the '.' doesn't kill editing
                if (value == isc.Validator._$dot) {
                    floatValue = "0.";
                } else {
                    floatValue = parseFloat(value);
                    if (isNaN(floatValue) || floatValue != value) return false;
                }
                validator.resultingValue = floatValue;

                return true;
            }
        },


        isDate: {
            type:"isDate",
            title:"Value is a date",
            valueType:"none",
            dataType:"none",
            condition : function (item, validator, value) {
                if (value == null || isc.is.emptyString(value) || isc.isA.Date(value)) return true;
                if (!validator.errorMessage) validator.defaultErrorMessage = isc.Validator.notADate;

                var dateValue = isc.Validator._acceptExcelFormats ? Date.parseInput(value) :
                                                                    Date.parseSchemaDate(value);
                // an "invalid date" will return true from isNaN()
                if (dateValue == null || isNaN(dateValue.getTime())) return false;

                validator.resultingValue = dateValue;
                return true;
            }
        },


        isTime: {
            type:"isTime",
            title:"Value is a logical Time value",
            valueType:"none",
            dataType:"none",
            condition : function (item, validator, value) {
                if (value == null || isc.is.emptyString(value) || isc.isA.Date(value)) return true;
                if (!validator.errorMessage) validator.defaultErrorMessage = isc.Validator.notATime;

                var dateValue = isc.Time.parseInput(value, true);
                // support being passed a full datetime string as well
                if (dateValue == null) {
                    dateValue = Date.parseSchemaDate(value);
                }
                if (dateValue != null) {
                    validator.resultingValue = dateValue;
                    return true;
                }
                return false;
            }
        },

        // This is used for validating ISC components defined in XML
        // Leave as un-exposed for now.
        isIdentifier: {
            type:"isIdentifier",
            valueType:"none",
            dataType:"none",
            condition : function (item, validator, value) {
                if (value == null || isc.is.emptyString(value)) return true;
                if (!validator.errorMessage) {
                    validator.defaultErrorMessage = isc.Validator.notAnIdentifier;
                }
                return value.match(/^[a-zA-Z_\$][\w\$]*$/) != null;
            }
        },

        // This is used for validating ISC components defined in XML
        // Leave as un-exposed for now.
        isRegexp: {
            type:"isRegexp",
            valueType:"none",
            dataType:"none", // This is really string or Regexp
            condition : function (item, validator, value) {
                if (value == null || isc.is.emptyString(value)) return true;
                if (!validator.errorMessage) validator.defaultErrorMessage = isc.Validator.notARegex;

                if (typeof value == 'object' && value.constructor == RegExp) return true;


                if (isc.Browser.isDOM) {
                    if (!isc.Validator._isRegexp) {
                        isc.Validator._isRegexp = new Function("value",
                            "try{var regex=new RegExp(value)}catch(e){return false}return true");
                    }
                    return isc.Validator._isRegexp(value);
                } else {
                    var regex = new RegExp(value);
                    return true;
                }
            }
        },

        // Tests whether the value for this field is a valid expression or function; if it is
        // valid, creates a StringMethod object with the value, and set the resultingValue to
        // the StringMethod
        isFunction: {
            type:"isFunction",
            valueType:"none",
            dataType:"none",
            condition :  function (item, validator, value) {
                if (value == null || isc.is.emptyString(value) || value == isc.Class.NO_OP ||
                    isc.isA.StringMethod(value))
                {
                    return true;
                }
                if (!validator.errorMessage) validator.defaultErrorMessage = isc.Validator.notAFunction;

                try {
                    isc.Func.expressionToFunction("", value);
                } catch (e) {
                    return false;
                }

                // catch the case where we have a function derived from an Action
                // in this case pick up the original action again.
                if (value.iscAction) value = value.iscAction;
                validator.resultingValue = isc.StringMethod.create({value:value});
                return true;
            }
        },

        // isColor() - used for validating ISC components defined in XML
        // Leave as un-exposed for now.
        isColor: {
            type:"isColor",
            valueType:"none",
            dataType:"text",
            condition : function (item, validator, value) {

                if (!validator.errorMessage) validator.defaultErrorMessage = isc.Validator.notAColor;

                // empty string/undefined/null is generally treated as the transparent color, so allow
                // that.  If an actual entry is required, you can specify the 'required' validator
                if (!value) return true;

                return isc.isA.color(value);
            }
        },

        // This is used for validating ISC components defined in XML
        // Leave as un-exposed for now.
        isMeasure: {
            type:"isMeasure",
            valueType:"none",
            dataType:["integer", "string"],
            condition : function (item, validator, value) {
                if (value == null || isc.is.emptyString(value) || value == "*") return true;
                if (!validator.errorMessage) validator.defaultErrorMessage = isc.Validator.notAMeasure;


                // if it ends in percent, check if it's all digits
                if (isc.isA.String(value) && value.charAt(value.length - 1) == '%') {
                    value = value.slice(0, -1);
                    // Not using parseInt here because parseInt returns a valid number if the
                    // string is prefixed with a valid number
                    return value.match(/\d+\.?\d*/) != null;
                }
                return isc.Validator.processValidator(item, validator, value, "integerOrAuto");
            }
        },

        // This is used for validating ISC components defined in XML
        // Leave as un-exposed for now.
        integerOrAuto: {
            type:"integerOrAuto",
            valueType:"none",
            dataType:["integer", "string"],
            condition : function (item, validator, value) {
                if (value == null || isc.is.emptyString(value) ||
                    (isc.isA.String(value) && value.toLowerCase() == "auto")) return true;
                return isc.Validator.processValidator(item, validator, value, "isInteger");
            }
        },

        // This is used for validating ISC components defined in XML
        // Leave as un-exposed for now.
        integerOrIdentifier: {
            type:"integerOrIdentifier",
            valueType:"none",
            dataType:["integer", "string"],
            condition : function (item, validator, value) {
                if (value == null || isc.is.emptyString(value)) return true;

                if (value.match(/^[a-zA-Z_\$][\w\$]*$/) != null) return true;

                return isc.Validator.processValidator(item, validator, value, "isInteger");
            }
        },


        // Integer validators
        // ------------------------------------------------------------------------------------

        // Tests whether the value for this field is a whole number within the range
        // specified.  The max and min properties on the validator
        // are used to determine the acceptable range.
        integerRange: {
            type:"integerRange",
            title:"Value is an integer within the specified range",

            valueType:"valueRange",
            dataType:"integer",    // You could apply an integerRange to a float type field,
                                   // but it'd be less common than just using a floatRange
            rangeStartAttribute:"min",
            rangeEndAttribute:"max",

            condition : function (item, validator, value) {
                // If we're passed a non numeric value, just return without adding an error.
                // This is appropriate since the type of the field will probably be specified as
                // "integer" meaning that the built in integer validator will also be present on the
                // field.
                var passedVal = value;
                if (!isc.isA.String(value)) value = parseInt(value,10);
                if (isNaN(value) || value != passedVal) return true;

                // Allow dynamic error messages to be eval'd, with pointers to min and max values
                validator.dynamicErrorMessageArguments = {validator:validator,
                                                          max:validator.max,
                                                          min:validator.min}


                // if a maximum was specified, return false if we're greater than the max
                if (isc.isA.Number(validator.max) &&
                    // exclusive means it's an error is value is exactly max
                    ((!validator.exclusive && value > validator.max) ||
                     (validator.exclusive && value >= validator.max)))
                {
                    if (!validator.errorMessage) {
                        validator.defaultErrorMessage = isc.Validator.mustBeLessThan
                    }
                    return false;
                }
                // if a minumum was specified, return false if we're less than the min
                if (isc.isA.Number(validator.min) &&
                    // exclusive means it's an error is value is exactly min
                    ((!validator.exclusive && value < validator.min) ||
                     (validator.exclusive && value <= validator.min)))
                {
                    if (!validator.errorMessage) {
                        validator.defaultErrorMessage = isc.Validator.mustBeGreaterThan;
                    }
                    return false;
                }
                return true;
            }
        },

        // String validators
        // ------------------------------------------------------------------------------------

        // This validator type applies to string values only.  If the value is a string value
        // validation will fail if the strings length falls outside the range specified by
        // validator.max and validator.min.
        // Note that non-string values will always pass validation by this validator type.<br>
        // Note that the errorMessage for this validator will be evaluated as
        // a dynamicString - text within ${...} will be evaluated as JS code
        // when the message is displayed, with max and min available as
        // variables mapped to validator.max and validator.min.
        lengthRange: {
            type:"lengthRange",
            title:"Value is a string whose length falls within the specified range",

            valueType:"valueRange",
            dataType:"text",
            rangeStartAttribute:"min",
            rangeEndAttribute:"max",

            condition : function (item, validator, value) {
                // skip empty fields
                if (value == null || isc.is.emptyString(value)) return true;

                // if value null/undefined, or isn't a string, return true
                if (!isc.isA.String(value)) return true;

                // Allow dynamic error messages to be eval'd, with pointers to min and max values
                validator.dynamicErrorMessageArguments = {validator:validator,
                                                          max:validator.max,
                                                          min:validator.min}

                // get the length of the value
                var length = value.length,
                    maxNumber = validator.max != null ? parseInt(validator.max,10) : null,
                    minNumber = validator.min != null ? parseInt(validator.min,10) : null;

                if (!isc.isA.Number(maxNumber)) maxNumber = null;
                if (!isc.isA.Number(minNumber)) minNumber = null;

                // if a maximum was specified, return false if length is greater than the max
                if (maxNumber != null && length > maxNumber) {
                    validator.defaultErrorMessage =
                        (maxNumber == minNumber ? isc.Validator.mustBeExactLength
                                                : isc.Validator.mustBeShorterThan);
                    return false;
                }

                // if a minumum was specified, return false if length is less than the min
                if (minNumber != null && length < minNumber) {
                    validator.defaultErrorMessage =
                        (maxNumber == minNumber ? isc.Validator.mustBeExactLength
                                                : isc.Validator.mustBeLongerThan);
                    return false;
                }
                return true;
            }
        },

        // Determine whether a string value contains some substring specified via
        // validator.substring.
        contains:{
            type:"contains",
            title:"Value contains the specified substring",
            valueType:"fieldType",
            dataType:"text",
            valueAttribute:"substring",
            condition : function (item, validator, value) {
                // skip empty fields
                if (value == null || isc.is.emptyString(value)) return true;
                if (!isc.isA.String(value)) value = isc.iscToLocaleString(value);
                return value.indexOf(validator.substring) > -1;
            }
        },

        // Determine whether a string value does not contain some substring specified via
        // validator.substring.
        doesntContain: {
            type:"doesntContain",
            title:"Value does not contain the specified substring",
            valueType:"fieldType",
            dataType:"text",
            valueAttribute:"substring",
            condition : function (item, validator, value) {
                // skip empty fields
                if (value == null || isc.is.emptyString(value)) return true;
                if (!isc.isA.String(value)) value = isc.iscToLocaleString(value);
                return value.indexOf(validator.substring) == -1;
            }
        },

        // Determine whether a string value contains some substring multiple times.
        // The substring to check for is specified via validator.substring.
        // The <code>validator.operator</code> property allows you to specify how to test
        // the number of substring occurrences. Valid values for this property are
        // ==, !=, <, <=, >, >=.
        // The number of matches to check for is specified via validator.count.
        substringCount: {
            type:"substringCount",
            title:"Value contains a specified substring multiple times",
            valueType:"custom",
            dataType:"text",
            // This will show a custom edit-item when rendered in a RuleEditor, allowing the
            // user to specify substring, count and operator.
            // Defined in RuleEditor.js
            // Implemented as a simple dynamicForm embedded in a canvasItem, so we can
            // just 'getValues' to pick up rule attributes


            editorType:"SubstringCountEditor",
            getAttributesFromEditor:function (fieldName, item) {
                var form = item.canvas;
                return form.getValues();
            },
            setEditorAttributes:function(fieldName, item, attributes) {
                var form = item.canvas;
                if (attributes == null) {
                    form.clearValues();
                    return;
                }
                form.setValue("substring", attributes.substring);
                form.setValue("count", attributes.count);
                form.setValue("operator", attributes.operator);
            },
            condition : function (item, validator, value) {
                // skip empty fields
                if (value == null || isc.is.emptyString(value)) return true;
                var substring = validator.substring;
                // get the number of times the value contains the substring and put it into "matchCount"
                for (var index = 0,    matchCount = 0; index < value.length; index++) {
                    index = value.indexOf(substring,index);
                    if (index > -1) matchCount++;
                    else break;
                }

                var operator = validator.operator,
                    count = validator.count
                ;
                if (!operator) operator = "==";
                if (!count) count = 0;

                switch (operator) {
                    case "==" : return matchCount == count;
                    case "!=" : return matchCount != count;
                    case "<" : return matchCount < count;
                    case "<=" : return matchCount <= count;
                    case ">" : return matchCount > count;
                    case ">=" : return matchCount >= count;
                }

                // otherwise return false
                return false;
            }
        },

        // regexp type validators will determine whether the value specified
        // matches a given regular expression.  The expression should be specified on the
        // validator object as the expression property.
        regexp: {
            type:"regexp",
            title:"Value matches a regular expression",
            valueType:"fieldType",
            dataType:"text",
            valueAttribute:"expression",
            condition : function (item, validator, value) {
                // skip empty fields
                if (value == null || isc.is.emptyString(value)) return true;

                // get the expression to validate and normalize it to a regExp value
                var expression = validator.expression;
                if (isc.isA.String(expression)) {
                    expression = new RegExp(expression);
                }

                // return whether or not the expression matches the value
                return expression.test(value);
            }
        },

        // Validate against a regular expression mask, specified as validator.mask.
        // If validation is successful a transformation can also be specified via the
        // validator.transformTo property. This should be set to a string in the
        // standard format for string replacement via the native JavaScript replace()
        // method.
        mask: {
            type:"mask",
            title:"Value matches a regular expression mask",
            valueType:"custom",
            dataType:"text",
            editorType:"MaskRuleEditor",
            getAttributesFromEditor:function (fieldName, item) {
                var form = item.canvas;
                return form.getValues();
            },
            setEditorAttributes:function(fieldName, item, attributes) {
                var form = item.canvas;
                if (attributes == null) {
                    form.clearValues();
                    return;
                }
                form.setValue("mask", attributes.mask);
                form.setValue("transformTo", attributes.transformTo);
            },

            condition : function (item, validator, value) {
                // skip empty fields
                if (value == null) return true;

                // cater for earlier validators converting the value
                if (!isc.isA.String(value) && isc.isA.Function(value.toString))
                    value = value.toString();

                if (isc.is.emptyString(value)) return true;
                var mask = validator.mask;

                // and convert to a regular expression if it's a string
                if (isc.isA.String(mask)) mask = validator.mask = new RegExp(mask);

                // check the value against the mask
                if (!mask.test(value)) {
                    return false;
                } else {
                    // if it passes the test

                    // if they specify a transformTo, transform the item and set the
                    //    resultingValue to the transformed value
                    if (validator.transformTo) {
                        validator.resultingValue = value.replace(mask, validator.transformTo);
                    }
                }

                // return that the mask was validated successfully
                return true;
            }
        },

        // Dates
        // ---------------------------------------------------------------------------------------
        // Tests whether the value for a date field is within the range specified.
        // Range is inclusive, and is specified via validator.min and
        // validator.max, which should be specified in "http://www.w3.org/TR/xmlschema-2/#dateTime".
        // date format or as a live JavaScript Date object (for client-only validators only).
        //
        // Note that the errorMessage for this validator will be evaluated as
        // a dynamicString - text within ${...} will be evaluated as JS code
        // when the message is displayed, with max and min available as
        // variables mapped to validator.max and validator.min.
        dateRange:{
            type:"dateRange",
            title:"Value is a date within the specified range",
            valueType:"valueRange",
            dataType:"date",
            rangeStartAttribute:"min",
            rangeEndAttribute:"max",
            // We don't want to pick the the default from
            // SearchForm for dates (which is a DateRangeItem)
            editorType:"RelativeDateItem",

            condition : function (item, validator, value) {
                if (value == null || isc.is.emptyString(value)) return true;

                if (!isc.isA.Date(value)) return false;

                var min = validator.min, max = validator.max;

                // make a one-time attempt to parse min and max to dates.  Handy when specifying
                // min and max dates in XML.
                if (min != null && !isc.isA.Date(min)) min = validator.min = Date.parseSchemaDate(min);
                if (max != null && !isc.isA.Date(max)) max = validator.max = Date.parseSchemaDate(max);

                // Allow dynamic error messages to be eval'd, with pointers to min and max values
                validator.dynamicErrorMessageArguments = {validator:validator,
                                                          max:max,
                                                          min:min}
                if (isc.isA.Date(min) &&
                    // exclusive means it's an error is value is exactly min
                    ((!validator.exclusive && value.getTime() < min.getTime()) ||
                     (validator.exclusive && value.getTime() <= min.getTime())))
                {
                    if (!validator.errorMessage) {
                        validator.defaultErrorMessage = isc.Validator.mustBeLaterThan
                    }
                    return false;
                }
                if (isc.isA.Date(max) &&
                    // exclusive means it's an error is value is exactly max
                    ((!validator.exclusive && value.getTime() > max.getTime()) ||
                     (validator.exclusive && value.getTime() >= max.getTime())))
                {
                    if (!validator.errorMessage) {
                        validator.defaultErrorMessage = isc.Validator.mustBeEarlierThan;
                    }
                    return false;
                }
                return true;
            }
        },

        // Times
        // ---------------------------------------------------------------------------------------
        // Tests whether the time-value for a date field is within the range specified.
        // Range is inclusive, and is specified via validator.min and
        // validator.max, which should be specified in "http://www.w3.org/TR/xmlschema-2/#dateTime".
        // date format or as a live JavaScript Date object (for client-only validators only).
        //
        // Note that the errorMessage for this validator will be evaluated as
        // a dynamicString - text within ${...} will be evaluated as JS code
        // when the message is displayed, with max and min available as
        // variables mapped to validator.max and validator.min.
        timeRange: {
            type:"timeRange",
            description:"Value is a logical time value within the specified range",
            valueType:"valueRange",
            dataType:"time",
            rangeStartAttribute:"min",
            rangeEndAttribute:"max",

            condition : function (item, validator, value) {
                if (value == null || isc.is.emptyString(value)) return true;

                if (!isc.isA.Date(value)) return false;

                var min = validator.min, max = validator.max;

                // make a one-time attempt to parse min and max to dates.  Handy when specifying
                // min and max dates in XML.
                if (min != null && !isc.isA.Date(min)) {
                    if (isc.isA.String(min) && min.contains(":")) {
                        // support a time-string like "10:30"
                        var parts = min.split(":"),
                            hours = parts[0] || 0,
                            minutes = parts[1] || 0,
                            seconds = parts[2] || 0,
                            milliseconds = parts[3] || 0
                        ;
                        min = validator.min = new Date(0,0,0, hours, minutes, seconds, milliseconds);
                    } else {
                        min = validator.min = Date.parseSchemaDate(min);
                    }
                }
                if (max != null && !isc.isA.Date(max)) {
                    if (isc.isA.String(max) && max.contains(":")) {
                        // support a time-string like "10:30"
                        var parts = max.split(":"),
                            hours = parts[0] || 0,
                            minutes = parts[1] || 0,
                            seconds = parts[2] || 0,
                            milliseconds = parts[3] || 0
                        ;
                        max = validator.max = new Date(0,0,0, hours, minutes, seconds, milliseconds);
                    } else {
                        max = validator.max = Date.parseSchemaDate(max);
                    }
                }

                // Allow dynamic error messages to be eval'd, with pointers to min and max values
                validator.dynamicErrorMessageArguments = {validator:validator,
                                                          max: max,
                                                          min: min};

                min.setFullYear(value.getFullYear());
                min.setMonth(value.getMonth());
                min.setDate(value.getDate());

                max.setFullYear(value.getFullYear());
                max.setMonth(value.getMonth());
                max.setDate(value.getDate());

                if (isc.isA.Date(min) &&
                    // exclusive means it's an error if value is exactly min
                    ((!validator.exclusive && value < min) ||
                     (validator.exclusive && value <= min)))
                {
                    if (!validator.errorMessage) {
                        validator.defaultErrorMessage = isc.Validator.mustBeLaterThanTime;
                    }
                    return false;
                }
                if (isc.isA.Date(max) &&
                    // exclusive means it's an error is value is exactly max
                    ((!validator.exclusive && value > max) ||
                     (validator.exclusive && value >= max)))
                {
                    if (!validator.errorMessage) {
                        validator.defaultErrorMessage = isc.Validator.mustBeEarlierThanTime;
                    }
                    return false;
                }
                return true;
            }
        },

        // Floats
        // ---------------------------------------------------------------------------------------
        // Validate a variable as a valid floating point value, within a value range.
        // Range is specified via validator.min and validator.max.
        // Also checks precision, specified as number of decimal places in
        // validator.precision. If validator.roundToPrecision is set,
        // a value that doesn't match the specified number of decimal places will be rounded
        // to the nearest value that does.
        //
        // backcompat only, replaced by floatRange and floatPrecision
        floatLimit: {
            type:"floatLimit",

            valueType:"custom",
            dataType:"float",
            condition : function (item, validator, value) {
                var roundedValue;

                // Check precision before max/min as rounding may push it over the edge.
                if (validator.precision != null) {
                    //>!BackCompat 2005.02.03
                    // Old functionality always had no 'roundToPrecision' param, but always
                    // rounded and passed.
                    if (validator.roundToPrecision == null) validator.roundToPrecision = true;
                    //<!BackCompat
                    if (!isc.Validator.processValidator(item, validator, value, "floatPrecision"))
                        return false;
                    // from now on test with the rounded version.
                    if (validator.resultingValue != null)
                        value = roundedValue = validator.resultingValue;
                }
                if (validator.min != null || validator.max != null) {
                    if (!isc.Validator.processValidator(item, validator, value, "floatRange")) {
                        return false
                    } else {
                        // the second processValidator call will have cleared out resultingValue
                        // which may have come from the precision validator.
                        if (roundedValue != null && validator.resultingValue == null &&
                            validator.roundToPrecision)
                            validator.resultingValue = roundedValue;
                    }
                }
                return true;
            }
        },

        // Tests whether the value for this field is a floating point number within the range
        // specified.  The max and min properties on the validator
        // are used to determine the acceptable range.
        // Note that the errorMessage for this validator will be evaluated as
        // a dynamicString - text within ${...} will be evaluated as JS code
        // when the message is displayed, with max and min available as
        // variables mapped to validator.max and validator.min.
        floatRange: {
            type:"floatRange",
            title:"Value is a floating point number within the specified range",
            // Editor implemented in RuleEditor.js - simple dynamicForm based canvasItem
            // with fields for each attribute.
            valueType:"custom",
            dataType:"float",
            editorType:"FloatRangeEditor",
            getAttributesFromEditor:function (fieldName, item) {
                var form = item.canvas;
                return form.getValues();
            },
            setEditorAttributes:function(fieldName, item, attributes) {
                var form = item.canvas;
                if (attributes == null) {
                    form.clearValues();
                    return;
                }
                form.setValue("min", attributes.min);
                form.setValue("max", attributes.max);
                form.setValue("exclusive", !!attributes.exclusive);
            },

            condition : function (item, validator, value) {
                // skip empty fields
                if (value == null || isc.is.emptyString(value)) return true;
                // If we're passed a non numeric value, just return without adding an error.
                // This is appropriate since the type of the field will probably be specified as
                // "float" meaning that the built in float validator will also be present on the
                // field.

                var floatValue = value;
                if (!isc.isA.String(value)) floatValue = parseFloat(floatValue);
                if (isNaN(floatValue) || floatValue != value) return true;


                // Allow dynamic error messages to be eval'd, with pointers to min and max values
                validator.dynamicErrorMessageArguments = {validator:validator,
                                                          max:validator.max,
                                                          min:validator.min}


                var max,
                    min;
                if (validator.max != null) {
                    max = isc.isA.Number(validator.max) ? validator.max : parseFloat(validator.max);
                }
                if (validator.min != null) {
                    min = isc.isA.Number(validator.min) ? validator.min : parseFloat(validator.min);
                }

                // is the value less than the max allowable? (if specified)
                if (isc.isA.Number(max) &&
                    // exclusive means it's an error is value is exactly max
                    ((!validator.exclusive && floatValue > max) ||
                     (validator.exclusive && floatValue >= max)))
                {
                    if (!validator.errorMessage) {
                        validator.defaultErrorMessage = isc.Validator.mustBeLessThan;
                    }
                    return false;
                }

                // is the value greater than the min allowable? (if specified)
                if (isc.isA.Number(min) &&
                    // exclusive means it's an error is value is exactly min
                    ((!validator.exclusive && floatValue < min) ||
                     (validator.exclusive && floatValue <= min)))
                {
                    if (!validator.errorMessage) {
                        validator.defaultErrorMessage = isc.Validator.mustBeGreaterThan;
                    }
                    return false;
                }
                return true;
            }
        },

        // Tests whether the value for this field is a floating point number with the
        // appropriate number of decimal places - specified in validator.precision
        // If the value is of higher precision, if validator.roundToPrecision
        // is specified, the value will be rounded to the specified number of decimal places
        // and validation will pass, otherwise validation will fail.
        floatPrecision: {
            type:"floatPrecision",
            title:"Value is a floating point number specified to the appropriate precision",
            // Editor implemented in RuleEditor.js - simple dynamicForm based canvasItem
            // with fields for each attribute.
            valueType:"custom",
            dataType:"float",
            editorType:"FloatPrecisionEditor",
            getAttributesFromEditor:function (fieldName, item) {
                var form = item.canvas;
                return form.getValues();
            },
            setEditorAttributes:function(fieldName, item, attributes) {
                var form = item.canvas;
                if (attributes == null) {
                    form.clearValues();
                    return;
                }
                form.setValue("precision", attributes.precision);
                form.setValue("roundToPrecision", attributes.roundToPrecision);
            },


            condition : function (item, validator, value) {

                // skip empty fields
                if (value == null || isc.is.emptyString(value)) return true;

                var floatValue = parseFloat(value);
                if (isNaN(floatValue) || floatValue != value) return false;

                // if validator.precision is defined, round to that precision.
                if (isc.isA.Number(validator.precision)) {
                    var multiplier = Math.pow(10, validator.precision);
                    var roundedValue = (Math.round(floatValue * multiplier))/multiplier;
                    if (validator.roundToPrecision) {
                        validator.resultingValue = roundedValue;
                        return true;
                    } else {
                        return (floatValue == roundedValue);
                    }
                }
            }
        },

        // Generic (typeless) validators
        // ---------------------------------------------------------------------------------------

        // RequiredIf type validators should be specified with an expression
        // property set to a stringMethod which takes three parameters:
        //   item - the DynamicForm item on which the error occurred (may be null)
        //   validator - a pointer to the validator object
        //   value - the value of the field in question
        // When validation is performed, the expression will be evaluated (or executed) - if it
        // returns true, the field will be treated as a required field, so validation
        // will fail if the field has no value.
        requiredIf: {
            type: "requiredIf",
            title: "Conditionally required field",

            valueType:"fieldType",
            dataType:"none",
            valueAttribute:"expression",
            editorType:"TextAreaItem",

            condition : function (item, validator, value, record) {
                // CALLBACK API:  available variables:  "item,validator,value"
                // Convert a string callback to a function
                if (validator.expression != null && !isc.isA.Function(validator.expression)) {
                    isc.Func.replaceWithMethod(validator, "expression", "item,validator,value,record");
                }

                var required = validator.expression(item,validator,value,record);

                // Default to displaying the 'requiredField' error message.
                if (validator.errorMessage == null)
                    validator.errorMessage = isc.Validator.requiredField;

                // if the item is not required, or isn't empty, return true
                return  !required || (value != null && !isc.is.emptyString(value));
            }
        },

        // Tests whether the value for this field matches any value from an arbitrary
        // list of acceptable values.  The set of acceptable values is specified via
        // the list property on the validator, which should be set to an array of
        // values. If validator.list is not supplied, the valueMap for the field will be used.
        // If there is no valueMap, not providing validator.list is an error.
        isOneOf: {
            type: "isOneOf",
            title: "Is one of list",
            valueType:"valueSet",
            dataType:"none",
            valueAttribute:"list",

            condition : function (item, validator, value, record) {
                // skip empty fields
                if (value == null || isc.is.emptyString(value)) return true;

                // get the list of items to match against, either declared on this validator
                // or automatically derived from the field's valueMap (item.valueMap)

                var valueMap = validator.list || (item ? (item.getValueMap ? item.getValueMap()
                                                                           : item.valueMap)
                                                       : null),
                valueList = valueMap;
                if (!isc.isAn.Array(valueMap) && isc.isAn.Object(valueMap)) {
                    valueList = isc.getKeys(valueMap);
                }

                if (valueList != null) {
                    // if any item == the value, return true
                    for (var i = 0, length = valueList.length; i < length; i++) {
                        if (valueList[i] == value) return true;
                    }
                //>DEBUG
                } else {
                    isc.Log.logWarn("isOneOf validator specified with no specified list of options " +
                                "or valueMap - validator will always fail. " +
                                "Field definition:" + isc.Log.echo(item), "validation");
                //<DEBUG
                }
                // otherwise, failure return false
                if (!validator.errorMessage) {
                    validator.defaultErrorMessage = isc.Validator.notOneOf;
                }
                return false;
            }
        },
        // A non-empty value is required for this field to pass validation.
        required: {
            type: "required",
            title: "Required field",
            valueType:"none",
            dataType:"none",
            condition : function (item, validator, value, record) {
                // Default to displaying the 'requiredField' error message.
                if (validator.errorMessage == null)
                    validator.errorMessage = isc.Validator.requiredField;

                var valid = (value != null && !isc.is.emptyString(value));
                // for multiple: true items, an empty array is considered a validation failure
                if (valid && item && item.multiple) {
                    if (isc.isAn.Array(value) && value.isEmpty()) valid = false;
                }
                return valid;
            },
            action : function (result, item, validator, record, component) {
                // For a conditional required validator we need to set the
                // item._required flag so field will be drawn with the correct style.
                if (!item.required) {
                    item._required = (result != null);
                }
            }
        },

        // Change the state/appearance of this field. Desired appearance is specified via
        // the fieldAppearance property on the validator object.
        //
        // If fieldAppearance is not specified, the default is "readOnly".
        readOnly: {
            type: "readOnly",

            valueType:"custom",
            dataType:"none",
            // This is actually a simple subclass of SelectItem


            editorType:"ReadOnlyRuleEditor",
            valueAttribute:"fieldAppearance",

            title: "Set field read-only state/appearance",

            // Mark this as a rule, not a validator. No condition, and supports
            // locator.
            isRule:true,
            supportedTargets:["FormItem", "Canvas", "Section"],

            action : function (result, target, validator, record, targetContainer, targetType) {

                if (validator.fieldAppearance == isc.Validator.HIDDEN) {

                    if (result == true) {
                        // Only "Canvas" would be expected to have no "targetContainer".
                        // Just call 'hide' on the target.
                        if (targetContainer == null) {
                            // check for the method is a sanity check only
                            if (target.hide) target.hide();
                        } else {

                            if (targetType == "Section") {
                                targetContainer.hideSection(target);
                            } else if (targetType == "FormItem") {
                                targetContainer.hideItem(target);
                            }

                        }
                    } else {
                        if (targetContainer == null) {
                            if (target.show) target.show();
                        } else {
                            if (targetType == "Section") {
                                targetContainer.showSection(target);
                            } else {
                                targetContainer.showField(target);
                            }
                        }
                    }

                } else if (validator.fieldAppearance == isc.Validator.DISABLED) {
                    if (result == true) {
                        if (targetContainer == null) {
                            target.disable();
                        } else {
                            if (targetType == "Section") {

                                target.disable();
                                if (target.items) {
                                    for (var i = 0; i < target.items.length; i++) {
                                        target.items[i].disable();
                                    }
                                }
                            } else {
                                targetContainer.disableField(target);
                            }
                        }
                    } else {
                        if (targetContainer == null) {
                            target.enable();
                        } else {
                            if (targetType == "Section") {
                                target.enable();
                                if (target.items) {
                                    for (var i = 0; i < target.items.length; i++) {
                                        target.items[i].enable();
                                    }
                                }
                            } else {
                                targetContainer.enableField(target);
                            }
                        }
                    }
                } else {
                    if (targetType != "FormItem") {
                        isc.logWarn("ReadOnly rule with fieldAppearance set to '"
                            + validator.fieldAppearance + "' not supported for specified locator:"
                            + validator.locator);
                    }

                    if (result == true) targetContainer.setFieldCanEdit(target.name, false);
                    else targetContainer.setFieldCanEdit(target.name, true);
                }
            }
        },

        // Tests whether the value for this field matches the value of some other field.
        // The field to compare against is specified via the otherField property
        // on the validator object (should be set to a field name).
        matchesField: {
            type: "matchesField",
            title: "Matches another field value",
            valueType:"fieldName",
            dataType:"none",
            valueAttribute:"otherField",
            condition : function (item, validator, value, record) {
                if (validator.otherField == null) {
                    isc.logWarn("matchesField validator is missing 'otherField' definition. " +
                                "Validator forced false.");
                    return false;
                }

                var otherFieldName;
                if (isc.isA.String(validator.otherField)) {
                    otherFieldName = validator.otherField;
                } else {
                    otherFieldName = validator.otherField.dataPath || validator.otherField.name;
                }


                // Extract value from record for otherField allowing DS.field format
                var otherValue = (isc.DataSource != null ?
                        isc.DataSource.getPathValue(record, otherFieldName, validator.otherField) :
                        record[otherFieldName]);

                // do the values match?
                return (value == otherValue);
            },

            // By default matchesField rules depend on the "otherField" so
            // changes to that field should trigger the rule to re-run along
            // with any changes to the main field.
            getDependentFields : function (rule, triggerEvent) {
                if (!rule.otherField || rule.otherField == "") return rule.fieldName;
                return [ rule.fieldName, rule.otherField ];
            }
        },

        // Returns true if the value for this field is unique for the criteria specified in
        // "criteriaFields", or across the whole DataSource if no criteriaFIelds is specified
        isUnique: {
            type: "isUnique",
            valueType:"none",
            dataType:"none",
            title: "Validate field value is unique on DataSource",
            requiresServer: true
        },

        // Returns true if the record implied by a relation exists.  The relation can be
        // derived automatically from the DataSourceField.foreignKey attribute of
        // the field being validated, or you can specify it manually via
        // validator.relatedDataSource and validator.relatedField.
        //
        // You can specify at DataSource level that this validator should be automatically
        // applied to all fields that specify a DataSourceField.foreignKey -
        // see DataSource.validateRelatedRecords.
        hasRelatedRecord: {
            type: "hasRelatedRecord",
            valueType:"none",
            dataType:"none",
            title: "Validate field value exists on a related DataSource",
            requiresServer: true
        },

        // Evaluates the Velocity expression provided in
        // Validator.serverCondition on the server side.
        serverCustom: {
            type: "serverCustom",
            valueType:"none",
            dataType:"none",
            title: "Validate field value using a custom server expression",
            requiresServer: true
        },

        // ---------------------------------------------
        // Rules: Implemented as special validator types
        // ---------------------------------------------


        //> @object MessageRule
        // A +link{Rule} that causes a message to be shown to a user, but does not prevent saving
        // or cause the data to be considered invalid.
        // @visibility rules
        //<

        //> @attr messageRule.message (String : null : IRW)
        // Message to display for this rule
        // @visibility rules
        //<


        //> @type MessageSeverity
        // @value "warning" message is a warning and should be displayed in a high-visibility style
        //  that indicates it is a warning
        // @value "information" message is informational and should be displayed in a neutral manner
        // @visibility rules
        //<


        //> @attr messageRule.severity (MessageSeverity : "info" : IR)
        // Severity of the message - see +link{type:MessageSeverity}.
        // @visibility rules
        //<

        //> @type MessageDisplayMode
        // @value "form" message is displayed by the form according to the form's standard rules for
        //  displaying validation errors
        // @value "transient" message is displayed as a pop-up which automatically fades after a short
        //  period, without user action
        // @value "dialog" message is displayed in a modal pop-up dialog that the user must dismiss
        //
        // @visibility rules
        //<

        //> @attr messageRule.displayMode (MessageDisplayMode : "dialog" : IR)
        // How should this message be displayed. See +link{messageDisplayMode}
        // @visibility rules
        //<


        //> @attr messageRule.duration (number : null : IR)
        // If +link{messageRule.displayMode} is set to <code>"transient"</code>, this property
        // specifies how long the message should appear for (in milliseconds).
        // @visibility rules
        //<

        message : {
            type:"message",
            title:"displays an informational or warning message",

            valueType:"none",
            dataType:"none",

            isRule:true,
            supportedTargets:["FormItem"],

            action : function (result, item, validator, record, component) {
                var displayMode = validator.displayMode,
                    // default to "message" rather than "errorMessage" for Message type rules.
                    messageAttr = (validator.message == null ? "errorMessage" : "message"),
                    message = isc.Validator.getErrorMessage(validator, messageAttr);

                if (displayMode == "form") {

                    if (!component.addFieldErrors) {
                        return;
                    }
                    if (result == true) {
                        component.addFieldErrors(item.name, message, true);

                    } else {
                        component.setFieldErrors(item.name, null, true);
                    }
                } else if (displayMode == "transient") {

                    if (result == true) {
                        var action = function () {
                            if (component && component.hasFocus) component.blur();
                            isc.showFadingPrompt(message, validator.duration);
                        };
                        isc.Page.setEvent("idle", action, isc.Page.FIRE_ONCE);
                    }
                } else {
                    // displayMode is "dialog"
                    var severity = validator.severity;
                    if (result == true) {
                        var action = function () {

                            if (component && component.hasFocus) component.blur();
                            if (severity == "warning") {
                                isc.warn(message);
                            } else {
                                isc.say(message);
                            }
                        };
                        // fire on timeout so we don't interfere with focus etc on
                        // editorExit / editorEnter
                        isc.Page.setEvent("idle", action, isc.Page.FIRE_ONCE);

                    }
                }
            }
        },

        //> @object PopulateRule
        // Populates a target field with a value calculated based on values in other fields.
        // <P>
        // Use +link{populateRule.formulaVars} to define variables available to the formula in terms of
        // DataSources and fieldNames in the input data, and use +link{populateRule.formula} to
        // define the formula itself.
        // <P>
        // <b>NOTE:</b> If the target field is on a form that has a +link{dataBoundComponent.rulesEngine, rulesEngine}
        // and this rule changes the target field value, rules that are dependent on the value are
        // also triggered even though the field's <code>changed</code> event does not fire.
        // @visibility rules
        //<

        //> @attr populateRule.formula (String : null : IR)
        // Formula to be evaluated.  All variables used by the formula must be single-letter capital
        // characters (eg A) - see +link{formulaVars}.
        // @visibility rules
        //<

        //> @attr populateRule.formulaVars (Object : null : IR)
        // Object mapping from variable names to fieldNames.  All variable names must be single-letter
        // capital characters (eg A).  For example, for a formula that should divide the field
        // "population" over the field "area", the formula might be "E/L" and formula vars would be:
        // <pre>
        //   {
        //       E: "population",
        //       L: "area"
        //   }
        // </pre>
        // If a formula is applied in a from where multiple DataSources are involved, use
        // <i>dataSourceId</i>.<i>fieldName</i> to define the fieldName.
        // For example, if the "area" field
        // above were part of a DataSource "landStatistics", "landStatistics.area" would be
        // the fieldName
        // to use in <code>formulaVars</code>.
        // <P>
        // There is no need to declare +link{rule.dependentFields} for a PopulateRule if the desired
        // <code>dependentFields</code> are just the formula vars - these are automatically used for
        // dependentFields.
        // @visibility rules
        //<

        populate : {
            type:"populate",
            title:"Populates a target field with a value calculated based on values in other fields",

            valueType:"custom",
            dataType:"none",
            editorType:"PopulateRuleEditor",
            getAttributesFromEditor:function (fieldName, item) {
                // item stores value as an object formula/formulaVars
                return item.getValue();
            },
            setEditorAttributes:function(fieldName, item, attributes) {
                // Attributes we're interested in are formulaVars and formula
                if (attributes != null && attributes.formula != null) {
                    item.setValue({formula:attributes.formula, formulaVars:attributes.formulaVars});
                } else {
                    item.clearValue();
                }
            },

            isRule:true,
            supportedTargets:["FormItem"],
            action : function (result, item, validator, record, component) {
                // do nothing unless we actually ran (so if appliesWhen doesn't pass we don't
                // run the 'populate' logic)
                if (result != true) return;

                // lazily create and cache the formula function (the vars will not change post
                // create)
                if (validator._formulaFunction == null) {
                    // Use FormulaBuilder.generateFunction() to convert the formula string to
                    // a function.
                    // This method takes
                    // - formula - an object with "text" (the formula string) and "formulaVars"
                    // - an array of field objects, which should contain "name" and "mappingKey"
                    //   We create this dynamically from the formulaVars here
                    // - an (optional) component, which we omit.

                    var formulaObject = {};
                    var formulaVars = formulaObject.formulaVars = validator.formulaVars;
                    formulaObject.text = validator.formula;

                    var fieldDescriptors = [];
                    for (var key in formulaVars) {
                        var fieldDescriptor = {};
                        fieldDescriptor.mappingKey = key;
                        fieldDescriptor.name = formulaVars[key];
                        fieldDescriptors.add(fieldDescriptor);
                    }

                    validator._formulaFunction =
                        isc.FormulaBuilder.generateFunction(formulaObject, fieldDescriptors, null, true);
                }

                var formulaResult = validator._formulaFunction(record, component);
                if (item.setValue) {
                    var oldValue = item.getValue();
                    item.setValue(formulaResult);

                    // Give any Rules associated via a rulesEngine a chance to fire.
                    if (oldValue != formulaResult && item.form && item.form.rulesEngine) {
                        // Must use another thread if triggered from rules engine
                        isc.Class.delayCall("processChanged", [item.form, item], 0, item.form.rulesEngine);
                    }
                } else if (component && component.setValue) {
                    var fieldName = item.fieldName;
                    if (fieldName == null) fieldName = item.dataPath;
                    component.setValue(fieldName, formulaResult);
                }
            },

            // By default populate rules depend on their formula source fields so
            // changes to those fields should trigger the rule to re-run.
            getDependentFields : function (rule, triggerEvent) {
                // if passed a non object as formulaVars, ignore it

                if (rule.formulaVars == null || !isc.isAn.Object(rule.formulaVars)) return null;
                var sourceField = [];
                for (var key in rule.formulaVars) {
                    sourceField.add(rule.formulaVars[key]);
                }
                return sourceField;
            }
        },

        //> @object SetRequiredRule
        // Marks a field as required.
        // <P>
        // This rule will most commonly be used in conjunction with +link{validator.applyWhen}
        // criteria set to conditionally set a field as required in response to some standard
        // +link{rule.TriggerEvent}.
        //
        // @visibility rules
        //<
        setRequired : {
            type:"setRequired",
            valueType:"none",
            dataType:"none",
            isRule:true,
            supportedTargets:["FormItem"],
            action : function (result, item, validator, record, component) {
                // If the validator was skipped (result == null), we'll set required to false.
                var required = !!result;
                var liveItem = item;
                if (!liveItem.setRequired && component.getItem) {
                    liveItem = component.getItem(item.name || item.dataPath);
                }
                if (liveItem && liveItem.setRequired) {
                    liveItem.setRequired(required);

                } else {
                    item.required = required;
                }
            }
        }
    },

    //> @attr Validator._acceptExcelFormats (Boolean : false : IRW)
    // Whether to force validators to accept Excel formats.  Default is not to do so.
    //<
    _acceptExcelFormats: false

});


isc.Validator.addClassMethods({

    //> @classMethod Validator.create()
    // A Validator shouldn't be created directly. Instead pass +link{Properties} as
    // each Validator in +link{formItem.validators} or wherever a Validator is needed.
    //
    // @visibility external
    //<
    // Log a warning if called directly
    create : function (A,B,C,D,E,F,G,H,I,J,K,L,M) {
        this.logWarn("A Validator shouldn't be created directly. Instead pass properties " +
                     "as each Validator in a FormItem's validators property or wherever a " +
                     "Validator is needed.");

        return isc.addProperties({}, A,B,C,D,E,F,G,H,I,J,K,L,M);
    },

    getValidatorType : function (validator) {
        // if no type was specified, get it from the validator.type property
        var type = validator.type;
        // use _constructor if specified. This allows us to handle validators converted from XML
        // using schema
        if (type == null && validator._constructor) type = validator._constructor;
        return type;
    },

    // Is the validator server-only?

    isServerValidator : function (validator) {
        if (validator.serverOnly) return true;

        // Check whether we have a build-in validator definition of the appropriate type.
        var validatorDefinition = this._validatorDefinitions[this.getValidatorType(validator)];
        if (validatorDefinition != null && validatorDefinition.requiresServer) return true;

        return false;
    },

    // Process validator and performAction are internal methods called by
    // DynamicForm, valuesManagers, and editable ListGrids to perform validation.
    //
    // The RulesEngine also makes use of this method to process Rules, which are
    // implemented as specialized validators.

    processValidator : function (item, validator, value, type, record) {

        // if the validator is server-side only, return true
        if (validator.serverOnly) return true;

        // pick up the type from the validator object if not explicitly passed
        if (type == null) type = isc.Validator.getValidatorType(validator);

        var result = true;

        // Check whether we have a build-in validator definition of the appropriate type.
        var validatorDefinition;
        if (type != null)  validatorDefinition = this._validatorDefinitions[type];

        // if the validator is marked as a "rule" we don't support validator.condition

        if (validator.isRule || (validatorDefinition && validatorDefinition.isRule)) {
            return true;
        }
        // If a validator definition was not found, check whether we have a
        // standard validator in the old format of the appropriate type.
        var validationFunction;
        if (validatorDefinition == null) {

            // use the validator.condition if one was specified
            if (validator.condition) {
                // CALLBACK API:  available variables:  "item,validator,value,record"
                // Convert a string callback to a function
                if (!isc.isA.Function(validator.condition)) {
                    //>DEBUG
                    this.logDebug("Creating function for validation condition:\r" +
                                  validator.condition);
                    //<DEBUG
                    isc.Func.replaceWithMethod(validator, "condition",
                                               "item,validator,value,record");
                }
                validationFunction = validator.condition;
            }
        } else {
            // If validator is server-only, return successful validation for client
            if (validatorDefinition.requiresServer == true) {
                return true;
            }
            // Pull validation function from definition
            validationFunction = validatorDefinition.condition;

            // Push default error message to validator if not already set
            if (!validator.errorMessage) {
                validator.defaultErrorMessage = validatorDefinition.defaultErrorMessage;
            }
        }

        // if we found a validating function, call it
        if (validationFunction != null) {
            // NOTE: first clear the "resultingValue" field and suggested value of the
            // validator, in case the validation rule decides to set it

            // for Array-valued fields (field.multiple=true), validate each value in the Array

            var validateEachItem = validator.validateEachItem;
            if (validateEachItem == null) validateEachItem = item.validateEachItem;
            if (item && item.multiple && validateEachItem && isc.isAn.Array(value)) {
                var resultingValue = [];
                for (var i = 0; i < value.length; i++) {
                    // Each call to validationFunction could set the resultingValue
                    delete validator.resultingValue;
                    // NOTE: don't stop on failure
                    result = result && validationFunction(item, validator, value[i], record);
                    // capture each resulting value
                    resultingValue[i] = (validator.resultingValue != null ?
                                         validator.resultingValue : value[i]);
                }
                // return the array value as the overall resulting value
                validator.resultingValue = resultingValue;
            } else {
                delete validator.resultingValue;
                result = validationFunction(item, validator, value, record);
            }
        //>DEBUG
        } else {
            this.logWarn("validator not understood on item: " + isc.echo(item) + ":\r" +
                         isc.echoFull(validator));
        //<DEBUG
        }
        return result;
    },

    performAction : function (result, item, validator, record, component, targetObjectType) {
        var type = this.getValidatorType(validator);

        // Check whether we have a build-in validator definition of the appropriate type.
        var validatorDefinition;
        if (type != null)  validatorDefinition = this._validatorDefinitions[type];

        // Rules require a target object type since the "locator" can point to
        // something other than a FormItem. This will be passed in by the RulesEngine if
        // the rule was derived from a locator. Otherwise it's always a field.
        if (targetObjectType == null) targetObjectType = "FormItem";
        var isRule = validator.isRule,
            supportedTargets = validator.supportedTargets;

        var actionFunction;
        if (validatorDefinition != null) {
            actionFunction = validatorDefinition.action;
            if (isRule == null) isRule = validatorDefinition.isRule;
            if (isRule && supportedTargets == null) {
                // assume if supportedTargets is unset the rule supports just FormItems like any
                // other validator.
                supportedTargets = validatorDefinition.supportedTargets || ["FormItem"]
            }
        }

        if (isRule && !supportedTargets.contains(targetObjectType)) {
            this.logWarn("Rule of type:" + type +
                " called for target object " + item + ", of type:" + targetObjectType +
                ". This is not a valid target for this rule. Ignoring.");
        }

        // if we didn't find an actionFunction, use the validator.action if one was specified

        if (actionFunction == null && validator.action) {
            // CALLBACK API:  available variables:  "result,item,validator,component"
            // Convert a string callback to a function
            if (!isc.isA.Function(validator.action)) {
                //>DEBUG
                this.logDebug("Creating function for validation action:\r" +
                              validator.action);
                //<DEBUG
                isc.Func.replaceWithMethod(validator, "action",
                                           "result,item,validator,record,component,targetObjectType");
            }
            actionFunction = validator.action;
        }
        // call the action method
        if (actionFunction != null) {
            actionFunction(result, item, validator, record, component, targetObjectType);
        }
    },

    getErrorMessage : function (validator, messageAttribute) {

        var errorMessage = messageAttribute ? validator[messageAttribute] : validator.errorMessage;

        if (errorMessage == null) errorMessage = validator.defaultErrorMessage;

        // Convert (potentially) dynamic error message strings to straight
        // strings
        if (errorMessage && validator.dynamicErrorMessageArguments) {
            errorMessage = errorMessage.evalDynamicString(
                                null, validator.dynamicErrorMessageArguments);
        }
        return errorMessage;
    },


    //>    @classMethod    Validator.addValidator()    (A)
    // Add a new validator type that can be specified as +link{Validator.type} anywhere
    // validators are declared, such as +link{DataSourceField.validators} or
    // +link{FormItem.validators}.
    // <br>
    // The <code>condition</code> argument should be a method of the same signature as
    // +link{Validator.condition()}.
    // <P>
    // This method is essentially a shortcut for building a +link{validatorDefinition} object
    // and passing that to +link{Validator.addValidatorDefinition()}
    //
    // @param type (String) type name for the new validator
    // @param condition (StringMethod) function or expression to evaluate to determine whether
    //                                 validation was successful
    //
    // @group validation
    // @visibility external
    // @see Validator.addValidators()
    //<
    addValidator : function (type, condition) {
        if (isc.isA.String(type)) {
            var valsObject = {};
            valsObject[type] = condition;
            return this.addValidators(valsObject);
        }
    },

    //>    @classMethod    Validator.addValidators()    (A)
    //  Add several new validator types at once, as though +link{addValidator()} were called
    //  several times.
    //
    //   @group    validation
    //   @param    newValidators    (object)    Set of validators to add.  This parameter should
    //      be a JavaScript object where the property names are validator type names, and the
    //      property values are condition functions or expressions, for example:<br>
    //      &nbsp;&nbsp;&nbsp;<code>{type1:condition1, type2:condition2}</code><br>.
    //
    // @visibility external
    // @see Validator.addValidator()
    //<
    addValidators : function (newValidators) {
        // wrap the functions in a definition object and pass through to addValidatorDefinition()
        for (var type in newValidators) {
            var definition = {};
            definition.type = type;
            definition.condition = newValidators[type];
            if (!isc.isA.Function(definition.condition)) {
                isc.Func.replaceWithMethod(definition, "condition", "item,validator,value");
            }
            newValidators[type] = definition;
        }
        this.addValidatorDefinitions(newValidators);
    },

    //>@classMethod  Validator.addValidatorDefinition() (A)
    // Add a new validator type that can be specified as +link{Validator.type} anywhere
    // validators are declared, such as +link{DataSourceField.validators} or
    // +link{FormItem.validators}.
    //
    // @param type (String) type name for the new validator
    // @param definition (ValidatorDefinition) the validator definition
    //
    // @group validation
    // @visibility external
    // @see Validator.addValidatorDefinitions()
    //<
    addValidatorDefinition : function (type, definition) {
        if (!isc.isAn.Object(definition)) {
            isc.logWarn("Invalid validator in call to addValidatorDefinition. Ignored.");
        }
        var valsObject = {};
        valsObject[type] = definition;
        return this.addValidatorDefinitions(valsObject);
    },

    //>@classMethod  Validator.addValidatorDefinitions() (A)
    // Add several new validator types at once, as though +link{addValidatorDefinition()}
    // were called several times.
    //
    // @group validation
    // @param newDefinitions (object) Set of validators to add.  This parameter should
    //      be a JavaScript object where the property names are validator type names, and the
    //      property values are +link{validatorDefinition}s.
    //
    // @visibility external
    // @see Validator.addValidatorDefinition()
    //<
    addValidatorDefinitions : function (newDefinitions) {
        if (!newDefinitions || !isc.isAn.Object(newDefinitions)) return;

        // Check for redefinition of validators and log warning
        for (var type in newDefinitions) {
            if (this._validatorDefinitions[type]) {
                isc.logWarn("addValidatorDefinitions: Validator definition already exists " +
                            "for type " + type + ". Replacing.");
            }
        }
        isc.addProperties(this._validatorDefinitions, newDefinitions);
    },

    //> @classMethod Validator.getValidatorDefinition() [A]
    // Return the +link{type:ValidatorDefinition} for some validator type name registered via
    // +link{addValidatorDefinition}
    // @param type (String) type name to retrieve the validator for
    // @return (ValidatorDefinition) registered validator definition (or null if no
    //  validator was found for this type name).
    //<

    getValidatorDefinition : function (type) {
        return this._validatorDefinitions[type];
    }

});

// Synonym with regexp. Same as in server side.
isc.Validator.addValidatorDefinition("regex", isc.Validator.getValidatorDefinition("regexp"));






/*    OPEN ISSUES
    * how to handle CanvasItems in containerItems?
    * how to handle values for a containerItem?
    * how to handle "name" for a containerItem?

*/





//>    @class    ContainerItem
//
//    Container formItem to show a filter for a set of datasource fields.
//
//<
isc.ClassFactory.defineClass("ContainerItem", "FormItem");

isc.ContainerItem.addProperties({

    //>    @attr    containerItem.items        (array : null : IRW)
    //        Array of sub-items for this container item.
    //<

    cellSpacing:0,
    cellPadding:2,
    cellBorder:0,

    //>    @attr    containerItem.recalculateItemsOnRedraw        (boolean : false : IR)
    //            If true, we recalculate the list of sub-items every time
    //            this.getInnerHTML is called.  Otherwise we do it automatically only
    //            when this.items is null.
    //<
    recalculateItemsOnRedraw:false,

    //>    @attr   containerItem._hasDataElement   (boolean : false : AR)
    //      Container items have no data element of their own.
    //<
    _hasDataElement:false,

    // Set changeOnKeypress to false since keypresses get bubbled and the keypress may occur
    // on a sub item for which we don't want to update

    changeOnKeypress:false

    //> @attr   containItem.itemCellStyle   (FormItemBaseStyle : null : IRA)
    // If specified this style will be written into each sub-item's cell, overriding the
    // items own "cellStyle" property.
    //<

});


// take methods directly from the DynamicForm itself
isc.ContainerItem.addMethods(isc.applyMask(isc.DynamicForm.getPrototype(), [
    // get all the routines that deal with outputting the table around the elements
    "getTableStartHTML", "_getTableElementID", "_getTableElement",
    "getCellStartHTML", "_getCellStartHTML", "getCellEndHTML", "_getCellEndHTML",
    "getTitleAlign", "getItemPromptHTML", "getTitleCellHTML",
    "_containsItemTitleAttrHTML", "_getTitleCellID", "isRTL",

    // get things that deal with nested sets of items
    "getItem", "fieldIdProperty"
    ]
));

isc.ContainerItem.addMethods({

//>    @method    ContainerItem.init()    (A)
//        initialize the containerItem object.
//
//        @param    [all arguments]    (object)    objects with properties to override from default
//<
init : function () {

    this.Super("init", arguments);

    // initialize the list of sub items, defaulting to an empty list
    this.setItems(this.items ? this.items : null);

},

// override destroy to destroy all items too
destroy : function () {
    if (this.items) {
        for (var i = 0; i < this.items.length; i++) {
            this.items[i].destroy();
        }
    }
    this.Super("destroy", arguments);
},

//>    @method    containerItem.setItems()    (A)
//
//     Reset the list of items displayed by this class.
//
//    @param    itemList    (array)        Array of item objects.  If they are not already
//                                    FormItem subclasses, they will be converted.
//<
setItems : function (itemList) {
    var oldItems = this.items ? this.items : null;

    // Set this.items to match the itemList passed in if there was one.
    if (itemList) this.items = itemList;
    else itemList = this.items;

    if (!this.items) return null;

    //>DEBUG
    this.logDebug("Creating " + this.items.length + " contained items");
    //<DEBUG

    // iterate through all the items, creating FormItems from object literals
    var appliedAccessKey = false;
    for (var itemNum = 0; itemNum < itemList.length; itemNum++) {
        var item = itemList[itemNum];
        // remove any empty items from the list
        if (!item) {
            itemList.removeItem(itemNum--);
            continue;
        }

        // override '_getElementTabIndex' for each item so the container item controls the
        // tabIndex of it's child-items
        // For now we don't allow child items to control their tab indices separately from
        // their container items tab index.

        isc.addMethods(
            item,
            {   _getElementTabIndex : function () {
                    return this.parentItem._getElementTabIndex();
                }
            }
        );

        // The sub-item is contained in the same containerWidget as this form item
        item.containerWidget = this.containerWidget;

        // set the item.parentItem and eventParent to point back to us
        item.parentItem = this;

        item.eventParent = this;
        item.form = this.form;

        // note that the sub items will never have a title cell written out for them
        item.showTitle = item.showTitle != null ? item.showTitle : false;

        // convert from a simple object into a FormItem

        if (!isc.isA.FormItem(item)) itemList[itemNum] = item = isc.FormItemFactory.makeItem(item);

        // Also apply the 'accessKey' to the first focusable sub item
        if (this.accessKey != null && !appliedAccessKey && item._canFocus()) {
            item.accessKey = this.accessKey;
            appliedAccessKey = true;
        }

        // if the item has a name property, add a reference to the object to us under that name
        // (We don't use the ID, since that is already a global pointer to the sub item)
        if (item.name != null) this[item.name] = item;

    }

    if (this.isDrawn()) {
        if (oldItems && oldItems != this.items) {
            this._clearingItems = {};
            for (var i = 0; i < oldItems.length; i++) {
                var oldItem = oldItems[i];
                if (!itemList.contains(oldItems[i])) {
                    this._clearingItems[oldItems[i].getID()] = true;
                }
            }
        }
    }

    // redraw this form item (default implementation will redraw the form / containing widget)
    this.redraw();
},

// simple getter for this.items

getItems : function () {
    return this.items;
},

_$titleCell:"_titleCell",

// override getTitleHTML() to avoid writing a <label> tag around the title, and setting an
// accessKey property on that label.  This is appropriate as we are setting up the accessKey
// directly on the first sub-element in the group (implemented in setItems)
writeOutLabelTag:true,
getTitleHTML : function(){
    var elementID, focusableItem;
    var title = this.getTitle();

    if (!this.writeOutLabelTag || !this.getCanFocus()) {
        return title;
    }

    if (this.accessKey != null) {
        title = isc.Canvas.hiliteCharacter(title, this.accessKey);
    }

    for (var i = 0; i < this.items.length; i++) {
        if (this.items[i].getCanFocus() && this.items[i].hasDataElement()) {
            focusableItem = this.items[i];
            break;
        }
    }

    if (!focusableItem) {
        return title;
    }
    return isc.SB.concat("<LABEL FOR=", focusableItem.getDataElementId(), ">", title, "</LABEL>");
},


// Override '_setElementTabIndex()', as the superclass implementation forces a form.redraw()
// for any focusable items without elements, and this may not be necessary.
_setElementTabIndex : function (tabIndex) {
    if (!this.isVisible() || !this.containerWidget.isDrawn()) return;

    this._elementTabIndex = tabIndex;

    for (var i = 0; i < this.items.length; i++) {
        if (this.items[i]._canFocus()) this.items[i]._setElementTabIndex(tabIndex);
    }
    // If none of the sub items forced this form to markForRedraw(), simply update our icons
    // and we're done - otherwise the form will redraw and write out the correct tabIndices
    // on everything anyway.
    if (!this.form.isDirty()) {
        this._updateIconTabIndices();
    }
},

// Container items are non-editable by default.
// Subclasses that allow editing will override this value as appropriate
isEditable : function () {
    return false;
},

//> @method ContainerItem._canFocus()   (A)
//  Override _canFocus() to return true if any of our sub-elements can accept keyboard focus
//  @return (boolean)   true if this form item can accept keyboard focus
//<
_canFocus : function () {
    if (!this.items) return false;
    for (var i=0; i < this.items.length; i++) {
        if (this.items[i]._canFocus()) return true;
    }
    return false;
},

//> @method ContainerItem.focusInItem()   (A)
//  Override focusInItem to focus in the first sub-item that will accept focus
//<
focusInItem : function () {
    if (!this.isVisible() || !this._canFocus()) return;
    for (var i=0; i < this.items.length; i++) {
        if (this.items[i]._canFocus()) {
            this.items[i].focusInItem();
            break;
        }
    }
},

//> @method ContainerItem.blurItem()   (A)
//  Override blurItem to call 'blurItem' on whichever sub-item is marked as having focus.
//<
blurItem : function () {
    for (var i=0; i < this.items.length; i++) {
        if (this.items[i].hasFocus) {
            this.items[i].blurItem();
            break;
        }
    }
},

// Override _applyHandlersToElement() - we have no 'focusElement', even if _canFocus() is true
// so only apply handlers to icons.
_applyHandlersToElement : function () {
    this._setUpIconEventHandlers();
},


// Notify sub items that they've been drawn/cleared at the appropriate times.
drawn : function () {
    var items = this.items;
    if (!items) return;
    for (var i = 0; i < items.length; i++) {
        if (items[i].visible != false) items[i].drawn();
    }
    return this.Super("drawn", arguments);
},

redrawn : function () {
    var items = this.items;
    if (!items) return;
    for (var i = 0; i < items.length; i++) {
        var item = items[i];
        if (item.visible != false) {
            if (!item.isDrawn()) item.drawn();
            else item.redrawn();
        } else {
            if (item.isDrawn()) item.cleared();
        }
    }
    // If setItems was called - ensure we call 'cleared' on any items that got yanked from
    // our items array (and therefore from the DOM).
    if (this._clearingItems) {
        for (var ID in this._clearingItems) {
            if (window[ID] != null) window[ID].cleared();
        }
        delete this._clearingItems;
    }
    return this.Super("redrawn", arguments);
},

cleared : function () {
    var items = this.items;
    if (!items) return;
    for (var i = 0; i < items.length; i++) {
        if (items[i].isDrawn()) items[i].cleared();
    }
    if (this._clearingItems) {
        for (var ID in this._clearingItems) {
            if (window[ID] != null) window[ID].cleared();
        }
        delete this._clearingItems;
    }
    return this.Super("cleared", arguments);
},

//>    @method    ContainerItem.makeNamedItem()    (A)
// Make a particular item of the specified name, taking it from this object or from the
// FormItem class.
//
// This implementation caches items so we don't end up creating them over and over for the same
// object.
//<
makeNamedItem : function (itemName, extraProperties) {
    // create the itemCache if it hasn't been made already
    if (!this.itemCache) this.itemCache = {};
    // get the item from the itemCache by name
    var item = this.itemCache[itemName];
    // if it wasn't found
    if (!item) {
        // find the item specification in either this object or as a constant in the FilterItem class
        item = (this[itemName] || this.getClass()[itemName]);
        // if any extra properties were passed in, create a new blank object and add them to the item properties
        if (extraProperties != null) {
            item = isc.addProperties({}, item, extraProperties);
        }
        // now create the item as a real FormItem, and store it in the cache for later
        item = this.itemCache[itemName] = isc.FormItemFactory.makeItem(item);
    }
    // and return the item!
    return item;
},

_$br:"<br>",

//>    @method    containerItem.getInnerHTML()    (A)
//
//     Return the HTML needed to draw this item.
//
//        @param    values    (string)    Value of the element (unused).
//        @return            (string)    HTML to draw this item.
//<
getInnerHTML : function (values, includeHint, includeErrors, returnArray) {

    if (!values) values = {};

    // if the items haven't been set for this element, call setItems to do so now
    //    this lets subclasses defer setting items until draw time
    if (!this.items || this.recalculateItemsOnRedraw || !isc.isA.FormItem(this.items[0])) this.setItems();


    if (!this.items) return "No items set for containerItem " + this;

    var clearInactiveContext;
    if (this.isInactiveHTML() && this._currentInactiveContext == null) {
        clearInactiveContext = true;
        this._currentInactiveContext = this.setupInactiveContext(null);

        if (this.logIsDebugEnabled("inactiveEditorHTML")) {
            this.logDebug("getInnerHTML(): Item is marked as inactive - set up " +
                "new inactive context ID:" + this._currentInactiveContext,
                "inactiveEditorHTML");
        }
    }

    // form items are only actually responsible for writing out error HTML if error orientation
    // is left or right
    var errorOrientation = this.getErrorOrientation(),
        showErrors,
        errorOnLeft = errorOrientation == isc.Canvas.LEFT,
        errorHTML;
    if (includeErrors &&
        (errorOnLeft || errorOrientation == isc.Canvas.RIGHT))
    {
        var errors = this.getErrors();
        if (errors) {
            showErrors = true;
            errorHTML = this.getErrorHTML(errors);
        }
    }

    // get a StringBuffer to hold the output
    var output = isc.StringBuffer.newInstance();

    // If we need to write out a hidden native data element, do so now.

    if (this._useHiddenDataElement()) {
        output.append(this._getHiddenDataElementHTML());
    }

    // start the table
    output.append(this.getTableStartHTML());

    // Check Visibility / Disabled State
    // --------------------------------------------------------------------------------------------

    var items = this.items;

    // iterate through the items, marking items as invisible if their .showIf is false
    for (var itemNum = 0; itemNum < items.length; itemNum++) {
        var item = items[itemNum];

        // note that the value of this item can't possibly be dirty
        item._markValueAsNotDirty()

        // set the form of the item to the same form we draw in
        item.form = this.form;

        // if the item has a showIf property
        //    evaluate that to see whether the item should be visible or not
        //    (note if the visible states of any items changes)
        if (item.showIf) {
            // CALLBACK API:  available variables:  "value"
            // Convert a string callback to a function
            if (!isc.isA.Function(item.showIf)) {
                isc.Func.replaceWithMethod(item, "showIf", "item,value,form");
            }

            var value = this.getItemValue(item, values);

            var    visible = (item.showIf(item,value,this.form) != false)
            ;
            if (visible != item.visible) {
                item.visible = visible;
            }
        }
    }


    // DynamicForm.getInnerHTML() makes use of applyTableResizePolicy() to determine the desired
    // sizes for it's component form items.
    // This (among other things) sets up the _size property on the form items to an array containing
    // the desired width and height.
    // Certain form items make use of this _size property in their getInnerHTML() methods. If
    // the property is not set they will default to using item.width and item.height.

    // Draw HTML for Items
    // --------------------------------------------------------------------------------------------

    // for each field in the list
    for (var itemNum = 0, len = this.items.length; itemNum < len; itemNum++) {
        // get a pointer to the item for that field
        var item = this.items[itemNum];
        // if a null item, skip it
        if (!item) continue;

        // if the item has been marked as invisble, skip it
        if (!item.visible) continue;

        var value = this.getItemValue(item, values);

        // if the item should start a row or passes the name boundary
        //     output the end and start row tag
        if (item._startRow || itemNum == 0) {
            if (itemNum != 0) output.append("</TR>");
            output.append("<TR>");
        }

        // If we show the error on the left write the error cell out before the first item in
        // the first row
        // Note: this means if there is no first item we'll fail to write out the error HTML
        // not a known use case for this.
        if (itemNum == 0 && showErrors && errorOnLeft) {
            // rowspan ensures that if we are showing multiple rows, the error text really shows up
            // on the left of all items
            var numRows = 1;
            for (var ii = 1; ii < this.items.length; ii++) {
                if (this.items[ii]._startRow) numRows++;
            }
            output.append("<TD ROWSPAN=",numRows,">",errorHTML,"</TD>");
        }

        var error = item.getErrors();

        // place title on the left of the item, in it's own cell
        if (item.titleOrientation == isc.Canvas.LEFT) {
            output.append(this.getTitleCellHTML(item, error));
        }

        // output the tag start for the item if it has a positive row and colSpan

        output.append(this.getCellStartHTML(item));

        // place title on top of the item, with no separate cell
        if (item.titleOrientation == isc.Canvas.TOP) {
            output.append(this.getTitleSpanHTML(item, error), this._$br);
        }

        // output the innerHTML for the item (including the hint, if there is one)
        output.append(item.getInnerHTML(value, true));

        // append the tag end for the item
        output.append(this.getCellEndHTML(item));

    }

    // If we are showing icons (or might be showing icons in the future), draw them into
    // a table cell after any sub-items - this prevents them being wrapped and shown up on the
    // next line of the page.
    // If 'showPickerIcon' is true write out a picker icon before any other icons


    if (this.showPickerIcon || (this.showIcons && this.icons != null)) {
        var width = this.getTotalIconsWidth();
        // have to explicitly add width of pickerIcon if we're showing it.
        if (this.showPickerIcon) width += this.getPickerIconWidth();

        // output the tag start for the item if it has a positive row and colSpan
        output.append(
            this._getCellStartHTML(
                (this.form.isRTL() ? isc.Canvas.RIGHT : isc.Canvas.LEFT),    // align
                null, //vAlign
                this.getCellStyle(),    // classname
                1, 1,                   // rowSpan / colSpan

                width, // width
                // can leave height, extrastuff unspecified
                null, null,
                // Avoid double border/margin/padding
                isc.Canvas._$noStyleDoublingCSS,
                // Don't pass in itemID or formID - don't write out a click handler on this cell
                null, null, null,
                // Suppress wrapping of icons (if we have more than one in the cell)
                (this.icons && (this.showPickerIcon || this.icons.length > 1))
            )
        );
        output.append(this.getIconsHTML(true));
        // append the tag end for the item (again suppress wrapping within the cell)
        output.append(this._getCellEndHTML(true));
    }

    // use the hint cell to write out right-orientated errors
    if (showErrors && !errorOnLeft) includeHint = true;

    if (includeHint) {
        var hint = this.getHint(),
            rightError = !errorOnLeft ? errorHTML : null,
            hintString = (hint && rightError) ? hint+rightError : (hint || rightError);

        if (hintString && !isc.isA.emptyString(hintString)) {
            // We inherit our _$hintCellTemplate from the FormItem class
            this._$hintCellTemplate[1] = this._getHintCellID();
            this._$hintCellTemplate[3] = this.getHintStyle();
            this._$hintCellTemplate[5] = hintString;
            output.append(this._$hintCellTemplate);
        }
    }


    // end the table
    output.append("</TR></TABLE>");

    if (clearInactiveContext) delete this._currentInactiveContext;

    return output.toString();
},

getPickerIcon : function () {
    var icon = this.Super("getPickerIcon", arguments);
    icon.writeIntoItem = false;
    return icon;
},


// get item value - used by getInnerHTML() to retrieve the value of a sub item of this container.

getItemValue : function (item, values) {
    if (values == null) values = {};

    if (!isc.isA.FormItem(item)) item = this.getItem(item);

    if (!item) return null;

    // get the value and error for this form element
    var name = item.getFieldName(),
        value = null
    ;

    // if the value was specified on the item, use that
    if (item.value != null) value = item.value;

    // if a name was specified in the item,
    if (value == null && name) {
        // get the value from the form.values

        value = values[name];
    }

    if (value == null) {
        // otherwise call getValue().
        // If the item has already been drawn and edited this should pick up the value from
        // item._value, otherwise it'll pick up the item default
        value = item.getValue();
    }
    return value;
},


// override _itemValueIsDirty() to check for each sub-item being dirty.
_itemValueIsDirty : function () {
    if (this.items == null) return false;
    for (var i = 0; i < this.items.length; i++) {
        if (this.items[i]._itemValueIsDirty()) return true;
    }

    return this._valueIsDirty;
},

// override _markValueAsNotDirty to mark the value as not dirty for this, and each item.
_markValueAsNotDirty : function () {
    this._valueIsDirty = false;
    for (var i = 0; i < this.items.length; i++) {
        this.items[i]._markValueAsNotDirty();
    }
},

// override updateDisabled to enable / disable all the child items.
updateDisabled : function (settingFormHandleDisabled) {
    this.Super("updateDisabled", arguments);
    if (this.items) {
        for (var i = 0; i< this.items.length; i++) this.items[i].updateDisabled(settingFormHandleDisabled);
    }
},

// override updateCanEdit to push read-only state to all the child items.
updateCanEdit : function () {
    this.Super("updateCanEdit", arguments);
    if (this.items) {
        for (var i = 0; i< this.items.length; i++) this.items[i].updateCanEdit();
    }
},

//>    @method    containerItem.getTextDirection()    (A)
//        Get the text direction of this canvas.
//        @group    appearance
//        @platformNotes    IE win only!
//        @return    (TextDirection)    direction -- Canvas.LTR or Canvas.RTL
//<
getTextDirection : function () {
    return this.form.getTextDirection();
},

// Override getLeft() / getTop() to look at the position table element for the containerItem
getLeft : function () {
    var element = this._getTableElement();
    if (element == null) {
        this.logWarn("getLeft() Unable to determine position for " +
                      (this.name == null ? "this item " : this.name) +
                      ". Position cannot be determined before the item is drawn " +
                      "- returning zero");
        return 0;
    }
    return this._getElementLeft(element);

},

getTop : function () {
    var element = this._getTableElement();
    if (element == null) {
        // We will not find an element if we are not drawn into the DOM
        this.logWarn("getTop() Unable to determine position for " +
                      (this.name == null ? "this item " : this.name) +
                      ". Position cannot be determined before the item is drawn " +
                      "- returning zero");
        return 0;
    }
    return this._getElementTop(element);

},

// Ditto with getVisibleWidth() / getVisibleHeight()
getVisibleWidth : function () {
    var element = this._getTableElement();
    if (element == null) return this.Super("getVisibleWidth", arguments);

    return element.offsetWidth;
},

getVisibleHeight : function () {
    var element = this._getTableElement();
    if (element == null) return this.Super("getVisibleHeight", arguments);

    return element.offsetHeight;
},

getTitleSpanHTML : function (item, error) {
    var output = isc.StringBuffer.create();

    output.append("<SPAN ", this._containsItemTitleAttrHTML(item),
                  " CLASS='", item.getTitleStyle(),
                  "' ALIGN='", this.getTitleAlign(item),
                  "'>");

    // get the actual title from the item
    output.append(item.getTitleHTML());

    // now end the title span
    output.append("</SPAN>");
    // and return the whole thing
    return output.release();
},

setReadOnlyDisplay : function (appearance) {
    this.readOnlyDisplay = appearance;
    if (this.items) {
        for (var i=0; i<this.items.length; i++) {
            this.items[i].setReadOnlyDisplay(appearance);
        }
    }
    if (this.canEdit == false && this.isDrawn()) {
        this.redraw();
    }
},

setCanEdit : function (canEdit) {
    this.Super("setCanEdit", arguments);
    if (this.items) {
        for (var i=0; i<this.items.length; i++) {
            this.items[i].setCanEdit(canEdit);
        }
    }
    if (this.canEdit == false && this.isDrawn()) {
        this.redraw();
    }
}

});






//> @class CanvasItem
// FormItem which renders a Canvas inline in a DynamicForm instance.
// <P>
// CanvasItem is +link{FormItem.shouldSaveValue,shouldSaveValue}:false by default, meaning that
// no value from the CanvasItem will be present in +link{form.values} and no value will be
// saved when +link{dynamicForm.saveData()} is called.  This is appropriate if your Canvas does
// not participate in displaying or editing the values of the form and is embedded in the form
// for layout purposes only.
// <P>
// If you set +link{FormItem.shouldSaveValue,shouldSaveValue}:true,
// +link{CanvasItem.showValue()} will be called to provide a value that your item should
// display.  Implement <code>showValue()</code> and call methods on the Canvas you've created
// to cause the value to be displayed.
// <P>
// <code>showValue()</code> will be called in various situations where the form receives data,
// including a call to +link{dynamicForm.setValues()}, +link{dynamicForm.editRecord()}, or if
// +link{dynamicForm.fetchData()} is called and a Record is returned.  Bear in mind that
// <code>showValue()</code> can be called when the form and your item have not yet been drawn;
// in this case, store the value for later display.
// <P>
// To provide a value to the form, call +link{CanvasItem.storeValue()} whenever the user changes
// the value in your Canvas.  Note that the form <b>will not</b> call
// <code>canvasItem.getValue()</code> in order to discover your item's value, so there is no
// purpose in overriding this method - use <code>storeValue</code> to proactively inform the
// form about changes instead.  This approach is necessary in order to enable change events.
// <P>
// If you cannot easily detect changes to values in your Canvas, a workaround is to call
// <code>storeValue</code> right before the form saves.
//
// @treeLocation Client Reference/Forms/Form Items
// @visibility external
//<
isc.ClassFactory.defineClass("CanvasItem", "FormItem");
isc.CanvasItem.addProperties({
    width:"*", height:null,


    //> @attr canvasItem.height (integer : null : IRW)
    // Height of the Canvas.  Can be either a number indicating a fixed height in pixels, a
    // percentage indicating a percentage of the overall form's height, or "*" indicating take
    // whatever remaining space is available. See the +link{group:formLayout} overview for details.
    // <p>
    // Height may also be explicitly specified on the +link{canvasItem.canvas}. In this
    // any <code>canvasItem.height</code> will be ignored in favor of the value applied
    // to the canvas directly. In either case, percentage values will be resolved using
    // standard formItem sizing rules as described in +link{group:formLayout}
    // @visibility internal
    //<


    //> @attr canvasItem.multiple (Boolean : null : IR)
    // Whether this CanvasItem is intended to hold multiple values.
    // <var class="smartgwt">
    // <P>
    // This attribute can affect the return type of getValue(). If this CanvasItem is storing
    // multiple values, then the return type of getValue() is <code>JavaScriptObject</code>
    // (a JavaScript array object) if multiple is null or false. However, if multiple is true,
    // then the return type is either <code>List</code> or <code>RecordList</code>.
    // </var>
    // @visibility external
    //<

    //> @attr canvasItem.shouldSaveValue (Boolean : false : IR)
    // Should this item's value be saved in the form's values and hence returned from
    // +link{dynamicForm.getValues,form.getValues()}?
    // <P>
    // See the +link{CanvasItem} class overview for a discussion of values-handling in
    // CanvasItems
    // @visibility external
    //<
    shouldSaveValue:false,

    // override _setElementValue() to call showValue()
    _setElementValue : function (newValue, dataValue) {

        var undef;
        if (dataValue === undef) {
            dataValue = this._value;
        }

        this.showValue(newValue, dataValue, this.form, this);
    },

    // Have showvalue no-op by default -- documented in registerStringMethods
    showValue : function (displayValue, dataValue) {
    },

    //> @method canvasItem.storeValue()
    // Store a value for this form item. This method will fire standard +link{formItem.change()}
    // and +link{formItem.changed()} handlers, and store out the method passed in such that
    // subsequent calls to +link{formItem.getValue()} or +link{dynamicForm.getValue()} will return
    // the new value for this item.
    // <P>
    // For canvasItems that manipulate values and display them in some arbitrary
    // canvas representation developers should call this method when the user interacts with
    // the embedded canvas in a way that modifies the value for the item.
    // <P>
    // If you cannot easily detect changes to values in your Canvas, a workaround is to call
    // <code>storeValue</code> right before the form saves.
    // @param    value     (any)                value to save for this item
    // @visibility external
    //<
    // Implemented at the FormItem level

    // ------------------------
    // Methods applied directly to this.canvas (fired in the scope of the canvas, not the
    // canvasItem-  use this.canvasItem to point back to the item).

    // set up observation of resizing

    _canvas_resized : function (deltaX, deltaY, reason) {
        this.Super("resized", arguments);

        // CanvasResized will cause a form redraw - only call this if we're not drag
        // resizing to avoid redrawing repeatedly during drag resize interactions.
        if (!this.dragResizing() && this.canvasItem) {
            this.canvasItem.canvasResized(deltaX, deltaY, reason);
        }
    },
    _canvas_dragResized : function () {
        this.canvasItem.canvasResized(1,1); // HACK:
        return this.Super("dragResized", arguments);
    },

    _canvas_focusInNextTabElement : function (forward, mask) {
        if (isc.isA.DynamicForm(this)) {
            return this.Super("_focusInNextTabElement", arguments);
        } else
            return this.canvasItem.form._focusInNextTabElement(forward, mask);
    },
    _canvas_getTabIndexSpan : function () {
        if (isc.isA.DynamicForm(this)) {
            return this.Super("getTabIndexSpan", arguments);
        }
        // This will go through all descendents recursively to figure out where they should
        // be in the tab-order.
        var tabStops = [];
        this.canvasItem._getCanvasTabDescendents(this, tabStops);
        var span = 0;
        for (var i = 0; i < tabStops.length; i++) {
            if (tabStops[i] == this) span +=1
            else span += tabStops[i].getTabIndexSpan();
        }
        return span
    },


      //> @attr    canvasItem.canvas        (Canvas : null : [IRW])
    //
    // The canvas that will be displayed inside this item.  You can pass an instance you've
    // already created, or its global ID as a String. You can also implement
    // +link{CanvasItem.createCanvas()} to dynamically create the canvas when the FormItem
    // is initialized.
    // <P>
    // If <code>canvas</code> and <code>createCanvas()</code> are unspecified, the
    // canvas for this item will be auto-created using the overrideable defaults:
    // +link{attr:CanvasItem.canvasProperties} and +link{attr:CanvasItem.canvasConstructor}
    // <P>
    // Note that +link{canvas.canvasItem} will be set on the canvas to point back to this
    // item.
    //
    //  @visibility external
    //<

    //> @attr canvas.canvasItem (CanvasItem : null : [IR])
    // If this canvas is being displayed in a +link{CanvasItem}, this property will be set
    // to point at the item. Otherwise this property will be null.
    // @visibility external
    //<

      //> @attr    canvasItem.canvasConstructor        (String : "Canvas" : [IRW])
    //
    // If +link{canvasItem.canvas, this.canvas} is not specified as a canvas instance at init
    // time, a canvas will be created instead. This property denotes the class of that widget
    // (Should be set to the name of a subclass of Canvas).
    //
    //  @visibility external
    //<



    //> @attr    canvasItem.canvasDefaults        (Object : {} : [IRW])
    //
    // Default properties for the canvas if this.canvas is not already a canvas instance.
    //
    //  @visibility external
    //<

    //> @attr    canvasItem.canvasProperties        (Object : {} : [IRW])
    //
    // Properties to apply to this canvas on creation if this.canvas is not already a canvas
    // instance.
    //
    //  @visibility external
    //<


    //> @attr canvasItem.autoDestroy (Boolean : false : [IRWA])
    // Should this item's +link{canvasItem.canvas,canvas} be automatically destroyed when the item
    // is destroyed? Form items are destroyed automatically when a call to
    // +link{DynamicForm.setItems()} removes them from their parent form, or if their
    // parent form is destroyed. This property governs whether, when this occurs,
    // the item's canvas should also be +link{Canvas.destroy(),destroyed}.
    // <P>
    // Setting this property to true
    // is typically appropriate for cases where a custom CanvasItem automatically creates
    // its canvas as part of its initialization flow.
    // Note that once a canvas has been destroyed it can not be re-used elsewhere within
    // an application.
    //
    // @visibility external
    //<

    autoDestroy: false

    //showTitle:false

    //> @attr CanvasItem.overflow (Overflow : null : IR)
    // CanvasItems support specifying overflow for the Canvas directly on the item.
    // @visibility external
    //<
});
isc.CanvasItem.addMethods({
    init : function () {
        this.Super("init", arguments);
        this._createCanvas(this.form, this);
        this.canvasInitialized = true;
    },

    // A straight canvasItem is non-editable. Subclasses that allow editing (such as the sliderItem)
    // will override this method to return true

    isEditable : function () {
        return false;
    },

    // override _canFocus -- if our canvas is focusable, we're focusable
    _canFocus : function () {
        // If the developer explicitly marks the item as canFocus true, assume they know what
        // they're doing.
        if (this.canFocus == true) return this.canFocus;
        var canvas = this.canvas;
        if (canvas && canvas._canFocus) return canvas._canFocus();
        return false;
    },

    // _createCanvas    Method to ensure that the canvas for this item has been instantiated
    // with appropriate properties, and added to the containerWidget as a child.
    _createCanvas : function () {

        // CanvasItem.canvas
        // - can be defined as a live canvas instance
        // - can be defined as a properties block to turn into a live canvas
        // If this.createCanvas() is specified, this.canvas is dynamically set to the
        // result of calling that method
        //
        // If this.canvas is still unspecified we'll use the autoChild pattern
        // (canvasConstructor, canvasDefaults, canvasProperties)
        //
        // Undocumented: The autoChild subsystem also supports dynamically getting defaults via
        // getDynamicDefaults() which would allow dynamic customization of the canvas autoChild
        // when it's auto-created.
        if (this.createCanvas != null) {
            var canvas = this.canvas;
            this.canvas = this.fireCallback("createCanvas", "form,item", arguments);
            // if this.createCanvas existed but returned nothing, hang onto our original
            // canvas object
            // It could just modify it in place and we should allow that
            if (this.canvas == null) this.canvas = canvas;
        }

        //>DEBUG
        if (!isc.isAn.Object(this.canvas) && !this.canvasProperties && !window[this.canvas]) {

            //>EditMode
            // Visual Builder habitually creates CanvasItems which will be furnished with a canvas
            // only after insertion into the editContext (because the canvas itself also needs to
            // be put into edit mode and added to the editContext).  We don't want the default
            // canvas because it means we get a warning in the log about creating it, then another
            // warning immediately that we've deleted it...
            if (isc.designTime) return;
            //<EditMode

            this.logWarn("CanvasItem: " +
                        (this.getFieldName() ? this.getFieldName() : this.getID()) +
                        " defined with no canvas property - creating a default " +
                        "canvas for this item.");
        }
        //<DEBUG

        // If the supplied canvas property is a string containing the ID of a valid Canvas
        // object, use that Canvas object
        if (!isc.isAn.Object(this.canvas) && isc.isA.Canvas(window[this.canvas])) {
            this.canvas = window[this.canvas]
        }

        var canvasProps = {
            // don't redraw when the form redraws - if the developer wishes to redraw this canvas
            // they will have to call redraw() on this form item, or on the Canvas directly.
            _redrawWithParent: false,
            // one time flag to suppress automatic drawing on addChild().  Very key, as a form
            // that has setItems() called on it after creation recreates all items with the
            // form already drawn.
            _dontDrawOnAddChild: true,

            // Don't auto-destroy with parent.
            // We can render our canvas as a child of some
            // arbitrary container widget, and we don't want it to be destroyed automatically
            // when that gets destroyed if our form / formItem isn't explicitly destroyed.
            // We use this pattern in Grid editing / filterEditor stuff: If the grid-body
            // in which the edit item was embedded is destroyed and rebuilt, we use the
            // same FormItem in the new version of the grid body, so we just reparent the
            // canvas rather than destroying and rebuilding.
            // If the form is actually destroyed we should get a destroy call on the item
            // which will clean up the canvases of all CanvasItems.
            destroyWithParent:false,

            // Always set initial tabIndex to -1. This will avoid the canvas from ever getting
            // an auto-assigned tab index and having a prev/next tab-widget.
            // Note that as part of 'setElementTabIndex()' we will explicitly assign the
            // desired tabIndex.
            tabIndex:-1,

            //hideUsingDisplayNone:true,

            disabled: this.shouldDisableCanvas()
        };
        canvasProps.resized = this._canvas_resized;
        canvasProps.dragResized = this._canvas_dragResized;

        // Have standard events on the canvas fall back through to us.


        canvasProps.handleClick = function () {
            var rv = this.Super("handleClick", arguments);
            // if this is a StatefulCanvas, rely on the 'handleActivate' path to
            // fire the item level click handler.
            if (!isc.isA.StatefulCanvas(this) && this.canvasItem) {
                rv = this.canvasItem.handleClick() && rv;
            }
            return rv;
        }
        // handleActivate will only be called for StatefulCanvas and subclasses
        canvasProps.handleActivate = function () {
            var rv = this.Super("handleActivate", arguments);
            if (this.canvasItem) rv = this.canvasItem.handleClick() && rv;
            return rv;
        }

        canvasProps.handleDoubleClick = function () {
            var rv = this.Super("handleDoubleClick", arguments);
            if (this.canvasItem) rv = this.canvasItem.handleDoubleClick() && rv;
            return rv;
        }

        canvasProps.handleKeyPress = function () {
            var rv = this.Super("handleKeyPress", arguments);
            if (this.canvasItem) {
                // fire item.keyPress / form.itemKeyPress if we get a keyPress on the
                // target canvas (or in fact a sub-canvas thereof).
                var itemRV = this.canvasItem._fireKeyPressHandlers(
                        this.canvasItem,
                        this.canvasItem.form,
                        isc.EH.getKey(),
                        isc.EH.getKeyEventCharacterValue()
                );
                if (itemRV == false) rv = false;
            }
            return rv;
        }

        canvasProps.handleKeyDown = function (event,eventInfo) {
            var rv = this.Super("handleKeyDown", arguments);
            if (this.canvasItem) {
                var itemRV = this.canvasItem.handleKeyDown(event, eventInfo);
                if (itemRV == false) rv = false;
            }
            return rv;
        }
        canvasProps.handleKeyUp = function (event,eventInfo) {
            var rv;
            if (this.keyUp != null) {
                rv = this.keyUp(event, eventInfo);
            }
            if (this.canvasItem) {
                var itemRV = this.canvasItem.handleKeyUp(event, eventInfo);
                if (itemRV == false) rv = false;
            }
            return rv;
        }

        // Override synthetic focus manipulation methods to fall back to the DF, since that
        // already manages moving focus between form items on tab / shift tab keypress when
        // the clickMask is up
        canvasProps._focusInNextTabElement = this._canvas_focusInNextTabElement;

        canvasProps.getTabIndexSpan = this._canvas_getTabIndexSpan;

        // pass our datasource, if any to the CanvasItem
        if (this.dataSource) canvasProps.dataSource = this.dataSource;
        // pass on our prompt (if any) to the CanvasItem
        if (this.prompt && this.applyPromptToCanvas) canvasProps.prompt = this.prompt;
        // pass on our overflow (is specified) to the canvas.
        if (this.overflow != null) canvasProps.overflow = this.overflow;

        // We'll set the tabIndex on the canvas when we write out our innerHTML.


        // pass on our accessKey if appropriate
        if (this.accessKey != null) canvasProps.accessKey = this.accessKey;

        // pass on 'showFocused'
        if (this.showFocused != null) canvasProps.showFocused = this.showFocused;
        if (this.showFocusedAsOver != null) canvasProps.showFocusedAsOver = this.showFocusedAsOver;

        if (isc.isA.String(this.canvas) && window[this.canvas]) this.canvas = window[this.canvas];

        // if the canvas hasn't been instantiated for us, use the autoChild method to create it
        // otherwise we have to do a bunch of manual patching to achieve the same effect.
        if (!isc.isA.Canvas(this.canvas)) {
            // pick up any properties specified directly on the 'this.canvas' object
            isc.addProperties(canvasProps, this.canvas, { canvasItem: this });
            if (canvasProps.ID != null) canvasProps.ID = null;
            // since we're auto-creating it's appropriate to autoDestroy
            this.autoDestroy = true;

            this.addAutoChild("canvas", canvasProps, isc.Canvas, this.containerWidget);
        } else {


            var originalProps = {};
            for (var prop in canvasProps) {
                originalProps[prop] = this.canvas[prop];
            }
            this.canvas._originalProps = originalProps

            // apply dynamic properties to existing canvas
            // (Call setter methods wherever necessary).
            this.canvas.setTabIndex(-1);
            if (this.applyPromptToCanvas) this.canvas.setPrompt(this.prompt);
            this.canvas.setAccessKey(this.accessKey);

            isc.addProperties(this.canvas, canvasProps, { canvasItem: this });
            if (canvasProps.dataSource) this.canvas.bindToDataSource();
            this.canvas.setDisabled(this.shouldDisableCanvas());
            this.containerWidget.addChild(this.canvas);
        }

        // We'll handle percent sizing specified on the embedded Canvas directly
        if (this.canvas != null) {
            this.canvas._initialPercentBox = this.canvas.percentBox;
            this.canvas.percentBox = "custom";
        }

        // If we're added to a containerWidget rather than the form, and a clickMask is showing
        // we may need to unmask explicitly

        if (this.containerWidget != this.form) {
            if (isc.EH.clickMaskUp()) {
                var CMIDs = isc.EH.getAllClickMaskIDs();
                for (var i = CMIDs.length -1; i >= 0; i--) {
                    var parentMasked = isc.EH.targetIsMasked(this.containerWidget, CMIDs[i]);
                    if (!parentMasked) {
                        isc.EH.addUnmaskedTarget(this.canvas, CMIDs[i]);
                        // We're iterating down from the top - once a widget is over one mask it's also
                        // over any masks below that one. Therefore we don't need to keep iterating
                        // down to the bottom adding unmasked targets.
                        break;
                    }
                }
            }
        }
        if (this.canvas) this.observe(this.canvas, "_focusChanged", "observer.canvasFocusChanged()");

        if (isc.isA.DynamicForm(this.canvas)) {
            this.observe(this.canvas, "setFocusItem", "observer.nestedFormSetFocusItem()");
        }
    },

    //> @method canvasItem.setCanvas()
    // Setter to update the +link{canvasItem.canvas} at runtime
    // @param canvas (Canvas) New canvas to display.
    // @visibility external
    //<
    setCanvas : function (canvas) {
        if (isc.isA.Canvas(this.canvas)) {
            if (this.canvas != canvas) {
                // "destroy" the old canvas.
                // if this.autoDestroy is true, this will truly destroy it
                // otherwise it simply deparents it and cleans up various props we
                // scribble onto it connecting it with the CanvasItem.
                this._destroyCanvas();
            // nothing to do if we're passed in our existing canvas
            } else {
                return;
            }
        }

        if (canvas) this.canvas = canvas;
        // No need to call _createCanvas if we haven't yet completed init (occurs automatically at the
        // end of init())
        if (this.canvasInitialized) {
            this._createCanvas();
            // a redraw appears required to initially size and draw the CanvasItem.
            if (this.containerWidget.isDrawn()) this.containerWidget.markForRedraw();
        }
    },


    /*
    // Ensure redraw on this item redraws the Canvas.
    redraw : function (suppressCanvasRedraw) {
        // redraw the canvas before redrawing our innerHTML - allows us to calculate sizes
        // accurately, if they change.
        if (!suppressCanvasRedraw && this.canvas.isDrawn()) {
            this.canvas.redraw("canvasItem.redraw");
        }
        this.Super("redraw", arguments);
    },
    */

    //> @method canvasItem.canEditChanged()
    // Notification method called when +link{formItem.canEdit} is modified. Developers
    // may make use of this to toggle between an editable and a read-only appearance
    // for custom FormItems.
    // @param canEdit (boolean) New canEdit value
    // @visibility external
    //<

    //> @method canvasItem.readOnlyDisplayChanged()
    // Notification method called when +link{formItem.readOnlyDisplay} is modified. Developers
    // may make use of this to alter the read-only appearance of custom FormItems.
    // @param readOnlyDisplay (ReadOnlyDisplayAppearance) New readOnlyDisplay value
    // @visibility external
    //<


    redrawn : function (suppressCanvasRedraw) {
        this.Super("redrawn", arguments);
        var disabled = this.shouldDisableCanvas();
        if (this.canvas.isDisabled() != disabled) this.canvas.setDisabled(disabled);
    },

    drawn : function () {
        this.Super("drawn", arguments);
        var canvas = this.canvas;
        if (canvas && canvas.parentElement != this.containerWidget) {
            this._createCanvas();
            this.placeCanvas();
        }
    },

    // clear the pointer on this item's canvas back to this item on destroy()
    destroy : function () {
        this._destroyCanvas();
        return this.Super("destroy", arguments);
    },

    _destroyCanvas : function () {
        if (this.canvas) {
            delete this.canvas.canvasItem;
            var parentElement = this.canvas.parentElement;
            delete this.canvas.parentElement;
            if (parentElement == this.containerWidget) {
                this.containerWidget.removeChild(this.canvas);
            }
            if (this.autoDestroy) {
                this.canvas.destroy(true);
            } else {

                isc._allowDeleteFuncProperty = true;

                // reset the various odd overrides to resized etc that won't apply without
                // a canvasItem
                isc.addProperties(this.canvas, this.canvas._originalProps);
                delete this.canvas._originalProps;

                delete isc._allowDeleteFuncProperty;

                // clear and pull out of the containerWidget.
                if (this.canvas.isDrawn()) {
                    this.canvas.clear();
                }
                this.canvas.deparent();

                // ignore the focus methods we observed at init
                this.ignore(this.canvas, "_focusChanged");
                if (isc.isA.DynamicForm(this.canvas)) {
                    this.ignore(this.canvas, "setFocusItem");
                }

                // reset the "percentBox" flag.
                this.canvas.percentBox = this.canvas._initialPercentBox;
                delete this.canvas._initialPercentBox;

                // and set this.canvas to null so this code doesn't run again later
                this.canvas = null;
            }
        }
    },

    placeCanvas : function (delayed) {
        var canvas = this.canvas;


        if (canvas.destroyed) return;

        // show or hide the Canvas according to whether this item is visible, so that showIfs work
        if (this.visible == false) {
            canvas.hide();

            isc.Canvas.moveOffscreen(canvas);
            return;
        }

        if (this.form && !this.form.isDrawn() && this.form.position == isc.Canvas.RELATIVE) {

            //this.logWarn("hiding Canvas during initial relative draw");
            canvas.hide();
            return;
        }

        // place the Canvas over the cell for the item

        // figure out the spacer's coordinates within the form
        var containerWidget = this.containerWidget,
            containerHandle = containerWidget.getHandle(),
            spacerParent = isc.Element.get(this.getID() + "_spacerParent");


        if (isc.Browser.isIE && !isc.Browser.isStrict && isc.Browser.version >= 9) {
            spacerParent = spacerParent.firstChild;
        }

        var spacerOffsets = isc.Element.getOffsets(spacerParent, containerHandle),
            left = spacerOffsets[isc.Canvas.LEFT] + containerWidget.getScrollLeft(),
            top = spacerOffsets[isc.Canvas.TOP] + containerWidget.getScrollTop();


        //>SingleDiv
        if (!containerWidget._drewClipDiv) {
            var borderSizes = containerWidget._calculateBorderSize();
            left -= borderSizes.left;
            top -= borderSizes.top;
        }
        //<SingleDiv

        // this.logWarn("placing Canvas at: " + [left, top]);

        // place the Canvas on top of that cell
        if (canvas.parentElement != containerWidget) {
            containerWidget.addChild(canvas);
        }
        canvas.moveTo(left, top);
        if (canvas.visibility == isc.Canvas.HIDDEN) {
            canvas.show();
        }
        if (!canvas.isDrawn()) {
            canvas.draw();
        }


        if (!delayed && isc.Browser.isMac && isc.Browser.isMoz && left == 0 && top == 0) {
            isc.Timer.setTimeout({target:this, methodName:"_delayedPlaceCanvas"}, 0);
        }
    },

    _delayedPlaceCanvas : function () {
        this.placeCanvas(true);
    },

    cleared : function () {
        this.Super("cleared", arguments);
        // when a canvasItem gets cleared, ensure the canvas is cleared too
        if (this.canvas && this.canvas.isDrawn()) {
            this.canvas.clear();
            // Set the one-time flag to reset handle size on adjust overflow and
            // mark for adjust overflow
            this.containerWidget._resetHandleOnAdjustOverflow = true;
            this.containerWidget._markForAdjustOverflow("CanvasItem canvas cleared");
        }
    },

    ensureCanvasCleared : function () {
        if (this.canvas && this.canvas.isDrawn()) {
            this.canvas.clear();
            this.containerWidget._resetHandleOnAdjustOverflow = true;
            this.containerWidget._markForAdjustOverflow("CanvasItem canvas cleared");
        }
    },

    // ensure the canvas floats in the right place if the item is moved
    moved : function () {
        if (this.isDrawn() || (this.containerWidget != null && this.containerWidget.isDrawn())) {
            this.placeCanvas();
        }
    },

    // Sizing
    // ---------------------------------------------------------------------------------------


    checkCanvasOverflow : function () {
        return this.sizeCanvas(true);
    },

    // Historically we sized the canvas to fit the entire cell (using getInnerWidth/height).
    // This is actually not what we want and has been reported as a bug (for example here
    // http://forums.smartclient.com/showpost.php?p=96220&postcount=7)
    // We now use getTextBoxWidth() / height which sizes in a manner consistent with other
    // form items.
    // Retain an undocumented flag to use the old behavior as a hedge against backcompat
    // issues going forward
    sizeCanvasAsTextBox:true,
    _sizeTextBoxAsContentBox : function () {
        return false;
    },

    sizeCanvas : function (firstResizePass) {
        var canvas = this.canvas;

        // if we can't overflow in the height direction, we don't need to do anything on the
        // first sizing pass
        if (firstResizePass &&
            !(canvas.overflow == isc.Canvas.VISIBLE || canvas.overflow == isc.Canvas.CLIP_H))
        {
            this.logDebug("ignoring first pass, can't overflow", "canvasItemSizing");
            return;
        }

        // get the sizes specified by layout policy
        var policyWidth = this.sizeCanvasAsTextBox ? this.getTextBoxWidth()
                                                    : this.getInnerWidth(),
            policyHeight = this.sizeCanvasAsTextBox ? this.getTextBoxHeight()
                                                    : this.getInnerHeight(),
            resizeWidth, resizeHeight;
        // we feed the specified height (whether it appears on the Canvas or CanvasItem) to
        // TableResizePolicy; if we give it a pixel size it will feed that back.  If we give it
        // a variable size (percent or "*"), that size will be incorporated into sizing the row
        // as a whole and we'll get the row height back.


        if (this.heightIncludesTitle &&
            this.showTitle && this.getTitleOrientation() == isc.Canvas.TOP)
        {
            policyHeight -= this.form.getTitleHeight(this);
        }
        resizeHeight = policyHeight;

        // TableResizePolicy doesn't consider the specified width of items when determining
        // column widths.  Hence only apply the width if our width is unset or "*"
        var specifiedWidth = canvas._userWidth || this.width;
        resizeWidth = (specifiedWidth == null || specifiedWidth == "*" || specifiedWidth == "100%"
                       ? policyWidth : specifiedWidth);

        var percentWidth = canvas._percent_width,
            percentHeight = canvas._percent_height;

        var parsedPercentWidth;
        if (policyWidth != null && percentWidth != null && percentWidth.endsWith("%") &&
            !window.isNaN(parsedPercentWidth = parseInt(percentWidth)))
        {
            resizeWidth = Math.round(policyWidth * (parsedPercentWidth/100));
        }

        // if width is not increasing past the current overflowed size, don't try to reduce the
        // height of an height-overflowed Canvas to less than the overflowed size, because
        // there's no reason to expect it to shrink (unless it's dirty, in which case we assume
        // it might change size)

        if (!canvas.isDirty() &&
            (resizeWidth == null || resizeWidth <= canvas.getVisibleWidth()) &&
            canvas.getHeight() < canvas.getVisibleHeight() &&
            resizeHeight <= canvas.getVisibleHeight())
        {
            this.logDebug("not applying height: " + resizeHeight +
                          " to overflowed Canvas with height: " + canvas.getVisibleHeight(),
                          "canvasItemSizing");
            resizeHeight = null;
        }


        if (!isc.isA.Number(resizeWidth)) resizeWidth = null;
        if (!isc.isA.Number(resizeHeight)) resizeHeight = null;
        // actually call 'resizeTo' on the canvas
        this._setCanvasSize(resizeWidth,resizeHeight,percentWidth,percentHeight);

        this.logDebug("this._size: " + this._size +
                      ", policy size: " + [policyWidth, policyHeight] +
                      ", specifiedSize: " + [specifiedWidth, canvas._userHeight || this.height] +
                      ", Resized Canvas to: " + [resizeWidth, resizeHeight],
                      "canvasItemSizing");

        // draw or redraw the Canvas so we get an accurate size
        if (!canvas.isDrawn()) {
            // skip the case where the containerWidget is undrawn (for example getting PrintHMTL of
            // an undrawn form)
            var containerDrawnState = this.containerWidget.getDrawnState();

            if (containerDrawnState == isc.Canvas.COMPLETE ||
                containerDrawnState == isc.Canvas.HANDLE_DRAWN) {
                    // move offscreen before draw so we don't impact the scroll height of
                    // the parent if we're not actually drawn
                    isc.Canvas.moveOffscreen(canvas);
                    canvas.draw();
            }

        } else canvas.redrawIfDirty("CanvasItem getting new size");

        var width = canvas.getVisibleWidth(),
            height = canvas.getVisibleHeight();

        this.logDebug("visible size of embedded Canvas: " + [width, height],
                      "canvasItemSizing");

        // if the Canvas overflows in the height direction, set this as a minimum

        if (!firstResizePass) this.minHeight = null;
        else this.minHeight = height > canvas.getHeight() ? height : null;

        // policyHeight is the space allocated to the row(s) this Canvas spans.  If we've
        // exceeded the space that table sizing policy allocated to our row(s), the policy will
        // need to be rerun, in order to reallocate space among other items that can flex.
        // Note: doesn't matter if we've exceeded width; our width isn't taken into account by
        // the policy.
        // Note: doesn't matter whether the Canvas has actually overflowed it's own specified
        // size, which might be much smaller; eg a button next to a TextArea doesn't need to be
        // as tall as the TextArea.
        if (height > policyHeight) return true;
    },

    _setCanvasSize : function (resizeWidth, resizeHeight, percentWidth, percentHeight) {
        this._resizingCanvas = true;
        this.canvas.resizeTo(resizeWidth, resizeHeight);
        if (percentWidth != null) this.canvas._percent_width = percentWidth;
        if (percentHeight != null) this.canvas._percent_height = percentHeight;
        this._resizingCanvas = false;
    },

    // Rather than embedding HTML for the canvas into the form, we write out a spacer of the
    // appropriate size, and float the Canvas over it.
    getElementHTML : function (value) {
        var canvas = this.canvas;



        // size the Canvas to the final size determined by the resize policy
        this.sizeCanvas();

        // Ensure that the canvas has it's tab index written out as specified
        this._setElementTabIndex(this.getGlobalTabIndex());



        // IE has an issue with getOffsetTop method if there is transitional doctype is used
        // to overcome this we add invisible 0px border - this helps somehow
        var invisibleBorder = "";
        if (isc.Browser.isIE && isc.Browser.isTransitional) {
            invisibleBorder = "border:0px solid transparent;"
        }
        return "<SPAN style='padding:0px;margin:0px;" + invisibleBorder + "' ID='"
                + this.getID() + "_spacerParent'>"
                + isc.Canvas.spacerHTML(canvas.getVisibleWidth(), canvas.getVisibleHeight())
                + "</SPAN>";
    },

    getPrintHTML : function (printProperties, callback) {
        // If we're printing, write the printHTML for our canvasItem directly into the item.

        if (this.canvas == null) return isc.nbsp;
        return this.canvas.getPrintHTML(printProperties, callback);
    },


    _applyHandlersToElement : function () {
        this._setUpIconEventHandlers();
    },


    // return specified widths (which can be %s or *s), whether they appear on the Canvas or
    // CanvasItem, to be fed to the table resize policy.  If there is no specified size, feed
    // the layout policy our default height.
    getHeight : function (reportOverflowedSize) {
        var canvas = this.canvas;
        if (reportOverflowedSize) {
            var visibleHeight = canvas.getVisibleHeight();
            if (visibleHeight > canvas.getHeight()) return visibleHeight;
        }
        return this.canvas._userHeight || this.height || this.canvas.defaultHeight;
    },

    getWidth : function () {
        return this.canvas._userWidth || this.width || this.canvas.defaultWidth;
    },

    // handle setWidth/Height(), as well as resize (below) - needed, for example, when used as
    // an item in the FilterEditor of a ListGrid where a user can resize columns
    setWidth : function (width) {
        this.Super("setWidth", arguments);
        this.canvas.setWidth(Math.max(1, this.getInnerWidth() - this.getTotalIconsWidth()));
    },
    setHeight : function (height) {
        this.Super("setHeight", arguments);
        this.canvas.setHeight(this.getInnerHeight());
    },

    // if the Canvas is resized by the user or programmatically outside of CanvasItem's layout
    // code,
    canvasResized : function (deltaX, deltaY, reason) {
        if (this._resizingCanvas) return;

        var canvas = this.canvas,
            newWidth = Math.max(1, canvas.getWidth()),
            newHeight = canvas.getHeight();


        if (reason != "init" &&
            reason != "overflow" && reason != "Overflow on initial draw")
        {
            if (deltaX != null && deltaX != 0) canvas._userWidth = newWidth;
            if (deltaY != null && deltaY != 0) canvas._userHeight = newHeight;
        }

        this.logDebug("canvas resized: new specified sizes: " + [newWidth, newHeight],
                      "canvasItemSizing");

        if (!canvas.isDrawn()) return;

        // redraw to change size
        this.redraw();
    },

    // shouldDisableCanvas - should we explicitly mark this items canvas as disabled
    // true if the item is disabled or read-only

    shouldDisableCanvas : function () {
        return this.isDisabled() || this.isReadOnly();
    },

    setReadOnlyDisplay : function (appearance) {
        this.readOnlyDisplay = appearance;
        if (this.canvas) {
            if (isc.isA.DynamicForm(this.canvas)) {
                this.canvas.readOnlyTextBoxStyle = this.getReadOnlyTextBoxStyle();
                this.canvas.setReadOnlyDisplay(appearance);
            }

        }
        if (this.readOnlyDisplayChanged) this.readOnlyDisplayChanged(appearance);
        if (!this.getCanEdit() && this.isDrawn()) this.redraw();
    },

    setCanEdit : function (canEdit) {
        var wasEditable = !this.isReadOnly();
        this.canEdit = canEdit;
        var isEditable = !this.isReadOnly();
        if (wasEditable != isEditable) {
            if (this.canvas) {
                if (this.canvas.setCanEdit) this.canvas.setCanEdit(canEdit);
            }
            this.updateCanEdit();
            if (this.canEditChanged) this.canEditChanged(canEdit);
            this.redraw();
        }
    },

    // override 'updateDisabled()' to disable the canvas
    updateDisabled : function (settingFormHandleDisabled) {
        this.Super("updateDisabled", arguments);

        this.canvas.setDisabled(this.shouldDisableCanvas(), settingFormHandleDisabled);
    },

    // override 'updateCanEdit()' to simulate read-only state on the canvas by
    // disabling canvas but leaving item enabled so title doesn't use disabled state.
    updateCanEdit : function () {
        this.Super("updateCanEdit", arguments);
        this.canvas.setDisabled(this.shouldDisableCanvas());
    },

    // Update enabled/disabled state of the element to match our read-only/disabled state.
    // This avoids a complete redraw when changing read-only state.
    setElementReadOnly : function () {
        this._setElementEnabled(!this.isReadOnly() && !this.isDisabled());
    },

    // Override _setElementTabIndex() to update the tabindex of the canvas (and avoid redrawing
    // the form)
    _setElementTabIndex : function (index) {
        this._setCanvasTabIndex(index);
    },

    _getCanvasTabDescendents : function (canvas, targetArray) {
        // If a CanvasItem contains a DF which contains another CanvasItem, we already
        // manage the tab-index of the form - no need to attempt to directly manage
        // the tabIndex of canvii embedded in it via canvasItems!
        if (canvas.canvasItem != null && canvas.canvasItem != this) return;
        targetArray.add(canvas);
        var children = canvas.members || canvas.children || {};
        for (var i = 0; i < children.length; i++) {
            this._getCanvasTabDescendents(children[i], targetArray);
        }
    },

    _setCanvasTabIndex : function (index) {
        //this.logWarn(this.name + " setCanvasTabIndex running - index:" + index);
        var canvas = this.canvas,
            widgets = [];
        if (canvas) this._getCanvasTabDescendents(canvas, widgets);

        for (var i = 0; i < widgets.length; i++) {
            canvas = widgets[i];
            // Don't assign tab index if widget canvas is explicitly not a tab stop.
            // Avoid skipping base canvas.
            if (canvas != this.canvas && canvas.tabIndex == -1) continue;

            // clears any pointers to prev/next in auto-tab-order
            canvas._removeFromAutoTabOrder();
            // use the internal method so we don't hit the user-specified tabIndex ceiling
            //this.logWarn("assigning:" + index + " to " + canvas);
            canvas._setTabIndex(index, false);
            // increment

            index += canvas == this.canvas ? 1 : canvas.getTabIndexSpan();
        }
    },

    // Override focusInItem / blurFocusItem to actually put focus into the canvas
    focusInItem : function (focusAtEndDirection) {
        if (this.canvas) {
            // We may be marked as canFocus:true and have a 'canvas' thats not explicitly
            // focusable but has focusable descendants.
            var targets = [],
                canvas;
            this._getCanvasTabDescendents(this.canvas, targets);
            var start = focusAtEndDirection == false ? targets.length-1 : 0,
                end = focusAtEndDirection == false ? 0 : targets.length-1,
                step = focusAtEndDirection == false ? -1 : 1;

            for (var i = start; focusAtEndDirection != false ? i <= end : i >= end; i+=step) {
                if (targets[i].isDrawn() && targets[i].isVisible() &&
                    !targets[i].isDisabled() && targets[i]._canFocus())
                {
                    if (this.logIsDebugEnabled("syntheticTabIndex") && focusAtEndDirection != null) {
                        this.logDebug("focusInItem() - shifting focus to " +
                            (focusAtEndDirection ? "start" : "end") +
                            " - moving focus to canvas:" + canvas,
                             "syntheticTabIndex");
                    }
                    canvas = targets[i];
                    break;
                }
            }
            if (canvas) {
                if (focusAtEndDirection != null) {
                    canvas.focusAtEnd(focusAtEndDirection);
                } else {
                    canvas.focus();
                }
                return;
            }
        }
        var isSynthetic = (focusAtEndDirection != null),
            showLogs= isSynthetic ? this.logIsDebugEnabled("syntheticTabIndex") :
                        this.logIsDebugEnabled("nativeFocus");
        if (showLogs) {
            this.logDebug("focusInItem() unable to find focusable canvas." +
                (isSynthetic ? " Attempting to focus at " +
                            (focusAtEndDirection ? "start" : "end") : ""),
                 (isSynthetic ? "syntheticTabIndex" : "nativeFocus"));
        }
        return this.Super("focusInItem", arguments);
    },

    blurItem : function () {
        if (this.canvas) this.canvas.blur();
        return this.Super("blurItem", arguments);
    },

    // Override moveFocusWithinItem to handle shifting focus to nested descendants.
    _moveFocusWithinItem : function (forward) {
        var showLogs = this.logIsDebugEnabled("syntheticTabIndex");

        var focusCanvas = isc.EH.getFocusCanvas(),
            canvas = this.canvas,
            widgets = [];
        if (canvas) this._getCanvasTabDescendents(canvas, widgets);
        for (var i = 0; i < widgets.length; i++) {
            if (focusCanvas == widgets[i]) {
                var step = forward? 1 : -1,
                    current = i + step;
                var prevCanvas = widgets[i],
                    nextCanvas = widgets[current];
                while (nextCanvas != null) {

                    if (nextCanvas.isDrawn() && nextCanvas.isVisible()
                        && !nextCanvas.isDisabled() && nextCanvas._canFocus())
                    {

                        if (!(!forward && isc.ListGrid &&
                              (
                                (isc.isA.ListGrid(nextCanvas.creator) &&
                                 (nextCanvas.creator.header == nextCanvas ||
                                  nextCanvas.creator.frozenHeader == nextCanvas)
                                ) ||
                                (isc.isA.ListGrid(nextCanvas) &&
                                 (nextCanvas.header == prevCanvas ||
                                  nextCanvas.frozenHeader == prevCanvas)
                                )
                              )
                             ) &&


                            (isc.isA.DynamicForm(nextCanvas) &&
                             nextCanvas._getStartItemForFocusAtEnd(forward) != null)
                           )
                        {
                            if (showLogs) {
                                this.logDebug("CanvasItem shifting focus " +
                                    (forward ? "forward" : "backward")
                                    + " from " +
                                    focusCanvas + " to " + nextCanvas,
                                    "syntheticTabIndex");
                            }
                            nextCanvas.focusAtEnd(forward);
                            return true;
                        }
                    }
                    prevCanvas = nextCanvas;
                    current = current + step;
                    nextCanvas = widgets[current];
                }
                // Must have been the last canvas
                break;
            }
        }
        if (showLogs) {
            this.logDebug("canvasItem.moveFocusWithinItem() current focus canvas:" +
                focusCanvas + ", unable to find next focus canvas", "syntheticTabIndex");
        }
        return false;
    },

    // observation of focusChanged on the Canvas

    canvasFocusChanged : function () {

        var hasFocus = this.canvas.hasFocus;
        if (hasFocus) {
            this.form.setFocusItem(this);
            this.elementFocus();
        } else this.elementBlur();
    },


    nestedFormSetFocusItem : function () {
        this.form.setFocusItem(this);
    },

    //> @method canvasItem.hasAdvancedCriteria()
    // Overridden to return true if +link{canvasItem.canvas} is a dynamicForm.
    // See <var class="smartclient">+link{getCriterion()}</var>
    // <var class="smartgwt"><code>setCriterionGetter()</code></var>
    // for details of editing advanced criteria using nested
    // dynamicForms.
    // @return (Boolean) true if this item's canvas is a DynamicForm
    // @group criteriaEditing
    // @visibility external
    //<
    hasAdvancedCriteria : function () {
        if (this.editCriteriaInInnerForm && isc.isA.DynamicForm(this.canvas)) return true;
        return this.Super("hasAdvancedCriteria", arguments);
    },

    //> @method canvasItem.canEditCriterion()
    // AdvancedCriteria objects may be edited via nested dynamicForms as described in
    // +link{canvasItem.getCriterion()}
    // <P>
    // This method has been overridden to return true if this item's canvas is a DynamicForm,
    // where the +link{dynamicForm.operator} matches the operator of the criterion passed in
    // and dynamicForm contains items where +link{formItem.canEditCriterion()} returns true
    // for each sub-criterion in the object passed in.
    // @param criterion (Criterion) criteria to test
    // @return (boolean) returns true if the specified criterion may be edited by this item
    // @group criteriaEditing
    // @visibility external
    //<
    canEditCriterion : function (criterion) {
        if (this.editCriteriaInInnerForm && isc.isA.DynamicForm(this.canvas)) {
            if (criterion.operator != this.canvas.operator) return false;
            //this.logWarn("going to compare criterion:" + this.echo(criterion) + " with form: " +
            //    this.canvas);
            for (var i = 0; i < criterion.criteria; i++) {
                var items = this.canvas.getItems(),
                    foundItem;
                for (var ii = 0; ii < items.length; ii++) {
                    if (items[ii].canEditCriterion(criterion.criteria[i])) {
                        //this.logWarn("item:" + items[ii] + " can edit:" + this.echo(criterion.criteria[i]));
                        foundItem = true;
                        break;
                    }
                }
                //this.logWarn("found item:" + foundItem + " for criterion:" + this.echo(criterion.criteria[i]));
                if (!foundItem) return false;
            }
            return true;
        }
        return this.Super("canEditCriterion", arguments);
    },

    //> @attr canvasItem.editCriteriaInInnerForm (Boolean : true : IRA)
    // Flag to disable the criteria editing overrides described in
    // <var class="smartclient">+link{getCriterion()}</var>
    // <var class="smartgwt"><code>setCriterionGetter()</code></var> whereby
    // if this item contains a DynamicForm as its canvas, it will be used to edit nested
    // AdvancedCriteria automatically.
    // <P>
    // This flag is required for cases where a canvasItem contains a DynamicForm, but the form
    // is not set up to show inner field values of nested objects, and therefore should not
    // attempt to apply nested advanced criteria directly to the form.
    //
    // @visibility external
    //<
    // This is required for the RelativeDateItem. It's quite likely to be required for
    // developer-defined canvasItems too. For simple items where the form just displays an atomic
    // field value in some custom way, setting this flag will be sufficient to get simple
    // operator-based advanced criteria editing working. If the inner form does something more
    // complex, developers would potentially need to override hasAdvancedCriteria, getCriterion, etc
    editCriteriaInInnerForm:true,

    //> @method canvasItem.getCriterion()
    // The standard formItem criteria editing APIs have been overridden in the canvasItem class
    // to simplify the editing of complex +link{AdvancedCriteria} objects using nested
    // DynamicForms.
    // <P>
    // The following pattern is supported without need for further modification:<br>
    // A complex Advanced criteria object may have nested sub criteria using the <code>"and"</code>
    // or <code>"or"</code> operators. For example:
    // <pre>
    // { _constructor:"AdvancedCriteria",
    //   operator:"and",
    //   criteria:[
    //      {fieldName:"field1", value:"value1", operator:"iContains"},
    //      {operator:"or", criteria:[
    //          {fieldName:"innerField1", value:"value1", operator:"equals"},
    //          {fieldName:"innerField2", value:"value2", operator:"iContains"}
    //       ]
    //      }
    //   ]
    // }
    // </pre>
    // To create a form capable of editing the above criteria without providing custom overrides
    // to +link{formItem.getCriterion()} et al, you would create a form with 2 items.
    // The 'field1' criterion could be edited by a simple form item such as a TextItem.
    // The nested criteria ('innerField1' and 'innerField2') could be edited by a canvasItem
    // whose canvas property was set to a DynamicForm showing items capable of editing the 2
    // inner criteria, and whose operator was specified as "or".<br>
    // For example:
    // <pre>
    //  isc.DynamicForm.create({
    //      items:[
    //          {name:"field1", type:"TextItem"},
    //          {name:"nestedItem", shouldSaveValue:true, type:"CanvasItem",
    //              canvas:isc.DynamicForm.create({
    //                  operator:"or",
    //                  items:[
    //                      {name:"innerField1", type:"TextItem", operator:"equals"},
    //                      {name:"innerField2", type:"TextItem"}
    //                  ]
    //              })
    //          }
    //      ]
    //  });
    //  </pre>
    // This form would be able to edit the above advanced criteria object via
    // +link{dynamicForm.setValuesAsCriteria()}. Edited values would be retrieved via
    // +link{dynamicForm.getValuesAsCriteria()}.
    // <P>
    // Note that the canvas item has <code>shouldSaveValue</code> set to true - this is required
    // to ensure the nested form is actually passed the values to edit.
    // <P>
    // The default implementation of this method checks for this.canvas being specified as a
    // dynamicForm, and in that case simply returns the result of
    // +link{dynamicForm.getValuesAsAdvancedCriteria()} on the inner form.
    // <P>
    // Note that this functionality may be entirely bypassed by
    // setting +link{editCriteriaInInnerForm} to false. This flag is useful when defining a
    // dynamicForm based canvasItem which is not intended for editing nested data -- for example
    // if a standard atomic field value is being displayed in some custom way using a
    // DynamicForm embedded in the item.
    //
    // @return (Criterion) criterion to merge with advanced criteria returned by
    //  +link{dynamicForm.getValuesAsCriteria()}
    // @group criteriaEditing
    // @visibility external
    //<
    getCriterion : function () {
        if (this.editCriteriaInInnerForm && isc.isA.DynamicForm(this.canvas)) {
            return this.canvas.getValuesAsAdvancedCriteria();
        } else return this.Super("getCriterion", arguments);
    },

    //> @method canvasItem.setCriterion()
    // Display a +link{criterion} object in this item for editing. Overridden from
    // +link{formItem.setCriterion()} in order to support editing nested criteria using
    // nested dynamicForms as described in +link{canvasItem.getCriterion()}.
    // <P>
    // Implementation checks for this.canvas being specified as a DynamicForm, and applies
    // criterion directly to the embedded form via setValuesAsCriteria()
    // @param criterion (Criterion) criteria to edit
    // @group criteriaEditing
    // @visibility external
    //<
    setCriterion : function (criterion) {
        if (this.editCriteriaInInnerForm && isc.isA.DynamicForm(this.canvas)) {
            this.canvas.setValuesAsCriteria(criterion, true);
        } else return this.Super("setCriterion", arguments);
    },

    // we copy the prompt onto the Canvas by default - override setPrompt to do the same at runtime

    //> @attr canvasItem.applyPromptToCanvas (Boolean : true : IRW)
    // If +link{formItem.prompt} is specified for this item, should the prompt be applied to the
    // +link{canvasItem.canvas} for this item?
    // @visibility external
    //<
    applyPromptToCanvas:true,

    //> @attr canvasItem.prompt (string : null : IRW)
    // @include FormItem.prompt
    // @visibility external
    //<

    //>@method canvasItem.setPrompt()
    // Set the +link{formItem.prompt} for this item. Default implementation will also apply the
    // prompt to +link{canvasItem.canvas} if +link{canvasItem.applyPromptToCanvas} is true.
    // @param prompt (string) new prompt for the item.
    // @visibility external
    //<
    setPrompt : function (prompt) {
        this.Super("setPrompt", prompt);
        if (this.applyPromptToCanvas && this.canvas) {
            if (isc.isA.Canvas(this.canvas)) this.canvas.setPrompt(prompt);
            else this.canvas.prompt = prompt;
        }
    }
});

isc.CanvasItem.registerStringMethods({
    //> @method canvasItem.createCanvas ()
    // This method allows dynamic creation of a CanvasItem's canvas, rather than
    // setting +link{CanvasItem.canvas} statically.
    // If specified this +link{stringMethod} will be called when the form item is
    // initialized and should return the Canvas to display for this item.
    // @param form (DynamicForm) the dynamicForm in which this item is contained
    // @param item (CanvasItem) the live form item instance
    // @return (Canvas) the canvas to be rendered inside this CanvasItem
    // @visibility external
    //<
    createCanvas:"form,item",


    //> @method canvasItem.showValue()
    // This method will be called whenever this form item's value is being set via a programmatic
    // call to e.g: +link{dynamicForm.setValues()} or +link{formItem.setValue()} and may be
    // overridden by CanvasItems intended to support displaying data values to update the
    // embedded Canvas to reflect the value passed in.
    // @param displayValue (any) new display-value for the item. This is the value after applying
    //   any custom formatter or valueMap
    // @param dataValue (any) underlying data value for the item
    // @param form (DynamicForm) the dynamicForm in which this item is contained
    // @param item (CanvasItem) the live form item instance
    // @visibility external
    //<
    showValue:"displayValue,dataValue,form,item"
});







//>    @class    TextItem
//
// FormItem for managing a text field.
//
// @visibility external
// @example textItem
//<
isc.ClassFactory.defineClass("TextItem", "FormItem");

//    Add class-level properties
//        You can access these properties on the static class object.
//        e.g.,    Canvas.myStaticProperty

isc.TextItem.addClassProperties({

    //>    @type CharacterCasing
    // @visibility external
    // @group validation
    // @value isc.TextItem.DEFAULT No character translation
    DEFAULT:"default",
    // @value  isc.TextItem.UPPER  Map characters to uppercase
    UPPER:"upper",
    // @value  isc.TextItem.LOWER  Map characters to lowercase
    LOWER:"lower",
    //<

    // Filter definitions for mask characters
    _filterDefinitions: {
        '0': { charFilter: "[0-9+\\-]" },
        '#': { charFilter: "[0-9]" },
        '9': { charFilter: "[0-9 ]" },
        'L': { charFilter: "[A-Za-z]" },
        '?': { charFilter: "[A-Za-z ]" },
        'a': { charFilter: "[0-9A-Za-z]" },
        'A': { charFilter: "[0-9A-Za-z]" },
        'C': { charFilter: "." }
    },

    _needNegativeMargins: isc.Browser.isIE &&
                          isc.Browser.version <= 9 &&
                          (isc.Browser.version <= 7 || !isc.Browser.isStrict)
});

isc.TextItem.addProperties({
    //>    @attr    textItem.width        (number : 150 : IRW)
    //            Default width for fields.
    //        @group    appearance
    // @visibility external
    //<
    width:150,

    //>    @attr    textItem.height        (number : 19 : IRW)
    //            Default height for text items.
    //        @group    appearance
    // @visibility external
    //<

    height:isc.Browser.isSafari ? 22 : 19,

    //>    @attr    textItem.textBoxStyle     (FormItemBaseStyle : "textItem" : IRW)
    //  Base CSS class name for this item's input element.
    // NOTE: See the +link{group:CompoundFormItem_skinning} discussion for special skinning considerations.
    // @group    appearance
    // @visibility external
    //<
    textBoxStyle:"textItem",

    //>    @attr    textItem.length        (number : null : IRW)
    //            if set, maximum number of characters for this field
    //        @group    validation
    // @visibility external
    //<
    length:null,

    // whether its possible for this type of FormItem to do autoCompletion
    canAutoComplete:true,

    //>    @attr    textItem._elementType            (string : "TEXT" : IRW)
    //            type of field (eg: "PASSWORD", "UPLOAD", etc)
    //<
    _elementType:"TEXT",

    //> @attr   textItem._hasDataElement    (boolean : true : IRW)
    //      Text items have a data element.
    // @group formValues
    // @visibility   internal
    // @see     method:FormItem.hasDataElement
    // @see     method:FormItem.getDataElement
    //<
    _hasDataElement:true,

    // Set flag to indicate that our data element is used as the textBox for this item.
    // This flag means updateState will apply the result of this.getTextBoxStyle() to this item's
    // data element - appropriate for native text boxes, text areas and selects.
    _dataElementIsTextBox:true,

    //> @attr   textItem.emptyStringValue   (any : null : IRW)
    // How should an empty string entered by the user be stored?
    // This value is typically set to <code>null</code> or <code>""</code>.
    // <P>
    // Note that a call to +link{setValue(),setValue(null)} or +link{setValue(),setValue("")}
    // automatically updates this property to ensure that "empty" values are stored in a
    // consistent format.
    // @group formValues
    // @visibility   external
    //<

    emptyStringValue:null,

    // Override redrawOnShowFormIcon - we can handle dynamically updating the item's HTML to
    // show / hide text item icons
    redrawOnShowIcon:false,
    // setting clipValue to true ensures we resize the text box when showing/hiding icons
    clipValue:true,

    // _nativeEventHandlers is a place to specify native event handlers to be applied to the
    // form item element once it has been written into the DOM (without having to override
    // '_applyHandlersToElement()'
    _nativeEventHandlers : {


        onmousedown : (
            isc.Browser.isIE ? function () {
                var element = this,
                    itemInfo = isc.DynamicForm._getItemInfoFromElement(element),
                    item = itemInfo.item;
                if (item) item._setupFocusCheck();

            } :
            // iPad doesn't give us normal touch start / touch end events when the user
            // touches a text item to focus in it.
            // Therefore use explicit handlers for this.

            isc.Browser.isTouch && !isc.Browser.isAndroid ? function (e) {
                var EH = isc.EventHandler;
                EH.DOMevent = e;
                var    event = EH.getMouseEventProperties(e);
                return EH.handleMouseDown(e, event);
            } : null
        ),

        onmouseup : (
            isc.Browser.isTouch && !isc.Browser.isAndroid ? function (e) {
                var EH = isc.EventHandler;
                EH.DOMevent = e;
                var    event = EH.getMouseEventProperties(e);
                return EH.handleMouseUp(e, event);
            } : null
        )


    },

    //>@attr TextItem.browserSpellCheck (boolean : null : IRWA)
    // @include FormItem.browserSpellCheck
    // @visibility internal
    //<


    //>@attr textItem.browserAutoCapitalize (boolean : null : IRWA)
    // @include FormItem.browserAutoCapitalize
    // @visibility external
    //<

    //>@attr textItem.browserAutoCorrect (boolean : null : IRWA)
    // @include formItem.browserAutoCorrect
    // @visibility external
    //<

    //>@attr textItem.browserInputType (String : null : IRA)
    // This property corresponds to the HTML5 "inputType" attribute applied to the &lt;input&gt;
    // element for this TextItem.
    // <p>
    // The only currently supported use of this attribute is hinting to touch-enabled mobile
    // devices that a particular keyboard layout should be used.  Even here, be careful; to
    // take a random example, using type "number" on Android up to at least 3.2 leads to a
    // keyboard with no "-" key, so negative numbers cannot be entered.
    // <p>
    // <b>Valid values:</b>
    // <table class="normal" cellpadding="2">
    //   <tbody>
    //   <tr>
    //     <td valign="top"><em>"text"</em></td>
    //     <td>Normal text keyboard</td>
    //   </tr>
    //   <tr>
    //     <td valign="top"><em>"digits"</em></td>
    //     <td>Makes the text field more suitable for entering a string of digits 0 - 9. On iOS,
    //         this causes the virtual keyboard to show a numeric keypad with only "0", "1",
    //         "2", ..., "9", and delete keys.</td>
    //   </tr>
    //   <tr>
    //     <td valign="top"><em>"email"</em></td>
    //     <td>Makes the text field more suitable for entering an e-mail address. On iOS, this
    //         causes the virtual keyboard to show special "@" and "." keys on the alphabetic
    //         keys screen.</td>
    //   </tr>
    //   <tr>
    //     <td valign="top"><em>"tel"</em></td>
    //     <td>Makes the text field more suitable for entering a telephone number. On iOS, this
    //         causes the virtual keyboard to show a numeric keypad with a "+*#" key for
    //         displaying punctuation keys.</td>
    //   </tr>
    //   <tr>
    //     <td valign="top"><em>"number"</em></td>
    //     <td>Makes the text field more suitable for entering a floating-point value. On iOS,
    //         this causes the virtual keyboard to start on the number and punctuation keys screen.
    //         <p>
    //         <b>NOTE:</b> This is not an appropriate text input type for credit card numbers,
    //         postal codes, ISBNs, and other formats that are not strictly parsable as floating-point
    //         numbers. This is because the browser is required to perform floating-point value
    //         sanitization to ensure that the value is a <a href="http://www.w3.org/TR/html5/infrastructure.html#valid-floating-point-number">valid floating-point number</a>.</td>
    //   </tr>
    //   <tr>
    //     <td valign="top"><em>"url"</em></td>
    //     <td>Makes the text field more suitable for entering a URL. On iOS, this causes the
    //         virtual keyboard to show a special ".com" key.</td>
    //   </tr>
    //   <tr>
    //     <td valign="top">Any&nbsp;vendor-<br>specific value</td>
    //     <td>If a browser supports another input type.</td>
    //   </tr>
    //   </tbody>
    // </table>
    // @visibility external
    //<

    //>@attr TextItem.selectOnFocus (boolean : null : IRW)
    // @include FormItem.selectOnFocus
    // @visibility external
    //<

    //>@attr TextItem.selectOnClick (boolean : null : IRW)
    // @include FormItem.selectOnClick
    // @visibility external
    //<

    //>@attr TextItem.changeOnKeypress (Boolean : true : IRW)
    // @include FormItem.changeOnKeypress
    // @visibility external
    //<

    //>@method TextItem.getSelectionRange()
    // @include FormItem.getSelectionRange()
    // @visibility external
    //<

    //>@method TextItem.setSelectionRange()
    // @include FormItem.setSelectionRange()
    // @visibility external
    //<

    //>@method TextItem.selectValue()
    // @include FormItem.selectValue()
    // @visibility external
    //<

    //>@method TextItem.deselectValue()
    // @include FormItem.deselectValue()
    // @visibility external
    //<

    //>@attr TextItem.readOnly  (boolean : null : IRWA)
    // Setter for the standard HTML readonly property of the input element.
    // If set to true, text will be non editable (though it can still be selected and copied etc)
    // @visibility internal
    //<

    //>@attr TextItem.showHintInField (boolean : null : IRWA)
    // If showing hint for this form item, should it be shown within the field?
    // <P>CSS style for the hint is +link{textItem.textBoxStyle} with the suffix
    // "Hint" appended to it. If the item is disabled the suffix "DisabledHint" will be
    // used.
    // @group appearance
    // @see FormItem.hint
    // @visibility external
    //<

    //>@attr TextItem.printFullText (Boolean : false : IRW)
    // When generating a print-view of the component containing this TextItem, should
    // the form item expand to accommodate its value? If set to false the text box will not expand
    // to fit its content in the print view, instead showing exactly as it does in the
    // live form.
    // @visibility external
    // @group printing
    //<
    printFullText:false,

    //> @attr textItem.saveOnEnter (Boolean : true : IRW)
    // Text items will submit their containing form on enter keypress
    // if +link{DynamicForm.saveOnEnter,saveOnEnter} is true. Setting this property to
    // <code>false</code> will disable this behavior.
    // @visibility external
    //<
    // default implementation of formItem.shouldSaveOnEnter() returns this
    saveOnEnter: true
});

isc.TextItem.addMethods({
    // _willHandleInput()
    // Can we use the "input" event in this browser / form item?
    // True for Moz and Safari, but not IE. See comments near FormItem._handleInput()
    _willHandleInput : function () {
        return !isc.Browser.isIE;
    },

    // If the user cuts or pastes into a text field, then, depending on the browser,
    // we will either get an input event or a cut or paste event.  In either case, we
    // need to update the mask with the text currently stored in the text box.
    _$fixMaskAfterCutPaste: "_fixMaskAfterCutPaste",
    _pendingFixMask: null,
    _nativeCutPaste : function () {

        if (this._pendingFixMask != null) {
            isc.Timer.clearTimeout(this._pendingFixMask);
        }
        this._pendingFixMask = isc.Timer.setTimeout({target: this, methodName: this._$fixMaskAfterCutPaste}, 0);
        return this.Super("_nativeCutPaste", arguments);
    },
    __handleInput : function () {
        // In IE10+, we only listen for ONINPUT to catch the case where the user clicks on the
        // -ms-clear pseudo-element: http://msdn.microsoft.com/en-us/library/windows/apps/hh465740.aspx
        if (isc.Browser.isIE && isc.Browser.version >= 10) {
            // If the data element value is empty and the _pendingFixMask timer is not set
            // (indicating the user just cut or pasted text), then clear the value.
            if (this._pendingFixMask == null && !this.getElementValue()) {
                this.clearValue();
            }

        } else {

            this._fixMaskAfterCutPaste();
            return this.Super("__handleInput", arguments);
        }
    },
    __handleSelect : function () {
        if (this.mask) {
            this._lastSelectRange = this.getSelectionRange();
        }
        return this.Super("__handleSelect", arguments);
    },
    _fixMaskAfterCutPaste : function () {
        delete this._pendingFixMask;
        if (this.mask != null) {
            var currentMaskedValue = this._maskValue(this._value),
                currentElementValue = this.getElementValue();

            // The user might have just cut the entire value. If the current element value is
            // empty, then call clearValue() and return early.
            if (!currentElementValue) {
                this.clearValue();
                return;
            }



            var selection = this.getSelectionRange(),
                pasteLen = currentElementValue.length - currentMaskedValue.length,
                pasteEnd;
            if (currentMaskedValue != currentElementValue && this._lastSelectRange && this._lastSelectRange[0] < this._lastSelectRange[1]) {
                pasteLen += (this._lastSelectRange[1] - this._lastSelectRange[0]);

                if (isc.Browser.isWebKit && this._lastSelectRange[1] == currentMaskedValue.length) {
                    pasteEnd = selection[0] + pasteLen;
                } else {
                    pasteEnd = selection[0];
                }
            } else {
                pasteEnd = selection[0];

                if (isc.Browser.isWebKit && pasteEnd == currentElementValue.length) {
                    selection[1] = selection[0] = 0;
                    pasteEnd = pasteLen;
                }
            }
            var pasteStart = pasteEnd - pasteLen;

            this.setElementValue(currentElementValue.substring(0, pasteStart));
            this._setSelection(pasteStart);

            var replayString = currentElementValue.substring(pasteStart);
            for (var i = 0; i < replayString.length; ++i) {
                var c = replayString[i],
                    characterValue = replayString.charCodeAt(i);
                if (i == 0) this._setUpInsertCharacterValue(characterValue);
                this._insertCharacterValue(characterValue);
            }
        }
    },

    // by putting 'nowrap' on the text box cell we avoid the value icon / text box appearing
    // on different lines
    getTextBoxCellCSS : function () {
        return this._$nowrapCSS
    },

    //> @method textItem.setElementReadOnly()
    // Change the read-only state of the form element immediately.
    //<
    setElementReadOnly : function (readOnly) {
        // Text HTML element has readonly property
        this._setElementReadOnly(readOnly);
    },

    // NOTE: this is here for doc generation
    //>    @method textItem.keyPress        (A)
    //        @group    event handling
    //            event handler for keys pressed in this item
    //<

    //>    @method    textItem.getElementHTML()    (A)
    //            output the HTML for a text field element
    //        @group    drawing
    //        @param    value    (string)    Value of the element [Unused because it is more reliably set by setValue].
    //        @return    (HTML)    HTML output for this element
    //<
    _$elementStartTemplate:[
        ,                   // [0] possible value icon stuff
        "<INPUT TYPE=",         // [1]
        ,                       // [2] this._elementType,
        " NAME='",               // [3]
        ,                       // [4] this.getElementName(),
        "' ID='",                 // [5]
        ,                       // [6] this.getDataElementId(),
            // We want the EH system to handle events rather than writing native
            // handlers into the form item.
        "' handleNativeEvents=false" // [7]
    ],
    _$tabIndexEquals:" TABINDEX=",
    _$rightAngle:">",

    _$disabled:" DISABLED ",
    _$native:"native",
    _$autoCompleteOff:" AUTOCOMPLETE=OFF ",
    _$accessKeyEquals:" ACCESSKEY=",


    _writeOuterTable : function () {
        var writeOT = this.Super("_writeOuterTable", arguments);
        if (writeOT) return true;
        var iconHTML = this._getValueIconHTML(this.getValue());
        if (iconHTML != null && iconHTML != "") {
            return true;
        }
        return false;
    },

    getElementHTML : function (value, dataValue) {
        var valueIconHTML = this._getValueIconHTML(dataValue);
        if (this.showValueIconOnly) return valueIconHTML;

        var result;

        if (this._isPrinting() || this.renderAsStatic()) {
            if (this.printFullText) {
                result = isc.StringBuffer.concat(
                    "<SPAN ",this.getElementStyleHTML(), ">",
                    dataValue == null ? "&nbsp;" : dataValue.asHTML(), "</SPAN>"
                );
            } else {
                result = isc.StaticTextItem._instancePrototype.getElementHTML.apply(this, arguments);
            }
        } else {

            var template = this._$elementStartTemplate,
                form = this.form,
                formID = form.getID(),
                itemID = this.getItemID()
            ;

            // May be null
            template[0] = valueIconHTML;

            var inputType = this._elementType;
            if (this.inputDataType != null && this.browserInputType == null) {
                this.browserInputType = this.inputDataType;
            }
            if (this.browserInputType != null) {
                inputType = this.browserInputTypeMap[this.browserInputType];
                if (inputType == null) inputType = this.browserInputType;
            }

            template[2] = inputType;
            template[4] = this.getElementName();
            template[6] = this.getDataElementId();

            // hang a flag on the element marking it as the data element for the
            // appropriate form item.
            template[8] = this._getItemElementAttributeHTML();

            // At this point we're appending to the end of the template Disable spellchecker in
            // Moz if appropriate so we don't get the red wavy line under email addresses etc.


            if (isc.Browser.isMoz || isc.Browser.isSafari) {
                if (this.getBrowserSpellCheck()) template[template.length] = " spellcheck=true";
                else template[template.length] = " spellcheck=false"
            }

            // iPhone / Safari specific native features
            if (isc.Browser.isSafari) {
                if (this.browserAutoCapitalize == false) {
                    template[template.length] = " autocapitalize=off";
                }
                if (this.browserAutoCorrect == false) {
                    template[template.length] = " autocorrect=off";
                }
                if (this.browserInputType == "digits") {
                    template[template.length] = " pattern='\\d*'";
                }
            }

            // If we get an oninput event for this browser, write it out into our element's HTML


            if (this._willHandleInput() || (isc.Browser.isIE && isc.Browser.version >= 10)) {
                template[template.length] = " ONINPUT='"
                template[template.length] = this.getID()
                template[template.length] = "._handleInput()'"
            }

            template[template.length] = " ONSELECT='";
            template[template.length] = this.getID();
            template[template.length] = "._handleSelect()'";


            if (this.isDisabled() || (this._elementType == "FILE" && this.isReadOnly())) {
                template[template.length] = this._$disabled;
            }

            // Write out 'readOnly' setting if present
            if (this.isInactiveHTML() || this.isReadOnly()) {
                template[template.length] = " READONLY=TRUE";
                if (isc.screenReader) template[template.length] = " aria-readonly=true";
            }

            if (this.isInactiveHTML() && value != null && value != isc.emptyString) {
                // run the value through makeXMLSafe - this'll escape single quotes
                // / avoid terminating the tag if a '>' char is present etc
                template[template.length] = " value='" + isc.makeXMLSafe(value) + "'";
            }

            // disable native autoComplete

            if (this._getAutoCompleteSetting() != this._$native) {
                template[template.length] = this._$autoCompleteOff;
            }

            template[template.length] = this.getElementStyleHTML();


            var tabIndex = this._getElementTabIndex();
            if (tabIndex != null) {
                var end = template.length;
                template[end] = this._$tabIndexEquals;
                isc._fillNumber(template, tabIndex, end+1, 5);
            }

            // Note: if we're showing a title for the element, we don't need to set
            // up an accessKey here, since the label tag takes care of that
            if (this.showTitle == false && this.accessKey != null) {
                template[template.length] = this._$accessKeyEquals;
                template[template.length] = this.accessKey;
            }

            template[template.length] = this._$rightAngle;

            result = template.join(isc.emptyString);

            // Trim the entries off the end of the template so we can reuse it.
            template.length = 8;
        }
        //this.logWarn("generated textItem HTML:"+ result);

        return result;
    },


    _sizeTextBoxAsContentBox : function () {
        if (this._isPrinting()) {

            if (isc.Browser.isIE && !isc.Browser.isIE9) {
                return false;
            }
            return this.Super("_sizeTextBoxAsContentBox", arguments);
        }
        return isc.Browser.isStrict;
    },

    // override _nativeElementBlur() to fire blur and change handlers in response to a native
    // blur
    //
    // Natively onblur is fired when focus is taken from the text item, but onchange will
    // only fire if the value on leaving the text item is different from what it was when
    // the user put focus into the text item.
    //
    // Since we do internal values handling, having the same element value when focus is
    // taken from a form item as when focus first went to a form item is not a guarantee
    // that our stored value for the form item has not changed, and vice versa -
    // typically we are saving values in response to key events due to 'changeOnKeypress'.
    //
    // Therefore instead of relying on the native change handler, on blur we will always fire
    // our change handler if changeOnBlur is true, and otherwise compare our stored value to
    // the current element value, and fire the change handler if they do not match.


    _nativeElementBlur : function (element, itemID) {
        // On blur always call elementChanged. This falls through to updateValue() which
        // saves out the new value and fires change handlers [if the value has been modified].
        // If the value is unchanged, elementChanged() essentially no ops so we can always call
        // it here.

       if (this.form && !this.form._setValuesPending) {
           this.form.elementChanged(this);
       }

        var returnVal = this.Super("_nativeElementBlur", arguments);

        if (!this.isReadOnly() &&
            (this.formatOnFocusChange || this.mask != null || this._elementValueAtFocus == null ||
             this._elementValueAtFocus != this.getEnteredValue()))
        {
            this.refreshDisplayValue();
        }

        // If showing hint within data field, see if it should be shown now.
        if (this.showHintInField) {
            var undef;
            var value = this.getElementValue();
            if (value === undef || value == null || isc.is.emptyString(value)) {
                this._showInFieldHint();
            }
        }

        if (this._delayedSelect != null) {
            isc.Timer.clear(this._delayedSelect);
            this._delayedSelect = null;
        }
        return returnVal;
    },

    // Helper to refresh display value:
    // Call mapValueToDisplay() so we format the stored value to the appropriate display value.
    // Required if we have a mask
    // Also required if a developer has custom formatters/parsers that are not 1:1
    // [EG: A forgiving data parser allowing variants on a display format].
    // In this second case - compare the current element value to the element value when
    // the user put focus into the item and skip the call if they're unchanged.
    // This means we'll catch cases where the user has modified the display value (even if it
    // ultimately mapped back to the same data value) but shouldn't run the formatter when
    // the user simply tabbed through the field.

    refreshDisplayValue : function () {
        var value = this.getValue();
        if (this.mapValueToDisplay) {
            value = this.mapValueToDisplay(value);
        }
        if (!this.hasFocus && this.showHintInField && (value == null || value == "")) {
            this._showInFieldHint();
        } else {
            this.setElementValue (value);
        }
    },

    //>    @method    textItem.getElementStyleHTML()    (I)
    //          Get the HTML string used to set the visual characteristics for a text item.
    //          This includes the STYLE=... & CLASS=... properties to be written into this
    //          form item's element.
    //            This varies by platform, as we attempt to make Netscape think in pixels rather than
    //          characters and rows
    //
    //        @group    appearance
    //        @return    (string)    String of HTML containing STYLE=... & CLASS=... properties for
    //                          this items element.
    //
    //<
    _$styleTemplate:[
        " CLASS='",          // [0]
        ,                   // [1] this.getTextBoxStyle(),
        "' STYLE='",         // [2]
        ,                   // [3] null or 'width:'
        ,,,,                // [4-7] null or width
        ,                   // [8] null or 'px;'


        ,                   // [9] null or 'height:'
        ,,,,                // [10-13] null or height
        ,                   // [14] null or 'px;'

            // text align property, known to be supported in IE6 and Moz/Firefox on
            // Windows, not supported on Safari 1.2
        ,                   // [15] null or 'text-align'
        ,                   // [16] null or this.textAlign
        ,                   // [17] null or ";"

            // In Mozilla we must use the '-moz-user-focus' css property to govern
            // whether this element can recieve focus or not.
            // (slots 18 and 19)
        (isc.Browser.isMoz ? "-moz-user-focus:"

            :  null),    // [18]
        ,                   // [19] Moz: 'normal' or 'ignore' - otherwise null
        "' "                // [20]
    ],
    _$widthColon:"WIDTH:",
    _$pxSemi:"px;",
    _$heightColon:"HEIGHT:",
    _$textAlignColon:"text-align:",
    _$semi:";",
    _$normal:"normal;", _$ignore:"ignore;",

    _$negativeMargins:"margin-top:-1px;margin-bottom:-1px;",
    getElementStyleHTML : function () {

        // in 'printFullText' / printing mode we write out a span rather than
        // an input.
        // Most of the css will be the same but we can skip a few steps
        var isStaticElement = this._isPrinting() && this.printFullText;

        var template = this._$styleTemplate,


            width = this.getTextBoxWidth(),
            height = this.getTextBoxHeight(),
            style = this.getTextBoxStyle();

        template[1] = style;


        if (isc.isA.Number(width)) {
            template[3] = this._$widthColon;
            isc._fillNumber(template, width, 4, 4);
            template[8] = this._$pxSemi;
        } else {
            template[3] = template[4] = template[5] = template[6] =
                template[7] = template[8] = null;
        }

        if (isc.isA.Number(height)) {
            template[9] = this._$heightColon;
            isc._fillNumber(template, height, 10, 4);
            template[14] = this._$pxSemi;
        } else {
            template[9] = template[10] = template[11] = template[12] =
                template[13] = template[14] = null;
        }

        if (this.textAlign) {
            template[15] = this._$textAlignColon;
            template[16] = this.textAlign;
            template[17] = this._$semi;
        } else {
            template[15] = template[16] = template[17] = null;
        }
        if (isc.TextItem._needNegativeMargins) {
            template[18] = isStaticElement ? null : this._$negativeMargins;
        }
        if (isc.Browser.isMoz && !isStaticElement) {
            template[19] = (this._getElementTabIndex() > 0 ? this._$normal
                                                           : this._$ignore);
        }
        return template.join(isc.emptyString);
    },

    _getMeasureCanvas : function () {
        return isc.TextItem._measureCanvas ||
               (isc.TextItem._measureCanvas = isc.Canvas.create({
                    _generated: true,
                    top: -1000,
                    overflow: "visible",
                    autoDraw: true,
                    height: 1,
                    width: 1,

                    markForRedraw : function () {}
                }));
    },


    _$INPUT: "INPUT",
    _$TEXTAREA: "TEXTAREA",
    _getTextBoxScrollWidth : function (textBoxHandle, b, c, d) {
        var tagName = textBoxHandle.tagName;
        if ((tagName === this._$INPUT || tagName === this._$TEXTAREA) &&
            (isc.Browser.isIE10 || isc.Browser.isMoz))
        {
            var textBoxStyle = this.getTextBoxStyle(),
                elementValue = this.getElementValue();
            if (elementValue == null) elementValue = "";
            else elementValue = String(elementValue);
            if (this._cachedValueScrollWidthInfo == null ||
                this._cachedValueScrollWidthInfo.textBoxStyle != textBoxStyle ||
                this._cachedValueScrollWidthInfo.elementValue != elementValue)
            {
                var measureCanvas = this._getMeasureCanvas();
                if (measureCanvas.styleName != textBoxStyle) measureCanvas.setStyleName(textBoxStyle);
                measureCanvas.setContents("<span style='white-space:nowrap'>" + isc.makeXMLSafe(elementValue) + "</span>");
                measureCanvas.redraw("value scrollWidth measurement: " + elementValue);
                var width = measureCanvas.getScrollWidth(true);
                this._cachedValueScrollWidthInfo = {
                    width: width,
                    textBoxStyle: textBoxStyle,
                    elementValue: elementValue
                };
            }
            return this._cachedValueScrollWidthInfo.width;
        } else {
            return this.invokeSuper(isc.TextItem, "_getTextBoxScrollWidth", textBoxHandle, b, c, d);
        }
    },

    //> @method textItem.getEnteredValue()
    // Returns the raw text value typed into this form field, which can differ from
    // +link{formItem.getValue()} in various cases - for example:
    // <ul>
    // <li>for items that constrain the value range, such as a +link{DateItem} with
    // +link{DateItem.enforceDate,enforceDate}:true, or a +link{ComboBoxItem} with
    // +link{ComboBoxItem.addUnknownValues,addUnknownValues}:false</li>
    // <li>for items with a defined valueMap or edit value formatter and parser functions
    // which converts display value to data value</li>
    // <li>while the item has focus if +link{changeOnKeypress} is false
    // </li></ul>
    // @return (string) current entered value
    // @visibility external
    //<
    getEnteredValue : function () {
        return this.getElementValue();
    },


    //>@method textItem.mapValueToDisplay()  (A)
    // Map from the internal value for this item to the display value.
    // @param   internalValue   (string)   Internal value for this item.
    // @return  (string)   Displayed value corresponding to internal value.
    // @group   drawing
    //<
    mapValueToDisplay : function (internalValue) {
        if (this.mask) {
            // Map value to editable mask
            var x = this._getMaskBuffer();
            if (!this.hasFocus)
                x = this._maskValue(internalValue);
            return x;
        }

        var value = isc.FormItem._instancePrototype.mapValueToDisplay.call(this, internalValue);
        // always display the empty string for null values, rather than "null" or "undefined"
        if (value == null) return isc.emptyString;

        return value;
    },

    // Don't apply arbitrary formatters specified via SimpleType definitions to this item's
    // display value - we have no way to parse it back to a real data value
    applyStaticTypeFormat:false,

    //>@method textItem.mapDisplayToValue()  (A)
    // @group    drawing
    // Map from a the display value for this item to the internal value.
    // @param   displayValue   (string)   Value displayed to the user.
    // @return  (string)   Internal value corresponding to that display value.
    //<
    mapDisplayToValue : function (displayValue) {
        var value;

        if (this.mask) {
            value = this._unmaskValue(displayValue);
        } else {
            value = this._unmapKey(displayValue);
        }
        value = this._parseDisplayValue(value);

        // if the value to be saved is an empty string, map it to 'null' if necessary
        if (isc.is.emptyString(value)) value = this.emptyStringValue;
        return value;
    },

    // override 'saveValue' so new value can be mapped into mask if used.
    saveValue : function (value, isDefault) {

        // Save the new value into our mask buffer
        if (this.mask) this._maskValue (value);

        this.Super("saveValue", arguments);
    },

    // override 'setValue'.
    // If passed null or the empty string, we store this as the 'empty string value' - this will
    // then be returned whenever the user clears out the text item element.
    setValue : function (value,b,c,d) {



        // Make sure in-field hint is hidden
        this._hideInFieldHint();

        var undef;
        if (value !== undef && (value == null || isc.is.emptyString(value)))
            this.emptyStringValue = value;

        // Translate incoming value based on characterCasing if needed
        if (value !== undef && value != null && this.characterCasing != isc.TextItem.DEFAULT) {
            if (this.characterCasing == isc.TextItem.UPPER) {
                if(isc.isA.Array(value))
                    this.arrayToUpperCase(value);
                else
                    value = value.toUpperCase();
            } else if (this.characterCasing == isc.TextItem.LOWER) {
                if(isc.isA.Array(value))
                    this.arrayToLowerCase(value);
                else
                    value = value.toLowerCase();
            }
        }

        // Let parent take care of saving the value
        value = this.invokeSuper(isc.TextItem, "setValue", value,b,c,d);

        // See if the in-field hint needs to be shown
        if (!this.hasFocus && this.showHint && this.showHintInField && this.getHint()) {
            var elementValue = this.getElementValue();
            if (elementValue == null || isc.isAn.emptyString(elementValue)) {
                this._showInFieldHint();
            }

        // If there is a mask and the new value is empty, then update the editor caret position
        // if focused.
        } else if (this.hasFocus && this.mask && (value == null || isc.isAn.emptyString(value))) {
            this._setSelection(this._firstNonMaskPos);
        }

        return value;
    },

    arrayToUpperCase : function(value) {
        for (var i = 0; i < value.length; i++) {
            value[i] = value[i].toUpperCase();
        }
    },
    arrayToLowerCase : function(value) {
        for (var i = 0; i < value.length; i++) {
            value[i] = value[i].toLowerCase();
        }
    },

    // Override getCriteriaFieldName - if we have a displayField, return it rather than the
    // item name
    getCriteriaFieldName : function () {
        if (this.criteriaField) return this.criteriaField;
        if (this.displayField) return this.displayField;
        return this.Super("getCriteriaFieldName", arguments);
    },


    // When focus is received, the hint should be hidden if TextItem.showHintInField is true.
    _nativeElementFocus : function (element, itemID) {
        // if this focus came from a redraw, don't select- we want to retain the pre-redraw selection
        var refocusAfterRedraw = this._refocussingAfterRedraw;

        var returnVal = this.Super("_nativeElementFocus", arguments);

        // Hide in-field hint if being shown
        var wasShowingInFieldHintAsValue = this._showingInFieldHint;
        this._hideInFieldHint();

        // If this TextItem is readonly, don't change the selection.
        if (this.isReadOnly()) return returnVal;


        if (!wasShowingInFieldHintAsValue && this._mouseIsDown) return;

        // In IE there's a native bug whereby if you change an element value on focus,
        // you see the caret at the end of the new string, then jump to the beginning of
        // the new string.
        // Work around this by forcing the caret back to the end so the user doesn't get
        // surprised by this result

        var forceCaretToEnd;

        // If the special flag is set to refresh the display value on focus / blur, refresh the
        // display value
        // (Allows the user to specify a different value when the item has focus)
        if (this.formatOnFocusChange) {
            var elementValue = this.getElementValue();
            this.refreshDisplayValue()
            if (elementValue != this.getElementValue() && isc.Browser.isIE) {
                forceCaretToEnd = true;
            }
        }

        if (this.mask) {
            // Force buffer back into control so unfilled mask spaces
            // will be shown with the maskPromptChar
            this._saveMaskBuffer(false);

            // Determine caret position. By default the caret is placed on the next
            // unfilled mask position or at the end of the field. If selectOnFocus
            // is true, the entire field is selected.
            var begin = 0;
            var end = this._length;
            var selectOnFocus = !refocusAfterRedraw && this._shouldSelectOnFocus();
            if (!selectOnFocus) {
                begin = this._getEndPosition();
                var value = this.getValue();
                if (value != null && begin == 0 && value.length > 0) {
                    // there are no unfilled mask-positions, a value is present and the user
                    // clicked somewhere mid-value - stick the caret there with no selection
                    begin = this.getSelectionRange()[0];
                }
                end = begin;
            }
            this._delayedSelect = this.delayCall ("_delayed_setSelection", [begin, end], 50);

        // There may be custom parser / formatter logic applied to any text item and this
        // may not be a 1:1 mapping [EG a forgiving date format parser allowing variants on
        // a display format].
        // In this case the developer would typically specify changeOnKeypress:false [so as
        // not to break on partial values] and on blur expect values to be updated and
        // if necessary reformatted to the appropriate display value.
        // We don't want to run potentially expensive formatters if the user hasn't changed
        // the display value, so record the element value on focus and don't reformat
        // if its unchanged on blur implying the user just tabbed through the field or
        // edited, then reverted their edits.
        } else {

            var selectOnFocus = !refocusAfterRedraw && this._shouldSelectOnFocus();
            var value = this.getEnteredValue();
            if (selectOnFocus) {
                if (value != null) {
                   this._delayedSelect = this.delayCall("_delayed_selectValue");
                }
            } else if (forceCaretToEnd) {
                var elementValue = this.getElementValue();
                if (elementValue.length > 0) {
                    this.setSelectionRange(elementValue.length, elementValue.length);
                }
            }
            this._elementValueAtFocus = value;
        }

        return returnVal;
    },
    _delayed_setSelection : function (begin, end) {
        this._delayedSelect = null;
        // manipulating the selection changes focus, so don't do it if focus has moved on
        if (!this._hasNativeFocus()) return;
        this._setSelection(begin, end);
    },
    _delayed_selectValue : function () {
        this._delayedSelect = null;
        // manipulating the selection changes focus, so don't do it if focus has moved on
        if (!this._hasNativeFocus()) return;
        this.selectValue();
    },

    // Case conversion and keyPressFilter handling

    //> @attr   textItem.characterCasing   (CharacterCasing : isc.TextItem.DEFAULT : IRWA)
    // Should entered characters be converted to upper or lowercase?
    // Also applies to values applied with +link{formItem.setValue}.
    // <P>
    // Note: character casing cannot be used at the same time as a +link{textItem.mask}.
    // @example formFilters
    // @visibility  external
    //<
    characterCasing: isc.TextItem.DEFAULT,

    //> @attr   textItem.keyPressFilter   (string : null : IRWA)
    // Sets a keypress filter regular expression to limit valid characters
    // that can be entered by the user. If defined, keys that match the
    // regular expression are allowed; all others are suppressed. The
    // filter is applied after character casing, if defined.
    // <P>
    // Note: keypress filtering cannot be used at the same time as a +link{textItem.mask}.
    // @see textItem.characterCasing
    // @setter setKeyPressFilter
    // @example formFilters
    // @visibility  external
    //<

    //>@method textItem.setKeyPressFilter()
    // Set the keyPressFilter for this item
    // @param filter (string) new keyPress filter for the item
    // @visibility external
    //<
    setKeyPressFilter : function (filter) {
        if (this.mask) {
            this.logWarn("setKeyPressFilter() ignored because mask is enabled");
            return;
        }
        this.keyPressFilter = filter;
        this._keyPressRegExp = null;
        if (this.keyPressFilter) {
            this._keyPressRegExp = new RegExp (this.keyPressFilter);
        }
    },

    init : function() {
        this.Super("init", arguments);

        // Setup mask or keyPress filter

        if (this.mask) {
            if ((isc.ComboBoxItem && isc.isA.ComboBoxItem(this)) ||
                (isc.SpinnerItem && isc.isA.SpinnerItem(this)))
            {
                this.logWarn("item.mask is unsupported for this FormItem type. " +
                    "This item has mask specified as '" + this.mask + "' - ignoring.");
                this.mask = null;
            } else {
                this._parseMask ();
                if (this.keyPressFilter) {
                    this.logWarn("init: keyPressFilter ignored because mask is enabled");
                }
            }
        } else if (this.keyPressFilter) {
            this._keyPressRegExp = new RegExp (this.keyPressFilter);
        }
    },

    // Override handleKeyPress to implement character casing, keypress filter, and
    // masking.
    handleKeyPress : function (event, eventInfo) {
        if (this.Super("handleKeyPress", arguments) == false)
            return false;

        // If field is read-only, nothing more to do
        if (this.isReadOnly()) return true;

        var keyName = event.keyName;

        // Let standard key handling process this keyPress if
        // - Ctrl or Alt or Meta key is also pressed
        // - not performing case conversion, key press filtering or masked entry

        if ((isc.EventHandler.ctrlKeyDown() || isc.EventHandler.altKeyDown() || isc.EH.metaKeyDown()) &&
        (eventInfo.characterValue === null || eventInfo.characterValue < 128)) {
            if (this.mask &&
                (keyName.startsWith("Arrow_") || keyName == "Home" || keyName == "End") &&
                !isc.EH.shiftKeyDown())
            {
                delete this._lastSelectRange;
            }

            return true;
        }
        if ((!this.characterCasing || this.characterCasing == isc.TextItem.DEFAULT) &&
            !this._keyPressRegExp &&
            !this.mask)
        {
            return true;
        }

        var characterValue = event.characterValue
        ;

        // Perform in-field navigation and deletion
        if (this.mask) {
            var selection = this._getSelection();
            var isSafari = isc.Browser.isSafari;

            var pos = selection.begin;

            // Handle backspace and delete keys
            if (keyName == "Backspace" || keyName == "Delete") {
                // If there is a selection, these keys the result is identical
                if ((selection.begin - selection.end) != 0 ||
                    (isSafari && this._lastSelection))
                {
                    if (isc.Browser.isSafari && this._lastSelection) {
                        selection = this._lastSelection;
                        this._lastSelection = null;
                    }
                    if (this.maskOverwriteMode) {
                        this._clearMaskBuffer(selection.begin, selection.end);
                    } else {
                        //var len = selection.end - selection.begin + (isSafari ? 1 : 0);
                        var len = selection.end - selection.begin;
                        this._shiftMaskBufferLeft(selection.begin, len);
                    }
                    this._saveMaskBuffer(true);
                    this._positionCaret(selection.begin, 0);
                } else {
                    // No selection
                    if (keyName == "Backspace") {

                        var shiftPos = pos - 1;
                        if (shiftPos >= 0) {
                            if (this.maskOverwriteMode) {
                                while (!this._maskFilters[shiftPos] && shiftPos >= 0) shiftPos--;
                                this._maskBuffer[shiftPos] = this.maskPromptChar;
                            } else {
                                this._shiftMaskBufferLeft(shiftPos);
                            }
                            this._saveMaskBuffer(true);
                            this._positionCaret(shiftPos, -1);
                        }
                    } else {
                        if (this.maskOverwriteMode) {
                            // Don't clear a non-entry position
                            if (pos == this._getNextEntryPosition (pos - 1)) {
                                this._maskBuffer[pos] = this.maskPromptChar;
                            }
                        } else {
                            this._shiftMaskBufferLeft (pos);
                        }
                        this._saveMaskBuffer(true);
                        this._positionCaret(pos, 0);
                    }
                }
                return false;

            } else if ((keyName.startsWith("Arrow_") || keyName == "Home" || keyName == "End") &&
                       !isc.EH.shiftKeyDown())
            {
                delete this._lastSelectRange;
            }

            this._setUpInsertCharacterValue(characterValue);

            // Handle ESC key
            if (keyName == "Escape") {
                this._clearMaskBuffer(0, this._length)
                this._saveMaskBuffer(true);
                this._setSelection(this._firstNonMaskPos == null ? 0 : this._firstNonMaskPos);
                return false;
            }
        }

        return this._insertCharacterValue(characterValue);
    },

    _insertCharacterValue : function (characterValue) {
        var selection = this._getSelection(),
            pos = selection.begin;

        // Completely unhandled characters can be filtered
        if ((this.mask && !this._isTypableCharacter (characterValue)) ||
            (!this.mask && ((!this._keyPressRegExp && !this._isAlphaCharacter (characterValue)) ||
                            (this._keyPressRegExp && !this._isTypableCharacter (characterValue)))))
        {
            return true;
        }

        var c = String.fromCharCode (characterValue);

        if (this.mask) {
            // Get next typable position
            var p = this._getNextEntryPosition (pos - 1);
            if (p < this._length) {
                var filter = this._maskFilters[p];
                if (filter) {
                    // Perform character case changes
                    if (filter.casing) {
                        c = this._mapCharacterCase (c, filter.casing);
                    }

                    // Validate against the mask filter
                    if (filter.filter.test (c)) {
                        if (!this.maskOverwriteMode) this._shiftMaskBufferRight(p);
                        this._maskBuffer[p] = c;
                        var next = p;
                        if (this._saveMaskBuffer(true)) {
                            next = this._getNextEntryPosition(p);
                        }
                        this._setSelection(next);
                    }
                }
            }
            return false;
        }

        // Perform character case changes
        var nc = c;
        if (!this.mask) nc = this._mapCharacterCase(c, this.characterCasing);

        // If no conversion was performed and a key press filter is not registered,
        // revert to standard keyPress handling
        if (c == nc && !this._keyPressRegExp) return true;

        // Check keyPress filter to determine if entered character is valid
        if (this._keyPressRegExp) {
            if (this._isTypableCharacter(characterValue) && !this._keyPressRegExp.test(nc)) {
                // Keypress is not valid. Suppress it by telling keyPress
                // handler that we handled the character but do nothing with it.
                return false;
            }
        }

        // If we get this far, the character entered is valid.
        // However, if case conversion was not performed we are done.
        if (c == nc) return true;

        // Case-converted character needs to be added to the current value.
        // Using the current selection (or insertion point) write the new character.
        var value = this.getValue() || "";
        selection = this.getSelectionRange();

        if ((selection[0] - selection[1]) != 0) {
            value = value.substring(0, selection[0]) + nc + value.substring(selection[1] + 1);
        } else {
            value = value.substring(0, selection[0]) + nc + value.substring(selection[1]);
        }

        // Push new value to field and update caret position
        if (this.changeOnKeypress) {
            this.setElementValue(value);
            this.updateValue();
        } else {
            this.setValue(value);
        }
        this.setSelectionRange(selection[0] + 1, selection[0] + 1);


        // Don't process this keyPress event further
        return false;
    },

    _setUpInsertCharacterValue : function (characterValue) {
        if (this.mask) {
            var selection = this._getSelection();
            var isSafari = isc.Browser.isSafari;

            // If there is a selection, see if it should be cleared first
            if (this._isTypableCharacter (characterValue) &&
                ((selection.begin - selection.end) != 0 || (isSafari && this._lastSelection)))
            {
                if (isc.Browser.isSafari && this._lastSelection) {
                    selection = this._lastSelection;
                    this._lastSelection = null;
                }
                if (this.maskOverwriteMode) {
                    this._clearMaskBuffer(selection.begin, selection.end);
                } else {
                    var len = selection.end - selection.begin;
                    this._shiftMaskBufferLeft(selection.begin, len);
                }
            }

            // For Safari, save selection
            if (isSafari && (selection.begin - selection.end) != 0 &&
                !this._isTypableCharacter(characterValue))
            {
                this._lastSelection = selection;
            } else {
                this._lastSelection = null;
            }
        }
    },

    // Helper methods to determine valid typed characters
    _isTypableCharacter : function (characterValue) {
        return ((characterValue >= 32 && characterValue <= 126) || characterValue > 127);
    },
    _isAlphaCharacter : function (characterValue) {
        return (characterValue >= 65 && characterValue <= 90) ||
            (characterValue >= 97 && characterValue <= 122);
     },
    _mapCharacterCase : function (c, casing) {
        if (casing == isc.TextItem.UPPER) {
            c = c.toUpperCase();
        } else if (casing == isc.TextItem.LOWER) {
            c = c.toLowerCase();
        }
        return c;
    },

    //> @attr textItem.formatOnBlur (Boolean : false : IRW)
    // With <code>formatOnBlur</code> enabled, this textItem will format its value
    // according to any specified static +link{formItem.formatValue(),static formatter}
    // as long as the item does not have focus. Once the user puts focus into the item
    // the formatter will be removed. This provides a simply way for developers to
    // show a nicely formatted display value in a freeform text field, without the need
    // for an explicit +link{formItem.formatEditorValue()}
    // and +link{formItem.parseEditorValue()} pair.
    // @visibility external
    //<
    // Implemented at the FormItem level.

    //> @attr textItem.formatOnFocusChange (Boolean : false : IRW)
    // Should +link{textItem.formatEditorValue} re-run whenever this item recieves or loses focus?
    // Setting this property allows developers to conditionally format the display value based on
    // item.hasFocus, typically to display a longer, more informative string while the item does
    // not have focus, and simplifying it down to an easier-to-edit string when the user puts
    // focus into the item.
    // @visibility external
    //<
    formatOnFocusChange:false,

    //> @attr   textItem.mask   (string : null : IRWA)
    // Input mask used to filter text entry.
    // <P>
    // Sample masks:
    // <UL>
    // <LI>Phone number: (###) ###-####</LI>
    // <LI>Social Security number: ###-##-####
    // <LI>First name: &gt;?&lt;??????????</LI>
    // <LI>Date: ##/##/####</LI>
    // <LI>State: &gt;LL</LI>
    // </UL>
    // Overview of available mask characters
    // <P>
    // <table class="normal">
    // <tr><th>Character</th><th>Description</th></tr>
    // <tr><td>0</td><td>Digit (0 through 9) or plus [+] or minus [-] signs</td></tr>
    // <tr><td>9</td><td>Digit or space</td></tr>
    // <tr><td>#</td><td>Digit</td></tr>
    // <tr><td>L</td><td>Letter (A through Z)</td></tr>
    // <tr><td>?</td><td>Letter (A through Z) or space</td></tr>
    // <tr><td>A</td><td>Letter or digit</td></tr>
    // <tr><td>a</td><td>Letter or digit</td></tr>
    // <tr><td>C</td><td>Any character or space</td></tr>
    // <tr><td>&nbsp;</td></tr>
    // <tr><td>&lt;</td><td>Causes all characters that follow to be converted to lowercase</td></tr>
    // <tr><td>&gt;</td><td>Causes all characters that follow to be converted to uppercase</td></tr>
    // </table>
    // <P>
    // Any character not matching one of the above mask characters or that is escaped
    // with a backslash (\) is considered to be a literal.
    // <P>
    // Custom mask characters can be defined by standard regular expression character set
    // or range. For example, a hexadecimal color code mask could be:
    // <UL>
    // <LI>Color: \#>[0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F]</LI>
    // </UL>
    // <P>
    // Note: input mask cannot be used at the same time as a +link{textItem.keyPressFilter}.
    // Also note that this property is not supported for
    // +link{ComboBoxItem} or +link{SpinnerItem}.
    // @setter  textItem.setMask()
    // @see textItem.keyPressFilter
    // @example maskedTextItem
    // @visibility  external
    //<

    //> @attr   textItem.maskOverwriteMode   (boolean : null : IRWA)
    // During entry into masked field, should keystrokes overwrite current position value?
    // By default new keystrokes are inserted into the field.
    // @visibility  external
    //<

    //> @attr   textItem.maskSaveLiterals   (boolean : null : IRWA)
    // Should entered mask value be saved with embedded literals?
    // @visibility  external
    //<

    //> @attr   textItem.maskPadChar   (string : " " : IRWA)
    // Character that is used to fill required empty mask positions
    // to display text while control has no focus.
    // @visibility  external
    //<
    maskPadChar: " ",

    //> @attr   textItem.maskPromptChar   (string : "_" : IRWA)
    // Character that is used to fill required empty mask positions
    // to display text while control has focus.
    // @visibility  external
    //<
    maskPromptChar: "_",

    //> @method textItem.setMask ()
    // Set the mask for this item.  Pass null to clear an existing mask.
    // <P>
    // Note that the current value of the field is cleared when changing the mask.
    // @param mask (string) mask to apply to text item
    // @see textItem.mask
    // @visibility external
    //<
    setMask : function (mask) {
        if (isc.isA.ComboBoxItem(this) || isc.isA.SpinnerItem(this)) {
            return;
        }
        // Setup mask
        this.mask = mask;
        this._parseMask ();
        if (this.keyPressFilter) {
            this._keyPressRegExp = null;
            this.logWarn("setMask: keyPressFilter ignored because mask is enabled");
        }

        // Clear the field value
        this.setValue ("");
    },

    _parseMask : function () {
        var mask = this.mask;
        if (!mask) return;

        // Create an array of mask position checks for keyPress filtering.
        // Each entry will be an object holding the regular expression of
        // valid characters, whether the character should be converted to
        // upper of lower case, and if the character is required (for
        // validation).
        this._maskFilters = [];
        this._firstNonMaskPos = null;

        // This buffer holds the prompt characters and fixed characters from
        // the mask along with the characters entered by the user. It is
        // updated and then rewritten to the field when ready.
        this._maskBuffer = [];

        this._length = 0;

        // Current casing state
        var casing = null;     // no casing change
        // Current escape sequence state
        var escaping = false;
        // Are we processing a custom regex?
        var inRegex = false;
        // Current custom regex value
        var customRegex = "";

        // Build the mask filters and buffer
        for (var i = 0, numMaskChars = mask.length; i < numMaskChars; ++i) {

            var c = mask.charAt(i);

            if (!inRegex) {
                if (!escaping && c === "\\") {
                    escaping = true;
                } else if (escaping) {
                    this._addLiteralToMask(c, casing);
                    escaping = false;
                } else if (c === "<") {
                    // lowercase
                    casing = (casing == isc.TextItem.LOWER ? null : isc.TextItem.LOWER);
                } else if (c === ">") {
                    // uppercase
                    casing = (casing == isc.TextItem.UPPER ? null : isc.TextItem.UPPER);
                } else if (c === "[") {
                    // Start of custom regex
                    inRegex = true;
                    customRegex += c;
                } else {
                    this._addUnknownToMask(c, casing);
                }

            } else {

                if (c === "]") {
                    // End of custom regex
                    inRegex = false;
                    customRegex += c;
                    this._addCustomRegexToMask(customRegex, casing);
                    customRegex = "";

                } else {
                    // Building custom regex
                    customRegex += c;
                }
            }
        }

        // If we've reached the end of the mask, but are still escaping or within a regex...
        if (!inRegex) {
            if (escaping) {
                this.logWarn("Invalid mask syntax: Trailing backslash");
                this._addLiteralToMask("\\", casing);
                escaping = false;
            }
        } else {
            this.logWarn("Invalid mask syntax: Character class regular expression was not terminated");
            inRegex = false;
            customRegex += "]";
            this._addCustomRegexToMask(customRegex, casing);
            customRegex = "";
        }
    },

    _addLiteralToMask : function(c, casing) {
        this._maskFilters.push (null);
        this._maskBuffer.push (c);
        this._length++;
    },

    _addCustomRegexToMask : function (customRegex, casing) {
        this._maskFilters.add({
            filter: new RegExp(customRegex),
            casing: casing
        });
        if (this._firstNonMaskPos == null) {
            this._firstNonMaskPos = this._maskFilters.length - 1;
        }
        this._maskBuffer.push(this.maskPromptChar);
        this._length++;
    },

    _addUnknownToMask : function(c, casing) {
        // Define standard keypress filters
        var def = isc.TextItem._filterDefinitions[c];
        if (def) {
            this._maskFilters.push(
                { filter: new RegExp (def.charFilter),
                  casing: casing }
            );
            if (this._firstNonMaskPos == null) {
                this._firstNonMaskPos = this._maskFilters.length - 1;
            }
            this._maskBuffer.push (this.maskPromptChar);
        } else {
            // No filter defined for character. Assumed to be a literal.
            this._maskFilters.push (null);
            this._maskBuffer.push (c);
        }
        // Add to our length
        this._length++;
    },

    // Mask handling private helper methods
    // Selection handling wrapper methods
    _getSelection : function () {
        var range = this.getSelectionRange();
        if (range == null) range = [0,0];
        return { begin: range[0], end: range[1] };
    },
    _setSelection : function (begin, end) {
        // end parameter is optional. If not passed, it is matched to begin
        // to set the caret position.
        if (this.hasFocus) {
            end = (isc.isA.Number(end) ? end : begin);
            this.setSelectionRange(begin, end);
        }
    },
    // Get position of next user-entered character (i.e. non-literal)
    _getNextEntryPosition : function (pos) {
        while (++pos < this._length) {
            if (this._maskFilters[pos]) return pos;
        }
        return this._length;
    },
    // Get last unentered character position
    _getEndPosition : function () {
        var lastMatch = 0;
        for (var i = this._length-1; i >= 0; i--) {
            if (this._maskFilters[i]) {
                if (this._maskBuffer[i] == this.maskPromptChar)
                    lastMatch = i;
                else
                    break;
            }
        }
        return lastMatch;
    },
    // Map the stored value to the display (edit) format.
    // There are two ways a value can be stored: with literals and without.
    // If stored with literals, all entered characters and literals are mapped
    // directly into the mask.
    // If stored without literals the characters have to be placed into the
    // mask from left to right as if typed by the user.
    // When this control has focus, maskPromptChars are used to fill in unentered
    // characters in the mask. When focus is lost, these same characters are
    // replaced by the maskPadChar.
    //
    _maskValue : function (value) {
        if (value == null) value = "";
        if (!isc.isA.String(value)) value = value.toString();

        // Clear buffer contents of entered characters. All that is left are
        // the literals and maskPromptChars.
        this._clearMaskBuffer (0, this._length);

        // Keep up with the last character matched into the mask.
        var lastMatch = -1;

        // Merge value into buffer
        if (this.maskSaveLiterals) {
            // value should be a one-to-one match for mask
            for (var i = 0, pos = 0; i < value.length; i++) {
                if (this._maskFilters[i]) {
                    // Position expects user entry
                    var c = value.charAt(i);

                    // Map a space to maskPromptChar when focused.
                    // Or place entered character into buffer.
                    if (c == " " ) {
                        if (!this.hasFocus)
                            this._maskBuffer[i] = c;
                    } else if (this._maskFilters[i].filter.test(c)) {
                        this._maskBuffer[i] = c;
                        lastMatch = i;
                    }
                }
            }
        } else {
            // try to place characters into mask as if type manually.
            for (var i = 0, pos = 0; i < this._length; i++) {
                if (this._maskFilters[i]) {
                    while (pos < value.length) {
                        var c = value.charAt (pos++);

                        // If there is a space in this position, let it be
                        // replaced with the maskPromptChar because it can
                        // be entered.
                        var maskFilter = this._maskFilters[i];
                        if (c == " ") {
                            if (!this.hasFocus) {
                                this._maskBuffer[i] = (maskFilter.casing ? this._mapCharacterCase(c, maskFilter.casing) : c);
                            }
                            break;
                        } else if (maskFilter.filter.test(c)) {
                            this._maskBuffer[i] = (maskFilter.casing ? this._mapCharacterCase(c, maskFilter.casing) : c);
                            lastMatch = i;
                            break;
                        }
                    }
                    if (pos > value.length) break;
                }
            }
        }

        value = this._getMaskBuffer();
        if (!this.hasFocus) {
            // If there are literals after the last matched entry, include
            // those in display.
            if (lastMatch >= 0) {
                for (var i = lastMatch + 1; i < this._length; i++) {
                    if (this._maskFilters[i]) break;
                    lastMatch++;
                }
            }
            // Chop display value to remove trailing spaces
            value = value.substring (0, lastMatch + 1);
        }

        return value;
    },
    // Map the edit value to the stored format.
    _unmaskValue : function (value) {
        // Display should be in masked format. Convert it to desired output format.
        if (value == null) value = "";

        // We need to know if there is anything in the display value other
        // than literals. This way an empty value is produced when done.
        // The resulting value should also be chopped after the last entered
        // or literal character.
        var hasNonLiterals = false;
        var lastValidChar = -1;

        var newValue = "";
        for (var i = 0, pos = 0; i < value.length; i++) {
            var c = value.charAt (i);

            if (this._maskFilters[i]) {
                if (c != this.maskPromptChar && this._maskFilters[i].filter.test (c)) {
                    // Valid character at this position
                    newValue += c;
                    hasNonLiterals = true;
                    lastValidChar = pos++;
                } else {
                    // Invalid character
                    newValue += this.maskPadChar;
                    pos++;
                }
            } else if (this.maskSaveLiterals) {
                // Literal character
                newValue += c;
                lastValidChar = pos++;
            }
        }

        // Truncate result
        if (!hasNonLiterals) {
            newValue = "";
        } else {
            newValue = newValue.substring (0, lastValidChar + 1);
        }

        return newValue;
    },

    // Mask buffer helper methods
    _getMaskBuffer : function () {
        if (this._maskBuffer == null) return "";
        return this._maskBuffer.join('');
    },
    _clearMaskBuffer : function (start, end) {
        for (var i = start; i < end && i < this._length; i++) {
            if (this._maskFilters[i]) this._maskBuffer[i] = this.maskPromptChar;
        }
    },
    _saveMaskBuffer : function (changed) {
        // Update our saved value so a call to getValue() will return our
        // current edit value. Don't call setValue() because it requires
        // the unformatted value and then formats it. We already have a
        // formatted (display) value.
        var buffer = this._getMaskBuffer();

        // Show current display value
        this.setElementValue (buffer);

        if (changed && this.changeOnKeypress) {
            var value = this._unmaskValue(buffer);

            // fire the change handler, (handles validation etc)
            var returnVal = this.handleChange(value, this._value);
            // The change handler may call 'setItems' on the form (particularly likely in LG
            // editing) in which case we'll be destroyed

            if (this.destroyed) return;
            // Ensure we have the latest value (stored as this._changeValue)
            value = this._changeValue;
            // We may need to perform some visual updates based on the new value - do this here
            this.updateAppearance(value);
            // save the value
            this.saveValue (value);
            // fire any specifed 'changed' handler for this item.
            this.handleChanged(value);

            return returnVal;
        }
        return true;
    },
    // Position caret at offset in field
    _positionCaret : function (pos, offset) {
        if (offset < 0) {
            while (!this._maskFilters[pos] && pos >= 0) pos--;
        } else {
            while (!this._maskFilters[pos] && pos < this._length) pos++;
        }
        this._setSelection (pos);
    },
    // Shift contents of buffer to left starting at <pos>
    _shiftMaskBufferLeft : function (pos, len) {
        // Skip any user-entered positions to find left-most position to
        // receive shifted contents.
        if (!len) len = 1;
        while (!this._maskFilters[pos] && pos >= 0) pos--;

        // Move each character <len> positions to the left where the character
        // matches the new position's filter.
        for (var i = pos, pos2 = i+len-1; i < this._length; i++, pos2 = j) {
            if (this._maskFilters[i]) {
                this._maskBuffer[i] = this.maskPromptChar;
                var j = this._getNextEntryPosition (pos2++);
                var filter = this._maskFilters[i];
                var c = this._maskBuffer[j];
                if (j < this._length && filter.filter.test (c)) {
                    // Perform character case changes
                    if (filter.casing) {
                        c = this._mapCharacterCase (c, filter.casing);
                    }
                    this._maskBuffer[i] = c;
                } else {
                    while (i < j) {
                        if (this._maskFilters[i]) this._maskBuffer[i] = this.maskPromptChar;
                        i++;
                    }
                    break;
                }
            }
        }
    },

    // Shift contents of buffer to right starting at <pos>
    _shiftMaskBufferRight : function (pos) {
        for (var i = pos, c = this.maskPromptChar; i < this._length; i++) {
            var filter = this._maskFilters[i];
            if (filter) {
                // Perform character case changes
                if (filter.casing) {
                    c = this._mapCharacterCase (c, filter.casing);
                }
                var j = this._getNextEntryPosition (i);
                var t = this._maskBuffer[i];
                this._maskBuffer[i] = c;
                if (j < this._length && this._maskFilters[j].filter.test (t)) {
                    c = t;
                } else {
                    break;
                }
            }
        }
    }

});








//>    @class    IntegerItem
// FormItem intended for input integer numbers.
//
// @visibility external
//<
isc.ClassFactory.defineClass("IntegerItem", "TextItem");
isc.IntegerItem.addProperties({

    type: "integer"
});
 



//> @class FloatItem
// <p>
// A TextItem for managing a text field that displays a floating point value.  FloatItem is the
// default FormItem if the +link{formItem.type} is "float".
// <p>
// FloatItem displays its value according to the +link{formItem.decimalPrecision} and
// +link{formItem.decimalPad} properties of the FormItem.  While the value is being edited,
// the item will display the value with its original precision and without extra zero-padding.
// </p>
//
// @group gwtFloatVsDouble
// @visibility external
//<
isc.ClassFactory.defineClass("FloatItem", "TextItem");

isc.FloatItem.addProperties({
// A boolean flag to store whether the item is currently displaying an editor.  This is
// used in the override of mapValueToDisplay() to display the full floating-point value while
// in "editor mode" and a formatted version of that value (where the format is specified by
// decimalPrecision and decimalPad) outside of "editor mode".
_inEditorMode: false,

type: "float"
});

isc.FloatItem.addMethods({

    // Note: similar code appears in StaticTextItem
    mapValueToDisplay : function (value) {
        if (!this._inEditorMode) {
            var floatValue = null;
            if (isc.isA.String(value)) {
                var parsedValue = window.parseFloat(value);
                if (!window.isNaN(parsedValue) && parsedValue == value) {
                    floatValue = parsedValue;
                }
            } else if (isc.isA.Number(value)) {
                floatValue = value;
            }
            if (floatValue != null) {
                if (this.decimalPrecision != null || this.decimalPad != null) {
                    return isc.Canvas.getFloatValueAsString(floatValue,
                        this.decimalPrecision, this.decimalPad);
                } else if (this.precision != null) {
                    return isc.Canvas.getNumberValueAsString(floatValue, 
                        this.precision, "float");
                }
            }
        }
        return this.Super("mapValueToDisplay", arguments);
    },

    handleEditorEnter : function () {
        this.Super("handleEditorEnter", arguments);

        this._inEditorMode = true;
        var value = this.getValue(),
            displayValue = this.mapValueToDisplay(value);
        this.setElementValue(displayValue, value);
    },

    handleEditorExit : function () {
        this.Super("handleEditorExit", arguments);

        this._inEditorMode = false;
        var value = this.getValue(),
            displayValue = this.mapValueToDisplay(value);
        this.setElementValue(displayValue, value);
    }
});

 



//> @groupDef gwtFloatVsDouble
// In GWT code, you should generally use Java Doubles rather than Java Float values.
// <p>
// In the current implementation of GWT, Float and Double types are both represented as
// JavaScript Number, so there is no storage or performance advantage to using Float over
// Double, and double provides higher precision.
// <p>
// In addition, because GWT uses true Java Floats <i>in development mode</i> but uses higher
// precision JavaScript Number values in compiled mode, math operations on Float can differ
// between development mode vs compiled mode.
// <p>
// The SmartGWT field type "float" is represented as a JavaScript Number, the same storage GWT
// uses for Doubles, so in any code that accesses or manipulates values stored in a field of
// type "float", use Record.getAttributeAsDouble(), DoubleItem.getValueAsDouble(), and similar
// APIs to avoid being tripped up by GWT's different behavior in development mode.
//
// @title Float vs Double
// @visibility sgwt 
//<

//> @method DoubleItem.getValueAsDouble()
// Return the value tracked by this form item.
// @return (Double) the value of the form, converted to a double.
// @visibility external
//<

//> @class DoubleItem
//TextForm item for managing a text field that displays a decimal value.
//@visibility external
//<
isc.ClassFactory.defineClass("DoubleItem", "FloatItem");








//>    @class    BlurbItem
// FormItem intended for inserting blurbs of instructional HTML into DynamicForms.
// <p>
// Set the <code>defaultValue</code> of this item to the HTML you want to embed in the form.
// @visibility external
//<
isc.ClassFactory.defineClass("BlurbItem", "FormItem");
isc.BlurbItem.addProperties({
    // avoid attempting to save this item in the form's values array
    shouldSaveValue:false,

    //>    @attr    blurbItem.height        (boolean : false : IRW)
    //            don't specify a height so the table cell will expand
    //            to show the entire contents.  Note that this can
    //            mess up dynamic height calculations in forms.
    //        @group    appearance
    //<
    height:null,

    //>    @attr    blurbItem.showTitle        (Boolean : false : IRW)
    // Blurb items show no title by default.
    //        @group    appearance
    // @visibility external
    //<
    showTitle:false,

    //>    @attr    blurbItem.colSpan        (number : "*" : IRW)
    // By default, texts span all remaining columns
    //        @group    appearance
    // @visibility external
    //<
    colSpan:"*",

    //>    @attr    blurbItem.startRow        (boolean : true : IRW)
    // These items are in a row by themselves by default
    //        @group    appearance
    //<
    startRow:true,

    //>    @attr    blurbItem.endRow            (boolean : true : IRW)
    // These items are in a row by themselves by default
    //        @group    appearance
    //<
    endRow:true,

    //>    @attr    blurbItem.textBoxStyle     (CSSStyleName : "staticTextItem" : IRW)
    //  Base css style for this item.
    //  @group    appearance
    //  @visibility external
    //<
    textBoxStyle:"staticTextItem",

    //> @attr blurbItem.canSelectText (boolean : true : IRW)
    // Should the user be able to select the text in this item?
    // @visibility external
    //<
    canSelectText:true,


    // override emptyDisplayValue to show &nbsp; so styling will work properly
    emptyDisplayValue:"&nbsp;",

    //>    @attr    blurbItem.wrap        (boolean : null : IRW)
    // @include FormItem.wrap
    //        @group    appearance
    // @visibility external
    //<

    //>@attr    blurbItem.clipValue (Boolean : false : IRW)
    // @include FormItem.clipValue
    // @group appearance
    // @visibility external
    //<
    clipValue:false

});





//>    @class    ButtonItem
// FormItem for adding a Button to a form.
// @visibility external
//<
isc.ClassFactory.defineClass("ButtonItem", "CanvasItem");

isc.ButtonItem.addProperties({
    // Override canFocus -- even though buttons have no data element, they can accept focus.
    canFocus:true,

    // avoid attempting to include this item in the form's values array
    shouldSaveValue:false,

    //>    @attr    buttonItem.height        (number : null : IRW)
    // By default buttonItems are sized to match their content (see +link{ButtonItem.autoFit}).
    // Specifying an explicit size for the button will disable this behavior.
    // @group appearance
    // @visibility external
    //<
    height:null,

    //>    @attr    buttonItem.width    (number : null : IRW)
    // By default buttonItems are sized to match their content (see +link{ButtonItem.autoFit}).
    // Specifying an explicit size for the button will disable this behavior.
    // @group appearance
    // @see buttonitem.autoFit
    //<
    width:null,

    //>    @attr    buttonItem.baseStyle        (CSSStyleName : null : IRW)
    // Optional <code>baseStyle</code> will be applied to the button.
    // @group appearance
    // @visibility external
    //<
    //baseStyle:null,

    //>    @attr    buttonItem.icon     (SCImgURL : null : IR)
    // Optional icon image to display on the button for this item.  See +link{button.icon}.
    // @group    appearance
    // @visibility external
    //<
    //icon :  null

    //>    @attr    buttonItem.titleStyle       (CSSStyleName : null : IRW)
    //  Optional CSS class to apply to the button title.
    //        @group    appearance
    //      @visibility internal
    //<

    titleStyle:null,

    //>    @attr    buttonItem.showTitle        (Boolean : false : IRW)
    // Buttons do not show a title by default.
    //        @group    appearance
    // @visibility external
    //<
    showTitle:false,

    //>    @attr    buttonItem.startRow        (Boolean : true : IRW)
    // These items are in a row by themselves by default
    // @group formLayout
    // @visibility external
    //<
    startRow:true,

    //>    @attr    buttonItem.endRow            (Boolean : true : IRW)
    // These items are in a row by themselves by default
    // @group formLayout
    // @visibility external
    //<
    endRow:true,

    //>    @attr    buttonItem.buttonConstructor      (Class : isc.Button : IRA)
    //      Constructor class for the button.
    // @visibility external
    //<
    buttonConstructor : isc.Button,

    //> @attr buttonItem.autoFit (Boolean : true : IR)
    // Should the button auto fit to its title. Maps to +link{isc.Button.autoFit} attribute.
    // Note that if an explicit width or height is specified for this item, it will be respected,
    // disabling autoFit behavior
    // @visibility external
    //<
    // We could have autoFit override the specified size properties instead of vice versa, but
    // this behavior gives us backwards compatibility (for example Button items sized to "*" will
    // not fill the available row without needing to also change the value of item.autoFit), and
    // it more closely matches the behavior of StatefulCanvas.autoFit, which is disabled once
    // setWidth() / setHeight() is called
    autoFit:true,

    //>    @attr    buttonItem.buttonDefaults   (Object : { ... } : IRA)
    //  Class level default properties to apply to our button item.
    //  Modify 'buttonProperties' at the instance level rather than modifying this object.
    //<
    buttonDefaults : {
        getTitle : function () { return this.canvasItem.getTitle(); }
    },

    autoDestroy: true,

    //> @attr buttonItem.buttonProperties (Object : null : IRA)
    // Custom Properties to apply to our button item.
    // @visibility external
    //<
    //buttonProperties : null

    //> @attr buttonItem.readOnlyDisplay (ReadOnlyDisplayAppearance : "disabled" : IRW)
    // If +link{formItem.canEdit} is set to <code>false</code>, how should this item
    // be displayed to the user?
    // <P>
    // The default value for ButtonItems is <code>disabled</code> - note that
    // <code>readOnly</code> and <code>static</code> appearances have no effect for ButtonItems
    //
    // @visibility external
    //<
    readOnlyDisplay: "disabled"


});

isc.ButtonItem.addMethods({

    //> @method buttonItem.click
    // Called when a ButtonItem is clicked on.
    //
    // @param    form    (DynamicForm) the managing DynamicForm instance
    // @param    item    (FormItem)    the form item itself (also available as "this")
    // @return (boolean) Return false to cancel the click event. This will prevent the event from
    //   bubbling up, suppressing
    //   +link{canvas.click,click} on the form containing this item.
    // @group eventHandling
    // @visibility external
    //<
    // NOTE: actually registered as a StringMethod on FormItem


    // Override getTitleHTML to return the title as text, rather than the HTML title with
    // <LABEL> tag and underlined accessKey
    getTitleHTML : function () {
        return this.getTitle();
    },

    //>    @method    buttonItem.setTitle()
    // Set the title.
    // @group    appearance
    // @param    newTitle    (string)    new title
    // @visibility external
    //<
    setTitle : function (title) {
        this.title = title;
        if (this.canvas) this.canvas.setTitle(title);
    },

    // Override _createCanvas to set up a Button as this item's canvas, with appropriate
    // properties.
    _createCanvas : function () {
        var dynamicButtonProperties = {
                canFocus : this._canFocus(),
                disabled : this.isDisabled(),

                width:this.width
            };
        if (this.height != null) dynamicButtonProperties.height = this.height;

        // Button-specific properties
        if (this.icon) dynamicButtonProperties.icon = this.icon;
        if (this.titleStyle) dynamicButtonProperties.titleStyle = this.titleStyle;
        if (this.baseStyle) dynamicButtonProperties.baseStyle = this.baseStyle;
        if (this.autoFit != null) dynamicButtonProperties.autoFit = this.autoFit;

        // Use 'addAutoChild' - this will handle applying the various levels of defaults
        // Note: also assign this.button to enable AutoTest getAutoChildLocator() to find this child
        this.canvas = this.button = this.createAutoChild("button", dynamicButtonProperties,
                                                                   this.buttonConstructor);
        this.Super("_createCanvas", arguments);

    },

    // if the button is auto-fitting to its content, avoid applying an explicit size
    // which would disable autoFit

    _setCanvasSize : function (width,height,c,d) {
        if (width == null && height == null) return;
        return this.invokeSuper(isc.ButtonItem, "_setCanvasSize", width,height,c,d);
    },

    readOnlyDisplayChanged : function (appearance) {
        if (this.button) {
            this.button.setDisabled(this.isReadOnly());
        }
    },

    canEditChanged : function (canEdit) {
        if (this.button) {
            this.button.setCanEdit(!this.isReadOnly());
            this.button.setDisabled(this.isDisabled());
        }
    }

    //>EditMode
    ,
    _passthroughProps : {
        width:true, height:true, icon:true
    },
    propertyChanged : function (propertyName, value) {
        if (this.canvas != null && this._passthroughProps[propertyName]) {
            this.canvas.setProperty(propertyName, value)
        }
    },
    handleClick : function () {
        if (this.editingOn) {
            if (isc.VisualBuilder && isc.VisualBuilder.titleEditEvent == "click") this.editClick();
            return false;
        }
        return this.Super("handleClick", arguments);
    },
    handleDoubleClick : function () {
        if (this.editingOn) {
            if (isc.VisualBuilder && isc.VisualBuilder.titleEditEvent == "doubleClick") this.editClick();
            return false;
        }
        return this.Super("handleDoubleClick", arguments);
    },

    _shouldAllowExpressions : function () {
        return false;
    }

    //<EditMode


});



// Class will not work without the ListGrid
if (isc.ListGrid) {



//>    @interface  PickList
// Interface to show a drop-down list of pickable options. Used by the +link{SelectItem} and
// +link{ComboBoxItem} classes. The generated drop down list of options will be an instance of
// +link{PickListMenu}.
// @treeLocation Client Reference/Forms/Form Items
// @visibility external
//<

isc.ClassFactory.defineInterface("PickList");

//>    @class PickListMenu
// +link{class:ListGrid} subclass used by FormItems which implement
// +link{interface:PickList} to display
// a list of selectable options.
//  @treeLocation Client Reference/Forms/Form Items
// @visibility external
//<


isc.ClassFactory.defineClass("PickListMenu", "ScrollingMenu");

isc.PickListMenu.addClassProperties({
    // object to hold cached pickListMenu instances for databound pickList items
    _cachedDSPickLists:{},
    // Don't cache more than 50 DS pickListMenus
    pickListCacheLimit:50
});

isc.PickListMenu.addProperties({

    // explicitly doc the default pickList empty message (may be overridden by pickListEmptyMessage
    // on a per-instance basis too)
    //> @attr PickListMenu.emptyMessage (string : "No Items To Show" : IRW)
    // @include ListGrid.emptyMessage
    // @visibility external
    // @group i18nMessages
    //<

    // explicitly doc dataProperties - useful to be able to customzie
    //> @attr PickListMenu.dataProperties
    // @include ListGrid.dataProperties
    // @visibility external
    //<

    // Don't get fields from the DS.
    useAllDataSourceFields:false,

    // disallow tabbing to the pickList
    tabIndex:-1,

    // Don't allow fields to be resized, if multiple are showing

    canResizeFields:false,
    // Since we don't support drag resize of fields, turn canFreezeFields off by default
    canFreezeFields:false,

    //> @attr pickListMenu.styleName (CSSStyleName : "pickListMenu" : IRW)
    // @include listGrid.styleName
    // @visibility external
    //<
    styleName:"pickListMenu",
    //> @attr pickListMenu.bodyStyleName (CSSStyleName : "pickListMenuBody" : IRW)
    // @include listGrid.bodyStyleName
    // @visibility external
    //<
    bodyStyleName:"pickListMenuBody",


    //> @attr pickListMenu.normalCellHeight (number : 16 : IRWA)
    // @include listGrid.normalCellHeight
    //<
    normalCellHeight:16,

    // When we select a row, scroll it such that it centers the row vertically but
    // has the viewport scrolled left.
    defaultScrollCenterX:false
});

isc.PickListMenu.addMethods({
    // Pick up valueIcons from this form item, if specified
    getValueIcon : function (field, value, record) {
        var formItem = this.formItem;

        // If this is a databound pickList, we have 2 sets of valueIcons to consider -
        // those specified by the formItem and those specified on the field definition itself.
        // - If the form item specifies any valueIcons, we typically want to show them on the
        //   column that matches the display-value for the formItem.
        //   - This will be the result of formItem.getDisplayField() or formItem.getValueField()
        // - If the developer specified explicit pickListFields for this item, this field may
        //   not be showing. In this case we allow the developer to explicitly specify a
        //   field to show the form item valueIcons via the property "formItem.valueIconField".
        //   If this property is set, always respect it.
        // - For all other fields, and if the form item has no custom valueIcons, just fall
        //   through to the standard ListGrid implementation, so we can pick up valueIcons
        //   specified on the datasource fields.
        var hasCustomValueIcons = formItem && !formItem.suppressValueIcons &&
                                  (formItem.valueIcons != null || formItem.getValueIcon !=null);

        if (hasCustomValueIcons) {
            var valueField = formItem.getValueFieldName(),
                valueIconField = formItem.valueIconField ||
                                 formItem.getDisplayFieldName() || valueField;
            if (this.getFieldName(field) == valueIconField) {
                // The form item expects the value passed to getValueIcon to be the 'valueField'
                // value, not the value from whatever field is being displayed in the pickList
                return formItem._getValueIcon(record[valueField]);
            }
        }

        return this.Super("getValueIcon", arguments);
    },

    // arrowKeyAction - for single selects we change selection as the user moves
    // around with arrow keys
    // for multi select, we change hilight only and allow the user to select with
    // "space" keypress
    getArrowKeyAction : function () {
        return this.allowMultiSelect ? "focus" : "select";
    },

    // showOverAsSelected - will remap all "Over" styles to use the
    // "Selected" class name.

    showOverAsSelected:true,

    // override rowClick / recordClick - we don't want to hide on keyboard navigation
    // Note that generateClickOnSpace is set up dynamically as
    // part of the 'show' override.
    rowClick : function (record, recordNum, fieldNum, keyboardGenerated) {
        this._keyboardRowClick = keyboardGenerated;
        this.Super("rowClick", arguments);
        delete this._keyboardRowClick;
    },
    recordClick : function (viewer,record,recordNum,field,fieldNum,value,rawValue) {
        var shouldDismiss = !this.allowMultiSelect;
        if (this._keyboardRowClick) {
            // Nothing to do here on Space key press.
            // On Arrow key press we should update the 'aria-activedescendant' attribute on
            // the formItem if running in screen reader mode.

            var isEnter = (isc.EH.getKey() == "Enter");
            if (!isEnter) {
                var formItem = this.formItem;
                if (isc.Canvas.ariaEnabled() &&
                    formItem != null &&
                    formItem.ariaRole === "combobox" &&
                    this.body != null)
                {
                    var startRow = this.body.getDrawArea()[0];
                    var rowElementId = this.getRowElementId(recordNum, recordNum - startRow);
                    formItem.setAriaState("activedescendant", rowElementId);
                }
                return;
            }
            // On Enter keypress, always dismiss the pickList
            shouldDismiss = true;
        }

        // hide before firing itemClick.
        // This avoids issues with focus, where the itemClick action is expected to put focus
        // somewhere that is masked until this menu hides.
        if (shouldDismiss) this.hide();
        // itemClick handles updating the formItem with the selected value(s) from the
        // pickList.

        if (record != null) this.itemClick(record);
    },

    // This method is fired when the user hits Space or Enter and
    // generateClickOnSpace/Enter is set to determine whether we should toggle
    // selection on the row.
    // Overridden to disable toggling selection on Enter keypress in a multi-select
    // pickList, since we dismiss the pickList in this case and it'd be confusing to
    // have the highlighted value be toggled when the user is really attempting
    // to accept the current selection and dismiss the pickList.
    selectOnGeneratedCellClick : function (rowNum, colNum, body) {
        if (this.allowMultiSelect && isc.EH.getKey() == "Enter") return false;
        return this.Super("selectOnGeneratedCellClick", arguments);
    },

    headerClick : function (fieldNum, header) {
        var rv = this.Super("headerClick", arguments);
        var field = this.getField(fieldNum);
        // check if the checkbox column header was clicked
        if (this.isCheckboxField(field) && this.allowMultiSelect) {
            this.multiSelectChanged();
        }
        return rv;
    },

    multiSelectChanged : function () {
        var formItem = this.formItem,
            fieldName = formItem.getValueFieldName(),
            sel = this.getSelection(),
            empty = true,
            values = [];

         for (var i = 0; i < sel.length; i++) {
            empty = false;
            var currSel = sel[i];
            values.add(currSel[fieldName]);
         }
         formItem.pickValue(empty ? null : values);
    },

    // 'pick' the selected value on click.  Matches Windows ComboBox behavior
    itemClick : function (record) {
        if (this.allowMultiSelect) {
            this.multiSelectChanged();
        } else {
            var formItem = this.formItem,
                fieldName = formItem.getValueFieldName();
             var value = record[fieldName];
            formItem.pickValue(value);
        }
    },

    hide : function (a,b,c,d) {
        var isVisible = this.isVisible() && this.isDrawn();

        this.invokeSuper(isc.PickListMenu, "hide", a,b,c,d);
        // If we're being hidden as part of a formItem.destroy(), this.formItem will have been
        // cleared out.
        if (!this.formItem) return;

        // put focus back in the item if this was a modal pickList
        if (isVisible && this.showModal) this.formItem.focusInItem();

        // Clear out the showingPickList flag
        this.formItem._showingPickList = null;

        // fire a notification for observing / overriding the pick list being hidden
        if (isVisible) this.formItem._pickListHidden();


        delete this.formItem._showOnFilter;

        // Clear the last hilite to reset the "last position" for the purpose
        // of arrow key navigation. Once the PickList is hidden, we don't want
        // it to remember where it was last if re-shown.
        this.clearLastHilite();
    },

    show : function () {
        // If the pickList is already showing we could arguably bail here, but this isn't
        // how Canvas.show() behaves (still calls 'setVisibility()')...
        // Instead we'll just avoid firing the _pickListShown notification function
        var alreadyShowing = this.isVisible() && this.isDrawn();

        // keyboard generated click handling:
        // "Enter" keypress always generates the "click" event.
        // This accepts the selected values and dismisses the editor
        this.generateClickOnEnter = true;

        // "Space" keypress
        // - no action for single-selects (selection already achieved
        //   on arrow keypress)
        // - Toggles selection for multi-selects, without dismissing the editor
        this.generateClickOnSpace = this.allowMultiSelect;
        // See also recordClick modifications / selectOnGeneratedCellClick


        this.bringToFront();
        this.Super("show", arguments);
        // fire a notification for observing / overriding the pick list being shown
        if (!alreadyShowing) {
            this.formItem._pickListShown();
        }
    },

    // Override showClickMask() - if this is a modal PickList, ensure that when the pickList is
    // hidden focus goes to the form item that spawned the pickList.
    showClickMask : function () {
        if (!this.clickMaskUp(this.getID())) {
            // Actually cmID will match this.getID() since that's the default ID for a CM
            // associated with a widget.
            var cmID = this.Super("showClickMask", arguments);
            if (this.formItem) {
                var form = this.formItem.form,
                    mask = isc.EH.clickMaskRegistry.find("ID", cmID);

                // Suppress the default behavior of putting focus into whatever had focus before
                // this CM was shown. We'll explicitly put focus into the appropraite item when
                // hiding the pickList
                if (mask._maskedFocusCanvas) mask._maskedFocusCanvas = null;
            }
        }
    },

    _$_backgroundColor:"background-color:",
    _$_color:"color:",
    getCellCSSText : function (record, rowNum, colNum) {

        // if it's selected apply the hilite color, if specified
        // Otherwise we rely on regular css class type styling.
        if (record == this.selection.getSelectedRecord()) {
            var cssText = [];
            if (this.hiliteColor != null) {
                cssText[0] = this._$_backgroundColor;
                cssText[1] = this.hiliteColor;
                cssText[2] = isc.semi;
            }

            if (this.hiliteTextColor != null) {
                cssText[3] = this._$_color;
                cssText[4] = this.hiliteTextColor;
                cssText[5] = isc.semi;
            }
            return cssText.join(isc.emptyString);
        }
    },

    // override keyDown to catch tabs and hide the pickList.


    keyDown : function () {
        var keyName = isc.EH.lastEvent.keyName;
        if (keyName == "Tab") {
            this.hide();
            return false;
        }
    },

    // Override _formatCellValue to call formatPickListValue() if defined (allowing for customized
    // formatting within the pickList).

    _formatCellValue : function (value, record, field, rowNum, colNum) {
        if (this.formItem == null) return this.Super("_formatCellValue", arguments);

        var fieldName = this.getFieldName(colNum);
        value = this.formItem.formatPickListValue(value, fieldName, record);

        return this.Super("_formatCellValue", [value,record,field,rowNum,colNum]);
    },

    // override keyPress to allow for navigation to different items by typing
    // the first letter of the option.
    bodyKeyPress : function (event, eventInfo) {
        var keyName = isc.EH.lastEvent.keyName;

        // Catch shift+tab in safari in keyPress rather than keydown
        if (isc.Browser.isSafari) {
            if (keyName == "Tab") {
                this.hide();
                return false;
            }
        }

        var charVal = isc.EH.getKeyEventCharacterValue();
        if (charVal != null) {

            var data = this.formItem.getAllLocalOptions();

            if (isc.isAn.Array(data) && data.length > 1) {

                var typedChar = String.fromCharCode(charVal),
                    // Normalize to a lowercase string for comparison.
                    typedChar = typedChar.toLowerCase(),
                    formItem = this.formItem,
                    valueField = formItem.getValueFieldName(),
                    currentIndex = data.indexOf(this.getSelectedRecord()),
                    newIndex = currentIndex < (data.length -1) ? currentIndex + 1 : 0;

                while (newIndex != currentIndex) {
                    if (currentIndex < 0) currentIndex = 0;

                    var value = data[newIndex][valueField];
                    value = formItem.mapValueToDisplay(value);
                    if (isc.isA.String(value) && value.length > 0 &&
                        value.charAt(0).toLowerCase() == typedChar) {
                            this.scrollRecordIntoView(newIndex);
                            this._hiliteRecord(newIndex);
                            return;
                    }
                    newIndex += 1;
                    if (newIndex >= data.length) newIndex = 0;
                }
            }
        }

        // If the "Enter" key was pressed, but no record was selected, dismiss the menu
        // (this is really useful for the ComboBox item when showAllRecords is true and
        // the user has entered a value that isn't in the c-box).
        if (this.getFocusRow() == null && keyName == "Enter") {
            this.cancel();
            return false;
        }

        // The superclass implementation will handle cancelling on escape click / selecting
        // on enter click
        return this.Super("bodyKeyPress", arguments);
    },


    // Override dataChanged -- avoid redrawing to show temp. loading rows - wait
    // for the rows to come back from the server instead.

    dataChanged : function (operation, record, row, lastUpdateData) {
        var data = this.data;
        if (!data) return;

        var data = this.requestVisibleRows();
        if (data && Array.isLoading(data[0])) {
            // this.logWarn("not redrawing since data still loading");
            return;
        }
        this.Super("dataChanged", arguments);

        // If the currently selected record changed, we should refresh our value to pick up the
        // change.
        var formItem = this.formItem;
        if (record && this.getSelectedRecord() == record && formItem) {
            var index = this.data.indexOf(record),
                modifiedRecord = index == -1 ? null : this.data.get(index);
            if (modifiedRecord) {
                var fieldName = formItem.getValueFieldName();
                formItem.setValue(modifiedRecord[fieldName]);
            } else {
                formItem.clearValue();
            }
        }
    },


    createBodies : function () {
        if (this.body && this.body._reused) delete this.body._reused;
        this.Super("createBodies", arguments);
    }
});

isc.PickListMenu.changeDefaults("bodyDefaults", {

    // Override getCellStyleName() - historically we'd simply select cells to indicate
    // highlighting - no separate over styling required.
    // Now we use standard ListGrid hilighting APIs, so there is a separate Over vs
    // Selected state for cells. However older skins will commonly not have the "Over"
    // styles in place. Therefore we have a flag "showOverAsSelected" which will
    // show the same styling for hilighted "Over" cells as for "Selected" cells.
    remapOverStyles:[
            0, // 0 = baseStyle
            2, // 1 = Over(1) --> "Selected"
            2, // 2 = Selected(2)
            2, // 3 = Selected(2) + Over(1) --> "Selected"
            4, // 4 = Disabled(4)
            6, // 5 = Disabled(4) + Over(1) --> "Disabled + Selected"
            6, // 6 = Disabled(4) + Selected(2)
            6, // 7 = Disabled(4) + Selected(2) + Over(1) --> "Disabled + Selected"
            8, // 8 = Dark(8)
            10, // 9 = Dark(8) + Over(1) --> "Dark + Selected"
            10, // 10 = Dark(8) + Selected(2)
            10, // 11 = Dark(8) + Selected(2) + Over(1) --> "Dark + Selected"
            12 // 12 = Dark(8) + Disabled(4)
    ],
    getCellStyleName : function (styleIndex, record, rowNum, colNum) {
        if (this.grid && this.grid.showOverAsSelected) {
            styleIndex = this.remapOverStyles[styleIndex];
        }
        return this.Super("getCellStyleName", [styleIndex, record, rowNum, colNum], arguments);
    }
});


isc.PickList.addInterfaceProperties({

    //> @attr PickList.pickListHeight (number : 300 : IRW)
    // Maximum height to show the pick list before it starts to scroll.
    // Note that by default the pickList will be sized to the height required by its content
    // so it will be taller when more rows are available as selectable options
    // @visibility external
    //<
    pickListHeight:300,

    //> @attr PickList.emptyPickListHeight (number : 100 : IRW)
    // Height for an empty pick list (showing the empty message), if the pick list has no
    // records and +link{PickList.hideEmptyPickList} is <code>false</code>.
    // @visibility external
    //<
    emptyPickListHeight:100,

    //> @attr PickList.emptyPickListMessage (string : "No items to show" : IRWA)
    // Empty message to display in the pickList if
    // +link{PickList.hideEmptyPickList} is <code>false</code>.
    // @group i18nMessages
    // @visibility external
    //<
    emptyPickListMessage:"No items to show",

    //> @attr PickList.hideEmptyPickList (boolean : null : IRW)
    // If this pickList contains no options, should it be hidden?
    // If unset, default behavior is to allow the empty pickList to show if it is databound.
    // @visibility external
    //<
    //hideEmptyPickList:null,

    //> @attr PickList.pickListWidth (number : null : IRW)
    // Default width to show the pickList.
    // If not specified, the width of this form item's element will be used instead.
    // <P>
    // Note that this is a minimum value - by default if the values displayed in this pickList
    // are wider than the specified width the list will expand to accomodate them.
    // @visibility external
    // @example listComboBox
    //<
    //pickListWidth : null,

    //> @attr PickList.pickListMaxWidth (number : 400 : IRW)
    // Maximum width for this item's pickList.
    // By default if the values displayed in this pickList are wider than the specified
    // +link{pickList.pickListWidth} the pickList will render wide enough to accomodate them.
    // This property allows the developer to limit how wide the pickList will render.
    // @visibility external
    //<
    pickListMaxWidth:400,

    // Styling.
    // Users must be able to style pickLists

    //> @attr pickList.pickListBaseStyle (CSSStyleName : "pickListCell" : IR)
    // Base Style for pickList cells.  See +link{group:cellStyleSuffixes} for details on how
    // stateful suffixes are combined with the pickListBaseStyle to generate stateful cell styles.
    // <p>
    // Note: if +link{pickListTallBaseStyle} is specified, this property will be used as the
    // +link{listGrid.normalBaseStyle,normalBaseStyle} and that property will be applied
    // to cells that do not match the default cell height, or if +link{listGrid.fastCellUpdates}
    // is true for the pickList in Internet Explorer.
    //
    // @visibility external
    //<
    pickListBaseStyle: "pickListCell",

    //> @attr pickList.pickListTallBaseStyle (CSSStyleName : null : IR)
    // Optional +link{listGrid.tallBaseStyle,tallBaseStyle} for pickList cells. If unset,
    // +link{pickListBaseStyle} will be applied to all cells.
    //
    // @visibility external
    //<
    //pickListTallBaseStyle: null,

    //> @attr pickList.pickListApplyRowNumberStyle (boolean : false : IRWA)
    // Default value for +link{listGrid.applyRowNumberStyle} for this item's generated
    // pickList.
    // @visibility external
    //<

    pickListApplyRowNumberStyle:false,



    //> @attr    PickList.pickListHiliteColor  (string : null : IR)
    // If specified this color will be applied to hilited rows in the pickList, when the
    // user rolls over them.  This color will be applied on top of the "over" CSS Style
    // specified by <code>pickListBaseStyle</code>
    //<
    //pickListHiliteColor:null,

    //> @attr    PickList.pickListHiliteTextColor  (string : null : IR)
    // If specified this color will be applied to the text of hilited rows in the pickList,
    // when the user rolls over them.  This color will be applied on top of the "over" CSS Style
    // specified by <code>pickListBaseStyle</code>
    //<
    //pickListHiliteTextColor:null,

    // autoSizePickList
    // If true the pickList will expand horizontally to accommodate its widest item.
    // Not supported for databound lists (since the items load incrementally)
    //autoSizePickList : false,

    //>Animation
    //> @attr PickList.animatePickList (boolean : null : IRWA)
    // If true, when the pickList is shown, it will be shown via an animated reveal effect
    // @visibility animation
    //<

    //> @attr PickList.animationTime (number : 200 : IRWA)
    // If this.animatePickList is true - this specifies the duration of the animation effect
    // applied when showing the pickList
    // @visibility animation
    //<
    pickListAnimationTime:200,
    //<Animation

    //> @attr PickList.fetchDelay (number : 200 : IRWA)
    // For a ComboBox or other pickList that accepts user-entered criteria, how many
    // milliseconds to wait after the last user keystroke before fetching data from the server.
    // The default setting will initiate a fetch if the user stops typing or pauses briefly.
    //
    // @visibility external
    //<
    fetchDelay:200,

    //> @attr PickList.pickListCellHeight (number : 16 : IRW)
    // Cell Height for this item's pickList.
    //<
    pickListCellHeight:16,

    //> @attr PickList.pickListProperties (ListGrid Properties : null : IR)
    // If specified this properties block will be applied to the +link{PickListMenu,pickList}
    // created for this FormItem.
    // @visibility external
    //<
    //pickListProperties : null,

    //> @attr PickList.pickListHeaderHeight (number : 22 : IRW)
    // If this pick list is showing multiple fields, this property determines the height of
    // the column headers for those fields. Set to zero to suppress the headers entirely.
    // @see pickListFields
    // @visibility external
    //<
    pickListHeaderHeight:22,


    allowMultiSelect: true


    // --------------------------------------------------------------------------------------
    // Data / databinding

    //> @attr    PickList.valueField  (string : null : IRA)
    // @include FormItem.valueField
    //<

    //> @attr   PickList.displayField   (string : null : IRW)
    // @include FormItem.displayField
    //<


    //> @attr PickList.pickListFields (Array of ListGridField : null : IRA)
    // This property allows the developer to specify which field[s] will be displayed in the
    // drop down list of options.
    // <P>
    // Only applies to databound pickLists (see +link{PickList.optionDataSource}, or pickLists
    // with custom data set up via the advanced +link{pickList.getClientPickListData()} method.
    // <P>
    // If this property is unset, we display the +link{PickList.displayField}, if specified,
    // otherwise the +link{PickList.valueField}.
    // <P>
    // If there are multiple fields, column headers will be shown for each field, the
    // height of which can be customized via the +link{pickList.pickListHeaderHeight} attribute.
    // <P>
    // Each field to display should be specified as a +link{ListGridField} object. Note that
    // unlike in +link{ListGrid,listGrids}, dataSource fields marked as
    // +link{DataSourceField.hidden,hidden:true} will be hidden by default in pickLists. To
    // override this behavior, ensure that you specify an explicit value for
    // +link{ListGridField.showIf,showIf}.
    //
    //  @see valueField
    //  @see pickList.pickListHeaderHeight
    //  @visibility external
    //<

    //> @attr   PickList.valueIconField (string : null : IRWA)
    // For Databound formItems, this property determines which column
    // +link{formItem.valueIcons} should show up in for this formItem's pickList.<br>
    // If unset, valueIcons show up in the +link{pickList.displayField} column if specified,
    // otherwise the +link{pickList.valueField} column.<br>
    // In most cases only the <code>displayField</code> or <code>valueField</code> will be visible.
    // This property is typically only required if custom +link{PickList.pickListFields}
    // have been specified for this item.
    // @see FormItem.valueIcons
    // @see PickList.pickListFields
    // @visibility external
    //<

    //> @attr   PickList.pickListCriteria (Criteria : null : IRWA)
    // If this item has a databound pickList (for example +link{PickList.optionDataSource} is
    // set), this property can be used to provide static filter criteria when retrieving the data
    // for the pickList.
    // @visibility external
    //<

    //> @attr PickList.optionDataSource        (DataSource | String : null : IRA)
    // If set, this FormItem will derive data to show in the PickList by fetching records from
    // the specified <code>optionDataSource</code>.  The fetched data will be used as a
    // +link{formItem.valueMap,valueMap} by extracting the
    // +link{formItem.valueField,valueField} and +link{formItem.displayField,displayField} in
    // the loaded records, to derive one valueMap entry per record loaded from the
    // optionDataSource.  Multiple fields from the fetched data may be shown in the pickList by
    // setting +link{pickListFields}.
    // <P>
    // The data will be retrieved via a "fetch" operation on the DataSource, passing the
    // +link{PickList.pickListCriteria} (if set) as criteria, and passing
    // +link{optionFilterContext} (if set) as DSRequest Properties.
    // <P>
    // The fetch will be triggered when the pickList is first shown, or, you can set
    // +link{SelectItem.autoFetchData,autoFetchData:true} to fetch when the FormItem is
    // first drawn.
    // <P>
    // Note that providing an initial value when
    // +link{FormItem.fetchMissingValues,fetchMissingValues} is enabled, or enabling
    // +link{SelectItem.defaultToFirstOption,defaultToFirstOption}, can also cause a fetch to
    // be initiated immediately upon form creation.  You can also call +link{PickList.fetchData()}
    // at any time to manually trigger a fetch.
    // <P>
    // Data paging is automatically enabled if the optionDataSource supports it.  As the
    // pickList is scrolled by the user, requests for additional data will be automatically
    // issued.
    // <P>
    // For a pickList attached to a +link{class:ComboBoxItem, ComboBoxItem}, new fetches are
    // issued as the user types, with criteria set as described under
    // +link{comboBoxItem.getPickListFilterCriteria()}.
    // If your dataSource is not capable of filtering results by search criteria (eg, the
    // dataSource is backed by an XML flat file), you can set +link{filterLocally} to have the
    // entire dataset loaded up front and filtering performed in the browser.  This disables
    // data paging.
    // <P>
    // Note that if a normal, static +link{formItem.valueMap,valueMap} is <b>also</b> specified
    // for the field (either directly in the form item or as part of the field definition in
    // the dataSource), it will be preferred to the data derived from the optionDataSource for
    // whatever mappings are present.
    //
    // @visibility external
    //<

    //> @attr PickList.showOptionsFromDataSource  (boolean : null : IRWA)
    // If this item is part of a databound form, and has a specified <code>valueMap</code>,
    // by default we show the valueMap options in the pickList for the item.
    // Setting this property to true will ensure that the options displayed in our pickList
    // are derived from the form's <code>dataSource</code>.
    // @group databinding
    // @visibility external
    //<


    //> @attr PickList.fetchDisplayedFieldsOnly     (boolean: null : IRA)
    // If this item has a specified <code>optionDataSource</code> and this property is
    // <code>true</code>, the list of fields used by this pickList will be passed to
    // the datasource as +link{dsRequest.outputs}. If the datasource supports this feature
    // the returned fields will be limited to this list. A custom datasource will need to
    // add code to implement field limiting.
    // <P>
    // This list of used fields consists of the values of +link{formItem.valueField,valueField},
    // +link{formItem.displayField,displayField} and +link{pickListFields,pickListFields}.
    // <P>
    // NOTE: When enabled, +link{getSelectedRecord,getSelectedRecord} will only include the
    // fetched fields.
    // @visibility external
    //<

    //> @attr PickList.optionFilterContext     (DSRequest Properties : null : IRA)
    // If this item has a specified <code>optionDataSource</code>, and this property is
    // not null, this will be passed to the datasource as +link{dsRequest} properties when
    // performing the filter operation on the dataSource to obtain the set of options for the
    // list.
    // @visibility external
    //<
    // This gives the developer the option of specifying (for example) an operation name.

    //> @attr PickList.filterLocally (Boolean : false : IRA)
    // If <code>filterLocally</code> is set for this item, and this item is showing options
    // from a dataSource, fetch the entire set of options from the server, and use these values
    // to map the item value to the appropriate display value. Also use <code>"local"</code>
    // type filtering on drop down list of options.
    // <P>
    // This means data will only be fetched once from the server, and then filtered on the
    // client.
    // <P>
    // Note - when this property is set to <code>false</code>, filtering will still be
    // performed on the client if a complete set of data for some criteria has been cached
    // by a fetch, and a subsequent fetch has more restrictive criteria. To explicitly
    // disable client-side filtering set the +link{useClientFiltering} property to false.
    //
    // @see FormItem.filterLocally
    // @visibility external
    //<

    //> @attr PickList.filterDisplayValue (boolean : null : IRA)
    // When performing the filter on the data displayed in the pickList, for valueMapped fields,
    // should the filter criteria be compared to the raw data values in the source list, or the
    // values mapped to display.
    // <P>
    // This setting only has an effect on non-databound pick lists.
    //
    // @visibility internal
    //<


    //> @attr pickList.sortField (String or integer : null : IR)
    // Specifies the field by which this item should be initially sorted.  Can be set to
    // either a +link{listGridField.name,field name} or the index of the field in the fields
    // Array. Note that if <code>sortField</code> is initially specified as a number, it will be
    // converted to a string (field name) after the item is initialized.
    //
    // @group sorting
    // @example sort
    // @visibility external
    //<

});


isc.PickList.addInterfaceMethods({

    // showPickList
    // API to be called by the form item when the pickList is to be shown
    // Note that positioning of the pickList should be set up by modifying
    // getPickListPosition().
    // If 'waitForData' is passed, don't show the list until the filter completes.

    showPickList : function (waitForData, queueFetches) {
        // Set a flag to note that we've started to show the pickList.

        this._showingPickList = true;



        // Only pass in the param to show when the filter is complete if we're waiting for
        // data - otherwise we'll show the pickList explicitly (below).
        if (!this.pickList) this.makePickList(waitForData, null, queueFetches);
        // This will ensure the pickList is associated with this form item and set up its
        // data and fields.

        else this.setUpPickList(waitForData, queueFetches);

        // call _showPickList to actually show the pickList.

        if (!waitForData && (!this.pickList.isDrawn() || !this.pickList.isVisible())) {
            this._showPickList();
        }


    },

    // Actually show the pick list. on the page.
    _showPickList : function () {
        // don't show the PL if it's got no data

        // also don't show if we're not drawn - this can happen if we're showing in response
        // to an asynch event like fetching data.
        var list = this.pickList;
        if (!this.isDrawn() || (this.shouldHideEmptyPickList() && list.getTotalRows() < 1)) {
            return;
        }

        // size and place the picklist
        this.placePickList();

        if (!list.isDrawn() || !list.isVisible()) {
            //>Animation
            // Show, or animate-show
            if (this.animatePickList) this.pickList.animateShow("wipe", null, this.pickListAnimationTime);
            else    //<Animation
                this.pickList.show();
        }

    },

    //>@method  PickList.fetchData()
    // Only applies to databound items (see +link{PickList.optionDataSource}).<br>
    // Performs a fetch type operation on this item's DataSource to retrieve the set of valid
    // options for the item, based on the current +link{PickList.pickListCriteria}.
    // @param [callback] (DSCallback) Callback to fire when the fetch completes. Callback will
    //              fire with 4 parameters:<ul>
    //  <li><code>item</code> a pointer to the form item
    //  <li><code>dsResponse</code> the +link{dsResponse} returned by the server
    //  <li><code>data</code> the raw data returned by the server
    //  <li><code>dsRequest</code> the +link{dsRequest} sent to the server
    //  </ul>
    // @param [requestProperties] (DSRequest properties) properties to apply to the
    //              dsRequest for this fetch.
    // @visibility external
    //<
    // @param maintainCache (boolean) By default when this method is called we drop any
    //            cached rows and re-fetch. Pass in this parameter to suppress this behavior.
    //            Note that if the fetch operation does not hit the server (which will occur
    //            if the data is already cached), the callback will not fire
    fetchData : function (callback, requestProperties, maintainCache) {
        if (this.getOptionDataSource() == null) {
            this.logWarn("fetchData() called for a non-databound pickList. Ignoring");
            return;
        }

        // Store the callback passed in on the request's internalClientContext object.
        // This will be picked up as part of filterComplete()
        if (requestProperties == null) requestProperties = {};

        if (callback != null) {
            requestProperties.internalClientContext = {
                _callback: callback
            };
        }
        // add componentContext for developer console rpc history tab

        requestProperties.componentContext = this.form.ID + "." + this.name;
        if (!this.pickList) {
            this.makePickList(false, requestProperties, false, true);
        } else {
            this.setUpPickList(false, false, requestProperties, !maintainCache);
        }
    },

    mapValueToDisplay : function (internalValue, a, b, c) {
        if (this.isSelectOther) {
            if (internalValue == this.otherValue) return this.otherTitle;
            if (internalValue == this.separatorValue) return this.separatorTitle;
        }

        return this.invokeSuper(isc.SelectItem, "mapValueToDisplay", internalValue,a, b, c);
    },

    //> @attr pickList.useClientFiltering (Boolean : null : IRA)
    // For +link{optionDataSource,databound} items, this property will be passed
    // to the generated ResultSet data object for the pickList as +link{resultSet.useClientFiltering}.
    // Setting to false will disable filtering on the client and ensure criteria are
    // always passed to the DataSource directly.
    // @visibility external
    //<


    // Create the pickList for this widget.

    makePickList : function (show, request, queueFetches, dropCache) {
        //>DEBUG
        var startTime = isc.timeStamp();
        //<DEBUG

        // Wherever possible, we reuse picklist menus across multiple items
        // If explicit pickListProperties have been specified for this item, use a unique
        // pickList instead - this avoids us having to correctly apply (and then clear) freeform
        // properties to a shared pickList object.
        // If we're databinding we cache pickLists on a per-dataSource basis.
        // If the dataSource and the specified set of fields match, we reuse the list - otherwise
        // create a new one (rather than re-binding)
        var reusePickList = this.reusePickList();
        if (reusePickList) {
            this.pickList = this.getSharedPickList();
        }

        // We have to build a new picklist if the global one doesn't exist, or if we're
        // not sharing pickLists.
        var reusingPL = this.pickList != null;
        if (!this.pickList) {
            // The pickList is a pickListMenu - subclass of ScrollingMenu with some
            // overrides specific to form item pickLists.
            // The pattern we will use is to set the pickList up here, then override the
            // properties to allow us to reuse the list for other

            // Determine desired properties from the various init params.
            var pickListProperties = this.pickListProperties || {};

            if (this.multiple) {
                // if we're a multiple: true pickList and noDoubleClicks isn't specified,
                // switch it ON now
                if (pickListProperties.noDoubleClicks == null) {
                    pickListProperties.noDoubleClicks = true;
                }
            } else {
                // if we're a multiple: false pickList and noDoubleClicks isn't specified,
                // switch it OFF now
                if (pickListProperties.noDoubleClicks == null) {
                    pickListProperties.noDoubleClicks = false;
                }
            }



            this.pickList = isc.PickListMenu.create(
                                // no need to set up showPickList - this is done with setFields
                                { headerHeight:this.pickListHeaderHeight },
                                pickListProperties
                            );

            // apply local fetchMode if 'filterLocally' was explicitly specified.
            var data = this.pickList.dataProperties || {};
            if (this.filterLocally) data.fetchMode = "local";
            if (this.useClientFiltering != null) {
                data.useClientFiltering = this.useClientFiltering;
            }

            this.pickList.dataProperties = data;

            // If this is a shared pickList, store it in a publically accessible place
            if (reusePickList) this.storeSharedPickList();
        }

        // If limiting the fetch fields, build the correct optionFilterContext.outputs
        if (this.fetchDisplayedFieldsOnly && this.optionDataSource &&
            (!this.optionFilterContext || !this.optionFilterContext.outputs))
        {
            var fields = this.pickListFields || [];
            if (this.valueField) fields.add(this.valueField);
            if (this.displayField) fields.add(this.displayField);
            if (fields.length > 0) {
                if (!this.optionFilterContext) this.optionFilterContext = {};
                this.optionFilterContext.outputs = fields.getUniqueItems().join(',');
            }
        }

        // fire 'setUpPickList' to set up the specific properties relevant to this form item
        this.setUpPickList(show, queueFetches, request, dropCache);

        //>DEBUG
        if (this.logIsInfoEnabled("timing"))
            this.logInfo("Time to initially create pickList:" + (isc.timeStamp() - startTime), "timing");
        //<DEBUG
    },

    //>@attr pickList.cachePickListResults (boolean : true : IR)
    // For databound pickLists (see +link{pickList.optionDataSource}), by default SmartClient
    // will cache and re-use datasets shown by pickLists in an LRU (least recently used) caching
    // pattern.
    // <P>
    // Setting this flag to false avoids this caching for situations where it is too aggressive.
    // @visibility external
    //<
    // Note: if true, this actually uses a central, shared picklist rather than just caching the
    // results
    cachePickListResults:true,

    // Can this item use a cached pickList menu instance?
    reusePickList : function () {
        // we can reuse the pickList if the pickListProperties are null
        // For databound pickLists we create a cache of pickLists to reuse based on the
        // datasource ID and pickList fields
        // For client-only pickLists we have a single central reusable pickList.
        return this.pickListProperties == null && this.cachePickListResults;
    },

    // Retrieves the cached pickList menu for an item
    getSharedPickList : function () {
        if (this._getOptionsFromDataSource()) {

            // Store pickList menus for databound pickLists on a per DS basis.
            // cache of lists looks like this
            // isc._cachedDSPickLists = {
            //  dataSourceID:[
            //      {_pickList:..., +fields:...}
            //  ]
            // }

            var ds = this.getOptionDataSource().getID(),
                cachedLists = isc.PickListMenu._cachedDSPickLists[ds];
            if (cachedLists) {
                for (var i = 0; i < cachedLists.length;i++) {
                    if (cachedLists[i]._fields == this.pickListFields) {
                        // Note when we last used the pickList so we can
                        // destroy on a leastRecentlyUsed basis
                        cachedLists[i]._lastAccess = isc.timeStamp();

                        var list = cachedLists[i]._pickList;
                        // Lazily catch the case where the list was destroyed by
                        // external code.
                        if (list.destroyed) {
                            cachedLists.removeAt(i);
                            this._clearSharedPickListItems(list);
                            i--;
                            continue;
                        }

                        return list;
                    }
                }
            }
            return null;
        } else {
            // As in the dataSource case, catch the case where somehow the shared pickList
            // got externally destroyed.
            if (isc.PickList._pickListInstance && isc.PickList._pickListInstance.destroyed) {
                this._clearSharedPickListItems(isc.PickList._pickListInstance);
                isc.PickList._pickListInstance = null;
                return null;
            }

            return isc.PickList._pickListInstance;
        }
    },
    _clearSharedPickListItems : function (pickList) {
        if (pickList._formItems != null) {
            for (var formItem in pickList._formItems) {
                if (window[formItem] && window[formItem].pickList == pickList) {
                    delete window[formItem].pickList;
                }
            }
        }
    },

    storeSharedPickList : function () {
        if (this._getOptionsFromDataSource()) {

            var ds = this.getOptionDataSource().getID(),
                cachedLists = isc.PickListMenu._cachedDSPickLists;
            if (!cachedLists[ds]) cachedLists[ds] = [];

            var newMenu = {_pickList:this.pickList, _fields:this.pickListFields,
                                 _lastAccess:isc.timeStamp()}
            cachedLists[ds].add(newMenu);
             if (isc.PickListMenu._DSPickListCacheSize == null) {
                 isc.PickListMenu._DSPickListCacheSize = 1;
             } else {
                 isc.PickListMenu._DSPickListCacheSize += 1;
                 if (isc.PickListMenu._DSPickListCacheSize > isc.PickListMenu.pickListCacheLimit) {
                     // If we've exceeded our pickListCacheLimit, destroy a pickList to make room
                     // for the new one.
                     // We store last access timestamps on each pickList we create so we
                     // can destroy them in a least-recently-used order.

                     var oldMenu, ts = isc.timeStamp();
                     for (var ds in cachedLists) {
                         var dsLists = cachedLists[ds];
                         for (var i = 0; i < dsLists.length; i++) {
                             var entry = dsLists[i];
                             if (entry._lastAccess <= ts && (entry != newMenu)) {
                                 oldMenu = entry;
                                 ts = entry._lastAccess;
                             }
                         }
                     }
                     if (oldMenu) {
                         isc.PickListMenu._DSPickListCacheSize -= 1;
                         var pickList = oldMenu._pickList;

                         var dsLists = cachedLists[pickList.getDataSource().getID()];
                         dsLists.remove(oldMenu);
                         if (pickList._formItems != null) {
                             for (var formItem in pickList._formItems) {
                                 if (window[formItem] && window[formItem].pickList == pickList)
                                     delete window[formItem].pickList
                             }
                         }
                         // destroy on a delay so it doesn't slow this method down
                         oldMenu._pickList.delayCall("destroy");

                     }

                 }
             }

        } else {
            isc.PickList._pickListInstance = this.pickList;
        }
    },

    getPickListCellHeight : function () {
        var cellHeight = this.pickListCellHeight;
        // If a developer specifies pickListProperties.cellHeight, respect that too
        if (this.pickListProperties && this.pickListProperties.cellHeight != null) {
            cellHeight = this.pickListProperties.cellHeight;
        }
        if (this.valueIcons != null || this.getValueIcon != null) {
            var valueIconHeight = this.getValueIconHeight();
            if (valueIconHeight > cellHeight) cellHeight = valueIconHeight;
        }
        return cellHeight;
    },

    // Set Up Pick List - apply properties to the pickList to link it to this form item.
    // Called every time the pick-list is shown.
    // For form items that re-use the same pickList this method must set properties connecting
    // the pickList to this form item.
    // Otherwise simply ensure the displayed set of data is up to date.
    setUpPickList : function (show, queueFetches, requestProperties, dropCache) {
        var pickList = this.pickList;

        var cellHeight = this.getPickListCellHeight();
        pickList.setCellHeight(cellHeight);

        // These methods both no-op if the pickList is already applied to this item
        // and already showing the correct set of fields
        this._applyPickListToItem();
        this.setUpPickListFields();

        // apply custom empty message if we have one - if not ensure orginal empty
        // message shows up rather than potentially picking up the empty message from
        // another pickList based item.
        if (!pickList.originalEmptyMessage) pickList.originalEmptyMessage = pickList.emptyMessage;
        pickList.emptyMessage = this.emptyPickListMessage || pickList.originalEmptyMessage;

        // Auto size on both width and height axes by default.
        // If (undocumented) autoSizePickList property is explicitly false, auto size on height
        // axis only.
        // Treat this.pickListWidth (or this.getElementWidth()) as a minimum
        // Treat this.pickListHeight as a maximum
        this.pickList.emptyMessageHeight = this.emptyPickListHeight;
        this.pickList.setWidth(Math.max(1,this.pickListWidth || this.getElementWidth()));

        var autoFitWidth, props = this.pickListProperties;
        if (props && props.autoFitFieldWidths != null) autoFitWidth = props.autoFitFieldWidths;
        else autoFitWidth = this.autoSizePickList && !this.pickList.showHeader;
        this.pickList.autoFitFieldWidths = autoFitWidth;
        this.pickList.setAutoFitData(autoFitWidth ? "both" : "vertical");
        var minHeight = 1;
        if (this.pickList.showHeader) minHeight += this.pickList.headerHeight;
        if (this.pickList.showFilterEditor) minHeight += this.pickList.filterEditorHeight;
        this.pickList.setHeight(minHeight);
        this.pickList.setAutoFitMaxHeight(this.pickListHeight);
        this.pickList.setAutoFitMaxWidth(this.pickListMaxWidth);


        // always refilter

        var sortField = this.sortField == null ? this.sortFieldNum : this.sortField;
        if (sortField != null) {

            var useDS = this._getOptionsFromDataSource();
            var dropResultSet = false;
            if (this.pickList.data && isc.isA.ResultSet(this.pickList.data)) {
                var ds = this.getOptionDataSource();
                if (this.pickList.getDataSource() != ds) {
                    dropResultSet = true;
                } else {

                    var criteria = this.getPickListFilterCriteria(),
                        context = {
                            textMatchStyle:this.textMatchStyle,
                            showPrompt:false
                        };
                    if (this.optionFilterContext != null) {
                        isc.addProperties(context, this.optionFilterContext);
                    }
                    if (this.optionOperationId != null) {
                        context.operationId = this.optionOperationId;
                    }
                    if (requestProperties != null) {
                        isc.addProperties(context, requestProperties);
                    }
                    if (!this.pickList.useExistingDataModel(
                            criteria, context.operationId, context))
                    {
                        dropResultSet = true;
                    }
                }
            }
            if (dropResultSet) this.pickList.setData([]);
            this.pickList.sort(sortField, this.sortDirection);
        }
        this.filterPickList(show, queueFetches, requestProperties, dropCache);
    },

    _applyPickListToItem : function () {
        var oldFormItem = this.pickList.formItem;
        if (oldFormItem == this) return;

        // Determine desired properties from the various init params.
        var pickListProperties = {};
        isc.addProperties(pickListProperties, {
            // Ensure there's a pointer back to the form item
            formItem:this,

            normalBaseStyle:this.pickListBaseStyle,
            // If a tall style is defined use it, otherwise assume the normal version
            // can stretch
            tallBaseStyle:(this.pickListTallBaseStyle || this.pickListBaseStyle),

            applyRowNumberStyle:this.pickListApplyRowNumberStyle,

            hiliteColor:this.pickListHiliteColor,
            hiliteTextColor:this.pickListHiliteTextColor,

            // Allow components to show the pickList as a modal list
            showModal:this.modalPickList,

            // Pass this.dateFormatter through to the pickList.
            // This will ensure that date type fields are displayed the same in the
            // pickList cells as in the text box for this item if a dateFormatter is specified
            dateFormatter:this.dateFormatter,

            // notify the new item whenever we get fresh data (replaces previous dataArrived
            // implementation which would notify the previous form item)
            dataArrived : function (startRow, endRow) {
                if (isc._traceMarkers) arguments.__this = this;
                this.Super("dataArrived", arguments);
                if (this.formItem) this.formItem.handleDataArrived(startRow,endRow,this.data);
            }
        });
        if (this.multiple && this.multipleAppearance == "picklist"
            && this.allowMultiSelect)
        {
            pickListProperties.selectionAppearance = "checkbox";
            pickListProperties.allowMultiSelect = true;

            pickListProperties.enableSelectOnRowOver = false;
            pickListProperties.selectionType = "simple";
            pickListProperties._selectFirstOnDataChanged = false;

            pickListProperties.className = "listGrid";
            pickListProperties.bodyStyleName = "gridBody";

        } else {
            // rowStyle is the default selectionAppearance
            pickListProperties.selectionAppearance = "rowStyle";
            pickListProperties.allowMultiSelect = false;

            pickListProperties.enableSelectOnRowOver = true;
            pickListProperties.selectionType = "single";
            pickListProperties._selectFirstOnDataChanged = true;

            pickListProperties.className="scrollingMenu",
            pickListProperties.bodyStyleName="pickListMenuBody"
        }
        // apply this.pickListProperties on top of defaults so advanced developers can customize
        // the pickList directly.
        if (this.pickListProperties) {
            isc.addProperties(pickListProperties, this.pickListProperties);
        }

        this.pickList.setProperties(pickListProperties);

        // Keep track of every form item for which 'this.pickList' points to this pickList
        // Required for shared pickLists - allows us to clear up these pointers if the cached
        // pickList gets removed to make room for more lists in the cache

        if (!this.pickList._formItems) this.pickList._formItems = {};
        this.pickList._formItems[this.getID()] = true;

        // If we're re-using a pickList, clear it's observations on the previous form item
        // it was associated with.
        if (oldFormItem) {

            if (this.pickList.isObserving(oldFormItem.containerWidget, "hide")) {
                this.pickList.ignore(oldFormItem.containerWidget, "hide");
            }
            if (this.pickList.isObserving(oldFormItem.containerWidget, "clear")) {
                this.pickList.ignore(oldFormItem.containerWidget, "clear");
            }
        }


        // Clean up the pickList if the form goes away

        if (!this.pickList.isObserving(this.containerWidget, "hide")) {
            this.pickList.observe(this.containerWidget, "hide", "observer.hide();");
        }

        if (!this.pickList.isObserving(this.containerWidget, "clear")) {
            this.pickList.observe(this.containerWidget, "clear",
                                                    "if(observer.isDrawn())observer.clear();");
        }
        // always mark the pickList as dirty. This ensures any cell formatter set up on this
        // item will be applied even if the data is unchanged
        this.pickList.markForRedraw();


    },

    // ------------
    // Data Management
    // ------------

    //>@method pickList.getOptionDataSource()
    // PickLists can derive their data directly from a valueMap, or retrieve data from a
    // dataSource to display as options.
    // <P>
    // This method will return the dataSource used to populate the pickList, or null if
    // none is specified (implies this list will derive its data from the valueMap for the item).
    // Default implementation will return +link{pickList.optionDataSource} if specified,
    // otherwise if this is a field with a specified <code>foreignKey</code> in a databound
    // form, returns the dataSource for the <code>foreignKey</code>.
    // Otherwise picks up <code>this.form.dataSource</code> if set.
    //
    // @return (DataSource) DataSource to use for fetching options
    // @visibility external
    //<


    // getPickListFields()
    getPickListFields : function () {
        // Allow the developer to specify a set of fields
        // Only really has meaning if the select item is databound, where multiple fields are
        // available, or if custom client pickList data was supplied.
        // For databound lists, properties such as valueMaps will be picked up from
        // the dataSource.
        if (this.pickListFields) {


            var value = this.emptyDisplayValue;
            if (value != null) {
                var fields = this.pickListFields,
                    valueField   = this.getValueFieldName(),
                    displayField = this.getDisplayFieldName(),
                    object, undef;
                if      ((object = fields.find("name", displayField)) != null &&
                    object.emptyCellValue === undef) object.emptyCellValue = value;
                else if ((object = fields.find("name",   valueField)) != null &&
                    object.emptyCellValue === undef) object.emptyCellValue = value;
            }
            return this.pickListFields;
        }


        var displayField = this.getDisplayFieldName(),
            fieldObj;
        if (displayField != null) {
            fieldObj = {width:"*", name:displayField}
            fieldObj.formatCellValue = this._formatDisplayFieldCellValue

        } else {
            fieldObj = {width:"*", name:this.getValueFieldName(),
                        // apply the same valueMap to this field so the display values show up
                        // correctly

                        valueMap:this.getValueMap()
                       }
        }
        // if this is a SelectItem and escapeHTML is set, apply it to the
        // pickList field too (this is supported at the LG level already)
        if (this.canEscapeHTML &&
                // outputAsHTML / asHTML are old and deprecated
                (this.escapeHTML || this.outputAsHTML || this.asHTML))
        {
            fieldObj.escapeHTML = true;
        }

        // If an empty display value is specified, apply it to the field as well so we show
        // empty options with the correct title.
        if (this.emptyDisplayValue != null) {
            // For consistency, unless canEscapeHTML is true for this item type (which indicates
            // that the item supports displaying HTML), escape the emptyDisplayValue.
            fieldObj.emptyCellValue = (this.canEscapeHTML ? this.emptyDisplayValue : String(this.emptyDisplayValue).asHTML());
        }

        // If we have an explicitly specified dateFormatter for this item, it will be passed to
        // the pickList but only respected for fields of type "date" there.
        // Assume this is what the user intended and default the type to "date" for the pickList
        // field. This'll be unnecessary if the dataSource field is already of type:"date" of
        // course.
        if (this.dateFormatter != null) {
            fieldObj.type = "date"
        }

        // hang a flag on the field object as being auto-generated.
        // in this case we'll assign our custom formatter to it.
        // Otherwise the user is on their own.
        fieldObj._isGeneratedField = true;

        return [fieldObj]
    },

    // custom formatter for the display field - if the valueField is empty, show the empty
    // cell value (unless there's an explicit display field value for the record in question
    // in which case the "right" behavior is ambiguous)
    _formatDisplayFieldCellValue : function (value,record,rowNum,colNum,grid) {

        if (value != null) return value;
        var item = grid.formItem,
            valueField = item ? item.getValueFieldName() : null
        ;
        if (record[valueField] == null && item) return item.emptyCellValue;
        return value;
    },

    formatPickListValue : function (value,fieldName,record) {
        // apply standard formatter to the value in the single generated field for
        // standard pick lists.
        // This handles formatters applied via simpleType as well as any
        // 'formatValue()' method applied to this item
        if (this.pickList.getField(fieldName)._isGeneratedField) {
            return this._formatDataType(value);
        }
        return value;
    },


    //> @method  PickList.getPickListFilterCriteria()  (A)
    // +link{group:stringMethods,StringMethod} to return a set of filter criteria to be applied to
    // the data displayed in the pickList when it is shown.
    // <P>
    // If this is a databound item the criteria will be passed as criteria to
    // +link{dataSource.fetchData()}.  Otherwise an equivalent client-side filter will be
    // performed on the data returned by +link{getClientPickListData()}.
    // <P>
    // By default combines +link{formItem.optionCriteria} with
    // +link{pickList.pickListCriteria} if specified, otherwise an empty
    // set of criteria so all records will be displayed.
    //
    // @return (Criteria) criteria to be used for databound or local filtering
    // @visibility external
    // @example databoundDependentSelects
    //<
    getPickListFilterCriteria : function () {
        var baseCrit = isc.addProperties({}, this.optionCriteria);
        return isc.DataSource.combineCriteria(baseCrit, this.pickListCriteria, null, this.textMatchStyle);
    },

    // This is a helper method to return the set of 'local' options.
    // default implementation returns null for databound pickLists, or the array of values for
    // non databound picklists.
    // Used by logic to navigate around via keyboard
    // Overridden on the Select item to handle databound pickLists with all rows cached
    getAllLocalOptions : function () {
        return this._getOptionsFromDataSource() ? null : this.getClientPickListData();
    },

    //> @method PickList.getValueFieldName()
    // @include FormItem.getValueFieldName()
    //<

    // Display fields -----------------------------------------------------------------

    //> @method   pickList.getDisplayFieldName()
    // @include FormItem.getDisplayFieldName()
    //<


    // For databound items, an explicitly specified displayField means we'll be displaying
    // values from one field of our dataSource, and returning data values from a different field.
    // (Something like a valueMap using the resultSet from the server as a mapping object).
    // This helper method will perform the conversion between display and data values based
    // on the pickList's data set, if necessary.
    // Retrieving the display value directly from the pickList dataSet often avoids us having to
    // run the fetchMissingValues logic in setValue() whereby we perform a fetch against the
    // dataSource when setValue() is called
    // Note that in contrast if this is a freeform data entry type field (EG a combobox)
    // and the user enters a value that is not present in the data-set, it will be stored as
    // the data value for the item, even though the user is theoretically setting the
    // display value.
    // Form items that use the pickList interface will typically call this method from
    // mapValueToDisplay() and mapDisplayToValue()
    // returnNull parameter tells us to return null if we can't find an entry in our
    // pickList data that matches the value passed in - used to detect "unknown" values in
    // the ComboBoxItem. This param is actually only used in the case where displayField matches
    // valueField - otherwise we can bypass the "find" logic in that case and just return
    // the value passed in.
    _translateValueFieldValue : function (value, toValueField, returnNull) {
        // If we were not passed a defaultValue (value to return if no match was found),
        // return
        // This method is only for use with databound pickLists.
        var ods = this.getOptionDataSource(),
            displayField = this.getDisplayFieldName(),
            valueField = this.getValueFieldName();

        // If we have no ODS and no displayField, just bail
        if (displayField == null && ods == null) {

            return;
        }

        // No need to translate if the fields are the same!
        // If passed the 'returnNull' param we actually do want to verify the entry exists
        // in the data however.
        if ((ods == null || !returnNull) &&
            (displayField == null || displayField == valueField)) return value;

        var resultSet = this.getPickListResultSet();
        var cache;
        if (resultSet != null) {
            // simple array of data
            if (isc.isAn.Array(resultSet)) {
                cache = resultSet;

            // ResultSet - reach into the cache
            // If we have a complete cache use it - this enables us to map data values that don't
            // match the current filter criteria.
            } else {
                cache = resultSet.allRows || resultSet.localData;
            }
        }

        var odsCacheData = (ods ? ods.getCacheData() : null);
        if (cache == null) {
            cache = odsCacheData;
            if (cache == null) return;
        }

        var toField = (toValueField ? valueField
                                    // if there's no explicit displayField name, use the
                                    // valueField for both to and from fields.
                                    : displayField || valueField),
            fromField = (toValueField ? displayField || valueField
                                      : valueField);
        var retVal;
        // if the stored value is an array, it means we have to construct the return value
        // from the array of values passed in. (when selectItem.multiple == true)
        if (isc.isAn.Array(value)) {
            retVal = "";
            var keys = isc.shallowClone(value);
            for (var i = 0; i < value.length; i++) {
                var currVal = keys[i];
                // find the record that matches the current value
                var record = cache.find(fromField, value[i]);
                if (record == null && odsCacheData != null && cache != odsCacheData) {
                    record = odsCacheData.find(fromField, value[i]);
                }

                if (record != null) {
                    retVal += record[toField];
                } else continue;

                if (i != value.length - 1) retVal += this.multipleValueSeparator;
            }
        } else {
            var record = cache.find(fromField, value);
            if (record == null && odsCacheData != null && cache != odsCacheData) {
                record = odsCacheData.find(fromField, value);
            }
            if (record != null) {
                 retVal = record[toField];
             }
        }
        return retVal;
    },

    // Gets the result set in use by the pick list.  This method is internal and is used by
    // form items to copy records from the pick list's result set to the form item's displayField
    // cache.
    getPickListResultSet : function () {
        var resultSet = this.pickList && this.pickList.formItem == this && !this.pickList.destroyed ?
                            // originalData will have been set if this pickList's data is
                            // grouped.
                            (this.pickList.originalData || this.pickList.data) : null;
        return resultSet;
    },

    _$true:"true",
    setUpPickListFields : function () {
        var fields = this.getPickListFields(),
            currentFields = this.pickList.fields;
        // Verify that the fields are not already up to date. If so just bail.
        var fieldsChanged = !currentFields || (currentFields.length != fields.length);
        if (!fieldsChanged) {
            for (var i= 0; i < fields.length; i++) {
                var field = fields[i], plField = currentFields[i];
                for (var prop in field) {
                    if (field[prop] != plField[prop]) {
                        fieldsChanged = true;
                        break;
                    }
                }
                if (fieldsChanged) break;
            }
        }

        if (!fieldsChanged) return;

        // Ensure that all fields are visible by default. This avoids the gotcha where
        // detail fields
        // would otherwise fail to show up in pickLists.
        for (var i = 0; i < fields.length; i++) {
            if (fields[i].showIf == null) {
                fields[i].showIf = this._$true;
            }
        }

        // For the display field (which will display the valueIcons specified for this item)
        // pick up valueIcon sizing etc from this item unless it was explicitly overridden
        if (this.valueIcons != null || this.getValueIcon != null) {
            for (var i = 0; i < fields.length; i++) {
                var field = fields[i];
                if (field[this.form.fieldIdProperty] == this.getValueFieldName()) {
                    if (field.valueIconHeight == null)
                        field.valueIconHeight = this.valueIconHeight;
                    if (field.valueIconWidth == null)
                        field.valueIconWidth = this.valueIconWidth;
                    if (field.valueIconSize == null)
                        field.valueIconSize = this.valueIconSize;
                    if (field.imageURLPrefix == null)
                        field.imageURLPrefix = this.imageURLPrefix || this.baseURL || this.imgDir;
                    if (field.imageURLSuffix == null)
                        field.imageURLSuffix = this.imageURLSuffix;
                }
            }
        }
        this.pickList.setFields(fields);
        var showHeader;
        if (this.pickListHeaderHeight == 0) showHeader = false;
        else if (this.pickListProperties) {
            if (this.pickListProperties.showHeader != null) {
                showHeader = this.pickListProperties.showHeader;
            } else if (this.pickListProperties.headerHeight == 0) {
                showHeader = false;
            }
        };

        if (showHeader == null) {
            var visibleFields = this.pickList.getFields();
            // if we're showing a checkbox column and only 1 other field, hide the header
            var hideHeaderThresh = (this.multiple && this.multipleAppearance == "picklist"
                                   && this.allowMultiSelect == true) ? 2 : 1;
            // Show the header if there are multiple fields.
            showHeader = (visibleFields.length > hideHeaderThresh);
        }
        if (showHeader) {
            // we are likely to be sharing a pickList across items, each of which may have a
            // different header height, so reset each time the header is shown
            // (No ops if no change anyway)
            this.pickList.setHeaderHeight(this.pickListHeaderHeight);
            this.pickList.setShowHeader(true);
        } else {
            this.pickList.setShowHeader(false);
        }
    },

    // If we have an explicitly specified optionDataSource for the field, we will databind the
    // list to that dataSource.
    // Otherwise, we'll use the valueMap to get a set of client-only options if we have one
    // - or attempt to derive an optionDataSource if not.
    _getOptionsFromDataSource : function () {
        // explicit datasource
        if (this.optionDataSource) return true;
        // check for auto-derived optionDataSource (this will be non-null if the field has a
        // foreignKey or the form as a whole has a DataSource)
        if ((this.showOptionsFromDataSource || !this.valueMap) &&
            this.getOptionDataSource() != null) return true;
        return false;
    },




    filterPickList : function (show, queueFetches, request, dropCache) {
        if (!queueFetches)
            this._filterPickList(show, request, null, dropCache);
        else {
            this._queuedFetch = true;
            this._showOnDelayedFilter = show;
            this.fireOnPause("fetch",
                             {target:this, methodName:"_filterPickList",
                              args:[null, request, true, dropCache]},
                             this.fetchDelay);
        }
    },

    _filterPickList : function (show, request, delayed, dropCache) {
        this._queuedFetch = null;

        if (delayed) show = this._showOnDelayedFilter;
        delete this._showOnDelayedFilter;
        this._showOnFilter = show;
        var useDS = this._getOptionsFromDataSource();
        if (useDS) {
            var ds = this.getOptionDataSource();

            // Pass the (already set up) fields into the 'setDataSource' method.
            if (this.pickList.getDataSource() != ds) {
                this.pickList.setDataSource(ds, (this.pickList.completeFields || this.pickList.fields));
            }
            // Will fall through to filterComplete() when the filter op. returns.
            this.filterDataBoundPickList(request, dropCache);
        } else {
            // Ignore any requestProperties passed in for a client-only filter.
            var records = this.filterClientPickListData();

            if (this.pickList.data != records) this.pickList.setData(records);

            // explicitly fire filterComplete() as we have now filtered the data for the
            // pickList
            this.filterComplete();
        }


    },

    // _pickListNeedsRefilter()
    // This method determines whether the pickList passed in has been filtered to match
    // this item's criteria.
    //
    // Used by getFirstOptionValue() and to avoid unnecessary refilters when the shared pickList is
    // assigned to different formItems
    // returns a boolean - true if a refilter will be required (pickList not showing data that will
    // match this item's option set).

    _pickListNeedsRefilter : function (pickList) {
        // Only applies to databound pickLists
        if (!this._getOptionsFromDataSource() || !pickList) return;

        var ds = this.getOptionDataSource();
        if (pickList.getDataSource() == ds && pickList.data) {
            var context = pickList.data.context,
                crit = pickList.data.criteria;
            if (context.textMatchStyle != this.textMatchStyle) return true;
            if (this.optionFilterContext != null) {
                for (var field in this.optionFilterContext) {
                    if (this.optionFilterContext[field] != context[field]) return true;
                }
            }
            // if criteria match we don't need a refilter
            if (ds.compareCriteria(crit,this.getPickListFilterCriteria(), context) == 0) {
                return false;
            }
        }
        return true;
    },

    // getFirstOptionValue - used by SelectItem / ComboBoxItem if defaultToFirstOption is true
    getFirstOptionValue : function () {
        var value;
        if (this._getOptionsFromDataSource()) {
            var pickList = this.pickList ||
                            (this.reusePickList() ? this.getSharedPickList() : null);
            if (pickList && !this._pickListNeedsRefilter(pickList)) {

                var record = pickList.data.get(0);
                // Don't attempt to default to the loading row marker. Note that handleDataArrived
                // already handles setting to the default value.
                if (record == null || Array.isLoading(record)) {
                    value = null;
                } else {
                    value = record[this.getValueFieldName()];
                }
            } else {
                // In this we don't yet have a pickList showing options that will
                // match this form item, so can't get our first optionValue.
                // Kick off a fetch. When it completes we'll have a valid first option
                // Pass in the parameter to avoid dropping cache and forcing a server turnaround
                // (We may be able to refilter on the client if it's just a criteria change)

                this.fetchData(null,null,true);
            }

        } else {
            var map = this.valueMap;
            if (isc.isAn.Array(map)) value = map[0];
            else if (isc.isAn.Object(map)) {
                // use for...in to pick up first defined property on the object
                for (var field in map) {
                    value = field;
                    break;
                }
            }
        }
        return value;
    },

    //>@method pickList.getClientPickListData() [A]
    // Returns the set of data to be displayed in this item's PickList.
    // <P>
    // This method will be called for non-databound form items implementing the PickList
    // interface.  The default implementation will derive data from the item's valueMap -
    // can be overridden to allow a custom set of options to be displayed.
    // <P>
    // Note that for PickLists that filter data based on user input
    // (+link{ComboBoxItem,ComboBox}), this method should return the data <b>before
    // filtering</b>.  To customize the data returned after filtering, override
    // +link{filterClientPickListData()} instead.
    // <P>
    // As an example, for a formItem with +link{valueField} set to "valueFieldName", the
    // default implementation would take a valueMap like the following:
    // <pre>
    //     valueMap: { value1: "display 1", value2: "display 2" }
    // </pre>
    // .. and returning the following set of records:
    // <pre>
    //     [
    //          { valueFieldName : "value1" },
    //          { valueFieldName : "value2" }
    //     ]
    // </pre>
    // Due to the valueMap, these records will appear as a two row pickList displayed as
    // "display 1" and "display 2".
    //
    // @return (Array of ListGridRecord) Array of record objects to be displayed in the
    //           pickList. Note that when a user picks a record from the list, the value of the
    //           field matching <code>item.valueField</code> will be picked. Also note that the
    //           fields to be displayed can be customized via <code>item.pickListFields</code>
    //
    // @visibility external
    //<
    getClientPickListData : function () {
        return isc.PickList.optionsFromValueMap(this);
    },

    // configure this pickList to observe/ignore established set of methods
    _manageObservers : function (script) {
        var targets = [ "moved", "parentMoved", "scrolled", "parentScrolled" ];

        for (var i = 0; i < targets.length; i++) {
            var observing = this.pickList.isObserving(this.containerWidget, targets[i]);
            if (script) {
                if (!observing) this.pickList.observe(this.containerWidget, targets[i], script);
            } else {
                if (observing) this.pickList.ignore(this.containerWidget, targets[i]);
            }
        }
    },

    // Override point to notify the item that the pickList has been shown / hidden
    _pickListHidden : function () {
        if (isc.Canvas.ariaEnabled()) {
            this.setAriaState("expanded", false);
            this.clearAriaState("owns");

            this.clearAriaState("activedescendant");
        }
        this._manageObservers();
        if (this.pickListHidden) this.pickListHidden();
    },

    _pickListShown : function () {
        if (isc.Canvas.ariaEnabled()) {

            this.setAriaState("expanded", true);
            this.setAriaState("owns", this.pickList._getAriaHandleID());
        }
        this._manageObservers("observer.moveBy(deltaX,deltaY)");
        if (this.pickListShown) this.pickListShown();
    },

    // selectDefaultItem
    // When the pickList is initially shown / re-filtered update the selection.
    // For select items the current value will be selected.
    // [Overridden by the comboBox class to always select the first record].

    selectDefaultItem : function () {
        // Select the value currently displayed in the form item
        return this.selectItemFromValue(this.getValue());
    },

    selectItemFromValue : function (value) {
        if (!isc.isAn.Array(value)) value = [value];
        var records = this.pickList.getSelection(),
            valueField = this.getValueFieldName(),
            allValuesFound = true,
            lastFoundRec
        ;

        // if the valueField isn't text- or number-based, use item.compareValues() to compare
        var type = this.getType(),
            basicType = isc.SimpleType.inheritsFrom(type, "text") ||
                        isc.SimpleType.inheritsFrom(type, "integer") ||
                        isc.SimpleType.inheritsFrom(type, "float"),
            comparator = basicType ? null : this.compareValues
        ;

        for (var i = 0; i < value.length; i++) {
            var currVal = value[i],
                record;
            // If the value is already selected we can just return. This is much quicker for most
            // cases since we don't have to iterate through the pickList data array
            if (records.find(valueField, currVal)) continue;

            var data = this.pickList.getData();
            if (isc.ResultSet && isc.isA.ResultSet(data)) {

                var cache = data.localData;
                if (cache) record = cache.find(valueField, currVal, comparator);
            } else {
                record = data.find(valueField, currVal, comparator);
            }
            if (record && record != Array.LOADING) {

                if (this.pickList.allowMultiSelect) this.pickList.selectRecord(record);
                else this.pickList.selection.selectSingle(record);
                lastFoundRec = data.indexOf(record);
            } else {
                allValuesFound = false;
            }
        }
        if (lastFoundRec != null) this.pickList.scrollRecordIntoView(lastFoundRec);
        // Return a boolean to indicate whether we successfully found and selected all records
        return allValuesFound;
    },


    // -- Data bound filtering --

    // filterComplete - callback fired when the data to be displayed has been filtered.
    // (Will be called for both databound and non-databound lists)
    filterComplete : function (response, data, request, fromSharedPickList) {
        if (!fromSharedPickList && request != null && request.clientContext != null) {
            // if we get back out of sequence responses, don't allow the earlier one to clobber the
            // more recent one.
            var lastID = this._lastFetchID,
                newID = request.clientContext.fetchID;
            if (lastID == null || lastID < newID) {
                this._lastFetchID = newID;
            } else {
                this.logWarn("Server returned out of order responses for databound fetch requests." +
                    " Ignoring superceded request results");

                return;
            }
        }

        this._fetchingPickListData = false;
        this._processingFilterComplete = true;

        this._updatePickListForFilterComplete(response,data,request);

        this._updateValueForFilterComplete(response,data,request);

        this._processingFilterComplete = false;

        if (isc.Canvas.ariaEnabled()) {
            if (this.pickList._fetchingForItem == null ||
                this.getID() === this.pickList._fetchingForItem ||
                fromSharedPickList)
            {
                this.pickList.clearAriaState("busy");
            }
        }

        // If a callback was passed in when the filter was intialized, fire it now, passing in
        // the resultSet as a single parameter.

        var callback = (request && request.internalClientContext ?
                            request.internalClientContext._callback : null);
        if (callback) {
            this.fireCallback(
                callback,
                "item,dsResponse,data,dsRequest",
                [this, response, data, request]
            );

            if (request && request.internalClientContext) {
                delete request.internalClientContext._callback;
            }
        }
    },

    _updatePickListForFilterComplete : function (response, data, request) {

        var list = this.pickList;

        if (!list || list.destroyed) return;
        var hasFocus = list.hasFocus || (list.body && list.body.hasFocus);
        var data = list.getData();

        // If showing an empty picklist because the entry is too short
        // update emptyMessage to indicate this. After a complete filter
        // with no actual matches the emptyMessage will be reset automatically.
        var tooShort = (this.isEntryTooShortToFilter && this.isEntryTooShortToFilter());
        if (tooShort) {
            list.emptyMessage = this.getEntryTooShortMessage();
        }
        if (data.getLength() == 0 && list.isVisible() && list.isDrawn()) {
            // no matches, so hide the dropdown, place focus in the form item itself
            if (this.hideEmptyPickList) {
                list.hide();
                if (hasFocus) this.focusInItem();
            } else if (this.allowPickListToClip) {
                var position = this.getPickListPosition();
                list.setRect([position[0], position[1]]);
            } else {
                isc.PickList._placeAdjacent(this, list);
            }
        } else {
            // if we set the flag to show the list after the filter, show it now!
            if (this._showOnFilter) this._showPickList();
            // If the list is already showing, call placePickList to ensure it resizes to
            // accommodate content if required.
            else if (list.isVisible() && list.isDrawn()) this.placePickList();

            // recalculate autoFit height/width so the grid will shrink as appropriate
            list.setHeight(1);
            list.setAutoFitData(list.autoFitData);


            delete this._showOnFilter;
        }
    },

    // Fired from filterComplete - handles updating display value etc.
    // Overridden in comboBoxItem to handle the cases where focus has been taken from the
    // item mid-fetch and we need to completeOnTab or addUnknownValues is false.
    _updateValueForFilterComplete : function (response,data,request) {
        // Always select the default item at this point since we have the latest data
        this.selectDefaultItem();

        // Fold the pickList data into the cache of records set up at the form item level by the
        // fetchMissingValue flow.
        // This means if the pickList is subsequently associated with another item / its data
        // changes, we can still get at all the records this item has seen.
        // This code flow is used for synchronous filter (client-side data / valueMaps) - we only
        // care if this actually came from a fetch so check for response / data being non-null

        if (response != null && data != null) {
            this._addDataToDisplayFieldCache(data, true);
            // rebuild the "display field valueMap" - suppress refresh- we'll handle that later.
            this.updateDisplayValueMap(false);

        }

        // _checkDisplayFieldValueOnFilterComplete
        // This flag is set up by the ComboBox / SelectItem class as part of checkDisplayFieldValue
        // if a pickList filter operation is running while that method is called.
        // In this case we need to recheck now the pickList data has loaded.
        // If the pickList data loaded a display value for this fields value, we're done,
        // otherwise we need to perform another fetch
        if (this._checkDisplayFieldValueOnFilterComplete) {
            delete this._checkDisplayFieldValueOnFilterComplete;
            this._checkForDisplayFieldValue(this._value);
        }

        // helper - if we're currently showing a data value and we just loaded the associated
        // display value, display it.
        this._updateDisplayValueForNewData();

    },

    // If this item has a databound pickList, and a 'displayField',
    // If the value has been set to some data value, which doesn't correspond
    // to a row loaded in the pickList, and we haven't mapped to a display value
    // (EG fetchMissingValues is false, etc), if the data does subsequently get loaded in
    // the pickList, we want to update the element value to show the display value.
    // We do this in response to new data arriving for the pickList (a filter operation completes)

    _updateDisplayValueForNewData : function () {
        if (this.isDrawn() && this.getValueFieldName() != null
            && this._getOptionsFromDataSource())
        {

            if (this.isA("ComboBoxItem")) {
                // in a ComboBox where addUnknownValues is true, do an immediate 'updateData'
                // Scenario:
                // - the user enters a valid display value but it hasn't been loaded yet so we don't
                //   know this. Therefore the comboBoxItem stores out the entered value as a data
                //   value
                // - pickList fetch for data returns, containing the entry that matches this display
                //   value. Running updateValue() again will ensure the data value is switched from
                //   the entered string to the appropriate data value for which that is a valid
                //   display value.
                //
                // NOTE: addUnknownValuesFalse is handled separatedly by
                // updateValueForFilterComplete override in ComboBoxItem class
                if (this.addUnknownValues == true) {
                    this.updateValue();
                }

                // If the text box has focus, never change the display value.

                if (this.hasFocus) return;
            }


            var currentVal;
            if (!this._itemValueIsDirty()) currentVal = this.getValue();
            else {
                if (this.isA("SelectItem")) currentVal = this._localValue;
                else currentVal = this.mapDisplayToValue(this.getElementValue());
            }
            // only update if we retrieved a record that matches the current value
            // This avoids us dropping a good display value when we lose it from the pickList cache
            var record = this.getSelectedRecord();
            if (record) {
                var displayVal = this.mapValueToDisplay(currentVal);

                if (this._displayValue != displayVal) {
                    this.setElementValue(displayVal);
                }
            }
        }
    },

    // This will only be called on a databound pickList
    _databoundFetchID:0,
    filterDataBoundPickList : function (requestProperties, dropCache) {
        if (isc._traceMarkers) arguments.__this = this;

        // If user has not entered enough characters to filter return empty data
        if (this.isEntryTooShortToFilter && this.isEntryTooShortToFilter()) {
            var data = this.pickList.originalData || this.pickList.data;
            if (data && isc.ResultSet && isc.isA.ResultSet(data)) {
                // A previous match was found but entry is now too short.
                // Clear previous matches.
                this.pickList.setData([]);
            }
            this.filterComplete();
            return;
        }

        var criteria = this.getPickListFilterCriteria(),
            context = {


                textMatchStyle:this.textMatchStyle,


                showPrompt:false
            };
        // this.optionFilterContext - an entry point to add properties to the operation context,
        // such as the name of a defined operation.
        if (this.optionFilterContext != null) isc.addProperties(context, this.optionFilterContext);

        // respect optionOperationId if specified
        if (this.optionOperationId != null) context.operationId = this.optionOperationId;

        // Additional properties to be applied to the request can be passed as a parameter.
        // This code path is used for the userVisible 'fetchData()' api
        if (requestProperties != null) {
            isc.addProperties(context, requestProperties);
        }





        var alreadyFetching = false,
            synchronousFilter = false;
        // originalData will be set if grouping has been applied to the pickList.
        var data = this.pickList.originalData || this.pickList.data;
        if (data && isc.ResultSet && isc.isA.ResultSet(data)) {
            if (dropCache) {
                // invalidateCache doesn't have a way to apply a callback.
                // Instead call '_invalidateCache' to drop the cache (without re-fetching)
                // Then continue with filterData() as normal
                data._invalidateCache();
            } else {
                if (!data.willFetchData(criteria, this.textMatchStyle)) {
                    // If we're currently fetching, but willFetchData returned false,
                    // we can't just call fetchData() and expect to be notified on callback.

                    if (!data.lengthIsKnown() || (data.getLength() != 0 && !data.rowIsLoaded(0))) {
                        alreadyFetching = true;
                        if (this.pickList._fetchingForItem != this.getID()) {
                            if (!this.isObserving(data, "fetchRemoteDataReply")) {
                                this.observe(data,
                                    "fetchRemoteDataReply",
                                    "observer._sharedPickListFetchComplete(observed, dsResponse, data, request)"
                                );
                                // _fetchingRequest is an ID hung onto the request context when
                                // ResultSets fetch remote data - use this to ensure we don't
                                // react to another fetch!
                                this._sharedPickListFetchRequest = data._fetchingRequest;
                            }
                        }
                    } else {
                        synchronousFilter = true;
                    }
                }
            }


            if (!alreadyFetching) {

                context.fetchID = this._databoundFetchID++;

                if (this.isObserving(data, "fetchRemoteDataReply")) {
                    this.ignore(data, "fetchRemoteDataReply");
                }
            }

        }
        this.pickList.filterData(criteria, {target:this, methodName:"filterComplete"}, context);

        // getOriginalData() will account for grouping
        var data = this.pickList.getOriginalData();
        if (synchronousFilter && this.pickList.data.getLength() > 0 &&
            (this.pickList.data.rowIsLoaded && !this.pickList.data.rowIsLoaded(0)))
        {
            this.logInfo("filterData with new criteria caused async fetch even though " +
                "data.willFetchData() returned false.", "pickListFilter");
            synchronousFilter = false;
        }

        if (synchronousFilter) this.filterComplete();
        else {
            this._fetchingPickListData = true;
            if (isc.Canvas.ariaEnabled()) {
                this.pickList.setAriaState("busy", true);
                this.clearAriaState("activedescendant")
            }
            if (!alreadyFetching) {
                this.pickList._fetchingForItem = this.getID();
            }
        }
    },

    _sharedPickListFetchComplete : function (resultSet, dsResponse, data, request) {
        if (this._sharedPickListFetchRequest != request.internalClientContext.requestIndex) {
            return;
        }
        this.ignore(resultSet, "fetchRemoteDataReply");
        this.filterComplete(dsResponse,data,request,true);
    },


    // defaultToFirstOption: whenever new data arrives for the first row, default to it if
    // our value is currently unset.
    handleDataArrived : function (startRow,endRow,data) {
        if (this.defaultToFirstOption && this.getValue() == null && startRow == 0) {
            this.setToDefaultValue();
        }
         // helper - if we're currently showing a data value and we just loaded the associated
        // display value, display it.
        this._updateDisplayValueForNewData();
        // update our local "displayFieldValueMap" cache (used at the FormItem level)
        this._addDataToDisplayFieldCache(data.getRange(startRow,endRow), true);
        var pfc = this._processingFilterComplete;
        // setting this flag avoids 'updateValueMap' from hiding the PickList if its showing
        // even though this isn't technically a "filterComplete" flow
        this._processingFilterComplete = true;
        this.updateDisplayValueMap(false);
        this._processingFilterComplete = pfc;

        if (this.dataArrived) this.dataArrived(startRow,endRow,data);
    },
    //> @method PickList.dataArrived()
    // If this item is showing a dataBound pickList, this notification method will be fired
    // when new data arrives from the server.
    // @param startRow (int) index of first row returned by the server
    // @param endRow (int) index of last row returned by the server
    // @param data (ResultSet) pointer to this pickList's data
    // @visibility external
    //<
    dataArrived : function (startRow, endRow, data) {
    },


    // -- Client side filtering --

    //> @attr PickList.textMatchStyle (TextMatchStyle : "startsWith" : IR)
    // When applying filter criteria to pickList data, what type of matching to use.
    // <P>
    // For a databound pickList (+link{optionDataSource} set), <code>textMatchStyle</code> is
    // sent to the server as +link{dsRequest.textMatchStyle}.
    // <P>
    // For a non-databound pickList, <code>textMatchStyle</code> is applied by
    // +link{filterClientPickListData()}.
    //
    // @visibility external
    //<
    textMatchStyle : "startsWith",

    //> @attr PickList.showAllOptions (boolean : null : IR)
    // If true, even non-matching options will be shown, with configurable
    // +link{separatorRows,separator rows} in between.  Not valid for
    // +link{optionDataSource,databound pickLists}.
    //
    // @visibility external
    //<

    //> @attr PickList.separatorRows (Array of ListGridRecord : [{isSeparator:true}] : IR)
    // Array of records to show between matching and non-matching rows in the PickList.
    // <P>
    // Not valid for +link{optionDataSource,databound pickLists}.
    //
    // @visibility external
    //<

    //> @method PickList.filterClientPickListData()
    // Returns the data to display in the pick list.
    // <P>
    // The default implementation applies the criteria returned by
    // +link{PickList.getPickListFilterCriteria()} to the data returned by
    // +link{PickList.getClientPickListData()}.  A record passes the filter if it has a
    // matching value for all fields in the criteria object.  Matching is performed according
    // to +link{textMatchStyle}.
    // <P>
    // If +link{PickList.showAllOptions} is set, all values are shown, with matching values
    // shown below a +link{PickList.separatorRows,separator}.
    //
    // @return (Array of ListGridRecord) array of record objects to display in the pickList
    //
    // @visibility external
    //<
    _$substring : "substring",
    separatorRows : [{ isSeparator:true }],
    filterClientPickListData : function () {
        // If the user has not entered enough characters to filter return no matched data
        if (this.isEntryTooShortToFilter && this.isEntryTooShortToFilter()) return null;

        var data = this.getClientPickListData();
        var criteria = this.getPickListFilterCriteria();

        if (criteria == null || isc.isA.emptyObject(criteria)) return data;

        var matches = [],
            nonMatches;

        if (this.showAllOptions) nonMatches = this.separatorRows.duplicate();

        var validCriterion = false;
        for (var fieldName in criteria) {
            var fieldCriterion = criteria[fieldName];
            if (!fieldCriterion || isc.isA.emptyString(fieldCriterion)) continue;

            validCriterion = true;

            // Handle being passed an array

            if (!isc.isAn.Array(fieldCriterion)) fieldCriterion = [fieldCriterion];

            for (var stringIndex = 0; stringIndex < fieldCriterion.length; stringIndex++) {
                var searchString = fieldCriterion[stringIndex];

                // Do a raw conversion to strings if passed non string values

                if (!isc.isA.String(searchString)) searchString += isc.emptyString;

                searchString = searchString.toLowerCase();

                var dataLength = data.getLength(),
                    valueFieldName = this.getValueFieldName();
                for (var i = 0; i < dataLength; i++) {
                    var possibleMatch = data[i][fieldName];
                    // for the valueField, run the record values through mapValueToDisplay() so
                    // they match what the users sees.  XXX running through the valueMap is
                    // appropriate here, but a formatter that returns HTML may screw this up
                    if (this.filterDisplayValue && fieldName == valueFieldName) {
                        possibleMatch = this.mapValueToDisplay(possibleMatch);
                    }

                    // For now we'll do a stringwise comparison regardless of the data type of
                    // the possible match
                    if (!isc.isA.String(possibleMatch)) possibleMatch += "";

                    possibleMatch = possibleMatch.toLowerCase();
                    // Remove any mismatches from the list of options
                    if ((this.textMatchStyle == this._$substring &&
                         !possibleMatch.contains(searchString)) ||
                        (this.textMatchStyle != this._$substring && // assume startsWith
                         !isc.startsWith(possibleMatch, searchString)))
                    {
                        if (this.showAllOptions) nonMatches.add(data[i]);
                    } else {
                        matches.add(data[i]);
                    }
                }
            }
        }

        if (!validCriterion) matches = data.duplicate();
        if (this.showAllOptions && nonMatches.length > 1) matches.addList(nonMatches);
        //this.logWarn("returning: " + this.echoAll(matches));
        return matches;
    },


    // --------------------------------------------------------------------------------------
    // PickList appearance
    // --------------------------------------------------------------------------------------


    // hide empty pickLists?
    shouldHideEmptyPickList : function () {
        if (this.hideEmptyPickList != null) return this.hideEmptyPickList;
        return !this._getOptionsFromDataSource();
    },

    // Position / sizing

    // getPickListPosition;
    getPickListPosition : function () {
        var left = this.getPageLeft();
        // RTL - align right of PickList with right of the item.
        if (this.containerWidget.isRTL() && this.pickList) {
            left += this.getWidth();
            left -= this.pickList.getVisibleWidth();
        }
        return [this.getPageLeft(), this.getPageTop() + this.getHeight()];
    },

    // placePickList: This sets the position in the page scope (Page-level coords)
    // Called when the pickList is about to be shown (or to reposition when already drawn)

    placePickList : function () {

        var pickList = this.pickList;
        // if the pickList is dirty, redraw now. This ensures the reported visible height is correct
        // so isc.PickList._placeAdjacent() does the right thing.
        if (pickList.isDirty() || (pickList.body && pickList.body.isDirty())) {
            pickList.redraw("Refreshing stale pickList content before positioning");

        } else if (!pickList.isDrawn()) {
            isc.Canvas.moveOffscreen(pickList);
            pickList.setVisibility("hidden");
            pickList.draw();
        }

        // Note: using isc.PickList._placeAdjacent() will keep the pickList onscreen if possible.
        if (this.allowPickListToClip) {
            var position = this.getPickListPosition(),
                left = position[0],
                top = position[1];
            pickList.setRect([left, top]);
        } else {
            isc.PickList._placeAdjacent(this, pickList);
        }
    },

    // pickValue
    // This method is fired when a value is selected from the pick list.
    // Implementation will vary depending on the form item to which it is applied
    pickValue : function (value) {}

});

// add some static methods to handle default data-management.
isc.PickList.addClassMethods({

    // optionsFromValueMap()
    // Method to determine set of data to display in the pickList based on the valueMap of
    // a (non databound) pickList item.

    optionsFromValueMap : function (item) {
        var valueMap = item.getValueMap(),
            records = [];

        if (valueMap == null) valueMap = [];

        // We have to turn the valueMap into an array of record type objects.

        var valueField = item.getValueFieldName(),
            displayField = item.getDisplayFieldName();

        if (isc.isAn.Array(valueMap)) {
            for (var i = 0; i < valueMap.length; i++) {
                records[i] = {}
                records[i][valueField] = valueMap[i];
                if (displayField != null) records[i][displayField] = valueMap[i];
            }

        } else if (isc.isAn.Object(valueMap)) {
            var i = 0;
            var type = item.getType(),
                convertToInt, convertToFloat, convertToBoolean;

            if (type != null) {
                if (isc.SimpleType.inheritsFrom(type, "integer"))
                {
                    convertToInt = true;
                } else if (isc.SimpleType.inheritsFrom(type, "float")) {
                    convertToFloat = true;
                } else if (isc.SimpleType.inheritsFrom(type, "boolean")) {
                    convertToBoolean = true;
                }
            }

            for (var j in valueMap) {
                records[i] = {};
                var key = j;
                if (convertToInt) {
                    var intVal = parseInt(key);
                    if (intVal == key) key = intVal;
                } else if (convertToFloat) {
                    var floatVal = parseFloat(key);
                    if (floatVal == key) key = floatVal;
                } else if (convertToBoolean) {
                    var boolVal = (key == "true" ? true : (key == "false" ? false : null));
                    if (boolVal != null) key = boolVal;
                }

                records[i][valueField] = key;
                if (displayField != null) records[i][displayField] = valueMap[j];
                i++;
            }
            records._derivedFromValueMapObject = true;
        }

        return records;
    },


    _placePickListRect : function (width, height, adjacentRect, minWidth, minHeight, isRTL) {
        var pageWidth = isc.Page.getWidth(),
            pageHeight = isc.Page.getHeight(),
            // param will give us negative origin coords if we're in RTL mode
            pageScrollLeft = isc.Page.getScrollLeft(true),
            pageScrollTop = isc.Page.getScrollTop(),
            pageScrollRight = (pageWidth + pageScrollLeft),
            pageScrollBottom = (pageHeight + pageScrollTop),

            adjacentRectLeft = Math.max(pageScrollLeft, Math.min(pageScrollRight,
                adjacentRect[0])),
            adjacentRectTop = Math.max(pageScrollTop, Math.min(pageScrollBottom,
                adjacentRect[1])),
            adjacentRectRight = Math.max(pageScrollLeft, Math.min(pageScrollRight,
                adjacentRect[0] + adjacentRect[2])),
            adjacentRectBottom = Math.max(pageScrollTop, Math.min(pageScrollBottom,
                adjacentRect[1] + adjacentRect[3]));

        var bestI = 0,
            maxArea = null,
            pass = 1;
        for (var i = 0; i < 4; ++i) {
            var boxWidth = ((i % 2 == 0) != isRTL ?
                    pageScrollRight - adjacentRectLeft : adjacentRectRight - pageScrollLeft),
                boxHeight = (i < 2 ?
                    pageScrollBottom - adjacentRectBottom : adjacentRectTop - pageScrollTop),
                newWidth = Math.min(width, boxWidth),
                newHeight = Math.min(height, boxHeight);

            // Pick whatever fits on-screen.
            if (newWidth == width && newHeight == height) {
                bestI = i;
                break;
            }

            // If the rectangle could be resized to fit on-screen then calculate the area that the
            // rectangle would occupy.
            if (newWidth >= minWidth && newHeight >= minHeight) {
                var area = newWidth * newHeight;
                if (maxArea == null || area > maxArea) {
                    maxArea = area;
                    bestI = i;
                }
            }
        }

        var bestBoxWidth = ((bestI % 2 == 0) != isRTL ?
                pageScrollRight - adjacentRectLeft : adjacentRectRight - pageScrollLeft),
            bestBoxHeight = (bestI < 2 ?
                pageScrollBottom - adjacentRectBottom : adjacentRectTop - pageScrollTop),
            bestWidth = Math.min(width, bestBoxWidth),
            bestHeight = Math.min(height, bestBoxHeight),
            bestLeft = ((bestI % 2 == 0) != isRTL ? adjacentRectLeft : adjacentRectRight - bestWidth),
            bestTop = (bestI < 2 ? adjacentRectBottom : adjacentRectTop - bestHeight);

        return [bestLeft, bestTop, bestWidth, bestHeight];
    },

    _placeAdjacent : function (widget, pickList) {
        var pickListRect = pickList.getPeerRect(),
            pickListWidth = pickListRect[2],
            pickListHeight = pickListRect[3],
            adjacentRect = widget.getPeerRect(),
            emptyPickListHeight = (
                (!widget.shouldHideEmptyPickList() &&
                pickList.getTotalRows() < 1 &&
                widget.emptyPickListHeight) || 0),
            minPickListHeight = Math.max(
                1, emptyPickListHeight, pickList.getHeaderHeight()),
            minPickListWidth = Math.max(1, widget.pickListWidth || 0),
            isRTL = widget.containerWidget.isRTL(),
            newPickListRect = isc.PickList._placePickListRect(
                pickListWidth, pickListHeight, adjacentRect,
                minPickListWidth, minPickListHeight, isRTL);

        var newPickListWidth = newPickListRect[2],
            newPickListHeight = newPickListRect[3];
        // assert pickListWidth >= newPickListWidth
        // assert pickListHeight >= newPickListHeight
        if (pickListWidth > newPickListWidth) {
            pickList.setAutoFitMaxWidth(newPickListWidth);
        }
        if (pickListHeight > newPickListHeight) {
            pickList.setAutoFitMaxHeight(newPickListHeight);
        }
        pickList.setPageRect(newPickListRect);
    }
});


} // end of if (isc.ListGrid)...







//>    @class    NativeSelectItem
// Select items rendered using a native HTML select item.
// @visibility internal
//<
// Note: This should be invisible to the developer in most cases. The Developer will define
// a form item with type 'select' and based on 'form.useNativeSelectItems', we'll render out
// a SelectItem, or a NativeSelectItem.


isc.ClassFactory.defineClass("NativeSelectItem", "FormItem");
isc.NativeSelectItem.addClassProperties({
    //>    @attr    isc.NativeSelectItem.DEFAULT_ROW_COUNT    (number : 6 : IRW)
    //        Default maximum number of rows to show for a multiple
    //      select box. (Overridden by selectItem.rows, or height).
    //        @group    appearance
    //        @see selectItem.getElementStyleHTML()
    //<
    DEFAULT_ROW_COUNT:6,

    // Used to track every Native select item on the page
    instances: []

});

// Include the shared SelectItem properties
isc.NativeSelectItem.addProperties(isc._SelectItemProperties);

// Properties to apply to native selects only.
isc.NativeSelectItem.addProperties({

    // Set the height to null by default - will size to 1 row of content.
    height:null,

    //> @attr   nativeSelectItem._hasDataElement    (boolean : true : IRW)
    //      Native Select items have a data element.
    // @group formValues
    // @visibility   internal
    // @see     method:FormItem.hasDataElement
    // @see     method:FormItem.getDataElement
    //<
    _hasDataElement:true,

    // This flag means updateState will apply the result of this.getTextBoxStyle() to this item's
    // data element - appropriate for native text boxes, text areas and selects.
    _dataElementIsTextBox:true,

    // _nativeEventHandlers is a place to specify native event handlers to be applied to the
    // form item element once it has been written into the DOM (without having to override
    // '_applyHandlersToElement()'
    _nativeEventHandlers : {
        // apply a native 'onchange' hander to notify us of changes.
        onchange : isc.FormItem._nativeChangeHandler
    }
});

// add the getOptionsHTML method as a static method so it can be used as a utility routine
//    without requiring the creation of a StaticItem instance
isc.NativeSelectItem.addClassMethods({

    //NativeSelectItem.getOptionsHTML()    (A) output the HTML for a select element's OPTION items
    getOptionsHTML : function (valueMap, selectedValue) {
        var output = isc.SB.create();

        // output each option in turn
        if (isc.isAn.Array(valueMap)) {
            for (var i = 0, len = valueMap.length; i < len; i++) {
                var storedValue = valueMap[i];

                // Note - you can break SelectItems by having a valueMap containing unescaped
                // HTML.  The example I saw was setting up a valueMap like this:
                //  { 0:"<select one>", 1:"Some Value", 2:"Some Other Value"}
                // We could catch this here, but it may be expensive to run 'asHTML()' on a
                // potentially large number of options. Developers must avoid using angle
                // braces in valueMap names.
                //if (isc.isA.String(option)) option = option.asHTML();

                output.append(this._getOptionHTML(storedValue, storedValue, selectedValue));
            }
        } else {
            for (var storedValue in valueMap) {
                var visibleValue = valueMap[storedValue];
                //if (isc.isA.String(option)) option = option.asHTML();

                output.append(this._getOptionHTML(storedValue, visibleValue, selectedValue));
            }
        }

        return output.toString();
    },

    _getOptionHTML : function (storedValue, visibleValue, selectedValue) {
        var template = this._template;
        if (!template) {
            this._selectedOption = " SELECTED ";
            template = this._template = [];

            template[0] = "<OPTION ";
            // [1] SELECTED or blank
            template[2] = ' VALUE="';
            // [3] actual value
            template[4] = '">';
            // [5] visible value
            template[6] = "</OPTION>";
        }
        template[1] = (storedValue == selectedValue ? this._selectedOption : null);
        template[3] = storedValue;
        template[5] = visibleValue;
        return template.join(isc._emptyString);
    },

    // NativeSelectItem.getOptionCount()    (A)
    //            Return the number of option elements in a valueMap.
    getOptionCount : function (valueMap) {
        // output each option in turn
        if (isc.isAn.Array(valueMap)) {
            return valueMap.length;
        } else {
            var count = 0;
            for (var key in valueMap) {
                count++;
            }
            return count;
        }
    }

});


//!>Deferred
isc.NativeSelectItem.addMethods({
    textMatchStyle: "startsWith",

    getPickListFilterCriteria : function () {
        var baseCrit = this.optionCriteria || {};
        return isc.addProperties(baseCrit, this.pickListCriteria);
    },

    init : function () {
        this.Super("init", arguments);
        isc.NativeSelectItem.instances.add(this);

        // have basic optionDataSource
        if (this.optionDataSource) {
            var ds = this.getOptionDataSource();
            var valFld = this.getValueFieldName();
            var dispFld = this.getDisplayFieldName();
            var self = this;

            var context = {
                textMatchStyle:this.textMatchStyle,
                showPrompt:false
            };
            if (this.optionFilterContext != null) isc.addProperties(context, this.optionFilterContext);


            // respect optionOperationId if specified
            if (this.optionOperationId != null) context.operationId = this.optionOperationId;

            var criteria = this.getPickListFilterCriteria();

            // fetch data manually and create a valuemap from the data
            ds.fetchData(criteria, function (dsResponse, data) {
                var valMap;
                if (!dispFld) valMap = [];
                else valMap = {};
                for (var i=0; i < data.getLength(); i++) {
                    var rec = data[i];
                    if (!dispFld) {
                        valMap.add(rec[valFld]);
                    } else {
                        valMap[rec[valFld]] = rec[dispFld];
                    }
                }

                self.setValueMap(valMap);
            }, context);

        }
    },

    destroy : function () {
        isc.NativeSelectItem.instances.remove(this);
        this.Super("destroy", arguments);
    },
    // by putting 'nowrap' on the text box cell we avoid the value icon / text box appearing
    // on different lines
    getTextBoxCellCSS : function () {
        return this._$nowrapCSS
    },

    //> @method nativeSelectItem.setElementReadOnly()
    // Change the read-only state of the form element immediately.
    //<
    setElementReadOnly : function (readOnly) {
        // A native select should be rendered as disabled to simulate read-only.
        this._setElementEnabled(!readOnly && !this.isDisabled());
    },

    // getElementHTML()            output the HTML for this element
    getElementHTML : function (value, dataValue) {
        // since we're redrawing the element, note that we have NOT added an unkown value
        //    to its options.  See nativeSelectItem.setElementValue
        this._unknownValueAdded = false;

        var form = this.form,
            formID = form.getID(),
            output = isc.StringBuffer.newInstance(),
            itemID = this.getItemID(),
            emptyString = isc._emptyString,
            valueIconHTML = this._getValueIconHTML(dataValue),
            result = ""
        ;

        if (this.showValueIconOnly) return valueIconHTML;

        if (valueIconHTML != null) output.append(valueIconHTML);
        if (this._isPrinting() || this.renderAsStatic()) {
            if (this.printFullText) {
                result = isc.StringBuffer.concat(
                    "<SPAN ",this.getElementStyleHTML(), ">",
                    dataValue == null ? "&nbsp;" : dataValue.asHTML(), "</SPAN>"
                );
            } else {
                result = isc.StaticTextItem._instancePrototype.getElementHTML.apply(this, arguments);
            }
        } else {
            output.append(
                        "<SELECT",
                        " NAME=" , this.getElementName(),
                        " ID=", this.getDataElementId(),

                        // hang a flag on the element marking it as the data element for the
                        // appropriate form item.
                        this._getItemElementAttributeHTML(),

                        (!this.showTitle && this.accessKey != null ?
                            " ACCESSKEY=" + this.accessKey : emptyString),
                         ((this.isReadOnly() || this.isDisabled()) ? " DISABLED "
                                                                                  : emptyString),
                        this.getElementStyleHTML(),
                        (this.multiple ? " MULTIPLE" : emptyString),
                        " TABINDEX=", this._getElementTabIndex()," handleNativeEvents=false>");

            output.append(this.getOptionsHTML(this.getValueMap()));
            output.append("</SELECT>");
            result = output.toString();
        }
        return result;
    },

    // Fired in response to a native onchange event
    _handleElementChanged : function (waited) {


        if (isc.Browser.isIE && !waited) {
            isc.Timer.setTimeout(this.getID() + "._handleElementChanged(true)", 10);
            return true;
        }
        return this.form.elementChanged(this.getID());

    },

    // If changeOnBlur is true, fire change after blur.
    _nativeElementBlur : function (element, itemID) {
        var returnVal = this.Super("_nativeElementBlur", arguments);

        if (this.changeOnBlur) this.form.elementChanged(this);
    },


    //getOptionsHTML()    output the HTML for a select element's options
    getOptionsHTML : function (valueMap) {
        var output = isc.NativeSelectItem.getOptionsHTML(valueMap? valueMap : this.getValueMap());

        if (this.isSelectOther) {
            output += "<OPTION VALUE=\"" + this.separatorValue + "\">" + this.separatorTitle
                    + "<OPTION VALUE=\"" + this.otherValue + "\">" + this.otherTitle
            ;
        }

        return output;

    },


    // getOptionCount()    (A)     Return the number of option elements in a valueMap.
    getOptionCount : function (valueMap) {
        return isc.NativeSelectItem.getOptionCount(valueMap? valueMap : this.getValueMap());
    },

    getTextBoxStyle : function () {
        if (this._isPrinting()) return isc.TextItem.getInstanceProperty("textBoxStyle");
        else return this.Super("getTextBoxStyle", arguments);
    },

    //  NativeSelectItem.getElementStyleHTML()    (I)
    //          Get the HTML string used to set the visual characteristics for a select item.
    //          This includes the STYLE=... & CLASS=... properties to be written into this
    //          form item's element.
    //          Uses this.height, this.width and this.rows to calculate desired size
    //          In most DOM browsers uses CSS styling to apply width (and height for multiple-select
    //          style).
    //          In Nav / IE 5.x calculates the appropriate number of rows to make the widget the
    //          desired height.
    //
    //        @group    appearance
    //        @return    (string)    String of HTML containing STYLE=... & CLASS=... properties for
    //                          this items element.
    //
    getElementStyleHTML : function () {

        var output = isc.SB.create(),
            style = isc.SB.create();
        if (this.textBoxStyle != null) output.append(" CLASS='", this.getTextBoxStyle(), "' ");

        // There are two interfaces for this item:
        // - the default single-select interface "drop list"
        // - the default multiple-select interface "pick list"

        // Pick List - determine the desired number of rows, and set the height for the item.
        if (this.multiple || this.rows) {

            //desired number of rows
            var rows = this.rows;

            // default rows if necessary (ensures a pickbox is drawn and that it looks consistent
            // across browsers).
            if (!isc.isA.Number(rows) || rows < 1)
                        rows = Math.min(isc.NativeSelectItem.DEFAULT_ROW_COUNT, this.getOptionCount());

            // If the height was specified, respect the specified height.

            if (this.height) {


                if (isc.isA.Number(this.height)) style.append("HEIGHT:", this.height, "px;");
            }

            output.append(" SIZE=", rows);
        }
        // otherwise were using a drop-list - allow the default form item handling to set the
        // height for the Dynamic Form table cell.

        // DOM specific styling code
        if (isc.Browser.isDOM) {


            var width = this.getElementWidth();
            if (isc.isA.Number(width)) {

                // Don't attempt to write out a negatively sized element!
                width = Math.max(width, 1);

                // output the width as a CSS WIDTH property
                style.append("WIDTH:", width, "px;");
            }

            // In Mozilla we must use the 'moz-user-focus' css property to govern
            // whether this element can recieve focus or not.
            if (isc.Browser.isMoz) {
                style.append("-moz-user-focus:",
                             (this._getElementTabIndex() > 0 ? "normal;" : "ignore;")
                );
            }

            // Force an explicit top and bottom margin of zero - by default there's
            // 1px above and below, increasing the total size of the item
            style.append("margin-top:0px;margin-bottom:0px;");

            style = style.toString();
            if (style.length > 0) output.append(" STYLE='", style, "'");
        }

        return output.toString();
    },

    // We allow SelectItems with no explicitly specified size, to size based on the content
    // of the select - so we avoid writing a width property into the element unless we need to
    // Modify _iconVisibilityChanged() to similarly avoid writing a width property into the
    // element unless it's required, so we don't react to an icon showing/hiding by shrinking
    // or growing the select item unnecessarily.
    _iconVisibilityChanged : function () {
        if (!isc.isA.Number(this.width)) return;
        return this.Super("_iconVisibilityChanged", arguments);
    },


    _getIconVMargin : function () {
        return 0;
    },

    mapValueToDisplay : function (dataValue) {
        if (isc.isAn.Array(dataValue)) {
            var displayArray = [];
            for (var i = 0; i < dataValue.length; i++) {
                displayArray[i] = this.mapValueToDisplay(dataValue[i]);
            }
            return displayArray;
        }
        return this.Super("mapValueToDisplay", arguments);
    },

    //selectItem.setElementValue() : set the value of the form element to the value passed in
    setElementValue : function (newDisplayValue, newValue) {

        // Select items support a 'native' value map - each item has the data value (option.value)
        // and the display value (option.text) specified.
        // We select the item by setting the element's value to the data value.
        // - If the 'data' value is not passed to this method - assume data / display values
        //   match
        // - If we don't fine an option with a .value that matches the value passed in, also
        //   check option.text as we may have been passed the display value only. This
        //   can happen if a developer calls 'item.setValue(<displayValue>)'
        if (arguments.length  == 1) newValue = newDisplayValue;

        // get a pointer to the element for this item
        var element = this.getDataElement();

        // if we don't currently have an element, bail
        if (!element) return null;

        // iterate through each option
        var options = element.options;

        if (!options) {
            //>DEBUG
            this.logDebug("setElementValue(): element.options is null. ???");
            //<DEBUG
            return null;    //???
        }

        // always update the value icon (if we're showing one)
        this._updateValueIcon(newValue);

        // if this is a single-select item, set its selectedIndex
        if (!this.multiple) {
            // normalize a null value to the empty string
             // this fixes a problem in Nav where it creates items called "undefined"
            if (newValue == null) newValue = "";

            // look for an option with matching value
            for (var i = 0; i < options.length; i++) {
                if (options[i].value == newValue) {
                    // only update the selectedIndex if it's not already correct, otherwise the
                    // native select scrolls unnecessarily
                    if (element.selectedIndex != i) {
                        element.selectedIndex = i;
                    }
                    return element.selectedIndex;
                }
            }

            // no matching value - look for an option with matching text
            for (var i = 0; i < options.length; i++) {
                if (options[i].text == newValue) {
                    // only update the selectedIndex if it's not already correct, otherwise the
                    // native select scrolls unnecessarily
                    if (element.selectedIndex != i) {
                        element.selectedIndex = i;
                    }
                    element.selectedIndex = i;
                    return element.selectedIndex;
                }
            }

            // add a new form option with the value
            if (this.addUnknownValues) {

                if (isc.Browser.isIE) {
                    var newElementNum = 0;
                    // if we've already added an unknown value since the list was redrawn,
                    //  simply munge that value (rather than adding a new one).
                    if (this._unknownValueAdded) {
                        options[newElementNum].text = newDisplayValue;
                        options[newElementNum].value = newValue;
                    } else {
                        options.add(new Option(newValue, newDisplayValue), newElementNum);
                        this._unknownValueAdded = true;
                    }

                } else {
                    if (this._unknownValueAdded) {
                        var newElementNum = options.length-1;
                        options[newElementNum].value = newValue;
                        options[newElementNum].text = newDisplayValue;
                    } else {
                        var newElementNum = options.length;
                        options[newElementNum] = new Option(newValue, newDisplayValue);
                        this._unknownValueAdded = true;
                    }
                }

                // only update the selectedIndex if it's not already correct, otherwise the
                // native select scrolls unnecessarily
                if (element.selectedIndex != newElementNum) {
                    element.selectedIndex = newElementNum;
                }

                return element.selectedIndex;
            } else {
                // not found -- return null
                return null;
            }

        // otherwise it's multi-select item
        } else {

            // "newValue" is the list of selected values - normalize to an array
            if (newValue == null) {
                newValue = [];
            } else if (isc.isA.String(newValue) && newValue.contains(",")) {
                newValue = newValue.split(",");
            } else if (!isc.isAn.Array(newValue)) {
                newValue = [newValue];
            } else {
                // duplicate the array since we're changing it below (and don't want to screw
                // up the original)
                newValue = newValue.duplicate();
            }

            // same thing with newDisplayValue - may be required if we're adding unknown values
            if (newDisplayValue == null) {
                newDisplayValue = [];
            } else if (isc.isA.String(newDisplayValue) && newDisplayValue.contains(",")) {
                newDisplayValue = newDisplayValue.split(",");
            } else if (!isc.isAn.Array(newDisplayValue)) {
                newDisplayValue = [newDisplayValue];
            } else {
                // duplicate the array since we're changing it below (and don't want to screw
                // up the original)
                newDisplayValue = newDisplayValue.duplicate();
            }

            // set option.selected on native option elements.  "newValue" will retain only the
            // values for which there are no native option elements.
            for (var i = 0; i < options.length; i++) {
                var option = element.options[i];

                var valueIndex = newValue.indexOf(option.value);

                // only update option.selected if it's not already correct, otherwise the
                // native select scrolls unnecessarily
                if (valueIndex > -1) {
                    if (option.selected != true) option.selected = true;
                    newValue.removeItem(valueIndex);
                } else {
                    if (option.selected != false) option.selected = false;
                }
            }

            // iterate through the options a second time in case we were passed display values
            if (newValue.length != 0) {
                for (var i = 0; i < options.length; i++) {
                    var option = element.options[i];
                    var valueIndex = newValue.indexOf(option.text);
                    if (valueIndex > -1) {
                        if (option.selected != true) option.selected = true;
                        newValue.removeItem(valueIndex);
                    }
                }
            }

            // if some values in "newValue" had no corresponding option element, add more
            // option elements to the native multi-select
            if (newValue.length != 0 && this.addUnknownValues) {

                for (var i = 0; i < newValue.length; i++) {
                    var newOption = options[options.length] =
                            new Option(newValue[i], newDisplayValue[i]);
                    newOption.selected = true;
                }
            }
            // XXX: this will be the values that had to be added, not the new value
            return newValue;
        }
    },

    // getRawElementValue()     return the value stored in the form element(s) for this item.
    getElementValue : function () {
        // get a pointer to the element for this item
        var element = this.getDataElement();

        // if no element was found, bail
        if (!element) return null;

        var options = element.options;
        if (!options || options.length == 0) return null;

        // if we're dealing with a single-select option
        if (!this.multiple) {
            var option = options[element.selectedIndex];
            // if no option found, forget it
            if (!option) return null;
            return (option.value != null ? option.value : option.text);

        // otherwise if a multi-select item, return an array
        } else {
            var output = [];
            // for each option
            for (var i = 0; i < options.length; i++) {
                var option = options[i];
                // if that option is selected
                if (option.selected) {
                    // add its value (or text if value was not specified) to the output
                    output.add(option.value != null ? option.value : option.text);
                }
            }
            // if zero or one values were selected, return the value rather than an array
            if (output.length < 2) return output[0];
            // otherwise return the array of values
            return output;
        }
    },

    //setElementValueMap        Set the valueMap for the actual form element to those passed in.
    setElementValueMap : function (valueMap) {
        // since we're resetting the element's options,
        //    note that we have NOT added an unkown value to its options.
        //    See nativeSelectItem.setElementValue
        this._unknownValueAdded = false;

        this.Super("setElementValueMap", arguments);
        // if this item doesn't currently have an element, bail
        var element = this.getDataElement();
        if (element == null) return;

        // get a pointer to the original options
        var elementOptions = element.options;
        // clear the elementValueMap
        elementOptions.length = 0;

        // add the new options
        if (isc.isAn.Array(valueMap)) {
            // array where key and value are the same
            for (var i = 0; i < valueMap.length; i++) {
                elementOptions[i] = new Option(valueMap[i], valueMap[i])
            }
        } else {
            // object of key:value pairs
            for (var key in valueMap) {
                elementOptions[elementOptions.length] = new Option(valueMap[key],key)
            }
        }

        if (this.isSelectOther) {

            // add the separators
            elementOptions[elementOptions.length] = new Option(this.separatorTitle, this.separatorValue);
            elementOptions[elementOptions.length] = new Option(this.otherTitle, this.otherValue);


        }
    },

    // override upateValue to handle 'selectOther' functionality
    updateValue : function () {

        if (this.isSelectOther) {

            // we have to re-implement some of the default updateValue function to get at the element
            // value.
            if (!this.hasElement() || this.getDataElement() == null) return;

            var oldValue = this._value,
                value = this.getElementValue();

            // if they selected the separator, return false to reject the value
            if (value == this.separatorValue) {
                this.setValue(oldValue);
                return false;
            }

            if (value == this.otherValue) {
                var oldTitle = this.getValueMapTitle(oldValue);
                value = prompt("Other value for \r'"+this.getTitle()+"'?", (oldTitle ? oldTitle : ""));
                if (value == null) {
                    this.setValue(oldValue);
                    return false;
                }

                // set the value of the field to value
                //    this has the side effect of adding the element to the field
                // - we'll continue through Superclass 'updateValue' implementation to handle firing
                //   change handlers, and actually saving the value.
                this.setElementValue(value);
            }
        }

        return this.Super("updateValue", arguments);
    }

});
//!<Deferred



// ** Limitations when working with a databound pickList. **
// We support databinding the pickList by setting optionDataSource (implemented in the
// PickList interface - not available for NativeSelectItems).
// We currently DO NOT support
//  - 'isSelectOther' behavior
// on select items with databound pickLists.





// Class will not work without the ListGrid
if (isc.ListGrid != null) {




//>    @class    SelectItem
// FormItem that allows picking between several mutually exclusive options via a select list.
// <P>
// Options may be derived from a <code>dataSource</code> or a <code>valueMap</code>.
// <P>
// Note that to select the first option as a default value for the item,
// +link{SelectItem.defaultToFirstOption} may be set.
//
// @implements PickList
// @see PickList.optionDataSource
// @see formItem.valueMap
// @visibility external
// @example selectItem
//<
isc.ClassFactory.defineClass("SelectItem", "FormItem");

// When the developer specifies a form item as type 'select', depending on the form's
// useNativeSelectItems property, we may give them a SelectItem, or a NativeSelectItem.
// The APIs and defaults for these classes match, but the implementations are completely
// different.
// Define 'select' type property defaults (to be applied to both classes) in a separate
// object to avoid duplication.

isc._SelectItemProperties = {

    // Have native and synthetic selects' text styling match
    textBoxStyle:"selectItemText",
    printTextBoxStyle:"textItem",

    //> @attr selectItem.height (number : 19 : IRW)
    // @include FormItem.height
    //<
    height:19,
    width:150,

    // Override the 'dirtyOnKeyDown' property to avoid marking the form item as dirty on every
    // keyDown. We'll handle marking as dirty explicitly when the value should be updated,
    // without being saved to the form or firing the change handler.
    dirtyOnKeyDown:false,
    // Similarly avoid firing 'updateValue' on every keypress - we will handle when to save the
    // value out.
    changeOnKeypress:false,

    // Override redrawOnShowIcon - we can to avoid redrawing the entire form when showing
    // or hiding icons for this item.
    redrawOnShowIcon:false,

    //> @attr selectItem.addUnknownValues (Boolean : true : IRWA)
    // If this item's value is set (via +link{setValue()} or similar) to a value which
    // is not present in the +link{valueMap}, should the value be rejected.
    // <P>
    // If set to <code>false</code> the setValue() call will have no effect if the value
    // is not a valid option.
    // <P>
    // If set to <code>true</code> the item's value will be update to the new value, and
    // the value will be added to the set of options displayed in the pick-list until the
    // next call to +link{setValueMap()} or +link{setValue()}.
    // <P>
    // Exception: If the value is set to <code>null</code> but there is no null entry in the
    // valueMap for this item, setting <code>addUnknownValues</code> to true will not cause
    // a null option to show up at the top of the select item pickList. Whether an empty
    // option is shown in the pickList is governed by +link{SelectItem.allowEmptyValue}
    // instead.
    // <P>
    // Note that this property has no effect if the selectItem has a specified
    // +link{optionDataSource}. If +link{setValue()} is called on a databound SelectItem
    // and the new value does not match any loaded options, the value will be accepted,
    // but not added to the options displayed in the pickList. Also note that if a
    // +link{displayField} exists, a fetch will be performed in an attempt to retrieve
    // a valid display value, as described under +link{formItem.fetchMissingValues}.
    //
    // @visibility external
    // @example comboFormDependentSelects
    //<

    addUnknownValues:true,

    //> @attr SelectItem.defaultValue (boolean : null : IRW)
    // Static default value for this SelectItem. To default to the first option use
    // +link{SelectItem.defaultToFirstOption} instead.
    // @visibility external
    //<

    //> @method SelectItem.defaultDynamicValue() (A)
    // Expression evaluated to determine the +link{SelectItem.defaultValue} when no value is
    // provided for this item. To default to the first option use
    // +link{SelectItem.defaultToFirstOption} instead.
    // @visibility external
    //<

    //> @attr SelectItem.defaultToFirstOption (Boolean : false : IRW)
    // Select the first option as the default value for this SelectItem.
    // <P>
    // If options are derived from a dataSource, the first value returned by the server will be
    // used, otherwise the first value in the valueMap.  Note that setting this property to
    // true will trigger a fetch at soon as the form is created, because the form will try to
    // establish a default value at that time.
    // <P>
    // If enabled, this setting overrides +link{SelectItem.defaultValue} and
    // +link{SelectItem.defaultDynamicValue}.
    //
    // @visibility external
    //<

    autoSizePickList:true,

    //>    @attr selectItem.multiple (Boolean : false : IRW)
    // If true, multiple values may be selected.
    // <P>
    // The SelectItem will either render as a drop-down allowing multiple selections, or a
    // multi-row list of options similar to a small headerless +link{ListGrid}, based on the
    // +link{multipleAppearance} setting.
    // <P>
    // The logical value of the formItem, as retrieved by +link{formItem.getValue,getValue()}
    // and set via +link{formItem.setValue,setValue()}, is an Array of Strings reflecting the
    // selected values.
    // <P>
    // When this value is true, we disable doubleClick events by default, instead issuing two
    // single clicks by forcibly setting +link{Canvas.noDoubleClicks, noDoubleClicks: true}.
    // If you need to work with doubleClick events, you can disable this default behavior by
    // explicitly setting formItem.pickListProperties.noDoubleClicks: false.
    // <P>
    // Note: <code>multiple:true</code> SelectItems with multipleAppearance:"grid" do not
    // currently support optionDataSource binding.  You can get around this by calling
    // +link{DataSource.fetchData()} directly and calling
    // +link{list.getValueMap,dsResponse.data.getValueMap()} to obtain a valueMap.
    //
    // @group    appearance
    // @visibility external
    //<
    // Note that if multipleAppearance is "grid", this currently uses NativeSelectItem

    //> @type MultipleAppearance
    // Appearance for a SelectItem that allows multiple selection
    //
    // @value "picklist" a drop-down picklist that allows multiple choices by
    //              clicking on a checkbox next to each item
    // @value "grid" a grid that displays all items in-place. Multiple selection is
    //              accomplished by ctrl-click or shift-click.
    // @visibility external
    //<

    //> @attr SelectItem.multipleAppearance   (MultipleAppearance : "picklist" : IR)
    // How should items with +link{SelectItem.multiple} set to 'true' be displayed?
    // @visibility external
    //<
    multipleAppearance: "picklist",

    //> @attr selectItem.useClientFiltering (Boolean : null : IRA)
    // @include pickList.useClientFiltering
    // @visibility external
    //<

    // ---------------------------
    // SelectOther is a behavior implemented on select items
    // This is doc'd as a separate form item type, and can be specified by creating a
    // form item of type 'selectOther' (or setting the 'isSelectOther' flag on a select item)
    // ---------------------------


    // isSelectOther - flag to specify this as a special 'selectOther' type item.
    // may be set up by the FormItemFactory when the item is created
    //isSelectOther:false,

    //>    @class    SelectOtherItem
    //
    // FormItem that shows a list of options, plus an "Other..." option that allows them to enter
    // another value.
    //
    // @treeLocation Client Reference/Forms/Form Items
    // @visibility external
    //<

    //>    @attr    selectOtherItem.separatorTitle        (string : "--------------------" : IRW)
    // Title for the separator between normal items and the <code>Other...</code> item in the drop
    // down list.  Selecting this item will not change the FormItem's value.
    // @group appearance
    // @visibility external
    //<
    separatorTitle:"--------------------",

    //>    @attr    selectOtherItem.separatorValue        (string : "----" : IRWA)
    // Value for the separator item between normal items and the <code>Other...</code> value.
    // If necessary the value may be changed to ensure it doesn't collide with any data values in
    // this item's +link{formItem.valueMap,valueMap}.
    // @group appearance
    // @visibility external
    //<
    separatorValue:"----",

    //>    @attr    selectOtherItem.otherTitle        (string : "Other..." : IRW)
    // Title for the <code>Other...</code> item. When this item is selected, the user will be
    // shown a prompt allowing them to enter a new value for the item.
    // @group appearance
    // @group i18nMessages
    // @visibility external
    //<
    otherTitle:"Other...",

    //>    @attr    selectOtherItem.otherValue        (string : "***other***" : IRWA)
    // Data value for the <code>Other...</code> item. If necessary this value may be changed to
    // ensure it doesn't collide with any data values in this item's
    // +link{formItem.valueMap,valueMap}.
    // @group appearance
    // @visibility external
    //<
    otherValue:"***other***"

};


// Include the shared select item properties
isc.SelectItem.addProperties(isc._SelectItemProperties);

// The following properties apply only to SelectItems (not NativeSelectItems)
isc.SelectItem.addProperties({


    //> @attr selectItem.showPickerIcon (Boolean : true : IRW)
    // @include FormItem.showPickerIcon
    // @visibility external
    //<
    showPickerIcon:true,

    //> @attr SelectItem.emptyDisplayValue (HTMLString : "&nbsp;" : IRW)
    // @include FormItem.emptyDisplayValue
    //<
    // override emptyDisplayValue to show a nonbreaking space so styling works correctly
    emptyDisplayValue:"&nbsp;",

    //> @method SelectItem.showPicker()
    // @include FormItem.showPicker()
    // @visibility external
    //<

    //> @attr SelectItem.textBoxStyle (FormItemBaseStyle : "selectItemText", [IRA])
    // @include FormItem.textBoxStyle
    // @visibility external
    // @group appearance
    //<
    // This is applied to the visible, selected value in the select item.


    //> @attr SelectItem.controlStyle (FormItemBaseStyle : "selectItemControl", [IRA])
    // @include FormItem.controlStyle
    // @group appearance
    // @visibility external
    //<
    controlStyle:"selectItemControl",

    //> @attr selectItem.pickerIconStyle (FormItemBaseStyle : "selectItemPickerIcon" : IRW)
    // @include FormItem.pickerIconStyle
    // @visibility external
    //<
    pickerIconStyle:"selectItemPickerIcon",



    // Select items can accept focus

    canFocus:true,

    //> @attr SelectItem.showFocused    (Boolean : true, [IRWA])
    // @include FormItem.showFocused
    // @visibility external
    //<
    showFocused:true,

    //> @attr selectItem.pickerIconWidth
    // @include FormItem.pickerIconWidth
    // @visibility external
    //<

    //> @attr selectItem.pickerIconHeight
    // @include FormItem.pickerIconHeight
    // @visibility external
    //<

    //> @attr selectItem.pickerIconSrc (SCImgURL : "[SKIN]/DynamicForm/SelectItem_PickButton_icon.gif" : IRWA)
    // @include FormItem.pickerIconSrc
    // @visibility external
    //<
    pickerIconSrc:"[SKIN]/DynamicForm/SelectItem_PickButton_icon.gif",

    // Override pickerIconDefaults
    // Don't make this pickerIcon independantly focusable - we only want the user to focus
    // once on the form item
    pickerIconDefaults : {

        //imgOnly:true
        tabIndex: -1,
        click : isc.Class.NO_OP
    },

    // Clip the content.

    clipValue:true,

    // -----------------------
    // Deprecated:

    //> @attr SelectItem.hiliteOnFocus   (boolean : true : [IRWA])
    // Should this SelectItem show a hilite when it receives keyboard focus?
    // @visibility external
    // @deprecated As of SmartClient version 5.5, use +link{SelectItem.showFocused} instead.
    //<

    //> @attr    SelectItem.hiliteColor (string : "#316AC5": IRWA)
    // Background color to apply to the select item's selected value when the SelectItem
    // receives focus, if <code>hiliteOnFocus</code> is true.
    // @visibility external
    // @deprecated As of SmartClient version 5.5, if +link{SelectItem.showFocused} is true,
    //  styling will be updated for this form item on focus. The hiliting effect can therefore
    //  be achieved via +link{SelectItem.textBoxStyle} and +link{SelectItem.controlStyle}
    //  instead.
    //<

    //> @attr    SelectItem.hiliteTextColor (string : "white": IRWA)
    // Text color to apply to the select item's selected value when the SelectItem
    // receives focus, if <code>hiliteOnFocus</code> is true.
    // @visibility external
    // @deprecated As of SmartClient version 5.5, if +link{SelectItem.showFocused} is true,
    //  styling will be updated for this form item on focus. The hiliting effect can therefore
    //  be achieved via +link{SelectItem.textBoxStyle} and +link{SelectItem.controlStyle}
    //  instead.
    //<

    //> @attr    SelectItem.pickButtonWidth  (number : null : IRWA)
    // How large should the pick button be rendered?
    // @visibility external
    // @deprecated As of SmartClient version 5.5, pickButtonWidth has been deprecated in
    //  favor of +link{SelectItem.pickerIconWidth}.
    //<

    //> @attr    SelectItem.pickButtonHeight  (number : null : IRWA)
    // How large should the pick button be rendered?
    //
    // @visibility external
    // @deprecated As of SmartClient version 5.5, pickButtonHeight has been deprecated in
    //  favor of +link{SelectItem.pickerIconHeight}.
    //<

    //> @attr    SelectItem.pickButtonSrc  (string : null : IRWA)
    // Source for image to show for the pick button
    // @visibility external
    // @deprecated As of SmartClient version 5.5, pickButtonSrc has been deprecated in
    //  favor of +link{SelectItem.pickerIconSrc}.
    //<

    // ------------



    // Allow customization of the hilite color-scheme applied to the pickList.
    // If specified, overrides styling applied via the selected state of pickListBaseStyle.

    //>@attr    SelectItem.pickListHiliteColor (string : null : IRWA)
    // If specified this property determines the backgroundColor to show highlighted items in the
    // pickList.  By default we don't specify this property, and pick up the styling based on
    // the <code>selected</code> state of the <code>pickListBaseStyle</code>.
    //<
    //pickListHiliteColor:"#316AC5",

    //>@attr    SelectItem.pickListHiliteTextColor (string : null : IRWA)
    // If specified this property determines the text color to show highlighted items in the
    // pickList.  By default we don't specify this property, and pick up the styling based on
    // the <code>selected</code> state of the <code>pickListBaseStyle</code>.
    //<
    //pickListHiliteTextColor:"white",


    //> @attr SelectItem.showOver (Boolean : true : IRWA)
    // When the user rolls over the select item, should the pickButton display it's
    // <code>Over</code> state?
    // @visibility external
    //<
    showOver:true,


    // Non-exposed property governing whether the pickList should be shown with a clickMask,
    // and take focus when shown.
    modalPickList:true,

    //>@attr    SelectItem.fireChangeOnSelect   (boolean : true : IRW)
    // whether +link{FormItem.change()}
    // fires each time the pickList selection changes, or only when the pickList is dismissed.
    //<

    //>@attr    SelectItem.changeOnValueChange (boolean : true : IRW)
    //  If true the change handler for this item will fire when the item has focus and
    //  modifies the selection for the item.
    //  If false, the change handler will only fire when the user leaves a modified selectItem.
    //<



    changeOnValueChange:true,

    //>@attr    SelectItem.changeOnKeyboardNavigation (boolean : true : IRW)
    //  If this.changeOnValueChange is true, and the user is navigating through values via
    //  keypresses such as up and down arrows, while the pick-list is not visible, should we
    //  fire change?
    //  Has no effect if this.changeOnValueChange is false.
    // @visibility internal
    //<

    changeOnKeyboardNavigation:true,

    // Don't allow native text selection of the content of the SelectItem

    canSelectText : false,

    //> @attr selectItem.allowEmptyValue  (Boolean : false : IRW)
    // If set to true, always show an empty option in this item's pickList, allowing the user
    // to clear the value (even if there is no empty entry in the valueMap for the item).
    // <P>
    // The empty value will be displayed with the
    // +link{formItem.emptyDisplayValue,emptyDisplayValue}.
    // <P>
    // With a +link{optionDataSource,databound selectItem}, enabling
    // <code>allowEmptyValue</code> disables data paging - all data matching the
    // +link{pickList.pickListCriteria,current criteria} will be requested.
    //
    // @visibility external
    //<
    allowEmptyValue : false,


    //> @attr selectItem.autoFetchData   (Boolean : true : [IRA])
    // If this select item retrieves its options from a <code>dataSource</code>, should options
    // be fetched from the server when the item is first drawn, or should this fetch be
    // delayed until the user opens the pickList.
    // <P>
    // The default is true in order to allow the user to select a value via keyboard input
    // while keyboard focus is on the SelectItem but the pickList has not actually been shown.
    //
    // @visibility external
    // @see PickList.optionDataSource
    //<
    autoFetchData:true,

    //>@attr SelectItem.showHintInField (boolean : null : IRWA)
    // If showing a hint for this form item, should it be shown within the field?
    // <P>CSS style for the hint is +link{selectItem.textBoxStyle} with the suffix
    // "Hint" appended to it.
    // @group appearance
    // @see FormItem.hint
    // @visibility external
    //<

    //>@attr SelectItem.progressiveLoading (boolean : null : IRW)
    // Indicates whether or not this SelectItem will load its list of options
    // +link{DataSource.progressiveLoading,progressively}.  This property is copied onto the
    // underlying +link{class:PickList}.
    // @see DataSource.progressiveLoading
    // @group progressiveLoading
    // @visibility external
    //<

    //> @attr selectItem.saveOnEnter (Boolean : true : IRW)
    // Select items will submit their containing form on enter keypress
    // if +link{DynamicForm.saveOnEnter,saveOnEnter} is true. Setting this property to
    // <code>false</code> will disable this behavior.
    // <P>
    // Note that if the drop down list of options (pickList) is visible an
    // <code>Enter</code> keypress is used to select a value from the available set of
    // options and will not automatically cause form submission.
    // @visibility external
    //<
    // default implementation of formItem.shouldSaveOnEnter() returns this
    saveOnEnter: true,

    //> @attr selectItem.openOnSpace (Boolean : false : IRW)
    // Causes the PickList to open when the spacebar is pressed, default false.
    // <P>
    // For native OS widgets, space opens the PickList on Macs, but not on Windows.  Consider
    // using this setting if your users are almost entirely Mac users, or enabling it only for
    // users running MacOS.
    // <P>
    // However, before using this setting, consider that it means that Spacebar will not be able
    // to be used for another purpose when focus is in a SelectItem.
    //
    // @visibility external
    //<
    openOnSpace: false,

    //> @attr selectItem.openOnDownArrow (Boolean : false : IRW)
    // Causes the PickList to open when the down arrow is pressed, default false.
    // <P>
    // For native OS widgets, down arrow changes the value of a select on Windows, but opens the
    // select on Macs.  This setting is not recommended unless you are certain that all users of
    // your applications will expect the Mac convention.
    // @visibility external
    //<
    openOnDownArrow: false
});


isc.SelectItem.addMethods({

    // At init time, pick up any deprecated properties applied to this select item.
    init : function () {
        //>!BackCompat 2006.3.9
        if (this.hiliteOnFocus != null) {
            this._warnDeprecated("hiliteOnFocus", "showFocused");
            this.showFocused = this.hiliteOnFocus;
        }

        if (this.pickButtonWidth != null) {
            this._warnDeprecated("pickButtonWidth", "pickerIconWidth");
            this.pickerIconWidth = this.pickButtonWidth;
        }
        if (this.pickButtonHeight != null) {
            this._warnDeprecated("pickButtonHeight", "pickerIconHeight");
            this.pickerIconHeight = this.pickButtonHeight;
        }

        if (this.pickButtonSrc != null) {
            this._warnDeprecated("pickButtonSrc", "pickerIconSrc");
            this.pickerIconSrc = this.pickButtonSrc;
        }
        //<!BackCompat
        return this.Super("init", arguments);
    },

    // For parity with ComboBoxItems, the contents of the SelectItem should be vertically centered.
    _shouldVerticallyCenterTextBox : function () {
        return true;
    },



    // Override drawn() - if this is a databound pickList we want to perform a filter before
    // the pickList itself ever gets shown.

    drawn : function (a,b,c,d) {
        this.invokeSuper(isc.SelectItem, "drawn", a,b,c,d);
        if (this.autoFetchData && this._getOptionsFromDataSource()) {
            this.fetchData(null, null, true);
        }
    },




    _getIconVMargin : function () {
        return 0;
    },

    // Override iconFocus() - if focus goes to the picker icon, shift it to the textbox instead

    _iconFocus : function (id, element) {
        var icon = this.getIcon(id);
        if (icon == this.getPickerIcon()) {
            element.blur();
            this.focusInItem();
            return;
        }

        return this.Super("_iconFocus", arguments);
    },

    //> @method selectItem.setElementReadOnly()
    // Change the read-only state of the form element immediately.
    //<
    setElementReadOnly : function (readOnly) {
        // Don't need to do anything as all changes are handled by styling and the
        // pick icon. This override keeps a redraw from occurring.
    },

    // ---------------------------------------------------------------------------------------
    // EVENTS
    // ---------------------------------------------------------------------------------------

    // Update the pick button appearance when the user rolls over the control table
    handleMouseMove : function () {
        if (this.showOver && !this.isDisabled()) {
            var pickerIcon = this.getPickerIcon();
            if (this._overControlTable() && this._iconShouldShowOver(pickerIcon)) {
                this._setIconImgState(pickerIcon, true);
            } else this._setIconImgState(pickerIcon, false);
        }
        return this.Super("handleMouseMove", arguments);
    },

    handleMouseOut : function () {
        if (this.showOver && !this.isDisabled()) {
            this._setIconImgState(this.getPickerIcon(), false);
        }
        return this.Super("handleMouseOut", arguments);
    },

    // on iconMouseOut - if the user rolled off the picker icon, but is still over the
    // control table - don't clear the picker icon hilight
    _iconMouseOut : function (id, a,b,c,d) {
        if (this.getIcon(id) == this.getPickerIcon() && this._overControlTable()) return;
        return this.invokeSuper("SelectItem", "_iconMouseOut", id, a,b,c,d);
    },

    // Override click to show the pick list
    handleClick : function () {
        if (!(this.isDisabled()) && !(this.isReadOnly())) {
            // Give this item explicit focus before we show the pickList
            // This should only be done if the item is not disabled, otherwise
            // focusInItem() causes a recursion error.

            this.focusInItem();
            this.showPickList();
        }
        // call to Super fires any developer specified click handlers
        return this.Super("handleClick", arguments);
    },

    // Override handleKeyPress to allow navigation via typing the first letters of valid
    // options
    handleKeyPress : function (event, eventInfo) {

        var returnVal = this.Super("handleKeyPress", arguments);
        if (returnVal == false) return false;

        // If item is read-only, there is nothing more to do
        if ((this.isDisabled()) || (this.isReadOnly())) return returnVal;

        var keyName = event.keyName;

        // On Enter keyPress resolve "other..." (or separator) selection to a meaningful value.
        if (keyName == "Enter" && this.isSelectOther) {
            if (this._selectOtherValue != null) this.updateValue();
        // Navigate where appropriate
        } else if (keyName == "Arrow_Down") {
            if (this.openOnDownArrow) {
                // note that this has the same effect as the altKeyDown check below, will still
                // occur, even if openOnDownArow is false
                this.showPickList();
            } else {
                if (isc.EH.altKeyDown()) this.showPickList();
                else this.moveToNextValue(1);
            }
            // Don't allow scrolling, etc
            returnVal = false;

        } else if (keyName == "Arrow_Up") {
            if (isc.EH.altKeyDown()) this.showPickList();
            else this.moveToNextValue(-1);
            // Don't allow scrolling, etc
            returnVal = false;

        } else if (keyName == "Home") {
            this.moveToFirstValue();
            // Don't allow scrolling, etc
            returnVal = false;
        } else if (keyName == "End") {
            this.moveToLastValue();
            // Don't allow scrolling, etc
            returnVal = false;
        } else if (keyName == "Space" && this.openOnSpace) {
            this.showPickList();
        } else {
            // If the user hit the first char of one of our options, move to that option
            var charVal = event.characterValue;
            if (charVal != null) {
                this.moveToChar(charVal);
            }
        }

        return returnVal;
    },

    // On Blur, if we were marked as dirty, fire the change handler
    _nativeElementBlur : function (element, itemID) {
        var returnVal = this.Super("_nativeElementBlur", arguments);
        if (this.changeOnBlur || this._itemValueIsDirty() || this._selectOtherValue) {

            if (isc.Browser.isMoz && this._selectOtherValue == this.otherValue)
                this.form.__suppressFocusHandler = true;
            this.updateValue();
        }

        return returnVal;
    },



    showPickList : function (waitForData, queueFetches) {
        var interfaceShowPickList = isc.PickList.getPrototype().showPickList;
        interfaceShowPickList.apply(this, arguments);
        if (this.pickList) {
            this.pickList.deselectAllRecords();
            // if either a value was passed in or this.defaultToFirstOption: true, select now
            if (this.getValue() != null) this.selectItemFromValue(this.getValue());
        }
    },

    // Override handleEditorExit() - when fired from a blur, if the pickList is visible we
    // don't want to fire the editorExit method, since focus is going to the pickList
    handleEditorExit : function () {

        if (this._showingPickList) return;
        return this.Super("handleEditorExit", arguments);
    },


    // When the pick list is shown, fire editorEnter - essentially interacting with the picklist
    // is the same as interacting with this item.
    _pickListShown : function () {
        // Note: this will no-op if we've already fired editorEnter()
        this.handleEditorEnter();
        var interfacePickListShown = isc.PickList.getPrototype()._pickListShown;
        interfacePickListShown.apply(this, arguments);
    },

    // override _pickListHidden so that change notifications can be fired when
    // fireChangeOnSelect is false, and the item is in multi mode
    _pickListHidden : function () {
        if (this.fireChangeOnSelect == false) this.updateValue();
        var interfacePickListHidden = isc.PickList.getPrototype()._pickListHidden;
        interfacePickListHidden.apply(this, arguments);
    },

    // Navigation methods:



    // helper to get the locally loaded set of options:
    // Returns set of options if the pickList is not databound
    // For databound picklists, returns the set of all rows, if every row is cached
    // Otherwise we know we don't have complete client side data so returns null.
    getAllLocalOptions : function () {
       var valsArray;
        if (this._getOptionsFromDataSource()) {
            // if we're databound, allow keyboard navigation iff we've got a full cache
            if (!this.pickList || this.pickList.destroyed) return;
            var rs = this.pickList.getOriginalData();
            if (!rs || !rs.lengthIsKnown() || !rs.allMatchingRowsCached()) return;

            // We're going to have to refilter (on the client) if the criteria have changed
            var criteria = this.getPickListFilterCriteria();
            if (rs.compareCriteria(criteria, rs.criteria) != 0) {
                if (!rs.allRowsCached() || !rs.useClientFiltering) return;
                this.filterPickList(false, false);
            }
            valsArray = rs.getAllRows();
        } else {
            valsArray = this.getClientPickListData();
        }
        return valsArray;
    },

    // moveToChar() - sets the value of this item to the next valid option that starts with
    // the specified character

    moveToChar : function (charVal) {
        var valsArray = this.getAllLocalOptions();
        if (!valsArray || valsArray.length < 1) return;

        var character = String.fromCharCode(charVal);
        if (character == null) return;
        // Normalize to a lowercase string for comparison.
        character = character.toLowerCase();

        var value = (this.isSelectOther && this._selectOtherValue != null) ? this._selectOtherValue :
                    (this._itemValueIsDirty() ? this._localValue : this.getValue()),
            valueField = this.getValueFieldName(),
            currentIndex = valsArray.findIndex(valueField, value),
            i = (currentIndex == valsArray.length -1 ? 0 : currentIndex + 1);

        while (i != currentIndex) {
            // avoid an infinite loop if the current value is not in the valueMap
            if (currentIndex < 0) currentIndex = 0;
            var testValue = valsArray[i][this.getValueFieldName()],
                displayValue = this.mapValueToDisplay(testValue);

            if (isc.isA.String(displayValue)) {
                var compareChar = displayValue.charAt(0).toLowerCase();
                if (compareChar == character) {
                    var newValue = testValue;

                    // use changeToValue() to update the value and fire the change handler
                    this.changeToValue(
                        newValue,
                        (this.changeOnValueChange && this.changeOnKeyboardNavigation)
                    );
                    return;
                }
            }
            i += 1;
            if (i >= valsArray.length) i = 0;
        }
    },

    // moveToNextValue() - sets the value of this item to the previous / next valid option in
    // the valueMap
    moveToNextValue : function (step) {

        var value;
        if (this.isSelectOther && this._selectOtherValue != null) value = this._selectOtherValue;
        else value = (this._itemValueIsDirty() ? this._localValue : this.getValue());

        var valueField = this.getValueFieldName();

        var currentIndex = -1,
            totalRows;

        var valsArray = this.getAllLocalOptions();
        if (!valsArray || valsArray.length < 1) {
            // getAllLocalOptions returns null if we have a partial-cache'd resultSet.
            // However we should be able to request the range directly from the RS in this case
            if (this._getOptionsFromDataSource() && this.pickList && !this.pickList.destroyed) {
                var rs = this.pickList.getOriginalData();
                var criteria = this.getPickListFilterCriteria();
                if (rs && rs.compareCriteria(criteria, rs.criteria) != 0) {
                    this.filterPickList(false, false);
                }
                valsArray = rs;
            }
        }
        if (valsArray == null || valsArray.length < 1) return;

        var currentIndex = valsArray.findIndex(valueField, value);
            totalRows = valsArray.getLength();

        currentIndex += step;
        // Native selects don't allow wrapping of arrow key navigation (so we won't either)
        if (totalRows == null || currentIndex >= totalRows || currentIndex < 0) return;

        // Note - this will kick off a fetch if the new record is outside our cached
        // data-set.
        var record = valsArray.get(currentIndex);
        if (record != null && !Array.isLoading(record)) {
            // use changeToValue() to update the value and fire the change handler
            var val = record[valueField]
            this.changeToValue(
                val,
                (this.changeOnValueChange && this.changeOnKeyboardNavigation)
            );
        }
    },

    // moveToFirstValue() / moveToLastValue() - sets the value of this item to the first/last
    // values in the valueMap for this item.
    moveToFirstValue : function () {

        if (this.optionDataSource) return;

        var valsArray = this.getClientPickListData(),
            valueField = this.getValueFieldName(),
            val = valsArray[0][valueField];
        this.changeToValue(
            val,
            (this.changeOnValueChange && this.changeOnKeyboardNavigation)
        );
    },

    moveToLastValue : function () {

        if (this.optionDataSource) return;

        var valsArray = this.getClientPickListData(),
            valueField = this.getValueFieldName(),
            val = valsArray[valsArray.length -1][valueField]
        this.changeToValue(
            val,
            (this.changeOnValueChange && this.changeOnKeyboardNavigation)
        );
    },

    // ---------------------------------------------------------------------------------------
    // FOCUS AND STYLING
    // ---------------------------------------------------------------------------------------

    _canFocus : function () {
        return true;
    },


    _getIconMouseDownFunction: function () {
        if (!this._iconMouseDownFunction) {
            this._iconMouseDownFunction =
                new Function ("if(window." + this.getID() + ")window." + this.getID() +
                               "._showingPickList=true;");
        }
        return this._iconMouseDownFunction;
    },

    _applyHandlersToElement : function (a,b,c,d) {
        // Apply the normal handlers
        this.invokeSuper(isc.SelectItem, "_applyHandlersToElement", a,b,c,d);
        if (isc.Browser.isIE) {
            var iconElement = this._getIconImgElement(this.getPickerIcon());
            if (iconElement) {
                iconElement.onmousedown = this._getIconMouseDownFunction();
            }
        }

    },


    // ----------------------------------------------------------------------------------------
    // Values Management
    // ----------------------------------------------------------------------------------------


    makePickList : function (show) {
        if (this.progressiveLoading === true || this.progressiveLoading === false) {
            if (this.pickListProperties == null) this.picklistProperties = {};
            this.picklistProperties.progressiveLoading = this.progressiveLoading;
        }

        if (!this.filterLocally && this.allowEmptyValue && this._getOptionsFromDataSource()) {
            if (this.pickListProperties == null)
                this.pickListProperties = {};
            if (this.pickListProperties.dataProperties == null)
                this.pickListProperties.dataProperties = {};
            // Using basic rather than local means if we do have outstanding filter criteria
            // we'll fetch fewer rows from the server, while still being able to manipulate
            // the cache to insert the empty row at the top.
            this.pickListProperties.dataProperties.fetchMode = "basic";
        }
        var interfaceMakePickList = isc.PickList.getPrototype().makePickList;
        return interfaceMakePickList.apply(this, arguments);
    },

    // changeToValue()
    // Helper method to fire the change handler for this item, then update to the value
    // specified.  Called when the user navigates to a new value.

    changeToValue : function (newValue, saveValue) {

        var value = (this._selectOtherValue || this._localValue || this.getValue());

        if (value == newValue) return;

        if (this.isSelectOther &&
            (newValue == this.separatorValue || newValue == this.otherValue))
        {
            this._setElementValue(this.getDisplayValue(newValue), newValue);
            // keep a pointer around to tell us which value the user selected.

            this._selectOtherValue = newValue;
            return;
        } else {
            delete this._selectOtherValue;
        }
        // Update the displayed HTML and store the new value locally.
        this.setLocalValue(newValue);
        // if we're saving the value out, fire 'updateValue()'
        if (saveValue) this.updateValue();
    },

    // setLocalValue:
    // Update the displayed value without saving the value out / firing the change handler.
    setLocalValue : function (value) {
        this._localValue = value;

        if (this.isVisible() && this.containerWidget.isDrawn()) {

            if (value == null) value = null;
            this._setElementValue(this.getDisplayValue(value), value);
        }
        this._markValueAsDirty();
    },

    // Override setElementValue to hang onto a copy of the current display value

    setElementValue : function (displayValue, dataValue, a,b,c) {
        this._displayValue = displayValue;


        var isEmptyDisplayValue = (displayValue == null ||
                                   isc.is.emptyString(displayValue) || displayValue == this.emptyDisplayValue);
        if (isc.Canvas.ariaEnabled() && this.outerAriaRole === "listbox") {
            this.setAriaRole(isEmptyDisplayValue ? "presentation" : "option");
        }

        // If showing hint within data field, see if it should be shown now.

        if (this.showHintInField && this.getHint()) {
            if (isEmptyDisplayValue) {
                // Set field class to our hint style
                if (this.hasDataElement()) {
                    var element = this.getDataElement();
                    element.className = this._getInFieldHintStyle();
                } else {
                    var textBox = this._getTextBoxElement();
                    if (textBox != null)
                        textBox.className = this._getInFieldHintStyle();
                }

                // Show the hint in the field
                // Note that hint is HTML which may not display correctly within the field.
                // To improve the situation, unescape common HTML sequences first.
                var hint = this.getHint();
                if (hint) hint = hint.unescapeHTML();
                displayValue = hint;
                this._showingInFieldHint = true;
            } else {

                if (this._showingInFieldHint) {
                    var textBox = this._getTextBoxElement();
                    if (textBox != null) textBox.className = this.getTextBoxStyle();
                }
                this._showingInFieldHint = false;
            }
        }
        return this.invokeSuper(isc.SelectItem, "setElementValue", displayValue, dataValue, a,b,c);
    },


    _hideInFieldHint : function (clearStyleOnly) {
        if (this._showingInFieldHint) {

            var textBox = this._getTextBoxElement();
            if (textBox != null) {
                textBox.className = this.getTextBoxStyle();
            }
            // Clear the hint text from the field
            if (!clearStyleOnly) {
                var value = this._localValue;
                if (value == null) value = this.getValue();
                this.setElementValue(this.getDisplayValue(value), value);
            }
            this._showingInFieldHint = false;
        }
    },

    // Override updateValue - this method will save out the value previously applied locally.
    updateValue : function () {
        // Handle the case of a selectOther item where the user has selected the separator
        // or the "Other..." options.
        if (this.isSelectOther && this._selectOtherValue != null) {
            var selectOtherValue = this.getSelectOtherValue(this._selectOtherValue);
            delete this._selectOtherValue;
            this.setLocalValue(selectOtherValue);
        }
        // if we're not dirty we don't need to update at all.
        if (!this._itemValueIsDirty()) return;

        var newValue = this._localValue;
        this._updateValue(newValue);
    },

    // Disable mapDisplayToValue entirely.
    // This method is called from _updateValue which is passed the displayValue in most form items
    // however in SelectItems we always pass the data value directly into this method so it needs
    // no further modification.
    mapDisplayToValue : function (value) {
        return value;
    },


    //>@attr    SelectOtherItem.selectOtherPrompt   (string : "Other value for <br>${item.getTitle()}?" : IR)
    // Title to show in prompt for "other" value.
    // Note this is a dynamic string. JavaScript content is supported within <code>\${...}</code>
    // tags, with local variables for <code>item</code> (a pointer to this item) and
    // <code>value</code> a pointer to the currently selected item value.
    // @group i18nMessages
    // @visibility external
    //<
    selectOtherPrompt:"Other value for <br>${item.getTitle()}?",

    //> @attr SelectOtherItem.dialogWidth (int : 250 : IRW)
    // Width for the "other value" prompt dialog.
    // @visibility external
    //<
    dialogWidth:250,

    // getSelectOtherValue - only used by isSelectOther items -- if the user selected
    // "Other" or the separator, return the desired value for the cell (uses the prompt if
    // necessary)
    getSelectOtherValue : function (value) {
        // separator should never be selected
        if (value == this.separatorValue) return true;
        if (value == this.otherValue) {

            // Note the prompt contents should probably be customizable
            var oldValue = this._localValue || this.getValue(),
                oldTitle = (oldValue == null ? "" : this.mapValueToDisplay(oldValue)),
                promptTitle = this.selectOtherPrompt.evalDynamicString(null,
                                                     {item:this, value:oldValue}),
                config = isc.addProperties({width:this.dialogWidth},
                                            // support dialogDefaults / dialogProperties to
                                            // configure the prompt - this mimics the
                                            // auto-child APIs
                                            this.dialogDefaults, this.dialogProperties);

            isc.askForValue(promptTitle, this.getID() + ".getSelectOtherValueCallback(value)",
                            config);
            return true;
        }
    },

    getSelectOtherValueCallback : function (value) {
        if (value != null) {
            value = this.mapDisplayToValue(value);
            this.changeToValue(value, this.changeOnValueChange);
        }
    },

    // Override setValue
    // - must handle 'addUnknownValues' logic
    // - must redraw the selected value text.
    setValue : function (value,b,c,d) {
        // Make sure this value is a valid option from our valueMap if necessary.

        value = this._getValidValue(value);


        var undef,
            localValue = this._localValue;
        if (localValue === undef) localValue = this._value;

        // Let the superclass implementation save the value out and mark as not dirty.
        // Note: at this point the value passed in may be null - if so we're relying on
        // the superclass implementation to get the defaultValue and set up the 'isDefaultValue'
        // flag on this item.
        // NOTE: we are passing a modified value.
        this.invokeSuper(isc.SelectItem, "setValue", value,b,c,d);
        value = this.getValue();

        // Calling setLocalValue actually sets the displayed element value (may have been
        // changed by the call to Super("setValue"...)
        if (value != localValue) this.setLocalValue(value);

        // if the pickList is showing, ensure it's showing the right set of data and has
        // the correct element selected.
        if (this.pickList && this.pickList.isDrawn() && this.pickListVisible()) {
            this.setUpPickList(true);
        }

        // See if the in-field hint needs to be shown
        if (!this.hasFocus && this.showHint && this.showHintInField && this.getHint()) {
            if (value === undef || value == null || isc.is.emptyString(value)) {
                this._showInFieldHint();
            } else {
                // hide the inFieldHint styling (and clear the flag) if we were
                // showing the in-field-hint.
                // No need to update the element value - we will have already handled
                // this as part of standard setValue() flow.
                this._hideInFieldHint(true);
            }
        }

        return value;
    },



    saveValue : function (value, a,b,c,d) {
        var oldValue = this._value;
        if (this._dropCacheOnValueChange(oldValue, value)) delete this._clientPickListData;
        return this.invokeSuper(isc.SelectItem, "saveValue", value, a,b,c,d);
    },
    _dropCacheOnValueChange : function (oldValue, newValue) {
        return (this.addUnknownValues && this._clientPickListData &&
                ((oldValue != null && !this._valueInValueMap(oldValue)) ||
                 (newValue != null && !this._valueInValueMap(newValue))    ) );
    },

    // Override 'markValueAsNotDirty' to clear out the temp local value.
    // note this means that callers should have already ensured that the displayed value
    // matches the appropriate value. This method is strictly internal, so this is an
    // acceptable assertion to make
    _markValueAsNotDirty : function (a,b,c,d) {
        this.invokeSuper(isc.SelectItem, "_markValueAsNotDirty", a,b,c,d);
        delete this._localValue;
    },

    // override getDefaultValue to pick up the first option if defaultToFirstOption is true
    // getDefaultValue should not be able to return a value that is not included
    // in the valueMap for this select.
    getDefaultValue : function () {

        // If an explicitly specified defaultValue exists, respect it.
        var dV =  this.Super("getDefaultValue", arguments);
        if (dV == null && this.defaultToFirstOption) dV = this.getFirstOptionValue();

        return this._getValidValue(dV);
    },

    // selectItems have a limited set of valid values (represented by the valueMap).
    // this method will take a value, and ensure it is a member of the value map for the item.
    // The value will be returned if valid - otherwise this method returns null.
    _getValidValue : function (value) {

        // Disallow values that aren't in the valueMap.
        // If passed a value not in the valueMap:
        // - if our current (or 'previous') value is valid, return it
        // - if not, clear the value
        if (!this._valueIsValid(value)) {


            var alreadyChecking = this._checkingValidValue;
            this._checkingValidValue = true;
            var validValue;
            if (alreadyChecking) validValue = value;
            else validValue = this._localValue || this.getValue();

            // If the value passed in is the 'current value', or our previous value isn't
            // valid, clear the value.
            // [a null value is always considered valid by this mechanism].
            if (value == validValue || !this._valueIsValid(validValue)) {
                validValue = null;
            }
            value = validValue;
        }
        return value;
    },

    _valueIsValid : function (value) {

        if (this.addUnknownValues || this.optionDataSource) return true;

        // Allow null values regardless of what the valueMap looks like

        if (value == null) return true;

        // Disallow values that aren't in the valueMap.
        // In multi-select mode, the value could be an array of choices.
        if (isc.isAn.Array(value)) {
            for (var i = 0; i < value.length; i++) {
                if (!this._valueInValueMap(value[i])) return false;
            }
            return true;
        } else {
            return this._valueInValueMap(value)
        }
    },

    _valueInValueMap : function (value) {
        var valueMap = this.getValueMap(),
            undef;
        if (isc.isAn.Array(valueMap)) {
            return valueMap.contains(value)
        } else if (isc.isAn.Object(valueMap)) {
            return (valueMap[value] !== undef);
        }
        return false;
    },

    //> @attr selectItem.escapeHTML (Boolean : false : IRW)
    // By default HTML values in a selectItem will be interpreted by the browser.
    // Setting this flag to true will causes HTML characters to be escaped, meaning the
    // raw value of the field (for example <code>"&lt;b&gt;AAA&lt;/b&gt;"</code>) is displayed
    // to the user rather than the interpreted HTML (for example <code>"<b>AAA</b>"</code>)
    // @group appearance
    // @visibility external
    //<
    // Implemented at the form item level - enable via the canEscapeHTML flag.
    canEscapeHTML:true,
    // escapeHTML also requires the pickList field has this property set - handled
    // for the standard 'value' field via code in PickList.js.
    // For custom fields the developer would have to set this flag directly.

    // Display vs internal value mapping
    // mapValueToDisplay() allows us to convert internal value to  display value.

    mapValueToDisplay : function (internalValue, a, b, c) {
        if (this.isSelectOther) {
            if (internalValue == this.otherValue) return this.otherTitle;
            if (internalValue == this.separatorValue) return this.separatorTitle;
        }

        return this.invokeSuper(isc.SelectItem, "mapValueToDisplay", internalValue,a, b, c);
    },


    // getSelectedRecord()
    // For SelectItems there's a _localValue which gets set up before we store our
    // value properly.
    // This is used to allow change to occur on blur rather than just on standard 'change' event.
    //
    // For selectItems use the current _local value rather than the stored item value when
    // picking up the current record from the pickList. Otherwise you get unexpected behaviour if
    // (for example) custom formatters make use of the pick list records -- we'll see the last
    // selected record rather than the newly selected record until the change handler fires and
    // stores the value out.
    getSelectedRecord : function () {

        if (this.pickList == null || this.pickList.destroyed) this.makePickList(false);

        // We can't just say this.pickList.getSelectedRecord(), since the
        // value may not have been picked from a pickListClick -- instead force a selection
        // that matches our item value then retrieve the selected value.
        var undef,
            value = this._localValue;
        if (value === undef) value = this.getValue();

        if (this.selectItemFromValue(value)) {
            return this.pickList.getSelectedRecord();
        }
        // if we didn't find the selectedRecord in the pickList, call 'super'
        // this will pick up this._selectedRecord if present -- that gets set up by
        // the fetchMissingValues code in FormItem.

        return this.Super("getSelectedRecord", arguments);
    },

    //> @method selectItem.getSelectedRecords()
    // For a SelectItem with an +link{optionDataSource} and allowing multiple selection
    // (+link{multiple,via multiple:true}), returns the list of currently selected records, or
    // null if none are selected.
    //
    // @return (Array of Record) the list of selected records, or null if none are selected
    // @visibility external
    //<
    getSelectedRecords : function () {
        var value = this._localValue;
        this.selectItemFromValue(value);
        var sel = this.pickList.getSelection();
        if (sel.length > 0) return sel;
        else return null;
    },

    // Map valueToDisplay needs to pick up
    // the mapping between displayField and valueField, if there is one.
    // We implement this by overriding _mapKey() to check for the value in
    // our pickList's dataSet, in addition to checking against any explicitly specified valueMap


    _mapKey : function (value, dontReturnKey, a,b,c,d) {
        var displayValue = this.invokeSuper(isc.SelectItem, "_mapKey", value, true ,a,b,c,d);
        // _translateFieldValue part of the pickList interface
        if (displayValue == null && this.getDisplayFieldName() != null) {
            var translatedValue = this._translateValueFieldValue(value, false);
            if (translatedValue !== undefined) displayValue = translatedValue;
        }
        if (displayValue == null && !dontReturnKey) displayValue = value;
        return displayValue;
    },


    // Override checkForDisplayFieldValue()
    // This is the method that, if we have a displayField specified, kicks off a fetch against
    // our optionDataSource to load the appropriate display value from the server.
    // In PickList based items we use the pickList data (if present) to map data to display
    // values.
    // Catch the case where checkForDisplayFieldValue is called when we are in the process of
    // fetching our pickList data from the server.
    // In this case we want to wait for the pickList data to be returned rather than kicking off
    // an additional fetch as our data value will usually be present in that pickList data.
    // When the pickList data returns we re-check this method. If the data is present, we're
    // done, otherwise we need to kick off another fetch as we didn't find our data value in
    // the pickList data array. This can happen if the pickList data is paged, for instance.
    _checkForDisplayFieldValue : function (newValue, delayed) {
        var inValueMap = false;
        if (this.multiple) {
            // assert isc.isAn.Array(newValue)
            for (var i = 0, len = newValue.getLength(); !inValueMap && i < len; ++i) {
                inValueMap = (this._mapKey(newValue[i], true) != null);
            }
        } else {
            inValueMap = (this._mapKey(newValue, true) != null);
        }
        if (inValueMap) return;

        if (this._fetchingPickListData) {
            this._checkDisplayFieldValueOnFilterComplete = true;
            return;
        }
        this.invokeSuper(isc.SelectItem, "_checkForDisplayFieldValue", newValue);
    },



    // INTERACTIONS WITH PICK-LIST ------------------------------------------------------------
    // comes from the interface

    // Include useful JSDoc from pickList

    //> @attr SelectItem.optionDataSource (DataSource | String : null : IRA)
    // @include PickList.optionDataSource
    //<

     //> @attr selectItem.textMatchStyle (TextMatchStyle : "startsWith" : IR)
    // @include PickList.textMatchStyle
    //<

    //> @attr SelectItem.pickListFields (Array of ListGridField : null : IRA)
    // @include PickList.pickListFields
    // @example relatedRecords
    //<

    //> @method SelectItem.fetchData()
    // @include PickList.fetchData()
    //<

    //>@attr selectItem.cachePickListResults (Boolean : true : IR)
    // @include pickList.cachePickListResults
    // @visibility external
    //<

    //> @attr SelectItem.fetchDisplayedFieldsOnly (boolean : null : IRA)
    // @include PickList.fetchDisplayedFieldsOnly
    //<

    //> @attr SelectItem.optionFilterContext (DSRequest Properties : null : IRA)
    // @include PickList.optionFilterContext
    //<

    //> @attr SelectItem.optionOperationId (string : null : [IR])
    // @include FormItem.optionOperationId
    // @visibility external
    //<

    //> @attr SelectItem.displayField (string : null : IRW)
    // @include PickList.displayField
    // @visibility external
    // @example relatedRecords
    //<

    //> @attr SelectItem.valueField   (string : null : IRW)
    // @include PickList.valueField
    // @visibility external
    // @example relatedRecords
    //<

    //> @method SelectItem.getDisplayFieldName() ([A])
    // @include PickList.getDisplayFieldName
    // @visibility external
    //<

    //> @method SelectItem.getValueFieldName() ([A])
    // @include PickList.getValueFieldName
    // @visibility external
    //<

    //> @attr SelectItem.filterLocally
    // @include PickList.filterLocally
    // @visibility external
    //<

    //> @method SelectItem.dataArrived()
    // @include PickList.dataArrived()
    // @visibility external
    //<

    //> @method SelectItem.getSelectedRecord()
    // @include FormItem.getSelectedRecord()
    // @visibility external
    //<

    //> @attr SelectItem.pickListCriteria (Criteria : null : IRWA)
    // @include PickList.pickListCriteria
    // @visibility external
    //<

    //> @attr SelectItem.showOptionsFromDataSource    (boolean : null : IRWA)
    // @include PickList.showOptionsFromDataSource
    // @visibility external
    //<

    //> @attr SelectItem.pickListProperties (ListGrid Properties : null : IRA)
    // @include PickList.pickListProperties
    // @visibility external
    //<

    //> @attr   SelectItem.sortField   (String or integer : null : IR)
    // @include PickList.sortField
    // @visibility external
    //<

    // Override the method to get pickList data to add unknown values and selectOther
    // properties

    getClientPickListData : function () {
        if (this._clientPickListData) return this._clientPickListData;

        var records = isc.PickList.optionsFromValueMap(this),
            valueField = this.getValueFieldName();

        // If allowEmptyValue is true, add an empty entry to the top of the list
        // Note that this is independant of addUnknownValues - we don't treat the null value
        // as an 'unknown' values - it's more like lack of a value.


        if (this.allowEmptyValue && !this.multiple && (records.find(valueField, null) == null)) {
            var emptyRecord = {};
            // Apply explicit null for the valueField. Avoids confusion about whether the
            // value is actually set at the form level.

            emptyRecord[valueField] = null;

            records.addAt(emptyRecord, 0);
        }
        // The current selected value must always be present in the records for the VM.

        var value = this.getValue();
        // don't add the value if value is an array;  this means the item is in
        // multiselect mode

        if (value != null && !isc.isAn.Array(value)) {
            if (records.find(valueField, value) == null) {
                var newRecord = {};
                newRecord[valueField] = value;
                records.addAt(newRecord, 0);
            }
        }

        if (this.isSelectOther) {
            var separator = {},
                other = {};

            separator[valueField] = this.separatorValue;
            other[valueField] = this.otherValue;

            records.addListAt([separator, other], records.length);
        }

        this._clientPickListData = records;


        return records;
    },

    // Use the 'formatPickListValue()' method to display the appropriate title for the
    // special selectOther options.
    formatPickListValue : function (value, fieldName, record) {
        if (this.isSelectOther && (fieldName == this.getValueFieldName())) {
            if (value == this.otherValue) return this.otherTitle;
            if (value == this.separatorValue) return this.separatorTitle;
        }

        // apply standard formatter to the value in the single generated field for
        // standard pick lists.
        // This handles formatters applied via simpleType as well as any
        // 'formatValue()' method applied to this item
        if (this.pickList.getField(fieldName)._isGeneratedField) {
            return this._formatDataType(value);
        }

        return value;

    },

    // pickValue : select the picked value.

    pickValue : function (value) {
        // selectOther behavior:
        // If the user selects the separator, treat this as a null selection.
        // If they select the "other" value, pop a prompt to get the value to use.

        if (this.isSelectOther) {
            if(this.getSelectOtherValue(value)) return;
        }

        // use changeToValue() to update the value and fire the change handler
        this.changeToValue(value, (this.changeOnValueChange && this.fireChangeOnSelect != false));
    },

    //>@method SelectItem.getPickListPosition() (A)
    // Returns the global left and top coordinates for the pickList.
    // Default implementation always draws the pick-list below the Select Item - override for
    // any special implementation.
    // @return (array)  2 element array indicating left and top coordinate for the pick list.
    //<
    getPickListPosition : function () {
        var itemTop = this.getPageTop(),
            top =  itemTop + this.getHeight(),
            left = this.getPageLeft(),
            pageTop = isc.Page.getScrollTop(),
            pageBottom = isc.Page.getHeight() + pageTop;

        // we auto-fit the pickList to the data.
        // If it's not yet drawn it may not have autofitted yet - so determine the expected height
        var height;
        if (!this.pickList.isDrawn()) {
            var data = this.pickList.data;
            if (data == null || data.getLength() == 0) {
                height = this.emptyPickListHeight;
            } else {
                height = Math.min(
                            (data.getLength() * this.pickList.cellHeight) +
                            (this.pickList.showHeader ? this.pickList.headerHeight : 0) +
                            (this.pickList.showFilterEditor ? this.pickList.filterEditorHeight : 0),
                            this.pickListHeight);
            }
        } else {
            height = this.pickList.getVisibleHeight();
        }

        // If the pickList would extend beyond the end of the page, show it above the select
        // item
        if (top + height > pageBottom) {
            top = Math.max(pageTop, (itemTop - height));
        }

        // In RTL mode we want to shift so the right edge rather than the left edge is aligned
        // with us
        if (this.containerWidget.isRTL()) {
            left += this.getVisibleWidth();
            left -= this.pickList.getVisibleWidth();
        }


        return [left,top];
    },

    // override updateValueMap to update the list and update the displayed value if necessary.
    updateValueMap : function (refreshDisplay) {
        this.Super("updateValueMap", arguments);
        // This method is called from filter-complete with a flag set to suppress display
        // of the new valueMap. In that case nothing to do here
        if (this._processingFilterComplete) return;

        // Otherwise we've got a totally new set of values or new optionDataSource.
        // Hide and detatch from the the pickList so it gets refreshed when re-shown.
        if (this._clientPickListData) delete this._clientPickListData;
        if (this.hasPickList()) {
            if (this.pickList.isVisible() && this.pickList.isDrawn()) {
                this.pickList.hide();
            }

            // clear the 'formItem' property on the pickList - ensures it will have its data
            // reset when it is next shown.
            delete this.pickList.formItem;

        }
        // if the current value is no longer valid, update it.
        var currentValue = this.getValue(),
            value = this._getValidValue(currentValue);
        if (currentValue != value) {
            this.setValue(value);
        } else {
            // Even if the value is unchanged, its display value is likely to be different, so
            // do a setElementValue.

            this._setElementValue(this.getDisplayValue());
        }
    },

    // Returns true if this item has its picklist already set up
    hasPickList : function () {
        // note the check for this.pickList.formItem pointing back to this select is required
        // for the case where the pickList is reused.
        return (this.pickList && !this.pickList.destroyed && this.pickList.formItem == this);
    },

    // Returns true if this item's pickList is showing.
    pickListVisible : function () {
        return (this.hasPickList() && this.pickList.isDrawn() && this.pickList.isVisible());
    },

    // If we get yanked out of the DOM, ensure our pickList gets hidden
    cleared : function () {
        var rv = this.Super("cleared", arguments);
        // hide the pickList *after* calling Super. This ensures that this.isDrawn() returns false
        // so we don't try to focus in a stale focus element thats been removed from the DOM, etc.

        if (this.pickListVisible()) this.pickList.hide();
        return rv;
    },

    // Override filterComplete to
    // - Add an empty entry to the top of the list if this.allowEmptyValue is true
    filterComplete : function () {
        // If we are allowing empty values we always to 'basic' filtering, which means
        // we can directly add the empty entry to the ResultSet's cache
        if (this.allowEmptyValue && !this.multiple && this._getOptionsFromDataSource()) {
            var data = this.pickList.getOriginalData(); // handle the pl being grouped
            if (isc.isA.ResultSet(data)) {
                var recordAdded = data.addEmptyValueRecord(this.getValueFieldName());
                if (recordAdded && this.pickListVisible()) this.pickList.markForRedraw();
            }
        }
        var interfaceFilterComplete = isc.PickList.getPrototype().filterComplete;
        interfaceFilterComplete.apply(this, arguments);
    }
});

// Mix in the PickList interface - handles creating and showing the PickList itself.
isc.ClassFactory.mixInInterface("SelectItem", "PickList");


isc.SelectItem.registerStringMethods({
    dataArrived:"startRow,endRow,data",
    getPickListFilterCriteria:""
});

} else {



    isc.Log.logInfo("Source for standard SelectItem class included in this module, but required " +
        "related class (ListGrid) is not loaded. This can occur if the Forms module is " +
        "loaded without the Grids module. SelectItem class will be defined as " +
        "a subclass of NativeSelectItem.", "moduleDependencies");

    isc.ClassFactory.defineClass("SelectItem", "NativeSelectItem");
}





//>    @class    CycleItem

//
// Form item that iterates through a set of options in response to the user clicking.
//<
// Leave internal for now - we will expose synthetic checkbox and radioItem subclasses instead
isc.defineClass("CycleItem", "FormItem");

isc.CycleItem.addProperties({

    canSelectText:false,

    canFocus:true,

    // V-Align the cycleItem (checkboxes, etc) in the center - this looks best for the (default)
    // case where the height of the checkboxItem exceeds the height of the media
    iconVAlign:"middle"
});

isc.CycleItem.addMethods({
    // On Space or Enter keypress, advance the value
    handleKeyPress : function () {
        var key = isc.EH.getKey();
        var readOnly = this.isReadOnly();
        if (!readOnly && key == "Space") {
            this.advanceValue();
            // returning false kills native behavior (such as scrolling on space)
            return false;
        }
        return this.Super("handleKeyPress", arguments);
    },

    // on click, advance the value
    handleClick : function () {
        if (this.isDisabled() || this.isReadOnly()) return;

        if (!this.hasFocus) this.focusInItem();
        this.advanceValue();
    },

    // treat doubleClick like click so 2 close together clicks will flip the value twice
    handleDoubleClick : function () {
        if (this.isDisabled() || this.isReadOnly()) return;
        this.advanceValue();
    },

    // This method will advance the value - selects the next option from this item's valueMap
    advanceValue : function () {

        var valueMap = this.getValueMap();
        if (isc.isA.Object(valueMap) && !isc.isA.Array(valueMap)) {
            valueMap = isc.getKeys(valueMap);
        }
        if (valueMap == null || valueMap.length < 2) {
            this.logInfo("CycleItem is non interactive as there are no options for this item.");
            return;
        }

        var value = this.getValue(),
            index = valueMap.indexOf(value);
        if (index == valueMap.length -1) index = -1;

        // Call saveValue rather than setValue() so our change etc handler fires
        var newValue = valueMap[index+1];

        if (!this.compareValues(newValue, this._value)) {
            var displayValue = this.mapValueToDisplay(newValue);
            this.setElementValue(displayValue, newValue);
            if (isc.Canvas.ariaEnabled()) this.setAriaState("checked", !!newValue);
            // use "updateValue" to actually save the value out. This should handle
            // firing change() / changed() handlers, validation, etc.
            this._updateValue(newValue);
        }
    },

    //> @method cycleItem.setElementReadOnly()
    // Change the read-only state of the form element immediately.
    //<
    setElementReadOnly : function (readOnly) {
        // A cycle button should be rendered as disabled to simulate read-only.
        this._setElementEnabled(!readOnly && !this.isDisabled());
    }
});





//>    @class    CheckboxItem
//
// Checkbox form item, implemented with customizable checkbox images
// @visibility external
// @example checkboxItem
//<
isc.defineClass("CheckboxItem", "CycleItem");

//> @class  BooleanItem
//
// Boolean form item, implemented with customizable checkbox images
// @visibility external
//<
// Alias for smartgwt
isc.addGlobal("BooleanItem", isc.CheckboxItem);

isc.CheckboxItem.addClassProperties({
    // Checkboxes require a 2 option valueMap - containing a true and false value.
    // This array is applied to checkboxItems as a defaultValueMap.
    // See comments in getValueMap() for how we deal with other specified valueMaps
    trueFalseValueMap:[true,false],

    trueFalseNullValueMap:[true, false, null]

});

isc.CheckboxItem.addProperties({

    type: "boolean",

    //> @attr CheckboxItem.titleStyle (FormItemBaseStyle : null : IRW)
    //  Base CSS class for this item's title cell.
    // <P>
    // <B>Note:</B> This styling applies to the standard form item title cell for this item - it
    // does not apply to item's +link{CheckboxItem.showLabel,label}.
    // To modify the styling for that text, use +link{CheckboxItem.textBoxStyle} instead.
    // @group   appearance
    // @visibility external
    //<

    //>    @attr    CheckboxItem.textBoxStyle    (FormItemBaseStyle : "labelAnchor" : IRW)
    //  Base CSS class for this item's title text
    // @group   appearance
    // @visibility external
    //<
    textBoxStyle:"labelAnchor",

    //> @attr checkboxItem.showTitle (Boolean : true : IR)
    // CheckboxItem has special behavior for titles, see +link{labelAsTitle}.
    //
    // @visibility external
    //<

    //> @attr   checkboxItem.labelAsTitle   (boolean : null : IRW)
    // By default a checkboxItem sets +link{showTitle}:true, and so takes up two cells with the
    // default +link{titleOrientation} of "left" (see +link{group:formLayout,form layout
    // overview}).  However, the title cell is left blank by default, and the title specified
    // by +link{formItem.title} is shown inside the formItem's cell instead, in an element
    // called the "label".
    // <P>
    // To instead show the title in it's original location, set <code>labelAsTitle:true</code>.
    // You can also set +link{showLabel}:false to suppress the label and/or title altogether.
    //
    // @visibility external
    //<
    //labelAsTitle:null,

    //> @attr checkboxItem.showLabel    (Boolean : true : IRW)
    // Should we show the label text next to the checkbox item.
    // @visibility external
    //<
    showLabel:true,

    // Default the height to 20px so it aligns with other form items on the same row
    height:20,

    // Required title:
    // We want to render required checkboxes with their titles in bold.
    // Normally this is done by the form, which writes out the item title, but since the title
    // is written directly into the element, we handle it in this caes.
    // Do this by applying "<b>" / "</b>" prefix and suffix to the title if we're required.
    // Note: we can't use the form requiredTitlePrefix and Suffix as these include a ":"
    // character which we don't want to render out.

    requiredTitlePrefix:"<b>",
    requiredTitleSuffix:"</b>",


    valueIconLeftPadding:4,
    valueIconRightPadding:3,



    //> @attr    CheckboxItem.showValueIconOver (Boolean : true : IRWA)
    // Should an "Over" state icon be shown when the user rolls over this checkbox
    // @group valueIcons
    // @visibility external
    //<
    showValueIconOver:true,

    //>@attr    CheckboxItem.showValueIconFocused (boolean : true : IRWA)
    // Should the icon be modified to reflect 'focused' state.
    // We use the "Over" state image to indicate focus as well as rollOver.
    // @group valueIcons
    // @visibility internal
    //<

    showValueIconFocused:true,

    //>@attr    CheckboxItem.showValueIconDown (Boolean : true : IRWA)
    // Should a "Down" state icon be shown when the mouse goes down over this checkbox
    // @group valueIcons
    // @visibility external
    //<
    showValueIconDown:true,

    //>@attr    CheckboxItem.showValueIconDisabled (Boolean : true : IRWA)
    // Should a "Disabled" state icon be shown when the item is disabled
    // @group valueIcons
    // @visibility external
    //<
    showValueIconDisabled:true,



    //> @attr   CheckboxItem.checkedImage   (SCImgURL : "[SKIN]/DynamicForm/checked.gif" : IRW)
    // URL for the image to display when this checkbox is selected, or checked.
    // <P>
    // This image is implemented using the +link{formItem.valueIcons,valueIcons subsystem},
    // and may be modified via the standard valueIcons properties such as
    // +link{checkboxItem.valueIconWidth}
    // <P>
    // Note that this is the base image name - if +link{showValueIconOver} et al are set, the
    // state (<code>"Over"</code>, <code>"Down"</code> and <code>"Disabled"</code>) will be
    // added to this name as the user interacts with the checkbox, as well as the
    // <code>".gif"</code> extension
    // @group appearance
    // @visibility external
    //<
    checkedImage:"[SKINIMG]/DynamicForm/checked.gif",

    //> @attr   CheckboxItem.uncheckedImage   (SCImgURL : "[SKIN]/DynamicForm/unchecked.gif" : IRW)
    // URL for the image to display when this checkbox is not selected, or unchecked
    // @group appearance
    // @visibility external
    //<
    uncheckedImage:"[SKINIMG]/DynamicForm/unchecked.gif",

    //> @attr   CheckboxItem.partialSelectedImage   (SCImgURL : "[SKIN]/DynamicForm/partialcheck.gif" : IRW)
    // URL for the image to display when this checkbox is partially selected
    // @group appearance
    // @visibility external
    //<
    partialSelectedImage:"[SKINIMG]/DynamicForm/partialcheck.gif",

    //>@attr checkboxItem.showUnsetImage (boolean : null : IRA)
    // Determines what image to display when the value for this checkbox is unset.
    // Set to true to display the +link{checkboxItem.unsetImage} for null values, or false to
    // use the +link{checkboxItem.uncheckedImage} for both null and explicitly unchecked values.
    // <P>
    // If this attribute is not set, the +link{checkboxItem.unsetImage} for null values if
    // +link{checkboxItem.allowEmptyValue} is true for this item, otherwise the unchecked
    // image will be used.
    // @visibility external
    //<

    //> @attr   CheckboxItem.unsetImage   (SCImgURL : "[SKIN]/DynamicForm/unsetcheck.gif" : IRW)
    // URL for the image to display when this checkbox is unset. Note that if
    // +link{checkboxItem.showUnsetImage} is false or +link{checkboxItem.allowEmptyValue} is false
    // the +link{checkboxItem.uncheckedImage} will be used for null values rather than this
    // image.
    // @group appearance
    // @visibility external
    //<
    unsetImage:"[SKINIMG]/DynamicForm/unsetcheck.gif",

    //> @attr CheckboxItem.valueIconWidth (number : 13 : IRW)
    // Width of the checkbox image.
    // @group valueIcons
    // @visibility external
    //<
    valueIconWidth:13,

    //> @attr CheckboxItem.valueIconHeight (number : 13 : IRW)
    // Height of the checkbox image.
    // @group valueIcons
    // @visibility external
    //<
    valueIconHeight:13

    //> @attr CheckboxItem.valueMap (object | Array : null : IRW)
    // Object defining how the checkbox "checked" state will be mapped to the field value.<br>
    // Checkboxes only support 2 states. By default a checked checkbox will have
    // value <code>true</code>, and an unchecked one will have value <code>false</code>
    // (or <code>null</code> if there is no default value and the value has never been set).
    // <p>
    // A valueMap can modify this in 2 ways:<br>
    // - If the desired checked/unchecked values can be resolved to <code>true</code>
    //   and <code>false</code> directly in JavaScript, the valueMap may be specified as
    //   a 2-element array containing these values. Examples of this would include<br>
    //   <code>&nbsp;&nbsp;[0,1]</code>:
    //   In this case an unchecked checkbox would have value <code>0</code> and a checked box
    //   would have value <code>1</code><br>
    //   <code>&nbsp;&nbsp;[null,"Some String"]</code>:
    //   In this case an unchecked checkbox would have value <code>null</code> and a checked
    //    box would have value <code>"Some String"</code><br>
    // - More arbitrary data values can be resolved to checked / unchecked values via an
    //   object mapping the arbitrary data values to display values of <code>true</code> and
    //   <code>false</code>. For example:<br>
    //   <code>&nbsp;&nbsp;{"A":false, "B":true}</code><br>
    //    In this case an unchecked checkbox would have value "A", and a checked box
    //    would have value "B"
    // <p>
    // Note: ValueMaps in other formats will be ignored by the CheckboxItem class. To update
    // the valueMap at runtime, always use +link{CheckboxItem.setValueMap()}
    // @group valueMap
    // @visibility external
    //<

});

isc.CheckboxItem.addMethods({
    //> @attr checkboxItem.allowEmptyValue  (Boolean : false : IRW)
    // By default checkboxes allow the user to toggle between true and false values only.
    // Setting this property to true will allow the user to toggle between three values -
    // <code>true</code>, <code>false</code> and unset.
    //
    // @visibility external
    //<
    allowEmptyValue : false,

    // getValueMap()
    // By default checkboxes wont be passed an explicit valueMap - instead they always have a
    // static map of true, false, or true,false,null if allowEmptyValue is true.
    //
    // However we also support an explicit valueMap to allow the user to set up a checkbox
    // showing more complex state (EG "on", "off").
    // We explicitly support
    // - a 2 element array where one element evaluates to true and one doesn't - such as
    //   [true,false], [0,1], [null,"foo"], etc
    // - an object where the left hand value will be stored on the server (can be anything)
    //   and the right hand value is true or false (or evaluates to true/false) - for example
    //   {"accepted":true, "denied":false}
    // It is likely that we'll also see valueMaps on boolean fields of the form
    // {"true":"Accepted", "false":"Denied"}. In this case the developer is mapping the stored
    // boolean value to a string to display - useful for other form item types such as selects
    // but we will just ignore the display value for checkboxItems.'
    //

    _$true:"true", _$false:"false",
    getValueMap : function () {
        // For efficiency cache the 'validated' valueMap - this is the valueMap converted to
        // a 2 element array that we treat as data values.
        if (this._validatedValueMap) return this._validatedValueMap;
        var valueMap = this.Super("getValueMap", arguments);
        if (valueMap != null) {
            var array = false,
                object = isc.isAn.Object(valueMap);
            if (isc.isAn.Array(valueMap)) {
                object = false;
                if (valueMap.length != 2) {
                    valueMap = null;
                } else {
                    // special case - remap "true" / "false" to boolean values
                    var falseStringIndex = valueMap.indexOf(this._$false);
                    if (falseStringIndex != -1) valueMap[falseStringIndex] = false;
                    var trueStringIndex = valueMap.indexOf(this._$true);
                    if (trueStringIndex != -1) valueMap[trueStringIndex] = true;

                    // If we're given a 2 element array as a valueMap and it doesn't consist of
                    // a true and false value, map the first element to true and the second to false

                    if (!((valueMap[0] && !valueMap[1]) || (!valueMap[0] && valueMap[1])) ) {
                        this.logInfo("Checkbox item created with valueMap:"+ valueMap +
                                        "which has no explicit true/false display values. Mapping the " + "first value to true and the second to false.");


                        var mapObject = {};
                        mapObject[valueMap[0]] = true;
                        mapObject[valueMap[1]] = false;
                        this.valueMap = valueMap = mapObject;
                        object = true;
                    } else {
                        array = true;
                    }
                }
            }

            if (object) {
                var vals = [],
                    keys = [],
                    notValid;
                for (var key in valueMap) {
                    if (vals.length == 2) {
                        notValid = true;
                        break;
                    }

                    var val = valueMap[key];
                    // Special case - "true" and "false" as a key or value should be treated as
                    // a boolean
                    if (key == this._$false) key = false;
                    else if (key == this._$true) key = true;
                    if (val == this._$false) {
                        val = valueMap[key] = false;
                    } else if (val == this._$true) {
                        val = valueMap[key] = true;
                    }

                    keys[keys.length] = key;
                    vals[vals.length] = val;
                }

                if (vals.length != 2) notValid = true;
                if (!notValid) {


                    // If the 'display' values are true / false, we can just use the valueMap
                    // as specified - we'll display the checkbox correctly and a call to set
                    // or getValue will return the non-boolean data value
                    if ((vals[0] && !vals[1]) || (!vals[0] && vals[1])) {
                        // We don't need to store out the valueMap in this case - it's already
                        // usable
                    } else if ((keys[0] && !keys[1]) || (!keys[0] && keys[1])) {
                        valueMap = keys;
                    } else notValid = true;
                }
                if (notValid) valueMap = null;

            // getValueMap should always return an array or object
            } else if (!array) {
                valueMap = null;
            }
        }
        // If we were passed an explicit map (2 elements), slot an explicit null value in as well
        if (valueMap && this.allowEmptyValue) {
            if (array) valueMap.add(null);
            else valueMap[this.emptyValueKey] = null;
        }
        return (this._validatedValueMap = valueMap ||
                (this.allowEmptyValue ? isc.CheckboxItem.trueFalseNullValueMap
                                      : isc.CheckboxItem.trueFalseValueMap));
    },
    emptyValueKey:"**NULL**",

    _unmapKey : function () {
        var val = this.Super("_unmapKey", arguments);
        if (val == this.emptyValueKey) val = null;
        return val;
    },

    //> @method CheckboxItem.setValueMap
    // Setter method to apply a valueMap to a checkbox item.<br>
    // Note that if this method is overridden, the override must call
    // <code>this.Super("setValueMap", arguments);</code> to maintain functionality in this
    // class.
    // @see CheckboxItem.valueMap
    // @group valueMap
    // @visibility external
    //<
    // Override setValueMap to clear the cached validated version of the valueMap passed in
    setValueMap : function () {
        this._validatedValueMap = null;
        return this.Super("setValueMap", arguments);
    },

    init : function (a,b,c,d) {

        this.invokeSuper(isc.CheckboxItem, "init", a,b,c,d);
        // for checkboxes we use 'showLabel' to determine whether we show text next to the
        // checkbox image. Implemented by converting to 'showValueIconOnly'.
        if (this.showValueIconOnly == null) this.showValueIconOnly = !this.showLabel;

        // Default textAlign to match the specified "align"
        // It doesn't make sense to have a separate textAlign for checkboxItems
        if (this.textAlign == null && this.align != null) {
            this.textAlign = this.align;
        }
    },

    // We need a setter for showLabel as we basically translate this into the showValueIconOnly
    // property.
    //> @method setShowLabel()
    // Sets +link{checkboxItem.showLabel}
    //<
    setShowLabel : function (showLabel) {
        this.showLabel = showLabel;
        this.showValueIconOnly = !showLabel;
        if (this.isDrawn()) this.redraw();
    },

    // Checkboxes display the title next to the checkbox rather than the value of the
    // item
    mapValueToDisplay : function (value, a,b,c) {
        if (this.labelAsTitle) return isc.emptyString;
        var title = this.invokeSuper(isc.CheckboxItem, "getTitleHTML", value, a,b,c);
        // If this item is displayed in a form that hilights required fields,
        // hilight our title directly.

        var form = this.form;
        if ((this.required || this._required) && form && form.hiliteRequiredFields) {
            title = this.requiredTitlePrefix + title + this.requiredTitleSuffix;
        }
        return title;
    },

    // Use explicit checked and unchecked images as valueIcons
     _$Over:"Over", _$Down:"Down", _$Disabled:"Disabled",
    getValueIcon : function (value) {



        var map = this.getValueMap();
        if (!isc.isAn.Array(map) && isc.isAn.Object(map)) value = map[value];

        // Default behavior uses this.checkedImage and this.uncheckedImage
        if (value) return this.checkedImage;
        else if (value === false) return this.uncheckedImage;
        // If unset return the 'unset' image
        else {
            if (this.showUnsetImage != null) {
                return this.showUnsetImage ? this.unsetImage : this.uncheckedImage;
            }
            return this.allowEmptyValue ? this.unsetImage : this.uncheckedImage;
        }
    },

    // Override getTitleHTML() - we want to show an empty string in the title cell unless
    // labelAsTitle is true
    getTitleHTML : function (a,b,c) {
        if (this.labelAsTitle) return this.invokeSuper(isc.CheckboxItem, "getTitleHTML", a,b,c);
        return isc.emptyString;
    },


    // Some HTML overrides:
    // always '_writeOuterTable' - we are typically just showing an image which has height 13
    // -- however we want to be centered regardless of our specified height, which means we'll
    // have to leave the text-box div unsized and instead size the outer table, and ensure the
    // text box cell is v-align:center'd
    _writeOuterTable : function () {
        return true;
    },

    getTextBoxHeight : function () {
        return null;
    },

    _$heightSemi:"height:", _$px:"px",
    getTextBoxCellCSS : function () {
        var txt = isc.Canvas._$noStyleDoublingCSS;
        var height = this.invokeSuper(isc.CheckboxItem, "getTextBoxHeight");
        if (height && isc.isA.Number(height))
            txt += this._$heightSemi + height + this._$px;

        return txt;
    },

    _shouldAllowExpressions : function () {
        return false;
    }

});









//>    @class    NativeCheckboxItem
// A checkbox for manipulating 2-valued fields based on the native checkbox element.
// @visibility external
//<
isc.ClassFactory.defineClass("NativeCheckboxItem", "FormItem");
isc.NativeCheckboxItem.addProperties({

    //>    @attr    nativeCheckboxItem.textBoxStyle (FormItemBaseStyle : "labelAnchor" : IRW)
    // Base CSS class applied to this item's title text (rendered next to the checkbox element).
    // @group appearance
    // @visibility external
    //<

    textBoxStyle:"labelAnchor",

    // If we're in screenReader mode, this form item will use the native 'title' attribute
    // to show hover prompts.
    implementsPromptNatively:isc.screenReader,

    //>    @attr    nativeCheckboxItem._elementType                (string : "CHECKBOX" : IRW)
    //            type of item ("CHECKBOX" or "RADIO")
    //        @group    appearance
    //<
    _elementType:"CHECKBOX",

    //> @attr   nativeCheckboxItem._hasDataElement    (boolean : true : IRW)
    //      Checkboxes have a data element.
    // @group formValues
    // @visibility   internal
    // @see     method:FormItem.hasDataElement
    // @see     method:FormItem.getDataElement
    //<
    _hasDataElement:true,

    //> @attr   nativeCheckboxItem.showLabel    (Boolean : true : IRW)
    //  @include CheckboxItem.showLabel
    // @visibility external
    //<
    showLabel:true,

    // _nativeEventHandlers is a place to specify native event handlers to be applied to the
    // form item element once it has been written into the DOM (without having to override
    // '_applyHandlersToElement()'
    _nativeEventHandlers:{
        // Fire change handlers in response to native clicks.

        onclick:isc.FormItem._nativeChangeHandler
    }


});

isc.NativeCheckboxItem.addMethods({

    _$HTMLTemplate:[
        "<TABLE role='presentation' CELLSPACING=0 CELLPADDING=0 BORDER=0 ID='", // 0
        ,                                                       // 1
        "' class='",                                            // 2
        ,                                                       // 3
        "'><TR>",                                               // 4

        // Actual element (checkbox)
        "<TD WIDTH=20><INPUT TYPE=",                            // 5
        ,                                                       // 6: this._elementType
        " NAME=",                                               // 7
        ,                                                       // 8: this.getElementName()
        " ID=",                                                 // 9
        ,                                                       // 10: this.getDataElementId()

        // hang a flag on the element marking it as the data element for the appropriate form item.
        ,                                                       // 11: this._getItemElementAttributeHTML()

        ,,,                                                     // 12,13,14: value=',vaue,', or null
        ,                                                       // 15: disabled or null
        " handleNativeEvents=false ",                           // 16
        ,,,                                                     // 17,18,19: title=',prompt,' or null
        " TABINDEX=",                                           // 20
        ,                                                       // 21: this._getElementTabIndex()
        (isc.Browser.isMoz ? " STYLE='-moz-user-focus:" : null),// 22
        ,                                                       // 23: normal;', ignore;', or null
        // No need to write out the accessKey, since we'll always write that out
        // in the label for the checkbox
        "></TD>",                                               // 24
        ,                                                       // 25: title text cell, or null
        "</TR></TABLE>"
    ],

    _$labelCellTemplate:[
        "<TD CLASS='",   // 0
        ,               // 1:
        // Note on alignment: It makes sense to align the title as close as
        // possible to the checkbox. For RTL pages we may want to write the
        // title out before the checkbox, and align right instead.
        "' ALIGN=LEFT",  // 2
        ,,              // 3,4: width= and width, or null
        ,,              // 5,6: height= and height, or null
        // we reapply the text-box class to the cell, to pick up fonts, etc.
        " style='" + isc.Canvas._$noStyleDoublingCSS,     // 7
        ,               // 8 additional cssText for the text box, if any
        "'>",            // 9
        ,               // 10: title
        "</TD>"
    ],

    _$safariTitleTemplate:[
        "<A HREF='javascript:void ",    // 0
        ,                               // 1: itemIDStr
        ".boxTitleClick()' ONMOUSEOVER='window.status = \"", // 2
        ,                               // 3: this.prompt
        "\"; return true' ONMOUSEOUT='window.status = \"\"; return true' CLASS='",    // 4
        ,                               // 5: this.getTextBoxStyle()
        "' title=\"",                     // 6
        ,                               // 7: this.prompt
        // Note - safari doesn't allow us to tab to links, so no need to exclude from page's tab order
        "\">",                          // 8
        ,                               // 9: title
        "</A>"
    ],

    // Override getInnerWidth to always return 20 if we're not showing the label - this is
    // enough space for the checkbox itself

    getInnerWidth : function (a,b,c,d) {

        if (!this.showLabel || this.showValueIconOnly) {
            return 20;
        }
        return this.invokeSuper(isc.NativeCheckboxItem, "getInnerWidth", a,b,c,d);
    },

    // Write out a table containing the checkbox followed by its label
    getElementHTML : function (value) {
        var formID = this.form.getID(),
            itemIDStr = this.getItemID(),
            template = this._$HTMLTemplate,
            title = this.getAnchorTitle()
        ;
        template[1] = this._getDOMID("checkboxTable");
        template[3] = this.getTextBoxStyle();

        template[6] = this._elementType;
        template[8] = this.getElementName();
        template[10] = this.getDataElementId();
        template[11] = this._getItemElementAttributeHTML();

        if (this.value != null) {
            template[12] = " VALUE='"; template[13] = this.value; template[14] = "'";

            // if we're printing - explicitly mark as checked if appropriate:
            if (this.containerWidget && this.containerWidget.isPrinting) {
                if (value == this.value) template[14] += " CHECKED='true'";
            }

        } else {
            template[12] = null; template[13] = null; template[14] = null;
        }


        if (this.isDisabled() || this.isReadOnly()) template[15] = " DISABLED";
        else template[15] = null;


        if (this.implementsPromptNatively) {
            if (this.prompt != null) {
                template[17] = " TITLE='"; template[18] = this.prompt; template[19] = "'";
            } else {
                template[17] = template[18] = template[19] = null;
            }
        }

        var tabIndex = this._getElementTabIndex();
        template[21] = tabIndex;

        if (isc.Browser.isMoz) {
            template[23] = (tabIndex > 0 ? "normal;'" : "ignore;'");
        }


        if (this.showLabel && !this.showValueIconOnly) {
            // size the title's cell to the available width less the space taken up by the
            // checkbox itself.
            var width = this.getElementWidth(),
                height = this.getInnerHeight();
            if (isc.isA.Number(width)) width = Math.max(20, width - 20);


             if (isc.Browser.isSafari && !this.isDisabled() &&
                 (isc.Browser.isChrome ? isc.Browser.safariVersion < 535 :
                                         isc.Browser.safariVersion < 534.5))
            {
                var titleTemplate = this._$safariTitleTemplate;
                titleTemplate[1] = itemIDStr;
                titleTemplate[3] = this.prompt;
                titleTemplate[5] = this.getTextBoxStyle();
                titleTemplate[7] = this.prompt;
                titleTemplate[9] = title;

                title = titleTemplate.join(isc.emptyString);
            }


            var titleCellTemplate = this._$labelCellTemplate;
            titleCellTemplate[1] = this.getTextBoxStyle();
            if (width != null) {
                titleCellTemplate[3] = " WIDTH="; titleCellTemplate[4] = width;
            } else {
                titleCellTemplate[3] = null; titleCellTemplate[4] = null;
            }

            if (height != null) {
                titleCellTemplate[5] = " HEIGHT="; titleCellTemplate[6] = height;
            } else {
                titleCellTemplate[5] = null; titleCellTemplate[6] = null;
            }
            // cssText - for now just worry about wrap
            if (this.wrap == false) {
                // nowrap cssText picked up from formItem
                titleCellTemplate[8] = this._$nowrapCSS;
            } else {
                titleCellTemplate[8] = null;
            }
            titleCellTemplate[10] = title;

            // actually write it into the table
            template[25] = titleCellTemplate.join(isc.emptyString);
        } else {
            template[25] = null;
        }

        return template.join(isc.emptyString);
    },

    // override getOuterElement - we have no textBox element, though we could concievably have
    // a control table or outer table element.
    getOuterElement : function (a,b,c) {
        if (!this.isDrawn()) return null;

        if (!this._writeOuterTable(this._wroteOutHint)  && !this.showPickerIcon) {
            var element = this.getCheckboxTableElement();

            if (element == null) {
                element = this.getDataElement();
            }
            return element;
        }

        return this.invokeSuper(isc.NativeCheckboxItem, "getOuterElement", a,b,c);
    },

    getCheckboxTableElement : function () {
        var ID = this._getDOMID("checkboxTable");
        return isc.Element.get(ID);
    },


    //>    @method    nativeCheckboxItem.getTitleHTML()    (A)
    //        @group    drawing
    //          return the HTML for the title of this formItem - overridden from
    //          FormItem.getTitleHTML()
    //
    //        @return    (HTML)    title for the formItem
    //<
    getTitleHTML : function () {
        // overridden to return an empty string so that DynamicForm.getTitleHTML() doesn't write
        // a title in the standard way on the left of this form item
        return "";
    },

    // Create a new method 'getLinkText' to get the title for the anchor beside the checkbox
    getAnchorTitle : function (a,b,c,d) {
        // use the superclass implementation of getTitle() -- will return the appropriate value,
        // as an HTML string with any accessKey character underlined.
        // This also sets up the label as a <label> for the checkbox item, and takes care of
        // accessKey behaviour
        return this.invokeSuper(isc.NativeCheckboxItem, "getTitleHTML", a,b,c,d);
    },

    //>    @method    nativeCheckboxItem.setElementValue()
    //        @group    elements
    //            set the value of the form element to the value passed in
    //
    //        @param    newValue     (any)                value to set the element to
    //<
    setElementValue : function (newValue) {
        // get a pointer to the element for this item
        var element = this.getDataElement();

        // if no element was found, bail
        if (!element) return null;

        // set the "checked" property of the DOM form element
        // NOTE: any non-null String is true in JavaScript, which is generally good, except for
        // the surprising case of specifying "false" as a String
        return element.checked = (newValue && newValue != "false");
    },

    //>    @method    nativeCheckboxItem.getElementValue()
    //        @group    elements
    //            return the value stored in the form element(s) for this item
    //
    //        @return    (any)        value of this element
    //<
    getElementValue : function () {
        // get a pointer to the element for this item
        var element = this.getDataElement();

        // if no element was found, bail
        if (!element) return null;

        // get the value of the item
        return (element.checked == true);
    },

    //>    @method    nativeCheckboxItem.boxTitleClick()
    //        @group    event handling
    //            handle a click on the label of a checkbox or radio button
    //            this toggles the state of the item
    //
    //<
    boxTitleClick : function () {
        // get a pointer to the element
        var element = this.getDataElement();

        // toggle the checked property of the element
        if (element) element.checked = !element.checked;

        // have the form call the elementChanged method of the item
        this.form.elementChanged(this.getItemID());
    },

    //> @method nativeCheckboxItem.setElementReadOnly()
    // Change the disabled state of the form element immediately.
    //<
    setElementReadOnly : function (readOnly) {

        this._setElementEnabled(!readOnly);
    },

    // Override updateState to handle re-styling our table and label HTML
    updateState : function () {

        this.Super("updateState", arguments);
        var tableElement = this.getCheckboxTableElement();
        if (tableElement) {
            var style = this.getTextBoxStyle();
            tableElement.className = style;
            if (this.showLabel && !this.showValueIconOnly) {
                var labelCell = tableElement.rows[0].cells[1];
                labelCell.className = style;
            }

        }
    }

});











//>    @class    HeaderItem
// FormItem for showing a header within a DynamicForm.
// <p>
// Set the <code>defaultValue</code> of this item to the HTML you want to embed in the form.
// @visibility external
//<
isc.ClassFactory.defineClass("HeaderItem", "FormItem");
isc.HeaderItem.addProperties({
    // avoid attempting to save this item's value in the form's values array
    shouldSaveValue:false,

    //>    @attr    headerItem.defaultValue (String : "Header" : IRW)
    // Header text
    //        @group    appearance
    // @visibility external
    //<
    defaultValue:"Header",

    //>    @attr    headerItem.height        (number : 20 : IRW)
    // Default height of this item
    //        @group    appearance
    //<
    height:20,

    //>    @attr    headerItem.showTitle        (Boolean : false : IRW)
    // Don't show a separate title cell for headers
    //        @group    appearance
    // @visibility external
    //<
    showTitle:false,

    //>    @attr    headerItem.textBoxStyle (FormItemBaseStyle : "headerItem" : IRW)
    //            Base CSS class for this item
    // @group   appearance
    // @visibility external
    //<
    textBoxStyle:"headerItem",

    //> @attr headerItem.canSelectText (boolean : true : IRW)
    // Should the user be able to select the text in this item?
    // @visibility external
    //<
    canSelectText:true,

    //>    @attr    headerItem.colSpan        (measure : "*" : IRW)
    //            by default, headers span all remaining columns
    //        @group    appearance
    // @visibility external
    //<
    colSpan:"*",

    //>    @attr    headerItem.startRow        (Boolean : true : IRW)
    //            these items are in a row by themselves by default
    //        @group    appearance
    // @visibility external
    //<
    startRow:true,

    //>    @attr    headerItem.endRow            (Boolean : true : IRW)
    //            these items are in a row by themselves by default
    //        @group    appearance
    // @visibility external
    //<
    endRow:true,

    // Override emptyDisplayValue to write out "&nbsp;" so styling will work properly
    emptyDisplayValue:"&nbsp;",

    // Indicate that the developer can't interact directly with the header item content
    isEditable : function () {
        return false;
    }

});






//>    @class    SectionItem
// Header item for a collapsible section in a +link{DynamicForm}.  Each <code>SectionItem</code>
// is associated with a number of other <code>item</code>s in the form, which will be shown or
// hidden as a group when the section is expanded or collapsed.  Clicking on a
// <code>SectionItem</code> will expand or collapse the section.
// <P>
// To make a form where only one section is expanded at a time, set
// +link{DynamicForm.sectionVisibilityMode} to "mutex".
//
// @treeLocation Client Reference/Forms/Form Items
// @visibility external
// @see DynamicForm.sectionVisibilityMode
// @example formSections
//<

// XXX not integrated with showIf - items whose showIf evaluates to true will show up even if they
// belong to a section that's hidden.  Also not integrated with validation.  Items that are in a
// hidden section during the validation run are not validated on the client.
isc.defineClass("SectionItem", "CanvasItem").addProperties({

    shouldSaveValue:false,

    //> @attr   SectionItem.defaultValue    (string : "Section Header" : IRW)
    // Section items show their <code>value</code> as title text for the section.
    // Therefore the simplest way to specify this text on the form item directly is via the
    // <code>defaultValue</code> attribute.
    // @visibility external
    //<
    defaultValue:"Section Header",

    //> @attr sectionItem.itemIds      (Array of ID: null : IR)
    // IDs of the items that should be considered a member of this section.
    // @visibility external
    //<

    //> @attr sectionItem.sectionVisible   (boolean: true : IR)
    // Whether this form section should initially be visible.
    // @deprecated As of SmartClient version 5.5, use +link{sectionItem.sectionExpanded}
    //             instead.
    // @visibility external
    //<
    sectionVisible:true,

    //> @attr sectionItem.sectionExpanded   (Boolean: true : IR)
    // Whether this form section should be initially collapsed. Can be set programmatically
    // via +link{SectionItem.expandSection()} and +link{SectionItem.collapseSection()}.
    // @visibility external
    // @example formSections
    //<
    sectionExpanded: true,

    //> @attr SectionItem.sectionHeaderClass (Classname : "SectionHeader" : [IRA])
    // Name of the Canvas subclass to use as a header that labels the section and allows
    // showing and hiding.  The default class be skinned, or trivial subclasses created to
    // allow different appearances for SectionItems in different forms.
    // Very advanced developers can use the following information to create custom header
    // classes.
    //
    // @visibility external
    //<
    sectionHeaderClass:"SectionHeader",

    //> @attr sectionItem.canCollapse (Boolean : true : IR)
    // Whether this section header can be collapsed.  If set false, suppresses open/close state
    // icon
    // @visibility external
    //<
    canCollapse:true,

    // destroy the (generated) SectionHeader when this item is destroyed
    autoDestroy:true,

    showTitle:false,
    startRow:true, endRow:true, colSpan:"*",

    width:"*",

    height:20
});

//>!BackCompat 2005.6.15
isc.addGlobal("GroupItem", isc.SectionItem);
//<!BackCompat

isc.SectionItem.addMethods({
    init : function () {
        //>!BackCompat 2005.12.22
        // We use sectionExpanded now - if the user override the default of sectionVisible,
        // synch up sectionExpanded
        if (this.sectionVisible == false) this.sectionExpanded = false;
        //<!BackCompat
        this.Super("init", arguments);
    },

    //> @attr sectionItem.canTabToHeader (boolean : null : IR)
    // If true, the header for this Section will be included in the page's tab
    // order for accessibility.
    // May also be set at the +link{DynamicForm} level via
    // +link{DynamicForm.canTabToSectionHeaders}.
    // <P>
    // See +link{group:accessibility}.
    // @visibility external
    //<

    _createCanvas : function () {
        var headerClass = isc.ClassFactory.getClass(this.sectionHeaderClass),
            attributes = {autoDraw: false,
                 section: this,
                 title: this.value != null ? this.value : this.defaultValue,
                 expanded : this.sectionExpanded,
                 layout: this,
                 height: this.height,
                 canCollapse:this.canCollapse,
                 canDrag: false,
                 getCurrentCursor : function () {
                     // support specifying cursor directly on the formItem
                     if (this.canvasItem && this.canvasItem.cursor != null) return this.canvasItem.cursor;
                     return  this.canCollapse == false ? isc.Canvas.DEFAULT : isc.Canvas.HAND;
                 }
            };

        if (this.baseStyle != null) attributes.baseStyle = this.baseStyle;
        if (this.printStyleName != null) attributes.printStyleName = this.printStyleName;

        // support canvasProperties / defaults for freeform customization
        isc.addProperties(attributes, this.canvasDefaults, this.canvasProperties);

        var label = headerClass.create(attributes);
        this.canvas = label;
        this.Super("_createCanvas", arguments);
    },

    // The user is unable to directly interact with the 'value' of a sectionItem
    isEditable : function () {
        return false;
    },

    //> @method     SectionItem.isExpanded()
    // Returns a boolean indicating whether this SectionItem is expanded.
    //
    // @return (Boolean) true if the section is expanded false if not
    // @visibility external
    //<
    isExpanded : function () {
        return this.sectionExpanded == true ? true : false;
    },

    setValue : function (newValue) {
        this.Super("setValue", arguments);
        // NOTE: call getValue(), since if newValue is null, we'll be reverted to default
        if (this.canvas) this.canvas.setTitle(this.getValue());
    },

    sectionHeaderClick : function () {
        this.cellClick();
        return false; // cancel event bubbling
    },


    getDragResizeTarget : function (sectionHeader) {
        return null;
    },

    // clicking on the sectionHeader toggles visibility of the section
    cellClick : function () {
        if (!this.canCollapse) return;
        if (this.sectionExpanded) {
            this.collapseSection();
        } else {
            this.expandSection();
        }
    },

    //> @method     SectionItem.expandSection()
    // Expands a section, showing all the items contained within the section.
    // @visibility external
    //<
    expandSection : function () {
        // notify the form we're about to expand - allows it to handle mutex sections
        this.form._sectionExpanding(this);

        this._createItems();

        if (this.itemIds == null) {
            this.logWarn("sectionItem defined with no items or itemIds");
            return;
        }

        for (var i = 0; i < this.itemIds.length; i++) {
            var itemName = this.itemIds[i],
            item = this.form.getItem(itemName);

            if (item == null) {
                this.logWarn("expandSection: no such item: " + itemName);
                continue;
            }
            // allow selective showing of sectionItems via showIf:
            // item.show()/hide() normally wipes out the showIf function to ensure member is
            // hidden or shown.  On show we want the showIf preserved.  On hide, we need it
            // wiped out (section hide should be unconditional) but we need to restore it when
            // the section is re-shown
            if (item.showIf == null && item._oldShowIf != null) item.showIf = item._oldShowIf;
            item.show(true);
        }

        this.canvas.setExpanded(true);
        this.sectionExpanded = true;

        // ask for layout/sizing recaculation
        this.form._itemsChanged = true;
    },

    _createItems : function () {
        // this.items is expected to be a bunch of form initialization blocks.  This allows
        // lazy initialization of FormItems in a given Section
        if (this.items != null && !this._itemsInstantiated) {
            this.form.addItems(this.items, this.form.items.indexOf(this)+1);
            this.itemIds = [];
            for (var i = 0; i < this.items.length; i++) {
                this.itemIds[i] = this.items[i].getFieldName();
                if (this.itemIds[i] == null) {
                    this.logWarn("unable to include item:"+ this.items[i] + " with no name in section");
                }
            }

            // set a flag so we don't re-instantiate the items next time this method is called
            this._itemsInstantiated = true;
        }
    },

    //>EditMode dynamic adding and removing of items
    addItem : function (item, index) {
        this.form.addItems(item, this.form.items.indexOf(this)+1+(index||0));

        this.itemIds = this.itemIds || [];
        this.itemIds.add(item.name);
    },
    removeItem : function (item) {
        var itemName = (isc.isA.Object(item) ? item.name : item);
        this.itemIds.remove(itemName);

        this.form.removeItems(item);
    },
    getItem : function (item) { return this.form.getItem(item) },
    //<EditMode

    //> @method     SectionItem.collapseSection()
    // Collapse a sectionItem, and hide all the items within the section (not including
    // the header).
    // @visibility external
    //<
    collapseSection : function () {
        // notify the form we're about to collapse (currently unused)
        this.form._sectionCollapsing(this);

        if (this.itemIds == null) {
            // The lack of itemIds is acceptable if we have not yet initialized our "items" array
            if (this.items == null || this._itemsInstantiated) {
                this.logWarn("collapseSection with no sectionItem.itemIds");
                return;
            }
        } else {
            for (var i = 0; i < this.itemIds.length; i++) {
                var itemName = this.itemIds[i],
                    item = this.form.getItem(itemName);
                if (item == null) {
                    this.logWarn("collapseSection: no such item: " + itemName);
                    continue;
                }
                // if this item has a showIf, hold onto it.  We want to hide it unconditionally
                // now, but want the showIf to work on a re-show
                if (item.showIf != null) item._oldShowIf = item.showIf;
                item.hide();
            }
        }
        this.canvas.setExpanded(false);
        this.sectionExpanded = false;

        // ask for layout/sizing recaculation
        this.form._itemsChanged = true;
    },

    _shouldAllowExpressions : function () {
        return false;
    },

    shouldDisableCanvas : function () {
        return this.isDisabled();
    }

});










//>    @class    HiddenItem
// HiddenItems track a value but have no visible appearance and do not take up space in the form
// layout.
// <P>
// When using SmartClient databinding it is usually not necessary to use a HiddenItem, since
// the DynamicForm will track values for which no actual form control exists, and will submit
// these 'extra' values when +link{dynamicForm.saveData()} is called.  HiddenItems only apply
// to forms that are submitted like ordinary HTML forms, via the
// +link{dynamicForm.submitForm()} method.
//
// @visibility external
//<
isc.ClassFactory.defineClass("HiddenItem", "FormItem");
isc.HiddenItem.addProperties({
    //>    @attr    hiddenItem.showTitle        (Boolean : false : IRW)
    //            we never show a separate title cell for hidden fields
    //        @group    appearance
    // @visibility external
    //<
    showTitle:false,

    // Don't specify a cell style - we don't want to take up any space
    cellStyle:null,

    //>    @attr    hiddenItem.width        (number : 0 : IRW)
    //            default width of this item
    //        @group    appearance
    //<
    width:0,

    //>    @attr    hiddenItem.height        (number : 0 : IRW)
    //            default height of this item
    //        @group    appearance
    //<
    height:0,

    //>    @attr    hiddenItem.colSpan                (number : 0 : IRW)
    //            hidden fields don't take up any columns
    //        @group    appearance
    // @visibility external
    //<
    colSpan:0,

    //>    @attr    hiddenItem.rowSpan                (number : 0 : IRW)
    //            hidden fields don't take up any rows
    //        @group    appearance
    // @visibility external
    //<
    rowSpan:0,



    //> @attr   hiddenItem._hasDataElement    (boolean : true : IRW)
    //      Hidden items have a data element.
    // @group formValues
    // @visibility   internal
    // @see     method:FormItem.hasDataElement
    // @see     method:FormItem.getDataElement
    //<
    _hasDataElement:true,


    //>@attr hiddenItem.canFocus  (boolean : false : IRA)
    //  Override canFocus - can never focus in a hidden item.
    //<
    canFocus:false,

    //> @attr hiddenItem.shouldPrint (boolean : false : IRWA)
    // Hidden items will not print
    //<
    shouldPrint:false

});
isc.HiddenItem.addMethods({

    // The user can't interact directly with a hiddenItem.
    isEditable : function () {
        return false
    },

    //>    @method    hiddenItem.getInnerHTML()    (A)
    //  Hidden items render out native hidden HTML input elements. Icons will not be displayed
    //        @group    drawing
    //        @param    value    (string)    Value of the element [Unused because it is more reliably set by setValue].
    //        @return    (HTML)    HTML output for this element
    //<
    getInnerHTML : function (value) {

        var output = isc.StringBuffer.concat(
                        "<INPUT ID='", this.getDataElementId(),
                        "' TYPE=HIDDEN NAME=" , this.getElementName() , ">"
                     );
        return output.toString();
    },

    //>    @method    hiddenItem.getRowSpan()    (A)
    //        @group    drawing
    //            Override formItem.getRowSpan() to return zero
    //          Ensures we don't draw a cell for the item
    //
    //<
    getRowSpan : function () {
        return 0;
    },

    //>    @method    hiddenItem.getColSpan()    (A)
    //        @group    drawing
    //            Override formItem.getColSpan() to return zero
    //          Ensures we don't draw a cell for the item
    //
    //<
    getColSpan : function () {
        return 0;
    },

    //>    @method    hiddenItem.shouldShowTitle()    (A)
    //        @group    drawing
    //      Override formItem.shouldShowTitle to return false - we don't want
    //      to draw a cell for the item title
    //
    //        @return    (HTML)    title for the formItem
    //<
    shouldShowTitle : function () {
        return false;
    },

    //>    @method    hiddenItem.getErrorHTML()    (A)
    //        @group    error handling
    //            output the HTML for an error message in a hidden field
    //            overridden to show an alert when debugging
    //
    //        @param    error        (string)    error message
    //
    //        @return    (HTML)        (nothing)
    //<
    getErrorHTML : function (error) {
        //>DEBUG
        this.logError("Error in hidden field '" + this.getFieldName() + "':\r  " + error);
        //<DEBUG
        return null;
    },

    //> @method hiddenItem.isStartRow()   (A)
    //      @group drawing
    //          override formItem.isStartRow() to return false
    //<
    isStartRow : function () {
        return false;
    },

    //> @method hiddenItem.isEndRow()   (A)
    //      @group drawing
    //          override formItem.isEndRow() to return false
    //<
    isEndRow : function () {
        return false;
    }


});










//>    @class    StaticTextItem
//    A FormItem that displays an uneditable value.
// @visibility external
//<
isc.ClassFactory.defineClass("StaticTextItem", "FormItem");
isc.StaticTextItem.addProperties({
    //>    @attr    staticTextItem.height        (number : null : IRW)
    //            don't specify a height so the table cell will expand
    //            to show the entire contents.  Note that this can
    //            mess up dynamic height calculations in forms.
    //        @group    appearance
    //<
    height:null,

    //> @attr staticTextItem.applyHeightToTextBox (Boolean : false : IRA)
    // If +link{formItem.height} is specified, should it be applied to the
    // item's text box element?
    // <P>
    // Overridden to be <code>false</code> for StaticTextItems by default.
    // <P>
    // See +link{shouldApplyHeightToTextBox()} for more information.
    // @visibility external
    //<
    applyHeightToTextBox:false,

    //>    @attr    staticTextItem.width        (number : null : IRW)
    //            If a width is specified, we write out a table to make width consistent,
    //            if <code>null</code> is used, we write out a SPAN which is cheaper.
    //        @group    appearance
    //<
    width:null,

    //>    @attr    staticTextItem.wrap        (Boolean : true : IRW)
    // @include FormItem.wrap
    //        @group    appearance
    // @visibility external
    //<
    wrap:true,

    //>@attr    staticTextItem.clipValue (Boolean : false : IRW)
    // @include FormItem.clipValue
    // @group appearance
    // @visibility external
    //<
    clipValue:false,

    //>    @attr    staticTextItem.textBoxStyle    (FormItemBaseStyle : "staticTextItem" : IRW)
    //  Base CSS class for this item
    // @group   appearance
    // @visibility external
    //<
    textBoxStyle:"staticTextItem",

    //> @attr staticTextItem.canSelectText (boolean : true : IRW)
    // Should the user be able to select the text in this item?
    // @visibility external
    //<
    canSelectText:true,

    // when dynamically showing/hiding icons we should be able to resize our textBox without
    // redraw.
    redrawOnShowIcon:false,

    //>    @attr    staticTextItem.outputAsHTML (boolean : null : IRW)
    // By default HTML values in a staticTextItem will be interpreted by the browser.
    // Setting this flag to true will causes HTML characters to be escaped, meaning the
    // raw value of the field (for example <code>"&lt;b&gt;AAA&lt;/b&gt;"</code>) is displayed
    // to the user rather than the interpreted HTML (for example <code>"<b>AAA</b>"</code>)
    // @group appearance
    // @visibility external
    // @deprecated in favor of +link{staticTextItem.escapeHTML}
    //<
//    outputAsHTML:false,

    // set useShortDateFormat to false.
    // This will use "toNormalDate()" rather than toShortDate for date values
    // Other than those in logical "date" type fields (where we don't want to show time).
    // Document this behaviour by explicitly calling it out in the dateFormatter docs for
    // StaticTextItems.
    //>    @attr staticTextItem.dateFormatter (DateDisplayFormat : null : [IRWA])
    // Display format to use for date type values within this formItem.
    // <P>
    // Note that Fields of type <code>"date"</code>, <code>"datetime"</code> or <code>"time"</code> will
    // be edited using a +link{DateItem} or +link{TimeItem} by default, but
    // this can be overridden - for <code>canEdit:false</code> fields, a
    // +link{StaticTextItem} is used by default, and the developer can always specify
    // a custom +link{formItem.editorType} as well as +link{formItem.type,data type}.
    // <P>
    // The +link{formItem.timeFormatter} may also be used to format underlying Date values as
    // times (ommitting the date part entirely). If both <code>dateFormatter</code> and
    // <code>timeFormatter</code> are specified on an item, for
    // fields specified as +link{formItem.type,type "time"} the
    // <code>timeFormatter</code> will be used, otherwise the <code>dateFormatter</code>
    // <P>
    // If <code>item.dateFormatter</code> and <code>item.timeFormatter</code> is unspecified,
    // date display format may be defined at the component level via
    // +link{DynamicForm.dateFormatter}, or for fields of type <code>"datetime"</code>
    // +link{DynamicForm.datetimeFormatter}. Otherwise for fields of type "date",
    // default is to use the system-wide default short date format, configured via
    // +link{Date.setShortDisplayFormat()}. For fields of type "datetime" or for Date values
    // in fields whose type does not inherit from the logical "date" type, default is to use
    // the system-wide normal date format configured via +link{Date.setNormalDisplayFormat()}
    // (using "toNormalDate()" on logical <code>"date"</code> type fields is not desirable as this
    // would display the time component of the date object to the user).<br>
    // Specify any valid +link{type:DateDisplayFormat} to
    // change the format used by this item.
    //
    // @see formItem.timeFormatter
    //
    // @group appearance
    // @visibility external
    //<
    //dateFormatter:null,
    useShortDateFormat:false,

    //> @attr staticTextItem.escapeHTML (Boolean : false : IRW)
    // By default HTML values in a staticTextItem will be interpreted by the browser.
    // Setting this flag to true causes HTML characters to be escaped, meaning the
    // raw value of the field (for example <code>"&lt;b&gt;AAA&lt;/b&gt;"</code>) is displayed
    // to the user rather than the interpreted HTML (for example <code>"<b>AAA</b>"</code>)
    // @group appearance
    // @visibility external
    //<
    // implemented at the formItem level - enable via the canEscapeHTML flag
    canEscapeHTML:true,
    escapeHTML:null,

     // override 'emptyDisplayValue' to write out "&nbsp;" instead of "" for styling
    emptyDisplayValue:"&nbsp;"

});
isc.StaticTextItem.addMethods({

    // Static text items are used for display only - non editable
    isEditable : function () {
        return false;
    },

    // A StaticTextItem's value is clipped if either clipValue or clipStaticValue is set.
    _getClipValue : function () {
        return !!this.clipValue || this._getClipStaticValue();
    },

    _canFocus : function () {
        if (this.canFocus != null) return this.canFocus;
        // needs to be focusable in screen reader mode because the value will only be read if the item
        // can be tabbed to
        return isc.screenReader;
    },

    // in canEdit: false mode, fields of type="float" become StaticTextItems, but we still want
    // to apply the decimalPad/precision flags on these displayed values.  Note that this code
    // also appears in FloatItem
    mapValueToDisplay : function (value) {
        if (isc.SimpleType.inheritsFrom(this.type, "float")) {
            var floatValue = null;
            if (isc.isA.String(value)) {
                var parsedValue = window.parseFloat(value);
                if (!window.isNaN(parsedValue) && parsedValue == value) {
                    floatValue = parsedValue;
                }
            } else if (isc.isA.Number(value)) {
                floatValue = value;
            }
            if (floatValue != null) {
                if (this.decimalPrecision != null || this.decimalPad != null) {
                    return isc.Canvas.getFloatValueAsString(floatValue,
                        this.decimalPrecision, this.decimalPad);
                } else if (this.precision != null) {
                    return isc.Canvas.getNumberValueAsString(floatValue,
                        this.precision, "float");
                }
            }
        }
        return this.Super("mapValueToDisplay", arguments);
    }
});









//> @class LinkItem
// A form item that displays a URL. In the default read-only mode (+link{FormItem.canEdit,canEdit}
// is <code>false</code>) the URL is shown as a link; in editable mode the URL is shown in a textbox.
// <P>
// The link to open is specified as the item value with +link{formItem.setValue} or
// +link{formItem.defaultValue}. The link title defaults to the URL unless
// +link{linkItem.linkTitle} is specified.
// <P>
// Additionally, a custom action can be triggered when the link is clicked: see
// +link{linkItem.target} for details.
//
// @visibility external
//<
isc.ClassFactory.defineClass("LinkItem", "TextItem");
isc.LinkItem.addProperties({

    //> @attr linkItem.disableIconsOnReadOnly (Boolean : false : IRW)
    // @include formItem.disableIconsOnReadOnly
    // @group formIcons
    // @visibility external
    //<
    disableIconsOnReadOnly:false,

    wrap: false,
    clipValue: false,

    // Default the iconVAlign to "center" because the height of the link is probably less than
    // the form item icons' height, and the link should be vertically centered with the icons
    // by default.
    //> @attr linkItem.iconVAlign (VerticalAlignment : "center" : IRWA)
    // @include FormItem.iconVAlign
    //<
    iconVAlign: "center",

    // Don't set a height on LinkItems by default

    height:null,

    // default to canEdit: false - an editable link is a possibility, but it doesn't seem
    // that most users would want/expect this to be the default state
    canEdit: false,

    // apply "static type format" to LinkItems in read-only mode
    shouldApplyStaticTypeFormat : function () {
        return !this.canEdit;
    },

    //> @attr linkItem.readOnlyDisplay
    // If +link{FormItem.canEdit,canEdit} is set to <code>false</code>, how should this
    // <code>LinkItem</code> be displayed to the user?
    // <p>
    // Link items are, by default, canEdit:false. Note that the link remains active regardless
    // of the <code>readOnlyDisplay</code> setting.
    // @include FormItem.readOnlyDisplay
    //<

    // For LinkItem, "disabled" is the only readOnly mode
    readOnlyTextBoxStyle:"staticTextItemDisabled"

    //> @attr linkItem.target (string : "_blank" : IRW)
    // By default, clicking a link rendered by this item opens it in a new browser window.  You
    // can alter this behavior by setting this property.  The value of this property will be
    // passed as the value to the <code>target</code> attribute of the anchor tag used to render
    // the link.
    // <P>
    // If you set linkItem.target to "javascript", the default behaviour is to catch and consume
    // mouse-clicks that would result in the link being followed.  Instead, the
    // +link{formItem.click()} event is fired.
    //
    // @visibility external
    //<

    //> @attr linkItem.linkTitle (HTMLString : null : IRW)
    // Optional title HTML to display for this item's link. If unset, the <code>LinkItem</code>'s
    // value (the URL) will be used for the link's title.
    // @setter setLinkTitle()
    // @visibility external
    //<

});

isc.LinkItem.addMethods({

    // Even though we don't have a data element, we don't need a focus proxy - <a..> will
    // recieve focus in all browsers
    _writeOutFocusProxy : function () {
        return (this.isReadOnly() ? false
                                  : this.Super("_writeOutFocusProxy", arguments));
    },

    _getLinkElement : function () {
        if (!this.isReadOnly()) return this.Super("_getLinkElement", arguments);
        if (!this.isDrawn()) return null;
        return (isc.Element.get(this.getID() + "_link"));
    },

    // Apply focus/blur handlers to the link itself
    getFocusElement : function () {
        return (this.isReadOnly() ? this._getLinkElement()
                                  : this.Super("getFocusElement", arguments));
    },

    hasDataElement : function () {
        return !this.isReadOnly();
    },

    _canFocus : function () {
        // In read-only mode we still want to be focusable
        return (this.isReadOnly() ? true : this.Super("_canFocus", arguments));
    },

    getTextBoxStyle : function () {
        if (!this.isReadOnly()) return this.Super("getTextBoxStyle", arguments);

        if (this._isPrinting() && this.printTextBoxStyle) {
            return this._getCellStyle(this.printTextBoxStyle);
        }
        return (this.readOnlyTextBoxStyle ? this._getCellStyle(this.readOnlyTextBoxStyle)
                                          : null);
    },

    // modify the text box template slightly - we're writing out a text box but it doesn't
    // need to be focusable
    _$textBoxTemplate:[ "<DIV ID='", // 0
                        ,            // 1: ID for text box
                        "' " + isc.DynamicForm._containsItem + "='", // 2
                        ,            // 3 [formItem ID]
                        , // 4

                        "' CLASS='", // 5
                        ,            // 6: this.getTextBoxStyle(),
                        "' STYLE='", // 7
                        ,            // 8: this.getTextBoxCSS(),
                        "'>",        // 9
                        ,            // 10: actual value
                        "</DIV>"
    ],

    // If a linkItem was written out as 'inactive' (EG part of print view) - how should we
    // handle this.
    // Default behavior is to suppress the navigation to href
    // Can be toggled to allow the navigation via undocumented flag "inactiveEditorLinkDisabled"
    inactiveEditorLinkDisabled:true,
    _inactiveLinkClicked : function (event) {
        if (!this.inactiveEditorLinkDisabled) {
            return this._linkClicked(event);
        }
        // Standard 'suppress native behavior' logic.
        if (!isc.Browser.isIE) {
            event.preventDefault();
        }
        return false;
    },

    _linkClicked : function (event) {
        // don't allow the click if the cell should not be interactive.
        var mustCancel = (this.destroyed || !this.isDrawn() || !this.isVisible() ||
                          this.renderAsDisabled());
        // If a clickMask is up and the item is masked, cancel the event.
        // Check both the containerWidget and the form. If they differ and  either is unmasked
        // the item is not considered masked.
        if (!mustCancel) {
            mustCancel = isc.EH.targetIsMasked(this.containerWidget);
            if (mustCancel && (this.form != this.containerWidget)) {
                mustCancel = isc.EH.targetIsMasked(this.form);
            }
        }
        if (!mustCancel && this.target == "javascript") {
            mustCancel=true;
            this.handleClick();
        }

        if (mustCancel) {

            if (!isc.Browser.isIE) {
                event.preventDefault();
            }
            return false;
        }

        return true;
    },
    getReadOnlyHTML : function (value) {
        this._retrievingInactiveHTML = true;
        var linkHTML = this.getLinkHTML(value);

        var template = this._$textBoxTemplate;
        template[1] = this._getTextBoxID();
        template[3] = this.getID();

        template[6] = this.getReadOnlyTextBoxStyle();
        template[8] = this.getTextBoxCSS(value);

        template[10] = linkHTML;

        delete this._retrievingInactiveHTML;
        return template.join(isc.emptyString);
    },

    getLinkHTML : function (text) {
        var valueIconHTML = this._getValueIconHTML(this._value);
        if (this.showValueIconOnly) return valueIconHTML;

        // convert to String
        if (text != null) text = isc.iscToLocaleString(text);
        if (text == null) text = isc.emptyString;

        var title = this.linkTitle;
        if (title == null) title = text;

        // Convert to actual link
        var target = this.target;
        if (target == "javascript") {
            text = "javascript:void";
        }

        var onclickString = [
            " onclick='if(window.",
            this.getID(),
            ") return ",this.getID()
        ];
        if (this.isInactiveHTML()) {
            onclickString.add(
                "._inactiveLinkClicked(event); " +
                "else {if (event.preventDefault != null) event.preventDefault(); return false;}' "
            );
        } else onclickString.add( "._linkClicked(event);' " );
        onclickString = onclickString.join("");

        if (!this.renderAsDisabled()) {
            text = isc.Canvas.linkHTML(text, title, target,
                                    (this.getID() + "_link"), this.getGlobalTabIndex(),
                                    this.accessKey,
                                    onclickString,
                                    isc.DynamicForm._itemPart + "='"
                                        + isc.DynamicForm._textBoxString + "'"
                );
        } else {
            text = "<span style='text-decoration: underline;'>" + title + "</span>";
        }

        if (valueIconHTML != null) text = valueIconHTML + text;

        return text;
    },

    // Override setElementValue to update the text box with the correct value
    setElementValue : function (value) {
        if (!this.isDrawn()) return;
        if (!this.isReadOnly()) return this.Super("setElementValue", arguments);


        if (this.hasFocus) this.blurItem();

        var textBox = this._getTextBoxElement();
        if (textBox) {
            textBox.innerHTML = this.getLinkHTML(value);
            // Re apply the event handlers
            this._applyHandlersToElement();
        }
    },

    //> @method linkItem.setLinkTitle()
    // Setter for +link{attr:linkTitle}.
    // @param title (HTMLString) new <code>linkTitle</code> HTML.
    // @visibility external
    //<
    setLinkTitle : function (title) {
        this.linkTitle = title;
        this.redraw();
    },

    //> @method linkItem.setElementReadOnly()
    // Change the read-only state of the form element immediately.
    //<
    setElementReadOnly : function (readOnly) {
        // TextItem already handles this by marking item as readonly or not.
        // LinkItem needs to render two different items completely so we override
        // here and force a redraw on ourselves.
        this.redraw();
    }

});







//>    @class    PasswordItem
// FormItem for password fields, where text input by the user should not be shown in readable text.
// @visibility external
//<
isc.ClassFactory.defineClass("PasswordItem", "TextItem").addClassProperties({
    _bullets:"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"
});
isc.PasswordItem.addProperties({
    _elementType:"PASSWORD",

    showClippedValueOnHover: false
});

isc.PasswordItem.addMethods({

valueHoverHTML : function () {
    var elementValue = this.getElementValue();
    if (elementValue == null) elementValue = "";
    var bullets = isc.PasswordItem._bullets;
    while (bullets.length < elementValue.length) isc.PasswordItem._bullets = bullets += bullets;
    return bullets.substring(0, elementValue.length);
}

});










//>    @class    RadioGroupItem
// FormItem that shows a set of mutually exclusive options as a group of radio buttons.
// @visibility external
//<
isc.ClassFactory.defineClass("RadioGroupItem", "ContainerItem");
isc.RadioGroupItem.addProperties({

    //>    @attr    radioGroupItem.itemHeight        (number : 20 : IRW)
    //            default height of EACH item
    //        @group    appearance
    //<
    itemHeight:20,

    //>    @attr    radioGroupItem.vertical            (Boolean : true : IRW)
    //            True == display options vertically, false == display in a single row
    //        @group    appearance
    // @visibility external
    //<
    vertical:true,

    //>    @attr    radioGroupItem.prompt            (string : null : IRW)
    // Don't have a prompt for the entire item -- use itemPrompt to set the prompt of each
    // individual button.
    //        @group    appearance
    //<
    prompt:null,

    //>    @attr    radioGroupItem.itemPrompt            (string : null : IRW)
    // Mouse-over prompt for the label of this item.  Use character <code>"*"</code>
    // to substitute in the item value. For example <code>"Select value *"</code> would
    // show the prompt <b>"Select value a"</b> when a user hovered over a radio button for
    // the value <code>"a"</code>.
    //        @group    appearance
    //<
    //itemPrompt:null

    //>    @attr    radioGroupItem.textBoxStyle (FormItemBaseStyle : "labelAnchor" : IRW)
    // Base CSS class applied to the text for items within this radiogroup.
    // @group appearance
    // @visibility external
    //<
    textBoxStyle:"labelAnchor",

    //>    @attr    radioGroupItem.wrap (boolean : null : IRW)
    // Should the text for items within this radio group wrap?
    // @group appearance
    // @visibility external
    //<

    // Don't write out the <label for...> tag around the title of a radioGroup as a whole
    // - we'll write it out around the individual radio-items' titles. Writing it out for
    // the group-title as a whole gives the odd effect of making this title the tab-stop
    // for the item in IE.
    writeOutLabelTag:false
});

isc.RadioGroupItem.addMethods({

    init : function () {
        // handle init-time only 'disabledValues' flag
        if (this.disabledValues != null) {
            this._disabledValues = {};
            for (var i = 0; i < this.disabledValues.length; i++) {
                var value = this.disabledValues[i];
                this._disabledValues[value] = true;
            }
        }
        return this.Super("init", arguments);
    },

    // Override _useHiddenDataElement to return false
    // we're not marked as having a data element (hasDataElement() == false) but our sub elements
    // do have live HTML data elements, so on form submission we don't need to write out a
    // hidden data element.
    _useHiddenDataElement : function () {
        return false;
    },

    //> @method radioGroupItem.valueHoverHTML() (A)
    // If defined, this method should return the HTML to display in a hover canvas when the
    // user holds the mousepointer over one of the radio-items in this RadioGroupItem.  Return
    // null to suppress the hover canvas altogether.
    //
    // @param value (Any) The sub-value (radio-item) to get the hoverHTML for
    // @param item (RadioGroupItem) Pointer to this item
    // @param form (DynamicForm)  This item's form
    // @return (HTMLString) HTML to be displayed in the hover
    // @group Hovers
    // @visibility external
    //<



    //>    @method    radioGroupItem.setItems()    (A)
    // Override setItems() to create radio buttons for each item in our valueMap.
    // We store items we've already created in a cache so we don't create them over and
    // over redundantly.
    //
    //        @group    drawing
    //
    //        @param    value    (string)    Value of the element [Unused because it is more reliably set by setValue].
    //        @return    (HTML)    HTML output for this element
    //<
    // Note: If the developer were to specify more than one item in the valueMap with the same
    // title/value, we would get in trouble here, as we'd end up with more than one entry in
    // our items array pointing to the same form item - This is an invalid usage by the
    // developer, but we don't currently catch this case explicitly.
    setItems : function () {
        var valueMap = this.getValueMap();
        if (!this.itemCache) this.itemCache = {};

        // create an array to hold the new set of items
        var items = [];

        if (isc.isAn.Array(valueMap)) {
            for (var i = 0; i < valueMap.length; i++) {
                var value = valueMap[i];

                items.add(this._getRadioItem(value, value));
            }
        } else {
            for (var value in valueMap) {
                var title = valueMap[value];

                items.add(this._getRadioItem(title, value));
            }
        }
        // call the superclass method to actually create the items
        return this.Super("setItems", [items]);
    },

    //> @method radioGroupItem._getRadioItem() (A)
    // Internal method to create radio item instances based on the title/value passed in.
    //
    // @param title (string) Title to display next to the radio item
    // @param value (any) value the radio item represents.
    // @return (RadioItem) RadioItem instance to plug into the items array.
    // @group drawing
    //<
    _getRadioItem : function (title, value) {

        var item = this.itemCache[value+"|"+title];
        // If we're reusing an item, clean off the old properties from when it was last used
        // such as _value / hasFocus

        if (item) {
            delete item._value;
            delete item.hasFocus;
            delete item.disabled;

        } else {

            var prompt;
            if (this.itemPrompt) {
                var starRE = new RegExp("\\*","g");
                prompt = this.itemPrompt.replace(starRE, title);
            }
            var itemObj = {
                    type:"radio",

                    // ContainerItem stores sub-items under subItem.name.  Give the RadioItems
                    // names that won't collide with other properties, but that we can look up
                    // by value.
                    name:"_radioItem" + value,

                    value:value,
                    // override getElementName - the 'name' written into the HTML element
                    // must be the same for all these elements to create a radiogroup.
                    getElementName : function () {
                        return this.parentItem.getElementName();
                    },

                    title:title,
                    prompt:prompt,
                    height:this.itemHeight,
                    _startRow:this.vertical,

                    // apply styling down to the items
                    textBoxStyle: this.textBoxStyle,
                    wrap:this.wrap,


                    updateValue : function () {
                        // pass in this.value (not this.getValue() which could be null)
                        this.parentItem.updatePreviousSelection(this.value);
                        return this.Super("updateValue", arguments);
                    },

                    setDisabled : function (disabled) {
                        this.parentItem._itemDisabled(this.value, disabled);
                        return this.Super("setDisabled", arguments);
                    },

                    shouldSaveValue:false,

                    // don't allow changed events from sub-items to fire on the form
                    suppressItemChanged: true
                }
            ;

            // support arbitrary modification of the item via an auto-child like properties block
            isc.addProperties(itemObj, this.itemProperties);

            var _this = this;

            if (!itemObj.itemHoverHTML && this.valueHoverHTML) {
                itemObj.itemHoverHTML = function () {
                    return _this.valueHoverHTML(value, _this, _this.form);
                }
            }

            item = this.itemCache[value+"|"+title] = isc.FormItemFactory.makeItem(itemObj);
        }
        // The RadioGroup manages the disabled status of individual sub-items based on their
        // value.
        // Apply the appropriate disabled status to the item in question.

        if (this._disabledValues != null && this._disabledValues[value] != null) {
            item.disabled = this._disabledValues[value];
        }
        return item;
    },

    //>@attr RadioGroupItem.itemProperties (RadioItem Properties : null : IR)
    // Map of properties to apply to generated items within this RadioGroup. This allows you to
    // customize the generated radio items for this item.
    // @visibility external
    //<
    // No need for a defaults block - the developer can just update the RadioItem class


    // getItemValue is used when generating member item HTML

    getItemValue : function (item) {
        var itemVal = item.value;
        if (itemVal == this.getValue()) return itemVal;
        return item.unselectedValue;
    },

    itemForValue : function (value) {
        return this["_radioItem" + value];
    },

    //> @attr radioGroupItem.disabledValues (Array of String : null : I)
    // This property allows you to specify an initial set of disabled options within
    // this radioGroup. Once the RadioGroupItem has been created +link{setValueDisabled()}
    // should be used to enable and disable options.
    // @visibility external
    //<

    //> @method radioGroupItem.setValueDisabled()
    // Disable or Enable a specific option within this radioGroup
    // @param value (any) value of option to disable
    // @param disabled (boolean) true to disable the option, false to enable it
    // @visibility external
    //<
    setValueDisabled : function (value, disabled) {
        if (this._disabledValues != null && this._disabledValues[value] == disabled) return;
        var item = this.itemForValue(value);
        if (item && this.items.contains(item)) {
            // call 'setDisabled' on the item directly.
            // This is overridden to update our "disabled values" object
            item.setDisabled(disabled);
        // Support changing the disabled status for a value even if it
        // doesn't have an associated item. This actually means you can disable a value
        // that isn't necessarily in the valueMap - and if the valueMap is updated to
        // include it the item in question will show up disabled.
        } else {
            this._disabledValues[value] = disabled;
        }
    },

    _itemDisabled : function (value, disabled) {
        if (this._disabledValues == null) this._disabledValues = {};
        this._disabledValues[value] = disabled;
    },

    // Override ContainerItem.getTitleHTML().
    getTitleHTML : function () {
        // We need something with an ID to set the 'aria-labelledby' attribute to.

        var title = this.getTitle();
        return isc.SB.concat("<label id=", this._getLabelElementID(), ">", title, "</label>");
    },

    //>    @method    radioGroupItem.getInnerHTML()    (A)
    //        @group    drawing
    //            output the HTML for this element
    //
    //        @param    value    (string)    Value of the element [Unused because it is more reliably set by setValue].
    //        @return    (HTML)    HTML output for this element
    //<
    getInnerHTML : function (value) {
        // always call setItems() since the valueMap may have changed!
        this.setItems();
        return this.Super("getInnerHTML", arguments);
    },

    // Override isEditable to indicate that the user can directly update the radioGroup's value
    isEditable : function () {
        return true;
    },

    //> @method radioGroupItem.setElementReadOnly()
    // Change the read-only state of the form element immediately.
    //<
    setElementReadOnly : function (readOnly) {
        // There is no element to mark read-only but we don't want the entire radio group
        // to be redrawn so we handle this method and do nothing. The radio items will be
        // updated individually.
    },

    //> @method radioGroupItem.setValue()
    //      Set the value of this radiogroup item.
    //
    //      @group formValues
    //      @param  newValue    (any)           value to set the item to
    //<
    setValue : function (newValue) {
        this._setValueCalled = true;

        var invalidValue = (this.valueMap == null) ||
            (isc.isAn.Array(this.valueMap) ? !this.valueMap.contains(newValue)
                                            : !isc.propertyDefined(this.valueMap, newValue));


        // use the default value if passed null, or a value for which we don't have a sub-item
        if (newValue == null || invalidValue) {
            newValue = this.getDefaultValue();
        }
        // "check" the appropriate sub item.
        // If there is none, uncheck whatever is currently selected.
        // Notes:
        // - We performed this check above to determine whether we need to apply a default
        //   value.  We still need to check again as we may not have a default value (or our
        //   default value could conceivably not be included in our valueMap).
        // - Clearing out our value if we don't find the value passed in means the user setting
        //   the value of a radio-group to anything not in the valueMap is an equivalent to
        //   setting the value to null.
        if (this.items != null) {
            var itemForValue = this.itemForValue(newValue);
            if (itemForValue != null) {
                this.itemForValue(newValue).setValue(newValue);
            }

            // if the previous value is not null, clear out the prev item's value
            // This serves 2 purposes:
            // - if we're clearing the value for this item, its required to update the
            //   currently selected item on the page
            // - otherwise the currently selected item will already have been cleared, but
            //   we still want to clear out its _value property.
             if (this._value != null && this._value != newValue) {
                 var prevItem = this.itemForValue(this._value);
                 if (prevItem) prevItem.setValue(null);
             } else if (this._changingValue) {
                var prevItem = this.itemForValue(this._changeValue);
                if (prevItem) prevItem.setValue(null);
             }
        }

        // save the new value
        this.saveValue(newValue);
    },

    // this function is fired as a result of a selection change in the radioGroup
    // Its purpose is to ensure that the item that was previously selected has its internal
    // _value updated to match the fact that it has been deselected.
    // We have to do this because when a radio item gets selected, it fires a change handler
    // for itself, but no change handler is fired for the previous selection which gets
    // automatically deselected due to native mutex behavior
    // This function updates the deselected item to ensure that a getValue() call for it returns
    // the appropriate value (null)
    updatePreviousSelection : function (newValue) {

        // Bail if 'updateValue' is fired on the currently selected item.
        // This avoids an infinite loop whereby
        //  "A" is initially selected
        //  User selects "B"
        //  'updateValue' on "B" fires 'updatePreviousSelection' on the group item
        //  updatePreviousSelection on the group item fires 'updateValue' on "A"
        //  'updateValue' on "A" fires 'updatePreviousSelection' on the group item
        // -- we stop the process there

        // Note: We support valueMaps as objects or arrays.
        // If the valueMap is an object, the keys are always strings - so this:
        //   valueMap:{true:"Yes", false:"No"}
        // Will map to "true" and "false" rather than true and false.
        // In this case we are likely to see the value of the item being set programmatically
        // to a non string value (such as boolean true), but the "newValue", picked up from
        // updateValue() on the selected item being a string value.
        // Ensure in this case we convert the old value to a string before comparing.
        // (if we skip this we can end up with failure to update or infinite loops in some cases)
        var currentValue = this.getValue();
        if (isc.isA.String(newValue)) currentValue = currentValue + "";
        if (currentValue == null || newValue == currentValue ||
            this.itemForValue(currentValue) == null)
        {
            return;
        }
        this.itemForValue(currentValue).updateValue();


    },

    // override updateValue to get the value from the items
    // NOTE: We're currently relying on the fact that 'elementChanged()' will be bubbled and the
    // default implementation of this internal handler will fire updateValue() on a form item.
    // We may want to prevent elementChanged from bubbling, since Form Items should be managing
    // their own elements completely opaquely.  If so we would want to write change handlers
    // onto the sub-items to fire this.parentItem.updateValue().
    updateValue : function () {
        var value;
        // Any non-selected radioItem will return "null" from getValue();
        for (var i = 0; i < this.items.length; i++) {
            value = this.items[i].getValue();
            if (value != null) {
                break;
            }
        }

        // bail if the value is unchanged
        if (value == this._value) return;

        // fire the change handler, and bail if the change failed validation, etc.
        // Note: this method will call 'setValue()' to reset to the old value, or any value
        // suggested by the validators
        if (this.handleChange(value, this._value) == false) {
            return;
        }
        // check for updates performed by the change handler.
        value = this._changeValue;

        // save the value
        this.saveValue(value);

        // fire any specifed 'changed' handler for this item.
        this.handleChanged(value);


    },

    //>    @method    radioGroupItem.setValueMap
    //  Override setValueMap to redraw the form with the new set of radio items
    //<
    // A better way to do this would be to replace just the inner HTML for the relevant cell in
    // the dynamicForm, but currently we have no way to do that.
    setValueMap : function (valueMap) {
        this.Super("setValueMap", arguments);
        // unfortunately, there is no way to just update these values
        //  so we have to tell the item to redraw (will redraw the form by default)
        this.redraw();
    },

    //>    @method    radioGroupItem.getHeight()    (A)
    //        @group    sizing
    //            output the height for this element
    //
    //        @return    (number)    height of the form element
    //<
    getHeight : function () {
        var cellPadding = isc.isA.Number(this.cellPadding) ? this.cellPadding : 0;
        if(!this.vertical) return this.itemHeight + cellPadding * 2;
        var valueMap = this.getValueMap(),
            numItems = 0;

        // if valueMap is an array, number of items is just the length of the array
        if (isc.isAn.Array(valueMap)) {
            numItems = valueMap.length;
        } else {
            for (var key in valueMap) {
                numItems++;
            }
        }
        return numItems * (this.itemHeight + cellPadding * 2);
    }
});









//>    @class    RadioItem
// Form item representing a member of a radio group, subclassed from +link{NativeCheckboxItem}.
// RadioItems items are created and managed automatically by +link{RadioGroupItem} instances
// and should not be instantiated directly.
//
//  @treeLocation   Client Reference/Forms/Form Items/RadioGroupItem
// @visibility external
//<
isc.ClassFactory.defineClass("RadioItem", "NativeCheckboxItem");
isc.RadioItem.addProperties({
    //>    @attr    radioItem.prompt            (string : null : IRW)
    // Mouse-over prompt for the label of this item
    //        @group    appearance
    //<
    //prompt:null

    //>    @attr    radioItem._elementType                (string : "RADIO" : IRW)
    //            type of item ("CHECKBOX" or "RADIO")
    //        @group    appearance
    //<
    _elementType:"RADIO",

    //>    @attr    radioItem.value             (any : true : IRW)
    //          "value" for this radio item, to be returned when the item is selected.
    //  @group formValues
    //  @visibility internal
    //<
    value : true,

    //>    @attr    radioItem.unselectedValue   (any : null : IRW)
    //          Value to be returned from this radio item when it is unselected.
    //   @group formValues
    //   @visibility internal
    //<

    //>    @attr    radioItem.defaultValue      (any : null : IRW)
    //          Override defaultValue to be null - note that the value returned from an unselected
    //          radioItem will always be null. Set to radioItem.value to have the radioItem be drawn
    //          in a selected state initially.
    //   @group formValues
    //<
    defaultValue:null
});
isc.RadioItem.addMethods({


    //>    @method    radioItem.setElementValue()
    //        @group    elements
    //            update the visible value displayed in the form element to the reflect value passed in
    //
    //        @param    newValue     (any)                value to set the element to
    //<
    setElementValue : function (newValue) {
        // get a pointer to the element for this item
        var element = this.getDataElement();

        // if no element was found, bail
        if (!element) return null;


        if (isc.isA.String(this.value)) newValue = (newValue + "");

        // set the value of the item
        return element.checked = (this.value == newValue);
    },

    //>    @method    radioItem.getElementValue()
    //        @group    elements
    //            return the value stored in the form element(s) for this item
    //
    //        @return    (any)        value of this element
    //<
    getElementValue : function () {
        // get a pointer to the element for this item
        var element = this.getDataElement(),
            selectedValue = this.value,
            unselectedValue = this.unselectedValue;

        // if no element was found, bail
        if (!element) return unselectedValue;

        // get the value of the item
        return (element.checked ? selectedValue : unselectedValue);
    },

    //>    @method    radioItem.boxTitleClick()
    //        @group    event handling
    //            handle a click on the label of a checkbox or radio button
    //            this toggles the state of the item
    //
    //<
    boxTitleClick : function () {
        // get a pointer to the element
        var element = this.getDataElement();

        // toggle the checked property of the element
        if (element && !element.checked) {
            element.checked = true;
            // call the elementChanged method of the form
            this.form.elementChanged(this.getItemID())
        }
    },

    //>    @method    radioItem.mapValueToDisplay()    (A)
    //        @group    drawing
    //            Map from the internal value for this item to the display value.
    //        @param    internalValue        (string)    Internal value for this item.
    //        @return    (string)    Displayed value corresponding to internal value.
    //<
    mapValueToDisplay : function (internalValue) {
        return internalValue;
    },

    //>    @method    radioItem.mapDisplayToValue()    (A)
    //        @group    drawing
    //            Map from a the display value for this item to the internal value.
    //
    //        @param    displayValue    (string)    Value displayed to the user.
    //        @return    (string)    Internal value corresponding to that display value.
    //<
    mapDisplayToValue : function (displayValue) {
        return displayValue;
    },

    //> @method radioItem.setElementReadOnly()
    // Change the read-only state of the form element immediately.
    //<
    setElementReadOnly : function (readOnly) {
        // A radio button should be rendered as disabled to simulate read-only.
        this._setElementEnabled(!readOnly && !this.isDisabled());
    }

});










//>    @class    ResetItem
// Button that resets the form to default values, by calling
// <code>DynamicForm.resetValues()</code>
//
// If you define a click handler on this item, you can return false to cancel the reset.
//
// @visibility external
//<

isc.ClassFactory.defineClass("ResetItem", "ButtonItem");
isc.ResetItem.addProperties({
    // default the title to "Reset"
    title:"Reset"
});

isc.ResetItem.addMethods({
    // override handleClick to reset the form unless a user defined click-handler suppresses
    // the reset.
    handleClick : function () {
        if (this.Super("handleClick", arguments) == false) return false;
        this.form.resetValues();
    }
});





// Class will not work without the ListGrid
if (isc.ListGrid) {





//> @class DateItem
//
// Item for manipulating Dates.
// <p>
// Can be rendered as a text field, or as 3 selects for day, month, year.  Includes optional
// pop-up picker.
//
// @visibility external
// @example dateItem
//<
isc.defineClass("DateItem", "ContainerItem");

isc.DateItem.addClassProperties({
    //> @classAttr DateItem.mapCache (object : {} : IRW)
    // Cache for the map of day, month and year values
    // -- so we don't have to calculate the values over and over.
    // Items are keyed in the map by "<code>day|month|year</code>.<code>start</code>.<code>end</code>".
    //<
    mapCache:{},

    //> @type DateItemSelectorFormat
    // Order of pickers and which pickers are present when using a DateItem with
    // +link{dateItem.useTextField} false.
    DAY_MONTH_YEAR:"DMY", // @value isc.DateItem.DAY_MONTH_YEAR Output fields in day, month, year order.
    MONTH_DAY_YEAR:"MDY", // @value isc.DateItem.MONTH_DAY_YEAR Output fields in month, day, year order.
    YEAR_MONTH_DAY:"YMD", // @value isc.DateItem.YEAR_MONTH_DAY Output fields in year, month, day order.

    DAY_MONTH:"DM",  // @value isc.DateItem.DAY_MONTH Output only day, month fields.
    MONTH_DAY:"MD",  // @value isc.DateItem.MONTH_DAY Output only month, day fields.
    YEAR_MONTH:"YM", // @value isc.DateItem.YEAR_MONTH Output only year, month fields.
    MONTH_YEAR:"MY", // @value isc.DateItem.MONTH_YEAR Output only month, year fields.
    // @visibility external
    //<

    DEFAULT_START_DATE:Date.createLogicalDate(1995, 0, 1),
    DEFAULT_END_DATE:Date.createLogicalDate(2020, 11, 31),
    DEFAULT_CENTURY_THRESHOLD:25,

    chooserWidth:150, // @classAttr isc.DateItem.chooserWidth (number) Width of the date chooser -- used to choose a date graphically.
    chooserHeight:171 // @classAttr isc.DateItem.chooserHeight (number) Height of the date chooser -- used to choose a date graphically.

});

isc.DateItem.addProperties({
    type: "date",

    //> @attr dateItem.textField (AutoChild TextItem : null : R)
    // Text field to hold the entire date in "type in" format, if 'useTextField' is true
    // for an item.
    //
    // @group dateItemAppearance
    // @visibility external
    //<

    // Its documented as an autoChild so Defaults / Properties are implied but
    // explicitly expose the 'properties' block as this is good to have clearly visible
    // for customization of items.
    //> @attr dateItem.textFieldProperties (TextItem properties : null : IRA)
    // Custom properties to apply to this dateItem's generated +link{dateItem.textField}.
    // Only applies if +link{dateItem.useTextField} is true.
    // @group dateItemAppearance
    // @visibility external
    //<

    textFieldDefaults: {
        name:"dateTextField", type:"text", changeOnBlur:true,
        // on keypress run standard 'change' behavior to store the value
        // as this._value - also mark as "dirty"
        // This allows us to preserve partially typed entries across redraws
        // while the item has focus.
        // We clear the dirty flag when we actually update the DateItem's value
        // on blur, or in setValue() if we're changing to a new value.
        changeOnKeypress:true,
        changed : function () {
            this.isDirty = true;
        },

        // Override the blur method to update the DateItem value
        // Using blur rather than saveValue / change allows changeOnKeypress to
        // be set to true without the dateItem clobbering the user's half-typed
        // strings
        blur : function () {
            this.isDirty = false;
            if (this.parentItem) this.parentItem.updateValue();
        },

        shouldSaveValue:false,
        // Pick up textBoxStyle from the DateItem for simplicity of styling the sub-item.

        getTextBoxStyle : function () {
            return this.parentItem ? this.parentItem.getTextBoxStyle() :
                    this.Super("getTextBoxStyle", arguments);
        },
        // Determine our size based on our parents specified textBox size
        getTextBoxWidth : function () {
            if (this.parentItem) return this.parentItem.getTextBoxWidth();
            return this.Super("getTextBoxWidth", arguments);
        },

        _shouldUpdateParentItem: true
    },

    // TextBoxStyle for the text-field textBox if showing

    textBoxStyle:"textItem",
    _sizeTextBoxAsContentBox : function () {
        return isc.Browser.isStrict;
    },


    // default to equals if this.operator is unset, rather than being sensitive to textMatchStyle
    getOperator : function (textMatchStyle) {
        if (!this.operator) return "equals";
        return this.operator;
    },

    //> @attr DateItem.daySelector (AutoChild SelectItem : null : R)
    // Select item to hold the day part of the date.
    // @group dateItemAppearance
    // @visibility external
    //<



    //> @attr dateItem.daySelectorProperties (SelectItem properties : null : IRA)
    // Custom properties to apply to this dateItem's generated +link{dateItem.daySelector}.
    // @group dateItemAppearance
    // @visibility external
    //<

    daySelectorDefaults: {
        name: "daySelector", title:"Day", prompt: "Choose a day", type: "select",
        valueMap: "this.parentItem.getDayOptions()", shouldSaveValue: false,
        // Override saveValue to update the parent.

        saveValue : function () {
            this.Super("saveValue", arguments);
            this.parentItem.updateValue();
        },
        // Don't adjust the selectors for errors (which are shown at the parent-item level)
        getErrorWidth:function () {return 0;},
        width: 45,
        // avoid additional changed events from this sub-item
        suppressItemChanged: true
    },

    //> @attr DateItem.monthSelector (AutoChild SelectItem : null : R)
    // Select item to hold the month part of the date.
    // @group dateItemAppearance
    // @visibility external
    //<

    //> @attr dateItem.monthSelectorProperties (SelectItem properties : null : IRA)
    // Custom properties to apply to this dateItem's generated +link{dateItem.monthSelector}.
    // @group dateItemAppearance
    // @visibility external
    //<

    monthSelectorDefaults: {
        name: "monthSelector", title:"Month", prompt: "Choose a month", type: "select",
        valueMap: "this.parentItem.getMonthOptions()", shouldSaveValue: false,
        saveValue : function () {
            this.Super("saveValue", arguments);
            this.parentItem.updateValue();
        },
        // Don't adjust the selectors for errors (which are shown at the parent-item level)
        getErrorWidth:function () {return 0;},

        width: 55,
        // avoid additional changed events from this sub-item
        suppressItemChanged: true
    },

    //> @attr DateItem.yearSelector (AutoChild SelectItem : null : R)
    // Select item to hold the year part of the date.
    // @group dateItemAppearance
    // @visibility external
    //<

    //> @attr dateItem.yearSelectorProperties (SelectItem properties : null : IRA)
    // Custom properties to apply to this dateItem's generated +link{dateItem.yearSelector}.
    // @group dateItemAppearance
    // @visibility external
    //<
    yearSelectorDefaults: {
        name:"yearSelector", title:"Year", prompt:"Choose a year", type:"select",
        valueMap:"this.parentItem.getYearOptions()", shouldSaveValue:false,
        saveValue:function () {
            this.Super("saveValue", arguments);
            this.parentItem.updateValue();
        },
        // Don't adjust the selectors for errors (which are shown at the parent-item level)
        getErrorWidth:function () {return 0;},
        width:60,
        // avoid additional changed events from this sub-item
        suppressItemChanged: true
    },

    // Default to 150 wide
    // This is an appropriate default if we're showing the text field
    // If we're showing the selectors, this value will be forced to 200 during setItems
    width:150,

    cellPadding:0,

    //> @attr dateItem.useSharedPicker (Boolean : true : [IR])
    // When set to true (the default), use a single shared date-picker across all widgets that
    // use one.  When false, create a new picker using the autoChild system.  See
    // +link{dateItem.pickerDefaults, picker} and
    // +link{dateItem.pickerProperties, pickerProperties} for details on setting up an unshared
    // picker.
    // @visibility external
    //<
    useSharedPicker: true,

    //> @attr dateItem.pickerConstructor (string : "DateChooser" : [IR])
    // SmartClient class for the +link{FormItem.picker} autoChild displayed to allow the user
    // to directly select dates.
    // @visibility external
    //<
    pickerConstructor: "DateChooser",

    //> @attr dateItem.pickerDefaults (DateChooser Properties: see below : [IR])
    // Defaults for the +link{DateChooser} created by this form item.
    //<
    pickerDefaults: {
        width: isc.DateItem.chooserWidth,
        height: isc.DateItem.chooserHeight,
        border:"1px solid black;",
        // show a cancel button that closes the window
        showCancelButton: true,
        autoHide: true,
        closeOnEscapeKeypress: true
    },

    //> @attr dateItem.pickerProperties (DateChooser Properties: see below : [IR])
    // Properties for the +link{DateChooser} created by this form item.
    // @visibility external
    //<

    //> @attr dateItem.useTextField (boolean : null : IRW)
    // Should we show the date in a text field, or as 3 select boxes?
    // @group basics
    // @visibility external
    // @example dateItem
    //<

    //> @attr dateItem.textAlign (Alignment : varies : IRW)
    // If +link{dateItem.useTextField} is <code>true</code> this property governs the alignment
    // of text within the text field. Defaults to <code>"right"</code> by default or
    // <code>"left"</code> if the page is in +link{isc.Page.isRTL(),rtl mode}.
    // @group appearance
    // @visibility external
    //<

    textAlign:isc.Page.isRTL() ? isc.Canvas.LEFT : isc.Canvas.RIGHT,

    //> @attr dateItem.useMask (boolean : null : IA)
    // If +link{dateItem.useTextField} is not <code>false</code> this property determines if
    // an input mask should be used. The format of the mask is determined by the
    // +link{dateItem.inputFormat} or +link{dateItem.dateFormatter} (in that order).
    // @group basics
    // @see dateItem.maskDateSeparator
    // @visibility external
    //<

    //> @attr   dateItem.maskDateSeparator   (string : null : IA)
    // If +link{dateItem.useTextField} and +link{dateItem.useMask} are both <code>true</code>
    // this value is the separator between date components. If unset +link{Date.getDefaultSeparator()}
    // will be used.
    // @group basics
    // @visibility external
    //<
//    maskDateSeparator: "/",

    //> @attr dateItem.enforceDate  (Boolean : false : IRWA)
    // Can this field be set to a non-date value [other than null]?
    // <P>
    // When set to true, +link{formItem.setValue()} will return false without setting the item value
    // and log a warning if passed something other than a valid date value.
    // If the dateItem is showing a +link{dateItem.useTextField,free-form text entry field},
    // and a user enters a text value which cannot be parsed into a valid date, the item will
    // automatically redraw and display the +link{dateItem.invalidDateStringMessage} (though at this
    // point calling +link{formItem.getValue()} will return the string entered by the user).
    // <P>
    // When set to false, a user may enter a value that is not a valid date (for example, "Not
    // applicable") and the value will not immediately be flagged as an error.  However note
    // that for the value to actually pass validation you would need to declare the field as
    // not of "date" type, for example:
    // <pre>
    //     {name:"startDate", type:"dateOrOther", editorType:"DateItem", useTextField:true },
    // </pre>
    // The type "dateOrOther" could be declared as a +link{SimpleType}, with validators that
    // will accept either a valid date or certain special Strings (like "Not Available").
    // <P>
    // Only applies to dateItems where +link{dateItem.useTextField} is true. Non-Date values
    // are never supported in items where useTextField is false.
    //
    // @visibility external
    //<
    // Note: this is very similar to setting validateOnChange, with the exception of actually
    // rejecting setValue() calls with an invalid date.
    enforceDate:false,

    //> @attr dateItem.invalidDateStringMessage (string : "Invalid date" : IRW)
    // Validation error message to display if the user enters an invalid date
    // @visibility external
    // @group i18nMessages
    //<
    invalidDateStringMessage:"Invalid date",

    //> @attr dateItem.showPickerIcon (boolean : true : IRW)
    // Should we show the pick button icon?
    // @visibility pickerIcon
    //<

    showPickerIcon:true,
    // Suppress the picker icon if we don't have the pickerConstructor class loaded.
    // This can occur when the Forms module is loaded without the Grid module.
    _shouldShowPickerIcon : function () {
        if (isc[this.pickerConstructor] == null) {
            this.logWarn("Date Item pickerConstructor class '" + this.pickerConstructor +
                "' is not loaded. This property may have been modified incorrectly " +
                " or a required module may not be loaded. Suppressing the pickerIcon.");
            return false;
        }
        return this.Super("_shouldShowPickerIcon", arguments);
    },


    //> @attr dateItem.pickerIconWidth (number : 20: IRW)
    // Width for the date item's pickerIcon.
    // @visibility pickerIcon
    //<
    pickerIconWidth:20,

    //> @attr dateItem.pickerIconHeight (number : 20 : IRW)
    // Height for the date item's pickerIcon.
    // @visibility pickerIcon
    //<
    pickerIconHeight:20,

    //> @attr dateItem.pickerIconSrc (SCImgURL : "[SKIN]/DynamicForm/DatePicker_icon.gif" : IRW)
    // Src for the picker icon image
    // @visibility pickerIcon
    //<
    pickerIconSrc:"[SKIN]/DynamicForm/DatePicker_icon.gif",

    // give the picker icon 3px of horizontal space by default
    pickerIconHSpace:3,

    //>@attr dateItem.pickerIconPrompt (string : "Show Date Chooser" : IR)
    // Prompt to show when the user hovers the mouse over the picker icon for this DateItem. May
    // be overridden for localization of your application.
    // @visibility external
    // @group i18nMessages
    //<
    pickerIconPrompt : "Show Date Chooser",

    //> @attr dateItem.pickerIconProperties (object : {...} : IRW)
    // Properties for the pickerIcon.
    // @visibility pickerIcon
    //<
    pickerIconProperties:{
    },



    //> @attr dateItem.startDate (Date : 1/1/1995 : IRW)
    // Minimum date the selectors will allow the user to pick.
    // <P>
    // <b>NOTE:</b> by design, setting <code>startDate</code> and <code>endDate</code> will not
    // always prevent the user from picking invalid values.  In particular:
    // <ul>
    // <li> the set of available days will only be restricted if the start and end dates fall
    // within the same month
    // <li> the set of available months will only be restricted if the start and end dates fall
    // within the same year
    // </ul>
    // <P>
    // This is <b>by design</b> as it allows the user to set the day, month and year in
    // whatever order is convenient, rather than forcing them to pick in a specific order.
    // <P>
    // For actual enforcement of a date being in correct range before data is submitted, a
    // +link{Validator} of type "dateRange" should always be declared.
    //
    // @group appearance
    // @visibility external
    //<
    startDate:isc.DateItem.DEFAULT_START_DATE,

    //> @attr dateItem.endDate (Date : 12/31/2020 : IRW)
    // Maximum date the selectors will allow the user to pick.
    // <P>
    // See +link{dateItem.startDate} for details on how this restriction works.
    //
    // @group appearance
    // @visibility external
    //<
    endDate:isc.DateItem.DEFAULT_END_DATE,

    //> @attr dateItem.centuryThreshold (number : 25 : IRW)
    // Only used if we're showing the date in a text field. When parsing a date, if the year
    // is specified with 1 or 2 digits and is less than the centuryThreshold, then the year will
    // be assumed to be 20xx; otherwise it will be interpreted according to default browser
    // behaviour, which will consider it to be 19xx.
    // <P>
    // If you need to allow 1 and 2 digit years, set this attribute to
    // <code>null</code> to have the control retain your year-value as entered.
    // @group appearance
    // @visibility external
    //<
    centuryThreshold:isc.DateItem.DEFAULT_CENTURY_THRESHOLD,

    //> @attr DateItem.use24HourTime (Boolean : true : IRW)
    // When showing the +link{class:DateChooser} and the field is of type "datetime", whether
    // the +link{dateChooser.showTimeItem, time field} should be set to use 24-hour time.  The
    // default is true.
    // <P>
    // Has no effect if +link{showPickerTimeItem} is explicitly set to <code>false</code>.
    // @visibility external
    //<
    use24HourTime: true,

    //> @attr dateItem.showPickerTimeItem (Boolean : true : IRW)
    // If this field is of type <code>"datetime"</code>, when showing the
    // +link{class:DateChooser}, should the +link{dateChooser.showTimeItem,time field} be
    // displayed?
    // <P>
    // Has no effect for fields of type <code>"date"</code>.
    // @visibility external
    //<
    showPickerTimeItem:true

    //> @attr dateItem.pickerTimeItemProperties (TimeItem Properties : null : IRWA)
    // A set of properties to apply to the +link{class:TimeItem} displayed in the picker when
    // +link{showPickerTimeItem} is true.
    // <P>
    // Has no effect for fields of type <code>"date"</code>.
    // @visibility external
    //<

    //> @attr dateItem.dateFormatter (DateDisplayFormat : null : IRW)
    // If +link{dateItem.useTextField} is <code>true</code> this property can be used to
    // customize the format in which dates are displayed for this item.<br>
    // Should be set to a standard +link{type:DateDisplayFormat}.
    // <P>
    // As with any formItem rendering out a date value, if no explicit dateFormatter is
    // supplied, dateFormatter will be derived from +link{DynamicForm.dateFormatter} or
    // +link{DynamicForm.datetimeFormatter},  depending on the specified +link{formItem.type} for
    // this field, if set, otherwise from the standard default +link{Date.setShortDisplayFormat()}
    // or +link{Date.setShortDatetimeDisplayFormat()}.
    // <P>
    // NOTE: For entirely custom formats, developers may apply a custom
    // <var class="smartclient">
    // +link{dateItem.formatEditorValue()} method. To ensure the
    // DateItem is able to parse user-entered date strings back into Dates, for most cases
    // developers can specify an explicit +link{dateItem.inputFormat}, or if necessary a
    // custom +link{dateItem.parseEditorValue()}.
    // </var>
    // <var class="smartgwt">
    // <code>editorValueFormatter</code>. To ensure the
    // DateItem is able to parse user-entered date strings back into Dates, for most cases
    // developers can specify an explicit +link{dateItem.inputFormat}, or if necessary a
    // custom <code>editorValuePaser</code>
    // </var>
    //
    // @visibility external
    //<



    //> @attr dateItem.displayFormat (DateDisplayFormat : null : IRW)
    // If +link{dateItem.useTextField} is <code>true</code> this property can be used to
    // customize the format in which dates are displayed.<br>
    // Should be set to a standard +link{type:DateDisplayFormat} or
    // a function which will return a formatted date string.
    // <P>
    // If unset, the standard shortDate format as set up via +link{Date.setShortDisplayFormat()}
    // will be used.
    // <P>
    // <B>NOTE: you may need to update the +link{DateItem.inputFormat, inputFormat} to ensure the
    // DateItem is able to parse user-entered date strings back into Dates</B>
    // @see dateItem.inputFormat
    // @visibility external
    // @deprecated Use +link{dateItem.dateFormatter} instead.
    //<
    //displayFormat:"toShortDate"

    //> @attr  dateItem.inputFormat  (DateInputFormat : null : IRW)
    // If +link{dateItem.useTextField} is <code>true</code> this property can be used to specify
    // the input format for date strings.
    // If unset, the input format will be determined based on the specified
    // +link{DateItem.dateFormtter} if possible (see +link{DateItem.getInputFormat()}), otherwise
    // picked up from the Date class (see +link{Date.setInputFormat()}).
    // <P>
    // Should be set to a standard +link{type:DateInputFormat}
    // <P>
    // Note that the +link{DateInputFormat} property is sufficient to parse date or datetime
    // strings specified in most standard date formats. However should an entirely custom
    // parsing function be required developers can
    // <var class="smartclient">implement a custom +link{dateItem.parseEditorValue()} method.</var>
    // <var class="smartgwt">apply a custom <code>editorValueParser</code> function.</var>
    //
    // @see dateItem.displayFormat
    // @visibility external
    //<
    //inputFormat:null,

    //> @attr dateItem.selectorFormat (DateItemSelectorFormat : null : IRW)
    // If showing date selectors rather than the date text field (so when
    // <code>this.useTextField</code> is false), this property allows customization of the
    // order of the day, month and year selector fields.  If unset these fields will match the
    // specified inputFormat for this item.
    // <P>
    // Note: selectors may be ommitted entirely by setting selectorFormat to (for example)
    // <code>"MD"</code>. In this case the value for the omitted selector will match the
    // +link{formItem.defaultValue,defaultValue} specified for the item.  For example,
    // if the selector format is "MD" (month and day only), the year comes from the Date
    // specified as the defaultValue.
    //
    // @visibility external
    //<

    //selectorFormat:null

});

isc.DateItem.addMethods({

    init : function () {
        // Set the default value of useTextField if not explicitly defined
        if (this.useTextField == null) this.useTextField = this.useMask || false;

        if (this.selectorFormat) {
            if (!this.selectorFormat.toUpperCase || this.selectorFormat.length == 0) {
                this.selectorFormat = null;
            } else {
                var format = this.selectorFormat.toUpperCase();
                if (!format.match("^[DMY]*$")) {
                    // invalid selectorFormat - this will cause incorrect rendering of select
                    // items when useTextField is false - assume the default of null so that
                    // default getSelectorFormat() bahavior applies
                    this.selectorFormat = null;
                }
            }
        }

        return this.Super("init", arguments);
    },

    // if selectorFormt is unset, back off to standard inputFormat.
    getSelectorFormat : function () {
        if (this.selectorFormat) {
            return this.selectorFormat;
        } else if (this.inputFormat && isc.isA.String(this.inputFormat)) {
            return this.inputFormat;
        } else {
            var inputFormat = Date.getInputFormat();
            if (isc.isA.String(inputFormat)) return inputFormat;
            // Asssume US date format if we can't deduce the desired format from the date input
            // format
            this.logInfo("DateItem selectorFormat unspecified - assuming US format");
            return "MDY"
        }
    },

    getInputFormatMask : function (inputFormat) {

        var separator = this.maskDateSeparator || this._getDefaultDateSeparator();

        var mask;
        // Could use indexOf etc but quicker just to look at the standard set of options
        if (inputFormat == "YMD") {
            mask = [this._yearMask,separator,this._monthMask,separator,this._dayMask];
        } else if (inputFormat == "DMY") {
            mask = [this._dayMask,separator,this._monthMask,separator,this._yearMask];
        } else {
            // assume MDY as last valid format
            mask = [this._monthMask,separator,this._dayMask,separator,this._yearMask];
        }

        // Support DateTimeItem with additional mask
        if (isc.isA.DateTimeItem(this)) {
            mask.addList([" ",this._timeMask]);
        }
        return mask.join("");

    },
    _monthMask:"[01][0-9]",
    _dayMask:"[0-3]#",
    _yearMask:"####",
    _timeMask: "[0-2][0-9]:[0-6][0-9]",

    _maskDisplayFormats:{
        "MDY": "toUSShortDate",
        "DMY": "toEuropeanShortDate",
        "YMD": "toJapanShortDate"
    },

    //> @method dateItem.setItems() (A)
    //
    //  Override the setItems() routine to set the order of the fields according to this.dateFormat
    //<
    _getDefaultDateSeparator:function () {
        return Date.getDefaultDateSeparator();
    },
    _getDefaultDateSeparatorRegex : function () {
        var sep = this._getDefaultDateSeparator();
        return new RegExp(sep, "/g");
    },
    setItems : function (itemList) {

        var DI = isc.DateItem,
            format = this.getSelectorFormat()
        ;

        if (itemList != null && itemList.length != 0) {
            this.logWarn("setItems() called for dateItem with itemList:" + itemList +
                            " - ignoring, and making use of default date fields");
        }

        // create a new itemList
        itemList = this.items = [];

        if (this.useTextField) {
            // Setup properties that are being merged from the date item into the text field
            var mergeProperties = {
                textAlign: this.textAlign,
                emptyDisplayValue: this.emptyDisplayValue,
                operator: this.operator,
                title: this.title
            };
            if (this.showHintInField) {
                mergeProperties.showHintInField = this.showHintInField;
                mergeProperties.hint = this.hint;
                this.hint = null;
            }

            var maskProperties = {};
            if (this.useMask) {
                var inputFormat = this.getInputFormat();
                // Default to US date format
                if (!inputFormat) inputFormat = "MDY";

                var mask = this.getInputFormatMask(inputFormat);

                maskProperties.mask = mask;
                maskProperties.maskSaveLiterals = true;
                maskProperties.maskOverwriteMode = true;

                // Display format must match input so we force it here
                if (this.inputFormat) {

                    this.dateFormatter = this._maskDisplayFormats[inputFormat];
                }
            }

            var textField = isc.addProperties(mergeProperties,
                                              this.textFieldDefaults,
                                              DI.TEXT_FIELD,
                                              this.textFieldProperties,
                                              maskProperties);
            // Ensure noone overrode the name of the dtf!
            textField.name = "dateTextField";
            // If we have a specified height, expand the text box to fill the available space

            if (this.textField) {

                textField.height = this.textField.height;
            } else if (this.height && (!this.textFieldProperties || !this.textFieldProperties.height))
            {
                textField.height = this.getInnerHeight();
            }

            itemList.add(textField);

            //>EditMode for dynamically changing useTextField

            var undef;
            this.daySelector = this.yearSelector = this.monthSelector = undef;
            //<EditMode

        } else {


            // iterate through the characters of the format
            for (var i = 0; i < format.length; i++) {
                var field = format.charAt(i);
                // assigning the selector for that format to the itemList
                var dayField, monthField, yearField,
                    item = null;
                if (field == "D") {
                    var dayField;
                    if (this.daySelectorProperties != null) {
                        dayField = isc.addProperties({}, this.daySelectorDefaults, DI.DAY_SELECTOR, this.daySelectorProperties);
                    } else {
                        dayField = isc.addProperties({}, this.daySelectorDefaults, DI.DAY_SELECTOR);
                    }
                    dayField.name = "daySelector";
                    item = dayField;
                    itemList.add(dayField);
                } else if (field == "M") {
                    var monthField;
                    if (this.monthSelectorProperties != null) {
                        monthField = isc.addProperties({}, this.monthSelectorDefaults, DI.MONTH_SELECTOR, this.monthSelectorProperties);
                    } else {
                        monthField = isc.addProperties({}, this.monthSelectorDefaults, DI.MONTH_SELECTOR);
                    }
                    monthField.name = "monthSelector";
                    item = monthField;
                    itemList.add(monthField);
                } else if (field == "Y") {
                    var yearField;
                    if (this.yearSelectorProperties != null) {
                        yearField = isc.addProperties({}, this.yearSelectorDefaults, DI.YEAR_SELECTOR, this.yearSelectorProperties);
                    } else {
                        yearField = isc.addProperties({}, this.yearSelectorDefaults, DI.YEAR_SELECTOR);
                    }
                    yearField.name = "yearSelector";
                    item = yearField;
                    itemList.add(yearField);
                }
                // Leave a gap between items via left-padding

                if (i > 0 && item) {
                    if (item.cssText == null) {
                        item.cssText = "padding-left:3px;";
                    }
                }
            }
        }

        // call the superclass routine to properly set the items
        this.Super("setItems", [itemList]);


        if (this.useTextField) {
            this.textField = this.dateTextField;
        }
    },

    // override getInnerWidth().
    // If we're showing selectors, explicitly fit to them (ignore any specified size)

    getInnerWidth : function () {

        if (this.useTextField) {
            return this.Super("getInnerWidth", arguments);
        }

        var size = 0,
            selectorCount = 0;
        if (this.daySelector) {
            selectorCount +=1;
            size += this.daySelector.width;
        }
        if (this.monthSelector) {
            selectorCount += 1;
            size += this.monthSelector.width;
        }
        if (this.yearSelector) {
            selectorCount += 1;
            size += this.yearSelector.width;
        }
        if (this.showPickerIcon) size += this.getPickerIconWidth();

        if (selectorCount > 0) size += (selectorCount-1) * this.selectorPadding;

        return size;
    },
    selectorPadding:2,

    // Override isEditable() to indicate that the user can edit this items value directly
    isEditable : function () {
        return true;
    },


    //> @method dateItem.getEnteredValue()
    // Returns the raw text value typed into this items text field if +link{dateItem.useTextField}
    // is true (otherwise returns the result of this.getValue()).
    // @return (String) value the user entered
    // @visibility external
    //<
    getEnteredValue : function () {
        if (this.useTextField && this.textField != null) {
            return this.textField.getEnteredValue();
        }
        return this.getValue();
    },

    //> @method dateItem.setElementReadOnly()
    // Change the read-only state of the form element immediately.
    //<
    setElementReadOnly : function (readOnly) {
        // There is no element to mark read-only but we don't want the date field(s)
        // to be redrawn so we handle this method and do nothing. The text item or
        // select items will be updated individually.
    },

    //> @method dateItem.setValue() (A)
    // Override setValue to set the values for the sub-items of the date.
    //<
    setValue : function (value) {
        this._setValueCalled = true;

        // may still be null if we're working with a text field
        var setToDefault = false;
        if (value == null) {
            var defaultValue = this.getDefaultValue();
            var undef;
            if (defaultValue !== undef) {
                value = defaultValue;
                setToDefault = true;
            }
        }

        var setToExisting = (isc.isA.Date(value) && isc.isA.Date(this._value)
                                    ? (this.useLogicalDates()
                                        ? (Date.compareLogicalDates(value,this._value) == 0)
                                        : (Date.compareDates(value, this._value) == 0)
                                      )
                                    : value == this._value);

        var date, invalidDate;
        // allow null values if useTextField is true and field is blank
        // Note - For consistency it would seem like 'allowEmptyValue' should be supported in
        // some way on DateItems, but we currently don't suport setting null dates on date items
        // showing selectors -
        // not clear how this mechanism would work
        // - once a date was null, presumably all 3 selectors would be showing "".
        // - when the user then chose a value from one selector, would we default the other 2 to
        //   some default?
        // - similarly if the 3 selectors showed a valid date, how would the user set it to an
        //   empty date (one at a time?)
        if (isc.is.emptyString(value)) value = null;
        if (value == null) {
            invalidDate = true;

            date = value;
            // remove any value previously picked from the dateChooser (used for the time portion)
            if (this._lastPickedTime) delete this._lastPickedTime;
        } else {

            date = this.parseDate(value);
            // parseDate returns null if passed something it doesn't understand
            if (date == null) {
                invalidDate = true;
                date = value;
            }
        }
        if (invalidDate) {

            // If setValue() is called with an invalid date:
            // - if we're not showing a text field, essentially no-op, and maintain the current
            //   value - we have no way of displaying a non-date value
            // - if we're showing a text field
            //  - if this.enforceDate is false, just allow the non-date
            //  - if this.enforceDate is true, 2 possibilities:
            //      - the non-date was entered by a user, and setValue() has subsequently been
            //        called due to a redraw - check this._inavlidDate flag for this case, and
            //        silently allow the non-date
            //      - this method was called direclty with a new non-date value. In this case
            //        just log a warning and refuse to set the value.
            var dropDate;
            if (!this.useTextField) {
                dropDate = true;
            // explicitly support 'clearValue()' on a date field with a textItem even if
            // enforceDate is set
            } else if (this.enforceDate && value != null) {
                var textField = this.dateTextField;
                dropDate = !this._invalidDate || !textField || (textField.getValue() != value);
            }

            if (dropDate) {
                //>DEBUG
                this.logInfo("dateItem.setValue(): invalid date passed: '" + value +
                            "'.  Ignoring this value. Non date values are only supported " +
                            " for dateItems where useTextField is true and enforceDate is false.");
                //<DEBUG
                return false;

            }
        }

        // If enforceDate was true, and we're changing from an invalidDate to a valid date,
        // clear errors.
        if (!invalidDate && this._invalidDate) {
            delete this._invalidDate;
            this.clearErrors();
            this.redraw();
        }

        // hang onto the value passed in
        this.saveValue(date, setToDefault);

        // Avoid attempting to parse / correct the dates in response to these setValues calls
        this._suppressUpdates = true;
        if (this.useTextField) {
            if (this.dateTextField) {
                // If the dateTextField is dirty this implies it has focus and the user
                // has entered some characters
                // Unless we're actually setting to a *new* date value, don't wipe out what
                // the user has entered.
                // This is required to ensure that if a redraw occurs
                // (which calls setItemValues(), then falls through to setValue())
                // we don't lose a partially typed entry
                // If it's truly a new value, we can change the typed entry of course.
                if (setToExisting && this.dateTextField.isDirty) {
                    this.dateTextField.setValue(this.dateTextField._value);
                } else {
                    // re-format the date-string entered by the user if necessary
                    var textValue = invalidDate ? date : this.formatDate(date);
                    this.dateTextField.setValue(textValue);
                    delete this.dateTextField.isDirty;
                }
            }

        }
        // set the day, month and year selectors
        if (this.daySelector) this.daySelector.setValue(date.getDate());
        if (this.monthSelector) this.monthSelector.setValue(date.getMonth());
        if (this.yearSelector) this.yearSelector.setValue(date.getFullYear());
        delete this._suppressUpdates;

        return true;
    },


    // if we're doing a direct submit of the DateItem value, convert it to the
    // dbDate format so it can be parsed on the server.
    _setHiddenDataElementValue : function (value) {
        var hde = this._getHiddenDataElement();
        if (hde != null) {
            if (isc.isA.Date(value)) hde.value = value.toDBDate();

            else hde.value = value;
        }
    },

    // Override getCellHeight() to ensure the containing form leaves enough space for this item.

    getCellHeight : function () {
        var cellHeight = this.Super("getCellHeight", arguments);
        if (isc.Browser.isIE && this.useTextField && isc.isA.Number(cellHeight)) cellHeight += 2;
        return cellHeight;
    },


    elementChanged : function () {
        return;
    },

    // override getCriteriaValue and ensure we return a sensible date value
    getCriteriaValue : function () {
        return this.parseDate(this.getValue());
    },

    // Override updateValue to verify that the contents of the element(s) make a valid date.
    updateValue : function () {
        // _suppressUpdates flag set when we're in the process of setting our sub items' values
        // to represent a known, valid date.

        if (this._suppressUpdates) return;

        // We're likely to manipulate the values of the form items as this method runs - avoid
        // re-running updateValue in response to 'saveValue()' on the sub items.
        this._suppressUpdates = true;

        var date;
        if (this.useTextField) {

            // Note: this method is called from "saveValue()" on the sub-items (after saving out
            // their values) so typically the sub item values will be up to date.
            // However this method may also be called externally while the text item is pending
            // an update (from blur [or keypress]).
            // Call updateValue() to ensure the text field value matches the current element
            // value for that field.
            this.dateTextField.updateValue();
            var value = this.dateTextField.getValue(),
                invalidDate;

            var dateString = value;
            if (value == isc.emptyString || value == null) date = null;
            else {
                // This will return a null value if the date string is invalid.
                // If enforceDate is false we allow a dateItem to be set to a non-date value
                // though typically validation would fail for the field if it's data-type was
                // date
                // If enforce date is true, accept this value, but show a validation error

                date = this.parseDate(value);
                if (date == null) {
                    invalidDate = true;

                    // we're going to store the text value even though it's not a valid date
                    date = value;
                } else {

                    // If the date was valid, the format may have slightly changed
                    // (01/01/01 -> 1/1/2001, for example) - if necessary update the text
                    // field here.
                    dateString = this.formatDate(date);
                    if (value != dateString) {
                        // we've set _suppressUpdates, so we won't end up in an infinite loop
                        // from this call
                        this.dateTextField.setValue(dateString);
                    }
                }
            }
            // Date <--> formatted string logic is slightly ambiguous since even if we show
            // the time-component of a date, it is unlikely we show all the way down to "ms".
            // Therefore compare the formatted string in the field with the stored date value
            // (formatted to a string). If they match, treat the value as unchanged.

            if (this._value == date ||
                (isc.isA.Date(this._value) && (this.formatDate(this._value) == dateString)))
            {
                delete this._suppressUpdates;
                return;
            }

            // If enforceDate is true and we're showing an invalid date error, clear it unless
            // we still have an invalid date
            if (this.enforceDate) {
                if (this._invalidDate && !invalidDate) {
                    delete this._invalidDate;
                    this.clearErrors();
                    this.redraw();
                } else if (invalidDate) {
                    this.logWarn("Invalid date string entered in date text field :"+ date);
                    if (!this._invalidDate) {
                        this._invalidDate = true;
                        this.setError(this.invalidDateStringMessage);

                        // We need to redraw to show the error. We don't want the user's entry
                        // to vanish, so we store it under a temp var. which the text field will
                        // display

                        this.redraw();
                    }
                }
            }


        } else {

            // If we're not showing a text field, start with the last remembered date, and update
            // that based on the values in the selector items
            // This actually means we won't change the time value (which of course could not
            // be edited by the user)
            date = (this._value || this.getDefaultValue());
            // copy the date object to allow us to reset to _value if change handler fails
            date = date.duplicate();

            var day, month, year;

            // Store the specified day first, and apply it after setting month/year
            //
            // Note: Before setting month / year, we set the date to 1 so that setting the month
            // will not lead to an invalid date like Feb 30.
            // This avoids the case where
            //  - the selectors are set to Feb 30, and the previous date was Jan 30.
            //  - the date object has 'setMonth()' called, setting the month is set to "Feb",
            //    causing the date to be automatically updated to March 2
            //  - the day is set to 30 (from the date selector), leaving us with a date of
            //    March 30.
            //  At this point the logic to roll the days back to the end of the month would fail
            day = (this.daySelector ? this.daySelector.getValue() : date.getDate());
            date.setDate(1);

            if (this.yearSelector) {
                year = this.yearSelector.getValue()
                date.setFullYear(year);
            }
            if (this.monthSelector) {
                month = this.monthSelector.getValue();
                date.setMonth(month);
            }

            // Now set date to the appropriate "day" value
            // this is the value of the daySelector, or if we're not showing a day selector
            // the previously selected day value
            date.setDate(day);

            // If set to an invalid date, such as Feb 30, or Feb 29th on a non-leap year, the month
            // will have been rolled forward (making it easy to catch such errors)
            // make sure the date's month is the same as that specified in the list
            // if it's not, we should roll back the day selector, and update the date to the
            // appropriate day / month
            if (month != date.getMonth()) {
                // This rolls the date back to the end of the previous month
                day = day - date.getDate();
                if (this.daySelector) this.daySelector.setValue(day);
                date.setMonth(month);
                date.setDate(day);
            }

            if (isc.SimpleType.inheritsFrom(this.type, "datetime")) {
                // if we're showing choosers for day/month/year but the fieldtype is "datetime"
                // then the time portion can only be picked via the dateChooser - when that
                // happens, we store off the _lastPickedTime
                var realValue = this._lastPickedTime ? this._lastPickedTime :
                        isc.isA.Date(this._value) ? this._value : null;
                if (realValue) {
                    var time = isc.Date.getLogicalTimeOnly(realValue);
                    date = isc.Date.combineLogicalDateAndTime(date, time);
                }
            }
        }
        delete this._suppressUpdates;
        // bail if the value hasn't changed
        if (this.compareValues(date, this._value) == true) return false;

        // now fire the default handlers:
        if (this.handleChange(date, this._value) == false) return;

        // In case the change handler modified the date
        date = this._changeValue;

        // save the value
        this.saveValue(date);

        // fire the 'changed' handler
        this.handleChanged(date);

    },

    // Override saveValue(), fired from setValue and updateValue
    // Mark date as logicalDate:true if type is specified as "date"
    // This ensures that a simple JSON serialization of values from a form will treat dateItem
    // values as logical dates.
    saveValue : function (value) {
        if (isc.isA.Date(value) && value.logicalDate == null && value.logicalTime == null) {
            if (this.useLogicalDates()) value.logicalDate = true;
        }
        // Note that since dates are passed around by reference we can just pass the
        // arguments object to super and the change will have been picked up.
        return this.Super("saveValue", arguments);
    },

    //> @method dateItem.resetValue()
    //      Overridden to get the value from the old value stored in the form, rather than
    //      replacing this item's value with the date object
    // @group elements
    //<
    resetValue : function () {
        var oldValue = this.form._oldValues[this.getFieldName()];
        if (isc.isA.Date(oldValue) && isc.isA.Date(this._value))
            oldValue = this._value.setTime(oldValue.getTime());
        this.setValue(oldValue);
    },


    // getItemValue() - method to get the initial value of items when writing out this
    // containerItem's innerHTML.
    // For the Date Item we give our sub items (selects / text item) the correct value when they
    // are initially set up.
    getItemValue : function (item, values) {

        if (isc.isAn.emptyObject(values)) values = null;

        var dateVal = isc.isA.Date(values),
            currDateVal = isc.isA.Date(this._value);

        if (values == this._value ||
            (dateVal && currDateVal && (Date.compareDates(values, this._value) == 0)))
        {
            return item.getValue();
        }

        // If we're rendering out inactiveItemHTML we may be showing a value that doesn't
        // match the value stored by the form item. An example of this is showing
        // inactive editor HTML in grids where alwaysShowEditors is true.
        if (item == this.dateTextField) return dateVal ? this.formatDate(values) : values;
        else if (item == this.daySelector) return dateVal ? values.getDate() : null;
        else if (item == this.monthSelector) return dateVal ? values.getMonth() : null;
        else if (item == this.yearSelector) return dateVal ? values.getFullYear() : null;

    },

    // Override getDisplayValue() to return the short-date formatted value.

    getDisplayValue : function () {
        var dataValue = this.getValue();
        if (!isc.isA.Date(dataValue)) return this.Super("getDisplayValue", arguments);
        if (this.useTextField || !this.items) {
            return this.formatDate(dataValue);
        } else {
            // If we're undrawn the sub items won't yet be populated! Do this now.
            if (!this.isDrawn()) {

                if (this.yearSelector) this.yearSelector.setValue(dataValue.getFullYear());
                if (this.monthSelector) this.monthSelector.setValue(dataValue.getMonth());
                if (this.daySelector) this.daySelector.setValue(dataValue.getDate());
            }
            // This will give us a the contents of each selector separated by a space,
            // for example "Jun 25 2009" for MDY dates
            return this.items.map("getDisplayValue").join(" ");
        }
    },

    //> @method dateItem.getDefaultValue() (A)
    //  Override getDefaultValue to guarantee that it returns a date if
    //  <code>item.enforceDate</code> is true. If no default date is supplied, defaults to the
    //  current date.
    //<
    // Note: As currently written this method will not consistently return the same date instance
    // unless this.defaultValue is explicitly specifed as a date object. Instead we create a
    // new date instance each time the method is called and return that.
    // This can be a gotcha - for exmaple when checking for changes to a date item we have to
    // use compareDates() rather than ==.
    getDefaultValue : function () {
        var value = this.Super("getDefaultValue");
        if (!isc.isA.Date(value)) {
            var dateValue = this.parseDate(value);
            if (isc.isA.Date(dateValue)) value = dateValue;
            else if (!this.useTextField || this.enforceDate) {
                var replaceDefaultValue;
                if (value != null) {
                    this.logWarn("Default DateItem value provided as:" + value +
                             ". This is not recognized as a valid date - defaulting to a new date");
                    // if this came from a static default value, replace it so we don't see
                    // multiple warnings
                    replaceDefaultValue = this.defaultValue == value;
                }

                // if we still don't have a valid date, default to a new Date().
                // NOTE: can't just set the defaultValue to "new Date()" as this object would then
                // be shared amongst all date instances
                // Exception: We DO support null value for dateItems where useTextField is true
                // even if enforceDate is set.
                if (!this.useTextField) value = this._getEmptyDate();

                if (replaceDefaultValue) this.defaultValue = value;
            }
        }
        return value;
    },

    _getEmptyDate : function () {
        var value = Date.createLogicalDate();
        return value;
    },

    // useLogicalDates(): does this item produce "logical date" values, or datetime values?
    // If this item has type explicitly specified as "date", we work with "logical date" objects.
    // These are native dates where the time component is basically opaque and is set such that
    // the date is always correct in browser native local time.
    // We never apply developer-specified custom timezones to logical dates.
    // If this item's type is unspecified, or the item is specified as type "datetime", this item
    // will produce datetime values (values where the time is meaningful and will be formatted
    // according to the timezone offset specified by the developer, if there is one).
    // See Date.createLogicalDate for more on this

    useLogicalDates : function () {
        var type = this.getType(),
            isDateField = isc.SimpleType.inheritsFrom(type, "date"),
            isDatetimeField = isc.SimpleType.inheritsFrom(type, "datetime");
        return type != null && isDateField && !isDatetimeField
    },

    //> @method dateItem.getStartDate() (A)
    // use this method, rather than referring to this.startDate, to guarantee that it
    //      returns a date
    //      Note - Does not update this.startDate - should it?
    //<
    getStartDate : function () {
        var startDate = this.startDate;
        if (isc.isA.String(startDate)) startDate = this.parseDate(this.startDate);
        if(!isc.isA.Date(startDate)) {
            //>DEBUG
            this.logWarn("startDate was not in valid date format - using default start date");
            //<DEBUG
            startDate = isc.DateItem.DEFAULT_START_DATE;
        }
        return startDate;
    },

    //> @method dateItem.getEndDate() (A)
    // use this method, rather than referring to this.endDate, to guarantee that it
    //      returns a date
    //<
    getEndDate : function () {
        var endDate = this.endDate;
        if (isc.isA.String(endDate)) endDate = this.parseDate(this.endDate);
        if(!isc.isA.Date(endDate)) {
            //>DEBUG
            this.logWarn("endDate was not in valid date format - using default end date");
            //<DEBUG
            endDate = isc.DateItem.DEFAULT_END_DATE;
        }
        return endDate;
    },


    _canFocus : function () {
        if (this.canFocus != null) return this.canFocus;
        return true;
    },

    // Override focusInItem to focus in the appropriate sub-item
    focusInItem : function () {
        if (!this.isVisible()) return;

        if (this.useTextField) {
            if (this.dateTextField) this.dateTextField.focusInItem();
        } else {
            var format = this.getSelectorFormat(),

                // Format will be "DMY" / "YMD" / "MDY" / etc.
                // (Parse the string rather than comparing with the DateItem.DAY_MONTH_YEAR class
                // constants - it's slower but will support the user specifying just "MY" or something)
                firstSelector = format.charAt(0)
            ;

            if (firstSelector == "D" && this.daySelector) this.daySelector.focusInItem();
            if (firstSelector == "M" && this.monthSelector) this.monthSelector.focusInItem();
            if (firstSelector == "Y" && this.yearSelector) this.yearSelector.focusInItem();
        }
        // If it couldn't find the appropriate sub-item, this method is a no-op
    },

    // override get/setSelectionRange - if we're showing a text field, call through to the
    // methods on that sub-item

    //> @method dateItem.setSelectionRange()
    // If +link{dateItem.useTextField} is true, falls through to standard
    // +link{formItem.setSelectionRange()} implementation on this items freeform text entry field.
    // Otherwise has no effect.
    // @param start (int) character index for start of new selection
    // @param end (int) character index for end of new selection
    // @visibility external
    //<
    setSelectionRange : function (start,end) {
        if (this.dateTextField) return this.dateTextField.setSelectionRange(start,end);
    },

    //> @method dateItem.getSelectionRange()
    // If +link{dateItem.useTextField} is true, falls through to standard
    // +link{formItem.getSelectionRange()} implementation on this items freeform text entry field.
    // Otherwise has no effect.
    // @return (array) 2 element array indicating start/end character index of current selection
    //  within our text entry field. Returns null if this item is undrawn or doesn't have focus.
    // @visibility external
    //<
    getSelectionRange : function () {
        if (this.dateTextField) return this.dateTextField.getSelectionRange();
    },

    //> @method dateItem.selectValue()
    // If +link{dateItem.useTextField} is true, falls through to standard
    // +link{formItem.selectValue()} implementation on this items freeform text entry field.
    // Otherwise has no effect.
    // @visibility external
    //<
    selectValue : function () {
        if (this.dateTextField) return this.dateTextField.selectValue();
    },

    //> @method dateItem.deselectValue()
    // If +link{dateItem.useTextField} is true, falls through to standard
    // +link{formItem.deselectValue()} implementation on this items freeform text entry field.
    // Otherwise has no effect.
    // @param [start] (Boolean) If this parameter is passed, new cursor insertion position will be
    //   moved to the start, rather than the end of this item's value.
    // @visibility external
    //<
    deselectValue : function (start) {
        if (this.dateTextField) return this.dateTextField.deselectValue()
    },

    //> @method dateItem.getDayOptions() (A)
    // Return the list of options for the day selector.
    //
    // @return (array) Array of day numbers from 1-31;
    //<
    getDayOptions : function () {

        var startDate = this.getStartDate(),
            endDate = this.getEndDate();

        // If the date range spans more than one month, return [1 - 31]
        // Only time we want to have this return a range smaller than 1-31 is if we have a range
        // within a single month (Feb 2 - 20th, 1945), for example.  Otherwise we force the
        // user to pick fields in a specific order.
        var startDay = 1,
            endDay = 31;

        // If it's within a single month in a year, return appropriate subset of days
        if (startDate.getYear() == endDate.getYear() &&
            startDate.getMonth() == endDate.getMonth())
        {
            startDay = startDate.getDate()
            endDay = endDate.getDate()
        }

        // if the list of options is already in the mapCache, just pull it from there
        var key = "day." + startDay + "." + endDay;
        if (isc.DateItem.mapCache[key]) return isc.DateItem.mapCache[key];

        // otherwise build the options and store it in the dayMapCache
        var options = isc.DateItem.mapCache[key] = [];
        for (var i = startDay; i <= endDay; i++) options[i - startDay] = i;

        return options;
    },

    //> @method dateItem.getMonthOptions() (A)
    // Return the list of options for the month selector.
    //
    // @return (array) Object of month number (0-based!) to short month name ["Jan","Feb",...]
    //<
    getMonthOptions : function () {

        var startDate = this.getStartDate(),
            endDate = this.getEndDate();

        // If the date range spans more than one year, return ["Jan" - "December"]
        // Only time we want to have this return an incomplete range is if we have a range
        // within a single year (Feb - April, 1945), for example.  Otherwise we force the user
        // to pick fields in a specific order.
        var startMonth = 0,
            endMonth = 11;

        // If it's within a single month in a year, return appropriate subset of days
        if (startDate.getYear() == endDate.getYear()) {
            startMonth = startDate.getMonth()
            endMonth = endDate.getMonth()
        }

        // if the list of options is already in the mapCache, just pull it from there
        var key = "month." + startMonth + "." + endMonth;
        if (isc.DateItem.mapCache[key]) return isc.DateItem.mapCache[key];

        // otherwise build the options and store it in the dayMapCache
        var options = isc.DateItem.mapCache[key] = {};

        // get the list of names as an array
        var monthNames = Date.getShortMonthNames();
        // and convert it to an object
        for (; startMonth <= endMonth; startMonth++) {
            options[startMonth] = monthNames[startMonth];
        }

        return options;
    },

    //> @method dateItem.getYearOptions() (A)
    // Return the list of options for the year selector.
    //
    // @return (array) Array of day numbers from this.startYear - this.endYear;
    //<
    getYearOptions : function () {

        var startYear = this.getStartDate().getFullYear(),
            endYear = this.getEndDate().getFullYear();

        // if the list of options is already in the mapCache, just pull it from there
        var key = "year." + startYear + "." + endYear;
        if (isc.DateItem.mapCache[key]) return isc.DateItem.mapCache[key];

        // otherwise build the options and store it in the dayMapCache
        var options = isc.DateItem.mapCache[key] = [];
        for (var i = startYear; i <= endYear; i++) {
            options[i-startYear] = i;
        }
        return options;
    },

    //> @method dateItem.parseDate()
    // Parse a date passed in as a string.
    // @group elements
    //
    // @param dateString (string) date value as a string
    // @param inputFormat (DateInputFormat) format for date strings to be parsed
    //
    // @return (date) date value
    //<
    parseDate : function (dateString, inputFormat) {

        if (isc.isA.Date(dateString)) return dateString;

        // If there is a custom editValue parser applied to this item, bypass standard
        // Date parsing logic entirely
        if (this.parseEditorValue != null) {
            var dateVal = this.parseEditorValue(dateString, this.form, this);
            return dateVal;
        }

        if (inputFormat == null) inputFormat = this.getInputFormat();

        var isLogicalDate = this.useLogicalDates();

        var date = Date.parseInput(dateString, inputFormat,
                                this.centuryThreshold, true, !isLogicalDate);
        return date;
    },

    // formatDate() - given a live date object, returns the formatted date string to display
    // Only applies if useTextField is true.
    formatDate : function (date) {
        if (this.formatEditorValue != null) {
            var record = this.form ? this.form.values : {};
            return this.formatEditorValue(date, record, this.form, this);
        }
        if (!isc.isA.Date(date)) return date;

        var formatter = this._getDateFormatter(),
            type = this.getType(),
            isDateField = isc.SimpleType.inheritsFrom(type, "date"),
            isDatetimeField = isc.SimpleType.inheritsFrom(type, "datetime");
        // this.showTime - undocumented flag to use 'toShortDatetime' rather than 'toShortDate'
        // when formatting the date. Used by the DateTimeItem subclass.

        return this.showTime ? date.toShortDatetime(formatter, isDatetimeField || !isDateField)
                             : date.toShortDate(formatter, isDatetimeField || !isDateField);
    },

    //>@method dateItem.parseEditorValue() (A)
    // Convert a text value entered in this item's text field to a final data value
    // for storage.
    // <P>
    // If +link{dateItem.useTextField} is true, entirely custom date formatting and
    // parsing logic may be applied via overrides to +link{parseEditorValue} and
    // +link{formatEditorValue}. These methods apply to this FormItem only -
    // system-wide Date and Datetime formatting and parsing may also be customized via
    // the APIs on the +link{Date} class. See +link{group:dateFormatAndStorage} for more
    // on this.
    // <P>
    // Note: custom parsing for this item may also be achieved by modifying the
    // +link{dateItem.inputFormat}. This mechanism provides support many common date formats
    // without the need for an entirely custom parser function.
    //
    // @param value (string) value as entered by the user
    // @param form (DynamicForm) pointer to the dynamicForm containing this item
    // @param item (FormItem) pointer to this item
    // @return (any) Data value to store for this item.
    // @visibility external
    //<

    //>@method dateItem.formatEditorValue() (A)
    // Convert this item's data value to a text value for display in this item's
    // text field.
    // <P>
    // If +link{dateItem.useTextField} is true, entirely custom date formatting and
    // parsing logic may be applied via overrides to +link{parseEditorValue} and
    // +link{formatEditorValue}. These methods apply to this FormItem only -
    // system-wide Date and Datetime formatting and parsing may also be customized via
    // the APIs on the +link{Date} class. See +link{group:dateFormatAndStorage} for more
    // on this.
    // <P>
    // Note: custom formatting for this item may also be achieved via the
    // +link{dateItem.dateFormatter} which allows you to directly specify various standard
    // date display formats.
    // @param value (any) Underlying data value to format. May be null.
    // @param record (ListGridRecord) The record currently being edited by this form.
    //      Essentially the form's current values object.
    // @param form (DynamicForm) pointer to the DynamicForm
    // @param item (FormItem) pointer to the FormItem
    // @return (string) display value to show in the editor.
    // @visibility external
    //<

    //>@method dateItem.getInputFormat() (A)
    // If +link{dateItem.useTextField} is <code>true</code> this method returns a
    // standard +link{type:DateInputFormat}, determining how values entered
    // by the user are to be converted to Javascript Date objects.
    // <P>
    // If an explicit +link{DateItem.inputFormat} has been specified it will be returned,
    // otherwise, the input format will be automatically derived from the
    // +link{dateItem.dateFormatter} property.
    // <P>
    // Note that the inputFormat will ignore any separator characters and padding of values.
    // However if necessary entirely custom date formatting and parsing may be achieved via the
    // <var class="smartclient">+link{dateItem.formatEditorValue()} and
    // +link{dateItem.parseEditorValue()} methods.</var>
    // <var class="smartgwt"><code>setEditorValueFormatter()</code> and
    // <code>setEditorValueParser()</code> APIs.</var>
    //
    // @return (DateInputFormat) expected format of date strings to parse
    // @visibility external
    //<
    getInputFormat : function () {
        // developer may explicitly specify an inputFormat (this used to be the only way to change
        // input/display format for text-based date items)
        if (this.inputFormat) return this.inputFormat;

        // If a display format, but no inputFormat is specified attempt to derive the inputFormat
        // from the displayFormat. This works for the standard shortDate display formatters but
        // you'll still need to specify an explicit input format for anything more exotic
        var dateFormatter = this._getDateFormatter();
        return Date.mapDisplayFormatToInputFormat(dateFormatter);
    },

    // Methods effecting the dateChooser

    getPickerIcon : function (a,b,c,d) {
        var icon = this.invokeSuper(isc.DateItem, "getPickerIcon", a,b,c,d);
        if (icon.prompt == null) icon.prompt = this.pickerIconPrompt;
        return icon;
    },

    //> @method DateItem.getFiscalCalendar()
    // Returns the +link{FiscalCalendar} object that will be used by this item's DateChooser.
    //
    // @return (FiscalCalendar) the fiscal calendar for this chooser, if set, or the global
    //            one otherwise
    // @visibility external
    //<
    getFiscalCalendar : function () {
        return this.fiscalCalendar || Date.getFiscalCalendar();
    },

    //> @method DateItem.setFiscalCalendar()
    // Sets the +link{FiscalCalendar} object that will be used by this item's DateChooser.  If
    // unset, the _link{Date.getFiscalCalendar, global fiscal calendar} is used.
    //
    // @param [fiscalCalendar] (FiscalCalendar) the fiscal calendar for this chooser, if set, or the global
    //            one otherwise
    // @visibility external
    //<
    setFiscalCalendar : function (fiscalCalendar) {
        this.fiscalCalendar = fiscalCalendar;
    },

    // override 'showPicker' - instead of creating a picker instance we're reusing a shared
    // one.
    showPicker : function () {
        if (isc[this.pickerConstructor] == null) {
            this.logWarn("Date Item pickerConstructor class '" + this.pickerConstructor +
                "' is not loaded. This property may have been modified incorrectly " +
                " or a required module may not be loaded. Refusing to show picker.");
            return;
        }

        if (!this.form._setValuesPending) this.updateValue();

        if (!this.picker) {
            if (this.useSharedPicker) {
                var props = {
                    fiscalCalendar: this.getFiscalCalendar(),
                    showFiscalYearChooser: this.showChooserFiscalYearPicker,
                    showWeekChooser: this.showChooserWeekPicker
                };
                this.picker = isc[this.pickerConstructor].getSharedDateChooser(props);
            } else {
                this.picker = isc[this.pickerConstructor].create(
                    isc.addProperties({}, this.pickerDefaults, this.pickerProperties,
                        {
                            border: "none",
                            _generated:true,
                            // When re-using a DateChooser, we're almost certainly displaying it as a
                            // floating picker rather than an inline element. Apply the common options for
                            // a floating picker
                            autoHide:true,
                            showCancelButton:true,
                            closeOnEscapeKeypress: true
                        }
                    )
                );
            }
        }

        var picker = this.picker;

        var oldItem = picker.callingFormItem;
        if (oldItem != this) {
            if (oldItem) oldItem.ignore(picker, "dataChanged");
            this.observe(picker, "dataChanged", "observer.pickerDataChanged(observed)");

            picker.callingFormItem = this;
            picker.callingForm = this.form;

            picker.locatorParent = this.form;
        }

        picker.startYear = this.getStartDate().getFullYear();
        picker.endYear = this.getEndDate().getFullYear();

        // set the year and week attributes
        picker.fiscalCalendar = this.getFiscalCalendar();
        picker.showFiscalYearChooser = this.showChooserFiscalYearPicker;
        picker.showWeekChooser = this.showChooserWeekPicker;

        // show a TimeItem in the picker if type is datetime
        var showTimeItem = this.shouldShowPickerTimeItem();

        picker.showTimeItem = showTimeItem;
        picker.use24HourTime = this.use24HourTime;
        if (this.pickerTimeItemProperties) picker.timeItemProperties = this.pickerTimeItemProperties;
        if (picker.setData) picker.setData(this.getValue() || new Date());
        if (picker.updateUI) picker.updateUI();

        return this.Super("showPicker", arguments);
    },

    shouldShowPickerTimeItem : function () {
        if (this.showPickerTimeItem == false) return false;
        return isc.SimpleType.inheritsFrom(this.type, "datetime");
    },

    // custom code to center the picker over the picker icon
    getPickerRect : function () {
        // we want the date chooser to float centered over the picker icon.
        var left = this.getPageLeft(),
            top = this.getPageTop(),

            chooserWidth = isc.DateItem.chooserWidth + 3,
            chooserHeight = isc.DateItem.chooserHeight + 3;

        left += Math.round((this.getVisibleWidth() - (this.getPickerIconWidth() /2)) -
                (chooserWidth/2));

        top += Math.round((this.getPickerIconHeight() / 2) - (chooserHeight/2));

        // NOTE: don't return chooserWidth/Height as part of the rect, which would cause the
        // picker to actually be resized to those dimensions, and they may match the natural
        // size at which the chooser draws given skinning properties.
        return [left, top];
    },


    //> @method dateItem.pickerDataChanged()
    // Store the date passed in, and fire the change handler for this item.
    // Called when the user selects a date from the date-chooser window.
    // @visibility internal
    //<
    pickerDataChanged : function (picker) {

        var pickerDate = picker.getData(),
            dateOnly = isc.Date.getLogicalDateOnly(pickerDate),
            year = dateOnly.getFullYear(),
            month = dateOnly.getMonth(),
            day = dateOnly.getDate(),
            timestamp = pickerDate.getTime()
        ;

        // The date-picker creates "logical dates" - dates where day/month/year are set
        // correctly in browser native local time and the time component is an arbitrary value.
        // If this field is being used for a "datetime" value we want to set the time portion
        // to zero in the display timezone so that it shows up as eg 06/16/2011 00:00
        // rather than some arbitrary time.
        var isLogicalDate = this.useLogicalDates();
        if (!isLogicalDate) {
            //this.setToZeroTime(date);
        }

        // avoid firing 'updateValue' while setting the values of sub items
        this._suppressUpdates = true;

        if (this.useTextField) {
            var formatted = this.formatDate(pickerDate);
            this.dateTextField.setValue(formatted);
        } else {
            var date = this._value || this.getDefaultValue(),
                hiddenSelector;
            if (this.yearSelector) this.yearSelector.setValue(year);
            else {
                date.setFullYear(year);
                hiddenSelector = true;
            }
            if (this.monthSelector) this.monthSelector.setValue(month);
            else {
                date.setMonth(month-1);
                hiddenSelector = true;
            }
            if (this.daySelector) this.daySelector.setValue(day);
            else {
                date.setDate(day);
                hiddenSelector = true;
            }

            if (isc.SimpleType.inheritsFrom(this.type, "datetime")) {
                var time = isc.Date.getLogicalTimeOnly(pickerDate);
                this._lastPickedTime = time;
            }

            // if this._value was unset before this method fired, set it now
            // This will be duplicated as part of update value and the selector values overlayed
            if (hiddenSelector) {
                this._value = date;
            }
        }
        this._suppressUpdates = false;

        // Explicitly call 'updateValue' to save the new date (handles firing change
        // handlers, etc. too)
        this.updateValue();

        // Ensure we have focus

        if (!this.hasFocus) this.focusInItem();

        // if we were already in a bad state, revalidate now
        var errors = this.getErrors();
        if (errors && errors.length > 0) this.validate();

        // if validateOnEditorExit is true we can get confused by the fact that we updated
        // the value and then shifted focus into the item and so assume there was no change and
        // not re-validate on exit.
        // Set the special flag to explicitly force a re-validation on editor exit

        if (this.validateOnExit || (this.form && this.form.validateOnExit)) {
            this._forceValidateOnExit = true;
        }

    },

    setHint : function (hintText) {
        if (this.useTextField && this.showHintInField) {
            this.dateTextField.setHint(hintText);
        } else {
            this.Super("setHint", arguments);
        }
    },

    // Override getPickerData() -- add support for providing a default picker date separate
    // from the default date for the item as a whole
    getPickerData : function () {
        var date = this.getValue();
        if (date != null) {
            // wangle the value into a date if possible
            if (!isc.isA.Date(date)) {
                date = new Date(date);
            }
            // Note that a dateItem can return an arbitrary string - in this case new Date(...)
            // will give us a Date object but with no meaningful time etc data.
            if (isc.isA.Date(date) && !isNaN(date.getTime())) return date;
        }
        return this.getDefaultChooserDate();
    },

    //> @attr DateItem.defaultChooserDate (Date : null : IRW)
    // Default date to show in the date chooser. If this items value is currently unset,
    // this property may be specified to set a default date to highlight in the dateChooser
    // for this item. If unset, the date chooser will highlight the current date by default.
    // Note that this has no effect if the item as a whole currently has a value - in that
    // case the date chooser will always highlight the current value for the item.
    // @visibility external
    //<
    //defaultChooserDate:null,

    //> @method DateItem.getDefaultChooserDate()
    // Returns the default date to display in the date chooser if this form items value is
    // currently unset.
    // <P>
    // Default implementation returns +link{dateItem.defaultChooserDate}
    // @return (Date) date to display, or null, indicating the current system date should be
    //   displayed.
    // @visibility external
    //<
    getDefaultChooserDate : function () {
        return this.defaultChooserDate;
    },

    _shouldAllowExpressions : function () {
        if (this.useTextField) {
            return this.Super("_shouldAllowExpressions", arguments);
        } else {
            return false;
        }
    },

    //> @attr DateItem.showChooserFiscalYearPicker (Boolean : false : IRW)
    // When set to true, show a button that allows the calendar to be navigated by fiscal year.
    // @visibility external
    //<
    showChooserFiscalYearPicker: false,
    //> @attr DateItem.showChooserWeekPicker (Boolean : false : IRW)
    // When set to true, show a button that allows the calendar to be navigated by week or
    // fiscal week, depending on the value of +link{showChooserFiscalYearPicker}.
    // @visibility external
    //<
    showChooserWeekPicker: false

    //>EditMode dynamically changing useTextField
    ,
    propertyChanged : function (propertyName) {
        if (propertyName == "useTextField" || propertyName == "useMask") {
            this.setItems();
        }
    }
    //<EditMode
});

}




// Class will not work without the ListGrid
if (isc.ListGrid) {





//>    @class    DateTimeItem
//
// Subclass of DateItem for manipulating +link{type:FieldType,datetimes}.
//
// @visibility external
//<
// Note: This edits 'datetime' type fields, not 'dateTime' type fields, we should possibly rename
// to DatetimeItem.
isc.defineClass("DateTimeItem", "DateItem");


isc.DateTimeItem.addProperties({
    //>    @attr dateTimeItem.useTextField   (Boolean : true : IRW)
    // DateTimeItems show datetime values in a freeform text entry area.
    // @group basics
    // @visibility external
    //<
    useTextField:true,

    //>    @attr    dateTimeItem.displayFormat  (DateDisplayFormat : null : IRW)
    // This property can be used to customize the format in which datetimes are displayed.<br>
    // Should be set to a standard +link{type:DateDisplayFormat} or
    // a function which will return a formatted date time string.
    // <P>
    // If unset, the standard shortDateTime format as set up in
    // +link{Date.setShortDatetimeDisplayFormat()} will be used.
    // <P>
    // <B>NOTE: you may need to update the +link{DateTimeItem.inputFormat, inputFormat}
    // to ensure the DateItem is able to parse user-entered date strings back into Dates</B>
    // @see dateTimeItem.inputFormat
    // @visibility external
    //<

    // set the undocumented showTime flag so we use 'toShortDatetime' rather than 'toShortDate'
    // when formatting our dates by default. Can be overridden via a custom formatter of course.
    showTime:true,

    // set the default data type to "datetime", otherwise the time portion can't be edited in
    // a non-databound item (it reverts to noon, our logicalDate marker) on loss of focus
    type: "datetime",

    //> @attr  dateTimeItem.inputFormat  (DateInputFormat : null : IRW)
    // @include dateItem.inputFormat
    // @visibility external
    //<

    // override shouldShowPickerTimeItem to force the picker to show the time even
    // if "type" isn't explicitly set to "datetime"
    shouldShowPickerTimeItem : function () {
        return this.showPickerTimeItem;
    }

});


} // end of if (isc.ListGrid)








//>    @class    SpacerItem
// A SpacerItem takes up a single cell in the FormLayout, of arbitrary size.
// @visibility external
//<
isc.ClassFactory.defineClass("SpacerItem", "FormItem");
isc.SpacerItem.addProperties({
    // avoid attempting to save this item in the form's values array
    shouldSaveValue:false,

    //>    @attr    spacerItem.showTitle        (Boolean : false : IRW)
    //            we never show a separate title cell for spacers
    //        @group    appearance
    // @visibility external
    //<
    showTitle:false,

    //>    @attr    spacerItem.width                (number : 20 : IRW)
    //            default width for the spacer
    //        @group    appearance
    // @visibility external
    //<
    width:20,

    //>    @attr    spacerItem.height            (number : 20 : IRW)
    //            default height for the spacer
    //        @group    appearance
    // @visibility external
    //<
    height:20,

    showHint:false,     // Don't show a hint for this item
    showIcons:false     // even if a user has defined icons for this item, suppress them
});
isc.SpacerItem.addMethods({

    // Override isEditable as this is non editable
    isEditable : function () {
        return false;
    },

    //>    @method    spacerItem.getElementHTML()    (A)
    //            output the HTML for this element
    //        @group    drawing
    //
    //        @param    value    (string)    Value of the element [Unused because it is more reliably set by setValue].
    //        @return    (HTML)    HTML output for this element
    //<
    getElementHTML : function (value) {
        return isc.Canvas.spacerHTML(this.width, this.height);
    },
    //>    @method    spacerItem.shouldShowTitle()    (A)
    //      Override formItem.shouldShowTitle to return false - we don't want
    //      to draw a cell for the item title
    //        @group    drawing
    //
    //        @return    (HTML)    title for the formItem
    //<
    shouldShowTitle : function () {
        return false;
    }
});








//>    @class    RowSpacerItem
// Form item that renders as a blank row in the form layout.<br>
// Set +link{rowSpacerItem.startRow} to <code>false</code> to create a rowSpacer that simply
// takes up every remaining column in the current row rather than starting a new row.
// @visibility external
//<
isc.ClassFactory.defineClass("RowSpacerItem", "SpacerItem");
isc.RowSpacerItem.addProperties({
    //>    @attr    rowSpacerItem.showTitle        (Boolean : false : IRW)
    //            we never show a separate title cell for separators
    //        @group    appearance
    // @visibility external
    //<
    showTitle:false,

    //>    @attr    rowSpacerItem.colSpan                (number : "*" : IRW)
    //            by default, separators span all remaining columns
    //        @group    appearance
    // @visibility external
    //<
    colSpan:"*",

    //>    @attr    rowSpacerItem.startRow        (Boolean : true : IRW)
    //            these items are in a row by themselves by default
    //        @group    appearance
    // @visibility external
    //<
    startRow:true,

    //>    @attr    rowSpacerItem.endRow            (Boolean : true : IRW)
    //            these items are in a row by themselves by default
    //        @group    appearance
    // @visibility external
    //<
    endRow:true,

    //>    @attr    rowSpacerItem.width                (number : 20 : IRW)
    //            default width for the separator
    //        @group    appearance
    //<
    width:20,

    //>    @attr    rowSpacerItem.height            (number : 20 : IRW)
    //            default height for the separator
    //        @group    appearance
    //<
    height:20
});









//>    @class    SubmitItem
// Button that saves the data in the form, by calling +link{DynamicForm.submit()} when clicked.
// +link{DynamicForm.submit()} for details on how to control what happens when a form is
// submitted.
//
// @see group:operations
//
// @visibility external
//<
isc.ClassFactory.defineClass("SubmitItem", "ButtonItem");
isc.SubmitItem.addProperties({
    //>@attr    SubmitItem.title    (String : "Submit" : IRW)
    // SubmitItems show a title of <code>"Submit"</code> by default. May be overridden.
    // @visibility external
    //<
    title:"Submit"
});

isc.SubmitItem.addMethods({

    handleClick : function () {
        if (this.Super("handleClick", arguments) == false) return false;
        // note that submit() is implemented to perform a native submission (via submitForm())
        // iff form.canSubmit is true - otherwise it falls through to dataBoundComponent
        // 'saveData()' method
        this.form.submit();
        this.form.completeEditing();
    }
});









//>    @class    CancelItem
// Button that cancels any changes in the form, by calling +link{DynamicForm.cancelEditing()}
// when clicked.
// See +link{DynamicForm.cancelEditing()} for details on what happens when a form editing is
// cancelled.
//
// @visibility external
//<
isc.ClassFactory.defineClass("CancelItem", "ButtonItem");
isc.CancelItem.addProperties({
    //>@attr    CancelItem.title    (String : "Cancel" : IRW)
    // CancelItems show a title of <code>"Cancel"</code> by default. May be overridden.
    // @visibility external
    //<
    title:"Cancel"
});

isc.CancelItem.addMethods({

    handleClick : function () {
        if (this.Super("handleClick", arguments) == false) return false;
        this.form.cancelEditing();
    }
});









//>    @class    TextAreaItem
//
//    Class for editable multi-line text areas (uses HTML <code>&lt;TEXTAREA&gt;</code> object)
// @visibility external
// @example textAreaItem
//<
isc.ClassFactory.defineClass("TextAreaItem", "FormItem");

isc.TextAreaItem.addClassProperties({


    //>    @type    TEXTAREA_WRAP
    //    @value    isc.TextAreaItem.OFF  don't allow wrapping at all
    OFF : "OFF",

    //    @value    isc.TextAreaItem.SOFT   when the entered text reaches the edge of the text area,
    //                                  wrap visibly but don't include line breaks in the textarea
    //                                  value
    SOFT : "SOFT",
    VIRTUAL : "SOFT",

    //    @value    isc.TextAreaItem.HARD   when the entered text reaches the edge of the text area,
    //                                  insert a line break
    ON : "HARD",
    HARD : "HARD",
    PHYSICAL : "HARD"
    //
    // @visibility external
    //<
});

isc.TextAreaItem.addProperties({
    //>    @attr    textAreaItem.wrap        (TEXTAREA_WRAP : isc.TextAreaItem.VIRTUAL : IRW)
    // Text wrapping style.
    //        @group    appearance
    // @visibility external
    //<
    wrap:isc.TextAreaItem.VIRTUAL,

    //>    @attr    textAreaItem.width        (number : 150 : IRW)
    //            default width of this item
    //        @group    appearance
    // @visibility external
    //<
    width:150,

    //>    @attr    textAreaItem.height        (number : 100 : IRW)
    //            default height of this item
    //        @group    appearance
    // @visibility external
    //<
    height:100,

    //>    @attr    textAreaItem.textBoxStyle (FormItemBaseStyle : "textItem" : IRW)
    //  Base CSS class to apply to this item's input element.
    // NOTE: See the +link{group:CompoundFormItem_skinning} discussion for special skinning considerations.
    //
    // @group   appearance
    // @visibility external
    //<
    textBoxStyle:"textItem",

    //>    @attr    textAreaItem.length        (number : null : IRW)
    //            if set, maximum number of characters for this field
    //        @group    validation
    //<
    // FIXME: not currently implemented
//    length:null,

    // Override redrawOnShowFormIcon - we can handle dynamically updating the item's HTML to
    // show / hide textArea item icons
    redrawOnShowIcon:false,
    // setting clipValue to true ensures we resize the text box when showing/hiding icons
    clipValue:true,


    //> @attr   textAreaItem._hasDataElement    (boolean : true : IRW)
    //      Text areas have a data element.
    // @group formValues
    // @visibility   internal
    // @see     method:FormItem.hasDataElement
    // @see     method:FormItem.getDataElement
    //<
    _hasDataElement:true,

    // This flag means updateState will apply the result of this.getTextBoxStyle() to this item's
    // data element - appropriate for native text boxes, text areas and selects.
    _dataElementIsTextBox:true,

    //> @attr   textAreaItem.emptyStringValue   (any : null : IRW)
    // @include textItem.emptyStringValue
    //<

    emptyStringValue:null,

    //> @attr   textAreaItem.lineBreakValue  (string : "\n" : IRW)
    //  What character string should be used to represent line breaks?<br>
    //  Multi-line values edited in TextAreaItems will use this string
    //  as a line separator.
    // @group formValues
    // @visibility   psft
    //<

    lineBreakValue:"\n",

    //> @attr   textAreaItem.iconVAlign  (VerticalAlignment : isc.Canvas.TOP : IR)
    //  Align icons with the top edge of text area icons by default.
    //  @group  formIcons
    // @visibility   external
    //<
    iconVAlign:isc.Canvas.TOP,

    // _nativeEventHandlers is a place to specify native event handlers to be applied to the
    // form item element once it has been written into the DOM (without having to override
    // '_applyHandlersToElement()'
    _nativeEventHandlers : {

        onmousedown : (
            isc.Browser.isIE ? function () {
                var element = this,
                    itemInfo = isc.DynamicForm._getItemInfoFromElement(element),
                    item = itemInfo.item;
                if (item) item._setupFocusCheck();

            } : null
        )
    },

    //> @method textAreaItem.getEnteredValue()
    // @include textItem.getEnteredValue()
    // @visibility external
    //<
    getEnteredValue : function () {
        return this.getElementValue();
    },

    //>@attr TextAreaItem.browserSpellCheck (boolean : null : IRWA)
    // @include FormItem.browserSpellCheck
    // @visibility internal
    //<

    //>@attr TextAreaItem.selectOnFocus (boolean : null : IRW)
    // @include FormItem.selectOnFocus
    // @visibility external
    //<

    //>@attr TextAreaItem.selectOnClick (boolean : null : IRW)
    // @include FormItem.selectOnClick
    // @visibility external
    //<

    //>@attr TextAreaItem.changeOnKeypress (Boolean : true : IRW)
    // @include FormItem.changeOnKeypress
    // @visibility external
    //<

    //>@method TextAreaItem.getSelectionRange()
    // @include FormItem.getSelectionRange()
    // @visibility external
    //<

    //>@method TextAreaItem.setSelectionRange()
    // @include FormItem.setSelectionRange()
    // @visibility external
    //<

    //>@method TextAreaItem.selectValue()
    // @include FormItem.selectValue()
    // @visibility external
    //<

    //>@method TextAreaItem.deselectValue()
    // @include FormItem.deselectValue()
    // @visibility external
    //<

    // supportsSelectionRange - does getSelectionRange() return null on this item? (IE only)
    // See FormItem._getIESelectionRange() for background on this
    // May cause poor performance determining selection range (for example on redraw) in
    // items with a lot of content
    supportsSelectionRange:true,

    //>@attr TextAreaItem.showHintInField (boolean : null : IRWA)
    // If showing hint for this form item, should it be shown within the field?
    // <P>CSS style for the hint is +link{textAreaItem.textBoxStyle} with the suffix
    // "Hint" appended to it.
    // @group appearance
    // @see FormItem.hint
    // @visibility external
    //<

    //>@attr TextAreaItem.printFullText (Boolean : true : IRW)
    // When generating a print-view of the component containing this TextArea, should
    // the form item expand to accommodate its value? If set to false the text box not expand
    // to fit its content in the print view, instead showing exactly as it does in the
    // live form, possibly with scrollbars.
    // @visibility external
    // @group printing
    //<
    printFullText:true,

    showClippedValueOnHover:false
});

isc.TextAreaItem.addMethods({

    // Don't allow any valueIcon to appear on a different line from the text area
    getTextBoxCellCSS : function () {
        return this._$nowrapCSS;
    },


    _sizeTextBoxAsContentBox : function () {
        return isc.Browser.isStrict;
    },


    // NOTE: this is here for doc generation
    //>    @method textAreaItem.keyPress        (A)
    //        @group    event handling
    //            event handler for keys pressed in this item
    //<



    // _willHandleInput()
    // Can we use the "input" event in this browser / form item?
    // True for Moz and Safari, but not IE. See comments near FormItem._handleInput()
    _willHandleInput : function () {
        return !isc.Browser.isIE;
    },

    //> @method textAreaItem.setElementReadOnly()
    // Change the read-only state of the form element immediately.
    //<
    setElementReadOnly : function (readOnly) {
        // TextArea HTML element has readonly property
        this._setElementReadOnly(readOnly);
    },

    //> @method textAreaItem.getElementHTML() (A)
    // Output the HTML for a text field element
    //
    // @param value (string)  Value of the element
    //                        [Unused because it is more reliably set by setValue].
    // @return (HTML)  HTML output for this element
    // @group drawing
    //<
    getElementHTML : function (value, dataValue) {
        // remember which element number we wrote this out as
        var form = this.form,
            formID = form.getID(),
            itemID = this.getItemID(),
            output = isc.StringBuffer.create(),
            valueIconHTML = this._getValueIconHTML(dataValue);
        if (valueIconHTML != null) output.append(valueIconHTML);
        if (!this.showValueIconOnly) {
            if ((!this.printFullText || !this._isPrinting()) && !this.renderAsStatic()) {
                output.append(
                    "<TEXTAREA NAME=" , this.getElementName(),
                    " ID=", this.getDataElementId(),

                    // hang a flag on the element marking it as the data element for the
                    // appropriate form item.
                    this._getItemElementAttributeHTML(),

                    this.getElementStyleHTML(),
                    (this.isDisabled() ? " DISABLED " : ""),

                    // disable native autoComplete
                    (this._getAutoCompleteSetting() != "native" ? " AUTOCOMPLETE=OFF " : ""),

                    // enable / disable native spellcheck in Moz
                    // Same setting in Safari - see comments in TextItem.js
                    ((isc.Browser.isMoz || isc.Browser.isSafari) ?
                        (this.getBrowserSpellCheck() ? " spellcheck=true" : " spellcheck=false") :
                        null),

                    " WRAP=", this.wrap,

                    " TABINDEX=", this._getElementTabIndex(),
                    (this.showTitle == false && this.accessKey != null ?
                        " ACCESSKEY=" + this.accessKey : ""),

                    // If this browser supports the "input" event write out a handler for it.
                    (this._willHandleInput ? " ONINPUT='" + this.getID() + "._handleInput()'"
                                           : null),

                    // If the readonly property is set, set it on the handle too
                    (this.isReadOnly() || this.isInactiveHTML() ?
                        (isc.Canvas.ariaEnabled() ? " aria-readonly='true' READONLY=TRUE" :
                            " READONLY=TRUE") : null),



                    // Ensure we pass events through the ISC event handling system.
                    " handleNativeEvents=false>",
                    (this.isInactiveHTML() ? value : null),
                    "</TEXTAREA>"
                );
            } else {
                if (value == null) value ="";
                // use a div with no sizing info specified. This'll fill the available
                // space in the form cell (and expand vertically as required)
                // note asHTML() to convert \n to <br> etc.
                output.append(
                    "<DIV style='",
                    // if width is specified as a number write it out
                    // if it's specified as "*" etc don't (we can't size based on the
                    // DyanmicForm's rendered width since printHTML is not rendered into
                    // the form!)
                    (isc.isA.Number(this.width) ? "width:" + this.width + "px;" : null)
                    ,"' class='", this.getTextBoxStyle(), "'>", value.asHTML(), "</DIV>"
                );
            }
        }

        //this.logWarn("textArea HTML:"+ output);
        return output.release();
    },

    //> @attr TextAreaItem.allowNativeResize (boolean : false, IRA)
    // Modern browsers allow drag-resizing of TextArea items. This flag may be set to enable
    // or suppress this behavior where supported.
    // @visibility internal
    //<

    allowNativeResize : false,
    handleMouseMove : function () {
        var returnVal = this.Super("handleMouseMove", arguments);
        if (returnVal == false || !this.allowNativeResize) return false;

        if (isc.EH.mouseIsDown() && this._resizeCheck == null) {
            this._resizeCheck = isc.Page.setEvent("idle", this.getID() + "._checkForElementResize()");
        }
    },
    _checkForElementResize : function () {
        var resized = false;
        var element = this.getDataElement();
        if (element) {
            if (element.offsetWidth != this.getTextBoxWidth()) resized = true;
            if (element.offsetHeight != this.getTextBoxHeight()) resized = true;
        }
        if (resized) this._nativeElementResize();

        if (!isc.EH.mouseIsDown()) {
            isc.Page.clearEvent("idle", this._resizeCheck);
            this._resizeCheck = null;
        }
    },

    _nativeElementResize : function () {
        var widget = this.containerWidget;
        if (widget) widget._markForAdjustOverflow("Native textarea resize");
    },

    // When focus is received, the hint should be hidden if TextAreaItem.showHintInField is true.
    _nativeElementFocus : function (element, itemID) {
        var returnVal = this.Super("_nativeElementFocus", arguments);

        // Hide in-field hint if being shown
        this._hideInFieldHint();


        // There may be custom parser / formatter logic applied to any text item and this
        // may not be a 1:1 mapping [EG a forgiving date format parser allowing variants on
        // a display format].
        // As with TextItem, store element value at focus so we can compare at blur time and
        // if necessary re-run the formatter on the stored item value
        // - note that by comparing element values rather than checking for changed data value we
        // catch the case where the user modified the display value to something that ultimately
        // maps back to the same data value [in which case a simple dataVal changed check might fail]
        this._elementValueAtFocus = this.getEnteredValue();

        return returnVal;
    },

    // Override _nativeElementBlur to fire 'change' explicitly in response to blur rather than
    // relying on the native 'ONCHANGE' handler method
    // (as with textItem)
    _nativeElementBlur : function (element, itemID) {
        var returnVal = this.Super("_nativeElementBlur", arguments);

        // Always fire elementChanged(). This will fall through to updateValue which will
        // no-op if the value is actually unchanged.
        this.form.elementChanged(this);


        // As with TextItem, call mapValueToDisplay() so we format the stored value to
        // the appropriate display value.
        // Required if a developer has custom formatters/parsers that are not 1:1
        // [EG: A forgiving data parser allowing variants on a display format].
        // Use the "_elementValueAtFocus" to avoid firing this unnecessarily.
        if (this._elementValueAtFocus == null ||
            this._elementValueAtFocus != this.getEnteredValue())
        {
            var value = this.getValue();
            if (this.mapValueToDisplay) {
                value = this.mapValueToDisplay(value);
            }
            this.setElementValue (value);
        }

        // If showing hint within data field, see if it should be shown now.
        if (this.showHintInField) {
            var undef;
            var value = this.getElementValue();
            if (value === undef || value == null || isc.is.emptyString(value)) {
                this._showInFieldHint();
            }
        }

        return returnVal;
    },

    //>    @method    textAreaItem.getElementStyleHTML()    (I)
    //          Get the HTML string used to set the visual characteristics for a textArea item.
    //          This includes the STYLE=... & CLASS=... properties to be written into this
    //          form item's element.
    //            This varies by platform, as we attempt to make Netscape think in pixels rather than
    //          characters and rows
    //
    //        @group    appearance
    //        @return    (string)    String of HTML containing STYLE=... & CLASS=... properties for
    //                          this items element.
    //
    //<
    getElementStyleHTML : function () {

        var width = this.getTextBoxWidth(),
            height = this.getTextBoxHeight();

        return isc.StringBuffer.concat(
            " CLASS='" + this.getTextBoxStyle(),

            (isc.Browser.isMoz && isc.isA.String(this.wrap) && this.wrap.toLowerCase() != "off" ?
                      "' ROWS=10 COLS=10" : "'"),
            " STYLE='",
            this.getElementCSSText(width,height),
            "' ");
    },

    //> @attr TextAreaItem.minHeight (int : 16 : IRW)
    // Minimum valid height for this TextAreaItem in px. If the specified +link{TextAreaItem.height}
    // is less than this value, the text area will still render at this height.
    // @visibility external
    //<
    minHeight:16,

    // helper to return the content of the "style" tag in the text box / data element
    getElementCSSText : function (width, height) {
        if (isc.isA.Number(width) && width <= 0) width = 1;
        if (isc.isA.Number(height) && height < this.minHeight) height = this.minHeight;

        return isc.StringBuffer.concat(

            this.allowNativeResize ? null : "resize:none;",
            // Ensure there's no margin(helps with sizing and v-alignment with icons)

            (isc.TextItem._needNegativeMargins
             ? "margin-top:-1px;margin-bottom:-1px;margin-left:0px;margin-right:0px;"
             : "margin:0px;"),
            (isc.isA.Number(width)     ? "WIDTH:" + width + "px;" : ""),
            (isc.isA.Number(height)    ? "HEIGHT:" + height + "px;" : ""),
            // text align property, known to be supported in IE6 and Moz/Firefox on
            // Windows, not supported on Safari 1.2
            (this.textAlign ? "text-align:" + this.textAlign + ";" : ""),


            (isc.Browser.isChrome ? "display:block;" : ""),
             // In Mozilla we must use the 'moz-user-focus' css property to govern
             // whether this element can recieve focus or not.
             (isc.Browser.isMoz ?
                    "-moz-user-focus:" + (this._getElementTabIndex() > 0 ? "normal;"
                                                                        : "ignore;") :
                    "")
        );
    },


    //>    @method    textAreaItem.mapValueToDisplay()    (A)
    //    Map from the internal value for this item to the display value.
    //        @group    drawing
    //        @param    internalValue        (string)    Internal value for this item.
    //        @return    (string)    Displayed value corresponding to internal value.
    //<
    mapValueToDisplay : function (internalValue, a,b,c,d) {
        var value = this.invokeSuper(isc.TextAreaItem, "mapValueToDisplay", internalValue, a,b,c,d);
        // always display the empty string for null values, rather than "null" or "undefined"
        if (value == null) value = isc.emptyString;
        return value;

    },

    //>    @method    textAreaItem.mapDisplayToValue()    (A)
    //    Map from a the display value for this item to the internal value.
    //        @group    drawing
    //
    //        @param    displayValue    (string)    Value displayed to the user.
    //        @return    (string)    Internal value corresponding to that display value.
    //<
    mapDisplayToValue : function (displayValue) {
        var value = this._unmapKey(displayValue);
        value = this._parseDisplayValue(value);
        // if the value to be saved is an empty string, map it to 'null' if necessary
        if (isc.is.emptyString(value)) value = this.emptyStringValue;
        return value;
    },

    //> @attr textAreaItem.formatOnBlur (Boolean : false : IRW)
    // With <code>formatOnBlur</code> enabled, this textAreaItem will format its value
    // according to any specified static +link{formItem.formatValue(),static formatter}
    // as long as the item does not have focus. Once the user puts focus into the item
    // the formatter will be removed. This provides a simply way for developers to
    // show a nicely formatted display value in a freeform text field, without the need
    // for an explicit +link{formItem.formatEditorValue()}
    // and +link{formItem.parseEditorValue()} pair.
    // @visibility external
    //<
    // Implemented at the FormItem level.


    // Don't apply arbitrary formatters specified via SimpleType definitions to this item's
    // display value - we have no way to parse it back to a real data value
    applyStaticTypeFormat:false,

    // override 'setValue' to update the data value to store when the element value is set to
    // the empty string.
    // See Text item setValue override for full description
    setValue : function (value) {

        // Make sure in-field hint is hidden
        this._hideInFieldHint();


        var undef;
        if (value !== undef && (value == null || isc.is.emptyString(value)))
            this.emptyStringValue = value;

        // Also clear out the '_hasEditedValue' flag, used to handle line break conversions
        // (See comments by the 'lineBreakValue' property)
        delete this._hasEditedValue;

        // Let parent take care of saving the value
        value = this.Super("setValue", arguments);

        // See if the in-field hint needs to be shown
        if (!this.hasFocus && this.showHint && this.showHintInField && this.getHint()) {
            if (value === undef || value == null || isc.is.emptyString(value)) {
                this._showInFieldHint();
            }
        }

        return value;
    },

    // Override 'updateValue()' to set a flag on this item marking it as having been edited.
    // This is used by 'getValue()' to determine whether we should convert line breaks to
    // the lineBreakValue for this item.
    updateValue : function () {
        this._hasEditedValue = true;
        return this.Super("updateValue", arguments);
    },

    compareValues : function (value1, value2) {
        if (value1 == null && value2 == null) return true;
        else if (value1 == null || value2 == null) return false;

        // Because a TextAreaItem is whitespace-sensitive, we don't want an actual number to be
        // treated as equal to a string of the number prefixed or suffixed with whitespace. For
        // example, we want the value 0 to be considered distinct from "0\n".
        // Also, 0 needs to be distinct from "-0" and "+0", and numbers have to be distinct from
        // a the string of the number with a unary plus (e.g. 8 distinct from "+8"), or prefixed
        // with zeroes (e.g. 7 distinct from "007"), or with a ".0" suffix.
        //
        // We don't have to worry about this problem for booleans because `true == "true"' and
        // `false == "false"' are both false.
        if (isc.isA.String(value2)) {
            if (isc.isA.Number(value1) || isc.isA.SpecialNumber(value1)) {
                return String(value1) == value2;
            }
        } else if (isc.isA.String(value1)) {
            if (isc.isA.Number(value2) || isc.isA.SpecialNumber(value2)) {
                return value1 == String(value2);
            }
        }

        return this.Super("compareValues", arguments);
    },

    // Override getValue() to convert any line break characters to this.lineBreakValue.
    // See comments by this.lineBreakValue definition for why we do this.
    getValue : function () {
        var value = this.Super("getValue", arguments);
        if (this._hasEditedValue && isc.isA.String(value)) {
            // replace every line break with our line break char string
            if (!this._lineBreakValueRegex)
                this._lineBreakValueRegex = new RegExp("(\\r\\n|[\\r\\n])", "g");
            value = "" + value;
            value = value.replace(this._lineBreakValueRegex, this.lineBreakValue);
        }

        return value;
    },

    // SCROLLING
    // Add support for programmatic scrolling of TextAreas



    getScrollHeight : function () {
        var element = this._getTextBoxElement();
        if (element == null) return this.getHeight();

        return element.scrollHeight;
    },

    getScrollWidth : function () {
        var element = this._getTextBoxElement();
        if (element == null) return this.getWidth();

        return element.scrollWidth;
    },

    _hscrollOn : function () {
        var element = this._getTextBoxElement();
        return element && element.scrollWidth > element.clientWidth;
    },

    _vscrollOn : function () {
        var element = this._getTextBoxElement();
        return element && element.scrollHeight > element.clientHeight;
    },

    getScrollTop : function () {
        var element = this._getTextBoxElement();
        if (element == null) return 0;
        return element.scrollTop;
    },

    getScrollLeft : function () {
        var element = this._getTextBoxElement();
        if (element == null) return 0;
        return element.scrollLeft;
    },

    scrollTo : function (left, top) {
        var element = this._getTextBoxElement();
        if (element == null) return;
        if (left != null) element.scrollLeft = left;
        if (top != null) element.scrollTop = top;
    },

    scrollToTop : function () {
        this.scrollTo(null, 0);
    },

    scrollToBottom : function () {
        var maxScroll = this.getScrollHeight() - this.getInnerHeight();
        if (maxScroll >= 0) {
            if (this._hscrollOn()) maxScroll += this.form.getScrollbarSize();
            this.scrollTo(null, maxScroll);
        }
    }

});








//>    @class    AutoFitTextAreaItem
// Class for editable multi-line text areas (uses HTML <code>&lt;TEXTAREA&gt;</code> object)
// automatically expands to accomodate its content
// @visibility external
// @example textAreaItem
//<
isc.ClassFactory.defineClass("AutoFitTextAreaItem", "TextAreaItem");


isc.AutoFitTextAreaItem.addProperties({

    // getScrollHeight / getScrollWidth
    // Neither IE nor moz seems to reliably support the scroll height of a text area if it
    // is less than the inner size of the text box. This means that if we've grown to accommodate
    // content we cant reliably detect when the content is shrunk by looking at our text-box.
    // We therefore create an offscreen 'tester' textarea which remains at the specified size
    // and measure the scrollHeight / scrollWidth of that element
    getTestBox : function (forceResize) {
        var value = this.mapValueToDisplay(this.getValue());
        var AFTAI = isc.AutoFitTextAreaItem;
        if (!AFTAI._testBoxCanvas) {
            AFTAI._testBoxCanvas = isc.Canvas.create({
                autoDraw:true, overflow:"hidden",
                left:0, top:-100,
                ariaState: {
                    hidden: true
                },
                contents:
                    ["<textarea ID='isc_autoFitTextArea_sizeTester' role='presentation' ",
                     "style='overflow:hidden;",
                     (isc.Browser.isIE ?
                        "margin-top:-1px;margin-bottom:-1px;margin-left:0px;margin-right:0px;" :
                        "margin:0px;"),
                     "'></textarea>"].join("")
            });
        }
        var box = isc.Element.get("isc_autoFitTextArea_sizeTester");
        // Match the text box's className and CSS Text to ensure our measurement is
        // accurate
        if (AFTAI.currentItem != this || forceResize) {
            box.className = this.getTextBoxStyle();

            // would be nice to apply this.getElementCSSText directly but doesn't seem to be
            // an obvious way to do this.
            if (isc.Browser.isMoz) {
                if (isc.isA.String(this.wrap) && this.wrap.toLowerCase() != "off") {
                    box.rows = 5; box.cols = 10;
                } else {
                    box.rows = ""; box.cols = "";
                }
            }

            box.setAttribute("wrap", this.wrap);

            // use assignSize - this will call setAttribute if necessary.
            var textBoxWidth = this.getTextBoxWidth(),
                textBoxHeight = this.getTextBoxHeight();
            AFTAI._testBoxCanvas._assignSize(box.style, "width", textBoxWidth);
            AFTAI._testBoxCanvas._assignSize(box.style, "height", textBoxHeight);

            box.style.textAlign = this.textAlign || "";

            box.cssText = this.getElementCSSText(textBoxWidth, textBoxHeight);
            AFTAI.currentItem = this;
        }

        box.value = value;

        var touch = box.scrollHeight;

        return box;
    },

    getScrollHeight : function (resized) {
        var testBox = this.getTestBox(resized);
        return testBox.scrollHeight;
    },

    getScrollWidth : function (resized) {
        var testBox = this.getTestBox(resized);
        return testBox.scrollWidth;
    },

    // force overflow to be hidden
    // Note: we're writing out the specified size rather than the overflowed size - this is
    // appropriate to force wrapping in the right places - we'll check the rendered size after
    // drawing and resize if necessary
    getElementCSSText : function (width, height) {
        var txt = this.Super("getElementCSSText", arguments);
        txt += "overflow:hidden;"
        return txt;
    },

    // These methods are required to determine the delta between the specified size of the
    // TextArea and the available space for content
    _getTextBoxHPadding : function () {
        if (this._tbhpadding != null) return this._tbhpadding;
        var textBox = this.getDataElement();
        if (!textBox) return 0;
        var leftPadding =
                parseInt(isc.Element.getComputedStyleAttribute(textBox, "paddingLeft")),
            rightPadding =
                parseInt(isc.Element.getComputedStyleAttribute(textBox, "paddingRight")),
            hPadding = (isc.isA.Number(leftPadding) ? leftPadding : 0) +
                       (isc.isA.Number(rightPadding) ? rightPadding : 0);

        this._tbhpadding = hPadding;
        return hPadding;

    },
    _getTextBoxVPadding : function () {
        if (this._tbvpadding != null) return this._tbvpadding;
        var textBox = this.getDataElement();
        if (!textBox) return 0;
        // In IE we've seen textBox.currentStyle be reported as null in some cases
        // if this happens, don't cache that value
        if (isc.Browser.isIE && textBox.currentStyle == null) return 0;
        var topPadding =
                parseInt(isc.Element.getComputedStyleAttribute(textBox, "paddingTop")),
            bottomPadding =
                parseInt(isc.Element.getComputedStyleAttribute(textBox, "paddingBottom")),
            vPadding = (isc.isA.Number(topPadding) ? topPadding : 0) +
                       (isc.isA.Number(bottomPadding) ? bottomPadding : 0);
        this._tbvpadding = vPadding;
        return vPadding;
    },

    //> @attr autoFitTextAreaItem.maxHeight (integer : null : IRW)
    // If specified, the autoFitTextArea will not grow taller than this height
    // @visibility external
    //<
    getMaxHeight : function () {
        return this.maxHeight;
    },

    //> @attr autoFitTextAreaItem.maxWidth (integer : null : IRW)
    // If specified, the autoFitTextArea will not grow wider than this width
    // @visibility internal
    //<

    getMaxWidth : function () {
        return this.maxWidth;
    },


    updateSize : function (resized) {
        var textBox = this.getDataElement();
        if (!textBox) return;

        var resetHandle, sizeChanged;

        var specifiedHeight = this.getTextBoxHeight(),
            vPadding = this._getTextBoxVPadding(),
            scrollHeight = this.getScrollHeight(resized),
            boxHeight = textBox.offsetHeight,
            calcHeight = scrollHeight + vPadding,
            maxHeight = this.getMaxHeight();
        if (maxHeight != null && maxHeight < calcHeight) {
            calcHeight = maxHeight;
        }
        if (calcHeight > boxHeight) {
            // stringifying the height makes no difference to non-css3 mode but is required
            // for css3 mode
            textBox.style.height = "" + calcHeight + "px";
            sizeChanged = true;

            // Catch the case where the box is shrinking
        } else if (calcHeight < boxHeight && boxHeight > specifiedHeight) {
            if (calcHeight < boxHeight) {
                // If we're shrinking, the dynamicForm will need to _resetHandleOnAdjustOverflow
                // to detect the shrinking of contents
                resetHandle = true;
                // stringifying the height makes no difference to non-css3 mode but is required
                // for css3 mode
                textBox.style.height = "" + Math.max(calcHeight, specifiedHeight) + "px";
            }
            sizeChanged = true
        }

        // width is trickier - we can expand easily to fit a non-wrapping line of text but
        // it will be hard to shrink since content will not rewrap smaller.
        var specifiedWidth = this.getTextBoxWidth(),
            hPadding = isc.Browser.isIE ? 0 : this._getTextBoxHPadding(),
            scrollWidth = this.getScrollWidth(resized),
            boxWidth = textBox.offsetWidth,
            calcWidth = hPadding + scrollWidth,
            maxWidth = this.getMaxWidth();
        if (maxWidth != null && calcWidth > maxWidth) calcWidth = maxWidth;

        if (calcWidth > boxWidth) {
            textBox.style.width = calcWidth;
            sizeChanged = true;

        } else if (calcWidth < boxWidth && boxWidth > specifiedWidth) {
            textBox.style.width = Math.max(specifiedWidth, calcWidth);
            resetHandle = true
            sizeChanged = true
        }

        if (resetHandle) this.containerWidget._resetHandleOnAdjustOverflow = true;
        if (sizeChanged) this.adjustOverflow("Updated size to fit content");
    },

    handleChanged : function () {
        this.updateSize();
        return this.Super("handleChanged", arguments);
    },
    drawn : function () {
        this.Super("drawn", arguments);
        delete this._tbhpadding;
        delete this._tbvpadding;
        this.updateSize(true);
    },
    redrawn : function () {
        this.Super("redrawn", arguments);
        delete this._tbhpadding;
        delete this._tbvpadding;
        this.updateSize(true);
    },
    _resetWidths : function () {
        this.Super("_resetWidths", arguments);
        this.updateSize(true);
    },

    // supportsSelectionRange - does getSelectionRange() return null on this item? (IE only)
    // See FormItem._getIESelectionRange() for background on this
    // Enable this in AutoFitTextAreas where modifying the value of the item is likely to change
    // the form's geometry and cause redraws and you really don't want to lose cursor positioning
    supportsSelectionRange:true
});






//>    @class TimeItem
//
// FormItem for editing times in a text field or via a set of selector components.
// <P>
// +link{TimeItem.timeFormatter} allows you to set the display format for this field.  See also
// +link{Time.setNormalDisplayFormat} for system-wide settings.
// <P>
// TimeItem automatically accepts both 12 and 24 hour time as well as partial times and a
// variety of possible time value separators.  Examples:
// <pre>
//        11:34:45 AM    => 11:34:45
//        1:3:5 AM    => 01:30:50
//        1:3p        => 13:30:00
//        11 34 am    => 11:34:00
//        11-34        => 11:34:00
//        113445        => 11:34:45
//        13445        => 01:34:45
//        1134        => 11:34:00
//        134            => 01:34:00
// </pre>
// <P>
// Values entered by the user are stored as JavaScript <code>Date</code> objects in local time.
// The day, month and year values of this <code>Date</code> object are not relevant and should
// be ignored.
// <P>
// By default, when used in a +link{class:SearchForm} or as a field in a +link{class:ListGrid}'s
// +link{listGrid.showFilterEditor, filter editor}, TimeItems will automatically generate
// AdvancedCriteria - for example, entering "11:00" into the item will generate a
// +link{operators.betweenInclusive, betweenInclusive} Criterion that selects all times between
// 11:00:00 and 11:59:59.  If the form is databound and the DataSource is marked as being
// +link{dataSource.allowAdvancedCriteria, allowAdvancedCriteria}:false, the criteria generated
// will be simple, checking for data with logical time values equal to the displayed value.
//
// @example timeItem
// @visibility external
//<



isc.ClassFactory.defineClass("TimeItem", "ContainerItem");
isc.TimeItem.addClassProperties({
    DEFAULT_TIME:"00:00:00"
});

isc.TimeItem.addProperties({
    //> @attr timeItem.textField (AutoChild TextItem : null : R)
    // Text field to hold the entire time in "type in" format, if
    // +link{timeItem.useTextField, useTextField} is true.
    //
    // @visibility external
    //<

    //> @attr timeItem.textFieldProperties (TextItem properties : null : IRA)
    // Custom properties to apply to the +link{timeItem.textField,text field} generated for
    // this timeItem when +link{timeItem.useTextField, useTextField} is true.
    // @visibility external
    //<

    textFieldDefaults: {
        name:"timeTextField", type:"text", changeOnBlur:true,
        // on keypress run standard 'change' behavior to store the value
        // as this._value - also mark as "dirty"
        // This allows us to preserve partially typed entries across redraws
        // while the item has focus.
        // We clear the dirty flag when we actually update the TimeItem's value
        // on blur, or in setValue() if we're changing to a new value.
        changeOnKeypress:false,
        changed : function () {
            this.isDirty = true;
        },

        // Override the blur method to update the TimeItem value
        // Using blur rather than saveValue / change allows changeOnKeypress to
        // be set to true without the timeItem clobbering the user's half-typed
        // strings
        blur : function () {
            this.isDirty = false;
            if (this.parentItem) this.parentItem.updateValue();
        },

        shouldSaveValue:false,

        // If textBoxStyle is defined on the TimeItem, pick it up.
        getTextBoxStyle : function () {
            var parent = this.parentItem;
            if (parent != null && parent.textBoxStyle != null) {
                return parent.textBoxStyle;
            }
            // return Super so we pick up default TextItem styling.
            return this.Super("getTextBoxStyle", arguments);
        },

        // Determine the width based on the parent's specified textBox size
        getTextBoxWidth : function (value) {
            if (this.parentItem) {
                return this.parentItem.getTextBoxWidth(value);
            }
            return this.Super("getTextBoxWidth", arguments);
        },

        _shouldUpdateParentItem: true,

        // Have change occur on enter too (useful for things like submitting forms on enter, embedded
        // TimeItems in ListGrid editors, etc)
        handleKeyPress : function () {
            var returnVal = this.Super("handleKeyPress", arguments);
            if (returnVal != false) {
                var keyName = isc.EH.getKey();
                if (keyName == "Enter") {
                    this.updateValue();
                    if (this.parentItem) this.parentItem.updateValue();
                    // Reformat to display the pretty value

                    this.setElementValue(this.mapValueToDisplay(this.getValue()));
                }
            }
            return returnVal;
        },

        mapValueToDisplay : function (value) {
            return this.parentItem.mapValueToDisplay.apply(this, arguments);
        },

        mapDisplayToValue : function (value) {
            return this.parentItem.mapDisplayToValue.apply(this, arguments);
        },

        init : function() {
            if (this.parseEditorValue) this.changeOnKeypress = false;
            // If a mask is to be used, configure it now.
            if (this.useMask) {
                var map = this.formatterMap[this._getTimeFormatter()];
                if (!map) {
                    this.useMask = false;
                    this.mask = null;
                    this.logWarn("Mask will not be used because timeFormatter " + this._getTimeFormatter() +
                        " is not recognized");
                } else {
                    // Map formatter to padded version if needed
                    if (map.formatter) this.timeFormatter = map.formatter;
                    this.mask = map.mask;
                }
                if (this.mask) {
                    this.maskSaveLiterals = true;
                    this.maskOverwriteMode  = true;
                }
            } else if (this.mask) {
                // Make sure user doesn't try to assign a custom mask
                this.mask = null;
            }

            // Let TextItem do remaining initialization
            this.Super("init", arguments);
        },

        setMask : function (mask) {
            this.logWarn("setMask: custom mask ignored");
        }
    },


    // Unit Selectors

    //> @attr timeItem.showHourItem (Boolean : true : IRW)
    // Controls whether to display the +link{hourItem} when +link{useTextField} is false.
    // @visibility external
    //<
    showHourItem:true,

    //> @attr timeItem.hourItem (AutoChild SelectItem : null : R)
    // Select item to hold the hours portion of the time or
    // +link{timeItem.getDuration, duration} when +link{timeItem.useTextField, useTextField}
    // is false.
    // @visibility external
    //<

    //> @attr timeItem.hourItemProperties (SelectItem properties : null : IRA)
    // Custom properties to apply to this timeItem's generated
    // +link{timeItem.hourItem, hour picker}.
    // @visibility external
    //<
    hourItemDefaults: {
        name: "hourItem", prompt: "Choose hours", type: "select",
        titleOrientation: "top", showTitle: true, addUnknownValues: false, titleAlign: "center",
        valueMap: "this.parentItem.getHourValues()", shouldSaveValue: false,
        align: "center",
        // Override saveValue to update the parent.

        saveValue : function () {
            this.Super("saveValue", arguments);
            this.parentItem.updateValue();
        },
        getValue : function () {
            var value = this.Super("getValue", arguments);
            return new Number(value);
        },
        // Don't adjust the selectors for errors (which are shown at the parent-item level)
        getErrorWidth:function () {return 0;},
        width: 40,
        // avoid additional changed events from this sub-item
        suppressItemChanged: true
    },
    //> @attr timeItem.hourItemTitle (string : "Hour" : IRW)
    // Title to show for the +link{timeItem.hourItem, hour picker}.
    // @group i18nMessages
    // @visibility external
    //<
    hourItemTitle: "Hour",
    //> @attr timeItem.hourItemPrompt (string : "Choose hours" : IRW)
    // The hover prompt to show for the +link{timeItem.hourItem, hour picker}.
    // @group i18nMessages
    // @visibility external
    //<
    hourItemPrompt: "Choose hours",

    //> @attr timeItem.showMinuteItem (Boolean : true : IRW)
    // Controls whether to display the +link{minuteItem} when +link{useTextField} is false.
    // @visibility external
    //<
    showMinuteItem:true,
    //> @attr timeItem.minuteItem (AutoChild SelectItem : null : R)
    // Select item to hold the minutes portion of the time or
    // +link{timeItem.getDuration, duration} when +link{timeItem.useTextField, useTextField}
    // is false.
    // @visibility external
    //<

    //> @attr timeItem.minuteItemProperties (SelectItem properties : null : IRA)
    // Custom properties to apply to this timeItem's generated
    // +link{timeItem.minuteItem, minute picker}.
    // @visibility external
    //<
    minuteItemDefaults: {
        name: "minuteItem", prompt: "Choose minutes", type: "select",
        titleOrientation: "top", showTitle: true, addUnknownValues: false, titleAlign: "center",
        align: "center",
        valueMap: "this.parentItem.getMinuteValues()", shouldSaveValue: false,
        // Override saveValue to update the parent.

        saveValue : function () {
            this.Super("saveValue", arguments);
            this.parentItem.updateValue();
        },
        getValue : function () {
            var value = this.Super("getValue", arguments);
            return new Number(value);
        },
        // Don't adjust the selectors for errors (which are shown at the parent-item level)
        getErrorWidth:function () {return 0;},
        width: 40,
        // avoid additional changed events from this sub-item
        suppressItemChanged: true
    },
    //> @attr timeItem.minuteItemTitle (string : "Min" : IRW)
    // Title to show for the +link{timeItem.minuteItem, minute picker}.
    // @group i18nMessages
    // @visibility external
    //<
    minuteItemTitle: "Min",
    //> @attr timeItem.minuteItemPrompt (string : "Choose minutes" : IRW)
    // The hover prompt to show for the +link{timeItem.minuteItem, minute picker}.
    // @group i18nMessages
    // @visibility external
    //<
    minuteItemPrompt: "Choose minutes",

    //> @attr timeItem.showSecondItem (Boolean : true : IRW)
    // Controls whether to display the +link{secondItem} when +link{useTextField} is false.
    // @visibility external
    //<
    showSecondItem:true,
    //> @attr timeItem.secondItem (AutoChild SelectItem : null : R)
    // Select item to hold the seconds portion of the time or
    // +link{timeItem.getDuration, duration} when +link{timeItem.useTextField, useTextField}
    // is false.
    // @visibility external
    //<
    //> @attr timeItem.secondItemProperties (SelectItem properties : null : IRA)
    // Custom properties to apply to this timeItem's generated
    // +link{timeItem.secondItem, seconds picker}.
    // @visibility external
    //<
    secondItemDefaults: {
        name: "secondItem", prompt: "Choose seconds", type: "select",
        titleOrientation: "top", showTitle: true, addUnknownValues: false, titleAlign: "center",
        valueMap: "this.parentItem.getSecondValues()", shouldSaveValue: false,
        align: "center",
        // Override saveValue to update the parent.

        saveValue : function () {
            this.Super("saveValue", arguments);
            this.parentItem.updateValue();
        },
        getValue : function () {
            var value = this.Super("getValue", arguments);
            return new Number(value);
        },
        // Don't adjust the selectors for errors (which are shown at the parent-item level)
        getErrorWidth:function () {return 0;},
        width: 40,
        // avoid additional changed events from this sub-item
        suppressItemChanged: true
    },
    //> @attr timeItem.secondItemTitle (string : "Sec" : IRW)
    // Title to show for the +link{timeItem.secondItem, second picker}.
    // @group i18nMessages
    // @visibility external
    //<
    secondItemTitle: "Sec",
    //> @attr timeItem.secondItemPrompt (string : "Choose seconds" : IRW)
    // The hover prompt to show for the +link{timeItem.secondItem, second picker}.
    // @group i18nMessages
    // @visibility external
    //<
    secondItemPrompt: "Choose seconds",

    //> @attr timeItem.showMillisecondItem (Boolean : false : IRW)
    // Controls whether to display the +link{millisecondItem} when +link{useTextField} is false.
    // @visibility external
    //<
    showMillisecondItem:false,
    //> @attr timeItem.millisecondItem (AutoChild SelectItem : null : R)
    // Select item to hold the milliseconds portion of the time or
    // +link{timeItem.getDuration, duration} when +link{timeItem.useTextField, useTextField}
    // is false.
    // @visibility external
    //<

    //> @attr timeItem.millisecondItemProperties (SelectItem properties : null : IRA)
    // Custom properties to apply to this timeItem's generated
    // +link{timeItem.millisecondItem, millisecond picker}.
    // @visibility external
    //<
    showMillisecondItem: false,
    millisecondItemDefaults: {
        name: "millisecondItem", prompt: "Choose milliseconds", type: "select",
        titleOrientation: "top", showTitle: true, addUnknownValues: false, titleAlign: "center",
        align: "center",
        valueMap: "this.parentItem.getMillisecondValues()", shouldSaveValue: false,
        // Override saveValue to update the parent.

        saveValue : function () {
            this.Super("saveValue", arguments);
            this.parentItem.updateValue();
        },
        // Don't adjust the selectors for errors (which are shown at the parent-item level)
        getErrorWidth:function () {return 0;},
        width: 42,
        // avoid additional changed events from this sub-item
        suppressItemChanged: true
    },
    //> @attr timeItem.millisecondItemTitle (string : "Ms" : IRW)
    // Title to show for the +link{timeItem.millisecondItem, millisecond picker}.
    // @group i18nMessages
    // @visibility external
    //<
    millisecondItemTitle: "Ms",
    //> @attr timeItem.millisecondItemPrompt (string : "Choose milliseconds" : IRW)
    // The hover prompt to show for the +link{timeItem.millisecondItem, millisecond picker}.
    // @group i18nMessages
    // @visibility external
    //<
    millisecondItemPrompt: "Choose milliseconds",

    //> @attr timeItem.ampmItem (AutoChild SelectItem : null : R)
    // Select item to hold the AM/PM value for the timeItem when
    // +link{timeItem.useTextField, useTextField} is false.
    // @visibility external
    //<

    //> @attr timeItem.ampmItemProperties (SelectItem properties : null : IRA)
    // Custom properties to apply to this timeItem's generated
    // +link{timeItem.ampmItem,AM/PM picker}.
    // @visibility external
    //<
    ampmItemDefaults: {
        name: "ampmItem", prompt: "Choose AM or PM", type: "select",
        titleOrientation: "top", showTitle: true, addUnknownValues: false, titleAlign: "center",
        valueMap: "this.parentItem.getAmpmOptions()", shouldSaveValue: false,
        align: "center", defaultDynamicValue: "this.parentItem.getAmpmOptions()[0]",
        // Override saveValue to update the parent.

        saveValue : function () {
            this.Super("saveValue", arguments);
            this.parentItem.updateValue();
        },
        // Don't adjust the selectors for errors (which are shown at the parent-item level)
        getErrorWidth:function () {return 0;},
        width: 40,
        // avoid additional changed events from this sub-item
        suppressItemChanged: true
    },
    //> @attr timeItem.ampmItemTitle (string : "AM/PM" : IRW)
    // Title to show for the +link{timeItem.ampmItem, AM/PM picker}.
    // @group i18nMessages
    // @visibility external
    //<
    ampmItemTitle: "AM/PM",

    // ORIGINAL CODE...


    width:100,

    //> @attr timeItem.displayFormat (TimeDisplayFormat: null : IRW)
    // What format should this item's time string be presented in?
    // @visibility external
    // @deprecated in favor of +link{timeItem.timeFormatter}
    //<
    //displayFormat:"toShort24HourTime",

    //> @attr timeItem.timeFormatter (TimeDisplayFormat: "toShort24HourTime" : IRW)
    // What format should this item's time string be presented in? If unset the format will be
    // derived from +link{dynamicForm.timeFormatter} or the system-side
    // @visibility external
    //<
    timeFormatter:"toShort24HourTime",

    // Note: we don't have an inputFormat (as we do with DateItems).
    // Time strings are not ambiguous like dates, and our time parsing function deals
    // with essentially any time format the user is likely to enter.

    //> @attr timeItem.allowEmptyValue (Boolean : true : IRW)
    // If true, this time item supports empty values when cleared out
    //<
    // Not clear whether we should mark this visibility external
    allowEmptyValue:true,


    //> @attr timeItem.useMask (Boolean : null : IRA)
    // If true, a data entry mask will be enabled in the field based on the
    // +link{TimeItem.timeFormatter}.
    // <p>
    // Note that if a non-padded +link{TimeItem.timeFormatter} is specified, it
    // will be changed to the corresponding padded version (ex. "toShort24HourTime"
    // will be changed to "toShortPadded24HourTime").
    // @visibility external
    //<

    // Default to 100 wide
    // This is an appropriate default if we're showing the text field
    // If we're showing the selectors, this value will be forced to 200 during setItems
    width:100,

    cellPadding:0,

    //> @attr timeItem.useTextField (Boolean : true : IRW)
    // Should we show the time in a text field, or as a number of SelectItems?
    // @visibility external
    //<
    useTextField: true,

    //> @attr timeItem.textAlign (Alignment : varies : IRW)
    // If +link{timeItem.useTextField} is <code>true</code>, this property governs the alignment
    // of text within the text field. Defaults to <code>"left"</code> by default or
    // <code>"right"</code> if the page is in +link{isc.Page.isRTL(),rtl mode}.
    // @visibility external
    //<

    textAlign:isc.Page.isRTL() ? isc.Canvas.RIGHT : isc.Canvas.LEFT,

    //> @attr timeItem.invalidTimeStringMessage (string : "Invalid time" : IRW)
    // Validation error message to display if the user enters an invalid time string.
    // @group i18nMessages
    // @visibility external
    //<
    invalidTimeStringMessage:"Invalid time",

    //> @attr timeItem.use24HourTime (Boolean : null : IRW)
    // Whether to enforce 24-hour time in the UI.
    // @visibility external
    //<
    use24HourTime: true
});

isc.TimeItem.addMethods({

    //> @attr timeItem.textBoxStyle (CSSStyleName : null : IRW)
    // Base CSS class for this item's text box.
    // If specified this style will be applied to the +link{timeItem.textField} if
    // +link{timeitem.useTextField} is set to <code>true</code>.
    // @visibility external
    //<
    // If unset (the default) standard text box styling will apply.
    textBoxStyle:null,

    // Override getTextBoxStyle() to return the textBoxStyle displayed in the
    // textField autoChild item (which will be derived from this.textBoxStyle iff specified).
    // Also override _sizeTextBoxAsContentBox to pick up the value from the textField
    // item.
    // These allow the textField autoChild to call "getTextBoxWidth()" on this item and
    // pick up the correct width based on the specified TimeItem width as well as the
    // styling / rendering implementation of te TimeItem.textField.
    getTextBoxStyle : function () {
        if (this.useTextField && this.textField) {
            return this.textField.getTextBoxStyle();
        }
        return this.Super("getTextBoxStyle", arguments);
    },
    _sizeTextBoxAsContentBox : function () {
        if (this.useTextField && this.textField) {
            return this.textField._sizeTextBoxAsContentBox();
        }
        return this.Super("_sizeTextBoxAsContentBox", arguments);
    },

    showing24HourTime : function () {
        return this.use24HourTime;
    },

    // helper to generate the list of valid entries for some picker (one of
    // hour/minute/second/millisecond)
    getSubItemValues : function (itemName) {
        var values = this[itemName + "Values"],
            unit = itemName.substring(0,1).toUpperCase() + itemName.substring(1, itemName.length)
        ;
        if (!values) {
            values = [];
            var min = this["get" + unit + "MinValue"](),
                max = this["get" + unit + "MaxValue"](),
                count = 0
            ;
            for (var i=min; i<=max; i+=this[itemName + "Increment"]) {
                if (i < 10) values.add(isc.NumberUtil.stringify(i, 2));
                else values.add(i);
                count++;
            }
            this[itemName + "Values"] = values;
        }
        return this[itemName + "Values"];
    },

    //> @attr timeItem.hourValues (Array of Number : null : IRW)
    // An array of values to make available in the +link{timeItem.hourItem,hour picker} when
    // +link{timeItem.useTextField,useTextField} is false.
    // <P>Used for specifying a limited set of valid Hour values, or when using the
    // TimeItem to record duration, rather than time per-se.
    // <P> See +link{timeItem.hourMinValue, hourMinValue},
    // +link{timeItem.hourMaxValue, hourMaxValue} and
    // +link{timeItem.hourIncrement, hourIncrement} for another method of controlling the
    // content in the hour picker.
    // @setter setHourValues
    // @visibility external
    //<

    //> @method timeItem.setHourValues() (A)
    // Sets the array of valid +link{timeItem.hourValues, hour values} to use when
    // +link{timeItem.useTextField,useTextField} is false.
    // <P>Used for limiting available valid Hour values, or when using the TimeItem to record
    // duration, rather than time per-se.
    // <P> See +link{timeItem.hourMinValue, hourMinValue},
    // +link{timeItem.hourMaxValue, hourMaxValue} and
    // +link{timeItem.hourIncrement, hourIncrement} for another method of controlling the
    // content in the hour picker.
    // @param values (Array of Number) array of available Hour values
    // @visibility external
    //<
    setHourValues : function (values) {
        this.hourValues = values;
        if (this.hourItem) this.hourItem.setValueMap(this.getHourValues());
    },
    //> @method timeItem.getHourValues() (A)
    // Returns an array of the current valid hour values, whether set directly as
    // +link{timeItem.hourValues} or generated according to
    // +link{timeItem.hourMinValue, hourMinValue},
    // +link{timeItem.hourMaxValue, hourMaxValue} and
    // +link{timeItem.hourIncrement, hourIncrement}.
    // @return (Array of Number) array of available Hour values
    // @visibility external
    //<
    getHourValues : function () {
        return this.getSubItemValues("hour");
    },

    //> @attr timeItem.hourMinValue (Number : null : IRW)
    // When +link{timeItem.useTextField,useTextField} is false and
    // +link{timeItem.hourValues, hourValues} is unset, this attribute specifies the minimum
    // value present in the hour picker.
    // <P>Used for specifying a limited set of valid Hour values, or when using the
    // TimeItem to record duration, rather than time per-se.  The default is zero in all cases.
    // <P> See also +link{timeItem.hourMaxValue, hourMaxValue} and
    // +link{timeItem.hourIncrement, hourIncrement}.
    // @visibility external
    //<
    hourMinValue: null,
    //> @attr timeItem.hourMaxValue (Number : null : IRW)
    // When +link{timeItem.useTextField,useTextField} is false and
    // +link{timeItem.hourValues, hourValues} is unset, this attribute specifies the maximum
    // value present in the hour picker.
    // <P>Used for specifying a limited set of valid Hour values, or when using the
    // TimeItem to record duration, rather than time per-se.  The default is 11 or 23,
    // according to the value of +link{timeItem.use24HourTime, use24HourTime} and
    // +link{timeItem.timeFormatter, timeFormatter}.
    // <P> See also +link{timeItem.hourMinValue, hourMinValue} and
    // +link{timeItem.hourIncrement, hourIncrement}.
    // @visibility external
    //<
    hourMaxValue: null,
    //> @attr timeItem.hourIncrement (Number : null : IRW)
    // When +link{timeItem.useTextField,useTextField} is false and
    // +link{timeItem.hourValues, hourValues} is unset, this attribute specifies the increment
    // to use when generating entries for the hour picker.  For example, if this attribute is
    // set to 5, the hour picker will contain only every fifth value between the
    // +link{timeItem.hourMinValue, hourMinValue} and
    // +link{timeItem.hourMaxValue, hourMaxValue}.
    // @visibility external
    //<
    hourIncrement: 1,
    getHourMinValue : function () {
        if (this.hourMinValue == null) this.hourMinValue = (this.showing24HourTime() ? 0 : 1);
        return this.hourMinValue;
    },
    getHourMaxValue : function () {
        if (this.hourMaxValue == null) this.hourMaxValue = (this.showing24HourTime() ? 23 : 12);
        return this.hourMaxValue;
    },

    //> @attr timeItem.minuteValues (Array of Number : null : IRW)
    // An array of values to make available in the +link{timeItem.minuteItem,minute picker} when
    // +link{timeItem.useTextField,useTextField} is false.
    // <P>Used for specifying a limited set of valid Minute values, or when using the
    // TimeItem to record duration, rather than time per-se.
    // <P> See +link{timeItem.minuteMinValue, minuteMinValue},
    // +link{timeItem.minuteMaxValue, minuteMaxValue} and
    // +link{timeItem.minuteIncrement, minuteIncrement} for another method of controlling the
    // content in the minute picker.
    // @setter setMinuteValues
    // @visibility external
    //<

    //> @method timeItem.setMinuteValues() (A)
    // Sets the array of valid +link{timeItem.minuteValues, minute values} to use when
    // +link{timeItem.useTextField,useTextField} is false.
    // <P>Used for limiting available valid Minute values, or when using the TimeItem to record
    // duration, rather than time per-se.
    // <P> See +link{timeItem.minuteMinValue, minuteMinValue},
    // +link{timeItem.minuteMaxValue, minuteMaxValue} and
    // +link{timeItem.minuteIncrement, minuteIncrement} for another method of controlling the
    // content in the minute picker.
    // @param values (Array of Number) array of available Minute values
    // @visibility external
    //<
    setMinuteValues : function (values) {
        this.minuteValues = values;
        if (this.minuteItem) this.minuteItem.setValueMap(this.getMinuteValues());
    },
    //> @method timeItem.getMinuteValues() (A)
    // Returns an array of the current valid minute values, whether set directly as
    // +link{timeItem.minuteValues} or generated according to
    // +link{timeItem.minuteMinValue, minuteMinValue},
    // +link{timeItem.minuteMaxValue, minuteMaxValue} and
    // +link{timeItem.minuteIncrement, minuteIncrement}.
    // @return (Array of Number) array of available Minute values
    // @visibility external
    //<
    getMinuteValues : function () {
        return this.getSubItemValues("minute");
    },

    //> @attr timeItem.minuteMinValue (Number : null : IRW)
    // When +link{timeItem.useTextField,useTextField} is false and
    // +link{timeItem.minuteValues, minuteValues} is unset, this attribute specifies the minimum
    // value present in the minute picker.
    // <P>Used for specifying a limited set of valid Minute values, or when using the
    // TimeItem to record duration, rather than time per-se.  The default is zero in all cases.
    // <P> See also +link{timeItem.minuteMaxValue, minuteMaxValue} and
    // +link{timeItem.minuteIncrement, minuteIncrement}.
    // @visibility external
    //<
    minuteMinValue: null,
    //> @attr timeItem.minuteMaxValue (Number : null : IRW)
    // When +link{timeItem.useTextField,useTextField} is false and
    // +link{timeItem.minuteValues, minuteValues} is unset, this attribute specifies the maximum
    // value present in the minute picker.
    // <P>Used for specifying a limited set of valid Minute values, or when using the
    // TimeItem to record duration, rather than time per-se.  The default 59.
    // <P> See also +link{timeItem.minuteMinValue, minuteMinValue} and
    // +link{timeItem.minuteIncrement, minuteIncrement}.
    // @visibility external
    //<
    minuteMaxValue: null,
    //> @attr timeItem.minuteIncrement (Number : null : IRW)
    // When +link{timeItem.useTextField,useTextField} is false and
    // +link{timeItem.minuteValues, minuteValues} is unset, this attribute specifies the increment
    // to use when generating entries for the minute picker.  For example, if this attribute is
    // set to 5, the minute picker will contain only every fifth value between the
    // +link{timeItem.minuteMinValue, minuteMinValue} and
    // +link{timeItem.minuteMaxValue, minuteMaxValue}.
    // @visibility external
    //<
    minuteIncrement: 1,
    getMinuteMinValue : function () {
        if (this.minuteMinValue == null) this.minuteMinValue = 0;
        return this.minuteMinValue;
    },
    getMinuteMaxValue : function () {
        if (this.minuteMaxValue == null) this.minuteMaxValue = 59;
        return this.minuteMaxValue;
    },

    //> @attr timeItem.secondValues (Array of Number : null : IRW)
    // An array of values to make available in the +link{timeItem.secondItem,second picker} when
    // +link{timeItem.useTextField,useTextField} is false.
    // <P>Used for specifying a limited set of valid Second values, or when using the
    // TimeItem to record duration, rather than time per-se.
    // <P> See +link{timeItem.secondMinValue, secondMinValue},
    // +link{timeItem.secondMaxValue, secondMaxValue} and
    // +link{timeItem.secondIncrement, secondIncrement} for another method of controlling the
    // content in the second picker.
    // @setter setSecondValues
    // @visibility external
    //<

    //> @method timeItem.setSecondValues() (A)
    // Sets the array of valid +link{timeItem.secondValues, second values} to use when
    // +link{timeItem.useTextField,useTextField} is false.
    // <P>Used for limiting available valid Second values, or when using the TimeItem to record
    // duration, rather than time per-se.
    // <P> See +link{timeItem.secondMinValue, secondMinValue},
    // +link{timeItem.secondMaxValue, secondMaxValue} and
    // +link{timeItem.secondIncrement, secondIncrement} for another method of controlling the
    // content in the second picker.
    // @param values (Array of Number) array of available Second values
    // @visibility external
    //<
    setSecondValues : function (values) {
        this.secondValues = values;
        if (this.secondItem) this.secondItem.setValueMap(this.getSecondValues());
    },
    //> @method timeItem.getSecondValues() (A)
    // Returns an array of the current valid second values, whether set directly as
    // +link{timeItem.secondValues} or generated according to
    // +link{timeItem.secondMinValue, secondMinValue},
    // +link{timeItem.secondMaxValue, secondMaxValue} and
    // +link{timeItem.secondIncrement, secondIncrement}.
    // @return (Array of Number) array of available Second values
    // @visibility external
    //<
    getSecondValues : function () {
        return this.getSubItemValues("second");
    },

    //> @attr timeItem.secondMinValue (Number : null : IRW)
    // When +link{timeItem.useTextField,useTextField} is false and
    // +link{timeItem.secondValues, secondValues} is unset, this attribute specifies the minimum
    // value present in the second picker.
    // <P>Used for specifying a limited set of valid Second values, or when using the
    // TimeItem to record duration, rather than time per-se.  The default is zero in all cases.
    // <P> See also +link{timeItem.secondMaxValue, secondMaxValue} and
    // +link{timeItem.secondIncrement, secondIncrement}.
    // @visibility external
    //<
    secondMinValue: null,
    //> @attr timeItem.secondMaxValue (Number : null : IRW)
    // When +link{timeItem.useTextField,useTextField} is false and
    // +link{timeItem.secondValues, secondValues} is unset, this attribute specifies the maximum
    // value present in the second picker.
    // <P>Used for specifying a limited set of valid Second values, or when using the
    // TimeItem to record duration, rather than time per-se.  The default is 59.
    // <P> See also +link{timeItem.secondMinValue, secondMinValue} and
    // +link{timeItem.secondIncrement, secondIncrement}.
    // @visibility external
    //<
    secondMaxValue: null,
    //> @attr timeItem.secondIncrement (Number : null : IRW)
    // When +link{timeItem.useTextField,useTextField} is false and
    // +link{timeItem.secondValues, secondValues} is unset, this attribute specifies the increment
    // to use when generating entries for the second picker.  For example, if this attribute is
    // set to 5, the second picker will contain only every fifth value between the
    // +link{timeItem.secondMinValue, secondMinValue} and
    // +link{timeItem.secondMaxValue, secondMaxValue}.
    // @visibility external
    //<
    secondIncrement: 1,
    getSecondMinValue : function () {
        if (this.secondMinValue == null) this.secondMinValue = 0;
        return this.secondMinValue;
    },
    getSecondMaxValue : function () {
        if (this.secondMaxValue == null) this.secondMaxValue = 59;
        return this.secondMaxValue;
    },

    //> @attr timeItem.millisecondValues (Array of Number : null : IRW)
    // An array of values to make available in the
    // +link{timeItem.millisecondItem,millisecond picker} when
    // +link{timeItem.useTextField,useTextField} is false.
    // <P>Used for specifying a limited set of valid Millisecond values, or when using the
    // TimeItem to record duration, rather than time per-se.
    // <P> See +link{timeItem.millisecondMinValue, millisecondMinValue},
    // +link{timeItem.millisecondMaxValue, millisecondMaxValue} and
    // +link{timeItem.millisecondIncrement, millisecondIncrement} for another method of
    // controlling the content in the millisecond picker.
    // @setter setMillisecondValues
    // @visibility external
    //<

    //> @method timeItem.setMillisecondValues() (A)
    // Sets the array of valid +link{timeItem.millisecondValues, millisecond values} to use when
    // +link{timeItem.useTextField,useTextField} is false.
    // <P>Used for limiting available valid Millisecond values, or when using the TimeItem to record
    // duration, rather than time per-se.
    // <P> See +link{timeItem.millisecondMinValue, millisecondMinValue},
    // +link{timeItem.millisecondMaxValue, millisecondMaxValue} and
    // +link{timeItem.millisecondIncrement, millisecondIncrement} for another method of controlling the
    // content in the millisecond picker.
    // @param values (Array of Number) array of available Millisecond values
    // @visibility external
    //<
    setMillisecondValues : function (values) {
        this.millisecondValues = values;
        if (this.millisecondItem) this.millisecondItem.setValueMap(this.getMillisecondValues());
    },
    //> @method timeItem.getMillisecondValues() (A)
    // Returns an array of the current valid millisecond values, whether set directly as
    // +link{timeItem.millisecondValues} or generated according to
    // +link{timeItem.millisecondMinValue, millisecondMinValue},
    // +link{timeItem.millisecondMaxValue, millisecondMaxValue} and
    // +link{timeItem.millisecondIncrement, millisecondIncrement}.
    // @return (Array of Number) array of available Millisecond values
    // @visibility external
    //<
    getMillisecondValues : function () {
        return this.getSubItemValues("millisecond");
    },

    //> @attr timeItem.millisecondMinValue (Number : null : IRW)
    // When +link{timeItem.useTextField,useTextField} is false and
    // +link{timeItem.millisecondValues, millisecondValues} is unset, this attribute specifies
    // the minimum value present in the millisecond picker.
    // <P>Used for specifying a limited set of valid Millisecond values, or when using the
    // TimeItem to record duration, rather than time per-se.  The default is zero in all cases.
    // <P> See also +link{timeItem.millisecondMaxValue, millisecondMaxValue} and
    // +link{timeItem.millisecondIncrement, millisecondIncrement}.
    // @visibility external
    //<
    millisecondMinValue: null,
    //> @attr timeItem.millisecondMaxValue (Number : null : IRW)
    // When +link{timeItem.useTextField,useTextField} is false and
    // +link{timeItem.millisecondValues, millisecondValues} is unset, this attribute specifies
    // the maximum value present in the millisecond picker.
    // <P>Used for specifying a limited set of valid Millisecond values, or when using the
    // TimeItem to record duration, rather than time per-se.  The default is 999.
    // <P> See also +link{timeItem.millisecondMinValue, millisecondMinValue} and
    // +link{timeItem.millisecondIncrement, millisecondIncrement}.
    // @visibility external
    //<
    millisecondMaxValue: null,
    //> @attr timeItem.millisecondIncrement (Number : null : IRW)
    // When +link{timeItem.useTextField,useTextField} is false and
    // +link{timeItem.millisecondValues, millisecondValues} is unset, this attribute specifies
    // the increment to use when generating entries for the millisecond picker.  For example,
    // if this attribute is set to 5, the millisecond picker will contain only every fifth
    // value between the +link{timeItem.millisecondMinValue, millisecondMinValue} and
    // +link{timeItem.millisecondMaxValue, millisecondMaxValue}.
    // @visibility external
    //<
    millisecondIncrement: 1,
    getMillisecondMinValue : function () {
        if (this.millisecondMinValue == null) this.millisecondMinValue = 0;
        return this.millisecondMinValue;
    },
    getMillisecondMaxValue : function () {
        if (this.millisecondMaxValue == null) this.millisecondMaxValue = 999;
        return this.millisecondMaxValue;
    },

    getAmpmOptions : function () {
        if (this.ampmValues) return this.ampmValues;
        var values = ["AM", "PM"];
        return values;
    },

    // value setters

    //> @method timeItem.setHours() (A)
    // Set the hour value of this TimeItem.
    // @param hours (Number) new hours value for this TimeItem.
    // @visibility external
    //<
    setHours : function (hours) {
        if (this.useTextField) {
            var date = this.textField.getValue();
            date.setHours(hours);
            this.textField.setValue(date);
        } else {
            if (!this.getHourValues().contains(hours)) {
                this.logWarn("setHours: " + hours + " is not a valid option");
            } else {
                this.hourItem.setValue(hours);
            }
        }
    },

    //> @method timeItem.setMinutes() (A)
    // Set the minute value of this TimeItem.
    // @param minutes (Number) new minutes value for this TimeItem.
    // @visibility external
    //<
    setMinutes : function (minutes) {
        if (this.useTextField) {
            var date = this.textField.getValue();
            date.setMinutes(minutes);
            this.textField.setValue(date);
        } else {
            if (!this.getMinuteValues().contains(minutes)) {
                this.logWarn("setMinutes: " + minutes + " is not a valid option");
            } else {
                this.minuteItem.setValue(minutes);
            }
        }
    },

    //> @method timeItem.setSeconds() (A)
    // Set the seconds value of this TimeItem.
    // @param seconds (Number) new seconds value for this TimeItem.
    // @visibility external
    //<
    setSeconds : function (seconds) {
        if (this.useTextField) {
            var date = this.textField.getValue();
            date.setSeconds(seconds);
            this.textField.setValue(date);
        } else {
            if (!this.getSecondValues().contains(seconds)) {
                this.logWarn("setSeconds: " + seconds + " is not a valid option");
            } else {
                this.secondItem.setValue(seconds);
            }
        }
    },

    //> @method timeItem.setMilliseconds() (A)
    // Set the milliseconds value of this TimeItem.
    // @param milliseconds (Number) new milliseconds value for this TimeItem.
    // @visibility external
    //<
    setMilliseconds : function (milliseconds) {
        if (this.useTextField) {
            var date = this.textField.getValue();
            date.setMilliseconds(milliseconds);
            this.textField.setValue(date);
        } else {
            if (!this.getMillisecondValues().contains(milliseconds)) {
                this.logWarn("setMilliseconds: " + milliseconds + " is not a valid option");
            } else {
                this.millisecondItem.setValue(milliseconds);
            }
        }
    },

    // this is just the order in which the various fields should be displayed - use the
    // autoChild pattern (show[AutoChildName]) to control visibility of fields...
    selectorFormat: "HMSL",
    getSelectorFormat : function () {
        return this.selectorFormat;
    },

    //> @method timeItem.getDuration() (A)
    // When +link{timeItem.useTextField, useTextField} is set to false, this method
    // returns the value of the time expressed as a duration in the +link{TimeUnit, timeUnit}
    // provided.  If no timeUnit is passed, the default is the smallest unit for which a
    // picker is visible.
    // @param [timeUnit] (TimeUnit) the unit of the return value
    // @return (int) the item's value, expressed as a duration in the TimeUnit passed
    // @visibility external
    //<
    getDuration : function (timeUnit) {
        timeUnit = timeUnit || this.getDefaultDurationTimeUnit();

        var value = this.getValue();
        if (!isc.isA.Date(value)) return null;

        var hours = value.getHours(),
            minutes = value.getMinutes(),
            seconds = value.getSeconds(),
            milliseconds = value.getMilliseconds(),
            duration = (hours*60*60*1000)+(minutes*60*1000)+(seconds*1000)+milliseconds
        ;

        if (timeUnit == "second") return duration/1000;
        else if (timeUnit == "minute") return duration/1000/60;
        else if (timeUnit == "hour") return duration/1000/60/60;
        else return duration;
    },
    getDefaultDurationTimeUnit : function () {
        if (this.millisecondItem && this.millisecondItem.isVisible()) return "millisecond";
        if (this.secondItem && this.secondItem.isVisible()) return "second";
        if (this.minuteItem && this.minuteItem.isVisible()) return "minute";
        if (this.hourItem && this.hourItem.isVisible()) return "hour";
    },

    setItems : function (itemList) {

        var TI = isc.TimeItem,
            format = this.getSelectorFormat()
        ;

        if (itemList != null && itemList.length != 0) {
            this.logWarn("setItems() called for timeItem with itemList:" + itemList +
                            " - ignoring, and making use of default date fields");
        }

        // create a new itemList
        itemList = this.items = [];

        var is24Hour = this.showing24HourTime();

        if (this.useTextField) {
            // Setup properties that are being merged from the date item into the text field
            var mergeProperties = {
                textAlign: this.textAlign,
                emptyDisplayValue: this.emptyDisplayValue,
                invalidTimeStringMessage: this.invalidTimeStringMessage,
                operator: this.operator,
                title: this.title,
                allowEmptyValue: this.allowEmptyValue,
                defaultValue: this.defaultValue,
                validateOnChange: this.validateOnChange,
                validators: this.validators,
                type: this.type
            };
            if (this.showHintInField) {
                mergeProperties.showHintInField = this.showHintInField;
                mergeProperties.hint = this.hint;
                this.hint = null;
            }

            var maskProperties = {
                timeFormatter: this.timeFormatter,
                useMask: this.useMask,
                formatterMap: this.formatterMap
            };
            /*
            if (this.useMask) {
                var inputFormat = this.getInputFormat();
                // Default to US date format
                if (!inputFormat) inputFormat = "MDY";

                var mask = this.getInputFormatMask(inputFormat);

                maskProperties.mask = mask;
                maskProperties.maskSaveLiterals = true;
                maskProperties.maskOverwriteMode = true;

                // Display format must match input so we force it here
                if (this.inputFormat) {

                    this.dateFormatter = this._maskDisplayFormats[inputFormat];
                }
            }
            */

            var textField = isc.addProperties(mergeProperties,
                                              this.textFieldDefaults,
                                              TI.TEXT_FIELD,
                                              this.textFieldProperties,
                                              maskProperties);
            // Ensure no one overrode the name of the dtf!
            textField.name = "timeTextField";
            // If we have a specified height, expand the text box to fill the available space

            if (this.height && (!this.textFieldProperties || !this.textFieldProperties.height))
            {
                textField.height = this.getTextBoxHeight();
            }

            if (this.parseEditorValue) textField.parseEditorValue = this.parseEditorValue;

            itemList.add(textField);

            //>EditMode for dynamically changing useTextField

            var undef;
            this.hourItem = this.minuteItem = this.secondItem = this.millisecondItem = this.ampmItem = undef;
            //<EditMode

        } else {


            // iterate through the characters of the format
            for (var i = 0; i < format.length; i++) {
                var field = format.charAt(i);
                // assigning the selector for that format to the itemList
                var item = null;

                if (field == "H" && this.showHourItem != false) {
                    item = isc.addProperties({title: this.hourItemTitle,
                                titleStyle: this.titleStyle,
                                prompt: this.hourItemPrompt},
                                this.hourItemDefaults, TI.HOUR_ITEM,
                                this.hourItemProperties, {name: "hourItem"}
                    );
                } else if (field == "M" && this.showMinuteItem != false) {
                    item = isc.addProperties({title: this.minuteItemTitle,
                                titleStyle: this.titleStyle,
                                prompt: this.minuteItemPrompt},
                                this.minuteItemDefaults, TI.MINUTE_ITEM,
                                this.minuteItemProperties, {name: "minuteItem"}
                    );
                } else if (field == "S" && this.showSecondItem != false) {
                    item = isc.addProperties({title: this.secondItemTitle,
                                titleStyle: this.titleStyle,
                                prompt: this.secondItemPrompt },
                                this.secondItemDefaults, TI.SECOND_ITEM,
                                this.secondItemProperties, {name: "secondItem"}
                    );
                } else if ((field == "L" || field == "m") && this.showMillisecondItem != false) {
                    item = isc.addProperties({ title: this.millisecondItemTitle,
                                titleStyle: this.titleStyle,
                                prompt: this.millisecondItemPrompt },
                                this.millisecondItemDefaults, TI.MILLISECOND_ITEM,
                                this.millisecondItemProperties, {name: "millisecondItem"}
                    );
                }

                if (item) {
                    // Leave a gap between items via left-padding

                    if (itemList.length > 0) {
                        if (item.cssText == null) {
                            item.cssText = "padding-left:3px;";
                        }
                    }
                    itemList.add(item);
                }
            }

            // show the AM/PM picker as appropriate
            if (!this.showing24HourTime()) {
                item = isc.addProperties({title: this.ampmItemTitle},
                        this.ampmItemDefaults, TI.AMPM_SELECTOR,
                        this.ampmItemProperties, {name: "ampmItem"});
                if (item.cssText == null) {
                    item.cssText = "padding-left:3px;";
                }
                itemList.add(item);
            }
        }

        // call the superclass routine to properly set the items
        this.Super("setItems", [itemList], arguments);


        if (this.useTextField) {
            this.textField = this.timeTextField;
        }
    }
});


isc.TimeItem.addMethods({
    _shouldAllowExpressions : function () {
        // Value expressions cannot be entered unless using a text field.
        if (!this.useTextField) return false;

        // Turn on value expressions support by default because it is able to handle plain values
        // in a more human-friendly way. For example, if the user types "11:59", then values where
        // the seconds are not zero are included if value expressions are allowed.
        return (this.allowExpressions != false);
    },

    // default to equals if this.operator is unset, rather than being sensitive to textMatchStyle
    getOperator : function (textMatchStyle) {
        if (!this.operator) return "equals";
        return this.operator;
    },

    short24TimeFormat:"HH:MM",
    shortTimeFormat:"HH:MM [am|pm]",
    long24TimeFormat:"HH:MM:SS",
    longTimeFormat:"HH:MM:SS [am|pm]",
    full24TimeFormat:"HH:MM:SS.lll",
    fullTimeFormat:"HH:MM:SS.lll [am|pm]",
    //>@method timeItem.getHint()
    // If <code>this.hint</code> is specified, display it as a hint. Otherwise a string display
    // the format of the time string, based on <code>this.timeFormatter</code>.
    // Developer may set a <code>hint</code> value or override this method to show a different
    // hint value.
    //<
    getHint : function () {
        if (!this.showHint) return "";
        if (this.hint != null) return this.hint;
        if (!this.useTextField) return "";
        var formatter = this._getTimeFormatter();
        switch (formatter) {
            case "to24HourTime":
            case "toPadded24HourTime":
                return this.long24TimeFormat;
            case "toTime":
            case "toPaddedTime":
                return this.longTimeFormat;
            case "toShort24HourTime":
            case "toShortPadded24HourTime":
                return this.short24TimeFormat;
            case "toShortTime":
            case "toShortPaddedTime":
                return this.shortTimeFormat;
            case "toFullTime":
            case "toFullPaddedTime":
                return this.fullTimeFormat;
        }
        return "";
    },

    //>    @method    timeItem.getDefaultValue()    (A)
    //  If no default value is set for this item, and <code>this.allowEmptyValue</code> is false
    //  this method will create a default time value based on TimeItem.DEFAULT_TIME.
    // @group defaults
    // @return (Time) default Time value
    //<
    getDefaultValue : function () {
        var defaultValue = this.defaultValue;
        if (!defaultValue && !this.allowEmptyValue) defaultValue = isc.TimeItem.DEFAULT_TIME;
        if (defaultValue && !isc.isA.Date(defaultValue))
            defaultValue = isc.Time.parseInput(defaultValue);

        return defaultValue;
    },

    getCriteriaValue : function () {
        if (this._shouldAllowExpressions()) return this.textField.getElementValue();
        var value = this.getValue();
        if (value == null || isc.is.emptyString(value)) return null;
        if (!isc.isA.Date(value)) value = isc.Time.parseInput(String(value));
        return value;
    },

    // Override getCriterion() to return a betweenInclusive `Criterion' to ignore the seconds
    // and milliseconds unless the textMatchStyle is "exact".

    getCriterion : function (textMatchStyle) {
        var crit,
            operator = this.getOperator(textMatchStyle, isc.isAn.Array(value)),
            fieldName = this.getCriteriaFieldName();
        var value = this.getCriteriaValue();

        if (value == null || isc.is.emptyString(value)) return;

        if (this._shouldAllowExpressions()) {
            crit = this.parseValueExpressions(value, fieldName, operator);
            if (crit != null) {
                // We need to make sure that if the user enters the expression "==11:59pm" or
                // "11:59pm", then time values with non-zero seconds will still be included.
                // If the `Criterion' operator is "equals", then fall through to the code that
                // constructs a "betweenInclusive" advanced criteria.
                if ("equals" != crit.operator) {
                    return crit;
                }
                // Use the parsed value for the case where the expression is "==...". The
                // parsed value is the sought-after `Date' whereas `value' is the expression
                // string, which will fail to be parsed by Time.parseInput().
                value = crit.value;
            }
        }

        if (isc.isA.String(value)) {
            value = isc.Time.parseInput(value);
        }

        if (isc.isA.Date(value) && "exact" != textMatchStyle) {
            // assert value.logicalTime;
            crit = {
                _constructor: "AdvancedCriteria",
                fieldName: fieldName,
                operator: "betweenInclusive",
                start: isc.Time.createLogicalTime(value.getHours(), value.getMinutes(), 0, 0),
                end: isc.Time.createLogicalTime(value.getHours(), value.getMinutes(), 59, 999),
                value: value
            };
            return crit;
        }

        crit = {
            fieldName: fieldName,
            operator: operator,
            value: value
        };
        return crit;
    },

    canEditCriterion : function (criterion, warnOnField) {
        if (criterion.fieldName != null && criterion.fieldName == this.getCriteriaFieldName() &&
            isc.isA.Date(criterion.value))
        {
            return true;
        }
        return this.Super("canEditCriterion", arguments);
    },

    _formatCriterionValue : function (value) {
        return this.mapValueToDisplay(value);
    },

    //>    @method    timeItem.mapValueToDisplay()    (A)
    // Maps this items value (a date) to a formatted string to display. Uses one of the
    // Time class's formatters to perform the conversion
    // @group    drawing
    // @param    value        (date|null)    value of this item
    // @return    (string)    Time value as a formatted time string
    //<
    mapValueToDisplay : function (value) {
        if (this.allowEmptyValue && (value == null || isc.isAn.emptyString(value))) return isc.emptyString;
        if (!isc.isA.Date(value)) {
            // Try to parse `value' as a time. If `value' can be parsed as a time, go ahead
            // and format it using the time formatter.
            var date = isc.Time.parseInput(String(value), true);
            if (date != null) value = date;
            else return value;
        }

        var displayValue = isc.Time.format(value, this._getTimeFormatter());
        displayValue = this.Super("mapValueToDisplay", displayValue);
        return displayValue;
    },

    //>    @method    timeItem.mapDisplayToValue()    (A)
    // Maps the string displayed in this item to a Date object with the appropriate time,
    // using the Time class's formatters.
    // @group drawing
    // @param value (string) String value stored in the form field.
    // @return (date|null) Date value to store with the appropriate time set.
    //<
    mapDisplayToValue : function (value) {
        var saveValue = this.Super("mapDisplayToValue", arguments);
        if ((saveValue == null || isc.isAn.emptyString(saveValue)) && this.allowEmptyValue) {
            saveValue = null;
        } else {
            var date;
            if (this.parseEditorValue) {
                date = this.parseEditorValue(value, this.form, this);
            }

            if (date == null) date = isc.Time.parseInput(String(value), true);

            if (date == null) return value;

            // Returns a date instance
            saveValue = date;
        }
        return saveValue;
    },

    parseTimeString : function (value) {
        if (value == null || isc.isAn.emptyString(value)) return null;

        var dateValue = isc.Time.parseInput(value),
            h, m, s, ms
        ;
        h = m = s = ms = 0;

        if (isc.isA.Date(dateValue)) {
            h = dateValue.getHours();
            m = dateValue.getMinutes();
            s = dateValue.getSeconds();
            ms = dateValue.getMilliseconds();
        }

        if (h+m+s+ms == 0) {
            // find milliseconds
            var parts = value.split(".");
            if (parts.length == 2) ms = parts[1];

            // find other time-parts
            parts = parts[0].split(":");
            if (parts.length > 2) s = parts[2]; // seconds
            if (parts.length > 1) m = parts[1]; // minutes
            h = parts[0];
        }

        var timeObject = isc.Date.createLogicalTime(h, m, s, ms);
        return timeObject;
    },

    // Override updateValue to verify that the contents of the element(s) make a valid time.
    updateValue : function () {
        // _suppressUpdates flag set when we're in the process of setting our sub items' values
        // to represent a known, valid date.

        if (this._suppressUpdates) return;

        // We're likely to manipulate the values of the form items as this method runs - avoid
        // re-running updateValue in response to 'saveValue()' on the sub items.
        this._suppressUpdates = true;

        var date;
        if (this.useTextField) {

            // Note: this method is called from "saveValue()" on the sub-items (after saving out
            // their values) so typically the sub item values will be up to date.
            // However this method may also be called externally while the text item is pending
            // an update (from blur [or keypress]).
            // Call updateValue() to ensure the text field value matches the current element
            // value for that field.
            this.timeTextField.updateValue();
            var value = this.timeTextField.getValue(),
                invalidDate;

            if (value == isc.emptyString || value == null) date = null;
            else if (isc.isA.Date(value)) date = value;
            else if (this._shouldAllowExpressions()) {
                date = isc.Time.parseInput(String(value), true);
                if (date == null) date = value;
            } else {
                // This will return a null value if the date string is invalid.
                // If enforceDate is false we allow a dateItem to be set to a non-date value
                // though typically validation would fail for the field if it's data-type was
                // date
                // If enforce date is true, accept this value, but show a validation error

                date = this.parseEditorValue ? this.parseEditorValue(value) : this.parseTimeString(value);
            }

        } else {
            var hour, min, sec, ms;

            if (this.hourItem != null) {
                hour = this.hourItem.getValue();
            }
            if (this.minuteItem != null) {
                min = this.minuteItem.getValue();
            }
            if (this.secondItem != null) {
                sec = this.secondItem.getValue();
            }
            if (this.millisecondItem != null) {
                ms = this.millisecondItem.getValue();
            }
            if (this.ampmItem != null) {
                // if it's PM and hour < 12, add 12 to hour - if it's AM, and hour is 12, make it 0
                var ampmValue = this.ampmItem.getValue(),
                    valueMap = this.getAmpmOptions()
                ;
                if (ampmValue == valueMap[1] && hour < 12) hour += 12;
                else if (ampmValue == valueMap[0] && hour == 12) hour = 0;
            }

            date = isc.Date.createLogicalTime(hour, min, sec, ms);
        }
        delete this._suppressUpdates;
        // bail if the value hasn't changed
        if (this.compareValues(date, this._value) == true) return false;

        // now fire the default handlers:
        if (this.handleChange(date, this._value) == false) return;

        // In case the change handler modified the date
        date = this._changeValue;

        // save the value
        this.saveValue(date);

        // fire the 'changed' handler
        this.handleChanged(date);
    },

    // Override setValue() - if passed a string, map it to the appropriate date before saving
    // (this is required since the string passed in won't go through 'mapDisplayToValue')
    setValue : function (value) {
        var newValue = value;
        if (isc.isA.String(newValue)) {
            if (!this._shouldAllowExpressions()) newValue = this.parseTimeString(newValue);
        }

        if (this.useTextField) {
            this.textField.setValue(value);
        } else if (isc.isA.Date(newValue)) {
            // setup the various SelectItems for the time-parts
            var hours = newValue.getHours(),
                minutes = newValue.getMinutes(),
                seconds = newValue.getSeconds()
            ;
            if (!this.showing24HourTime()) {
                if (hours > 11) hours -= 12;
                if (hours == 0) hours = 12;
            }
            if (hours < 10) hours = isc.NumberUtil.stringify(hours, 2);
            if (this.hourItem) this.hourItem.setValue(hours);
            if (minutes < 10) minutes = isc.NumberUtil.stringify(minutes, 2);
            if (this.minuteItem) this.minuteItem.setValue(minutes);
            if (seconds < 10) seconds = isc.NumberUtil.stringify(seconds, 2);
            if (this.secondItem) this.secondItem.setValue(seconds);
            if (this.millisecondItem) this.millisecondItem.setValue(newValue.getMilliseconds());
            var item = this.ampmItem,
                valueMap = this.getAmpmOptions()
            ;
            if (item) item.setValue(newValue.getHours() > 11 ? valueMap[1] : valueMap[0]);
        }

        return this.Super("setValue", [newValue], arguments);
    },

    getValue : function () {
        if (this.useTextField) {
            var value = this.textField.getValue();
            var date = isc.Time.parseInput(String(value), true);
            if (date != null) value = date;
            return value;
        }

        var h = this.hourItem ? this.hourItem.getValue() : 0,
            m = this.minuteItem ? this.minuteItem.getValue() : 0,
            s = this.secondItem ? this.secondItem.getValue() : 0,
            ms = this.millisecondItem ? this.millisecondItem.getValue() : 0
        ;

        var item = this.ampmItem;
        if (item != null) {
            // if it's PM and h < 12, add 12 to h - if it's AM, and h is 12, make it 0
            var ampmValue = item.getValue(),
                valueMap = this.getAmpmOptions()
            ;
            if (ampmValue == valueMap[1] && h < 12) h += 12;
            else if (ampmValue == valueMap[0] && h == 12) h = 0;
        }

        return isc.Date.createLogicalTime(h, m, s, ms);
    },

    //> @method timeItem.getEnteredValue()
    // Returns the raw text value typed into this items text field if +link{timeItem.useTextField}
    // is true (otherwise returns the result of this.getValue()).
    // @return (String) value the user entered
    // @visibility external
    //<
    getEnteredValue : function () {
        if (this.useTextField && this.textField != null) {
            return this.textField.getEnteredValue();
        }
        return this.getValue();
    },

    // Override compareValues - we want to compare date values as times
    compareValues : function (value1, value2) {
        if (value1 == value2) return true;
        if (isc.isA.Date(value1) && isc.isA.Date(value2) &&
             isc.Time.compareTimes(value1, value2)) return true;

        return false;
    },

    formatterMap: {
        toTime:{mask:"[0-1]#:[0-6]#:[0-6]# [ap]m", formatter:"toPaddedTime"},
        to24HourTime:{mask:"[0-2]#:[0-6]#:[0-6]#", formatter:"toPadded24HourTime"},

        toPaddedTime:{mask:"[0-1]#:[0-6]#:[0-6]# [ap]m"},
        toPadded24HourTime:{mask:"[0-2]#:[0-6]#:[0-6]#"},

        toShortTime:{mask:"[0-1]#:[0-6]# [ap]m", formatter:"toShortPaddedTime"},
        toShort24HourTime:{mask:"[0-2]#:[0-6]#", formatter:"toShortPadded24HourTime"},

        toShortPaddedTime:{mask:"[0-1]#:[0-6]# [ap]m"},
        toShortPadded24HourTime:{mask:"[0-2]#:[0-6]#"}
    },
    init : function() {
        if (!this.use24HourTime) {
            this.timeFormatter = "toShortTime"
        }
        this.Super("init", arguments);
    },

    //> @attr   timeItem.mask   (string : null : IRWA)
    // Internal-use only for a TimeItem.
    // @see attr:timeItem.useMask
    // @visibility  external
    //<
    //> @attr   timeItem.maskSaveLiterals   (Boolean : null : IRWA)
    // Internal-use only for a TimeItem.
    // @see attr:timeItem.useMask
    // @visibility  external
    //<
    //> @attr   timeItem.maskOverwriteMode   (Boolean : null : IRWA)
    // Internal-use only for a TimeItem.
    // @see attr:timeItem.useMask
    // @visibility  external
    //<

    //> @method timeItem.setMask ()
    // A custom mask cannot be defined for a time item.
    // @see attr:timeItem.useMask
    // @visibility external
    //<
    setMask : function (mask) {
        this.logWarn("setMask: custom mask ignored");
    },


    _canFocus : function () {
        if (this.canFocus != null) return this.canFocus;
        return true;
    },

    // Override focusInItem to focus in the appropriate sub-item
    focusInItem : function () {
        if (!this.isVisible()) return;

        if (this.useTextField) {
            if (this.textField) this.textField.focusInItem();
        } else {
            var format = this.getSelectorFormat(),
                // Format will be "DMY" / "YMD" / "MDY" / etc.
                // (Parse the string rather than comparing with the DateItem.DAY_MONTH_YEAR class
                // constants - it's slower but will support the user specifying just "MY" or something)
                firstSelector = format.charAt(0)
            ;

            if (firstSelector == "H" && this.hourItem) this.hourItem.focusInItem();
            if (firstSelector == "M" && this.minuteItem) this.minuteItem.focusInItem();
            if (firstSelector == "S" && this.secondItem) this.secondItem.focusInItem();
            if (firstSelector == "L" && this.millisecondItem) this.millisecondItem.focusInItem();
        }
        // If it couldn't find the appropriate sub-item, this method is a no-op
    },

    // override get/setSelectionRange - if we're showing a text field, call through to the
    // methods on that sub-item

    //> @method timeItem.setSelectionRange()
    // If +link{timeItem.useTextField} is true, falls through to standard
    // +link{formItem.setSelectionRange()} implementation on this items freeform text entry field.
    // Otherwise has no effect.
    // @param start (int) character index for start of new selection
    // @param end (int) character index for end of new selection
    // @visibility external
    //<
    setSelectionRange : function (start,end) {
        if (this.textField) return this.textField.setSelectionRange(start,end);
    },

    //> @method timeItem.getSelectionRange()
    // If +link{timeItem.useTextField} is true, falls through to standard
    // +link{formItem.getSelectionRange()} implementation on this items freeform text entry field.
    // Otherwise has no effect.
    // @return (array) 2 element array indicating start/end character index of current selection
    //  within our text entry field. Returns null if this item is undrawn or doesn't have focus.
    // @visibility external
    //<
    getSelectionRange : function () {
        if (this.textField) return this.textField.getSelectionRange();
    },

    //> @method timeItem.selectValue()
    // If +link{timeItem.useTextField} is true, falls through to standard
    // +link{formItem.selectValue()} implementation on this items freeform text entry field.
    // Otherwise has no effect.
    // @visibility external
    //<
    selectValue : function () {
        if (this.textField) return this.textField.selectValue();
    },

    //> @method timeItem.deselectValue()
    // If +link{timeItem.useTextField} is true, falls through to standard
    // +link{formItem.deselectValue()} implementation on this items freeform text entry field.
    // Otherwise has no effect.
    // @param [start] (Boolean) If this parameter is passed, new cursor insertion position will be
    //   moved to the start, rather than the end of this item's value.
    // @visibility external
    //<
    deselectValue : function (start) {
        if (this.textField) return this.textField.deselectValue()
    },

    setHint : function (hintText) {
        if (this.useTextField && this.showHintInField) {
            this.textField.setHint(hintText);
        } else {
            this.Super("setHint", arguments);
        }
    },

    validators: [
        { type: "isTime" }
    ]

});








//>    @class    ToolbarItem
//
// Set of horizontally arranged buttons.
//
// @visibility external
//<
isc.ClassFactory.defineClass("ToolbarItem", "CanvasItem");

isc.ToolbarItem.addProperties({

    // Override canFocus -- even though toolbars have no data element, they can accept focus.
    canFocus:true,

    // Avoid attempting to save this item's value in the values array.
    shouldSaveValue:false,

    // Toolbars are non editable
    isEditable : function () {
        return false;
    },

    //>    @attr    toolbarItem.buttons        (Array : null : IRW)
    //  List of buttons for the toolbar.
    //  Each buttons should be specified as a simple JS object with properties to apply to the
    //  button to be displayed. Note that any <code>click</code> stringMethod applied to the
    //  button will be passed 2 parameters: <code>form</code> and <code>item</code>.
    //        @group    items
    // @visibility external
    //<

    //>    @attr    toolbarItem.showTitle        (Boolean : false : IRW)
    //    Don't show a title for toolbars
    //        @group    appearance
    // @visibility external
    //<
    showTitle:false,


    //>    @attr    toolbarItem.buttonSpace     (number : 4 : IRW)
    //  Space between buttons.
    //        @group    appearance
    //<
    buttonSpace:4,

    //>    @attr    toolbarItem.startRow        (Boolean : true : IRW)
    //            these items are in a row by themselves by default
    //        @group    appearance
    // @visibility external
    //<
    startRow:true,

    //>    @attr    toolbarItem.endRow            (Boolean : true : IRW)
    //            these items are in a row by themselves by default
    //        @group    appearance
    // @visibility external
    //<
    endRow:true,

    //>    @attr    toolbarItem.colSpan            (boolean : true : IRW)
    //            these items span all columns
    //        @group    appearance
    // @visibility external
    //<
    colSpan:"*",

    //>@attr    toolbarItem.canvasConstructor  (Class : Button : IRA)
    //  Our toolbar will be rendered out as a toolbar widget.
    //  @visibility internal
    //<
    canvasConstructor:isc.Toolbar,

    //>@attr    toolbarItem.vertical    (Boolean    : false : IRA)
    //  Should the toolbar stack its buttons vertically or horizontally?
    //  @visibility external
    //<
    vertical:false,

    //>@attr    toolbarItem.buttonConstructor   (Class : isc.AutoFitButton : IRWA)
    //  Use AutoFitButtons rather than simple Buttons for this toolbar's buttons by default.
    //  @visibility external
    //<
    buttonConstructor:isc.AutoFitButton

    //>@attr    toolbarItem.buttonProperties   (object : null : IRA)
    //  Default properties for this toolbar's buttons.
    //  @visibility external
    //<


    //>    @attr    toolbarItem.buttonBaseStyle        (CSSStyleName : null : IRW)
    //  If specified this baseStyle will be applied to the buttons in this toolbar.
    //        @group    appearance
    //      @visibility external
    //<
    //buttonBaseStyle:null,

    //>    @attr    buttonItem.buttonTitleStyle       (CSSStyleName : "buttonItem" : IRW)
    //  CSS class to apply to the buttons' titles.
    //      @visibility internal
    //<



});

isc.ToolbarItem.addMethods({

    _createCanvas : function () {

        var buttons = (this.buttons || this.items || []),
            buttonProperties = this.buttonProperties || {};

        isc.addProperties(buttonProperties, {
            // override handleClick to fire 'click' passing in 'form' and 'item' as params
            handleActivate : function () {
                var item = this.parentElement.canvasItem,
                    form = item.form;
                if (this.click != null) return this.click(form, item);
            }
        });


        if (this.buttonBaseStyle && !buttonProperties.baseStyle) {
            buttonProperties.baseStyle = this.buttonBaseStyle;
        }
        if (this.buttonTitleStyle && !buttonProperties.titleStyle) {
            buttonProperties.titleStyle = this.buttonTitleStyle;
        }

        // Update properties on the individual button init blocks as required.
        this._updateButtons(buttons);

        this.canvas = { overflow:isc.Canvas.VISIBLE,
                        buttons:buttons,
                        membersMargin:this.buttonSpace,
                        vertical:this.vertical,
                        // Button defaults are applied via Toolbar.button property
                        buttonProperties:buttonProperties };

        var height = this.height,
            width = this.width;
        if (!isc.isA.Number(width) && this._size)
            width = isc.isA.Number(this._size[0]) ? this._size[0] : null;
        if (!isc.isA.Number(height) && this._size)
            height = isc.isA.Number(this._size[1]) ? this._size[1] : null;

        if (height) this.canvas.height = height;
        if (width) this.canvas.width = width;

        if (this.buttonConstructor != null)
            this.canvas.buttonConstructor = this.buttonConstructor;

        return this.Super("_createCanvas", arguments);
    },

    // Helper function to update the button init blocks before creating them as button instances
    _updateButtons : function (buttons) {
        if (!buttons || buttons.length == 0) return;
        // override 'click' on button init blocks to always be functions taking 'form,item'
        // as params
        for (var i = 0; i < buttons.length; i++) {
            if (buttons[i].click && isc.isA.String(buttons[i].click)) {
                buttons[i].click = isc.Func.expressionToFunction("form,item", buttons[i].click);
            }
        }

    },

    //>EditMode dynamic addition/removal of buttons
    getButton : function (button) {
        return isc.Class.getArrayItem(button, this.buttons, "name");
    },
    addButton : function (button, index) {
        this.buttons = this.buttons || [];
        this.buttons.addAt(button, index);
        this.setButtons(this.buttons);
    },
    removeButton : function (button) {
        var index = isc.Class.getArrayItemIndex(button, this.buttons, "name");
        if (index != -1) {
            this.buttons.removeAt(index);
            this.setButtons(this.buttons);
        }
    },
    //<EditMode

    setButtons : function (buttons) {
        this._updateButtons(buttons);
        this.buttons = buttons;
        // if we're not initialized, no-op since the buttons will get set up with _createCanvas
        // when we init
        if (!this.canvas) return;

        this.canvas.setButtons(buttons);
    },

    // Allow setItems as well as setButtons for back-compat
    setItems : function (buttons) {
        return this.setButtons(buttons);
    }

});









//>    @class UploadItem
//
// FormItem that creates an HTML &lt;input type="file"&gt; control, with an interface that
// allows a user to pick a file from his machine to upload to the server.
// <P>
// <b>NOTE:</b> use +link{FileItem}, <b>not</b> UploadItem, if you are using the SmartClient
// Server framework.  FileItem is much easier to use and addresses all the limitations of
// UploadItem discussed below.  See the +link{group:upload,Uploading Files} overview for
// details.
// <P>
// If a form containing an UploadItem is +link{canvas.redraw(),redrawn} (which may
// happen if other form items are shown or hidden, the form is
// +link{canvas.redrawOnResize,resized}, or other items show validation errors) then the value
// in the upload item is lost (because an HTML upload field may not be created with a value).
// For this reason, if you are building a form that combines an UploadItem with other FormItems
// that could trigger redraw()s, recommended practice is to place each UploadItem in a distinct
// DynamicForm instance and create the visual appearance of a single logical form via combining
// the DynamicForms in a +link{Layout}.
// <P>
// <B>NOTE: Browser-specific behaviors:</B>
// <ul>
// <li> while getDisplayValue() can be used to retrieve the filesystem path of the uploaded file on some
// browsers, different browsers will return either just the file name without path or the full path.  It
// is plausible that some browsers may switch behavior in the future to not supply this value at all.  Do
// not rely on this value.
// <li> the appearance of the UploadItem is not consistent across browsers and we do not
// recommend trying to make it consistent or trying to apply styling to the upload control at all.  It is
// a potential security problem if an end user is unable to reliably recognize the upload control, hence,
// all browsers limit what styling can be applied.  Various hacks exists to get further control of
// styling, but it is likely these hacks will be broken by browser upgrades in the future.
// </ul>
//
// @group upload
// @visibility external
//<


isc.ClassFactory.defineClass("UploadItem", "TextItem");
isc.UploadItem.addProperties({
    _elementType:"FILE",

    // _nativeEventHandlers is a place to specify native event handlers to be applied to the
    // form item element once it has been written into the DOM (without having to override
    // '_applyHandlersToElement()'
    _nativeEventHandlers : {
        // apply a native 'onchange' hander to notify us of changes.

        onchange : isc.FormItem._nativeChangeHandler
    },

    // this flag causes FormItem to set multiple="true" for the element in the DOM
    _propagateMultiple: true,

    //> @attr uploadItem.multiple (Boolean : true : [IR])
    // @include fileItem.multiple
    // @visibility external
    //<
    multiple: true,

    //> @attr uploadItem.accept (String : null : [IR])
    // @include fileItem.accept
    // @visibility external
    //<

    maintainSelectionOnTransform: false
});


isc.UploadItem.addMethods({




    writeTextBoxWidth: isc.Browser.isMoz && isc.Browser.version >= 22,

    //> @attr uploadItem.width (number : 150 : IRW)
    // Width for this uploadItem. Note that SmartClient will not apply this size to the
    // native HTML &lt;input ...&gt; element written out by this formItem as this leads to
    // inconsistent appearance across different browsers. The specified width
    // acts as a minimum cell width for the item.
    // @visibility external
    //<
    //> @attr uploadItem.height (number : 19 : IRW)
    // Height for this uploadItem. Note that SmartClient will not apply this size to the
    // native HTML &lt;input ...&gt; element written out by this formItem as this leads to
    // inconsistent appearance across different browsers. The specified height
    // acts as a minimum cell width for the item.
    // @visibility external
    //<

    shouldFixRowHeight:function () {
        return true;
    },
    shouldWriteTextBoxWidth:function () {
        if (this.writeTextBoxWidth != null) return this.writeTextBoxWidth;
        return false;
    },
    shouldWriteTextBoxHeight:function () {
        if (this.writeTextBoxHeight != null) return this.writeTextBoxHeight;
        return false;
    },
    getTextBoxWidth : function () {
        if (!this.shouldWriteTextBoxWidth()) return null;
        return this.Super("getTextBoxWidth", arguments);
    },
    getTextBoxHeight : function () {
        if (!this.shouldWriteTextBoxHeight()) return null;
        return this.Super("getTextBoxHeight", arguments);
    },


    _sizeTextBoxAsContentBox : function () {
        return false;
    },


    _getEventMaskWidth : function () {
        var width = this.getElementWidth();
        if (!isc.isA.Number(width)) width = 185;
        return width;
    },

    // Override _updateValue - if the change handler etc attempts to modify the value
    // log a warning that we can't support this and save the value the user entered.
    _updateValue : function (newValue) {
        // unmap the value if necessary
        newValue = this.mapDisplayToValue(newValue);

        // Bail if we have already saved the value (avoids firing change on arrow keypresses,
        // etc.)
        if (newValue == this._value) return true;

        // fire the change handler, and bail if the change failed validation, etc.
        // Note: this method will call 'setValue()' to reset to the old value, or any value
        // suggested by the validators
        var returnVal = this.handleChange(newValue, this._value);

        // UploadItems don't support setting the value. If a change handler returned false, or
        // we otherwiser attempted to change the value, log a warning and drop that changed val
        if (this._changeValue != newValue) {
            this.logWarn("Upload Items do not support programmatically modifying the value entered " +
                         "by the user. Ignoring attempt to update from change handler");
        }

        // save the value

        this.saveValue(this.mapDisplayToValue(this.getElementValue()));
        delete this._changeValue;

        this.handleChanged(this._value);
        return returnVal;
    },

    redrawn : function () {
        this.Super("redrawn", arguments);
        this.updateValue(this.getElementValue());
    },

    //> @method uploadItem.setElementReadOnly()
    // Change the read-only state of the form element immediately.
    //<
    setElementReadOnly : function (readOnly) {
        // The element is actually a "FILE" and only has a disabled state.
        this._setElementEnabled(!readOnly && !this.isDisabled());
    },

    //>    @method    uploadItem.setValue()
    // Attempting to set the value for an upload form item is disallowed for security reasons.
    // Therefore this method will just log a warning, and not modify the value of the item.
    // @visibility external
    //<
    setValue : function (newValue) {
        var val = this.getValue();
        if (newValue == null || isc.isAn.emptyString(newValue)) {
            if (val == null || isc.isAn.emptyString(val)) return;
            return this.Super("setValue", arguments);
        }

        if (newValue != val) {
            this.logWarn("Attempting to set the value for an upload form item. This is disallowed " +
                         "for security reasons - returning the current value of the form item");
        }
        return val;
    },

    _handleElementChanged : function () {
        this.Super("_handleElementChanged", arguments);
        this.checkForImplicitSave();
    },

    //>    @method    uploadItem.setElementValue()
    //        @group    elements
    //            Override setElementValue to explicity NOT manipulate the form element
    //            in Nav, since this is likely to crash.
    //        @param    newValue     (any)                value to set the element to
    //<
    setElementValue : function (newValue) {
        // we CAN clear an upload item's value, but not set it to anything else.
        if (newValue == null || isc.isAn.emptyString(newValue)) {
            // Moz and Safari do allow setting element value to "" but IE does not
            // However redrawing a form with an uploadItem in it will clear its value natively
            if (isc.Browser.isIE) {
                this.redraw();
                return;
            }
            return this.Super("setElementValue", arguments);
        }

        var val = this.getElementValue();
        if (newValue != val) {
            this.logInfo("Attempting to set the value for an upload form item. This is disallowed " +
                         "for security reasons - returning the current value of the form item");
        }

        // don't try to set the value of an upload field.  This should not be allowed by the
        // browser, as part of its security model, and attempting it will cause errors in some
        // browsers.
        return val;
    },

    // Make 'refreshDisplayValue' a no-op - there's no way to reformat the data value for display
    // for an upload item - it'd fall through to setElementValue().

    refreshDisplayValue:function () {
    },

    _shouldAllowExpressions : function () {
        return false;
    },



    _shouldSelectOnFocus : function () {
        return false;
    }

});


isc.UploadItem.addProperties({
    //> @method uploadItem.getSelectionRange()
    // <strong>This method is not supported</strong> by <code>UploadItem</code>.
    // @return (Array of int) <code>null</code>
    // @visibility external
    //<
    _isGetSelectionRangeSupported: false,
    getSelectionRange : function () {
        this.logWarn("UploadItem does not support the getSelectionRange() method.");
    },

    //> @method uploadItem.setSelectionRange()
    // <strong>This method is not supported</strong> by <code>UploadItem</code>.
    // @visibility external
    //<
    _isSetSelectionRangeSupported: false,
    setSelectionRange : function () {
        this.logWarn("UploadItem does not support the setSelectionRange() method.");
    },

    //> @method uploadItem.selectValue()
    // <strong>This method is not supported</strong> by <code>UploadItem</code>.
    // @include TextItem.selectValue()
    //<
    selectValue : function () {
        this.logWarn("UploadItem does not support the selectValue() method.");
    },

    //> @method uploadItem.deselectValue()
    // <strong>This method is not supported</strong> by <code>UploadItem</code>.
    // @include TextItem.deselectValue()
    //<
    deselectValue : function () {
        this.logWarn("UploadItem does not support the deselectValue() method.");
    }
});








//>    @class        ComboBoxItem
// The Combobox is a text input field which can show a list of options via a drop-down
// PickList.
// <p>
// The set of options will be filtered based on the current value in the text field, so only
// options that match what has been typed so far will be displayed.
// The set of options can be derived from a ValueMap or dynamically retrieved from a
// dataSource.  See the +link{interface:PickList} interface for further settings.
// <P>
// The two most common use cases for ComboBoxItems are:
// <ul><li>With +link{comboBoxItem.addUnknownValues} set to true, the ComboBoxItem acts as a
//  freeform text entry field with the picklist providing essentially a set of suggested completions
//  similar to a URL bar in a web browser.</li>
//     <li>With +link{comboBoxItem.addUnkownValues} set to false, the ComboBoxItem acts similarly
//  to a SelectItem where a fixed set of options is available to the user and the text entry field
//  is essentially used to filter which of these options are visible</li></ul>
// <P>
// Other commonly used settings to configure ComboBoxItem behavior are:<br>
// - +link{ComboBoxItem.defaultToFirstOption} - this will select the first option from the pickList
// as a default value for the item - and<br>
// - +link{ComboBoxItem.completeOnTab} which causes the
// current selection in the pickList (if there is one) to be chosen when the user tabs out of the
// field, allowing a user to type a few characters and hit tab to auto-complete to the first matched
// option. <code>completeOnTab</code> is automatically set to true if +link{addUnknownValues} is
// false.
// <P>
// ComboBoxItem does not provide built-in support for multiple selection.  For a Combobox
// that does provide such a multiple-select feature use +link{MultiComboBoxItem}.
//
// @see interface:PickList
// @implements PickList
// @treeLocation Client Reference/Forms/Form Items
// @example listComboBox
// @visibility comboBox
// @example listComboBox
//<
// Example of a Combo-Box type item in Windows applications: The URL bar for most browsers.
// ComboBox is used for:
// - "search": ComboBox is in a search form which is bound to the DataSource being searched
//   - eg: autoCompletion on names in an address book
// - "find-related": ComboBox is in an editing form and is being used to pick related records
//   from other DataSources
//   - eg: when editing an Account, find a User record to use as account.owner
//   - in this case, we want to
//     - default to displaying identifying fields from the related records
//     - store the primary key of the related record, NOTE however:
//       - the fieldName in the record being edited may not be the same as the fieldName
//         of the PK in the related record
//       - we may not actually store the PK if field.foreignKey indicates some other field.
//         Eg, the primaryKey might be a meaningless internal value and we may store instead
//         some other unique value
isc.defineClass("ComboBoxItem", "TextItem", "PickList").addMethods({

    //>@attr ComboBoxItem.defaultValue (boolean : null : IRW)
    // Static default value for this ComboBoxItem. To default to the first option use
    // +link{ComboBoxItem.defaultToFirstOption} instead.
    // @visibility external
    //<

    //> @method ComboBoxItem.defaultDynamicValue() (A)
    // Expression evaluated to determine the +link{ComboBoxItem.defaultValue} when no value is
    // provided for this item. To default to the first option use
    // +link{ComboBoxItem.defaultToFirstOption} instead.
    // @visibility external
    //<

    //> @attr ComboBoxItem.defaultToFirstOption (Boolean : false : IRW)
    // Select the first option as the default value for this ComboBoxItem. If options are derived
    // from a dataSource, the first value returned by the server will be used, otherwise the first
    // value in the valueMap. If enabled, this setting overrides +link{ComboBoxItem.defaultValue}
    // and +link{ComboBoxItem.defaultDynamicValue}.
    // @visibility external
    //<

    //>@attr ComboBoxItem.showHintInField (boolean : null : IRWA)
    // If showing a hint for this form item, should it be shown within the field?
    // <P>CSS style for the hint is +link{selectItem.textBoxStyle} with the suffix
    // "Hint" appended to it.
    // @group appearance
    // @see FormItem.hint
    // @visibility external
    //<

    //> @attr   comboBoxItem.mask  (string : null : IRWA)
    // Not applicable to a ComboBoxItem.
    // @visibility  external
    //<
    //> @attr   comboBoxItem.maskSaveLiterals   (boolean : null : IRWA)
    // Not applicable to a ComboBoxItem.
    // @visibility  external
    //<
    //> @attr   comboBoxItem.maskPadChar   (string : " " : IRWA)
    // Not applicable to a ComboBoxItem.
    // @visibility  external
    //<
    //> @attr   comboBoxItem.maskPromptChar   (string : "_" : IRWA)
    // Not applicable to a ComboBoxItem.
    // @visibility  external
    //<
    //> @attr   comboBoxItem.maskOverwriteMode   (boolean : null : IRWA)
    // Not applicable to a ComboBoxItem.
    // @visibility  external
    //<

    // Default to auto-sizing pickList
    // This means the pickList will expand to the size required to accomodate its content.
    // As the user types and filters occur, this means the pickList may resize horizontally but
    // seems to have no obvious performance impact
    autoSizePickList:true,


    //>@attr ComboBoxItem.progressiveLoading (Boolean : true : IRW)
    // Indicates whether or not this ComboBoxItem will load its list of options
    // +link{DataSource.progressiveLoading,progressively}.  This property is copied onto the
    // underlying +link{class:PickList}.
    // @see DataSource.progressiveLoading
    // @group progressiveLoading
    // @visibility external
    //<


    // Override drawn() - if this is a databound pickList we want to perform a filter before
    // the pickList itself ever gets shown.

    drawn : function (a,b,c,d) {
        this.invokeSuper(isc.ComboBoxItem, "drawn", a,b,c,d);
        if (this.autoFetchData && this._getOptionsFromDataSource()) {

            this.filterWithValue = false;
            this.fetchData(null, null, true);
        }
    },

    //> @attr comboBoxItem.showPickerIcon (Boolean : true : IRW)
    // @include FormItem.showPickerIcon
    // @visibility external
    //<
    showPickerIcon:true,

    //> @attr comboBoxItem.pickerIconWidth (int : 15 : IRWA)
    // @include FormItem.pickerIconWidth
    // @visibility external
    //<
    pickerIconWidth:15,

    //> @attr comboBoxItem.pickerIconHeight (int : null : IRWA)
    // Don't specify an explicit height for the picker icon - instead have it size to match the
    // height of the combo box item.
    // @visibility external
    //<
    pickerIconHeight:null,

    // Have native and synthetic selects' text styling match
    textBoxStyle:"selectItemText",

    //> @attr comboBoxItem.pickerIconSrc (SCImgURL : "[SKIN]/DynamicForm/ComboBoxItem_PickButton_icon.gif" : IRWA)
    // @include FormItem.pickerIconSrc
    // @visibility external
    //<
    pickerIconSrc:"[SKIN]/DynamicForm/ComboBoxItem_PickButton_icon.gif",

    // Apply some default properties to the picker icon.
    pickerIconProperties:{
        // We don't want it to be imgOnly because we need click handling, but we don't want it
        // in the page's tab order
        tabIndex:-1,
        showOver:true
    },

    // override modalPickList - we don't want to take focus from the text item when the
    // pickList is shown.
    modalPickList:false,

    //> @attr ComboBoxItem.showPickListOnKeypress  (Boolean : true : IRW)
    // Should the list of options be displayed whenever the user types into the
    // the combo-box textArea, or only when the user clicks on the pick button or uses the
    // explicit <code>Alt+Arrow Down</code> key combination?
    // @visibility comboBox
    //<
    showPickListOnKeypress:true,

    //> @attr comboBoxItem.saveOnEnter (Boolean : true : IRW)
    // ComboBox items will submit their containing form on enter keypress
    // if +link{DynamicForm.saveOnEnter,saveOnEnter} is true. Setting this property to
    // <code>false</code> will disable this behavior.
    // <P>
    // Note that if the drop down list of options (pickList) is visible an
    // <code>Enter</code> keypress is used to select a value from the available set of
    // options and will not automatically cause form submission.
    //
    // @visibility external
    //<
    // default implementation of formItem.shouldSaveOnEnter() returns this
    saveOnEnter: true,


    //> @attr ComboBoxItem.completeOnTab (boolean : null : IRW)
    // If true, when the pickList is showing, the user can select the current value by hitting
    // the <code>Tab</code> key.
    // <P>
    // Note that <code>completeOnTab</code> is not compatible with +link{formatOnBlur}
    // @visibility comboBox
    //<
    //completeOnTab:null,

    //> @attr ComboBoxItem.formatOnBlur (Boolean : false : IRW)
    // With <code>formatOnBlur</code> enabled, this item will format its value
    // according to any specified static +link{formItem.formatValue(),static formatter}
    // as long as the item does not have focus. Once the user puts focus into the item
    // the formatter will be removed. This provides a simply way for developers to
    // show a nicely formatted display value in a freeform text field, without the need
    // for an explicit +link{formItem.formatEditorValue()}
    // and +link{formItem.parseEditorValue()} pair.
    // <P>
    // Note that this attribute is not compatible with +link{completeOnTab}
    // @visibility external
    //<


    // Implement pickListShown to set up a page level mousedown handler to hide the pickList.

    pickListShown : function () {
        if (this.pickList.isVisible()) {
            // Don't pass in the 'fire once' param - if the user clicks outside we'll clear the
            // click event in response to that click. If the user clicks on the pickList
            // (or it's scrollbar, for example) we want to continue listening for future clicks
            // outside.
            this._pageClickID =
                this.ns.Page.setEvent("mouseDown", this, null, "_clickOutside");
        }
    },

    // If the user clicks on the page while the pickList is up, hide the pickList

    _clickOutside : function () {
        var pl = this.pickList;
        if (!pl || !pl.isVisible()) return;

        var target = isc.EH.lastEvent.target;
        // Special-cases:
        // - if the user showed the header of the PL and clicked on the header menu
        //   we don't want to dismiss the menu
        // - if the user showed the filter of the PL and clicked in the filter row
        //   we don't want to dismiss the menu
        // - if the user show the filter row of the PL, and clicked a pickList from a
        //   selectItem within that filter form we don't want to dismiss...
        if (!pl.contains(target, true) &&
            (!pl._cornerMenu || !pl._cornerMenu.contains(target, true)) &&
            (!pl.filterEditor || pl.filterEditor.getEditForm() != target)
           )
        {
            var hitInnerPickList = false;
            if (pl.filterEditor) {
                var innerPickLists = pl.filterEditor.getEditForm().getItems().getProperty("pickList");
                for (var i = 0; i < innerPickLists.length; i++) {
                    if (innerPickLists[i] && innerPickLists[i].contains(target)) {
                        hitInnerPickList = true;
                        break;
                    }
                }
            }
            if (!hitInnerPickList) {
//                this.logWarn("target:" + target);
                pl.hide();
            }
        }

        // What to do on mouseUp?
        // - If the click occurred over the pickerIcon we don't want to re-show in response to
        //   the click that follows this mouseDown.
        // - If the click occurred over the PickList, (or its scrollbar), in IE native focus will
        //   be taken from the textbox, even though the scrollbar is non-focusable.
        //   Restore focus on mouse up so the user can continue typing / do arrow-key navigation
        //   from the textbox.
        if (!this._dismissOnMouseUp) {
            this._dismissOnMouseUp =
              this.ns.Page.setEvent("mouseUp", this, isc.Page.FIRE_ONCE, "_dismissPickListClick");
        }
        // Note: if the user is dragging the scrollbar, we'll see a dragStop event, with no
        // mouseUp
        if (!this._refocusOnDragStop) {
            this._refocusOnDragStop =
              this.ns.Page.setEvent("dragStop", this, isc.Page.FIRE_ONCE, "_refocusFromPLMouseUp");
        }
    },
    _dismissPickListClick : function () {
        // If the click happened over this item, cancel it. this will prevent the
        // pickList from showing when it was hidden on a mousedown and the user clicks this item.
        if (this.form._getEventTargetItemInfo().item == this) {
            this.ns.Page.clearEvent("dragStop", this._refocusOnDragStop);
            // no need to clear the dismissOnMouseUp event since it's marked as FIRE_ONCE
            delete this._refocusOnDragStop;
            delete this._dismissOnMouseUp;
            return false;
        }
        this._refocusFromPLMouseUp(true);
    },

    _refocusFromPLMouseUp : function (fromClick) {
        // Only clear the event we're not responding to since the events are marked as FIRE_ONCE

        if (fromClick == true) {
            this.ns.Page.clearEvent("dragStop", this._refocusOnDragStop);
        } else {
            this.ns.Page.clearEvent("mouseUp", this._dismissOnMouseUp);
        }
        delete this._refocusOnDragStop;
        delete this._dismissOnMouseUp;

        if (this.pickList && this.pickList.isVisible() &&
            this.pickList.contains(isc.EH.getTarget()))
        {
            this.focusInItem();
        }
    },

    // Implement pickListHidden to clear the page level click event to hide the pickList, if
    // we haven't already.
    pickListHidden : function () {
        if (this._pageClickID) this.ns.Page.clearEvent("mouseDown", this._pageClickID);
        delete this._pageClickID;
    },

    // Override handkeKeyPress - as the user types, we want to re-filter the list.
    // Also proxy Arrow key presses to the PickList to navigate through options.
    // Behavior:
    // - We don't want the list showing if
    //      - there are no matches
    //      - the textItem is empty
    // - Otherwise we want the list to show.
    _$ArrowUp:"Arrow_Up", _$ArrowDown:"Arrow_Down",
    _$PageUp:"Page_Up", _$PageDown:"Page_Down",
    _$Escape:"Escape", _$Enter:"Enter", _$Tab:"Tab",
    handleKeyPress : function () {
        if (!this.hasFocus || this.isReadOnly()) return this.Super("handleKeyPress", arguments);

        var keyName = isc.EH.lastEvent.keyName,
            pickList = this.pickList,
            pickListVisible = (pickList ? (pickList.isDrawn() && pickList.isVisible()) : false);

        // deliver PageUp/Down to the body to cause scrolling
        if (pickListVisible && (keyName == this._$PageDown || keyName == this._$PageUp)) {
            return pickList.body.handleKeyPress(isc.EH.lastEvent);
        }

        var enteredValue = this.getEnteredValue(),
            value = this.getValue(),
            isEmpty = (!enteredValue || enteredValue == isc.emptyString);

        if (keyName == this._$ArrowDown && isc.EH.altKeyDown()) {
            // By default we hide the picklist whenever we get a keypress and the value is
            // empty (this is for the case where the user clears out the combobox text value).
            // Set a flag here to avoid hiding the combobox unless the value has subsequently
            // changed.
            this._shownWithValue = enteredValue;
            this._shownOnEmpty = isEmpty;

            this.filterWithValue = false;
            this.showPickList();
            return false;
        }

        // when addUnknownValues is false, we don't actually update the value unless the
        // user picks something from the pickList or takes focus from the field.
        // Allow an Escape keypress to reset the display value to match the underlying
        // data value again.
        if (keyName == this._$Escape && this.addUnknownValues == false) {
            var displayValue = this.getDisplayValue(value);
            if (isc.isAn.emptyString(displayValue) && this._getShowHintInField()) {
                this._showInFieldHint();
            } else {
                this.setElementValue(displayValue);
            }
        }

        // Should we complete (select the current item from the PickList) on Enter keypress?
        // - If addUnknownValues is true, always select the hilighted row in the pickList on
        //   enter keypress (complete on enter)
        // - If addUnknownValues is false:
        //  - if the user has moved up or down the list using the arrow keys then hits enter,
        //    complete on enter
        //  - otherwise, if completeOnEnter (currently undocumented) is explicitly set, respect it

        //
        //  - otherwise complete on enter unless we're embedded in a search form and have
        //    an option dataSource set - in which case assume the user is actually trying to do
        //    a substring match and enter should kick off a search.
        var isEnter = keyName == this._$Enter,
            completeOnEnter = false;
        if (isEnter && pickListVisible) {
            if (this.addUnknownValues == false) completeOnEnter = true;
            else if (this._userNavigated) completeOnEnter = true;
            else if (this.completeOnEnter != null) completeOnEnter = this.completeOnEnter;
            else {
                completeOnEnter = !this.form.isSearchForm ||
                                  !this._getOptionsFromDataSource();
            }
        }

        var navigate = pickListVisible && (keyName == this._$ArrowDown || keyName == this._$ArrowUp);
        this._userNavigated = navigate;

        if (pickListVisible) {
            // pass navigation keys (arrows) through to the drop-down list
            // so the user can navigate through the selection
            if (navigate ||
                // also pass "Enter" up to the list to trigger 'itemClick' behavior
                // and "Escape" to trigger 'cancel' behavior
                (isEnter && completeOnEnter) || keyName == this._$Escape)
            {
                pickList.bodyKeyPress(isc.EH.lastEvent);
                // kill the native behavior - shifting the cursor in the text field
                return false;
            }
            // hide the pickList on enter if completeOnEnter is false.

            if (isEnter) this.hidePicker();
        }

        // We want to style "pending" values differently from actually chosen values
        // when addUnknownValues is false

        if (!this.addUnknownValues) this._markPending();

        return this.Super("handleKeyPress", arguments);
    },

    // check for Tab keypress (for completeOnTab) on keyDown rather than keypress
    // This is required for Safari where the focus shift to the next item occurs before
    // any Tab keypress event actually fires.
    shouldCompleteOnTab : function () {
        return this.completeOnTab || (this.addUnknownValues == false);
    },
    handleKeyDown : function () {
         if (!this.hasFocus || this.isReadOnly()) return this.Super("handleKeyDown", arguments);

        var keyName = isc.EH.lastEvent.keyName,
            pickList = this.pickList,
            pickListVisible = (pickList ? (pickList.isDrawn() && pickList.isVisible()) : false);

        this._completeOnTabValue = null;

        // If completeOnTab is true, pick the currently selected value on tab.
        // Also do so if autoComplete:"smart" is enabled (inline autoCompletion),
        // as tab accepts an inline completion.
        if (keyName == this._$Tab &&
            (this.shouldCompleteOnTab() || this._getAutoCompleteSetting() == this._$smart))
        {
            // If we're currently pending a filter completion, wait for the filter to
            // complete before completing. Hang onto a 'completeOnTabValue' which we'll
            // check when the fetch completes.
            if (this._loadingData()) {
                this._completeOnTabValue = this.getEnteredValue();
            } else if (pickListVisible) {
                this._fireTabCompletion();
            }
        }

        return this.Super("handleKeyDown", arguments);
    },
    _loadingData : function () {
        return (this._queuedFetch || this._fetchingPickListData);
    },

    _fireTabCompletion : function () {
        var pickList = this.pickList;
        var selection = pickList.getSelectedRecord();
        if (selection != null) pickList.itemClick(selection);
        //this.logWarn("completing on keyDown, selection was: " + isc.echo(selection) +
        //             " value now: " + this.getValue());
    },

    // TextItem will call 'refreshDisplayValue' on blur to display the current data-value
    // remapped to display value.
    // This is intended to handle cases where there's a custom parser / formatter pair that
    // are not 1:1 [EG a forgiving date parser]. In this case a user may tab into a field,
    // modify the textual value to something that parses back to the underlying data value, then
    // tab out again and this method is responsible for displaying the formatted value again.
    // However -this interferes with our completeOnTab behavior since we would clobber the
    // user entered value and when the fetch completes _completeOnTabValue would no longer match
    // the result of 'getEnteredValue()'. -- therefore if we're currently
    // "_loadingData()" - IE performing asynchronous keypress handling or fetch -
    // don't clobber the user-entered value.
    refreshDisplayValue : function () {
        if (this._loadingData()) return;
        return this.Super("refreshDisplayValue", arguments);
    },

    // Expose getEnteredValue() - commonly useful for pickLists
    //> @method comboBoxItem.getEnteredValue()
    // @include textItem.getEnteredValue()
    // @visibility external
    //<

    //> @attr comboBoxItem.addUnknownValues (Boolean : true : IRW)
    // This property controls whether the user can enter a value that is not present in the
    // set of options for this item.
    // <P>
    // If set to false, the value the user enters in the text box is essentially used to filter the
    // set of options displayed in the pickList.
    // <P>
    // In this mode, when focus is taken from the field, if the entered value does not match
    // any entries in the +link{valueMap} or +link{optionDataSource}, it will be discarded. Note
    // that in this mode, +link{comboBoxItem.completeOnTab} behavior is automatically enabled
    // so if the user enters a valid partial value such that one or more options is displayed in
    // the pickList, and hits the Tab key, the first matching option will be chosen automatically.
    // In this mode the user may also hit the <code>"Escape"</code> key to discard their edits.
    // <P>
    // Note also that when <code>addUnknownValues</code> is set to false, the underlying value
    // returned by +link{getValue()} will not be updated until a value is explicitly chosen. This
    // means any change or changed handlers will not fire directly in response to the user typing
    // in the field - they will fire when the user actually selects a value, or takes focus from
    // the field.
    // <P>
    // If setting this property to <code>false</code> on a databound ComboBoxItem, data paging should
    // be disabled - this is required since the ComboBoxItem requires all data to be present in
    // order to determine whether an entered value is new. Data Paging can be disabled by modifying
    // the +link{listGrid.dataFetchMode,dataFetchMode} on the +link{pickListProperties} for this
    // item.
    // <P>
    // If this property is set to true, the user is not limited to entering values present in
    // the set of options for the item. Instead the set of options essentially become a set of
    // suggestions that may be used, or the user can enter an entirely new value.
    // @visibility external
    //<
    // Notes:
    // - If the valueMap is an object mapping data values to display values, or there
    //   is an optionDataSource specified with both display and value fields, should we allow
    //   the user to enter *data* values rather than display values?
    //   o if addUnknownValues is false, this is easy. We would ignore an entered value that
    //     didn't match a display value, even if it happened to be a data value.
    //     However it can be handled (if there's an optionDataSource) by
    //     setting the filterFields for the item to include the valueField - that way if the
    //     developer enters a data value, it'll show up in the pickList, and complete on tab.
    //   o if addUnknownValues is true this is less likely to be an issue since you'd be unlikely
    //     to be applying a valueMap on a field that supported freeform text entry. However we
    //     have to treat an entered value that doesn't match any display value as a data value of
    //     of course -- so if there is a matching data value in the valueMap already we would
    //     select it. The trick is that we'll not update the item to show the display value
    //     until the user takes focus from the field. This is handled by logic in handleEditorExit()
    //     to call _checkForDisplayFieldValue() if necessary.


    addUnknownValues:true,

    //> @attr comboBoxItem.allowEmptyValue (Boolean : true : IRW)
    // If +link{comboBoxItem.addUnknownValues} is <code>false</code>, this property
    // determines whether
    // the user can clear the comboBoxItem value, or whether they are constrained to
    // choosing one of the available options (in which case clearing the text box will
    // simply revert to the last picked value when the user leaves the field).
    // @visibility external
    //<
    allowEmptyValue:true,

    // Helper to determine if a user-entered value matches something in the
    // pickList for this comboBoxItem. Note that the value passed in is the display value,
    // not the data value.
    isUnknownValue : function (value) {
        var vm = this.getValueMap();
        if (vm != null) {
            if (isc.isAn.Array(vm)) {
                if (vm.contains(value)) return false;

            } else if (isc.isAn.Object(vm)) {
                for (var key in vm) {
                    if (vm[key] == value) return false;
                }
            }
        }

        // At this point we didn't find the value in the valueMap - check for it
        // being present in the loaded optionDataSource data.
        var ODS = this.getOptionDataSource();
        if (ODS != null) {
            return this._translateValueFieldValue(value, true, true) == null;
        }
        return true;
    },

    // On editorExit:
    // - if addUnknownValues is false, check to see if we have a valid displayValue in our
    //   text box - if so, update our value (pick the appropriate explicitly entered option,
    //   or drop the value if there isn't one).
    //   - If we are currently fetching data for the pickList there are 2 exceptions here:
    //   1) User hit tab - in this case we will have set up _completeOnTabValue and when the
    //      fetch completes we'll auto-select the first option. Nothing else to do here
    //   2) User didn't hit tab but just took focus from the item. In this case we'll detect
    //      we don't have focus and check for the validity of the entered value on filterComplete,
    //      so again nothing to do here.
    //   Both these cases are ultimately handled by _updateValueForFilterComplete
    //
    // - if addUnknownValues is true updateValue() should already have stored the appropriate
    //   data value.
    //   If the value entered matched some entry in our pickList, and we have a displayField,
    //   we should have already mapped back to a data value and stored.
    //   Otherwise we have stored the entered value as a display value (User entered an "unknown"
    //   value).
    //   In this case, if we have both a displayField and a dataField, the
    //   entered value may be the data field value for some entry in our optionDataSource.
    //   This is a tricky case - we have to store the user-entered value as a data value
    //   so to keep it consistent with a call to 'setValue()' we should display the
    //   displayValue for the option if there is one.
    //   *We avoid this while focus is actually in the item to avoid changing user entered
    //    partial filter values etc.
    //   - call mapValueToDisplay to determine if we have a loaded entry where the entered value
    //     matches a data value, and if so display it.
    //   If not:
    //   1) We may have a pending fetch - allow _updateDisplayValueForNewData() to update the
    //   display value when the fetch completes
    //   2) Otherwise we need to kick off a new fetch to get the display value - in this case
    //   use "checkForDisplayFieldValue()" to kick off that new fetch.


    handleEditorExit : function () {

        // Assertion: all the logic below handles editor exit due to click outside, tab keypress and
        // essentially handles resolving the entered value to a live data value and displaying it
        // if necessary.
        // If the user actually clicked on the pickList we'll get an elementBlur ==> editorExit
        // on mouseDown, before itemClick has had a chance to fire.
        // Skip all this logic in that case.

        if (this._mouseDownInPickList()) return;

        // clear the _userNavigated flag used to determine whether to complete on enter keypress
        this._userNavigated = false;

        var elementValue = this.getEnteredValue(),
            value = this.getValue();

        if (this._completeOnTabValue != null && this._completeOnTabValue == elementValue &&
            !this._loadingData())
        {
            this._fireTabCompletion();
            this._completeOnTabValue = null;
            // re-fetch the elementValue - it should now be good if there was a valid completion
            elementValue = this.getEnteredValue();
        }

        // If completeOnTabValue is populated we're waiting for a fetch to complete to
        // update our value so skip logic to update value and/or displayed value on exit.
        if (this._completeOnTabValue == null) {
            if (this.addUnknownValues) {


                var ods = this.getOptionDataSource();
                if (value == elementValue && (ods == null || this.getDisplayFieldName != null)) {
                    if (value != null && this.shouldFetchMissingValue(value)) {
                        // _checkForDisplayFieldValue() will check whether we currently have a dataSource
                        // record for the value - if not it will kick off a fetch to retrieve such a value
                        // and update the display value when the fetch completes.

                        this._checkForDisplayFieldValue(value);
                    }

                    // map the value passed to the visible value as necessary
                    var displayValue = this.mapValueToDisplay(value);
                    if (displayValue != value) {
                        if (isc.isAn.emptyString(displayValue) && this._getShowHintInField()) {
                            this._showInFieldHint();
                        } else {
                            this.setElementValue(displayValue);
                        }
                    }
                }
            } else {
                // If we're currently loading data wait for that to complete - otherwise
                // validate the entered value against our live pickList data and drop it
                // if its an "unknown" value now.
                if (!this._loadingData()) {

                    // If the user entered something in the text box that actually matches
                    // a value in the pickList, update the data value. Otherwise reset to
                    // original value.

                    if (this.isUnknownValue(elementValue)) {
                        this.setElementValue(this.getDisplayValue());
                    } else {
                       this._updateValue(elementValue, true);
                    }
                }
            }
        }
        // Don't validateOnExit if we're waiting on a complete-on-tab from an asynch fetch.
        this._suppressValidateOnEditorExit = (this._completeOnTabValue != null);
        var returnVal = this.Super("handleEditorExit", arguments);
        this._suppressValidateOnEditorExit = null;
        return returnVal;
    },

    // Reimplement/override _updateValueForFilterComplete()
    // If completeOnTab is true and the user hit tab mid-filter, we can complete now.
    // Also if addUnknownValues is false and focus was taken from the field mid
    // filter (without a tab keypress), accept or reject the typed value now.
    _updateValueForFilterComplete : function (response,data,request) {

        // Always select the default item at this point since we have the latest data
        this.selectDefaultItem();

        if (!this.hasFocus) {
            if (this._completeOnTabValue != null &&
                this._completeOnTabValue == this.getEnteredValue())
            {
                this._fireTabCompletion();
                this._completeOnTabValue = null;
            }
            // Check for 'unknown' value even if _completeOnTabValue was true.
            // We may have not returned any matches in which case _fireTabCompletion() would no-op
            // and we should now clear out the element value and restore any previously saved value
            if (!this.addUnknownValues) {
                var elementValue = this.getEnteredValue();
                if (this.isUnknownValue(elementValue)) {
                    var displayValue = this.getDisplayValue();
                    if (isc.isAn.emptyString(displayValue) && this._getShowHintInField()) {
                        this._showInFieldHint();
                    } else {
                        this.setElementValue(displayValue);
                    }
                } else {
                    this._updateValue(elementValue, true);

                    elementValue = this.getEnteredValue();
                    if (this.showHintInField &&
                       (elementValue == null || isc.is.emptyString(elementValue))) {

                        this._showInFieldHint();
                    }
                }
            }

            // If validateOnExit is true, we suppressed it in handleEditorExit as we didn't
            // want to validate the partially entered value.
            // In this case, force validation now.
            this._performValidateOnEditorExit(this.getValue());
        }

        // call _updateDisplayValueForNewData()
        // This is overridden in PickList.js
        // For ComboBoxItems, if addUnknownValues is true, and we just loaded a record
        // for which the entered value is the display value, this method will call
        // 'updateValue()' which will save out the relevant data value.
        // Otherwise this method will update the textbox to show the displayValue for the
        // item (standard behavior) iff the text box doesn't currently have focus - which
        // ensures we don't change the display value mid text-entry.
        this._updateDisplayValueForNewData();
    },

    //> @attr comboBoxItem.pendingTextBoxStyle (CSSStyleName : null : IRW)
    // Optional "pending" style for this item's text box.
    // <P>
    // If +link{comboBoxItem.addUnknownValues} is false, when the user modifies the
    // value displayed in the combobox item text box, the underlying data value (as returned
    // from item.getValue()) is not immediately updated - instead the value is used to filter
    // the set of results displayed in the comboBoxItem pickList.
    // <P>
    // While the comboBoxItem is in this pending state (where the result of getEnteredValue() will
    // not necessarily match the display value for whatever is returned by getValue()), the
    // pendingTextBoxStyle may be applied to the text box for the item.
    // <P>
    // When the element value is updated to display the actual value for the item (typically due
    // to the user selecting a value from the pickList), the standard
    // +link{TextItem.textBoxStyle} will be reapplied.
    // <P>
    // May be left unset in which case the standard text box style is always applied.
    // Has no effect if +link{addUnknownValues} is true.
    // @visibility external
    //<

    _markPending : function () {
        var wasPending = this._pendingElementValue;
        this._pendingElementValue = true;
        this._pendingEnteredValue = this.getEnteredValue();
        if (!wasPending) this.updateState();
    },
    _markNotPending : function () {
        if (!this._pendingElementValue) return;
        this._pendingElementValue = null;
        this._pendingEnteredValue = null;
        this.updateState();
    },
    getTextBoxStyle : function () {
        if (this.pendingTextBoxStyle == null || !this._pendingElementValue || this._isPrinting()) {
            if (this._isPrinting()) return isc.TextItem.getInstanceProperty("textBoxStyle");
            return this.Super("getTextBoxStyle", arguments);
        }
        return this.pendingTextBoxStyle;
    },


    // Override _updateValue()
    // If addUnknownValues is false, don't update the data value based on a change to the
    // text-box value unless the value was explicitly picked
    // from the pickList (this includes enter keypress, completeOnTab), or
    // focus is being taken from the item (in which case we pass in the forceSave param).
    _updateValue : function (value, forceSave) {
        var suppressSave = !forceSave && !this._valuePicked && this.addUnknownValues == false;
        // If allowEmptyValue is true, and the user actually clears the text value,
        // actually save out the empty value, even if addUnknownValues is false.
        if (suppressSave && this.allowEmptyValue && (value == "")) {
            suppressSave = false;
        }
        if (!suppressSave) {
            this._markNotPending();
            // Catch the case where we're already updated
            var dataValue = this.mapDisplayToValue(value);

            if (this._valuePicked) this.explicitChoice = this._valuePicked;

            if (this.compareValues(dataValue, this._value)) {
                return true;
            }
            this.explicitChoice = this._valuePicked;
            return this.Super("_updateValue", arguments);

        // If addUnknownValues is false just refilter the pickList
        } else {

            if (this.changeOnKeypress &&
                this.length != null && isc.isA.String(value) && value.length > this.length) {
                value = value.substring(0, this.length);
                this.setElementValue(value);
            }


            // continue to filter the picklist based on the user-entered value

            if (this._mouseDownInPickList()) return;
            if (this._pendingElementValue == null) return;
            if (this._pendingEnteredValue == this._lastFilterValue) return;
            this._lastFilterValue = this._pendingEnteredValue;
            this.refreshPickList(value);
        }
    },

    // Override handleChanged to re filter the list on every change (which occurs on every
    // keypress that changed the entered value).
    // Note handleChanged() is not called if addUnknownValues is false and the user
    // entered value has no completion - in that case, pickList refersh is triggered directly from
    // _updateValue()
    handleChanged : function (newValue, oldValue) {
        //this.logWarn("handleChange: newValue: " + newValue + " oldValue: " + oldValue);
        var returnVal = this.Super("handleChanged", arguments);
        this.refreshPickList(this.getEnteredValue());
        return returnVal;
    },
    // refresh the pickList for a change in the user-entered value
    // Note that newValue is a display value.
    refreshPickList : function (newValue) {

//         this.logWarn("refreshPickList called with newValue: " + newValue +
//                      ", valuePicked: " + this._valuePicked + this.getStackTrace());

        var isEmpty = (!newValue || newValue == isc.emptyString);

        // clear out the shownOnEmpty string if we're not empty any more. This means if
        // the user clicks the picker icon [pick list shows], then enters a character,
        // [pick list filters], then deletes that character, the pick list will hide
        if (!isEmpty) delete this._shownOnEmpty;

        // if the text field is empty, ensure the list is hidden unless it was explicitly
        // shown with an empty value (Arrow down keypress / picker click)
        var pickList = this.pickList,
            pickListVisible = (pickList ? pickList.isVisible() : false);
        if (isEmpty && !this._shownOnEmpty) {
            if (pickListVisible) pickList.hide();
        } else if (this.showPickListOnKeypress || pickListVisible) {
            // showPickList will set up the pickList initially, or if already set up
            // will re-filter
            // Note - if our value changed in response to the user picking something from the
            // list we don't want to show the pickList again.
            // Note - pass in the second 'queueFetches' parameter - we don't want to kick off
            // multiple server fetches while the user is rapidly typing in the text field

            if (!this._valuePicked && this.hasFocus) {

                if (!this.filterWithValue && (this._shownWithValue != newValue)) {
                    this.filterWithValue = true;
                    delete this._shownWithValue;
                }
                this.showPickList(true, true);
            }
        }
    },

    // Override selectDefaultItem to always select the first item in the list.
    // This will happen on every re-filter.
    selectDefaultItem : function () {
        if (this.pickList == null || this.pickList.destroyed) return;

        var selection = this.pickList.selection;

        // If we have a record matching our value (or our pending, user-typed value)
        // select it as the default. This ensures that if the user hits tab or enter
        // we auto-complete correctly
            if (this.optionDataSource) {
            var record;

            if (!this.addUnknownValues && this._pendingEnteredValue != null) {
                record = this.getPickListRecordForValue(this._pendingEnteredValue)
            } else {
                record = this.getSelectedRecord();
            }

                if (record) {
                    this.pickList.clearLastHilite();
                    this.delayCall("selectItemFromValue", [record[this.valueField]]);
            return;
        }
        }

        // Otherwise select the first record in the list by default.
        var record = this.pickList.getRecord(0);
        // Don't attempt to select null / loading / separator rows
        if (record == null || Array.isLoading(record) ||
            record[this.pickList.isSeparatorProperty]) return;

        selection.selectSingle(record);
        // Clear last hilite - required so keyboard navigatioin will pick up the current position
        // from the selection, not the last hilite position.
        this.pickList.clearLastHilite();
        this.pickList.scrollRecordIntoView(0);
    },

    // Override getSelectedRecord to look at the pickList if present
    getSelectedRecord : function () {

        var record = this.Super("getSelectedRecord", arguments);

        // If we didn't get selected record via 'fetchMissingValues',
        // use the pickList to try to find the selectedRecord in the pickList data
        if (record == null && this._value != null && this.getOptionDataSource()) {
            record = this.getPickListRecordForValue(this._value);
            if (record != null) {
                // Store the record from the pickList in our displayField cache
                this._addDataToDisplayFieldCache([record]);
                this._updateSelectedRecord();
            }
        }
        return record;
    },

    // Given a value, reach into the pickList and find the associated record.
    getPickListRecordForValue : function (value) {
        var record;
            if (this.pickList == null || this.pickList.destroyed) {
                if (this.progressiveLoading === true || this.progressiveLoading === false) {
                    if (this.pickListProperties == null) this.picklistProperties = {};
                    this.picklistProperties.progressiveLoading = this.progressiveLoading;
                }
                this.makePickList(false);
            }

            if (this.pickList && this.pickList.data) {
            record = this.pickList.data.find(this.getValueFieldName(), value);
        }
        return record;
    },


    // ------------------------
    // Filtering data
    // ------------------------

    // Include useful JSDoc from pickList

    //> @attr ComboBoxItem.optionDataSource (DataSource | String : null : IRA)
    // @include PickList.optionDataSource
    //<

    //> @attr ComboBoxItem.pickListFields (Array of ListGridField : null : IRA)
    // @include PickList.pickListFields
    //<

    //> @method ComboBoxItem.fetchData()
    // @include PickList.fetchData()
    //<

    //> @attr comboBoxItem.cachePickListResults (boolean : true : IR)
    // For databound pickLists (see +link{pickList.optionDataSource}), by default SmartClient
    // will cache and re-use datasets shown by different pickLists displayed by different
    // SelectItems in an LRU (least recently used) caching pattern.
    // <P>
    // Setting this flag to false avoids this caching for situations where it is too
    // aggressive.
    // <p>
    // Note that this does not control re-use of data <b>within a single pickList</b>.  To
    // control when client-side filtering is used in ComboBoxItem, see
    // +link{ComboBoxItem.useClientFiltering} and +link{ComboBoxItem.filterLocally}.
    //
    // @visibility external
    //<

    //> @attr ComboBoxItem.fetchDisplayedFieldsOnly (boolean : null : IRA)
    // @include PickList.fetchDisplayedFieldsOnly
    //<

    //> @attr ComboBoxItem.optionFilterContext (DSRequest Properties : null : IRA)
    // @include PickList.optionFilterContext
    //<

    //> @attr ComboBoxItem.optionOperationId (string : null : [IR])
    // @include FormItem.optionOperationId
    // @visibility external
    //<


    //> @attr ComboBoxItem.displayField (string : null : IRWA)
    // @include PickList.displayField
    // @visibility external
    //<

    //> @attr ComboBoxItem.valueField   (string : null : IRW)
    // @include PickList.valueField
    // @visibility external
    //<

    //> @method ComboBoxItem.getDisplayFieldName() ([A])
    // @include PickList.getDisplayFieldName()
    // @visibility external
    //<

    //> @method ComboBoxItem.getValueFieldName()
    // @include PickList.getValueFieldName()
    // @visibility external
    //<

    //> @attr ComboBoxItem.filterLocally
    // @include PickList.filterLocally
    // @visibility external
    //<

    //> @method ComboBoxItem.getSelectedRecord()
    // @include FormItem.getSelectedRecord()
    // @visibility external
    //<

    //> @attr ComboBoxItem.pickListCriteria (Criteria : null : IRWA)
    // @include PickList.pickListCriteria
    // @visibility external
    //<

    //> @attr ComboBoxItem.showOptionsFromDataSource (boolean : null : IRWA)
    // @include PickList.showOptionsFromDataSource
    // @visibility external
    //<

    //> @attr ComboBoxItem.pickListProperties (ListGrid Properties : null : IRA)
    // @include PickList.pickListProperties
    // @visibility external
    //<

    //> @attr   ComboBoxItem.sortField   (String or integer : null : IR)
    // @include PickList.sortField
    // @visibility external
    //<


    // Default pickList interface 'filtering' basically returns every record (this is the
    // desired behavior for select items).
    // For pickLists we want to show only the subset of options that matches the string
    // currently in the combo box text element.

    // Set filterDisplayValues to ensure that if we're working with client side options,
    // the filter criteria are recognized as the display values for the data not the raw values.
    filterDisplayValue:true,

    //> @attr ComboBoxItem.autoFetchData   (Boolean : false : [IRA])
    // If this combo box retrieves its options from a <code>dataSource</code>, should options
    // be fetched from the server when the item is first written out, or should this fetch be
    // delayed until the user opens the pickList.
    //
    // @visibility external
    // @see PickList.optionDataSource
    //<
    autoFetchData:false,

    //> @attr ComboBoxItem.filterWithValue (Boolean : varies : [RA])
    // Read-only property set by the ComboBoxItem to indicate whether we should use the
    // current typed-in value as part of the filter criteria returned by
    // +link{ComboBoxItem.getPickListFilterCriteria()}.  You can check this flag in order to
    // mimic the ComboBoxItem's default behavior if you provide a custom implementation of
    // <code>getPickListFilterCriteria()</code>.
    // @see comboBoxItem.getPickListFilterCriteria()
    // @see comboBoxItem.filterFields
    // @visibility external
    //<

    //> @attr ComboBoxItem.alwaysFilterWithValue (boolean : false : [RA])
    // If set to true, the default implementation of
    // +link{comboBoxItem.getPickListFilterCriteria()} will always add the current item's value
    // to any specified pickListFilterCriteria, regardless of the current value of
    // +link{ComboBoxItem.filterWithValue}.
    // <P>
    // This was the default behavior for SmartClient version 5.6 and earlier.
    // @visibility internal
    //<

    //> @attr ComboBoxItem.filterFields (Array of String : null : IR)
    // As the user types into this item's textBox, a comboBoxItem will show the
    // pick-list of options, and filter the set of results displayed by the current value in the
    // text box. For a databound comboBoxItem, by default the entered value is filtered against
    // the +link{comboBoxItem.displayField} if one is specified, otherwise the
    // +link{comboBoxItem.valueField}.
    // <P>
    // This attribute allows the developer to explicitly change which fields to filter against,
    // causing the user-entered text to be matched against any of the specified set of fields from
    // the +link{optionDataSource}.
    // <P>
    // This essentially causes +link{getPickListFilterCriteria()} to return an +link{AdvancedCriteria}
    // object representing "field1 starts with value or field2 starts with value or ...".  The
    // +link{OperatorId,operator} used is controlled by +link{textMatchStyle} as usual, that is,
    // "startsWith" implies the operator "iStartsWith, "substring" implies "iContains" and "exact"
    // implies "iEquals".
    // <P>
    // The most common use case for this setting would be when a comboBoxItem is showing multiple
    // +link{comboBoxItem.pickListFields} - if the same set of fields is specified as
    // +link{comboBoxItem.criteriaFields}, the user can use the text-box to filter against
    // whichever fields are visible in the pickList.
    // <P>
    // <var class="smartclient">For finer grained control over comboBoxItem filtering, the
    // +link{comboBoxItem.getPickListFilterCriteria()} method may be overridden.</var>
    // <var class="smartgwt">For finer grained control over comboBoxItem filtering, the
    // +link{comboBoxItem.setPickListFilterCriteriaFunction()} may be specified.</var>
    // @visibility external
    //<

    //> @groupDef comboBoxFiltering
    // The criteria used to decide which options should appear in
    // the drop-down +link{PickList} shown by a ComboBox are determined as follows.
    // <P>
    // While the user is typing in a value, the
    // +link{comboBoxItem.getPickListFilterCriteria()} method will return
    // the typed-in value as part
    // of the criteria, so that only matching values are shown.  Matching is determined by the
    // +link{comboBoxItem.textMatchStyle,textMatchStyle}.  Note that the +link{filterFields}
    // attribute may be used to determine which fields filtering is performed against for
    // databound comboBoxItems.
    // <P>
    // If the user explicitly shows the down-down pickList, via either clicking on the drop
    // down icon or using the <i>Ctrl+Arrow Down</i> key combination, the typed-in value is
    // ignored for filtering.
    // <P>
    // If included in the criteria, the typed-in value will be included as a value for the
    // +link{ComboBoxItem.displayField,displayField} (or for the
    // +link{ComboBoxItem.valueField,valueField} if <code>this.displayField</code> is
    // unspecified).
    // <P>
    // Static criteria, specified via +link{comboBoxItem.optionCriteria} or
    // +link{comboBoxItem.pickListCriteria,pickListCriteria}, will always be included by the
    // default implementation (combined with the typed in value if appropriate).
    // <P>
    // <var class="smartgwt">For custom pick list filter criteria, we recommend
    // calling the
    // {@link com.smartgwt.client.widgets.form.fields.ComboBoxItem#setPickListFilterCriteriaFunction}
    // method.</var>
    // <var class="smartclient"><code>getPickListFilterCriteria()</code> may be overridden for
    // custom behavior.</var>
    // If you are implementing your own pickList filter criteria, the
    // <b>read-only</b> property +link{ComboBoxItem.filterWithValue,this.filterWithValue} can
    // be read to determine whether the ComboBox would ordinarily ignore the typed-in value for
    // filtering.  Note that in addition to cases where the user explicitly shows the pickList,
    // <code>filterWithValue</code> will also be <code>true</code> during a call to
    // +link{ComboBoxItem.fetchData()} on a databound comboBox.
    // <P>
    // <b>NOTE:</b> The defaut implementation of this method will return an
    // +link{AdvancedCriteria} object if multiple +link{filterFields} are specified, or
    // if there are field collisions between any specified static +link{optionCriteria},
    // +link{pickListCriteria} and the entered value. AdvancedCriteria are not supported by
    // all DataSource types, including the built-in server-side SQL dataSources in SmartClient Pro
    // edition (though they are supported by SQL dataSources in Power and Enterprise editions).
    // <P>
    // <b>Client-Side Filtering</b><br>
    // By default, the ComboBoxItem will automatically use client-side filtering whenever
    // it receives a complete set of results for a given search string, and then the
    // user types more letters (so reducing the results further).
    // <p>
    // Client-side filtering may malfunction if the server filtering behavior can't be
    // replicated client-side (for example, Google Search).  To disable client-side
    // filtering so that the comboBox always contacts the server for data whenever the
    // use changes the search string, set +link{comboBoxItem.useClientFiltering} to false.
    // <p>
    // However, disabling client-side filtering will slow down the UI and cause more
    // round-trips to the server, so if client-side filtering is malfunctioning but
    // <i>should work</i>, try to correct the problem rather than disable the feature.
    // <p>
    // For example, if the initial search works correctly but adding more letters
    // always causes zero matches, most likely the Records returned by the server lack
    // values for the field(s) targeted by the filter criteria, or the field values
    // returned by the server don't match the criteria values.
    // <P>
    // View the returned data in the RPC tab in the Developer Console and enable the
    // "ResultSet" log category in the "Results" tab to troubleshoot how the filter
    // criteria are being applied to data, and look closely at your
    // settings for +link{valueField} and +link{displayField}.
    //
    // @title ComboBoxItem PickList Filtering
    // @visibility external
    //<

    //> @attr comboBoxItem.useClientFiltering (Boolean : null : IRA)
    // @include pickList.useClientFiltering
    // @visibility external
    //<

    //> @method  ComboBoxItem.getPickListFilterCriteria()    (A)
    // +link{group:stringMethods,StringMethod} to return filter criteria for options displayed for
    // this item.
    // <P>
    // See +link{group:comboBoxFiltering} for details on how pickList filter criteria
    // are calculated by default for a comboBoxItem.
    //
    // @return (Criteria) criteria to be used for databound or local filtering
    // @visibility external
    //<
    getPickListFilterOperator : function () {
        return (this.textMatchStyle == "startsWith" ? "iStartsWith" :
                                  this.textMatchStyle == "exact" ? "iEquals" : "iContains")
    },
    getPickListFilterCriteria : function () {
        var crit = this.optionCriteria,
            pickListCriteria = this.pickListCriteria;

        if (pickListCriteria != null) {
            if (crit == null) crit = pickListCriteria;
            else {
                crit = isc.DataSource.combineCriteria(crit, pickListCriteria);
            }
        }
        if (this.alwaysFilterWithValue || this.filterWithValue) {
            // if filterFields are specified, explicitly use them.
            // Otherwise filter against the field that will actually be displayed
            // in the text box - this will be the displayField if specified, otherwise
            // the valueField.
            var filterFields = this.filterFields;
            if (filterFields == null) {
                filterFields = [this.getDisplayFieldName() || this.getValueFieldName()]
            }

            var value = this.getEnteredValue();
            var liveCrit;
            if (filterFields.length == 1) {
                liveCrit = {};
                liveCrit[filterFields[0]] = value;

            // Multiple fields: Build an advanced criteria object and do 'OR'
            // matches to filter on each field passed in.
            } else {
                liveCrit = {
                    _constructor:"AdvancedCriteria",
                    operator:"or",
                    criteria:[]
                }
                for (var i = 0; i < filterFields.length; i++) {
                    liveCrit.criteria.add({
                        fieldName:filterFields[i],
                        value:value,
                        operator:this.getPickListFilterOperator()
                    });
                }
            }
            if (crit == null) crit = liveCrit;
            else crit = isc.DataSource.combineCriteria(crit, liveCrit);
        }

        return crit || {};
    },

    //> @attr ComboBoxItem.minimumSearchLength   (integer : null : [IRA])
    // Minimum length in characters before a search is performed. If too few characters
    // are entered the pick list shows +link{ComboBoxItem.searchStringTooShortMessage,
    // searchStringTooShortMessage}.
    //
    // @visibility external
    //<

    //> @attr ComboBoxItem.searchStringTooShortMessage   (String : "Enter a longer search string to search" : [IRA])
    // Message to display in pick list when +link{ComboBoxItem.minimumSearchLength,
    // minimumSearchLength} characters have not been entered.
    //
    // @visibility external
    //<
    searchStringTooShortMessage: "Enter a longer search string to search",

    isEntryTooShortToFilter : function () {
        var value = this.getEnteredValue();
        var tooShort = (value != null && value.length < this.minimumSearchLength);
        var result = ((this.filterWithValue || this.alwaysfilterWithValue) && tooShort);
        return result;
    },
    getEntryTooShortMessage : function () {
        return this.searchStringTooShortMessage;
    },
    // Never refresh for fetched value while focus is in our text box if the user has typed
    // If the user entered a display value we're already showing the right thing
    // If the user entered a data value, we'll remap to display on editor exit.
    _refreshForDisplayValueChange : function () {
        if (!this.hasFocus) return true;
        // Pending --> user has entered something that we haven't stored as this.value
        if (!this.addUnknownValues) return !this._pendingElementValue;

        // addUnknownValues:true - in this case we store out the value as the user types and
        // don't store a pending state. Use explicitChoice to determine whether the value was
        // set programmatically or picked from the list as opposed to just being typed.
        return !!this.explicitChoice;
    },


    //> @method comboBoxItem.getClientPickListData()
    // @include PickList.getClientPickListData()
    //<

    //> @method comboBoxItem.filterClientPickListData()
    // @include PickList.filterClientPickListData()
    //<

    //> @attr comboBoxItem.textMatchStyle (TextMatchStyle : "startsWith" : IR)
    // @include PickList.textMatchStyle
    //<

    //> @attr comboBoxItem.showAllOptions (boolean : null : IR)
    // @include PickList.showAllOptions
    //<

    //> @method ComboBoxItem.dataArrived()
    // @include PickList.dataArrived()
    // @visibility external
    //<

    //> @attr comboBoxItem.separatorRows (Array of ListGridRecord : [{isSeparator:true}] : IR)
    // @include PickList.separatorRows
    //<

    // When the user blurs from the combo box item we want to ensure we hide the pickList.

    elementBlur : function () {
        // note that handleEditorExit() / editorExit are called from super impl of elementBlur
        this.Super("elementBlur", arguments);


        if (isc.Browser.isMobile) {
            this.delayCall("hidePickListOnBlur", [true], 100);
        } else {
            this.hidePickListOnBlur();
        }
    },
    hidePickListOnBlur : function (checkFocus) {
        if (checkFocus && ((this.hasFocus && this.containerWidget.hasFocus) ||
                            (this.pickList && this.pickList.body.hasFocus))
            )
        {
            return;
        }

        // if we show the header context menu we'll blur the native text item.
        // Don't hide the menu in response to this.
        if (this.pickList && this.pickList._showingHeaderContextMenu) return;

        var pickList = this.pickList,
            EH = isc.EH,
            event = EH.lastEvent;
        //this.logWarn("eventType is: " + event.eventType +
        //             " event.target: " + event.target +
        //             " pickList.isAncestor: " + pickList.contains(event.target,true) +
        //             ", activeElement: " + this.echoLeaf(document.activeElement));

        // don't hide the pickList if it recieved the mouseDown event that tripped this blur.
        if (this._mouseDownInPickList() ||
                // ignore a delayed blur firing in IE when focus is still in the text input element
                (isc.Browser.isIE && this.getActiveElement() == this.getDataElement())
           )
        {
                return;
        }

        // ensure we don't show when
        // - delayed filter is kicked off
        // - asynchronous filter completes
        delete this._showOnFilter;
        delete this._showOnDelayedFilter;

        // bail if the pickList isn't showing or has been passed to another form item already
        if (!pickList || !pickList.isVisible() || pickList.formItem != this) return;

        // hide the pickList
        pickList.hide();
    },
    _mouseDownInPickList : function () {
        var pickList = this.pickList;
        if (!pickList || !pickList.isVisible() || !pickList.isDrawn()) return false;
        var EH = isc.EH, event = isc.EH.lastEvent;
        if (   // the preceding event was a selectionChange event, which fires right after
               // mouseDown in the pickList, strangely triggered by some action we take in
               // Element._getElementFromSelection()
               // Special case because we can't look at event.target to determine if it was
               // a click on the pickList
               ( event.eventType == "selectionChange" &&
                 pickList.contains(isc.EH.mouseDownTarget()) )
               ||

               // it's a mouseDown/click on the pickList or one of it's subcomponents
               // have to specially check for click in a filter editor
               (
                (event.eventType == EH.MOUSE_DOWN || event.eventType == EH.CLICK ||
                // last event was a mouseMove and the mouse is still over the mouseDown target
                (event.eventType == EH.MOUSE_MOVE && EH.stillWithinMouseDownTarget())) &&
                (pickList.contains(event.target,true) || (pickList.filterEditor && pickList.filterEditor.getEditForm() == event.target))
               )
           )
        {
            return true;
        }
        return false;
    },

    editorEnter : function (form, item, value) {
        // Hide in-field hint if being shown
        this._hideInFieldHint();
    },
    editorExit : function (form, item, value) {
        var undef;
        if (this._completeOnTabValue == null && this.showHintInField &&
            (value === undef || value == null || isc.is.emptyString(value)))
        {
            this._showInFieldHint();
        }
    },

    // Override showPicker to ensure we have focus, and show the pick list
    showPicker : function () {
        this.focusInItem();

        this.filterWithValue = false;

        var value = this.getEnteredValue(),
            isEmpty = (!value || value == isc.emptyString);
        this._shownWithValue = value;
        this._shownOnEmpty = isEmpty;
        return this.showPickList();
    },

    // pickValue
    // This method is fired when a value is selected from the pick list.
    // update the value of this item, and fire the change handler.
    pickValue : function (value) {

        // Make sure in-field hint is hidden
        this._hideInFieldHint();

        var displayValue = this.mapValueToDisplay(value);
        this.setElementValue(displayValue);
        // If we're focused, select the value so the user can single-click edit it.
        if (this.hasFocus) this.selectValue();

        // Hang a flag on the item so that we don't re-show the pick list
        this._valuePicked = true;
        this._pickedReverseValueMap = {};
        this._pickedReverseValueMap[displayValue] = value;
        // Note: updateValue() will fire change handlers and store the new data value.
        // The data value is derived back from the new display value via 'mapDisplayToValue()'

        this.updateValue();
        delete this._valuePicked;
    },

    // override setValue map to update the list and update the displayed value if necessary.
    setValueMap : function () {
        this.Super("setValueMap", arguments);
        if (this.pickList) {

            if (this.pickList.isVisible()) this.pickList.hide();

            // clear out the formItem property - ensures data / fields get re-set up when
            // the pickList is next shown.
            delete this.pickList.formItem
            // dont show the pickList if it's not already visible
            this.setUpPickList(this.pickList.isVisible());
        }
        // The display-version of the value is likely to have changed, so update the element
        // value at this point (but never update the typed-in value when addUnknownValues is
        // false)
        if (this.addUnknownValues) {
            var displayValue = this.getDisplayValue();
            if (isc.isAn.emptyString(displayValue) && this._getShowHintInField()) {
                this._showInFieldHint();
            } else {
                this.setElementValue(displayValue);
            }
        }
    },

    // Override setValue and mapValueToDisplay to ensure that on an explicit setValue() to
    // a value that's not currently loaded in the pickList dataSet, we show the display value
    // rather than the underlying data value.
    setValue : function (newValue, allowNullValue, partialValue,a,b) {
        // if setValue() is called on this item, but doesn't change the item value,
        // and addUnknownValues is false, and we have
        // a pending value entered in our text box right now, reset to display the pending
        // value rather than the display value for the specified value.
        // This means if a form item is redrawn we won't drop the pending value the
        // user entered.

        var valueChanged = this._value != newValue;
        if (!valueChanged) {
            if (this._pendingElementValue) {
                this.setElementValue(this._pendingEnteredValue);
                return;
            }
        }

        // clear the pending style
        this._markNotPending();

        this._programmaticSetValue = true;
        this.invokeSuper(isc.ComboBoxItem, "setValue", newValue, allowNullValue, partialValue, a, b);
        delete this._programmaticSetValue;

        // See if the in-field hint needs to be shown
        if (!this.hasFocus && this.showHint && this.showHintInField && this.getHint()) {
            var undef;
            if (newValue === undef || newValue == null || isc.is.emptyString(newValue)) {
                this._showInFieldHint();
            }
        }
        // treat a call to setValue like the user picking a value from the form

        if (partialValue || newValue == null) {
            this.explicitChoice = false;
        } else if (valueChanged) {
            this.explicitChoice = true;
        }
    },

    // override getDefaultValue to support defaultToFirstOption
    // override getDefaultValue to pick up the first option if defaultToFirstOption is true
    // getDefaultValue should not be able to return a value that is not included
    // in the valueMap for this select.
    getDefaultValue : function () {

        var dV = this.Super("getDefaultValue", arguments);
        if (dV == null && this.defaultToFirstOption) {
            dV = this.getFirstOptionValue();
        }
        return dV;
    },



    // Overrides to logic to generate / parse criteria for use in search forms / filter editors
    // ---
    // If addUnknownValues is false and we have a displayField, and we are set to an "unknown"
    // value (IE we don't have a selected record), the most likely scenario is that the user
    // has entered a partial displayField value in the text box.
    // In this case use the displayField as the criteria fieldName and the pickList filter operator
    // as the search operator.
    // Also handle being passed criterion for the display field value and entering them in the
    // text box.


    //> @attr comboBoxItem.generateExactMatchCriteria (boolean : null : IRWA)
    // When a comboBoxItem is used to generate search criteria in a SearchForm this property
    // governs whether, if the user explicitly chose an option from the pickList, we explicitly
    // generate criteria that will search for an exact match against the chosen value.
    // <P>
    // In order to achieve this, when this property is set to true, this item will generate
    // +link{AdvancedCriteria} in <var class="smartclient">its +link{getCriterion()} method</var>
    // <var class="smartgwt">the default <code>FormItemCriterionGetter</code>'s <code>getCriterion()</code>
    // method</var>.
    // <P>
    // See +link{shouldGenerateExactMatchCriteria()} for behavior when this flag is unset.
    // @visibility external
    //<

    //> @method comboBoxItem.shouldGenerateExactMatchCriteria() [A]
    // When a comboBoxItem is used to generate search criteria in a SearchForm,
    // if the user explicitly chose an option from the pickList, should the criterion generated
    // by <var class="smartclient">+link{getCriterion()}</var>
    // <var class="smartgwt">the <code>FormItemCriterionGetter</code>'s <code>getCriterion()</code> method</var>
    // enforce a search for an exact match against the chosen value?
    // <P>
    // In order to achieve this, when this property is set to true, this item will generate
    // +link{AdvancedCriteria} in <var class="smartclient">its +link{getCriterion()}</var>
    // <var class="smartgwt">the default <code>FormItemCriterionGetter</code>'s <code>getCriterion()</code></var>
    // method.
    // <P>
    // Default implementation will return +link{generateExactMatchCriteria} if specified, otherwise
    // true if the DataSource for this item
    // +link{DataSource.supportsAdvancedCriteria(),supports advanced criteria}, false if it
    // does not.
    // @return (Boolean) should getCriterion() generate exact-match search criteria when
    //   a value was explicitly chosen from this item's set of options?
    // @visibility external
    //<
    shouldGenerateExactMatchCriteria : function () {
        if (this.generateExactMatchCriteria != null) return this.generateExactMatchCriteria;
        var DS = this.form.getDataSource();

        if (DS && DS.supportsAdvancedCriteria()) return true;
        return false;
    },

    //> @method comboBoxItem.hasAdvancedCriteria()
    // Will this item return advancedCriteria if +link{DynamicForm.getValuesAsCriteria()} is
    // called on this item's form?
    // Overridden for ComboBoxItem to return true if +link{generateExactMatchCriteria} is true
    // - in this case if an exact value is chosen from our set of options (always the
    // case if <code>addUnkownValues</code> is false), we will use advancedCriteria to ensure
    // the generated search criteria exactly matches the chosen value for this item.
    // <P>
    // Note that +link{AdvancedCriteria} are not supported by all dataSources. When a form is bound
    // to a dataSource, we therefore default <code>generateExactMatchCriteria</code> to false unless
    // the dataSource is known to support AdvancedCriteria.
    // <P>
    // As with formItem.hasAdvancedCriteria() this will also return true if a +link{operator}
    // was explicitly specified for this item
    // @return (Boolean) true if the result of getCriterion() will be an AdvancedCriteria object.
    // @group criteriaEditing
    // @visibility external
    //<
    // Desired behavior is really to have explicitly chosen values always do an exact match
    // against the value field, whereas partially entered values will do a partial match against
    // the display field (as they would with the pickList). For this to happen reliably we would
    // essentially always return advanced criteria - but AC are not supported by all dataSources
    // so we don't want this to be our default behavior.
    hasAdvancedCriteria : function () {
        if (this.Super("hasAdvancedCriteria", arguments)) return true;
        if (this.addUnknownValues && this.explicitChoice && this.shouldGenerateExactMatchCriteria())
            return true;
        return false;
    },
    //> @method comboBoxItem.getCriterion()
    // Returns criterion derived from the current value of this item.
    // <P>
    // If +link{addUnknownValues} is true for this item, we implement the following behavior.<br>
    // If the user explicitly selected an item from the pickList, we treat this as an attempt
    // to explicitly match the data value. In this case returned criteria will match the
    // selected (data) value against this item's fieldName.<br>
    // If the user typed a value into the text field, we treat this as an attempt to do a substring
    // type filter. In this case returned criteria will match the entered text value against the
    // displayField for this item if one is specified.
    // <P>
    // If +link{addUnknownValues} is false we always match the chosen data value against the item's
    // fieldName.
    // <P>
    // Note that +link{shouldGenerateExactMatchCriteria()} will be called in the case when a
    // value was explicitly picked from the set of options. If that method returns true, we will
    // return AdvancedCriteria with an operator specified to ensure an exact match occurs.
    //
    // @return (Criterion) criterion object based on this fields current edited value(s).
    // @group criteriaEditing
    // @visibility external
    //<
    getCriteriaFieldName : function () {
        if (this.criteriaField != null) return this.criteriaField;

        if (this.displayField != null && this.addUnknownValues && !this.explicitChoice) {
            return this.displayField;
        }

        // Explicit choice from pickList - return the valueField for the item.
        // Note: DO NOT CALL Super.getCriteriaFieldName() - we subclass TextItem which returns
        // displayField if set
        return this.getDataPath() || this.getFieldName();
    },

    //> @method comboBoxItem.getCriteriaValue()
    // Overridden to return the text box value if a displayField is specified for this item
    // and the user explicitly entered a display value in the text box field rather than
    // picking a specific value from the set of available options in the pickList.
    // @return (any) filter criterion value for this field
    //<
    getCriteriaValue : function () {
        if (this.displayField != null && this.addUnknownValues && !this.explicitChoice) {
            return this.getEnteredValue();
        }
        return this.Super("getCriteriaValue", arguments);
    },

    getOperator : function (textMatchStyle) {
        var operator;
        // Note if addUnknownValues is false, return the filter operator if we're showing an
        // 'unknown' value, even if we don't have a displayField specified. In this case we
        // still want to do a substring match rather than an exact match.
        if (this.addUnknownValues && !this.explicitChoice) {
            operator = this.getPickListFilterOperator();
        } else {
            operator = this.Super("getOperator", arguments);
        }
        return operator;
    },

    //> @method comboBoxItem.canEditSimpleCriterion() [A]
    // Overridden to support editing simple criterion on the display field if one is specified
    // as well as the value field.
    // @return (boolean) true if this item can edit the specified fieldName
    //<
    canEditSimpleCriterion : function (fieldName) {
        if (this.getDataPath() && fieldName == this.getDataPath()) return true;
        if (this.criterionField) return fieldName == this.criterionField;
        if (this.displayField && fieldName == this.displayField) return true;
        return this.getFieldName() == fieldName;
    },

    // Override setSimpleCriterion - if the crit passed in is for the display field, we don't want
    // 'getCriterion()' to return a criterion on the data field.
    // Achieve this via the special "partialValue" param on setValue()
    setSimpleCriterion : function (value, fieldName) {
        if (this.criterionField == null && this.displayField != null && fieldName == this.displayField) {
            this.setValue(value, null, true);
        } else {
            this.setValue(value);
        }
    },


    //> @method comboBoxItem.canEditCriterion()
    // This method is overridden in comboBoxItem. When addUnknownValues is true,
    // comboBoxItems allow the user to edit substring match type criteria applied to the
    // display field (if one is specified).
    // <P>
    // The user can also edit criteria attempting to match exactly against the item's field name.
    //
    // @group criteriaEditing
    // @visibility external
    //<
    canEditCriterion : function (criterion, warnOnField) {
        if (!this.addUnknownValues) {
            return this.Super("canEditCriterion", arguments);
        }

        if (this.getDataPath() && criterion.fieldName == this.getDataPath()) return true;

        var critField = criterion.fieldName,
            valueField = this.criterionField || this.getFieldName(),
            // We have to invoke super implementation of getOperator to pick up
            // our standard operator (typically just "equals" as we have an option dataSource)
            // as we override getOperator() to look at this.getSelectedRecord() which is
            // irrelevant to whether we can handle new criteria passed in.
            defaultOperator = this.Super("getOperator", []);

        if (critField != null) {
            if (this.displayField == null) {
                if (critField == valueField) {
                    return criterion.operator == defaultOperator
                            || criterion.operator == this.getPickListFilterOperator();
                }
            } else {

                if (critField == valueField) {
                    return criterion.operator == defaultOperator;
                } else if (critField == this.displayField) {
                    return criterion.operator == this.getPickListFilterOperator();
                }
            }
        }
        return false;
    },

    //> @method comboBoxItem.setCriterion()
    // Overridden to support editing criterion against the display field or value field when
    // +link{addUnknownValues} is true.
    // @group criteriaEditing
    // @visibility external
    //<
    setCriterion : function (criterion) {
        if (!this.addUnknownValues || this.displayField == null ||
            criterion.fieldName != this.displayField)
        {
            return this.Super("setCriterion", arguments);
        }

        // This is essentially setValue() but taking an explicit displayValue rather than
        // data value.
        var displayVal = criterion.value;
        this.setElementValue(displayVal);
        this._checkForDisplayFieldValue(displayVal);
        var dataValue = this.mapDisplayToValue(displayVal);
        if (!this.compareValues(dataValue, this._value)) this.saveValue(dataValue);
    },

    // Override checkForDisplayFieldValue()
    // This is the method that, if we have a displayField specified, kicks off a fetch against
    // our optionDataSource to load the appropriate display value from the server.
    // In PickList based items we use the pickList data (if present) to map data to display
    // values.
    // Catch the case where checkForDisplayFieldValue is called when we are in the process of
    // fetching our pickList data from the server.
    // In this case we want to wait for the pickList data to be returned rather than kicking off
    // an additional fetch as our data value will usually be present in that pickList data.
    // When the pickList data returns we re-check this method. If the data is present, we're
    // done, otherwise we need to kick off another fetch as we didn't find our data value in
    // the pickList data array. This can happen if the pickList data is paged, for instance.

    _checkForDisplayFieldValue : function (newValue, delayed) {
        var inValueMap = (this._mapKey(newValue, true) != null);

        if (inValueMap) {
            return;
        }
        if (this._fetchingPickListData) {
            this._checkDisplayFieldValueOnFilterComplete = true;
            return;
        }
        // We can also bypass this if the pickList has a *complete* cache
        if (this.pickList != null && this.pickList.formItem == this &&
            isc.isA.ResultSet(this.pickList.data) && this.pickList.data.allRowsCached())
        {
            return;
        }
        this.invokeSuper(isc.ComboBoxItem, "_checkForDisplayFieldValue", newValue);
    },

    // Map valueToDisplay and mapDisplayToValue need to pick up
    // the mapping between displayField and valueField, if there is one.
    // We implement this by overriding _mapKey() / mapDisplayToValue() to check for the value in
    // our pickList's dataSet, in addition to checking against any explicitly specified valueMap


    _mapKey : function (value, dontReturnKey, a,b,c,d) {
        var displayValue = this.invokeSuper(isc.ComboBoxItem, "_mapKey", value, true ,a,b,c,d);

        // _translateFieldValue part of the pickList interface
        if (displayValue == null && this.getDisplayFieldName() != null)
            displayValue = this._translateValueFieldValue(value, false);
        if (displayValue == null && !dontReturnKey) displayValue = value;
        return displayValue;
    },


    mapDisplayToValue : function (value, a, b, c) {


        if (this._pickedReverseValueMap) {
            for (var i in this._pickedReverseValueMap) {
                if (i == value) {
                    return this._pickedReverseValueMap[i];
                }
            }
            delete this._pickedReverseValueMap;
        }
        if (this.getDisplayFieldName() != null) {
            var displayValue;
            displayValue = this._translateValueFieldValue(value, true);
            if (displayValue != null) value = displayValue;
        }
        return this.invokeSuper(isc.ComboBoxItem, "mapDisplayToValue", value, a,b,c);
    },

    _shouldAllowExpressions : function () {
        return false;
    }
});


isc.ComboBoxItem.registerStringMethods({
    dataArrived:"startRow,endRow,data",
    getPickListFilterCriteria:""
});


// Define this package and its core dependencies.
// Note:  MultiComboBoxItem uses "widgets/TileLayout", part of the ISC_Grids module, for its
// "flow" layout style.  It is not listed here as a dependency to allow this class to be used
// without pulling in the ISC_Grids module.


//> @class MultiComboBoxItem
// A MultiComboBoxItem is a combo box that allows the user to select multiple options.  Each
// selected option is represented as a button that can be clicked to deselect the option.
//
// The relative layout of the buttons to the combo box is specified with the
// +link{MultiComboBoxItem.layoutStyle} attribute.  The buttons will be kept in the order that
// they were added, with the most recently added button being adjacent to the combo box.
//
// MultiComboBoxItem uses the +link{AutoChild} pattern to construct the combo box and the buttons
// so that they can be easily customized.
// @see ComboBoxItem
// @inheritsFrom CanvasItem
// @example multiComboBoxItem
// @visibility comboBox
//<
isc.ClassFactory.defineClass("MultiComboBoxItem", "CanvasItem");

isc.MultiComboBoxItem.addClassProperties({
//> @type MultiComboBoxLayoutStyle
// Specifies the layout of the combo box and buttons in a MultiComboBoxItem.
// @value "flow" Use a flow layout
FLOW: "flow",
// @value "horizontal" Use a horizontal layout with the combo box on the right
HORIZONTAL: "horizontal",
// @value "horizontalReverse" Use a horizontal layout with the combo box on the left
HORIZONTAL_REVERSE: "horizontalReverse",
// @value "vertical" Use a vertical layout
VERTICAL: "vertical",
// @value "verticalReverse" Use a vertical layout with the combo box at the bottom
VERTICAL_REVERSE: "verticalReverse",
// @see MultiComboBoxItem.layoutStyle
// @visibility external
//<

// The name of the attribute of the buttons used to store the value represented by the buttons.
_$buttonDataAttribute: "_mcb_correspondingValue",

//> @classAttr MultiComboBoxItem.defaultHint (string : "Enter values" : R)
// The default hint string.
// @group i18nMessages
// @visibility external
//<
defaultHint: "Enter values"
});

isc.MultiComboBoxItem.addClassMethods({

// _binarySearch() returns the lowest index in <code>values</code> at which <code>value</code>
// may be inserted without breaking the sort order of <code>values</code>, as induced by
// <code>compareFn</code>.
//
// This function assumes that <code>values</code> are already sorted by <code>compareFn</code>.
// @param value (any) the value to search for.
// @param values (Array of any) an array of values in which to search.
// @param [compareFn] (Function) a comparator function used to compare values in
// <code>values</code> with <code>value</code>.  <code>compareFn</code> is called with two
// arguments. The first is a value from <code>values</code> and the second is always
// <code>value</code>.  <code>compareFn</code> defaults to
// MultiComboBoxItem._defaultCompareFn() if it is not specified.
// @param [strict] (boolean) Should this function search for identical values to <code>value</code>
// (via ===), or is a zero of <code>compareFn</code> sufficient for determining equality?  The
// default value is true.  Note that compareFn must return zero when passed identically equal
// values for this function to work correctly.
// @return (integer) If <code>value</code> is in <code>values</code> then this function returns
// the index of <code>value</code> in the array.  Otherwise, the return value is
// <code>-(insertion index)-1</code>, where the insertion index is the lowest index at which
// <code>value</code> could be inserted into <code>values</code> while maintaining the sort order.
_binarySearch : function (value, values, compareFn, strict) {
    if (!compareFn) {
        compareFn = isc.MultiComboBoxItem._defaultCompareFn;
    }
    if (strict === undefined) {
        strict = true;
    }

    var low = 0, len = values.length, high = len - 1;
    var i = 0, comparison;
    while (low <= high) {
        i = Math.floor((low + high) / 2);
        comparison = compareFn(values[i], value);
        if (comparison < 0) {
            low = i + 1;
        } else if (comparison > 0) {
            high = i - 1;
        } else {
            // `comparison` == 0

            // `values[i]` equals `value` according to the compare function. However,
            // it may be that `i` is in the middle of a range of equal values. Keep
            // decrementing `i` until it is the lowest index of that range.
            // If `strict` is true then we are actually looking to return the index of an
            // identically equal value in the `values` array.

            if (strict) {
                var j = i;
                while (j < len && compareFn(values[j], value) == 0) {
                    if (values[j] === value) {
                        return j;
                    }
                    ++j;
                }
            }

            while (i > 0 && compareFn(values[i - 1], value) == 0) {
                if (strict && values[i - 1] === value) {
                    return i - 1;
                }
                --i;
            }

            // i is the insertion index, so return -(insertion index)-1
            return -i-1;
        }
    }

    // Return the lowest index such that `values` at that index is greater than `value`.  That
    // is the index at which `value` could be inserted while maintaining sort order.
    // The actual return value is -(insertion index)-1, so that callers can know whether
    // the value was in the `values` array.
    if (comparison !== undefined && comparison < 0) {
        // values[i] < value, so i + 1 is the correct index.
        return -(i + 1)-1;
    } else {
        // values[i] > value, so i is the correct index.
        return -i-1;
    }
},

// Default comparator function used by _binarySearch() if <code>compareFn</code> is not provided.
// @param lhs (any)
// @param rhs (any)
// @return (number) -1 if <code>lhs</code> is less than <code>rhs</code>, 0 if <code>lhs</code>
// and <code>rhs</code> are equal, or 1 if
// <code>lhs</code> is greater than <code>rhs</code>
_defaultCompareFn : function (lhs, rhs) {
    if (lhs < rhs) {
        return -1;
    } else if (lhs > rhs) {
        return 1;
    } else {
        return 0;
    }
},

// Moves the member of the layout `oldLayout` at `memberIndex` to index `newIndex` in another
// layout `newLayout`.
_transferMember : function (oldLayout, memberIndex, newLayout, newIndex, dontAnimate) {
    var member = oldLayout.getMember(memberIndex);
    if (oldLayout.Class == "FlowLayout" || newLayout.Class == "FlowLayout") {
        oldLayout.removeMember(member, dontAnimate);
    }
    newLayout.addMember(member, newIndex, dontAnimate);
},

// Moves members of `oldLayout` occupying a range of indices to `newLayout`.
_transferMembers : function (numMembers, oldLayout, memberIndex, newLayout, newIndex, dontAnimate) {
    var _transferMember = isc.MultiComboBoxItem._transferMember;
    for (var i = 0; i < numMembers; ++i) {
        _transferMember(oldLayout, memberIndex, newLayout, newIndex + i, dontAnimate);
    }
},

// Similar to `_transferMembers`, but the members that are moved are placed in reverse order.
_transferMembersReverse : function (numMembers, oldLayout, memberIndex, newLayout, newIndex, dontAnimate) {
    var _transferMember = isc.MultiComboBoxItem._transferMember;
    for (var i = 0; i < numMembers; ++i) {
        _transferMember(oldLayout, memberIndex + (numMembers-i-1), newLayout, newIndex + i, dontAnimate);
    }
},

isOrderReversedForStyle : function (layoutStyle) {
    return layoutStyle == isc.MultiComboBoxItem.HORIZONTAL ||
           layoutStyle == isc.MultiComboBoxItem.VERTICAL_REVERSE;
}
});

isc.MultiComboBoxItem.addProperties({

//> @attr multiComboBoxItem.shouldSaveValue (Boolean : true : IR)
// @include FormItem.shouldSaveValue
//<
shouldSaveValue: true,

//> @attr MultiComboBoxItem.optionDataSource (DataSource | String : null : IR)
// The <code>optionDataSource</code> of the combo box.
// @see ComboBoxItem.optionDataSource
// @visibility external
//<

//> @attr MultiComboBoxItem.autoFetchData (Boolean : false : IR)
// Should the MultiComboBoxItem fetch data from the +link{optionDataSource,data source}
// immediately or wait until the user first opens the pickList.
// @see ComboBoxItem.autoFetchData
// @visibility external
//<

//> @attr MultiComboBoxItem.valueMap (Array or Object : null : IRW)
// The <code>valueMap</code> of the combo box.
// @see FormItem.valueMap
// @visibility external
//<

//> @attr MultiComboBoxItem.comboBox (AutoChild ComboBoxItem : null : RA)
// An +link{AutoChild} attribute to create the combo box in a MultiComboBoxItem.
// @visibility external
//<

//> @attr MultiComboBoxItem.comboBoxProperties (ComboBoxItem Properties : null : IRA)
// Properties to be used in creating a +link{ComboBoxItem}.
// @visibility external
//<

//> @attr MultiComboBoxItem.comboBoxDefaults (ComboBoxItem Properties : null : RA)
// Default properties for the +link{ComboBoxItem} in a MultiComboBoxItem.
// @visibility external
//<
comboBoxDefaults: {
    showTitle: false,
    width: "*",
    hint: isc.MultiComboBoxItem.defaultHint,
    showHintInField: true,
    completeOnTab: true,
    shouldSaveValue: false,
    editorType: "ComboBoxItem"
},

//> @attr MultiComboBoxItem.displayField (string : null : IRA)
// The <code>displayField</code> of the combo box.
// @see ComboBoxItem.displayField
// @visibility external
//<

//> @attr MultiComboBoxItem.valueField (string : null : IR)
// The <code>valueField</code> of the combo box.
// @see ComboBoxItem.valueField
// @visibility external
//<

//> @attr MultiComboBoxItem.button (MultiAutoChild IButton : null : RA)
// An +link{AutoChild} attribute used to create the buttons in the MultiComboBoxItem.
// @visibility external
//<

//> @attr MultiComboBoxItem.buttonProperties (IButton Properties : null : IRA)
// Properties to be used in creating each option button.
// @visibility external
//<

//> @attr MultiComboBoxItem.buttonDefaults (IButton Properties : null : RA)
// Default properties to be used in creating each +link{IButton}.
// @visibility external
//<
buttonDefaults: {
    _constructor: "IButton",
    align: "left",
    icon: "[SKIN]actions/remove.png",
    iconAlign: "right",
    iconOrientation: "right",

    click : function () {
        this.creator._buttonClick(this);
    }
},

//> @attr MultiComboBoxItem.layoutStyle (MultiComboBoxLayoutStyle : "flow" : IRW)
// Specifies the layout style of the combo box and the buttons in the MultiComboBoxItem.
// Available values are "flow" (the default), "horizontal", "horizontalReverse",
// "vertical", and "verticalReverse".
//
// <ul>
// <li><b>"flow"</b>:&nbsp; The buttons appear to the left of the combo box.  When there is no
// more room, the combo box and/or buttons flow onto a new line.  The buttons autoFit
// by default.</li>
// <li><b>"horizontal"</b>:&nbsp; The combo box appears on right and buttons are horizontally
// stacked directly left of it.  The buttons must autofit.</li>
// <li><b>"horizontalReverse"</b>:&nbsp; Like "horizontal" but the combo box appears on the left.
// The buttons must autofit.</li>
// <li><b>"vertical"</b>:&nbsp; The combo box appears on top and buttons are stacked beneath it.
// Buttons do not autofit by default.</li>
// <li><b>"verticalReverse"</b>:&nbsp; Like "vertical" but the combo box appears at bottom.
// The buttons do not autofit by default.</li>
// </ul>
// @visibility external
//<
layoutStyle: isc.MultiComboBoxItem.FLOW,

//> @attr MultiComboBoxItem.autoFitButtons (boolean : null : IR)
// Specifies whether to autofit the buttons in the MultiComboBoxItem.  The
// default value is true if +link{layoutStyle} is "flow", but false for a
// layoutStyle of "vertical" or "verticalReverse".  If the <code>layoutStyle</code>
// is "horizontal" or "horizontalReverse" then the buttons will autofit regardless
// of the setting of this property.
// @visibility external
//<

//> @attr MultiComboBoxItem.comboBoxWidth (number : null : IRW)
// Specifies the size of the combo box field.
// <P>
// Note that this attribute only has an effect in "flow", "horizontal", and
// "horizontalReverse" +link{layoutStyle,modes}.  In the other modes, the combo box
// is as wide as the overall MultiComboBoxItem.
// @visibility external
//<
comboBoxWidth: 130,

//> @attr MultiComboBoxItem.useInsertionOrder (Boolean : true : IR)
// Specifies whether to arrange the buttons of the MultiComboBoxItem in the order that they were
// selected (the default), or to sort the buttons by +link{displayField}.
// @visibility external
//<
useInsertionOrder: true,

// A function taking two arguments, both records, that returns a number less than zero,
// greater than zero, or exactly zero if the first record is less than, greater than, or equal
// to the second record, respectively.
sortOrder: function (recordA, recordB) {
    var displayField = this.getDisplayFieldName(),
        a = recordA[displayField],
        b = recordB[displayField];

    return isc.MultiComboBoxItem._defaultCompareFn(a, b);
}

// The ComboBoxItem
//_comboBoxItem: null,

// A list of the values of the buttons in the MultiComboBoxItem, in order of insertion, with
// the most recently inserted value at index 0.
//_buttonValues: [],

// A duplicate of _buttonValues in a sorted order as determined by sortOrder.
//_sortedButtonValues: [],

// A map from the button value to the button
//_valueToButton: {},

// A list of selected values where corresponding buttons have not yet been created because the
// full data record for these values has not yet been loaded from the server.
//_unavailableButtonValues: []

// Stores the buttons and the combo box
//_buttonsLayout: null,

// Stores the <code>dataValue</code> argument of <code>showValue()</code> until the
// MultiComboBoxItem is drawn.
//_dataValue: null,
});

isc.MultiComboBoxItem.addMethods({

init : function () {
    this.multiple = true;
    this.fetchMissingValues = true;
    if (this.value == null && this.defaultValue != null) {
        this.value = this.defaultValue;
    }
    this._buttonValues = [];
    this._sortedButtonValues = [];
    this._valueToButton = {};
    this._unavailableButtonValues = [];
    return this.Super("init", arguments);
},

// Invokes `CanvasItem.storeValue()` with a duplicate of `_buttonValues`.
_storeValue : function () {
    this.storeValue(this._buttonValues.duplicate());
},

// Create a button to represent a selected option having valueField <code>value</code>.
// @param value (any)
// @param displayValue (HTMLString)
_createButtonForValue : function (value, displayValue) {
    var button = this.createAutoChild("button", {
        title: displayValue
    });
    this._valueToButton[value] = button;

    var autoFitButtons = this._getAutoFitButtons();
    if (!autoFitButtons) {
        button.setWidth(this.canvas.getWidth());
    }
    button.setAutoFit(autoFitButtons);

    button[isc.MultiComboBoxItem._$buttonDataAttribute] = value;
    return button;
},

_comboBoxItemChanged : function (embeddedForm, comboBoxItem) {
    var value = comboBoxItem.getValue();

    // A value has been selected using the combo box's pick list.  The pick list should
    // have the full record for the selected value, because it was able to display
    // the option to the user.  Copy that record to the displayField cache (maintained
    // by FormItem) so that it can provide the display value for the button title, and
    // offer data for use by the sort function.  FormItem's displayField cache is reused
    // so that we don't have to maintain a separate cache and we can use FormItem's
    // implementation of mapValueToDisplay() and getDisplayValue().
    var resultSet = this._comboBoxItem.getPickListResultSet();
    if (resultSet != null) {
        var valueField = this.getValueFieldName(),
            record = resultSet.find(valueField, value);

        if (record != null) {
            var needsRefresh = this._addRecordToDisplayFieldCache(record);
            // assert !needsRefresh
            this.updateDisplayValueMap(false);
        }
    }

    this._insertButtonForValue(value);
    this._storeValue();

    // Clear the combo box item's text field so that more entries can be added.
    comboBoxItem.clearValue();
},

// Convert an index of a button in `_buttonValues` or `_sortedButtonValues` to an index into
// `_buttonsLayout`.  The conversion depends on the layout style and the sort type (insertion
// order or by `sortOrder`).
_transformIndex : function (k, j, isInsert) {
    var useInsertionOrder = this.useInsertionOrder,
        layoutStyle = this.layoutStyle,
        numButtons = this._buttonValues.length,
        index;

    if (useInsertionOrder) {
        // The most recently added buttons appear next to the combo box.
        // In "flow" mode, new buttons appear at the highest index.
        // In the other modes, new buttons appear at index 0.
        // Note that although the order is reversed in "verticalReverse" (or in
        // "horizontalReverse"), the layout handles the reversal and so the
        // combo box is still said to be at index 0 (or numButtons).
        if (layoutStyle == isc.MultiComboBoxItem.FLOW) {
            index = numButtons - k - 1;
            if (isInsert) {
                ++index;
            }
        } else {
            // assert layoutStyle == isc.MultiComboBoxItem.HORIZONTAL ||
            //        layoutStyle == isc.MultiComboBoxItem.HORIZONTAL_REVERSE ||
            //        layoutStyle == isc.MultiComboBoxItem.VERTICAL ||
            //        layoutStyle == isc.MultiComboBoxItem.VERTICAL_REVERSE
            index = k;
        }
    } else {
        index = j;
    }

    if ((useInsertionOrder && layoutStyle == isc.MultiComboBoxItem.HORIZONTAL) ||
        layoutStyle == isc.MultiComboBoxItem.HORIZONTAL_REVERSE ||
        layoutStyle == isc.MultiComboBoxItem.VERTICAL ||
        (useInsertionOrder && layoutStyle == isc.MultiComboBoxItem.VERTICAL_REVERSE))
    {
        // The combo box is at index zero and the buttons start at index 1.
        ++index;
    }
    return index;
},

// Converts `sortOrder` to a function of arguments that are values of `valueField`.
_getSortOrder : function () {
    var dataSource = this.getOptionDataSource();
    if (dataSource != null) {
        // If the MultiComboBoxItem is data source driven then return a compare
        // function that will compare by displayField value.

        var self = this,
            valueField = this.getValueFieldName();

        return function (a, b) {
            var cache = self._displayFieldCache,
                recordA = (cache == null ? null : cache.find(valueField, a)),
                recordB = (cache == null ? null : cache.find(valueField, b));

            if (recordA == null || recordB == null) {
                var ods = self.getOptionDataSource(),
                    odsCacheData = (ods == null ? null : ods.getCacheData());
                if (odsCacheData != null) {
                    if (recordA == null) recordA = odsCacheData.find(valueField, a);
                    if (recordB == null) recordB = odsCacheData.find(valueField, b);
                }
            }

            return self.sortOrder.call(self, recordA, recordB);
        }
    } else {
        // If the MultiComboBoxItem is based on a static valueMap then use
        // a simple compare function.
        return isc.MultiComboBoxItem._defaultCompareFn;
    }
},

// Ensure that a button corresponding to the option with a valueField of <code>value</code> is
// in the _buttonsLayout.  This method will create such a button if necessary and insert it
// into the layout while maintaining a sorted order.
// @param value (any)
_insertButtonForValue : function (value) {
    var sortOrder = this._getSortOrder();

    // Determine the sorted index of `value` in `_sortedButtonValues`
    var j = isc.MultiComboBoxItem._binarySearch(
                value, this._sortedButtonValues, sortOrder),
        exists = (j >= 0);

    // If the selected value is not the same as one already present,
    // then create a new button and add it.
    if (!exists) {
        j = -(j + 1); // the insertion index

        var displayValue = this.getDisplayValue(value),
            button = this._createButtonForValue(value, displayValue),
            index = this._transformIndex(0, j, true);

        this._buttonValues.unshift(value);
        this._sortedButtonValues.addAt(value, j);
        this._buttonsLayout.addMember(button, index);

    } else {
        var index = this._transformIndex(
                // Compute an index into _buttonValues only if necessary:
                !this.useInsertionOrder || this._buttonValues.indexOf(value), j, false);

        // "Flash" the existing button.
        var existingButton = this._buttonsLayout.getMember(index);
        existingButton.animateFade(50, function () {
            existingButton.animateFade(100, null, 100);
        }, 100);
    }
},

_$finishedHidingButton: "_finishedHidingButton",
_buttonClick : function (button) {
    // ignore button clicks if the item itself is readOnly
    if (this.isReadOnly()) return;

    var value = button[isc.MultiComboBoxItem._$buttonDataAttribute],
        sortOrder = this._getSortOrder(),
        j = isc.MultiComboBoxItem._binarySearch(value, this._sortedButtonValues, sortOrder),
        k = this._buttonValues.indexOf(value);



    // Remove the button from the layout
    delete this._valueToButton[value];
    var buttonsLayout = this._buttonsLayout;
    buttonsLayout.hideMember(button, {target: this, methodName: this._$finishedHidingButton, args: [buttonsLayout, button]});

    // Remove the record corresponding to the value from the FormItem's displayField cache.
    this._removeValueFromDisplayFieldCache(value);

    this._buttonValues.splice(k, 1);
    this._sortedButtonValues.splice(j, 1);
    this._storeValue();
},

_finishedHidingButton : function (buttonsLayout, button) {
    buttonsLayout.removeMember(button, true);
    button.destroy();
},

// Clears the _buttonsLayout and sets the value of the MultiComboBoxItem to an empty array.
_removeAllButtons : function () {
    var layout = this._buttonsLayout,
        layoutStyle = this.layoutStyle,
        useInsertionOrder = this.useInsertionOrder;

    if (layoutStyle == isc.MultiComboBoxItem.FLOW) {
        // The combo box item (tile) is last.
        for (var ri = layout.tiles.length - 1; ri > 0; --ri) {
            var button = layout.tiles[ri - 1];

            layout.removeTile(button);
            button.destroy();
        }
    } else {
        var buttons;
        if ((useInsertionOrder && layoutStyle == isc.MultiComboBoxItem.HORIZONTAL) ||
            layoutStyle == isc.MultiComboBoxItem.HORIZONTAL_REVERSE ||
            layoutStyle == isc.MultiComboBoxItem.VERTICAL ||
            (useInsertionOrder && layoutStyle == isc.MultiComboBoxItem.VERTICAL_REVERSE))
        {
            // The combo box item is at index zero.
            buttons = layout.members.slice(1);
        } else {
            // The combo box item is last.
            buttons = layout.members.slice(0, -1);
        }

        layout.removeMembers(buttons, true);
        buttons.map("destroy");
    }

    // Clear out the FormItem's displayField cache.
    if (!this.isDrawn()) {
        this.invalidateDisplayValueCache(false);
    }

    this._valueToButton = {};
    this._buttonValues = [];
    this._sortedButtonValues = [];
    this._unavailableButtonValues = [];
},

// Override drawn() here to re-invoke showValue() with arguments that were passed to that
// function before the MultiComboBoxItem was drawn.
drawn : function () {
    var ret = this.Super("drawn", arguments);
    if (this._dataValue !== undefined) {
        var displayValue = this.getDisplayValue(this._dataValue);
        this.showValue(displayValue, this._dataValue, this.form, this);
        delete this._dataValue;
    }
    return ret;
},

//> @method MultiComboBoxItem.showValue
// @include CanvasItem.showValue
//
// The value of a MultiComboBoxItem to the form is an array of valueField values corresponding
// to the selected combo box options.
// @visibility external
//<
showValue : function (displayValue, dataValue, form, item) {
    if (!this.isDrawn()) {
        this._dataValue = dataValue;
    } else if (!dataValue) {
        this._removeAllButtons();
    } else if (isc.isAn.Array(dataValue)) {
        this._removeAllButtons();
        for (var i = 0, dataValueLength = dataValue.length; i < dataValueLength; ++i) {
            var value = dataValue[i];

            if (this._isValueInCache(value)) {
                this._insertButtonForValue(value, displayValue[i]);
            } else {
                this._unavailableButtonValues.push(value);
            }
        }
    }
},

// Checks whether the display value corresponding to the field value <code>value</code> has
// been fetched from the data source and is available.
_isValueInCache : function (value) {
    // _mapKey() is an internal method of FormItem.  The second argument set to true forces
    // _mapKey() to return null if the value is not in the valueMap.
    return (!this.optionDataSource || (this._mapKey(value, true) !== null));
},

// Override FormItem.setValueMap() to set the valueMap on both this form item and the combo box.
setValueMap : function (valueMap) {
    this.Super("setValueMap", arguments);
    this._comboBoxItem.setValueMap.apply(this._comboBoxItem, arguments);
},

updateValueMap : function (refreshDisplay) {
    // Because the valueMap changed, we need to ensure that the button titles reflect the current
    // display values, reconstruct `_sortedButtonValues` according to the new data, and, if we
    // are not in insertion order mode, rearrange the buttons according to the new sorted order.

    var sortOrder = this._getSortOrder(),
        autoFitButtons = this._getAutoFitButtons(),
        useInsertionOrder = this.useInsertionOrder,
        numButtons = this._buttonValues.length;

    var width100 = this.canvas.getWidth();

    var sortedButtonValues = [];
    for (var i = 0; i < numButtons; ++i) {
        var value = this._buttonValues[i],
            displayValue = this.getDisplayValue(value),
            j = isc.MultiComboBoxItem._binarySearch(
                    value, sortedButtonValues, sortOrder),
            button = this._valueToButton[value];

        button.setTitle(displayValue);

        // Refresh the autoFit of the button
        if (!autoFitButtons) {
            button.setWidth(width100);
        }
        button.setAutoFit(autoFitButtons);

        sortedButtonValues.addAt(value, -(j + 1));
    }
    var oldSortedButtonValues = this._sortedButtonValues;
    this._sortedButtonValues = sortedButtonValues;

    // If the sorted order of the values changes, then rearrange the corresponding buttons
    // into sorted order.
    if (!useInsertionOrder) {
        var buttonsLayout = this._buttonsLayout;
        for (var i = 0; i < numButtons; ++i) {
            var value = oldSortedButtonValues[i],
                j = sortedButtonValues.indexOf(value);

            if (i != j) {
                buttonsLayout.reorderMember(
                        this._transformIndex(undefined, i, false),
                        this._transformIndex(undefined, j, false));
            }
        }
    }

    // The fetchMissingValues feature ultimately calls this method after it receives
    // display values fetched from the server.  Those display values may be available
    // here so use them to create buttons corresponding to selected values.
    if (!this._unavailableButtonValues.isEmpty()) {
        var len = this._unavailableButtonValues.length,
            offset = 0;

        for (var i = 0; i < len; ++i) {
            var j = i - offset, value = this._unavailableButtonValues[j];

            if (this._isValueInCache(value)) {
                var displayValue = this.getDisplayValue(value);
                this._insertButtonForValue(value, displayValue);
                this._unavailableButtonValues.splice(j, 1);
                ++offset;
            }
        }
    }
},

//> @method MultiComboBoxItem.setLayoutStyle()
// @param layoutStyle (MultiComboBoxLayoutStyle) the new layout style
// @see layoutStyle
// @visibility external
//<
setLayoutStyle : function (layoutStyle) {
    var oldLayoutStyle = this.layoutStyle;

    if (!layoutStyle || (oldLayoutStyle == layoutStyle)) {
        return;
    }

    // Some information that is used throughout this method:
    // When useInsertionOrder is true the HORIZONTAL, HORIZONTAL_REVERSE, VERTICAL, and
    // VERTICAL_REVERSE layout styles all have the combo box item at index 0.  HORIZONTAL
    // and VERTICAL_REVERSE display the combo box at the last index (at numButtons), so
    // these both use Layout.reverseOrder to flip the buttons and combo box.
    // In FLOW mode or if useInsertionOrder is false then the buttons are at the index
    // at which they are displayed so reversing the order of the buttons and combo box
    // is achieved by actually moving the buttons around in the layout (or moving them
    // in reverse order to another layout).

    var FLOW = isc.MultiComboBoxItem.FLOW,
        HORIZONTAL = isc.MultiComboBoxItem.HORIZONTAL,
        HORIZONTAL_REVERSE = isc.MultiComboBoxItem.HORIZONTAL_REVERSE,
        VERTICAL = isc.MultiComboBoxItem.VERTICAL,
        VERTICAL_REVERSE = isc.MultiComboBoxItem.VERTICAL_REVERSE,
        transferMember = isc.MultiComboBoxItem._transferMember,
        _transferMembers = isc.MultiComboBoxItem._transferMembers,
        _transferMembersReverse = isc.MultiComboBoxItem._transferMembersReverse,

        useInsertionOrder = this.useInsertionOrder,
        comboBoxWidth = this.comboBoxWidth,
        dontAnimate = true,
        numButtons = this._buttonValues.length,
        oldReverseOrder = oldLayoutStyle == HORIZONTAL ||
                          oldLayoutStyle == VERTICAL_REVERSE,
        reverseOrder = isc.MultiComboBoxItem.isOrderReversedForStyle(layoutStyle),

        // Used only if oldLayoutStyle or layoutStyle is FLOW
        transferMembers = (useInsertionOrder ? _transferMembersReverse : _transferMembers);

    if (layoutStyle == FLOW) {

        if (!isc.FlowLayout) {
            // If the FlowLayout class is unavailable then log a message and keep the old
            // layout style.

            isc.Log.logWarn(
                "The ISC_Grids module must be loaded to use layoutStyle:\"flow\".  " +
                "The MultiComboBoxItem will continue to use the \"" + oldLayoutStyle + "\"" +
                " layout style.");
            return;
        }

        // "flow" uses FlowLayout instead of HStack or VStack for _buttonsLayout, so a
        // FlowLayout will need to be created and the buttons and combo box transferred
        // to it.  In insertion order mode the buttons are reversed as they are transfered
        // to the FlowLayout.

        var buttonsLayout = this._createFlowLayout(),
            animateTileChange = buttonsLayout.animateTileChange;

        buttonsLayout.animateTileChange = false;

        transferMember(this._buttonsLayout,
            (!useInsertionOrder && oldReverseOrder ? numButtons : 0),
            buttonsLayout, 0, dontAnimate);
        transferMembers(
                numButtons, this._buttonsLayout, 0, buttonsLayout, 0, dontAnimate);

        this.canvas.removeMember(0, dontAnimate);
        this._buttonsLayout.destroy();
        this._buttonsLayout = buttonsLayout;
        this.canvas.addMember(buttonsLayout, 0, dontAnimate);

    } else if (layoutStyle == HORIZONTAL || layoutStyle == HORIZONTAL_REVERSE ||
               layoutStyle == VERTICAL || layoutStyle == VERTICAL_REVERSE)
    {
        // The "horizontal" and "horizontalReverse" layout styles use an HStack for
        // _buttonsLayout, while "vertical" and "verticalReverse" use a VStack.
        // If the type of layout is different between the old layout style and the new
        // layout style, then a new HStack or VStack will need to be created and all of
        // the buttons will need to be moved into it.

        var isHorizontal = (layoutStyle == HORIZONTAL || layoutStyle == HORIZONTAL_REVERSE),
            oldIsHorizontal = (oldLayoutStyle == HORIZONTAL ||
                               oldLayoutStyle == HORIZONTAL_REVERSE),
            createLayout = (isHorizontal ? this._createHStack : this._createVStack);

        if (oldLayoutStyle == FLOW || (isHorizontal != oldIsHorizontal))
        {
            var buttonsLayout = createLayout();
            if (oldLayoutStyle == FLOW) {
                this._buttonsLayout.animateTileChange = false;
                transferMember(this._buttonsLayout, numButtons, buttonsLayout, 0, dontAnimate);
                transferMembers(
                        numButtons, this._buttonsLayout, 0, buttonsLayout,
                        (!useInsertionOrder && reverseOrder ? 0 : 1),
                        dontAnimate);
            } else if (useInsertionOrder || (oldReverseOrder == reverseOrder)) {
                // Move everything in the same order.
                _transferMembers(
                        numButtons+1, this._buttonsLayout, 0, buttonsLayout, 0, dontAnimate);
            } else {
                // Move the combo box from back to front, or front to back, and move the
                // buttons in order.
                transferMember(this._buttonsLayout, (oldReverseOrder ? numButtons : 0),
                        buttonsLayout, 0, dontAnimate);
                _transferMembers(numButtons, this._buttonsLayout, 0,
                        buttonsLayout, (oldReverseOrder ? 1 : 0), dontAnimate);
            }
            this.canvas.removeMember(this._buttonsLayout, dontAnimate);
            this._buttonsLayout.destroy();
            this._buttonsLayout = buttonsLayout;
            this.canvas.addMember(buttonsLayout, 0, dontAnimate);
            if (useInsertionOrder && reverseOrder) {
                this._buttonsLayout.reverseOrder = true;
                this._buttonsLayout.reflow();
            }
        } else if (useInsertionOrder) {
            // Reverse the order of the members if necessary
            if (reverseOrder != oldReverseOrder) {
                this._buttonsLayout.reverseOrder = reverseOrder;
                this._buttonsLayout.reflow();
            }
        } else {
            // assert !useInsertionOrder

            // Under sorted order, just the combo box needs to be moved.
            if (!reverseOrder) {
                transferMember(
                        this._buttonsLayout, numButtons, this._buttonsLayout, 0, dontAnimate);
            } else {
                transferMember(
                        this._buttonsLayout, 0, this._buttonsLayout, numButtons, dontAnimate);
            }
        }
    } else {
        return;
    }

    this.layoutStyle = layoutStyle;

    // Set the width of the combo box to the full field width in VERTICAL and VERTICAL_REVERSE
    // modes, or to this.comboBoxWidth in FLOW, HORIZONTAL, and HORIZONTAL_REVERSE modes.
    if (layoutStyle == FLOW) {
        this._buttonsLayout.getMember(numButtons).setWidth(comboBoxWidth);
    } else {
        // assert layoutStyle == HORIZONTAL || layoutStyle == HORIZONTAL_REVERSE ||
        //        layoutStyle == VERTICAL || layoutStyle == VERTICAL_REVERSE

        var width = (layoutStyle == HORIZONTAL || layoutStyle == HORIZONTAL_REVERSE ?
                     comboBoxWidth : this.canvas.getWidth()),
            reverseOrder = isc.MultiComboBoxItem.isOrderReversedForStyle(layoutStyle),
            i = (useInsertionOrder || !reverseOrder ? 0 : numButtons);

        this._buttonsLayout.getMember(i).setWidth(comboBoxWidth);
    }

    // Set the autoFit on the buttons.
    // The autoFitButtons property can change with the layout
    // style if the user did not explicitly provide a value for the
    // autoFitButtons attribute and so it assumes a default value according to
    // the current layout style.
    // `autoFit` is set in any case to make sure that the buttons have the correct width.
    this._setAutoFitButtons(this._getAutoFitButtons(layoutStyle));

    if (animateTileChange !== undefined) {
        buttonsLayout.animateTileChange = animateTileChange;
    }
},

destroy : function () {
    if (this.embeddedForm) this.embeddedForm.destroy();
    if (this._buttonsLayout) this._buttonsLayout.destroy();
    this.Super("destroy", arguments);
},

_setAutoFitButtons : function (autoFitButtons) {
    var i = (this.layoutStyle == isc.MultiComboBoxItem.FLOW ||
             (!this.useInsertionOrder &&
              (this.layoutStyle == isc.MultiComboBoxItem.HORIZONTAL ||
               this.layoutStyle == isc.MultiComboBoxItem.VERTICAL_REVERSE)) ? 0 : 1),
        numButtons = this._buttonValues.length,
        imax = i + numButtons,
        width100 = this.canvas.getWidth();

    for (; i < imax; ++i) {
        var button = this._buttonsLayout.getMember(i);

        if (!autoFitButtons) {
            button.setWidth(width100);
        }
        button.setAutoFit(autoFitButtons);
    }
},

//> @method MultiComboBoxItem.setAutoFitButtons
// Sets the +link{autoFitButtons} property.
// @param autoFitButtons (boolean) whether to autofit the buttons
// @visibility external
//<
setAutoFitButtons : function (autoFitButtons) {
    var oldAutoFitButtons = this._getAutoFitButtons(),
        newAutoFitButtons = this._getAutoFitButtons(null, autoFitButtons);

    this.autoFitButtons = autoFitButtons;

    if (oldAutoFitButtons != newAutoFitButtons) {
        this._setAutoFitButtons(newAutoFitButtons);
    }
},

_createCanvas : function () {
    var _this = this;

    if (this.layoutStyle == isc.MultiComboBoxItem.FLOW && !isc.FlowLayout) {
        // If isc.FlowLayout is not available then the MultiComboBoxItem cannot have the "flow"
        // layout style.  In this case the layout style will default to "verticalReverse".
        // This is possible because "widgets/TileLayout" is not listed as a dependency of this
        // package, so the FlowLayout class may not have been loaded.

        isc.Log.logWarn(
                "The ISC_Grids module must be loaded to use layoutStyle:\"flow\".  " +
                "The MultiComboBoxItem will default to using the " +
                "\"" + isc.MultiComboBoxItem.VERTICAL_REVERSE + "\" layout style.");
        this.layoutStyle = isc.MultiComboBoxItem.VERTICAL_REVERSE;
    }

    var comboBoxWidth = 0;
    if (this.layoutStyle == isc.MultiComboBoxItem.FLOW ||
        this.layoutStyle == isc.MultiComboBoxItem.HORIZONTAL ||
        this.layoutStyle == isc.MultiComboBoxItem.HORIZONTAL_REVERSE)
    {
        comboBoxWidth = this.comboBoxWidth;
    } else {
        comboBoxWidth = "100%";
    }

    var comboBoxItemProperties = isc.addProperties({}, this.comboBoxDefaults,
            this.comboBoxProperties, {
                // Provide a dummy name for the combo box item to satisfy the
                // requirement that each FormItem must have a name that is a valid
                // JavaScript identifier.
                name: "_dummy",

                optionDataSource: this.optionDataSource,
                valueMap: this.valueMap,
                displayField: this.displayField,
                valueField: this.valueField,
                addUnknownValues: false, // Prevent a "changed" event when the user types
                                         // in something that is not a known option.
                changed : function (embeddedForm, comboBoxItem) {
                    _this._comboBoxItemChanged.apply(_this, [embeddedForm, comboBoxItem]);
                }
            });

    var autoFetchData = (this.autoFetchData !== undefined ?
                         this.autoFetchData : comboBoxItemProperties.autoFetchData);
    comboBoxItemProperties.autoFetchData = autoFetchData;
    var embeddedForm = this.embeddedForm = isc.DynamicForm.create({
        numCols: 1,
        width: comboBoxWidth,
        fields: [comboBoxItemProperties]
    });

    this._comboBoxItem = embeddedForm.getItems()[0];

    // Set _buttonsLayout according the the value of layoutStyle.
    // The layout has initial height:1 so that the MultiComboBoxItem's height will be the minimum
    // height required to fit the buttonsLayout.
    var canvasProperties = isc.addProperties({}, this.canvasProperties, {
        autoDraw: false,
        height: 1
    });

    var reverseOrder = isc.MultiComboBoxItem.isOrderReversedForStyle(this.layoutStyle);
    if (this.layoutStyle == isc.MultiComboBoxItem.HORIZONTAL ||
        this.layoutStyle == isc.MultiComboBoxItem.HORIZONTAL_REVERSE) {

        this._buttonsLayout = this._createHStack();
        this._buttonsLayout.reverseOrder = reverseOrder;

    } else if (this.layoutStyle == isc.MultiComboBoxItem.VERTICAL ||
               this.layoutStyle == isc.MultiComboBoxItem.VERTICAL_REVERSE)
    {
        this._buttonsLayout = this._createVStack();
        this._buttonsLayout.reverseOrder = reverseOrder;

    } else if (this.layoutStyle == isc.MultiComboBoxItem.FLOW) {
        this._buttonsLayout = this._createFlowLayout();
    }
    this._buttonsLayout.addMember(embeddedForm);
    canvasProperties.members = [this._buttonsLayout];

    this.canvas = isc.HLayout.create(canvasProperties);
    this.Super("_createCanvas", arguments);
},

readOnlyDisplayChanged : function (appearance) {
    if (this.embeddedForm) {
        this.embeddedForm.readOnlyTextBoxStyle = this.getReadOnlyTextBoxStyle();
        this.embeddedForm.setReadOnlyDisplay(appearance);
    }
},

canEditChanged : function (canEdit) {
    if (this.embeddedForm) this.embeddedForm.setCanEdit(canEdit);
    var buttons = isc.getValues(this._valueToButton);
    if (buttons && buttons.length > 0) {
        buttons.map("setCanEdit", canEdit);
    }
},

// Returns the effective setting of <code>autoFitButtons</code> considering the current
// layout style
_getAutoFitButtons : function (layoutStyle, autoFitButtons) {
    var autoFitButtons = autoFitButtons || this.autoFitButtons,
        layoutStyle = layoutStyle || this.layoutStyle;

    if (layoutStyle == isc.MultiComboBoxItem.HORIZONTAL ||
        layoutStyle == isc.MultiComboBoxItem.HORIZONTAL_REVERSE)
    {
        return true;

    } else if (this.autoFitButtons != null) {
        return this.autoFitButtons;

    } else if (layoutStyle == isc.MultiComboBoxItem.FLOW) {
        return true;
    } else {
        // assert layoutStyle == isc.MultiComboBoxItem.VERTICAL ||
        //        layoutStyle == isc.MultiComboBoxItem.VERTICAL_REVERSE
        return false;
    }
},

_createHStack : function () {
    return isc.HStack.create({
        autoDraw: false,
        height: 1,
        members: [],
        animateMembers: true,
        animateMemberTime: 100
    });
},

_createVStack : function () {
    return isc.VStack.create({
        autoDraw: false,
        height: 1,
        members: [],
        animateMembers: true,
        animateMemberTime: 100
    });
},

_createFlowLayout : function () {
    return isc.FlowLayout.create({
        autoDraw: false,
        tiles: [],
        tileMargin: 0,
        layoutMargin: 0,
        height: 1,

        animateTileChange: false,

        // A "visible" overflow expands the layout size instead of introducing scrollbars.
        overflow: "visible",

        // isc.FlowLayout is missing member functions that are available in other layouts:
        // getMember(), getMemberNumber(), getMembers(), addMember(), removeMember(), and
        // reorderMember(), so implement them here.

        getMember : function (position) {
            return this.getTile(position);
        },

        getMemberNumber : function (member) {
            if (isc.isA.Number(member)) {
                return member;
            }

            var numTiles = this.getLength();
            for (var i = 0; i < numTiles; ++i) {
                if (member === this.getTile(i)) {
                    return i;
                }
            }
            return -1;
        },

        getMembers : function () {
            var tiles = [], length = this.getLength();
            for (var i = 0; i < length; ++i) {
                var tile = this.getTile(i);
                tiles.add(tile);
            }
            return tiles;
        },

        addMember : function (newMember, position) {
            this.addTile(newMember, position);

            // The `autoFit: true` setting on a new button can cause the flow layout to
            // render the new button on top of a button in the same row.  Laying out
            // the tiles again fixes the issue.
            this.layoutTiles();
        },

        removeMember : function (member) {
            this.removeTile(member);
        },

        hideMember : function (member, callback) {
            member.hide();
            this.fireCallback(callback);
        },

        reorderMember : function (memberNum, newPosition) {
            if (memberNum != newPosition) {
                var tile = this.getTile(memberNum);
                this.removeMember(tile);
                this.addMember(tile, newPosition);
            }
        }
    });
}
});





//> @groupDef upload
// SmartClient provides special client and server-side support for file upload that allows
// uploaded files to be treated like ordinary DataSource fields.  This includes:
// <ul>
// <li>the +link{FileItem} and +link{MultiFileItem} FormItems that enable users to upload one or
// more files as a background operation, without leaving the current page
// <li>server-side support that allows binary uploads to be treated as a normal DataSource field
// values, with all other aspects of server-side integration unchanged
// <li>built-in SQL &amp; Hibernate DataSource support that can store and retrieve uploaded
// files from SQL databases
// </ul>
// The following documentation assumes you are using the SmartClient Java Server.  If you are
// not, skip to the sections near the end of this document.
// <P>
// <b>Single file upload: "binary" field type</b>
// <P>
// To use SmartClient's client-server upload system, you use a DataSource field of
// +link{dataSourceField.type,type} "binary".  By default, a DynamicForm bound to a DataSource
// with a field of type "binary" will use the +link{FileItem}, which displays a standard HTML
// &lt;input type="upload"&gt; form control.
// <P>
// When you call +link{dynamicForm.saveData()} on a DynamicForm containing a FileItem,
// SmartClient processes the save identically to a saveData() call that did not include a file
// upload:
// <ul>
// <li> if you are using the built-in SQL connectors via serverType:"sql", the file will be
// saved to SQL as described under +link{type:FieldType,field type "binary"}.
// <li> if you have server-side business logic, the inbound request may be routed to your
// business logic via RPCManager dispatch or DMI declarations as normal, your business logic
// will receive a normal DSRequest, and you are expected to provide a normal DSResponse.
// </ul>
// <P>
// Client-side callbacks, such as the callback passed to saveData(), fire normally.
// <P>
// Note that FileItems cannot be programmatically populated - this is a browser security
// restriction over which we have no control.  This restriction means that we are unable to
// populate a FileItem with the correct filename when a form is editing an existing record.
// Also, when you call saveData() on a form that is editing a new record, the FileItem will
// be cleared on successful completion of the saveData() call; this is a side-effect of the
// form being placed into "edit" mode.  In both of these cases, the fact that the FileItem
// has been cleared will not cause the persisted binary data to be removed by SmartClient
// Server on subsequent calls to setData().  If the user selects another file, it will
// overwrite the existing one; if the FileItem is left blank, the server simply ignores it.
// If you actually wish to wipe out the value of a binary field, call
// +link{DataSource.updateData(),updateData()} on the underlying dataSource, passing an
// explicit null value for the binary field.
// <P>
// <b>Processing File Uploads with server-side business logic</b>
// <P>
// Server-side business logic that processes file uploads may retrieve upload files via the
// server side API dsRequest.getUploadedFile(<i>fieldName</i>).  The uploaded file is returned
// as an instance of ISCFileItem, which provides access to a Java InputStream as well as
// metadata about the file (size, name).
// See the server-side JavaDoc (com.isomorphic.*) for details.
// <P>
// <span style="color:red;font-weight:bold;">NOTE:</span> request processing engines such as
// Struts may parse the inbound request before SmartClient receives it.  If you are creating an
// RPCManager object inside of a Struts Action and the file being uploaded is not available via
// <code>dsRequest.getUploadedFile()</code>, this is likely to be the problem, and you should
// remove Struts from the processing of the upload.
// <P>
// Server-side validation errors may be provided, including validation errors for the uploaded
// file (such as too large or invalid content), and will be displayed in the form that
// attempted an upload.
// <P>
// Be aware of the following special concerns when processing file uploads:
// <ul>
// <li> if you provide your own Java Servlet or JSP that creates an instance of RPCManager in
// order process SmartClient requests, many APIs of the HttpServletRequest are not safe to call
// before you have created the RPCManager, passing in the HttpServletRequest.  These include
// getReader(), getParameter() and other commonly called methods.  This is a limitation of
// Java Servlets, not specific to SmartClient
// <li> unlike other DataSource "add" and "update" operations, you are not expected to return
// the file as part of the data returned in the DSResponse
// </ul>
// <P>
// <b>Multi file upload: MultiFileItem</b>
// <P>
// The MultiFileItem provides an interface for a user to save one or more files that are
// related to a DataSource record, where each file is represented by a record in a
// related DataSource.
// <P>
// See the +link{MultiFileItem} docs for details.
// <P>
// <b>Upload without the SmartClient Server</b>
// <P>
// If it is acceptable that the application will do a full-page reload after the upload
// completes, you can simply:
// <ul>
// <li> set +link{DynamicForm.encoding} to "multipart"
// <li> include an +link{UploadItem} to get a basic HTML upload control
// <li> set +link{DynamicForm.action} to a URL where you have deployed server-side code to
// handle the upload
// <li> call +link{dynamicForm.submitForm()} to cause the form to be submitted
// </ul>
// This cause the DynamicForm component to submit to the form.action URL like an ordinary HTML
// &lt;form&gt; element.  Many
// +externalLink{http://www.google.com/search?q=html+file+upload+example,online tutorials}
// are available which explain how to handle HTML form file upload in various server-side
// technologies.
// <P>
// Note that when you submitForm(), the only values that will be sent to your actionURL are
// values for which actual FormItems exist.  This differs from saveData(), in which the
// entire set of +link{dynamicForm.values,form values} are always sent.  To handle submitting
// extra values, use +link{HiddenItem}s.
// <P>
// For further details, see the +link{UploadItem} docs.
// <P>
// <b>Background upload without the SmartClient Server</b>
// <P>
// Achieving background file upload without using the SmartClient server is also possible
// although considerably more advanced.  In addition to the steps above, create a hidden
// &lt;iframe&gt; element in the page, and use +link{dynamicForm.target} to target the form
// submission at this IFRAME.  In order receive a callback notification when the upload
// completes, after processing the file upload, your server should output HTML content for the
// IFRAME that includes a &lt;SCRIPT&gt; block which will navigate out of the IFRAME (generally
// via the JavaScript global "top") and call a global method you have declared as a callback.
//
// @title Uploading Files
// @visibility external
// @treeLocation Client Reference/Forms/Form Items/FileItem
// @example upload
// @example customBinaryField
// @example multiFileItem
//<


//>    @class FileItem
//
// Binary data interface for use in DynamicForms. Allows users to select a single file for
// upload. In read-only mode (canEdit:false) displays the contents of "imageFile" fields.
//
// <P>
// <b>Editable mode</b>
// <P>
// See the +link{group:upload,Upload Overview} for information on using this control.
//
// <P>
// <b>Read-only mode</b>
// <P>
// Displays one of two UIs, according to the value of
// +link{fileItem.showFileInline, showFileInline}.  If showFileInline is false, this Item
// displays the View and Download icons and the filename.  Otherwise, it streams the image-file
// and displays it inline.
//
// @group upload
// @treeLocation Client Reference/Forms/Form Items
// @visibility external
//<
isc.ClassFactory.defineClass("FileItem", "CanvasItem");

isc.FileItem.addProperties({
    // we want our value to show up in the forms values object!
    shouldSaveValue:true,

    // this flag causes FormItem to set multiple="true" for the element in the DOM
    _propagateMultiple: true,

    //> @attr fileItem.multiple (Boolean : true : [IR])
    // When true, allow the file-selection dialog shelled by the browser to select multiple
    // files.
    // <P>
    // Support is not full-cycle at the server - that is, there are server APIs for retrieving
    // each file that was uploaded, but no built-in support for storing multiple files against
    // a single DataSource field.  However, you can write custom server DMI code to do
    // something with the files - for instance, you could create multiple new DataSource
    // records for each file via a server DMI like this below:
    //
    // <pre>
    //    String fileNameStr = (String)dsRequest.getValues().get("image_filename").toString();
    //
    //    String[] fileNames = fileNameStr.split(", ");
    //    List files = dsRequest.getUploadedFiles();
    //
    //    for (int i = 0; i < files.size(); i++) {
    //        ISCFileItem file = (ISCFileItem)files.get(i);
    //        InputStream fileData = file.getInputStream();
    //        DSRequest inner = new DSRequest("mediaLibrary", "add");
    //        Map values = new HashMap();
    //        values.put("title", dsRequest.getValues().get("title"));
    //        values.put("image", fileData);
    //        values.put("image_filename", fileNames[i]);
    //        values.put("image_filesize", file.getSize());
    //        values.put("image_date_created", new Date());
    //
    //        inner.setValues(values);
    //        inner.execute();
    //    }
    //
    //    DSResponse dsResponse = new DSResponse();
    //
    //    dsResponse.setStatus(0);
    //
    //    return dsResponse;
    // </pre>
    //
    // @visibility external
    //<
    multiple: true,

    //> @attr fileItem.accept (String : null : [IR])
    // A comma-separated list of valid MIME types, used as a filter for the file picker window.
    // @visibility external
    //<

    //> @attr fileItem.showFileInline  (boolean : null : IR)
    // Indicates whether to stream the image and display it
    // inline or to display the View and Download icons.
    //
    // @visibility external
    //<

    _$blob:"blob",
    _createCanvas : function () {
        if (!isc.isA.Canvas(this.canvas)) {
            // Save the read-only state of our canvas

            this._isReadOnly = this.isReadOnly();
            this.canvas = (this._isReadOnly ? this._createReadOnlyCanvas()
                                            : this._createEditableCanvas());
        }
        this.containerWidget.addChild(this.canvas);
    },

    _createReadOnlyCanvas : function () {
        var canvas;

        if (this.type == this._$blob) {
            // A read-only blob is rendered as a StaticTextItem.
            canvas = isc.DynamicForm.create({
                autoDraw:false,
                // suppress redraws as much as possible - redraw == killing the item value.
                _redrawWithParent:false,
                redrawOnResize:false,
                canSubmit:true,
                action:this.action,
                targetItem:this,
                items:[
                    {type:"text", editorType:"StaticText",
                     width:this.width, height:this.height,
                     name:this.getFieldName(), showTitle:false
                    }
                ]
            });
        } else {
            canvas = isc.Canvas.create({
                height: 10
            });
        }
        return canvas;
    },

    _createEditableCanvas : function () {
        return isc.DynamicForm.create({
            // only one column: the UploadItem
            numCols:1,
            colWidths:["*"],
            // suppress redraws as much as possible - redraw == killing the item value.
            _redrawWithParent:false,
            redrawOnResize:false,
            canSubmit:true,
            action:this.action,
            targetItem:this,
            addOperation:this.form.addOperation,
            updateOperation:this.form.updateOperation,
            removeOperation:this.form.removeOperation,
            fetchOperation:this.form.fetchOperation,
            getSaveOperationType:function () {
                if (this.targetItem && this.targetItem.form)
                    return this.targetItem.form.getSaveOperationType();
                return this.Super("getSaveOperationType", arguments);
            },
            items:[
                {targetItem: this, editorType: isc.TUploadItem || isc.UploadItem,
                    width: this.width, height: this.height, multiple: this.multiple,
                    accept: this.accept, name: this.getFieldName(), showTitle: false,
                    getElementName : function () {
                        return this.getFieldName();
                    },
                    changed : function (form, item, value) {
                        this.targetItem.storeValue(value);
                    },
                    _fireStandardHandler : function () {
                        var targetItem = this.targetItem;
                        return targetItem._fireStandardHandler.apply(targetItem, arguments);
                    }
                },
                // FileItems are used with the SmartClient server - _transaction item to contain
                // details of the transaction when submitted to the server.

                {name:"_transaction", type:"HiddenItem"}
            ]
        });
    },

    // Update enabled/disabled state of the element to match our read-only/disabled state.
    setElementReadOnly : function (readOnly) {
        // The two states require two different canvas's therefore a redraw.
        // This override is necessary because CanvasItem avoids redraws by default.
        this.redraw();
    },


    redraw : function () {
        // This occurs when changing the state of canEdit.
        if (this._isReadOnly != this.isReadOnly()) {
            var value = this.getValue();
            if (this.canvas) {
                delete this.canvas.canvasItem;
                this.canvas.destroy(true);
            }
            this._isReadOnly = this.isReadOnly();
            this.setCanvas(this._isReadOnly ? this._createReadOnlyCanvas()
                                            : this._createEditableCanvas());
            this.setValue(value);
        }
        this.Super("redraw", arguments);
    },

    // support setValue() if the newValue is empty (to clear a programmatically set value)
    // and ignore setting the value to the current value
    setValue : function (newValue) {
        if (this.isReadOnly()) {
            var form = this.form,
                record = form.getValues()
            ;
            if (this.type == "blob") {
                // Update the StaticTextItem value
                this.canvas.items[0].setValue(newValue);
            } else if (this.type == "imageFile" && this.showFileInline != false) {
                this.canvas.setHeight("*");
                this.canvas.setWidth("*");
                this.canvas.setContents(this.getImageHTML());
            } else {
                if (this.showFileInline == true) {
                    // non-imageFile field
                    this.logWarn("setValue(): Unsupported field-type for showFileInline: " +
                                 this.type);
                }
                this.canvas.setHeight(20);
                this.canvas.setWidth("100%");
                this.canvas.setContents(this.getViewDownloadHTML(newValue, record));
            }
            return this.Super("setValue", arguments)
        } else {
            if (newValue == null || isc.isA.emptyString(newValue)) {
                this.canvas.items[0].setValue(newValue);
                return this.Super("setValue", arguments);
            }

            return this.canvas.items[0].setValue(newValue);
        }
    },

    setWidth : function (width) {
        if (this.canvas && !this.isReadOnly()) {
            this.canvas.items[0].setWidth(width);
        }
        this.Super("setWidth", arguments);
    },
    setHeight : function (height) {
        if (this.canvas && !this.isReadOnly()) {
            this.canvas.items[0].setHeight(height);
        }
        this.Super("setHeight", arguments);
    },

    getViewDownloadHTML : function (value, record) {

        if (isc.isA.String(value)) return value;
        if (record == null) return null;

        var form = this.form,
            ds = form.getDataSource(),
            field = ds ? ds.getField(this.name) : {},
            filenameField = ds ? ds.getFilenameField(this.name) : this.name + "_filename",
            name = record[filenameField]
        ;


        if (!field.filenameSuppressed && (name == null || isc.isAn.emptyString(name))) {
            return this.emptyCellValue;
        }


        var viewIconHTML = isc.Canvas.imgHTML({
            src: "[SKIN]actions/view.png",
            width: 16,
            height: 16,
            extraCSSText: "cursor:" + isc.Canvas.HAND,
            extraStuff: " onclick='" + this.getID() + ".viewFile()'"
        });
        var downloadIconHTML = isc.Canvas.imgHTML({
            src: "[SKIN]actions/download.png",
            width: 16,
            height: 16,
            extraCSSText: "cursor:" + isc.Canvas.HAND,
            extraStuff: " onclick='" + this.getID() + ".downloadFile()'"
        });

        return "<nobr>" + viewIconHTML + "&nbsp;" + downloadIconHTML +
            (name ? "&nbsp;" + name : "") + "</nobr>";
    },

    getImageHTML : function () {
        var record = this.form.getValues(),
            field = this.form.getField(this.name),
            urlProperty = this.name + "_imgURL",
            value
        ;
        if (!record[this.name]) return " ";

        if (!record[urlProperty]) {
            var dimensions = isc.Canvas.getFieldImageDimensions(field, record);

            value = record[urlProperty] =
                isc.Canvas.imgHTML(this.form.getDataSource().streamFile(record, field.name),
                    dimensions.width, dimensions.height);
        } else
            value = record[urlProperty];

        return value;
    },

    viewFile : function () {
        isc.DS.get(this.form.dataSource).viewFile(this.form.getValues(), this.name);
    },

    downloadFile : function () {
        isc.DS.get(this.form.dataSource).downloadFile(this.form.getValues(), this.name);
    },

    _shouldAllowExpressions : function () {
        return false;
    }
});



// Class will not work without the ListGrid
if (isc.ListGrid) {



//>    @class RelationItem
//
// Enables editing and saving of records related to the one being displayed in the "master" form
// (the form containing this item).
//
// @treeLocation Client Reference/Forms/Form Items
// @visibility experimental
//<
isc.ClassFactory.defineClass("RelationItem", "CanvasItem");
isc.RelationItem.addProperties({

    canvasConstructor: "ListGrid",
    canvasDefaults: {
        canEdit: true
    },

    pickerConstructor: "RelationPicker",
    showEditButton: true,
    editButtonDefaults: {
        click: "item.showPicker(item.showPickerModal(), icon)",
        prompt: "Edit new/selected item"
    },

    showPickerModal : function () {
        return this.form && !this.form.saveOperationIsAdd();
    },

    showRemoveButton: true,
    removeButtonDefaults: {
        src: "[SKIN]DynamicForm/Remove_icon.gif",
        click: "item.removeSelectedData()",
        prompt: "Remove selected item"
    },

    canEditWithNoMasterRecord: false
});

//!>Deferred
isc.RelationItem.addMethods({

init : function () {
    this.hasMasterRecord = false;

    this.Super("init", arguments);

    // if we're showing the various buttons, add them now
    if (this.showEditButton) this.editButton = this.addIcon(this.editButtonDefaults);
    if (this.showRemoveButton) this.removeButton = this.addIcon(this.removeButtonDefaults);
},

getPickerData : function () {
    // return the first selected record, if available;
    var selectedRecord = this.canvas.getSelectedRecord();
    if (selectedRecord) return selectedRecord;

    // new record
    return this.getDataSource().getForeignKeysByRelation(this.form.getValues(), this.form.dataSource);
},

showPicker : function (modal, icon, pickerProperties, rect) {
    if (pickerProperties == null) pickerProperties = {};
    pickerProperties.dataSource = this.dataSource;

    this.Super("showPicker", [modal, icon, pickerProperties, rect], arguments);

    // propagate the masterRecord at show() time in case the use saves directly out of the picker.
    var foreignKeyValues = {};
    if (!this.form.saveOperationIsAdd())
        foreignKeyValues = this.getDataSource().getForeignKeysByRelation(this.form.getValues(), this.form.dataSource);
    this.picker.setForeignKeyValues(foreignKeyValues);
},

// XXX (why) do we need this?
getValue : function () {
    return;
},

removeSelectedData : function () {
    this.canvas.removeSelectedData();
},

// A setValue means that a new master record has been selected.
setValue : function () {
    // use the primary key to issue a re-filter.  Must do this on a timeout because we're in the
    // middle of setValues() and will need to call getValues() on the DF.
    this.delayCall("filterRelation");
},

filterRelation : function () {
    var values = this.form.getValues();
    var wasDisabled = this.isDisabled();

    if (this.form.saveOperationIsAdd()) {
        // the record doesn't have values for all primary key fields - we're adding a new record,
        // so set the data to an empty array because no relations exist yet for this record.
        this.canvas.setData([]);
        this.hasMasterRecord = false;
    } else {
        // we're editing an existing record (has primary keys), so filter the relations view by the
        // primaryKeys
        var ds = this.getDataSource();
        if (ds) {
            this.canvas.filterData(ds.getForeignKeysByRelation(values, this.form.dataSource));
            this.hasMasterRecord = true;
        }
    }
    if (wasDisabled != this.isDisabled()) this.updateDisabled();

    if (this.picker) this.picker.clearData();
},

isDisabled : function () {
    var dis = this.Super("isDisabled", arguments);
    if (dis) return true;
    if (this.canEditWithNoMasterRecord) return false;
    return !!this.hasMasterRecord;
},

_shouldAllowExpressions : function () {
    return false;
}

});
//!<Deferred

isc.defineClass("RelationPicker", "VLayout").addProperties({
    className: "dialogBackground"
});

//!>Deferred
isc.RelationPicker.addMethods({

creatorName:"picker",
initWidget : function () {
    this.Super("initWidget", arguments);

    this.addAutoChild("editor", { dataSource: this.dataSource }, "DynamicForm");

    this.addAutoChild("toolbar", {
        membersMargin: 2
    }, "HLayout");
    this.addAutoChild("saveButton", {
        title: "Save",
        click: "this.picker.editor.saveData(this.picker.getID()+'.hide()')"
    }, "AutoFitButton", this.toolbar);
    this.addAutoChild("clearButton", {
        title: "Clear",
        click: "this.picker.clearData();"
    }, "AutoFitButton", this.toolbar);
    this.addAutoChild("cancelButton", {
        title: "Cancel",
        click: "this.picker.hide();this.picker.clearData()"
    }, "AutoFitButton", this.toolbar);

},

hide : function () {
    this.Super("hide", arguments);
    this.hideClickMask();
},

setData : function (data) {
    this.editor.setData(data);
},

getData : function () {
    return this.editor.getValues();
},

clearData : function () {
    this.editor.clearValues();
    this.setData(this.foreignKeyValues);
},

dataChanged : function () {

},

setForeignKeyValues : function (values) {
    this.foreignKeyValues = values;
}

});
//!<Deferred

}



// Class will not work without the ListGrid
if (isc.ListGrid) {




//> @class MultiFileItem
// The MultiFileItem provides an interface for a user to save one or more files that are
// related to a DataSource record, where each file is represented by a record in a
// related DataSource.
// <P>
// Use MultiFileItem when a record may have one or more files associated with it (such as
// attachments to an email message) where no information about the files needs to be stored other
// than the files themselves.  If you have several fields associated with each file (such as an
// uploaded document with a version, comments and processes associated with it), consider
// instead an ordinary DataSource with on field of type "binary", and using the +link{FileItem}
// for upload.
// <P>
// See the +link{group:upload,Uploading Files} overview for more information on upload.
// <P>
// <b>DataSource Setup</B>
// <P>
// In a relationship sometimes called a "master-detail" relationship, the MultiFileItem stores
// files in a "detail" DataSource which are related to a "master" DataSource record being
// edited by the form which contains the MultiFileItem.
// <P>
// To use a MultiFileItem:
// <ul>
// <li> declare a "detail" DataSource to store the related files.  At a minimum, this
// DataSource must have:
// <ul>
// <li> a +link{dataSourceField.primaryKey,primaryKey} field
// <li> a field declaring a +link{dataSourceField.foreignKey,foreignKey} relationship to the
// primaryKey of the "master" DataSource
// <li> a field of type "binary"
// </ul>
// <li> +link{DataBoundComponent.dataSource,bind} a DynamicForm to the "master" DataSource
// <li> in the DynamicForm bound to the "master" DataSource, declare a field with
// +link{formItem.editorType,editorType}:"MultiFileItem" and a <code>dataSource</code>
// property set to the ID of the "detail" DataSource
// </ul>
// An example "detail" DataSource for storing files is shown below.  This "detail" DataSource
// assumes a "master" DataSource with +link{DataSource.ID} "masterRecord" and with a primaryKey
// field "id".
// <pre>
// <code>
//   &lt;DataSource ID="uploadedFiles" serverType="sql"&gt;
//     &lt;fields&gt;
//        &lt;field name="fileId" type="sequence" primaryKey="true" hidden="true"/&gt;
//        &lt;field name="masterRecordId" type="number" foreignKey="masterRecord.id" hidden="true"/&gt;
//        &lt;field name="file" type="binary" title="File"/&gt;
//     &lt;/fields&gt;
//   &lt;/DataSource&gt;
// </code>
// </pre>
// <P>
// Aside from a single "binary" field, the "detail" DataSource should generally have only
// hidden fields, as shown above.  Additional internal fields (such as a "lastUpdated" field)
// may be added, but will not be editable via MultiFileItem.
// <P>
// <b>Display</b>
// <P>
// The MultiFileItem appears as a list of files related to the current record.  An optional
// button, the +link{multiFileItem.removeButton,removeButton} allows removing files.  A
// second optional button, the +link{multiFileItem.editButton,editButton}, launches a
// picker for uploading further files.
// <P>
// <b>Saving</b>
// <P>
// In all cases, uploading a new file is an "add" DSRequest against the
// +link{multiFileItem.dataSource}.
// <P>
// The MultiFileItem has two modes, according to whether the "master" record is being newly created
// via an "add" operation or whether the master record is pre-existing ("update" operation).
// <P>
// If the master record is pre-existing, each file added by the user is uploaded as soon as the
// user exits the picker launched from the edit button, and the list of files shown in the main
// form reflects the actual list of stored files.
// <P>
// If the master record is being newly created, files are not actually uploaded until
// <b>after</b> the master record is confirmed saved, and the list of fields shown in the main
// form reflects files which will be uploaded after the master record is saved.
// <P>
// In both cases, if there are multiple files to upload, they are uploaded one at a time, as a
// series of separate "add" DSRequests against the +link{multiFileItem.dataSource}.
// <P>
// Also in both cases, deletion of any file is immediate.  In the case of a pre-existing master
// record, all files shown actually exist as DataSource records, and deletion is performed as a
// "remove" DSRequest against the +link{multiFileItem.dataSource}.
//
// @group upload
// @treeLocation Client Reference/Forms/Form Items
// @visibility external
//<
isc.ClassFactory.defineClass("MultiFileItem", "RelationItem");
isc.MultiFileItem.addProperties({

    //> @attr multiFileItem.dataSource (DataSource or ID : null : IR)
    // DataSource where files are stored.
    // <P>
    // This DataSource is expected to have a field of type "binary" as well as a primaryKey and
    // foreignKey declaration to some other DataSource; see the +link{MultiFileItem} for an
    // overview.
    // <P>
    // This DataSource need only be capable of "fetch", "add" and "remove" - "update" is unused.
    //
    // @visibility external
    //<

    // NOTE: show flags for edit/removeButton set default true in superclass

    //> @attr multiFileItem.removeButton (AutoChild StatefulCanvas : null : IR)
    // Button for removing files.  Supports the properties of a +link{FormItemIcon}.
    //
    // @visibility external
    //<

    //> @attr multiFileItem.editButton (AutoChild StatefulCanvas : null : IR)
    // Button for launching a picker to add new files for upload.  Supports the properties of a
    // +link{FormItemIcon}.
    //
    // @visibility external
    //<

    // the MultiFilePicker is constructed through the FormItem's picker management system
    pickerConstructor: "MultiFilePicker",

    canvasDefaults : {

        showHeader:false,

        canHover: true,
        cellHoverHTML : function (record, rowNum, colNum) {
            if (this.canvasItem.form.saveOperationIsAdd()) {
                var value = record[this.getFieldName(colNum)];
                if (value && this.displayShortName) {
                    value = value.replace(/^.*[\\\/]/, isc.emptyString)
                }
                return value;
            }
        },
        getCellValue : function (value, rowNum, colNum) {
            var value = this.Super("getCellValue", arguments);
            if (this.canvasItem.form.saveOperationIsAdd()) {
                if (!this.displayShortName || !value) return value;
                value = value.replace(/.*(\\|\/)/g, isc.emptyString);
            }
            return value;
        }
    },

    // Override getDynamicDefaults to pick up dynamic defaults for the ListGrid based on
    // this item's direct settings
    getDynamicDefaults : function (childName) {
        if (childName == "canvas") {
            var defaults = {};
            if (this.emptyMessage != null) defaults.emptyMessage = this.emptyMessage;
            if (this.displayShortName != null) defaults.displayShortName = this.displayShortName;
            return defaults;
        }
        return this.Super("getDynamicDefaults", arguments);

    },

    //> @attr multiFileItem.emptyMessage (String : "Click icon to add..." : IR)
    // Empty message to display when there are no files listed.
    // @visibility external
    // @group i18nMessages
    //<
    emptyMessage:"Click icon to add...",

    //> @attr multiFileItem.editButtonPrompt (String : "Add files" : IR)
    // The +link{FormItemIcon.prompt,prompt} of the +link{editButton,edit button}.
    // @group i18nMessages
    // @visibility external
    //<
    editButtonPrompt: "Add files",

    //> @attr multiFileItem.removeButtonPrompt (String : "Remove selected files" : IR)
    // The +link{FormItemIcon.prompt,prompt} of the +link{removeButton,remove button}.
    // @group i18nMessages
    // @visibility external
    //<
    removeButtonPrompt: "Remove selected files",

    //> @attr multiFileItem.pickerUploadButtonInitialTitle (String : "OK" : IR)
    // The initial title of the upload button in the picker launched by the
    // +link{editButton,edit button} that is used before the form is saved.
    // @see pickerUploadButtonTitle
    // @group i18nMessages
    // @visibility external
    //<
    pickerUploadButtonInitialTitle: "OK",

    //> @attr multiFileItem.pickerUploadButtonTitle (String : "Save" : IR)
    // The title of the upload button in the picker launched by the
    // +link{editButton,edit button} that is used after the form is saved.
    // @see pickerUploadButtonInitialTitle
    // @group i18nMessages
    // @visibility external
    //<
    pickerUploadButtonTitle: "Save",

    //> @attr multiFileItem.pickerCancelButtonTitle (String : "Cancel" : IR)
    // The title of the cancel button in the picker launched by the
    // +link{editButton,edit button}.
    // @group i18nMessages
    // @visibility external
    //<
    pickerCancelButtonTitle: "Cancel",

    //> @attr multiFileItem.pickerAddAnotherFileButtonTitle (HTMLString : "<u>Add&nbsp;another</u>" : IR)
    // The contents of the "Add another" file button in the picker launched by the
    // +link{editButton,edit button}.
    // @group i18nMessages
    // @visibility external
    //<
    pickerAddAnotherFileButtonTitle: "<u>Add&nbsp;another</u>",

    //> @attr multiFileItem.pickerUploadProgressLabel (HTMLString : "Saving \${fileName} \${formattedFileSize}" : IR)
    // Specifies the label of the progress meter in the picker launched by the
    // +link{editButton,edit button}.  This property is a dynamic string, similar to the
    // +link{Canvas.dynamicContents} feature, with the variables <code>fileName</code> and
    // <code>formattedFileSize</code>.
    // @group i18nMessages
    // @visibility external
    //<
    pickerUploadProgressLabel: "Saving ${fileName} ${formattedFileSize}",

    //> @attr multiFileItem.displayShortName    (boolean : true : IR)
    //
    // If true, just the filename (without the path) is shown in the list of files.  If
    // false, the full path including the filename is shown.
    //
    // @visibility external
    //<
    displayShortName: true,

    iconWidth:16,
    iconHeight:16,

    editButtonDefaults : isc.addProperties({},isc.RelationItem.getInstanceProperty('editButtonDefaults'),{
        src: "[SKIN]MultiUploadItem/icon_add_files.png",
        showOver:false
    }),

    // RelationItem calls this method from click on the edit item to determine whether
    // the picker should be modal.
    // Always show the picker modal - user can dismiss via save or cancel click, which
    // is a cleaner UI than auto dismissing (cancelling) on outside click

    showPickerModal : function () {
        return true;
    },

    removeButtonDefaults : isc.addProperties({},isc.RelationItem.getInstanceProperty('removeButtonDefaults'), {
        src: "[SKIN]MultiUploadItem/icon_remove_files.png",
        showOver:false
    }),

    canEditWithNoMasterRecord: true
});

//!>Deferred
isc.MultiFileItem.addMethods({

init : function () {
    // Override the edit and remove button contents with the user-specified prompt.
    this.editButtonDefaults = isc.addDefaults({
        prompt: this.editButtonPrompt
    }, this.editButtonDefaults);
    this.removeButtonDefaults = isc.addDefaults({
        prompt: this.removeButtonPrompt
    }, this.removeButtonDefaults);

    this.pickerProperties = isc.addProperties({}, this.pickerProperties, {
        uploadWithoutPKButtonName: this.pickerUploadButtonInitialTitle,
        uploadWithPKButtonName: this.pickerUploadButtonTitle,
        cancelButtonName: this.pickerCancelButtonTitle,

        addAnotherFileButtonProperties: {
            contents: this.pickerAddAnotherFileButtonTitle
        },

        progressMeterProperties: {
            progressLabelProperties: {
                // Override the dynamic contents of the progress label canvas to allow it to be
                // customized without exposing the internal class MultiFileProgressMeter.
                dynamicContents: false,
                dynamicContentVars: null,
                contents: null,
                getContents : function () {
                    var progressMeter = this.creator,
                        multiFileItem = progressMeter.creator.creator,
                        contents = multiFileItem.pickerUploadProgressLabel,
                        result = contents.evalDynamicString(multiFileItem, {
                            fileName: progressMeter.fileName,
                            formattedFileSize: progressMeter.getFormattedFileSize()
                        })
                    ;
                    return "<b><nobr>" + result + "</nobr></b>";
                },
                show : function () {
                    this.setContents(this.getContents());
                    this.Super("show", arguments);
                }
            }
        }
    });

    return this.Super("init", arguments);
},

removeSelectedData : function () {
    // if we're editing an existing record on the server just call removeSelectedData on the
    // canvas (listGrid) to clear the value from the dataSource
    if (!this.form.saveOperationIsAdd()) return this.Super("removeSelectedData", arguments);

    // remove the field from the canvas and also the editor by calling the removeUploadField method
    // on the editor which will automatically update the canvas
    var selection = this.canvas.getSelection();
    // form is unset on the selection...
    for (var i = 0; i < selection.length; i++) this.picker.removeUploadField(selection[i]._form,true);
},


// notification fired when the form completes a save.  Allows RelationItems (including MUI) to
// save related records after the main record has been saved such that a PK is definitely
// available.
formSaved : function (request, response, data) {
    // if we're using a picker, show it in modal mode for the duration of the save so that any
    // validation errors are immediately visible
    if (this.picker) {
        this.showPicker(true, this.editButton);

        this.picker.setForeignKeyValues(
            this.getDataSource().getForeignKeysByRelation(data, this.form.dataSource)
        );

        this.picker.saveData(this.getID()+".saveDataCallback()");
        // cancel further rpc callback processing until we're done saving
        return false;
    } else {
        this.saveDataCallback();
    }
},

saveDataCallback : function () {
    if (this.picker) this.picker.hide();
    this.form.formSavedComplete();
    if (this.picker && isc.ResultSet && !isc.isA.ResultSet(this.canvas.data)) {
        this.canvas.setData([]);
        this.canvas.fetchData(this.picker.foreignKeyValues);
    }
},

// called when the data changes in the picker
pickerDataChanged : function (picker) {
    // if this is going to be an update, then we're showing existing records in the canvasItem and
    // the user must save directly from the picker - so do nothing
    if (!this.form.saveOperationIsAdd()) return;

    // add operation - we're showing filenames selected so far in the canvasItem - update the
    // list. When the user saves the form with the CanvasItem on it we'll have to commit.
    this.canvas.setData(this.picker.getData());
},

destroy : function () {
    // FIXME we can't clear the value in an upload field, so we have to blow those away, but we
    // could recycle everything else
    if (this.picker) this.picker.destroy();
    this.Super("destroy", arguments);
}

});
//!<Deferred

//> @class MultiFilePicker
// The MultiFilePicker is a pop-up picker used by the +link{MultiFileItem} to allow the user to
// enter several files for upload.
//
// @group upload
// @treeLocation Client Reference/Forms/Form Items/MultiFileItem
// @visibility external
//<

// Manages a stack of upload fields where the user can add and remove fields via buttons.

isc.defineClass("MultiFilePicker", "VStack").addProperties({
    height: 1,

    layoutMargin:10,

    styleName: "windowBackground",

    // NOTE: next two properties are basically cosmetic - they do not allow you to limit the
    // maximum number of uploaded

    //> @attr multiFilePicker.minUploadFields        (integer : 1 : [IRW])
    //
    // Initial number of upload fields to show.
    //
    // @visibility internal
    //<
    minUploadFields: 1,

    //> @attr multiFilePicker.maxUploadFields        (integer : null : [IRW])
    //
    // The maximum number of upload fields to show.  If not specified, user can add as many
    // upload fields as he wishes.
    //
    // @visibility internal
    //<

    // min file size in bytes at which we show the progress bar (~200k)
    minFileSizeForProgressBar: 204800,

    // in milliseconds, how frequently we ask the server for the number of bytes uploaded so far.
    progressCheckFrequency: 1000,

    progressMeterConstructor: "MultiFileProgressMeter",

    uploadLayoutConstructor: "VStack",

    uploadWithPKButtonName: "Save",
    uploadWithoutPKButtonName: "OK",
    cancelButtonName: "Cancel",

    showUploadRemoveButton: true,
    uploadWithoutPK: false
});

//!>Deferred
isc.MultiFilePicker.addMethods({

addAnotherFileButtonDefaults: {
    canFocus:true, // make this keyboard accessible
    contents: "<u>Add&nbsp;another</u>"
},

creatorName:"picker",
initWidget : function () {
    this.Super("initWidget", arguments);

    this.addAutoChild("uploadLayout", {height: 1});
    this.addAutoChild("addAnotherFileButton", {
        width: 75,
        height: 20,
        align: "left",
        cursor: isc.Canvas.HAND,
        icon: "[SKIN]MultiUploadItem/icon_add_files.png",
        click : "this.picker.addUploadField()"
    }, "Label");

    this.addAutoChild("toolbar", {
        width: 1,
        height: 1,
        membersMargin: 10,
        layoutMargin: 10,
        layoutAlign: "right"
    }, "HStack");

    this.addAutoChild("saveButton", {
        title: this.hasKeys() ? this.uploadWithPKButtonName: this.uploadWithoutPKButtonName,
        width: 80,
        updateTitle : function () {
            var picker = this.parentElement.picker;
            var newTitle = picker.hasKeys() ? picker.uploadWithPKButtonName : picker.uploadWithoutPKButtonName;
            if (newTitle != this.title) this.setTitle(newTitle);
        },
        click : function () {
            var picker = this.parentElement.picker;
            if (!picker.hasKeys() && !picker.uploadWithoutPK) {
                //
                // this flow will just show the record in the main listGrid but not actually
                // save anything to server - assumption is that save would occur on 'saveData' on
                // the form
                picker.hide();
                picker.dataChanged();
            } else {
                // This will actually directly submit the form, storing the files on the existing
                // record
                picker.saveData();
            }
        },
        observes : [{source: this, message:"setForeignKeyValues", action: "observer.updateTitle()"}]
    }, "IButton", this.toolbar);

    this.addAutoChild("cancelButton", {
        title: this.cancelButtonName,
        width: 80,
        // user can hit the cancel button during validation error from server.  do a saveData() to
        // complete callback flow
        click : function () {
            this.picker.hide();

            if (this.picker.creator.form.saveOperationIsAdd()) this.picker.revertData();
        }
    }, "IButton", this.toolbar);

    this.clearData();
},

hasKeys : function () {
    return (this.foreignKeyValues && !isc.isAn.emptyObject(this.foreignKeyValues));
},

setForeignKeyValues : function (foreignKeyValues) {
    this.foreignKeyValues = foreignKeyValues;
},

revertData : function () {
    this.clearData(true);
},

clearData : function (revertOnly) {
    var forms = this.getForms(),
        formCount = 0
    ;
    for (var i = forms.length-1; i >=0; i--) {
        var remove = false;
        // if we're reverting, just remove items that are newly added
        if (revertOnly) remove = (forms[i].newlyAdded == true);
        else remove = true;

        // remove forms with no values (empty upload fields)
        if (isc.isAn.emptyObject(forms[i].getValues())) remove = true;

        if (remove) this.removeUploadField(forms[i]);
        else formCount++;
    }
    // preallocate the desired number of file upload fields
    for (var i = formCount; i < this.minUploadFields; i++) this.addUploadField();
},

// add another upload form, called via button
addUploadField : function () {

    var binaryField;

    if(this.dataSource==null) {
        isc.logWarn("A datasource must be defined on a MultiFileItem ("+this.id+") or it will both not function and elements will not render properly.");
    } else {
        var ds = isc.DataSource.get(this.dataSource),
            fields = ds.getFields();
        for (var key in fields) {
            if (fields[key].type == "binary" || fields[key].type == "imageFile") {
                binaryField = key;
                break;
            }
        }
        if (binaryField == null) {
            isc.logWarn("The datasource defined on a MultiFileItem ("+this.id+") must define a binary field");
        }
    }


    var form = this.createAutoChild("uploadForm", {
        dataSource: this.dataSource,
        cellPadding: 0,
        numCols: 2,
        colWidths: ['*','*'],
        width: 250,
        newlyAdded: true,
        elementChanged : function () {
            this.Super("elementChanged", arguments);
            this.picker.dataChanged();
        },
        fields : [
            {name: binaryField, multiple: false}
        ]
    }, isc.DynamicForm);

    var uploadFormLayout = this.createAutoChild("uploadFormLayout", {
        members: [form],
        height: 21
    }, isc.HLayout);

    this.uploadLayout.addMember(uploadFormLayout);
    if (this.showUploadRemoveButton) {
        var removeButton = this.createAutoChild("uploadRemoveButton", {
            form: form,
            picker: this,
            contents: isc.emptyString,
            cursor: isc.Canvas.HAND,
            icon: "[SKIN]MultiUploadItem/icon_remove_files.png",
    //        title: "Remove",
            click: "this.picker.removeUploadField(this.form, true)",
            iconSpacing : 6,
            width: 22, // 16px icon + 6px icon spacing
            height: 20
        }, isc.Label);
        uploadFormLayout.addMember(removeButton, 0);
    }

    if (this.maxUploadFields && this.maxUploadFields <= this.uploadLayout.getMembers().length)
        this.addAnotherFileButton.hide();
},

// remove one of the uploaded fields, called from button
removeUploadField : function (form, reAddToMin) {

    if (form._queueNum) {
        isc.rpc.cancelQueue(form._queueNum);
        this.transactionNum = null;
        if(this.progressMeter) this.progressMeter.hide();
    }

    if (form.parentElement) form.parentElement.destroy();
    if (reAddToMin && this.uploadLayout.getMembers().length < this.minUploadFields) {
        this.addUploadField();
    }

    if (this.maxUploadFields && this.maxUploadFields > this.uploadLayout.getMembers().length) {
        this.addAnotherFileButton.show();
    }

    this.dataChanged();
},

// get all the forms in the editor
getForms : function () {
    return this.uploadLayout.getMembers().map("getMember", this.showUploadRemoveButton ? 1 : 0);
},

// observable
dataChanged : function () {

},

show : function () {
    var forms = this.getForms();
    if (forms) forms.setProperty("newlyAdded", false);
    this.Super("show", arguments);
},

hide : function () {
    this.Super("hide", arguments);
    this.hideClickMask();
},

// get data for display in the list of files
getData : function () {
    var data = [];
    var forms = this.getForms();
    for (var i = 0; i < forms.length; i++) {
        var form = forms[i];
        if (!form) continue;

        var values = form.getValues();

        // skip forms with no values (empty upload fields)
        if (isc.isAn.emptyObject(values)) continue;

        // store a pointer back to the form on the values object
        values._form = form;
        data[data.length] = values;
    }
    return data;
},

// save the first non-empty upload form.  We upload each file serially, waiting for a server
// response before proceeding to the next file by calling this method again.
// Note that we could upload a maximum of two files at once (based on the HTTP 1.1 limit of 2
// connections per server), but that would prevent any other kind of traffic, eg, progress
// checks.
saveData : function (callback) {
    if (!callback) callback = this.saveCallback;
    if (!callback) callback = this.getID()+".hide()";
    this.saveCallback = callback;

    var forms = this.getForms();

    // client or server validation error may have occurred, but we have the master record, so let
    // the user correct the problem and save.
    this.saveButton.setTitle(this.uploadWithPKButtonName);

    // find the first form with values
    // remove all forms with no values
    var form;
    var fileName;
    while (forms.length > 0) {
        form = forms[0];
        fileName = form.getFields()[0].getValue();
        if (!fileName) {
            this.removeUploadField(form);
            forms.remove(form);
        }
        else break;
    }

    if (forms.length == 0) {
        // done saving forms
        if (this.progressMeter) {
            this.progressMeter.hide();
        }

        this.transactionNum = null;
        delete this.saveCallback;
        this.fireCallback(callback);
        this.clearData();
        return;
    }

    if (!form.validate()) return;

    if (!this.progressMeter) {
        this.progressMeter = this.createAutoChild("progressMeter", {
            progressCheckFrequency : this.progressCheckFrequency
        });
        this.addMember(this.progressMeter, 0);
    }

    fileName = fileName.replace(/.*(\\|\/)/g, isc.emptyString);
    this.progressMeter.setFileName(fileName);
    this.progressMeter.hideProgressBar();
    this.progressMeter.show();

    if (this.hasKeys()) {
        for (var key in this.foreignKeyValues) form.setValue(key, this.foreignKeyValues[key]);
    }
    var dsRequest = form.saveData(this.getID()+".saveDataCallback(dsRequest, dsResponse, data)",
                         {
                             params: {formID : form.getID()},
                             // we want to be notified of an error so we can kill the progressCheck thread
                             willHandleError: true,
                             form: form,
                             showPrompt: false,
                             saveDataCallback: callback,
                             timeout: 0
                         }
    );
    this.transactionNum = dsRequest.transactionNum;
    form._queueNum = this.transactionNum;
    this.progressCheck(form.getID(), this.transactionNum);
},

// called when we get a server response to an upload attempt.  If it succeeded, proceeds to
// upload next file.
saveDataCallback : function (dsRequest, dsResponse, data) {
    var form = dsRequest.form;

    // if there was an error, kill the progressCheck thread
    if (dsResponse.status != isc.RPCResponse.STATUS_SUCCESS) {
        this.progressMeter.hide();
        this.transactionNum = null;
        if (dsResponse.status == isc.RPCResponse.STATUS_VALIDATION_ERROR) {
            form.setErrors(dsResponse.errors, true);
        } else {
            isc.warn(data);
        }
        // form didn't save - bail out and let the user correct any errors
        return;
    }

    // the form saved successfully - remove it from the list
    form._queueNum = null; // don't cancelQueue()
    this.removeUploadField(form);

    // save the next form
    this.saveData(dsRequest.saveDataCallback);

    if (this.callingFormItem.fileUploaded) {
        this.callingFormItem.fileUploaded(dsRequest, dsResponse);
    }
},

// check the progress of a file upload via a DMI performed in parallel to the upload itself.
// Calling progressCheck sets up a periodic polling of the server until all uploads complete
progressCheck : function (formID, origTransactionNum) {
    this.lastProgressCheckTime = new Date().getTime();
    isc.DMI.callBuiltin({
        methodName: "uploadProgressCheck",
        callback: this.getID()+".progressCallback(rpcRequest, rpcResponse, data, "+origTransactionNum+")",
        arguments: formID,
        requestParams : {willHandleError: true, showPrompt: false, formID: formID}
    });
},

progressCallback : function (rpcRequest, rpcResponse, data, origTransactionNum) {
    // delayed progress callback - upload may have completed
    var formID = rpcRequest.formID;
    var form = window[formID];

    if (!form || this.transactionNum !== origTransactionNum) return;

    // assume transient failure - retry request immediately (single server failure in a server farm
    // is a common reason for this)
    if (rpcResponse.status != isc.RPCResponse.STATUS_SUCCESS) this.progressCheck(formID);

    // server reports validation errors - typically file too large.
    if (data.errors) {
        isc.rpc.cancelQueue(this.transactionNum);
        form.setErrors(data.errors, true);
        this.saveButton.show();
        this.transactionNum = null;
        this.progressMeter.hide();
        return;
    }

    this.progressMeter.setFileSize(data.totalBytes);

    // if the total size of the file is less than the configured amount for which we would show
    // a progress bar, just return - no further requests will be made for this file.
    if (data.totalBytes < this.minFileSizeForProgressBar) {
        this.progressMeter.hideProgressBar();
        return;
    }


    // file is large enough for us to show a progress meter.  Show it and schedule the next progressCheck
    this.progressMeter.setBytesReceived(data.bytesSoFar);
    this.progressMeter.showProgressBar();
    this.progressMeter.setPercentDone(100 * data.bytesSoFar / data.totalBytes);

    var checkDelay = this.progressCheckFrequency - (new Date().getTime() - this.lastProgressCheckTime);
    if (checkDelay < 0) checkDelay = 0;
    this.delayCall("progressCheck", [formID, origTransactionNum], checkDelay);
}

});
//!<Deferred

// progress meter for reporting on progress of uploaded files
isc.defineClass("MultiFileProgressMeter", "VStack").addClassMethods({

formatBytes : function (fileSize) {
    var suffix;
    if (fileSize < 1024) {
        fileSize = Math.round(fileSize/1024);
        suffix = "B";
    } else if(fileSize < (1024*1024)) {
        fileSize = Math.round(fileSize/1024);
        suffix = "KB";
    } else {
        fileSize = Math.round(fileSize/(1024*1024)*100)/100;
        suffix = "MB";
    }

    return fileSize+"&nbsp;"+suffix;
}

});
isc.MultiFileProgressMeter.addProperties({
    height: 50
});

//!>Deferred
isc.MultiFileProgressMeter.addMethods({

initWidget : function () {
    this.Super("initWidget", arguments);

    this.progressLabelProperties = isc.addDefaults(this.progressLabelProperties, {
        dynamicContentsVars: { progressMeter: this },
        dynamicContents: true,
        contents: "<b><nobr>Saving ${progressMeter.fileName} ${progressMeter.getFormattedFileSize()}</nobr></b>"
    });

    this.addAutoChild("progressLabel", {
        height: 1
    }, "Canvas");
},

setFileName : function (fileName) {
    this.fileName = fileName;
    delete this.fileSize;
    this.bytesSoFar = 0;
    this.bytesReceived = 0;
    this.progressLabel.markForRedraw();
    if (this.progressBar) this.setPercentDone(0);
},

setFileSize : function (fileSize) {
    this.fileSize = fileSize;
    this.progressLabel.markForRedraw();
},

setBytesReceived : function (bytesReceived) {
    this.bytesSoFar = this.bytesReceived;;
    this.bytesReceived = bytesReceived;
    this.progressLabel.markForRedraw();
},

getFormattedFileSize : function () {
    if (!this.fileSize) return isc.emptyString;

    var result = "<br>";
    if (this.bytesReceived) {
        result += isc.MultiFileProgressMeter.formatBytes(this.bytesReceived) + " of ";
    }
    result += isc.MultiFileProgressMeter.formatBytes(this.fileSize);
    if (this.bytesSoFar && this.progressCheckFrequency) {
        var delta = this.bytesReceived - this.bytesSoFar;
        delta = isc.MultiFileProgressMeter.formatBytes(delta * 1000 / this.progressCheckFrequency);
        result += " (" + delta + "/sec)";
    }
    return result;
},

showProgressBar : function () {
    this.addAutoChild("progressBar", {
        overflow: "visible"
    }, "Progressbar");
    this.progressBar.show();
},

hideProgressBar : function () {
    if (this.progressBar) this.progressBar.hide();
},

setPercentDone : function (percentDone) {
    this.progressBar.setPercentDone(percentDone);
}

});

//!<Deferred

}

// Make old name available as a synonym
isc.addGlobal("MultiUploadItem", isc.MultiFileItem);
isc.addGlobal("MultiUploadPicker", isc.MultiFilePicker);



// Class will not work without the ListGrid
if (isc.ListGrid) {




//> @class DialogUploadItem
// A FormItem that allows uploading a single file as a field in a record stored in a related
// DataSource.
//
// @visibility internal
//<

isc.defineClass("DialogUploadItem", "StaticTextItem").addProperties({
    iconHeight: 16,
    iconWidth: 16,
    icons: [
        {src: "[SKIN]MultiUploadItem/icon_add_files.png", name:"upload", prompt: "Upload File",  click: "item.showPicker(true)"},
        {src: "[SKIN]MultiUploadItem/icon_remove_files.png", name:"remove", prompt: "Remove File", click: "item.removeFile()"}
    ],

    pickerConstructor: "DialogUploadPicker",
    noFileString: "[NONE]",


init : function () {
    this.Super("init", arguments);

    if (!this.pickerDefaults) this.pickerDefaults = {};
    isc.addProperties(this.pickerDefaults, {
        dataSource: this.dataSource
    });
},

mapValueToDisplay : function (value) {
    return value == null ? this.noFileString : this.Super("mapValueToDisplay", arguments);
},

showPicker : function () {
    this.Super("showPicker", arguments);
    var primaryKey = this.getValue('primaryKey');
    // pass primaryKey to ensure that uploads overwrite any existing file for this field.
    this.picker.foreignKeyValues = {
        primaryKey: primaryKey
    };
},

removeFile : function () {
    var primaryKey = this.getValue();
    if (primaryKey != this.defaultValue) {
        var ds = isc.DataSource.get(this.dataSource);
        ds.removeData({primaryKey: primaryKey}, this.getID()+".removeFileCallback(dsResponse)");
    }
},

removeFileCallback : function (dsResponse) {
    if (dsResponse.status != isc.DSResponse.STATUS_SUCCESS) {
        isc.warn("Unable to remove file: " + dsResponse.data);
        return;
    }
    this.setValue(this.defaultValue);
},
fileUploaded : function (dsRequest, dsResponse) {
    var data = dsResponse.data;
    var valueMap = {};
    valueMap[data.primaryKey] = data.file_filename;
    this.setValueMap(valueMap);
    this.setValue(data.primaryKey);
},

destroy : function () {
    if (this.picker) this.picker.destroy();
    this.Super("destroy", arguments);
},

_shouldAllowExpressions : function () {
    return false;
}


});


isc.defineClass("DialogUploadPicker", "MultiFilePicker").addProperties({
    maxUploadFields: 1,
    uploadWithoutPKButtonName: "Upload",
    uploadWithPKButtonName: "Upload",
    showUploadRemoveButton: false,
    uploadWithoutPK: true
});

}



// SOAPUploadItem depends on MultiUploadItem, which requires ListGrid (not part of the forms module)
if (isc.ListGrid) {



//> @class SOAPUploadItem
// @visibility internal
//<
isc.ClassFactory.defineClass("SOAPUploadItem", "DialogUploadItem");
isc.SOAPUploadItem.addProperties({
    dataSource: "sessionFiles"
});

}

//> @class SpinnerItem
//
// Item for picking a number. Includes arrow buttons to increase / decrease the value
//
// @treeLocation Client Reference/Forms/Form Items
// @visibility external
// @example spinnerItem
//<
isc.ClassFactory.defineClass("SpinnerItem", "TextItem");

isc.SpinnerItem.addClassProperties({

    //> @const  SpinnerItem.INCREASE_ICON    (FormItemIcon Properties : {...} : IRW)
    // Icon to increase the spinner's value (an up-arrow by default)
    // @visibility external
    //<
    INCREASE_ICON:  {width:16, height:9, src:"[SKIN]/DynamicForm/Spinner_increase_icon.png",
                        name:"increase",
                        showOver:false,
                        showFocusedWithItem:false,
                        // We don't need to support native focus, and we'll use mouseStillDown
                        // rather than standard icon click to handle activation
                        imgOnly:true,
                        hspace:0
                    },

    //> @const  SpinnerItem.DECREASE_ICON   (FormItemIconProperties : {...} : IRW)
    // Icon to decrease the spinner's value (a down-arrow by default)
    // @visibility external
    //<
    DECREASE_ICON:  {width:16, height:9, src:"[SKIN]/DynamicForm/Spinner_decrease_icon.png",
                        name:"decrease",
                        showOver:false,
                        showFocusedWithItem:false,
                        imgOnly:true,
                        hspace:0
                    }

    //> @attr   spinnerItem.mask  (string : null : IRWA)
    // Not applicable to a SpinnerItem.
    // @visibility  external
    //<
    //> @attr   spinnerItem.maskSaveLiterals   (boolean : null : IRWA)
    // Not applicable to a SpinnerItem.
    // @visibility  external
    //<
    //> @attr   spinnerItem.maskPadChar   (string : " " : IRWA)
    // Not applicable to a SpinnerItem.
    // @visibility  external
    //<
    //> @attr   spinnerItem.maskPromptChar   (string : "_" : IRWA)
    // Not applicable to a SpinnerItem.
    // @visibility  external
    //<
    //> @attr   spinnerItem.maskOverwriteMode   (boolean : null : IRWA)
    // Not applicable to a SpinnerItem.
    // @visibility  external
    //<

});

isc.SpinnerItem.addProperties({

    // Don't fire the change handler on every keypress, as we need the user to be able
    // to enter partial numbers (like the string "-") without us trying to validate it as a
    // number
    changeOnKeypress:false,

    // Don't allow tabbing to the spinner icons - you already have keyboard support via up and
    // down arrows, and we don't support rolling the vals up and down via keypresses on the
    // icons...
    canTabToIcons:false,

    // Default to matching the height of the 2 spinners exactly.
    height:18,

    //> @attr   SpinnerItem.step    (number : 1 : IRW)
    //  How much should the value be incremented / decremented when the user hits the icons
    //  to increase / decrease the value?
    // @visibility external
    // @example spinnerItem
    //<
    step:1

    //>@attr    SpinnerItem.max (number : null : IRW)
    // Maximum valid value for this item
    // @visibility external
    // @example spinnerItem
    //<

    //>@attr    SpinnerItem.min (number : null : IRW)
    // Minimum valid value for this item
    // @visibility external
    // @example spinnerItem
    //<


});

isc.SpinnerItem.addMethods({

    // Override init to ensure 'step' is a valid numeric value
    init : function () {
        this.Super("init",arguments);
        var step = this.step;
        if (step != null && !isc.isA.Number(step)) {
            step = parseFloat(step);
            if (!isc.isA.Number(step)) step = 1;
            this.step = step;
        }
    },

    // Override 'setUpIcons' to add the Increase / Decrease icons
    _setUpIcons : function () {
        // Add the pickbutton to the set of icons
        if (this.icons == null) this.icons = [];

        var inc = isc.addProperties({}, isc.SpinnerItem.INCREASE_ICON),
            dec = isc.addProperties({}, isc.SpinnerItem.DECREASE_ICON);

        this.icons.addListAt([inc,dec], 0);

        this.Super("_setUpIcons", arguments);
    },

    // Override getIconsHTML to write the increase / decrease icons out, one above the other


//icon, over,disabled,focused
    _$iconsHTMLCellStart: "<td tabIndex='-1'" + (isc.Browser.isIE ? " style='font-size:0px'" : ""),
    _$rowspanEquals2GT: " rowspan='2'>",
    _$iconsHTMLCellEnd: "</td>",
    getIconsHTML : function () {
        if (!this.showIcons) return isc.emptyString;

        var template = this._spinnerTableTemplate;
        if (template == null) {


            var cellStart = this._$iconsHTMLCellStart,

                sampleIcon = {},
                vAlign = this._getIconVAlign(sampleIcon),
                vMargin = this._getIconVMargin(sampleIcon);

            template = this._spinnerTableTemplate = [
                "<table role='presentation' style='margin-top:",      // [0]
                vMargin,                                              // [1]
                ";margin-bottom:",                                    // [2]
                vMargin,                                              // [3]
                "' border=0 cellpadding=0 cellspacing=0><tbody><tr>", // [4]
                cellStart,                                            // [5]
                ">",                                                  // [6]
                null,                                                 // [7] this.getIconHTML(this.icons[0])
                "</td>",                                              // [8]
                                                                      // <- extra iconHTML inserted here
                                                                      // For each extra icon, there are 4 template entries
                                                                         // this._$iconsHTMLCellStart, // [0]
                                                                         // " rowspan='2'>",           // [1]
                                                                         // iconHTML,                  // [2]
                                                                         // "</td>"                    // [3]
                "</tr><tr>",                                          // [9 + 4 * nRoomForExtraIcons]
                cellStart,                                            // [10 + 4 * nRoomForExtraIcons]
                ">",                                                  // [11 + 4 * nRoomForExtraIcons]
                null,                                                 // [12 + 4 * nRoomForExtraIcons] this.getIconHTML(this.icons[1])
                "</td></tr></tbody></table>"                          // [13 + 4 * nRoomForExtraIcons]
            ];
            this._nRoomForExtraIcons = 0;
        }

        // How many extra icons do we have? (The first two are the increase and decrease spinner icons.)
        var nExtraNeedRoomFor = this.icons.length - 2;

        // If we need space for fewer icons, shrink the template by 4 times the difference between
        // the number of extra icons that the template can currently accommodate and the number
        // of extra icons that we have.
        if (nExtraNeedRoomFor < this._nRoomForExtraIcons) {
            template.splice(9 + 4 * nExtraNeedRoomFor, 4 * (this._nRoomForExtraIcons - nExtraNeedRoomFor));
            this._nRoomForExtraIcons = nExtraNeedRoomFor;

        // If we don't have enough space for extra icons, then insert 4 times the number of extra
        // template entries that we need.
        } else {
            var d = nExtraNeedRoomFor - this._nRoomForExtraIcons;
            if (d > 0) {
                var spliceArgs = [9, 0];
                spliceArgs[2 + 4 * d - 1] = null;
                template.splice.apply(template, spliceArgs);
                this._nRoomForExtraIcons = nExtraNeedRoomFor;
            }
        }

        template[7] = this.getIconHTML(this.icons[0]);
        template[12 + 4 * this._nRoomForExtraIcons] = this.getIconHTML(this.icons[1]);

        var i = 9;
        for (var d = 2; d < this.icons.length; ++d, i += 4) {
            var icon = this.icons[d];

            // don't write out the icon if it specified a showIf, which returns false
            if (!this._shouldShowIcon(icon) || this._writeIconIntoItem(icon)) {
                template[i] = null;
                template[i + 1] = null;
                template[i + 2] = null;
                template[i + 3] = null;
            } else {
                template[i] = this._$iconsHTMLCellStart;
                template[i + 1] = this._$rowspanEquals2GT;
                template[i + 2] = this.getIconHTML(icon);
                template[i + 3] = this._$iconsHTMLCellEnd;
            }
        }

        return template.join(isc.emptyString);
    },

    getIconHTML : function (icon) {
        if (this._isPrinting()) return null;
        return this.Super("getIconHTML", arguments);
    },

    getTextBoxStyle : function () {
        if (this._isPrinting()) return isc.TextItem.getInstanceProperty("textBoxStyle");
        else return this.Super("getTextBoxStyle", arguments);
    },

    // Override getIconVMargin to return zero for the spinners.
    _getIconVMargin : function(icon) {
        if (icon == this.icons[0] || icon == this.icons[1]) return 0;
        return this.Super("_getIconVMargin", arguments);
    },

    // we're writing our 2 spinner icons one above the other
    // Only account for the width of one of them, not both
    getTotalIconsWidth : function () {
        var width = this.Super("getTotalIconsWidth", arguments);
        // A width of zero implies we're not showing any icons
        if (width > 0) {
            var spinWidthExcess = Math.max(this.icons[0].width, this.icons[1].width);
            width -= spinWidthExcess;
        }
        return width;
    },

    // Use 'mouseStillDown' to handle the user holding the mouse over the increase/decrease
    // icons
    mouseStillDown : function (form, item, event) {
        if (this.isDisabled() || this.isReadOnly()) return;

        // increment counter for simple value ramp
        this._mouseStillDownCounter++;

        if (this._valueIsDirty) this.updateValue();

        var nativeTarget = event.nativeTarget;

        var lastTargetIcon = this._lastTargetIcon,
            targetIcon = null;
        if (nativeTarget == this._getIconImgElement(this.icons[0])) {
            this.increaseValue();
            targetIcon = this.icons[0];
        } else if (nativeTarget == this._getIconImgElement(this.icons[1])) {
            this.decreaseValue();
            targetIcon = this.icons[1];
        }

        if (lastTargetIcon != targetIcon) {
            if (lastTargetIcon != null) {
                var img = this._getIconImgElement(lastTargetIcon);
                if (img) {
                    this._iconBlur(lastTargetIcon.name, img);
                }
            }

            this._lastTargetIcon = targetIcon;
            if (targetIcon != null) {
                var img = this._getIconImgElement(targetIcon);
                if (img) {
                    this._iconFocus(targetIcon.name, img);
                }
            }
        }
    },

    // reset counter for simple value ramp
    mouseDown : function (form, item, event) {
        if (this.isDisabled() || this.isReadOnly()) return;
        // force focus into the item on mouseDown - required to ensure we save out change made via
        // icon click
        if (!this.hasFocus) this.focusInItem();
        this._mouseStillDownCounter = 0;
        isc.Page.setEvent(isc.EH.MOUSE_UP, this, isc.Page.FIRE_ONCE, "_clearLastTargetIcon");
    },

    _clearLastTargetIcon : function () {
        var lastTargetIcon = this._lastTargetIcon;
        if (lastTargetIcon != null) {
            var img = this._getIconImgElement(lastTargetIcon);
            if (img) {
                this._iconBlur(lastTargetIcon.name, img);
            }
        }
        this._lastTargetIcon = null;
    },

    // Override handleKeyPress to increase / decrease on up / down arrow keys
    handleKeyPress : function (event, eventInfo) {

        if (this.Super("handleKeyPress", arguments) == false) return false;
        var keyName = event.keyName,
            readOnly = this.isReadOnly()
        ;
        if (!readOnly && keyName == "Arrow_Up") {
            this.increaseValue();
            return false;
        }
        if (!readOnly && keyName == "Arrow_Down") {
            this.decreaseValue();
            return false;
        }
    },

    increaseValue : function () {
        this.updateValue();
        var value = this.getValue();
        if (value != null && this.max == value) return;
        var mouseStillDownCounter = this._mouseStillDownCounter;
        // If we get called directly when the mouse is not down, don't crash, just increment by
        // a single step.
        // Otherwise apply a simple value ramp - double the step size every 2 seconds
        var step = this.step * (mouseStillDownCounter != null ?
                            Math.pow(2,
                                Math.floor(
                                    this._mouseStillDownCounter/(2000/isc.EH.STILL_DOWN_DELAY)
                                )
                            ) :
                                1);
        return this._increaseValue(value, step);
    },

    decreaseValue : function () {
        this.updateValue();
        var value = this.getValue();
        if (value != null && this.min == value) return;
        var mouseStillDownCounter = this._mouseStillDownCounter,
            step = (0-this.step) * (mouseStillDownCounter != null ?
                            Math.pow(2,
                                Math.floor(
                                    this._mouseStillDownCounter/(2000/isc.EH.STILL_DOWN_DELAY)
                                )
                            ) :
                                1);

        return this._increaseValue(value, step);
    },

    // Actually modify the value

    _increaseValue : function (value, step) {

        var minVal = this.min, maxVal = this.max;

        // If it's not a value to start with, default to zero.
        // If zero is outside our range, default to minVal (or maxVal if min is not defined)
        // instead
        if (!isc.isA.Number(value)) {
            value = 0;
            if ((minVal != null && value < minVal) || (maxVal != null && value > maxVal)) {

                value = (minVal != null ? minVal : maxVal);
            }
        // Otherwise increment as requested
        } else {


            var stepDP,
                valueDP;

            if (Math.round(step) == step) {
                stepDP = 0
            } else {

                var stepString = step + "";
                stepDP = stepString.length - (stepString.indexOf(".") +1);
            }
            if (Math.round(value) == value) {
                valueDP = 0;
            } else {
                var valueString = value + "";
                valueDP = valueString.length - (valueString.indexOf(".") +1);
            }
            value += step;

            var decimalPlaces = Math.max(stepDP, valueDP);
            if (decimalPlaces > 0) {

                value = parseFloat(value.toFixed(decimalPlaces));
            }

            // Don't go beyond max / min
            if (step > 0 && maxVal != null && value > maxVal) value = maxVal;
            else if (step < 0 && minVal != null && value < minVal) value = minVal;
        }
        var form = this.form,
            record = this.form ? this.form.getValues() : null,
            formattedVal = (this.formatEditorValue != null)
                            ? this.formatEditorValue(value,record,form,this)
                            : value;

        this.setElementValue(formattedVal);
        this.updateValue();

    },

    // Override mapDisplayToValue to return a numeric value rather than a string.
    mapDisplayToValue : function (value) {
        value = this.Super("mapDisplayToValue", arguments);

        if (isc.isA.String(value)) {
            var floatVal = parseFloat(value);
            if (floatVal == value) value = floatVal;
        }
        return value;
    },

    // Override updateValue (called when a user modifies the value) to validate the value as
    // numeric.
    updateValue : function () {

        var value = this.getElementValue();

        // unmap the value if necessary
        value = this.mapDisplayToValue(value);

        if (value == this._value) return;

        // If the user entered an invalid number just refuse to accept it.
        if (value != null && (!isc.isA.Number(value) || (this.max != null && value > this.max) ||
                                (this.min != null && value < this.min)) )
        {
            var oldValue = this.mapValueToDisplay(this._value);
            this.setElementValue(oldValue);
            return;
        }

        // Allow the superclass implementation to actually update the value
        this.Super("updateValue", arguments);
    },

    // Override setValue to disallow setting to a non numeric value programatically
    setValue : function (value, allowNullValue, a,b,c,d) {
        if (value != null && !isc.isA.Number(value)) {
            var numVal = parseFloat(value);
            if (numVal == value) value = numVal;
            else {
                this.logWarn("setValue(): passed '" + value +
                                "'. This is not a valid number - rejecting this value");
                value = null;
            }
        }
        if (value != null) {
            if (this.max != null && value > this.max) {
                this.logWarn("setValue passed "+ value +
                                " - exceeds specified maximum. Clamping to this.max.");
                value = this.max;
            }
            if (this.min != null && value < this.min) {
                this.logWarn("setValue passed "+ value +
                                " - less than specified minimum. Clamping to this.min.");
                value = this.min;
            }
        }

        return this.invokeSuper(isc.SpinnerItem, "setValue", value, allowNullValue, a,b,c,d);
    }

});





//> @class SliderItem
// FormItem that uses a +link{class:Slider} component to present an interface for picking
// from either a continuous range or a range with a small number of discrete values.
//
// @treeLocation Client Reference/Forms/Form Items
// @visibility external
// @example sliderItem
//<



isc.ClassFactory.defineClass("SliderItem", "CanvasItem");

isc.SliderItem.addProperties({

    // Passthroughs: certain properties are very likely to be set and so we allow setting them
    // right on the SliderItem
    // ---------------------------------------------------------------------------------------

    //> @attr sliderItem.vertical (Boolean : false : IR)
    // @include slider.vertical
    //<
    vertical:false,

    //>    @attr sliderItem.minValue (float : 1 : IRW)
    // @include slider.minValue
    // @example sliderItem
    //<
    minValue:1,

    //>    @attr sliderItem.maxValue (float : 100 : IRW)
    // @include slider.maxValue
    // @example sliderItem
    //<
    maxValue:100,

    //>    @attr sliderItem.numValues (integer : null : IRW)
    // @include slider.numValues
    // @example sliderItem
    //<

    //> @attr sliderItem.roundValues (Boolean : true : IR)
    // @include slider.roundValues
    // @example sliderItem
    //<
    roundValues:true,

    //> @attr sliderItem.roundPrecision (number : 1 : IR)
    // @include slider.roundPrecision
    // @example sliderItem
    //<
    roundPrecision:1,

    //>    @attr sliderItem.defaultValue (int : 1 : [IRW])
    // Default value for this sliderItems is 1.
    // @visibility external
    //<
    defaultValue:1,

    // sliderProperties: use for setting any other Slider property
    // ---------------------------------------------------------------------------------------

    //> @attr sliderItem.sliderProperties (Slider properties : null : IR)
    // Properties to add to the automatically created +link{class:Slider} used by this
    // FormItem.  See the +link{class:Slider} class for reference.
    //
    // @visibility external
    //<

    // ---------------------------------------------------------------------------------------

    shouldSaveValue:true,

    sliderDefaults : {
        autoDraw:false,
        showTitle:false,
        valueChanged : function () {
            if (this.canvasItem) this.canvasItem.sliderChange();
        }
    },
    autoDestroy:true
});

isc.SliderItem.addMethods({
    init : function () {
        this.Super("init", arguments);
        // used for change detection
        this._currentValue = this.getDefaultValue();
    },

    // Override _createCanvas to set up a Slider as this item's canvas, and set up appropriate
    // set of properties.
    _createCanvas : function () {
        // create our slider based on our sliderDefaults, and appropriate instance properties.
        var sliderDefaults = this.sliderDefaults;

        var sliderProperties = isc.addProperties(
                                {},
                                sliderDefaults,
                                {
                                    vertical:this.vertical,
                                    minValue:this.minValue,
                                    maxValue:this.maxValue,
                                    value:this.defaultValue,
                                    numValues:this.numValues,
                                    roundValues:this.roundValues,
                                    roundPrecision:this.roundPrecision,
                                    tabIndex:this.getGlobalTabIndex()
                                },
                                this.sliderProperties
                               );

        this.canvas = isc.Slider.create(sliderProperties);

        this.Super("_createCanvas", arguments);
    },

    // Sliders are editable
    isEditable : function () {
        return this.getCanEdit();
    },

    readOnlyDisplayChanged : function (appearance) {
        var readOnly = this.isReadOnly(),
            c = this.canvas;

        if (c.children) c.children.map("setDisabled", readOnly);
    },

    canEditChanged : function (canEdit) {
        if (this.canvas) {
            if (canEdit) {
                this.canvas.setDisabled(false);
                if (this.canvas.children) this.canvas.children.map("setDisabled", false);
            } else {
                this.canvas.setDisabled(true);
                if (this.canvas.children) this.canvas.children.map("setDisabled", true);
            }
        }
    },

    // Override setValue to update the value on the slider
    setValue : function (value) {
        this._setValueCalled = true;

        var defaultVal;
        if (value == null) {
            defaultVal = this.getDefaultValue();
            // don't apply the default value if it's not set - this allows for the distinction
            // between setting the value to 'null' vs 'undefined'
            if (defaultVal != null) value = defaultVal;
        }
        // update the previous value so we don't fire a change handler on 'sliderChanged'
        this._currentValue = value;
        this.canvas.setValue(value, (defaultVal != null));
        // No need to save this value - that should be handled by sliderChange
    },

    getValue : function () {
        return this.canvas.getValue();
    },

    //>@attr sliderItem.changeOnDrag (Boolean : true : IRW)
    // Should this sliderItem update its value and fire change handlers while the user is
    // actively dragging the slider.
    // Setting this attribute value to <code>false</code> will suppress any change notifications
    // from the user dragging the slider thumb until the user releases the mouse at the final
    // position.
    // This can be useful to avoid repeatedly firing expensive operations such as server fetches
    // while the user drags through a range of values.
    // @visibility external
    //<

    changeOnDrag:true,

    // Define a sliderChange function to handle value changes
    sliderChange : function() {

        // Note: the slider.valueChanged method doesn't inform us what the old value was, so
        // we need to track this ourselves in order to pass it to any item level change handler.
        var val = this.canvas.getValue();

        if (this._currentValue != val) {
            // if changeOnDrag is false, don't update anything until the user is done dragging
            // the slider around
            if (this.changeOnDrag || !this.canvas.valueIsChanging()) {
                this._updateValue(val);
                this._currentValue = val;
            }

        // catch the case where there was an explicit setValue() call, and save out the new
        // value from the slider.
        } else {
            this.saveValue(val);
        }
    },

    //>    @method    sliderItem.setMinValue()   ([])
    // @include slider.setMinValue()
    //<
    setMinValue : function (newValue) {
        this.canvas.setMinValue(newValue);
        // update this just in case someone checks canvasItem.minValue, since we do declare it
        // as a property on the CanvasItem.
        this.minValue = newValue;
    },

    //>    @method    sliderItem.setMaxValue()   ([])
    // @include slider.setMaxValue()
    //<
    setMaxValue : function (newValue) {
        this.canvas.setMaxValue(newValue);
        this.maxValue = newValue;
    },

    //>    @method    sliderItem.setNumValues()   ([])
    // @include slider.setNumValues()
    //<
    setNumValues : function (newNumValues) {
        this.canvas.setNumValues(newNumValues);
        this.numValues = newNumValues;
    },

    _shouldAllowExpressions : function () {
        return false;
    }




});


//>    @class    ColorItem
// FormItem for selecting a color via a pop-up ColorPicker.
//
// @treeLocation Client Reference/Forms/Form Items
// @visibility external
//<

isc.ClassFactory.defineClass("ColorItem", "TextItem");

//> @class  ColorPickerItem
//
// Form item for colors input
// @visibility external
//<
// Alias for smartgwt
isc.addGlobal("ColorPickerItem", isc.ColorItem);

isc.ColorItem.addProperties({
    // Don't update on keystrokes, as we're verifying the color on change.
    changeOnBlur:true,
    changeOnKeypress:false,

    // Properties for the default formItem picker handling code
    pickerConstructor: "ColorPicker",
    pickerDefaults: {
        // By default the form item 'picker' subsystem will fired pickerDataChanged in response
        // to a picker firing its dataChanged() method.
        // ColorChoosers support 'colorSelected()' rather than dataChanged, so override this
        // notification method to fire pickerColorSelected instead.
        colorSelected : function (color, opacity) {
            this.callingFormItem.pickerColorSelected(color, opacity)
        }
    },


    //>    @attr    colorItem.showPickerIcon    (Boolean : true : IRW)
    // Should the pick button icon be shown for choosing colors from a ColorPicker
    // @visibility external
    //<
    showPickerIcon:true,

    //>    @attr    colorItem.pickerIconWidth (Integer : 18 : IRW)
    // @include FormItem.pickerIconWidth
    // @visibility external
    //<
    pickerIconWidth:18,

    //>    @attr    colorItem.pickerIconHeight    (Integer : 18 : IRW)
    // @include FormItem.pickerIconHeight
    // @visibility external
    //<
    pickerIconHeight:18,

    //> @attr colorItem.pickerIconSrc (SCImgURL : "[SKIN]/DynamicForm/ColorPicker_icon.png" : IRW)
    // @include FormItem.pickerIconSrc
    // @visibility external
    //<
    // Note - by default this image has a transparent patch allowing the
    // background color to show through.
    pickerIconSrc:"[SKIN]/DynamicForm/ColorPicker_icon.png",

    //> @attr colorItem.pickerIconPrompt (string : "Click to select a new color" : IR)
    // @include formItem.pickerIconPrompt
    // @group i18nMessages
    // @visibility external
    //<
    pickerIconPrompt : "Click to select a new color",

    //> @attr colorItem.pickerIconProperties (object : {...} : IRWA)
    // Properties for the picker icon.
    // @visibility external
    //<
    pickerIconProperties:{
        showOver:false
    },

    //>@attr colorItem.defaultPickerMode (string : "simple" : IR)
    // The +link{ColorPicker} associated with this ColorItem can operate in either a "simple" mode
    // (where it displays just the 40 basic colors and allows the user to click one), or a
    // "complex" mode (where the user can specify a color from anywhere in the spectrum, with
    // an optional alpha element). The defaultPickMode attribute specifies which of these two
    // modes is in force when the picker first loads.
    // @see ColorPicker.defaultPickMode()
    // @visibility external
    //<
    defaultPickerMode: "simple",

    //>@attr colorItem.allowComplexMode (Boolean : true : IR)
    // Should "complex" mode be allowed for the +link{ColorPicker} window associated with
    // this ColorItem?<p>
    // If false, no "More" button is shown on the simple picker
    // @visibility external
    //<
    allowComplexMode: true,

    //> @attr   colorItem.supportsTransparency  (Boolean : false : IRW)
    // Determines whether the +link{ColorPicker} associated with this ColorItem allows the user
    // to set transparency/opacity information whilst selecting a color. If false, no opacity
    // slider is shown and all colors are 100% opaque.<p>
    // <b>Note</b> ColorItems are representations of HTML color strings, they do not implicitly
    // support transparency.  Setting supportsTransparency to true just allows the user to
    // set opacity with the picker; if you actually want to capture that information,  you will
    // also need to override +link{pickerColorSelected}.
    // @visibility external
    //<
    supportsTransparency : false,

    // Disable native spellChecking on color fields
    browserSpellCheck:false,

    type: "color"
});

isc.ColorItem.addMethods({

    init : function() {
        this.pickerDefaults.defaultPickMode = this.defaultPickerMode;
        this.pickerDefaults.allowComplexMode = this.allowComplexMode;
        this.pickerDefaults.supportsTransparency = this.supportsTransparency;
        this.Super("init", arguments);
    },

    // Override updateValue to validate the color, and update the icon color
    updateValue : function () {

        var oldValue = this._value,
            value = this.getElementValue();

        // unmap the value if necessary
        value = this.mapDisplayToValue(value);

        if (value == this._value) return;

        // If the user entered an invalid color just refuse to accept it.
        if (value != null && !isc.isA.color(value)) {
            this.setElementValue(oldValue);
            return;
        }

        // Allow the superclass implementation to actually update the value
        this.Super("updateValue", arguments);

        // Assuming the change wasn't rejected, update our icon background color.
        if (this.showPickerIcon && this._value != oldValue) {
            this.setIconBackgroundColor(this.getPickerIcon(), this._value);
        }
    },

    //>    @method    colorItem.getDefaultValue()    (A)
    //        Override getDefaultValue to guarantee that it returns a color (or null)
    //<
    getDefaultValue : function () {
        var value = this.Super("getDefaultValue", arguments);
        if (value && !isc.isA.color(value)) {
            this.logWarn("Default value:" + value + " is not a valid color identifier." +
                        " Ignoring this default.");
            value = this.defaultValue = null;
        }
        return value;
    },


    // Override 'showPicker' to pass in supportsTransparency
    showPicker : function () {
        this.picker = isc.ColorChooser.getSharedColorPicker(this.pickerDefaults);
        var picker = this.picker;

        var oldItem = picker.callingFormItem;
        if (oldItem != this) {
            picker.callingFormItem = this;
            picker.callingForm = this.form;
            picker.setSupportsTransparency(this.supportsTransparency);
        }
        picker.setHtmlColor(this._value || "");
        if (picker.allowComplexMode) {
            if (picker._currentPickMode == 'simple') {
                picker.modeToggleButton.setTitle(picker.moreButtonTitle);
            } else {
                picker.modeToggleButton.setTitle(picker.lessButtonTitle);
            }
        }

        return this.Super("showPicker", arguments)

    },


    //>    @method    colorItem.pickerColorSelected()
    //  Store the color value selected by the user from the color picker.  You will need to
    //  override this method if you wish to capture opacity information from the +link{ColorPicker}
    //  @param  color (String)   the selected color as a string
    //  @param  opacity (int)  the selected opacity, from 0 (transparent) to 100 (opaque).
    //                            Only applicable if +link{supportsTransparency} is true.
    // @visibility external
    //<
    pickerColorSelected : function (color, opacity) {
        // If using a mask for color entry, the valuemap cannot be used.
        if (!this.mask) {
            color = this.mapValueToDisplay(color);
        }
        this.setElementValue(color);
        this.updateValue();
    },

    // Override setValue to ensure we update the color swatch icon.
    setValue : function (newValue) {
        this.Super("setValue", arguments);
        this.setIconBackgroundColor(this.getPickerIcon(), this._value);

    }

});



// Class will not work without the ListGrid
if (isc.ListGrid) {

// ValueMapEditor class
// Simple subclass of HLayout containing a button and a listGrid
// used by the valueMapItem class (currently not functional as a standalone widget)
isc.ClassFactory.defineClass("ValueMapEditor", "VLayout");

isc.ValueMapEditor.addProperties({

    // Defaults for the button to toggle state
    mapTypeConstructor : isc.Button,

    mapTypeDefaults : {
        autoDraw:false,

        width:"100%",

        click : function () {
            this.creator.canvasItem.toggleObjectArray();
        },

        showIf : function () {
            return this.creator.canvasItem.showMapTypeButton;
        },

        getTitle : function () {
            var canvasItem = this.creator.canvasItem,
                isObject = canvasItem.saveAsObject;
            if (isObject) return canvasItem.saveAsObjectTitle;
            else return canvasItem.saveAsArrayTitle;
        }
    },

    selectorConstructor : isc.ListGrid,

    selectorDefaults : {
        // allow it to auto-expand as required
        height:1,
        overflow:"visible",
        bodyOverflow:"visible",
        inherentHeight:true,

        // Make it editable, and support adding new value map options easily
        showNewRecordRow:true,
        listEndEditAction:"next",
        canEdit:true,
        editEvent:isc.EH.CLICK,

        // NOTE: tall enough to avoid row slightly enlarging when starting edit, which
        // will normally cause the containing form to redraw
        cellHeight:22,

        // Disable selection of rows.
        selectionType:isc.Selection.NONE,
        selectOnEdit:false,

        leaveScrollbarGap:false,
        showSortArrow:isc.ListGrid.NONE,
        canSort:false,
        canResizeFields:false,

        // Override dataChanged to save the new valueMap as the user edits the ListGrid
        // rows
        dataChanged : function () {

            this.Super("dataChanged", arguments);
            if (this.creator && this.creator.canvasItem)
                this.creator.canvasItem.updateValue();
        },

        // validation: Ensure the value map ends up being a valid structure.
        stopOnErrors:true,

        validateCellValue : function (rowNum, colNum, newValue, oldValue) {

            var fieldName = this.getFieldName(colNum),
                canvasItem = this.creator.canvasItem,
                valueMapErrors =
                    canvasItem._validateSelectorCellValue(fieldName, rowNum, newValue);

            if (valueMapErrors) return valueMapErrors;
            return this.Super("validateCellValue", arguments);
        },

        // ContextMenu:
        // We want to show a context menu allowing the user to
        // - add a new option
        // - delete the clicked option (if there is one)
        // - toggle between Array and JS Object Literal type valueMaps

        cellContextClick : function (record, rowNum, colNum) {
            var cm = this.creator.canvasItem.getSelectorContextMenu(record);
            cm.showContextMenu();
            // cancel the native cm
            return false;
        },

        getHeaderContextMenu : function () {
            return this.creator.canvasItem.getSelectorContextMenu();
        }
    }
});

isc.ValueMapEditor.addMethods({
    initWidget : function () {
        if (!this.canvasItem) {
            this.logWarn("ValueMapEditors are not supported as standalone widgets at this time.");
            return;
        }

        this.Super("initWidget", arguments);

        this.addAutoChild("mapType", {});

        this.addAutoChild("selector", {

            newRecordRowMessage : this.canvasItem.newOptionRowMessage,

            showHeader:this.showHeader,

            // this._getSelectorFields() will return either a single field for an array type
            // valueMap, or 2 fields (for value / display value) for object type valueMaps.
            fields:this.canvasItem._getSelectorFields(),

            // this._getSelectorData() will turn this item's valueMap into a valid data array
            // for the ListGrid
            data:this.canvasItem._getSelectorData()

        });
    }
});


//>    @class    ValueMapItem
//
// Form item with an interface especially designed for editing valueMaps.
// The valueMaps will show up in an editable ListGrid, with context menu items for adding /
// removing values.
// Supports editing JS object literal format value maps or arrays. Context menu includes an
// option to change between these formats.
//<
isc.ClassFactory.defineClass("ValueMapItem", "CanvasItem");


isc.ValueMapItem.addProperties({

    // Override height to be small - will auto expand to accomodate content
    height:1,

    autoDestroy:true,

    shouldSaveValue:true,

    //>@attr ValueMapItem.saveAsObject (boolean : false : IR)
    // Whether to create an Array or Object valueMap.  Switchable by the user
    // if +link{showMapTypeButton} is true.
    //<

    //>@attr    ValueMapItem.showMapTypeButton  (boolean : true : IR)
    // Should we show the button to toggle between object / array valueMaps?
    // We also support this functionality via a context menu.
    //<
    showMapTypeButton: true,

    //> @attr ValueMapItem.allowDuplicates (boolean : false : IRW)
    // Whether to allow duplicates when editing Arrays (ie, when +link{saveAsObject}
    // is false).  Duplicate property names in Objects are never allowed.
    //<

    //> @attr ValueMapItem.showHeader (boolean : true : IR)
    // Whether headers should be show in the grid of values.  Typically not desired for
    // editing simple arrays of items.
    //<
    showHeader:true,

    //>@attr    ValueMapItem.newOptionRowMessage    (string : "Click to add a new option" : IR)
    //  Message to display in the 'new option row' - the row which can be clicked by the
    // user to add a new option to the valueMap.
    //<
    newOptionRowMessage: "Click to add a new option",

    //>@attr    ValueMapItem.displayTitle   (string : "Display" : IR)
    //  Title for the display value field in our selector listGrid. Only shown if this item's
    //  value is an object (mapping a display value to an internal value).
    //<
    displayTitle: "Display",

    //>@attr    ValueMapItem.valueTitle (string : "Value" : IR)
    //  Title for the value field in our selector listGrid.
    //<
    valueTitle: "Value",


    //>@attr    ValueMapItem.undefinedKeyErrorMessage   (string : "Each valueMap option must have a defined value" : IRW)
    //  Error message to display if the user attempts to save an undefined key in a
    //  JS Literal object valueMap
    //<
    undefinedKeyErrorMessage : "Each valueMap option must have a defined value",

    //>@attr    ValueMapItem.duplicateValueErrorMessage (string : "Please enter a unique value for this option" : IRW)
    //  Error message to display if the user attempts to save duplicate values in this item's
    //  valueMap.
    //<
    duplicateValueErrorMessage : "Please enter a unique value for this option",

    //>@attr    ValueMapItem.saveAsArrayTitle   (string : "Stored == Displayed" : IRW)
    //  Title for the button / menu item to save our valueMap in array (rather than JS
    //  Object literal) format
    //<
    saveAsArrayTitle : "Stored == Displayed",

    //>@attr    ValueMapItem.saveAsObjectTitle  (string : "Stored != Displayed" : IRW)
    //  Title for the button / menu item to save our valueMap in JS Object literal (rather
    //  than Array) format.
    //<
    saveAsObjectTitle : "Stored != Displayed"

});

isc.ValueMapItem.addMethods({

    // _createCanvas()
    // Create the ListGrid used to actually edit the valueMap and set up APIs between the
    // ListGrid and the ValueMapItem
    _createCanvas : function () {

        if (this.canvas) return;

        // Ensure we are working with a valid value (note: should already have been set to
        // this.defaultValue if appropriate, so this handles the case where this.defaultValue
        // is null)
        if (this._value == null) this._value = [];

        this.canvas = isc.ValueMapEditor.create({
            autoDraw:false, _generated:true,
            ID:this.getID() + "_editor",
            canvasItem:this,

            showMapType:this.showMapTypeButton,
            showHeader:this.showHeader,

            // Allow it to expand to accomodate content.
            height:this.height,
            overflow:isc.Canvas.VISIBLE
        });

        this.Super("_createCanvas", arguments);
    },

    _getMapTypeButton : function () {
        return this.canvas.mapType;
    },

    // _getSelectorGrid() - returns a pointer to the ListGrid in the valueMap editor we created
    // on init.
    _getSelectorGrid : function () {
        return this.canvas.selector;
    },

    // _getSelectorData()
    // Converts our (valueMap) value to an array of records that can be displayed by the
    // selector ListGrid
    _getSelectorData : function (value) {
        var value = value || this.getValue();
        if (!value) value = this.saveAsObject ? {} : [];

        var data = [];
        if (isc.isAn.Array(value)) {
            for (var i = 0; i < value.length; i++) {
                data[i] = {value:value[i]}
            }
        } else {
            var i = 0;
            for (var fieldValue in value) {
                data[i] = {value:fieldValue, display:value[fieldValue]}
                i++;
            }
        }
        return data;
    },

    // _getSelectorFields()
    // Fields for the selector listGrid. If we're saving the valueMap as an object, returns
    // 2 fields for display and internal value, otherwise just returns a value field.
    _getSelectorFields : function () {
        if (!this._displayField || !this._valueField) {
            this._displayField = {name:"display", title:this.displayTitle};
            this._valueField = {name:"value", title:this.valueTitle};
        }
        var fields = [this._valueField];
        if (this.saveAsObject) fields.add(this._displayField);

        return fields;
    },


    // _validateSelectorCellValue()   Method to validate the edited value to ensure it will
    // produce a valid valueMap.

    _validateSelectorCellValue : function (fieldName, index, newValue) {
        // No restrictions on the display value for valueMaps.
        if (fieldName != "value") return;
        if (!this.saveAsObject && this.allowDuplicates) return;

        var grid = this._getSelectorGrid(),
            newRow = index > grid.data.getLength(),
            currentValue = this._value,
            currentKeys;

        if (this.saveAsObject) {


            if (newValue == null || newValue == "") {
                return [this.undefinedKeyErrorMessage]
            }
            // NOTE: getKeys() returns [] for null
            currentKeys = isc.getKeys(currentValue);
        } else {
            currentKeys = currentValue || [];
        }

        var collidingValues;
        if (newRow) {
            if (currentKeys.contains(newValue)) collidingValues = true;
        } else {
            for (var i = 0; i < currentKeys.length; i++) {
                if (index == i) continue;
                if (newValue == currentKeys[i]) {
                    collidingValues = true;
                    break;
                }
            }
        }

        // If we have colliding values, notify the user
        if (collidingValues) return [this.duplicateValueErrorMessage];
        return null;
    },

    // getSelectorContextMenu()     Method to return a contextMenu for our selector listGrid.
    // Called from a click on some cell or on the header.
    // Will be passed a non-null record parameter if the event occurred over a saved row.

    getSelectorContextMenu : function (record) {

        if (!this._selectorCM) {

            var items = [
                {title:"Add new option", click:"menu.canvasItem.addOption()"},
                {title:"Delete option", enableIf:"menu.record != null",
                    click:"menu.canvasItem.removeOption(menu.record)"
                }
            ];
            if (this.showMapTypeButton) {
                items.add(
                {   dynamicTitle:"this.canvasItem._getToggleObjectArrayTitle()",
                    click:"menu.canvasItem.toggleObjectArray()"
                }
                );
            }

            this._selectorCM = this.ns.Menu.create({
                                    canvasItem : this, ID:this.getID() + "_selectorMenu",
                                    data:items
                               });
        }

        this._selectorCM.record = record;
        return this._selectorCM;
    },


    // Helper methods for the menu options

    //>@method  ValueMapItem.addOption()
    // Start editing a new option added to the end of the valueMap
    //<
    addOption : function () {
        this._getSelectorGrid().startEditingNew();
    },

    //>@method  ValueMapItem.removeOption()
    // Remove an option from the valueMap.
    //<

    removeOption : function (record) {
        var grid = this._getSelectorGrid();
        grid.data.remove(record);
        // datachanged will call 'upateValue' on this item
        grid.data.dataChanged();
    },

    // _getToggleObjectArrayTitle()     returns the dynamic title for the menu item used
    // to toggle between saving valueMap as an object and as an array.
    _getToggleObjectArrayTitle : function () {
        var saveAsObject = this.saveAsObject;
        return saveAsObject ? this.saveAsArrayTitle
                            : this.saveAsObjectTitle;
    },

    // Helper method to toggle between saving valueMap as an object and as an array.
    toggleObjectArray : function () {
        this.setSaveAsObject(!this.saveAsObject);
    },


    // setSaveAsObject()
    // method to modify whether the valueMap produced is an object or an array.
    // Will munge data into approprate format
    setSaveAsObject : function (saveAsObject) {
        if (this.saveAsObject == saveAsObject) return;

        // convert the current value to the new value
        // We could warn if we're moving from an object to an array as we'll lose some data.
        var newValue,   value = this.getValue();
        if (saveAsObject) {
            newValue = {};
            if (value != null) {
                for (var i = 0; i < value.length; i++) {
                    newValue[value[i]] = value[i];
                }
            }

        } else {
            newValue = [];
            for (var internalValue in value) {
                newValue.add(internalValue);
            }
        }

        this._value = newValue;
        this.saveAsObject = saveAsObject;

        var grid = this._getSelectorGrid(),
            data = this._getSelectorData();

        // If we're not initialized for some reason, bail here - when we get initialized we'll
        // set up our UI with the appropriate properties.
        if (!grid) return;

        grid.setData(data);

        grid.setFields(this._getSelectorFields());

        // redraw the button (if we're showing it) to refresh the title
        if (this.showMapTypeButton) this._getMapTypeButton().markForRedraw();

    },


    // Override isEditable
    isEditable : function () {
        return true;
    },

    // Override updateValue() to retrieve the value from our selector ListGrid
    updateValue : function () {

        var grid = this._getSelectorGrid(),
            data = grid.data,
            saveAsObject = this.saveAsObject,
            value = saveAsObject ? {} : [],
            changed;

        if (saveAsObject) {
            // Make a copy of the values currently stored as this._value. We use this for
            // change detection
            var dupVals = {};
            if (isc.isAn.Array(this._value)) {
                changed = true;
             } else {
                isc.addProperties(dupVals, this._value);
            }

            var dupKeys = isc.getKeys(dupVals);

            for (var i = 0; i < data.length; i++) {
                var record = data[i],
                    newVal = record.display;

                value[record.value] = record.display;
                // If the key was previously undefined or mapped to a different display value,
                // we've changed
                if (!dupKeys.contains(newVal) || (dupVals[record.value] != newVal))
                    changed = true;
                delete dupVals[record.value];
            }

            // If the old values contained any value not contained in the new values, we
            // need to fire our change handler.
            if (isc.getKeys(dupVals).length != 0) changed = true;

        } else {

            // If our saved data is of the wrong type, or of a different length,
            // we know the value has changed - ensure we fire the change handler.
            if (!isc.isAn.Array(this._value) || (this._value.length != data.length)) {
                changed = true;
            }

            for (var i = 0; i < data.length; i++) {
                var record = data[i],
                    newVal = record.value;
                value[i] = newVal;

                // Avoid trying to examine this._value if we know the value has changed.
                // This avoids us trying to access values by index on an object.
                if (!changed && (this._value[i] != newVal)) changed = true;
            }

        }


        if (!changed) return;

        // fire the change handler, and bail if the change failed validation, etc.
        // Note: this method will call 'setValue()' to reset to the old value, or any value
        // suggested by the validators
        if (this.handleChange(value, this._value) == false) return;

        value = this._changeValue;
        delete this._changeValue;

        // save the value
        this.saveValue(value);
    },

    // Override setValue to update the data in the ListGrid
    setValue : function (value) {
        this._setValueCalled = true;

        if (value != null && !isc.isAn.Object(value)) {
            this.logWarn("setValue() passed an invalid object. Must be a valueMap (either " +
                         "specified as an array or a raw JS object.");
            return
        }

        var valueIsArray = isc.isAn.Array(value),
            saveAsObject = (!!this.saveAsObject);

        // If we're passed an array when we should be saving as an object (or vice versa)
        // update this.saveAsObject
        if (value != null && valueIsArray == saveAsObject) {
            this.logInfo("setValue() passed a valueMap of type "  +
                            (valueIsArray ? "Array" : "Object") +
                         ".  Updating this.saveAsObject to match this data type.");
            this.setSaveAsObject(!valueIsArray);
            saveAsObject = this.saveAsObject;

        }

        this._value = value;
        this._getSelectorGrid().setData(this._getSelectorData());
    }

})

}

// Editor for Arrays of simple types (eg DynamicForm.colWidths)
isc.defineClass("ArrayItem", isc.ValueMapItem).addProperties({
    showMapTypeButton:false,
    showHeader:false,
    saveAsObject:false,
    allowDuplicates:true,
    newOptionRowMessage:"Click to add values"
});

// Editor for mappings expressed as objects, where Array format is not allowed
isc.defineClass("MappingItem", isc.ValueMapItem).addProperties({
    showMapTypeButton:false,
    saveAsObject:true
});


// NOTE: This class is Tree-based and thus does not support composite primary keys


// Class will not work without the ListGrid
if (isc.ListGrid) {



//>    @class    PickTreeItem
// FormItem that allows picking a value from a hierarchical data model.
//
// @treeLocation Client Reference/Forms/Form Items
// @visibility external
// @example pickTree
//<
isc.ClassFactory.defineClass("PickTreeItem", "CanvasItem");

isc.PickTreeItem.addClassProperties({
    // create a single 'itemSelected' method to be applied to our menu buttons.
    // (fired in the scope of the button
    _itemSelected : function (item) {
        return this.canvasItem._itemSelected(item);
    },

    // getTitle() method for our TreeMenuButton - overridden to handle the case where the
    // form item value represents an as-yet-unloaded node (so 'getTitle()' is not available).
    _getButtonTitle : function () {
        var selection = this.getSelectedItem();
        if (selection == null) {
            // If a value is selected in the item, but we don't have the same node selected in
            // this TreeMenuButton widget, check the static valueMap for a title to display.
            var item = this.canvasItem, value = this.canvasItem.getValue();
            if (value != null) return item.mapValueToDisplay(value);
        }
        return this.Super("getTitle", arguments);
    },


    // If this item's menu is databound, and loadDataOnDemand is false, this method will be
    // fired when the entire tree is loaded (happens around init time).
    // We want to ensure that any value we have is associated with the appropriate node at this
    // point. This allows us to call setSelectedItem() on the TreeMenuButton, which hilights
    // the path to the node for our selected value.

    _treeDataLoaded : function () {
        var item = this.canvasItem;
        item.setValue(item.getValue());
    }

});

isc.PickTreeItem.addProperties({

     // Override canFocus -- even though buttons have no data element, they can accept focus.
    canFocus:true,

    // We will be saving out the selected values
    shouldSaveValue:true,

    // Class level defaults to apply to the button we create.
    // Picked up via the addAutoChild mechanism.

    buttonDefaults : {
        height:19
    },

    // Note: we use the 'addAutoChild()' method to create our canvas, so we can specify
    // properties / defaults via "buttonDefaults" and "buttonProperties".

    //> @attr PickTreeItem.dataSource (datasource : null : IRA)
    // If specified, the tree of possible options will be derived from the dataSource as a
    // ResultTree, rather than using this.valueTree.  Options can be loaded on demand or up
    // front according tp +link{attr:PickTreeItem.loadDataOnDemand}.
    // @visibility external
    //<

    //> @attr PickTreeItem.valueTree  (tree : null : IR)
    // A +link{class:Tree} of options from which the user can select.
    // @visibility external
    // @example pickTree
    //<

    //> @attr PickTreeItem.loadDataOnDemand   (boolean : null : IRA)
    // If this is a databound item, should the load our set of possible options be loaded
    // on demand (as submenus are displayed), or upfront?
    // @visibility external
    //<

    //loadDataOnDemand : false,

    //> @attr   PickTreeItem.displayField   (string : null : IR)
    // Specifies an alternative field from which display values should be retrieved for this
    // item.<br>
    // If this item is not databound (+link{attr:PickTreeItem.dataSource} is unset), this is
    // implemented by picking up the value of the specified field from the
    // +link{attr:valueTree}.<br>
    // Otherwise this item will attempt to map its underlying value to a display value
    // by retrieving a record from the +link{attr:PickTreeItem.dataSource} where the
    // +link{PickTreeItem.valueField} matches this item's value, and displaying the
    // <code>displayField</code> value from that record.<br>
    // @visibility external
    //<

    //> @attr PickTreeItem.valueField (string : null : IR)
    // Which field in the tree-data should be returned as this item's value?
    // If unspecified, the path will be used
    // @visibility external
    // @example pickTree
    //<


    //> @attr PickTreeItem.emptyMenuMessage   (string : "No items to display" : IRA)
    // This message will be displayed as a single, disabled option in any empty menu/submenu
    // created from this item's data tree.
    // @visibility external
    // @example pickTree
    //<
    emptyMenuMessage:"No items to display",

    //> @attr PickTreeItem.emptyDisplayValue (string : null : IRW)
    // @include formItem.emptyDisplayValue
    // <p>
    // If the emptyDisplayValue is null (the default) then this item will use the standard title
    // of the tree menu button that is shown when no values are selected.
    // @group display_values
    // @visibility external
    //<
    emptyDisplayValue: undefined,

    //> @attr pickTreeItem.readOnlyDisplay (ReadOnlyDisplayAppearance : "disabled" : IRW)
    // If +link{formItem.canEdit} is set to <code>false</code>, how should this item
    // be displayed to the user?
    // <P>
    // The default value for PickTreeItems is <code>disabled</code> - note that
    // <code>readOnly</code> and <code>static</code> appearances have no effect for
    // PickTreeItems.
    //
    // @visibility external
    //<
    readOnlyDisplay: "disabled"

    //> @attr PickTreeItem.canSelectParentItems (boolean : null : IRW)
    // @include menu.canSelectParentItems
    // @group selection
    // @visibility external
    // @example treesEditing
    //<
});

isc.PickTreeItem.addMethods({
    init : function () {
        this.Super("init", arguments);
        // optionDataSource is a synonym of dataSource
        if (this.optionDataSource == null && this.dataSource == null && this.valueTree == null) {
            this.logWarn("This form item requires a 'valueTree'.");
        }
    },

    // override getOptionDataSource to pick up this.dataSource if specified.
    // This is required to allow standard 'fetchMissingValues' logic to show a display value
    // for databound trees with a default value that has not yet loaded.
    getOptionDataSource : function () {
        var ds = this.optionDataSource || this.dataSource;
        if (ds != null) ds = isc.DataSource.get(ds);
        return ds;
    },

    buttonConstructor:"TreeMenuButton",

    // Override getDisplayValue so if a developer calls it it returns the current display
    // value -- IE the title of the button
    // *Remember - this method isn't used internally, it's simply an accessor for developers
    getDisplayValue : function () {
        return this.canvas.getTitle();
    },

    //> @method pickTreeItem.setValueTree()
    // Setter to change the +link{pickTreeItem.valueTree} at runtime
    // @param valueTree (Tree) new value tree for the item
    // @visibility external
    //<
    setValueTree : function (valueTree) {
        this.valueTree = valueTree;
        if (this.canvas) {
            this.canvas.setData(this.valueTree);
        }
    },

    // Override _createCanvas to set up a TreeMenuButton as this item's canvas
    _createCanvas : function () {

        var dynamicButtonProperties = {
                // override getTitle to check for the case where the form item value represents
                // a currently unselected node.
                getTitle : isc.PickTreeItem._getButtonTitle,

                canFocus : this._canFocus(),
                disabled : this.isDisabled(),

                // Allow the tree of options to be specified as item.dataSource or item.data.
                dataSource : this.getOptionDataSource(),
                // Pass optionCriteria through to the menuButton as 'criteria'
                criteria:this.optionCriteria,

                // Set the operationId used by the tree menu's ResultTree to fetch data
                // from the data source.
                menu: { _requestOperationId: this.optionOperationId },

                data : this.valueTree,

                // canSelectParentItems should be inherited from this item's setting
                canSelectParentItems : this.canSelectParentItems,

                itemSelected : isc.PickTreeItem._itemSelected,

                emptyMenuMessage:this.emptyMenuMessage,
                unselectedTitle: this._getEmptyDisplayValue(),

                // If databound, should we load the entire set of data up-front?
                loadDataOnDemand : this.loadDataOnDemand,
                treeDataLoaded : isc.PickTreeItem._treeDataLoaded,

                // setting the displayField on the button ensures it gets picked up by
                // SelectionTreeMenu.getItemTitle()
                displayField:this.displayField
            };


        // If the item has a specified width, and the class doesn't, pick up the width
        // from the item.
        if (dynamicButtonProperties.width == null && this.width != null) {
            var policyWidth = this.getInnerWidth();

            var iconWidth = 0;
            if (this.icons && this.icons.length > 0) {
                for (var i=0; i<this.icons.length; i++) {
                    iconWidth += this.getIconWidth(this.icons[i]) + this.iconHSpace;
                }
            }

            // don't allow zero or negative width
            dynamicButtonProperties.width = Math.max(1, policyWidth - iconWidth);
        }

        // Use 'addAutoChild' - this will handle applying the various levels of defaults
        this.canvas = this.addAutoChild("button", dynamicButtonProperties,
                                        this.buttonConstructor, this.container);
        // set autoDestroy to true so the button gets destroyed when this item gets destroyed
        this.autoDestroy = true;
        this.Super("_createCanvas", arguments);


        if (this._value != null) this.setValue(this._value);
    },

    // Don't have a default width for this item type as a class - pick it up from
    // the PickTreeItem.
    // If a developer specifies a width, we'll respect it.
    width:null,


    // Values management.
    // We're selecting nodes from a tree data structure.
    // The method 'getSelectedNode()' is available to return a pointer to the actual selected
    // node object, but this is not the 'value' of this form item.
    // If this.valueField is specified, the value of this item will be node[this.valueField].
    // Otherwise:
    // - if this is a databound tree, the value will be the primary key of this node (assumed
    //   to have a single primary key)
    // - if this is a client-only tree, and the modelType of the tree is "parent", the
    //   idProperty for the selected node will be returned as item's value.
    // - If the modelType is not parent, the path of the node will be returned.



    // User selection of value will triger this._itemSelected();
    _itemSelected : function (item) {
        //!DONTCOMBINE
        var value = this._mapNodeToValue(item);
        // updateValue() will store the new value
        // If the change handler didn't return false, also keep a pointer to the node.
        this._userPickedNode= item;
        var rv = this._updateValue(value);
        if (rv == false) delete this._userPickedNode;
        // if a change/changed handler actually called setValue() on the item, return
        // false so we don't clobber the new item value with the user-picked value
        // in the button title, etc.
        if (this.getValue() != value) rv = false;
        // returning false will avoid the item getting marked as selected in the tree.
        return rv;
    },

    // Override 'saveValue()' so that 'getSelectedNode()' returns the node associated with the value

    saveValue : function (value, isDefault) {
        // userPickedNode set up as part of the change flow - saves us having to look up the
        // node in the tree.
        if (this._userPickedNode != null) {
            this._selectedNode = this._userPickedNode;
            delete this._userPickedNode;
        } else {

            this._selectedNode = this._getNode(value);
            if (this.canvas) {
                // If we couldn't find a node, this will update the button title to reflect either
                // the empty message, or the valuemapped version of this item's value.
                this.canvas.setSelectedItem(this._selectedNode);
            }

        }
        return this.Super("saveValue", arguments);
    },

    // Override CanvasItem.showValue() to set the title of the TreeMenuButton after the value
    // of this form item is set.  The displayValue passed into this method is from the value
    // map defined by FormItem.  It might not be an actual, valid display value, if the record
    // is not cached, so this method tries to find the data record in the ResultTree of the
    // button's Menu.  If such a tree node/record exists in the ResultTree then it is selected.
    // Otherwise this method checks the displayValue to see if it is valid before setting it
    // as the button title.
    showValue : function (displayValue, dataValue) {
        var node = this._getNode(dataValue);
        if (node != null) {
            this.canvas.setSelectedItem(node);
        } else {
            // _mapKey() is an internal method of FormItem.  The second argument set to true forces
            // _mapKey() to return null if the value is not in the valueMap.
            var ds = this.optionDataSource || this.dataSource;
            var isValueInCache = !ds || (this._mapKey(dataValue, true) !== null),
                title = isValueInCache ? displayValue : "";
            this.canvas.setTitle(title);
        }
    },

    // Given a selected node, what is the 'value' that we'll pass back to a 'getValue()' call
    // (same value should work in a 'setValue()' call to select the node in question).
    _mapNodeToValue : function (node) {

        if (this._usePathAsId()) return this.valueTree.getPath(node);

        return node[this._getValueFieldName()];
    },

    // _usePathAsId - if we're using a tree with the relationships specified as paths,
    // we use the paths as IDs by default.
    _usePathAsId : function () {
        return (!this.valueField && this.valueTree && (this.valueTree.modelType != "parent"));
    },

    // If we're using a field-name property on the node as a unique node ID, what is it?
    _getValueFieldName : function () {
        var fieldName = this.valueField;
        if (!fieldName) {
            fieldName = this.valueTree ? this.valueTree.idField
                                       : this._getPrimaryKeyFieldName();
        }

        return fieldName;
    },

    // For databound items, this method determines the primary key fieldName (used to determine
    // the 'value' property based on the selected node).
    _getPrimaryKeyFieldName : function () {

        if (!this.getOptionDataSource()) return null;

        if (!this._pkfield) {

            var ds = isc.DataSource.getDataSource(this.getOptionDataSource()),
                pkArray = ds.getPrimaryKeyFieldNames(),
                pk = isc.isAn.Array(pkArray) ? pkArray[0] : pkArray;

            if (isc.isAn.Array(pkArray) && pkArray.length > 1) {
                this.logWarn("Multiple primary key fields not supported by PickTreeItem - using '" +
                             pk + "' as single primary key field");
            }

            this._pkfield = pk;
        }

        return this._pkfield;
    },


    // No need for override to 'getValue()' - update value will have already saved out the
    // selected value

    // Special 'getSelectedNode()' method to give us a pointer to the selected object
    //>@method  PickTreeItem.getSelectedNode()
    //  Returns a pointer to the currently selected node within this item's tree
    //<
    getSelectedNode : function () {
        // this property is set up when selection changes (or on setValue() calls)
        return this._selectedNode;
    },

    // have getSelectedRecord simply fall through to getSelectedNode

    getSelectedRecord : function () {
        return this.getSelectedNode();
    },

    // on updateValueMap(), refresh our canvas to show the new display value.
    updateValueMap:function (needsRefresh) {
        this.Super("updateValueMap", arguments);
        if (needsRefresh) this.canvas.markForRedraw();
    },

    // Given a specified 'value' for this item, find the approprate node in our data tree.
    _getNode : function (value) {
        if (!value) return null;

        // If we have a dataSource we're going to be looking up the item in the
        // ResultTree derived from the dataSource - available as this.canvas.getTree().
        var tree = (this.getOptionDataSource() ? this.canvas.getTree() : this.valueTree);
        if (this._usePathAsId()) return this.valueTree.find(value);

        return tree.find(this._getValueFieldName(), value);

    },

    _shouldAllowExpressions : function () {
        return false;
    },


    //> @attr pickTreeItem.optionDataSource (DataSource : null : IRW)
    // @include formItem.optionDataSource
    // @visibility external
    //<

    //> @method pickTreeItem.fetchData()
    // Only applies to databound items (see +link{pickTreeItem.optionDataSource}).<br>
    // Performs a fetch type operation on this item's DataSource to retrieve/refresh the tree
    // of data displayed as rows in this items menu.
    // @visibility external
    //<

    fetchData : function () {
        var ods = this.getOptionDataSource();
        if (ods == null) {
            this.logWarn("fetchData() called on pickTree item with no option data source. Ignoring.");
            return;
        }


        var resultTree = this.canvas.getTree();
        if (!resultTree || !resultTree.invalidateCache) {
            return;
        }
        // The menu reacts to invalidateCache on the resultTree by destroying submenus and
        // regenerating top level items.
        resultTree.invalidateCache();
    },

    _getEmptyDisplayValue : function () {
        if (this.emptyDisplayValue != null) {
            return this.emptyDisplayValue;
        } else {
            // If the emptyDisplayValue property is null then use the default value of
            // the unselectedTitle property of the tree menu button.
            var cl = isc[this.buttonConstructor],
                unselectedTitle = cl && cl.getInstanceProperty("unselectedTitle");
            return unselectedTitle || "";
        }
    },

    //> @method pickTreeItem.setEmptyDisplayValue()
    // Setter for +link{pickTreeItem.emptyDisplayValue}.
    // @param emptyDisplayValue (string)
    // @group display_values
    // @visibility external
    //<
    setEmptyDisplayValue : function (emptyDisplayValue) {
        this.emptyDisplayValue = emptyDisplayValue;
        if (this.button != null) {
            this.button.unselectedTitle = this._getEmptyDisplayValue();
        }
    }
});



//>    @class    IPickTreeItem
// Subclass of +link{PickTreeItem} which shows an +link{IMenuButton} rather than a
// simple +link{MenuButton} as it's main button.
//
// @treeLocation Client Reference/Forms/Form Items
// @visibility external
//<
isc.defineClass("IPickTreeItem", "PickTreeItem");
isc.IPickTreeItem.addProperties({
        // This is actually ITreeMenuButton. TreeMenuButton and ITreeMenuButton are
        // currently unexposed.
        buttonConstructor:"ITreeMenuButton"
});

}








//>    @class    PopUptextAreaItem
//    A FormItem that displays an uneditable (static) value, with an icon to show a floating
//  text area, which can be used to edit the value.
// @visibility popUpTextAreaItem
//<

isc.ClassFactory.defineClass("PopUpTextAreaItem", "StaticTextItem");

isc.PopUpTextAreaItem.addProperties({

    // Override canFocus - pop up text areas can accept focus
    canFocus:true,

    //>    @attr    popUpTextAreaItem.wrap        (boolean : false : IRW)
    // Don't wrap the specified text - this allows it to be truncated more easily
    //        @group    appearance
    //<
    wrap:false,

    //>    @attr    popUpTextAreaItem.width        (number : 150 : IRW)
    //            Default width for fields.
    //        @group    appearance
    //<
    width:150,

    //>    @attr    popUptextAreaItem.clipValue (boolean : true : IRW)
    // Override clipValue to force any text displayed to be truncated.
    //        @group    appearance
    //<
    clipValue:true,

    //>    @attr    popUpTextAreaItem.popUpOnEnter (boolean : false : IRW)
    // Should the text area pop up when the user tabs into this field.
    //        @group    appearance
    //<

    popUpOnEnter:false,

    // whether to show the pop-up on a click anywhere in the item (as opposed to just on the
    // icon)
    popUpOnAnyClick:true,

    //>    @attr    popUpTextAreaItem.textAreaWidth (number : 100 : IRW)
    // How wide should the pop up textArea be drawn?
    //        @group    appearance
    //<
    textAreaWidth : 100,

    //>    @attr    popUpTextAreaItem.textAreaHeight (number : 100 : IRW)
    // How tall should the pop up textArea be drawn?
    //        @group    appearance
    //<
    textAreaHeight : 100,

    //>    @attr    popUpTextAreaItem.iconOnly (boolean : false : IRW)
    // If true, display the icon to launch the pop up with no text.
    // @group appearance
    // @visibility popUpTextAreaItem
    //<
    iconOnly : false,

    //>    @attr    popUpTextAreaItem.popUpIconSrc (string : [SKIN]/DynamicForm/PopUpTextAreaEditor_icon.gif : IRW)
    // If specified, use this src for the icon that launches to the pop up text area.
    // @group appearance
    // @visibility popUpTextAreaItem
    //<
    popUpIconSrc : "[SKIN]/DynamicForm/PopUpTextAreaEditor_icon.gif",

    //>    @attr    popUpTextAreaItem.popUpIconWidth (number : 20 : IRW)
    // Width for the popUp launcher icon.
    // @group appearance
    // @visibility popUpTextAreaItem
    //<
    popUpIconWidth:20,

    //>    @attr    popUpTextAreaItem.popUpIconHeight (number : 20 : IRW)
    // Height for the popUp launcher icon.
    // @group appearance
    // @visibility popUpTextAreaItem
    //<
    popUpIconHeight:20,

    // Setting iconVAlign to "center" ensures that if the icon height exceeds the content's height
    // (EG a single line of text) the text will be centered wrt the the icon.

    iconVAlign:isc.Canvas.CENTER


});

//!>Deferred
isc.PopUpTextAreaItem.addMethods({

    _setUpIcons : function () {
        if (this.icons == null) this.icons = [];

        var icon = {
            name:"popUpIcon",

            src:this.popUpIconSrc,

            showOver:false,

            width:this.popUpIconWidth,
            height:this.popUpIconHeight,


            click:this._popUpIconClick

        };

        // Add this to the icons array.
        this.icons.addAt(icon, 0);

        this.Super("_setUpIcons", arguments);
    },

    // click handler for the pop up icon - (applied directly to the icon so not fired in the
    // scope of the item)
    _popUpIconClick : function (form,item,icon) {
        if (item.popUpOnAnyClick || item.isDisabled()) return;
        item.showPopUp(true);
    },

    // Support showing the pop up text area from a click on the item (either on the icon or the
    // static text)

    handleCellClick : function () {
        if (this.Super("handleCellClick") == false) return false;
        if (this.popUpOnAnyClick && !this.isDisabled()) this.showPopUp(true);
    },

    // showPopUp - method to actually show the pop up.
    showPopUp : function (shouldFocus) {
        var value = this.getValue();

        if (!this._popUpForm) this.setupPopUpForm();

        this.placePopUp();

        var item = this._popUpForm.getItem("textArea");
        item.setValue(value);

        this._popUpForm.bringToFront();
        this._popUpForm.show();
        if (shouldFocus) this._popUpForm.focusInItem("textArea");

        // Show a clickMask to hide the pop up form on click-outside

        this._popUpForm.showClickMask({target:this, methodName:"hidePopUp"}, true,
                                        [this._popUpForm])

    },

    // If the item is hidden, ensure we also hide the pop up form.
    visibilityChanged : function () {
        if (!this.isVisible()) this._hiddenObservation();
    },

    _hiddenObservation : function () {
        var pUF = this._popUpForm;
        if (!pUF || !(pUF.isVisible() && pUF.isDrawn())) return;

        pUF.hide();
    },

    // Whenever the item is moved by its container widget, we need to update the popUpForm's
    // position (if it is drawn).
    moved : function () {
        this._movedObservation();
    },

    _movedObservation : function () {
        var pUF = this._popUpForm;
        if (!pUF || !(pUF.isVisible() && pUF.isDrawn()) ) return;

        // If we've been moved out of the viewport, hide the TA temporarily (will reshow if
        // scrolled back into view, unless something else gets focus)
        var top = this.getTop(), left = this.getLeft(),
            width = this.getInnerWidth(), height = this.getInnerHeight(),
            container = this.containerWidget,
            scrollTop = container.getScrollTop(), scrollLeft = container.getScrollLeft(),
            viewportWidth = container.getViewportWidth(),
            viewportHeight = container.getViewportHeight()
        ;

        if (top < scrollTop || (top+height) > (scrollTop+viewportHeight) ||
            left < scrollLeft || (left+width) > (scrollLeft + viewportWidth) )
        {
            pUF.hide();
        } else {
            this.placePopUp();
        }

    },

    // if the ZIndex is modified, we need to ensure that if the pop up form is visible it
    // continues to float above the form item.
    zIndexChanged : function () {
        var pUF = this._popUpForm;
        if (!pUF || !(pUF.isVisible() && pUF.isDrawn()) ) return;
        pUF.bringToFront();
    },


    placePopUp : function () {
        var top = this.getTextAreaTop(),
            left = this.getTextAreaLeft(),
            width = this.getTextAreaWidth(),
            height = this.getTextAreaHeight();


        this._popUpForm.moveTo(left,top);
        this._popUpForm.resizeTo(width,height);

        var item = this._popUpForm.getItem("textArea");
        item.setWidth(width);
        item.setHeight(height);
    },

    setupPopUpForm : function () {
        if (this._popUpForm != null) return;
        var PUF = isc.DynamicForm.create({
            autoDraw:false,
            ID:this.getID() + "_popUpForm",
            _generated:true,


            separateContentInsertion: false,


            cellPadding:0,

            // Hang a pointer back to this item on both the pop up form and the text area.
            targetItem:this,


            values : {textArea:this.getValue()},
            //numCols:1,
            items:[
                {name:"textArea",
                    showTitle:false,
                    type:"textArea",
                    //width:"*",
                    //height:"100%",
                    selectOnFocus:true,
                    // if the user tabs out of the item, hide it.
                    // Note: we set up the rowNum / colNum vars below

                    targetItem:this,

                    focus : function (suppressHandlers) {
                        this.targetItem.textAreaFocus();
                    },

                    // "Dirty" our targetItem on keypress so updateValue gets called at the
                    // right moments
                    keyDown : function (item, form, keyName, characterValue) {
                        this.targetItem._markValueAsDirty();
                        return this.targetItem.textAreaKeyDown(item, keyName, characterValue);
                    },

                    keyPress : function (item, form, keyName, characterValue) {
                        return this.targetItem.textAreaKeyPress(item, keyName, characterValue);
                    },

                    blur: function () {
                        this.targetItem.textAreaBlur();
                    }
                }
            ],

            //showEdges:true,
            //canDragResize:true,

            hide : function (a,b,c,d) {
                var returnVal = this.invokeSuper(isc.DynamicForm, "hide", a,b,c,d);
                this.hideClickMask();
                return returnVal;
            }
        });

        this._popUpForm = PUF;

        var container = this.containerWidget;

        // Destroy if the item is destroyed
        PUF.observe(container, "destroy", "observer.hide();observer.destroy()");
    },

    // hidePopUP - method to hide the pop up (and ensure our value is up to date)
    hidePopUp : function () {
        if (this._popUpForm) {
            // updateValue will look at the value of the pop up form textArea
            this.updateValue();
            this._popUpForm.hide();
        }
    },

    // Override 'destroy' to ensure we also destroy the pop-up textArea's form
    destroy : function () {
        if (this._popUpForm) {
            this._popUpForm.destroy();
            delete this._popUpForm;
        }
        return this.Super("destroy", arguments);
    },


    //>    @method     popUptextAreaItem.getTextAreaTop()
    // Method to determine where the pop up text area should be shown.  Default implementation
    // will display the pop up over this item.
    //    @return  (number)   Page level top coordinate for the text area in px.
    //<
    getTextAreaTop : function () {
        // by default look at the position of this form item
        var top = this.getPageTop();

        if (isc.Browser.isIE) top -= 1;
        return top;
    },

    //>    @method     popUptextAreaItem.getTextAreaLeft()
    // Method to determine where the pop up text area should be shown.  Default implementation
    // will display the pop up over this item.
    //    @return  (number)   Page level left coordinate for the text area in px.
    //<
    getTextAreaLeft : function () {
        return this.getPageLeft();
    },

    //>    @method     popUptextAreaItem.getTextAreaWidth()
    // How wide should the text area be drawn.  Default implementation looks at
    // <code>this.textAreaWidth</code>.
    //    @return  (number)   Width to apply to this textArea
    //<
    getTextAreaWidth : function () {
        return Math.max(this.textAreaWidth, this.getInnerWidth());
    },

    //>    @method     popUptextAreaItem.getTextAreaHeight()
    // How tall should the text area be drawn.  Default implementation looks at
    // <code>this.textAreaHeight</code>.
    //    @return  (number)   Height to apply to this textArea
    //<
    getTextAreaHeight : function () {
        return this.textAreaHeight;
    },

    // MapValueToDisplay - override to show blank if necessary
    mapValueToDisplay : function () {
        if (this.iconOnly) return "";
        return this.Super("mapValueToDisplay", arguments);
    },


    // By Default, on blur of the T.A., hide the T.A.
    textAreaBlur : function () {
        this.hidePopUp();
    },

    textAreaFocus : function () {},


    //>    @method popUptextAreaItem.textAreaKeyPress()
    // Handler for keypress occurring on the textarea.
    // <P>
    // The TextAreaItem from the pop-up is passed is as "item", while "this" is the
    // PopUpTextAreaItem.
    // @param item (formItem) item that recieved the event (a pointer to the text area itself)
    // @param keyName (keyName) which key was pressed
    // @param characterValue (number) numeric value of the character produced by the keypress
    //  (will be null for non character keys)
    //    @return  (boolean)   Return false to cancel the key event
    //<
    // No-op by default.
    textAreaKeyPress : function (item, keyName, characterValue) { },
    textAreaKeyDown : function (item, keyName, characterValue) { },

    //Override setValue -- if we're showing the pop up, update its value
    setValue : function (newValue) {
        var oldDisplayValue = this.mapValueToDisplay(this.getValue());
        this.Super("setValue", arguments);
        var newDisplayValue = this.mapValueToDisplay(this.getValue());

        if (oldDisplayValue != newDisplayValue) {
            this.setElementValue(newDisplayValue)
            if (this._popUpForm && this._popUpForm.isVisible()) {
                this._popUpForm.setValue("textArea", newDisplayValue);
            }
        }
    },

    // Override 'updateValue' -- if we're showing the pop-up, get the value from the pop up
    // [ensuring it's up to date first]
    updateValue : function () {


        if (this._popUpForm && this._popUpForm.isVisible() &&
            !this._popUpForm._setValuesPending)
        {
            var item = this._popUpForm.getItem("textArea");
            item.updateValue();
            var newValue = this._popUpForm.getValue("textArea");
            this.setElementValue(this.mapValueToDisplay(newValue));
            this._updateValue(newValue);

        } else {
            return this.Super("updateValue", arguments);
        }

    },

    // Override setElementValue to set the value of the static text
    setElementValue : function (newVal) {
        if (this.iconOnly) return;
        return this.Super("setElementValue", arguments);
    },

    // override 'focusInItem' - by default we'll want to put focus in the textArea if it's
    // visible - otherwise on the icon that launches it.
    focusInItem : function () {
        if (this._popUpForm && this._popUpForm.isVisible()) {
            this._popUpForm.focusInItem('textArea');
        } else if (this.showIcons) {
            this.focusInIcon(this.icons[0]);
        } else {
            // showIcons:false - in the absence of a visible icon the pop-up must show
            // immediately on focus, as there would otherwise be no element to hold/show the
            // current focus
            this.showPopUp(true);
        }
    },

    // override setElementTabIndex() -- we want to reset the tabIndex of the icons and avoid
    // redrawing the form.
    _setElementTabIndex : function (tabIndex) {

        this._elementTabIndex = tabIndex;

        if (!this.isVisible() || !this.containerWidget.isDrawn()) return;

        this._updateIconTabIndices();
    },

    //> @method popUpTextAreaItem.setElementReadOnly()
    // Change the read-only state of the form element immediately.
    //<
    setElementReadOnly : function (readOnly) {
        // All disabling is related to the icons and clickability. This override prevents
        // redrawing the item.
    }

});
//!<Deferred









isc.defineClass("ExpressionItem", "PopUpTextAreaItem");

isc.ExpressionItem.addMethods({
    mapValueToDisplay : function (value) {
        if (isc.isA.StringMethod(value)) return value.getDisplayValue();
        else if (isc.isA.Function(value)) {
            if (value.iscAction) {
                return "[" + value.iscAction.title + "]";
            }
            return isc.Func.getBody(value);
        }
        else return this.Super("mapValueToDisplay", arguments);
    },

    getValue : function () {
        var value = this.Super("getValue");
        if (isc.isA.Function(value)) return isc.Func.getBody(value);
        else return value;
    },
    textAreaWidth:400,

    // ---------------------------------------------------------------------------------------

    showActionIcon:true,
    actionIconSrc:"[SKIN]/actions/add.png",
    actionIconWidth:20,
    actionIconHeight:20,

    // Where should the actionIcon show up in the icons array?

    actionIconPosition:1,
    _setUpIcons : function () {
        this.Super("_setUpIcons", arguments);
        if (this.showActionIcon) {
            if (this.icons == null) this.icons = [];
            var position = this.actionIconPosition;
            this.icons.addAt({
                name:"action",
                src:this.actionIconSrc,
                showOver:false,
                width:this.actionIconWidth,
                height:this.actionIconHeight,
                // return false from click handler so we don't fire standard cellclick
                // which would show the pop up (unless we also set popUpOnAnyClick to false)
                click:this.getID() + ".showActionMenu();return false;"
            }, position);

            // have to explicitly set the required icon properties - usually handled by
            // setUpIcons, but that's already run at this point.
            this._setUpIcon(this.icons[position]);
        }
    },

    // Override updateAppearance to refresh our element value. Required since
    // the element value isn't edited directly.
    updateAppearance : function (newValue) {
        this.setElementValue(this.mapValueToDisplay(newValue));
    },

    showActionMenu : function () {
        var item = this,
            menu = isc.ActionMenu.create({
            sourceComponent : this.form.currentComponent,
            sourceMethod : this.name,
            components: this.form.allComponents,
            bindingComplete : function (binding) {
                // binding may be null - if the user selected top level "[None]" option
                item._updateValue(binding);
            }
        });
        // Need to draw the menu first or placeNear() might not have the correct dimensions
        menu.show();
        var iconRect = this.getIconPageRect(this.icons[1]);
        menu.placeNear(iconRect[0] + this.actionIconWidth,
                       iconRect[1] + this.actionIconHeight - this.containerWidget.getScrollTop());
    }


});







// ActionMenuItem - specifically for editing 'actions'
// Appearance is similar to the ExpressionItem, and we steal some methods from that class, but
// don't base this on PopUpTA.

isc.defineClass("ActionMenuItem", "StaticTextItem");

isc.ActionMenuItem.addProperties({

    canFocus:true,

    wrap:false,
    width:150,
    clipValue:true
})

isc.ActionMenuItem.addMethods({

    // ---------------------------------------------------------------------------------------

    showActionIcon:true,
    actionIconSrc:"[SKIN]/actions/add.png",
    actionIconWidth:20,
    actionIconHeight:20,

    // Pick up mapValueToDisplay from expressionItem (handles StringMethods)
    mapValueToDisplay : isc.ExpressionItem.getPrototype().mapValueToDisplay,
    // Pick up updateAppearance - required to display the result of selecting a mapping from
    // a menu in our text-box
    updateAppearance : isc.ExpressionItem.getPrototype().updateAppearance,

    // Steal the icon / menu mgmt code from expressionItem
    actionIconPosition:0,
    _setUpIcons : isc.ExpressionItem.getPrototype()._setUpIcons,
    showActionMenu : isc.ExpressionItem.getPrototype().showActionMenu

});







//>    @class    SearchForm
//
// A SearchForm is a DynamicForm specialized for a user to enter search criteria.
// <P>
// All DynamicForm properties and methods work on SearchForm.  SearchForm extends and
// specializes DynamicForm for searching; for example, SearchForm sets
// <code>hiliteRequiredFields</code> false by default because fields are typically not
// required in a search.
//
// @see class:DynamicForm
//
// @treeLocation Client Reference/Forms
// @visibility external
//<


// create the form as a descendant of the DynamicForm
isc.ClassFactory.defineClass("SearchForm", "DynamicForm");

// add constants
isc.SearchForm.addProperties({
    //> @attr SearchForm.canEditFieldAttribute (String : "canFilter" : IRA)
    // @include dataBoundComponent.canEditFieldAttribute
    // This property is overridden in SearchForm to allow editing of dataSource fields marked as
    // <code>canFilter:true</code> by default.
    // @visibility external
    //<
    canEditFieldAttribute:"canFilter",


    isSearchForm:true,

    // hiliteRequiredFields - false
    // Don't hilight required fields in bold by default.
    hiliteRequiredFields:false,

    // if there are operation-specific schema on a DataSource we're binding to, use the fetch
    // schema
    operationType:"fetch",

    // This flag allows editing of canSave:false fields
    _canEditUnsaveableFields:true,

    // set this to false to cause select-items not to show a blank entry
    // (used this way by filter-clauses in FilterBuilder)
    allowEmptyValues: true,

    // set storeAtomicValues to true. Search forms are for editing criteria.
    // If we have a field whose type is an opaque simpleType we don't expect to
    // be passed values of that raw data type, nor to be creating criteria with
    // values of that raw type -- instead we work with the atomic type in our criteria.
    storeAtomicValues:true
});

isc.SearchForm.addMethods({
    // When creating DateItems, show the text field by default (unless the definition block
    // explicitly says otherwise)

    _$DateItem:"DateItem",
    createItem : function (item, type, a,b,c) {
        var ds = this.getDataSource(),
            isDSField = ds ? ds.getField(item[this.fieldIdProperty]) != null : false;

        // If we're looking at a dataSource field, ensure the user can always enter a null value
        // (Allows searching for all entries in boolean / date / valueMapped fields)
        if (isDSField) {
            // convert from a simple object into a FormItem
            var className = isc.FormItemFactory.getItemClassName(item, type, this),
                classObject = isc.FormItemFactory.getItemClass(className);

            if (classObject == isc.DateItem && item && (item.useTextField == null))
                item.useTextField = true;

            // Default to this.allowEmptyValues (plural), unless the singular version is
            // explicitly set on the item
            if (item.allowEmptyValue == null) {
                item.allowEmptyValue = this.allowEmptyValues;
            }
        }

        return this.invokeSuper(isc.SearchForm, "createItem", item, type, a,b,c);
    },

    submitValues : function (values, form) {
        if (this.search != null) {
            return this.search(this.getValuesAsCriteria(), this);
        }
    },

    validate : function (a, b, c) {
        if (this.validateTypeOnly) {
            return this.invokeSuper(isc.SearchForm, "validate", a, b, true);
        } else {
            return this.invokeSuper(isc.SearchForm, "validate", a, b, c);
        }
    },

    // override getEditorType() so we can default date fields to using the DateRangeItem
    defaultDateEditorType:"DateRangeItem",
    getEditorType : function (field) {
        // support field.editorType being specified directly
        if (field.editorType != null) return field.editorType;

        var type = field.type;
        if (type && isc.SimpleType.inheritsFrom(type, "date")) {
            if (field.editorType == null) {
                return this.defaultDateEditorType;
            }
        }

        var isFileType = (type == this._$binary || type == this._$file ||
                            type == this._$imageFile);
        if (isFileType && field.editorType == null) {
            return "StaticTextItem";
        }

        return this.Super("getEditorType", arguments);
    }


});

isc.SearchForm.addProperties({
    //> @attr searchForm.showFilterFieldsOnly (Boolean : true : IRWA)
    // @include dataBoundComponent.showFilterFieldsOnly
    // @visibility external
    //<
    showFilterFieldsOnly:true,

    //> @attr searchForm.validateTypeOnly (boolean : true : IRWA)
    // If true (the default), calls to the <code>SearchForm</code>'s <code>validate()</code>
    // method will validate only field types (ie, is the value a valid string, a valid number,
    // or whatever); any other validations are skipped.
    //
    // @visibility internal
    //<

    validateTypeOnly:true
});

isc.SearchForm.registerStringMethods ({
    //>    @method SearchForm.search()
    // Triggered when a SubmitItem is included in the form is submitted and gets pressed.
    //
    // @param    criteria  (Criteria)      the search criteria from the form
    // @param    form      (SearchForm)    the form being submitted
    // @group submitting
    // @see method:dynamicForm.submit()
    // @see method:dynamicForm.submitValues()
    // @visibility external
    //<
    search : "criteria,form"
});





//>    @class    ValuesManager
//
// The ValuesManager manages data from multiple member forms.
// <P>
// If a single logical form needs to be separated into multiple DynamicForm instances for
// Layout purposes (for example, spanning one logical form across multiple Tabs), a
// ValuesManager can be used to make the forms act as one logical form, supporting all
// value-related APIs otherwise called on DynamicForm directly.
// <P>
// A ValuesManager has no visual representation - it is strictly a logical entity, and the
// member forms provide the user interface.  You can initialize a ValuesManager with a set of
// member forms (by setting +link{ValuesManager.members} at init) or add and remove member
// forms dynamically.
// <P>
// Calling +link{ValuesManager.setValues()} on a ValuesManager will automatically route new
// field values to whichever member form is showing an editor for that field.  Likewise,
// calling +link{ValuesManager.validate()} will validate all member forms, and
// +link{ValuesManager.saveData()} will initiate a save operation which aggregates values from
// all member forms.
// <P>
// Like a DynamicForm, a ValuesManager can be databound by setting
// +link{valuesManager.dataSource}.  In this case all member forms must also be bound to the
// same DataSource.
// <P>
// In general, when working with a ValuesManager and its member forms, call APIs on the
// ValuesManager whenever you are dealing with values that span multiple forms, and only call
// APIs on member forms that are specific to that form or its fields.
// <P>
// Note that, just as a DynamicForm can track values that are not shown in any FormItem, a
// ValuesManager may track values for which there is no FormItem in any member form.  However,
// when using a ValuesManager these extra values are only allowed on the ValuesManager itself.
// Member forms will not track values for which they do not have FormItems.
//
// @treeLocation Client Reference/Forms
// @visibility external
// @example formSplitting
//<
isc.ClassFactory.defineClass("ValuesManager");

isc.ValuesManager.addClassMethods({
    //> @classMethod valuesManager.getById()
    // Retrieve a ValuesManager by it's global +link{valuesManager.ID,ID}.
    // @param ID (String) global ID of the ValuesManager
    // @return (ValuesManager) the ValuesManager, or null if not found
    // @visibility external
    //<
    getById : function (sId) {
        var vm = window[sId] || null;
        return vm ? (isc.isA.ValuesManager(vm) ? vm : null) : null;
    }
});

isc.ValuesManager.addProperties({

    //> @attr valuesManager.dataSource  (DataSource | String : null : [IRWA])
    // Specifies a dataSource for this valuesManager.  This dataSource will then be used for
    // validation and client-server flow methods.  Can be specified as a dataSource object or
    // an identifier for the dataSource.<br>
    // Note that member forms should have the same dataSource applied to them to allow their
    // items to inherit properties from the DataSource fields.
    // @visibility external
    // @see valuesManager.setDataSource()
    // @see valuesManager.getDataSource()
    //<
    //dataSource: null,

    //> @attr valuesManager.addOperation
    // +link{DSRequest.operationId,operationId} to use when performing add operations.
    // @include DataBoundComponent.addOperation
    // @visibility external
    //<

    //> @attr valuesManager.updateOperation
    // +link{DSRequest.operationId,operationId} to use when performing update operations.
    // @include DataBoundComponent.updateOperation
    // @visibility external
    //<

    //> @attr valuesManager.members (Array of DynamicForm : null : [IRW])
    // The set of member components for this valuesManager.  These can be specified at init time
    // via the <code>members</code> property, or updated at runtime via <code>addMember()</code>
    // and <code>removeMember()</code>.<br>
    // Note: Alternatively a DataBoundComponent can be initialized as a member of a valuesManager
    // by setting the <code>valuesManager</code> property of the component to a pre-defined
    // valuesManager instance, or by calling <code>setValuesManager</code> on the component.
    // @visibility external
    // @see valuesManager.addMember()
    // @see valuesManager.removeMember()
    // @see dataBoundComponent.setValuesManager()
    //<
    //members : null,

    //>    @attr valuesManager.unknownErrorMessage    (string : null : [IRW])
    // The error message for a failed validator that does not specify its own errorMessage.
    // <P>
    // If unset this value will be derived from the default
    // +link{dataBoundComponent.unknownErrorMessage} when the valuesManager is initialized.
    //<
    unknownErrorMessage : null

    //> @attr valuesManager.disableValidation   (boolean : null : [IRWA])
    // @include DynamicForm.disableValidation
    //<

    //> @attr valuesManager.autoSynchronize   (boolean : null : [IRWA])
    // If explicitly set to false, prevents the ValuesManager from automatically propagating
    // data value changes to its members.  You can manually synchronize member data values
    // at any time with a call to +link{synchronizeMembers}.
    // @visibility external
    //<

});

//!>Deferred
isc.ValuesManager.addMethods({
    // Allow a V.M to be initialized with member form(s)
    init : function () {
        // get a global ID so we can be called in the global scope
        this.ns.ClassFactory.addGlobalID(this);

        if (this.unknownErrorMessage == null) {
            this.unknownErrorMessage = isc.Canvas.getPrototype().unknownErrorMessage;
        }

        if (this.dataSource) this.bindToDataSource(this.dataSource);

        // Initialize this.values [and ensure it's a new object, so it can't be manipulated
        // externally]
        if (this.values == null) this.values = {};
        isc.DynamicForm._duplicateValues(this, this.values, {});

        // Set up values based on members / init values.
        if (this.members != null) {

            var members = this.members;
            this.members = null;
            if (!isc.isAn.Array(members)) members = [members];
            for (var i = 0; i < members.length; i++) {
                this.addMember(members[i]);
            }
        }
        // remember the current values for resetting
        this.rememberValues();
    },

    // on destroy
    // - disconnect from member forms (Don't destroy - they may want to be re-used in a
    //   different VM)
    // - clear global ID
    destroy : function () {
        var members = this.members;
        if (members) {
            // iterate backwards so the changing length of the members array doesn't mess up
            // our loop
            for (var i = members.length-1; i >= 0; i--) {
                this.removeMember(members[i]);
            }
        }
        // clear the global ID
        window[this.getID()] = null;
        this.Super("destroy", arguments);
    },

    // This is a VM-specific override of the method specified on EditorActionMethods.  It is
    // necessary because forms only edit flat structures (even if dataPaths mean that the
    // values in those flat structures are derived from arbitrary places in a complex nested
    // structure), whereas ValuesManagers have to cope with any kind of data structure
    _saveDataReply : function (request, response, data) {
        if (!this.suppressServerDataSync && response && response.status >= 0 && data != null) {
            if (isc.isAn.Array(data)) data = data[0];
            if (request.data) request.data = isc.shallowClone(request.data);

            this.setValues(data);

            // If this was an add operation, drop the currently specified saveOperationType now
            // if the response included primary key data for the newly added record we're now
            // updating an existing record. We already have logic to catch this case in
            // getSaveOperationType().
            if (this.saveOperationType == "add") delete this.saveOperationType;
            // Also drop the saveOperatonType on member forms. This ensures
            // isNewRecord etc return the expected values on the DynamicForm.

            var memberForms = (this.members ? this.members.duplicate() : []);
            for (var i = 0; i < memberForms.length; i++) {
                var form = memberForms[i];
                if (!isc.isA.DynamicForm(form)) continue;
                form.saveOperationType = null;
            }
        }

        this._callbackState = {
            request: request,
            response: response,
            data: data
        };
        this.formSavedComplete();


    },

    // given a member with dataArity:"multiple",
    _updateMultipleMemberValue : function (index, field, value, member) {
        field = (field != null) ? this._combineDataPaths(index,field) : index;
        return this._updateValue(field, value, member);
    },

    // _updateValue and _clearValue() -- called by member components to notify us of field
    // value changes
    _updateValue : function (fieldName, value, member) {
        if (this._synchronizingMembers) return;

        var isIndex = isc.isA.Number(fieldName);

        // warn on value with no associated items in dynamicForms
        if (!isIndex && isc.isA.DynamicForm(member) && member.getItem(fieldName) == null) {
            this._itemlessValueWarning(member, fieldName);
            return;
        }

        var isDataPath;
        // if the component has a dataPath, prepend it to the fieldName/dataPath passed in so
        // we store values hierarchically (unless the field's dataPath is fully-qualified)
        var dataPath = member.getFullDataPath();
        var field;
        if (member && member.getField) {
            field = member.getField(fieldName);
        }

        if (!isIndex) {
            if (fieldName != null && !isc.isA.String(fieldName)) {

                fieldName = fieldName.dataPath || fieldName.name;
            }
            if (fieldName) {
                // handle an item having an "absolute" dataPath
                if (dataPath == null || fieldName.startsWith(isc.Canvas._$slash)) {
                    dataPath = fieldName;
                } else {
                    dataPath = this._combineDataPaths(dataPath, fieldName);
                }
            }
            // If we didn't pick up a field from the item, grab it from our DS
            if (field == null) {
                var ds = this.getDataSource();
                if (ds) field = ds.getFieldForDataPath(dataPath);
            }
        } else { // We were passed a record index, not a field name
            if (!dataPath) {
                dataPath = fieldName;
            } else if (!dataPath.endsWith(isc.Canvas._$slash)) {
                dataPath += isc.Canvas._$slash + fieldName;
            } else {
                dataPath += fieldName;
            }
        }

        isc.DynamicForm._saveFieldValue(dataPath, field, value, this.values, member, true);
        var isDataPath = dataPath.contains(isc.Canvas._$slash);
        if (isDataPath && this.autoSynchronize !== false) {
            var elements = dataPath.split(isc.Canvas._$slash);
            if (parseInt(elements[elements.length-1]) == elements[elements.length-1]) {
                // The last element of the dataPath is an index, which implies that the updating
                // member is a selectionComponent.  Therefore, we refresh every member of the
                // VM that has "member" as a selectionComponent
                this.synchronizeMembers(member);
            } else {
                var fields = this.getFieldsForDataPath(dataPath || fieldName);
                this._synchronizingMembers = true;
                for (var i = 0; i < fields.length; i++) {
                    if (fields[i].form == member) continue;
                    fields[i].saveValue(value);
                }
                delete this._synchronizingMembers;
            }
        }
    },

    //> @method ValuesManager.synchronizeMembers()
    //   Update all of this ValuesManager's members to reflect the current values held by the
    //   ValuesManager.  It is not normally necessary to manually synchronize members, but you
    //   will need to do so if you switch off +link{autoSynchronize,automatic synchronization}.
    // @visibility external
    //<
    // Undocumented selComp param allows _updateValue to use this method for its own purposes
    // (ie, refreshing just those members that have a particular selectionComponent)
    synchronizeMembers : function (selComp) {
        if (!this.members) return;
        this._synchronizingMembers = true;
        for (var i = 0; i < this.members.length; i++) {
            if (!selComp || this.members[i].selectionComponent == selComp) {
                this._setMemberValues(this.members[i]);
            }
        }
        delete this._synchronizingMembers;
    },

    _combineDataPaths : function (baseDP, fieldDP) {
        if (isc.isAn.Object(fieldDP)) {
            fieldDP = fieldDP.dataPath || fieldDP.name;
        }
        return isc.DynamicForm._combineDataPaths(baseDP, fieldDP);
    },
    // fieldName may be an array of field IDs
    _itemlessValueWarning : function (member, fieldName) {
        this.logWarn("Member Form: " + member +
                 " has explicitly specified value for field[s] '" + fieldName + "', but has" +
                 " no item associated with this fieldName. Ignoring this value. " +
                 "Values may be set for fields with no associated form item directly " +
                 "on the valuesManager via valuesManager.setValues(), but not on " +
                 "member forms. See ValuesManager documentation for more info.");
    },

    _clearValue : function (field, form) {


        var dataPath = form.getFullDataPath();
        if (dataPath) field = this._combineDataPaths(dataPath, field);
        return isc.DynamicForm._clearFieldValue(field, this.values);
    },

    // ----------------------------------------------------------------------------------------
    // Databound functionality
    // ----------------------------------------------------------------------------------------

    //> @method ValuesManager.bindToDataSource() ([A])
    //   Associate this ValuesManager with a DataSource.  Allows the developer to inherit
    //   properties from the DataSource fields to be applied to these values, such as validators,
    //   and to call data flow methods using this ValuesManager's data.
    // @param (dataSource)  Datasource object, or identifier
    // @visibility internal
    //<
    // For the public version of this method use 'setDataSource'
    bindToDataSource : function (ds) {


        if (!isc.isA.DataSource(ds)) ds = isc.DataSource.getDataSource(ds);
        if (ds != null) this.dataSource = ds;
    },

    //>@method  valuesManager.setDataSource() (A)
    // Specifies a dataSource for this valuesManager.  This dataSource will then be used for
    // validation and client-server flow methods.
    // @visibility external
    // @param dataSource (string | DataSource)  Datasource object or identifier to bind to.
    //<
    setDataSource : function (dataSource, fields) {
        // we don't use 'fields'
        this.bindToDataSource(dataSource);
    },

    //>@method  valuesManager.getDataSource() (A)
    // Returns the dataSource for this valuesManager.  Will return null if this is not a
    // data-bound valuesManager instance.
    // @visibility external
    // @return (DataSource)  Datasource object for this valuesManager.
    //<
    getDataSource : function () {
        if (isc.isA.String(this.dataSource)) {
            if (this.serviceNamespace || this.serviceName) {
                this.dataSource = this.lookupSchema();
            } else {
                var ds = isc.DS.get(this.dataSource);
                if (ds != null) return ds;

                // support "dataSource" being specified as the name of a global, and if so, assign
                // that to this.dataSource
                ds = this.getWindow()[this.dataSource];
                if (ds && isc.isA.DataSource(ds)) return (this.dataSource = ds);
            }
        }
        return this.dataSource;
    },

    lookupSchema : function () {
        // see if we have a WebService instance with this serviceName / serviceNamespace
        var service;
        if (this.serviceName) service = isc.WebService.getByName(this.serviceName, this.serviceNamespace);
        else service = isc.WebService.get(this.serviceNamespace);

        if ((this.serviceNamespace || this.serviceName) && service == null) {
            this.logWarn("Could not find WebService definition: " +
                         (this.serviceName ? "serviceName: " + this.serviceName : "") +
                         (this.serviceNamespace ? "   serviceNamespace: " + this.serviceNamespace : "")
                         + this.getStackTrace());
        }

        // If this.dataSource is not a String, we shouldn't have ended up here
        if (!isc.isA.String(this.dataSource)) {
            this.logWarn("this.dataSource was not a String in lookupSchema");
            return;
        }

        if (service) return service.getSchema(this.dataSource);
    },

    // support retrieving a pointer to a field object defined in a dataSource by fieldID / dataPath

    getDataSourceField : function (fieldID) {
        var ds = this.getDataSource();
        if (!ds || !fieldID) return null;

        fieldID = fieldID.trim("/");
        var dataSource = this.getDataSource(),
            segments = fieldID.split("/"),
            field;
        for (var i = 0; i < segments.length; i++) {
            if (isc.isAn.emptyString(segments[i])) continue;
            var fieldId = segments[i];
            field = dataSource.getField(fieldId);
            dataSource = field ? isc.DataSource.getDataSource(field.type) : dataSource;
        }
        return field;
    },


    //>@method valuesManager.getItems()
    // Retrieves all form items contained within this valuesManager's member forms
    // @return (array of FormItems) form items present in this valuesManager
    //<
    getItems : function () {
        if (!this.members) return;
        var items = [];
        for (var i = 0; i < this.members.length; i++) {
            var form = this.members[i];
            if (!form.getItems) continue;
            items.addList(form.getItems());
        }
        return items;
    },
    // getFields() synonym of getItems
    getFields : function () {
        return this.getItems();
    },

    // Getting pointers to actual items
    //>@method ValuesManager.getItem()
    // Retrieve a +link{FormItem} from this ValuesManager.
    // <P>
    // Takes a field +link{formItem.name,name} or +link{type:DataPath}, and searches through the
    // members of this valuesManager for an editor for that field. If found the appropriate
    // formItem will be returned. If the "retrieveAll" parameter is true, this method will return
    // all FormItems that are bound to the supplied name or dataPath (a dataPath can be bound
    // to more than one FormItem, as long as those FormItems are on different forms); if
    // "retrieveAll" is false or unset, and there is more than one binding for the dataPath,
    // this method just returns the first one it finds.<p>
    // Note that if a dataPath is passed in, it should be the full data path for the item,
    // including any canvas level +link{canvas.dataPath,dataPath} specified on the member
    // form containing this form item.
    // <br>Note: Unlike the <code>DynamicForm</code> class, this method will not return an
    // item by index
    // @param itemID (fieldName | dataPath) item fieldName or dataPath identifier
    // @param [retrieveAll] (boolean)       If true, return the list of all FormItems that
    //                                      are bound to this name or dataPath on a member
    //                                      form of this ValuesManager
    // @return (formItem) form item for editing/displaying the appropriate field, or null if
    //  no formItem can be found for the field.
    // @visibility external
    //<
    getItem : function (name, retrieveAll) {
        return this._findMemberByField(name, true, retrieveAll);
    },

    getField : function (id) {
        return this.getItem(id);
    },

    getFieldsForDataPath : function (id) {
        return this.getItem(id, true);
    },

    //>@method  ValuesManager.getMembers()
    //  Retrieves an array of pointers to all the members for this valuesManager.
    // @return (array)   array of member forms
    // @visibility external
    // @group members
    //<
    getMembers : function () {
        return this.members;
    },

    //>@method  ValuesManager.getMember()
    //  Returns a pointer to a specific member.
    // @param   ID  (string)    ID of the member component to retrieve
    // @return (Canvas)   member (or null if unable to find a member with the
    // specified ID).
    // @visibility external
    // @group members
    //<

    getMember : function (ID) {
        // Since the members are all DynamicForm instances, their IDs are global
        var member = window[ID];
        // sanity check
        if (this.members && this.members.contains(member)) return member;
        return null;
    },

    //>@method  ValuesManager.getMemberForField()
    // Given a fieldName or dataPath, this method will find the member responsible for
    // interacting with that field's value.
    // If no form is found, returns null.
    // @param fieldName (string) fieldName or dataPath to check
    // @return (Canvas) member responsible for displaying this field (may be null).
    // @group members
    // @visibility external
    //<
    getMemberForField : function (fieldName, retrieveAll) {
        return this._findMemberByField(fieldName, false, retrieveAll);
    },

    // helper for getItem() / getMemberForField()
    // Determines which member manages a fieldName or dataPath and returns the appropriate member
    // or item from within a member form
    // Handles cases where a dataPath is partially specified on an item and partially on
    // a member form

    _findMemberByField : function (fieldName, getItem, retrieveAll) {

        if (!this.members || fieldName == null || isc.isAn.emptyString(fieldName)) return null;

        var trimmedFieldName = fieldName.trim(isc.Canvas._$slash);

        // determine whether the fieldName passed in was a dataPath once before
        // looping through members
        var dataPathSegments = trimmedFieldName.split(isc.Canvas._$slash);

        // retrieveAll parameter - implies we should return an array of components that match
        // the specified fieldName
        // We only really expect to see multiple components pointing at the same field if we have
        // a 'multiple:true' field with a dataArity:"multiple" component such as a listGrid and
        // a / some dataArity:"single" components such as forms for editing details of it.
        // Note that this is not true of FormItem-level bindings - it is legitimate for multiple
        // form fields to bind to the same "normal" (ie, non-multiple, non-DataSource-typed)
        // DS field as long as they are on different forms.  This allows, eg, binding a complex
        // structure to a set of DFs displayed in a tabSet, with some general information -
        // customer name or order number, for example - displayed on every form
        var results = retrieveAll ? [] : null;
        for (var i = 0; i < this.members.length; i++) {
            var member = this.members[i],
                memberDataPath = member.getFullDataPath();

            // if a member has dataPath set to "/" work with the fieldName directly
            if (memberDataPath == isc.Canvas._$slash || isc.isAn.emptyString(memberDataPath)) {
                memberDataPath = null;
            } else if (memberDataPath != null) {
                memberDataPath = memberDataPath.trim(isc.Canvas._$slash);
            }

            if (dataPathSegments && dataPathSegments.length > 0 && memberDataPath != null) {

                var soFar = null;
                for (var ii = 0; ii < dataPathSegments.length; ii++) {
                    // we've split the dataPath into segments
                    // generate a string showing the partial dataPath up to this depth
                    soFar = !soFar ? dataPathSegments[ii]
                                   : (soFar + isc.Canvas._$slash + dataPathSegments[ii]);
                    if (memberDataPath.endsWith(isc.Canvas._$slash)) {
                        memberDataPath = memberDataPath.substring(0,memberDataPath.length-1);
                    }

                    // if we have a match, we still may need to check fields within the
                    // member to ensure the fields match
                    // Example - a member may have dataPath "contacts"
                    // and an item in that member may have dataPath "address/email"
                    if (memberDataPath == soFar) {
                        // If the member has an explicit dataPath matching the
                        // dataPath passed in, just return it

                        if (!getItem && (ii == dataPathSegments.length-1)) {
                            if (!retrieveAll) return member;

                            results.add(member);
                            // break out of the inner loop and check the next member!
                            break;
                        }
                        if (member.getField) {
                            var remainingPath = dataPathSegments.slice(ii+1).join(isc.Canvas._$slash);

                            // this'll catch the case where the item has a partial datapath
                            // or the last level of nesting is handled by fieldName
                            var item = member.getField(remainingPath);
                            if (item) {
                                if (getItem) {
                                    if (!isc.isA.FormItem(item)) item = null;
                                    if (retrieveAll) {
                                        if (item) results.add(item);
                                    } else {
                                        return item
                                    }
                                } else {
                                    if (retrieveAll) results.add(member);
                                    else return member;
                                }
                            }
                        }
                    }
                }
            } else {
                // handle being passed (EG) "/someField" - this can happen if
                // a dataPath is specified on a component as explicit "/", or if a field has
                // an absolute path
                // If the form had no expicit dataPath we can just use getItem() whether the
                // value passed in is a datapath or a fieldName
                if (this.members[i].getItem) {
                    var field = this.members[i].getField(fieldName);
                    if (!field) {
                        if (fieldName.startsWith(isc.Canvas._$slash)) {
                            field = this.members[i].getField(fieldName.substring(1));
                        }
                    }
                    if (field) {
                         if (getItem) {
                            if (!isc.isA.FormItem(field)) field = null;
                            if (retrieveAll) {
                                if (field) results.add(field);
                            } else {
                                return field;
                            }
                         } else {
                             if (retrieveAll) results.add(member);
                             else return member;
                         }
                    }
                }
            }
        }
        return retrieveAll ? results : null;
    },


    // How to handle fileItems?
    // Assume only one fileItem per member form - on saveData(), we'll grab the fileItemForm
    // from our member form and use it to submit all our values.

    getFileItemForm : function () {
        if (!this.members) return;
        var hasFileItemForm = false, fileItemForm;
        for (var i = 0; i < this.members.length; i++) {
            if (this.members[i].getFileItemForm == null) continue;
            var form = this.members[i].getFileItemForm();
            if (form) {
                if (hasFileItemForm) {
                    this.logWarn("ValuesManager defined with more than one member form " +
                            " containing a FileItem. This is not supported - binary data may " +
                            "only be uploaded from one FileItem when saving ValuesManager data");
                } else {
                    fileItemForm = form;
                    hasFileItemForm = true;
                }
            }
        }
        return fileItemForm;
    },

    // Validation:

    //> @method valuesManager.validate()
    // Validate the current set of values for this values manager against validators defined
    // in the member forms. For databound valuesManagers, also perform validation against any
    // validators defined on datasource fields.
    // <P>
    // Note that if validation errors occur for a value that is not shown in any member forms,
    // those errors will cause a warning and +link{handleHiddenValidationErrors()} will be
    // called.  This can occur if:<br>
    // - A datasource field has no corresponding item in any member form<br>
    // - The item in question is hidden<br>
    // - The member form containing the item is hidden.
    //
    // @return  (Boolean)   true if all validation passed
    // @visibility external
    // @example formSplitting
    //<

    validate : function (validateHiddenFields, ignoreDSFields, typeValidationsOnly,
            checkValuesOnly, skipServerValidation, suppressShowErrors)
    {
        // Just bail if client-side validation is disabled.
        // Note that we'll still show the errors returned from a failed server save due to
        // 'setErrors' behavior
        if (this.disableValidation) return true;

        // skip validation if we're databound and our datasource has validation disabled
        if (this.dataSource && this.dataSource.useLocalValidators != null &&
            this.useLocalValidators == false) return true;

        var hadErrors = this.hasErrors();

        // clear hidden errors before starting any validation run
        this.clearHiddenErrors();

        // For databound valuesManagers, each member form will be responsible for validating
        // the fields it shows, and the valueManager will validate the rest.  Note that this
        // works OK even for member forms that are attached to multiple: true fields, because
        // you cannot edit a value for one record and then switch records with the
        // selectionComponent without losing your edits, unless you explicitly save (in other
        // words, the current values in a member form are the only ones that can have errors,
        // unless the data that came into the ValuesManager in the first place was in error)
        var returnVal = true,
            // fields are returned from ds in {fieldName:fieldObject} format
            dsFields = this.dataSource ? isc.addProperties({}, this.getDataSource().getFields())
                                       : null,
            validators = {},
            dataPaths = {},
            isDataPath = false;

        // Keep track of all possible dataPaths, so we know which ones have been validated by
        // member components and which must be validated here
        this.buildDataPathsRecursively(dataPaths, "", this.getDataSource());

        // First go through all the member forms and perform validation.
        if (this.members) {
            // Wrap field validation in a queue so that server validators are
            // sent as a single request.
            var wasAlreadyQueuing = isc.rpc ? isc.rpc.startQueue() : false;

            for (var i = 0; i < this.members.length; i++) {
                if (!isc.isA.DynamicForm(this.members[i])) continue;
                var form = this.members[i],
                    disableValidation = form.disableValidation,
                    items = this.members[i].getItems();

                if (!disableValidation) {
                    // we don't want any user-defined handleHiddenValidationErrors to fire on the
                    // form - instead we'll fire this method at the valuesManager level only.
                    // Implement this by applying our own 'handleHiddenValidationErrors' method to
                    // the form that notifies us what the errors were.
                    if (form.handleHiddenValidationErrors != null) {
                        this.logInfo("form level 'handleHiddenValidationErrors' method suppressed " +
                                     "in favor of valuesManager level handler", "validation");
                        form._prevHHVE = form.handleHiddenValidationErrors;
                    }
                    form.handleHiddenValidationErrors = this._handleHiddenFormErrors;
                }

                for (var j = 0; j < items.length; j++) {
                    var fieldName = items[j].getFullDataPath() || items[j].getFieldName();
                    isDataPath = isDataPath || (fieldName && fieldName.contains(isc.Canvas._$slash));
                    // IF the form shares a dataSource with this VM instance,
                    // remove the appropriate field from our copy of the dataSource fields -
                    // we have already validated this value.

                    if (dsFields && this.members[i].getDataSource() == this.getDataSource()) {
                        delete dsFields[fieldName];
                    }
                    if (dataPaths && fieldName) {
                        delete dataPaths[fieldName.trim(isc.Canvas._$slash)];
                    }
                }
                // Allow the form to perform its own validation.
                // Validate hidden fields (makes sense since we validate hidden forms!)
                // Pass the additional param to suppress validating DS fields for which there
                // are no items though, since we handle these at the VM level.
                // This will also display validation errors, or fire the method to handle
                // validation errors while hidden.
                var formSuccess = disableValidation ? true :
                        // suppress showErrors here - we'll show them explicitly from
                        // VM.showErrors
                        form.validate(true, true, typeValidationsOnly,
                            checkValuesOnly, skipServerValidation, true);
                returnVal = (returnVal && formSuccess);

                if (!disableValidation) {
                    if (form._preHHVE) form.handleHiddenValidationErrors = form._preHHVE;
                    else delete form.handleHiddenValidationErrors;
                }
                if (!formSuccess) {
                    // If the form itself is hidden, add its full set of errors to our hidden
                    // form validation errors object.
                    if (!(form.isDrawn() && form.isVisible())) {
                        this.addHiddenErrors(form.errors, form);

                    // Otherwise, add just the hidden errors.
                    } else {
                        this.addHiddenErrors(form.getHiddenErrors(), form);
                    }
                }
            }

            // Submit server validation requests queue
            if (!wasAlreadyQueuing && isc.rpc) isc.rpc.sendQueue();
        }


        // we now have to perform validation on the DS fields not present in any member form
        var values = this.getValues(),
            errors = {},
            work = isDataPath ? dataPaths : dsFields;

        for (var fieldName in work) {

            var fieldObject = work[fieldName],
                validators = fieldObject.validators,
                value = isc.DynamicForm._getFieldValue(fieldName, fieldObject, values, null, true);

            if (validators != null) {
                // iterate through the validators again, this time actually checking each one
                for (var i = 0; i < validators.length; i++) {
                    // validators is an array of validator objects, each of which has the form
                    //    {type:"validatorType", errorMessage:"message", optionalParam:"", ...}
                    var validator = validators[i];
                    if (!validator) continue;
                    // Unless we're looking at a 'required' or 'requiredIf' field,
                    // don't try to validate null values.

                    if (value == null
                        && validator.type != 'required' && validator.type != "requiredIf")
                    {
                        continue;
                    }
                    // we have no item, so pass the field object to processValidator()
                    // This roughly matches what we do in ListGrid validation
                    if (!this.processValidator(fieldObject, validator, value, null, values)) {
                        if (errors[fieldName] == null) errors[fieldName] = [];
                        var errorMessage = validator.errorMessage || this.unknownErrorMessage;
                        errors[fieldName].add(errorMessage);
                    }
                }
            }

            // for consistency with forms - if we got a single error, store as a string, not
            // a single element array
            if (errors[fieldName] && errors[fieldName].length == 1) errors[fieldName] = errors[fieldName][0];

        }

        // add hidden errors from fields that are not associated with any form.
        this.addHiddenErrors(errors);

        var hasErrors = this.hasErrors();
        // if we had errors before, or we have errors now, show errors

        if (hadErrors || hasErrors) {
            this.showErrors(true);
        }

        if (isc.getKeys(errors).length > 0)  returnVal = false;

        return returnVal;
    },

    buildDataPathsRecursively : function(dataPaths, parentDP, dataSource) {
        if (!isc.isA.DataSource(dataSource)) return;

        if (dataSource.__vmVisited) {
           this.logWarn("detected ds loop at: " + parentDP + ", refusing to recurse further");
           return;
        }
        dataSource.__vmVisited = true;

        var fields = dataSource.getFields();
        for (var key in fields) {
            dataPaths[parentDP + key] = fields[key];
            if (dataSource.fieldIsComplexType(key)) {
                var subDS = dataSource.getSchema(fields[key].type);
                this.buildDataPathsRecursively(dataPaths, parentDP + key + isc.Canvas._$slash, subDS);
            }
        }
        delete dataSource.__vmVisited;
    },

    //> @method valuesManager.getValidatedValues()
    // Call +link{valuesManager.validate()} to check for validation errors. If no errors are found,
    // return the current values for this valuesManager, otherwise return null.
    // @return (object|null) current values or null if validation failed.
    // @group errors
    // @visibility external
    //<
    getValidatedValues : function () {
        if (!this.validate()) return null;
        return this.getValues();
    },

    // Handler for hidden form validation errors. This method is applied directly to the
    // member form
    _handleHiddenFormErrors : function (errors) {
        var vm = this.valuesManager;
        vm.addHiddenErrors(errors, this);
        return false;   // suppress the standard warning
    },


    clearHiddenErrors : function () {
        delete this.hiddenErrors;
    },

    // addHiddenErrors()
    // For a valuesManager, hidden validation errors may come from:
    // - a field in the dataSource not associated with any member form item
    // - a hidden item in a member form
    // - a hidden or undrawn member form.

    addHiddenErrors : function (errors, form) {
        if (errors == null || isc.isAn.emptyObject(errors)) return;

        if (!this.hiddenErrors) this.hiddenErrors = {};
        if (form) {
            if (isc.isA.Canvas(form)) form = form.getID();
        } else form = this.getID();

        if (!this.hiddenErrors[form]) this.hiddenErrors[form] = {};

        for (var fieldName in errors) {
            this.hiddenErrors[form][fieldName] =
                this._addFieldErrors(this.hiddenErrors[form][fieldName], errors[fieldName]);
        }
    },

    // Returns the current snapshot of hidden errors in a flat list
    getHiddenErrors : function (suppressSynch) {

        if (!suppressSynch) {
            this.synchHiddenErrors();
        }

        if (!this.hiddenErrors) return null;
        var flatErrors = {};
        for (var form in this.hiddenErrors) {
            this.assembleHiddenErrorsRecursively(flatErrors, this.hiddenErrors[form]);
        }
        return flatErrors;
    },

    assembleHiddenErrorsRecursively : function (flatErrors, formErrors, key, index) {
        if (key == null) key = "";
        var outKey = key;
        if (index != null) outKey += "[" + index + "]";
        if (isc.isA.List(formErrors)) {
            for (var i = 0; i < formErrors.length; i++) {
                if (formErrors[i] !== null) {
                    if (isc.isAn.Object(formErrors[i])) {
                        this.assembleHiddenErrorsRecursively(flatErrors, formErrors[i], outKey, i);
                    } else {
                    if (flatErrors[outKey] == null) flatErrors[outKey] = [];
                        flatErrors[outKey][i] = formErrors[i];
                    }
                }
            }
        } else if (isc.isAn.Object(formErrors)) {
            for (var objKey in formErrors) {
                if (isc.isAn.Object(formErrors[objKey])) {
                    if (outKey == "") {
                        this.assembleHiddenErrorsRecursively(flatErrors, formErrors[objKey], objKey);
                    } else {
                        this.assembleHiddenErrorsRecursively(flatErrors, formErrors[objKey],
                                                         outKey + isc.Canvas._$slash + objKey);
                    }
                } else {
                    if (outKey == "") {
                        flatErrors[objKey] = formErrors[objKey];
                    } else {
                        flatErrors[outKey + isc.Canvas._$slash + objKey] = formErrors[objKey];
                    }
                }
            }
        } else {
            flatErrors[outKey] = formErrors;
        }
        return flatErrors;
    },


    // synchHiddenErrors()
    // This is a method to ensure that our 'hiddenErrors' object returned by getHiddenErrors()
    // and passed to handleHiddenValidationErrors is in synch with the current set of
    // visible forms / items.
    // Required in the case where
    // - setErrors() was called,
    // - form/item visibility was changed,
    // - showErrors() called.

    synchHiddenErrors : function () {

        var hiddenErrors = this.hiddenErrors,
            vmID = this.getID();

        // Logic for errors that occurred on fields with no associated member form item
        // (when errors were stored)
        if (hiddenErrors && hiddenErrors[vmID]) {
            for (var field in hiddenErrors[vmID]) {
                var errors = hiddenErrors[vmID][field],
                    item = this.getItem(field),
                    memberForm = item ? item.form : null;

                // If there is now an associated member form item, we need to add the
                // field error to the form, and update this.hiddenErrors
                if (item) {
                    memberForm.addFieldErrors(field, errors);
                    // clear out the hidden error under the valuesManager's ID - the error
                    // is now associated with a form.
                    delete hiddenErrors[vmID][field];
                }
            }
        }

        // Update hidden errors for each form.
        // Quickest to just re-generate hidden errors per form rather than trying to synch with
        // existing stored hiddenErrors object.
        var vmErrors = hiddenErrors[vmID];
        hiddenErrors = this.hiddenErrors = {};
        if (vmErrors) hiddenErrors[vmID] = vmErrors;
        // Now iterate through every member's errors and add to hidden members arrays if
        // necessary
        if (this.members) {
            for (var i = 0; i< this.members.length; i++) {
                if (!isc.isA.DynamicForm(this.members[i])) continue;
                var member = this.members[i],
                    memberID = member.getID(),
                    memberErrors = member.errors;
                if (!memberErrors || isc.isAn.emptyObject(memberErrors)) continue;

                // shortcut - if the form is hidden always store all its errors. This may
                // overwrite an already up to date this.hiddenErrors[formID] but is quicker
                // than iterating through the errors doing comparisons.
                if (!member.isVisible() || !member.isDrawn()) {
                    memberErrors = isc.addProperties({}, memberErrors);
                    hiddenErrors[memberID] = memberErrors;
                } else {
                    // catch items that have been hidden or removed
                    for (var field in memberErrors) {
                        var item = member.getItem(field);
                        if (!item) {
                            if (!hiddenErrors[vmID]) hiddenErrors[vmID] = {};
                            hiddenErrors[vmID][field] = memberErrors[field];
                            // just delete the field from the form's errors object
                            delete memberErrors[field];

                        } else if (!item.visible) {
                            if (!hiddenErrors[memberID]) hiddenErrors[memberID] = {};
                            hiddenErrors[memberID][field] = memberErrors[field];
                        }
                    }
                }
            }
        }

    },

    //>    @method    valuesManager.processValidator()    (A)
    //            process a single validator for a field.
    //
    //        @param    [item]        (object)    Form item displaying the value. May be null if no
    //                                      form item exists for the field.
    //        @param    validator    (object)    validation object
    //        @param    value        (string)    value for this field.
    //      @param  [type]      (string)    validator type. if not passed this is derived from
    //                                      the <code>type</code> property on the validation parameter
    // @param record (object) Field values for record being validated.
    //
    //        @return    (boolean)    true == passed validation, false == validation failed
    //        @group    validation
    //<
    processValidator : function (item, validator, value, type, record) {

        return isc.Validator.processValidator(item, validator, value, type, record);
    },

    // _handleHiddenValidationErrors()
    // Internal method to display validation errors when we can't show them in a form.
    // This is used to handle
    // - errors coming from an undrawn or hidden member form
    // - errors coming from hidden items in a member form
    // - errors coming from a dataSource field for which we have no member form item.
    // Note these errors are all combined into a single object retrieved via this.getHiddenErrors()
    // if a developer needs to determine which form an error came from, they can use
    // getMemberForField()
    // Additional suppressSynch parameter - if we know the hidden errors are in synch with
    // the currently visible set of members / fields (IE this has been called directly from
    // setErrors() or validate()) we can skip the logic to ensure that this.hiddenErrors
    // is up to date.
    _handleHiddenValidationErrors : function (suppressSynch) {
        var errors = this.getHiddenErrors(suppressSynch);

        // bail if there were no errors on hidden fields
        if (errors == null || isc.getKeys(errors).length == 0) return;

        // If we have an implementation to handle the hidden validation errors, call it now.
        var returnVal;
        if (this.handleHiddenValidationErrors) {
            returnVal = this.handleHiddenValidationErrors(errors);
        }

        if (returnVal == false) return;

        // Log a warning unless this was suppressed by the handleHiddenValidationErrors method.
        var errorString = "Validation failed with the following errors:";
        var errorArray = isc.isAn.Array(errors) ? errors : [errors];
        for (var i = 0; i < errorArray.length; i++) {
            var theErrors = errorArray[i];
            for (var fieldName in theErrors) {
                var fieldErrors = errors[fieldName];
                if (!isc.isAn.Array(fieldErrors)) fieldErrors = [fieldErrors];
                if (fieldErrors.length == 0) continue;

                errorString += "\n" + fieldName + ":";
                for (var i = 0; i < fieldErrors.length; i++) {
                    errorString += (i == 0 ? "- " : "\n - ") + fieldErrors[i];
                }
            }
        }
        this.logWarn(errorString, "validation");
    },

    // Validation errors APIs

    //>    @method    valuesManager.setErrors() [A]
    // Sets validation errors for this valuesManager to the specified set of errors.
    // Errors should be of the format:<br>
    // <code>{field1:errors, field2:errors}</code><br>
    // where each <code>errors</code> object is either a single error message string or an
    // array of error messages.<br>
    // If <code>showErrors</code> is passed in, error messages will be displayed in the
    // appropriate member form items. For fields with no visible form item,
    // +link{valuesManager.handleHiddenValidationErrors()} will be fired instead.<br>
    // Note that if <code>showErrors</code> is false, errors may be shown at any time via
    // a call to +link{ValuesManager.showErrors()}.
    //
    // @param   errors  (object) list of errors as an object with the field names as keys
    // @param   showErrors (boolean) If true display errors now.
    // @group errors
    // @visibility external
    //<
    setErrors : function (errors, showErrors) {
        this.clearHiddenErrors();
        if (isc.isA.List(errors)) errors = errors[0];
        //errors = isc.DynamicForm.formatValidationErrors(errors);

        var memberForms = (this.members ? this.members.duplicate() : []);

        for (var i = 0; i < memberForms.length; i++) {
            if (!isc.isA.DynamicForm(memberForms[i])) continue;
            var form = memberForms[i],
                hiddenForm = !form.isVisible() || !form.isDrawn(),
                items = form.getItems(),
                formErrors = {},
                hiddenFormErrors = {},
                selectionChain = form.getSelectionChain();
            for (var j = 0;j < items.getLength(); j++) {
                var item = items[j],
                    itemDataPath = item.getFullDataPath(),
                    itemName = item.getFieldName(),
                    itemErrors = this.getItemErrors(errors, itemDataPath, selectionChain);
                if (itemErrors != null) {
                    formErrors[itemName] = itemErrors;

                    if (hiddenForm || !item.visible) {
                        hiddenFormErrors[itemName] = itemErrors;
                    }
                    this.deleteItemErrors(errors, itemDataPath, selectionChain);
                }
            }
            // suppress redraw and suppress form-level hiddenValidationError handling
            form.setErrors(formErrors, false);

            // hang onto the hidden form errors so we can fire the hiddenValidationErrors
            // handler.
            // Note: track hidden errors by form - see comments near
            // addHiddenErrors() / _getHiddenErrors() for more on this
            if (!isc.isAn.emptyObject(hiddenFormErrors))
                this.addHiddenErrors(hiddenFormErrors, form);
        }

        this.addHiddenErrors(errors);
        // We know stored hidden errors object is up to date
        if (showErrors) this.showErrors(true);
    },

    getItemErrors : function (errors, itemNameOrDataPath, selectionChain) {
        var dataPath = itemNameOrDataPath.trim(isc.Canvas._$slash),
            isDataPath = dataPath.contains(isc.Canvas._$slash);
        if (isc.isAn.Array(errors)) errors = errors[0];
        if (!isDataPath) {
            var serverErrors = errors[itemNameOrDataPath];
        } else {
            var elements = dataPath.split(isc.Canvas._$slash),
                serverErrors = errors,
                nestedListCount = 0;
            for (var i = 0; i < elements.length; i++) {
                serverErrors = serverErrors[elements[i]];
                if (isc.isAn.Array(serverErrors)) {
                    if (selectionChain.length > nestedListCount) {
                        serverErrors = serverErrors[selectionChain[nestedListCount++]];
                    } else {
                        // We have a missing or incomplete selection chain, so we have no real
                        // way to decide which entry in this list is the right one to use.  All
                        // we can do is select the first...
                        serverErrors = serverErrors[0];
                    }
                }

                // The error structure is sparse - bail as soon as we encounter a missing path
                if (!serverErrors) break;
            }
        }
        if (serverErrors) {
            if (!isc.isAn.Array(serverErrors)) serverErrors = [serverErrors];
            var clientErrors = [];
            for (var i = 0; i < serverErrors.length; i++) {
                if (serverErrors[i].errorMessage) {
                    clientErrors.add(serverErrors[i].errorMessage);
                } else {
                    clientErrors.add(serverErrors[i]);
                }
            }
            return clientErrors.length > 1 ? clientErrors : clientErrors[0];
        }
    },

    deleteItemErrors : function (errors, itemNameOrDataPath, selectionChain) {
        var dataPath = itemNameOrDataPath.trim(isc.Canvas._$slash),
            isDataPath = dataPath.contains(isc.Canvas._$slash);
        if (isc.isAn.Array(errors)) errors = errors[0];
        if (!isDataPath) {
            delete errors[itemNameOrDataPath];
        } else {
            var elements = dataPath.split(isc.Canvas._$slash);
            var serverErrors = errors,
                traversedObjects = [],
                nestedListCount = 0;
            for (var i = 0; i < elements.length; i++) {
                traversedObjects.add(serverErrors);
                serverErrors = serverErrors[elements[i]];
                if (isc.isAn.Array(serverErrors)) {
                    if (selectionChain.length > nestedListCount) {
                        serverErrors = serverErrors[selectionChain[nestedListCount++]];
                    } else {
                        serverErrors = serverErrors[0];
                    }
                }
                if (!serverErrors) break;
            }

            if (serverErrors) delete serverErrors;

            // Clean up any empty container objects
            for (var i = traversedObjects.length - 1; i >= 0; i--) {
                if (isc.isAn.emptyObject(traversedObjects[i])) {
                    delete traversedObjects[i];
                }
            }
        }
    },

    // little helper to combine errors into arrays
    // Returns the errors object to use
    _addFieldErrors : function (oldErrors, newErrors) {
        if (!oldErrors) return newErrors;
        if (!newErrors) return oldErrors;
        if (!isc.isAn.Array(oldErrors)) oldErrors = [oldErrors];
        if (isc.isA.String(newErrors)) oldErrors.add(newErrors);
        else oldErrors.addList(newErrors);

        return oldErrors;

    },

    //> @method valuesManager.addFieldErrors()
    // Adds validation errors to the existing set of errors for the field in question.
    // Errors passed in should be a string (for a single error message) or an array of strings.
    // Pass in the showErrors parameter to immediately display the errors to the user by
    // redrawing the appropriate member form item (or if no visible item is found for the field
    // firing +link{valuesManager.handleHiddenValidationErrors()}.
    // @param fieldName (string) name of field to apply errors to
    // @param errors (string | array of strings) error messages for the field
    // @param showErrors (boolean) should the error(s) be immediately displayed to the user?
    // @group errors
    // @visibility external
    //<
    addFieldErrors : function (fieldName, errors, showErrors) {
        var hidden = true;
        var form = this.getMemberForField(fieldName);
        if (form != null && isc.isA.DynamicForm(form)) {
            form.addFieldErrors(fieldName, errors, false);
            var item = form.getItem();
            if (form.isVisible() && form.isDrawn() && item && item.visible) {
                hidden = false;
            }
        }

        if (hidden) {
            if (!this.hiddenErrors) this.hiddenErrors = {};
            var formName = form ? form.getID() : this.getID();
            if (!this.hiddenErrors[formName]) this.hiddenErrors[formName] = {};

            this.hiddenErrors[formName][fieldName] =
                this._addFieldErrors(this.hiddenErrors[formName][fieldName], errors);

        }

        if (showErrors) this.showFieldErrors(fieldName);
    },

    //> @method valuesManager.setFieldErrors()
    // Sets validation errors for some field in the valuesManager.<br>
    // Errors passed in should be a string (for a single error message) or an array of strings.
    // Pass in the showErrors parameter to immediately display the errors to the user by
    // redrawing the appropriate member form item (or if no visible item is found for the field
    // firing +link{valuesManager.handleHiddenValidationErrors()}.
    // @param fieldName (string) name of field to apply errors to
    // @param errors (string | array of strings) error messages for the field
    // @param showErrors (boolean) should the error(s) be immediately displayed to the user?
    // @group errors
    // @visibility external
    //<
    setFieldErrors : function (fieldName, errors, showErrors) {
        var hidden = true;
        var form = this.getMemberForField(fieldName);
        if (form != null && isc.isA.DynamicForm(form)) {
            form.setFieldErrors(fieldName, errors, false);
            var item = form.getItem();
            if (form.isVisible() && form.isDrawn() && item && item.visible) {
                hidden = false;
            }
        }

        if (hidden) {
            if (!this.hiddenErrors) this.hiddenErrors = {};
            this.hiddenErrors[fieldName] = errors;
        }

        if (showErrors) this.showFieldErrors(fieldName);
    },

    //>    @method    valuesManager.clearErrors()
    //            Clears all errors from member forms.
    //      @param  showErrors (boolean)    If true, clear any visible error messages.
    //        @group    errors
    //      @visibility external
    //<
    clearErrors : function (showErrors) {
        this.setErrors({}, showErrors);
    },

    //> @method valuesManager.clearFieldErrors()
    // Clear all validation errors associated with some field in this form
    // @param fieldName (string) field for which errors should be cleared
    // @param show (boolean) if true, and the field is present in one of our member forms,
    //                       redraw it to clear any currently visible validation errors
    // @group errors
    // @visibility external
    //<
    clearFieldErrors : function (fieldName, show) {
        var form = this.getMemberForField(fieldName);
        if (form && isc.isA.DynamicForm(form)) form.clearFieldErrors(fieldName, show);

        if (this.hiddenErrors) delete this.hiddenErrors[fieldName];
    },

    //> @method valuesManager.getErrors()
    // Returns the set of errors for this valuesManager.
    // Errors will be returned as an object of the format <br>
    // <code>{field1:errors, field2:errors}</code><br>
    // Where each errors object is either a single error message or an array of error message
    // strings.
    // @return (object) Object containing mapping from field names to error strings. Returns null
    //                  if there are no errors for this valuesManager.
    // @group errors
    // @visibility external
    //<
    // Stored errors include those stored as "hiddenErrors", with no associated form (came from
    // a datasource field definition only, presumably), and errors from member forms
    getErrors : function () {
        // pick up stored hidden errors.
        // [don't bother to synch - we're not interested in whether they're truly hidden or not now]
        var errors = isc.addProperties({}, this.getHiddenErrors(true));
        // add errors from each member form

        if (this.members) {
            for (var i = 0; i < this.members.length; i++) {
                if (!isc.isA.DynamicForm(this.members[i])) continue;
                isc.addProperties(errors, this.members[i].getErrors());
            }
        }
        if (!isc.isA.emptyObject(errors)) return errors
        return null
    },

    //> @method valuesManager.getFieldErrors()
    // Returns any validation errors for some field in this valuesManager.
    // Errors will be returned as either a string (a single error message), or an array
    // of strings. If no errors are present, will return null.
    // @param fieldName (string) fieldName to check for errors
    // @return (string | array of strings) error messages for the field passed in
    // @group errors
    // @visibility external
    //<
    getFieldErrors : function (fieldName) {
        var form = this.getMemberForField(fieldName)
        if (form && isc.isA.DynamicForm(form)) return form.getFieldErrors(fieldName);
        if (this.hiddenErrors && this.hiddenErrors[this.getID()])
            return this.hiddenErrors[this.getID()][fieldName];
    },

    //> @method valuesManager.hasErrors()
    // Are there any errors associated with any fields in this valuesManager?
    // @return (Boolean) returns true if there are any outstanding validation errors, false
    //                  otherwise.
    // @group errors
    // @visibility external
    //<
    hasErrors : function () {
        if (this.hiddenErrors && !isc.isA.emptyObject(this.hiddenErrors)) {
            for (var form in this.hiddenErrors) {
                if (this.hiddenErrors[form] && !isc.isAn.emptyObject(this.hiddenErrors[form]))
                    return true;
            }
        }
        if (this.members == null) return false;
        for (var i = 0; i < this.members.length; i++) {
            if (isc.isA.DynamicForm(this.members[i]) && this.members[i].hasErrors()) return true;
        }
        return false;
    },

    //> @method valuesManager.hasFieldErrors()
    // Are there any errors associated with a field in this valuesManager?
    // @param fieldName (string) field to check for errors
    // @return (Boolean) returns true if there are any outstanding validation errors, false
    //                  otherwise.
    // @group errors
    // @visibility external
    //<
    hasFieldErrors : function (fieldName) {
        var form = this.getMemberForField(fieldName);
        if (form && isc.isA.DynamicForm(form) && form.hasFieldErrors(fieldName)) return true;
        var hiddenErrors = this.getHiddenErrors(true);
        if (hiddenErrors && hiddenErrors[fieldName] != null) return true;
        return false;
    },

    //> @method valuesManager.showErrors()
    // Method to explicitly show the latest set of validation errors present on this
    // ValuesManager.<br>
    // Will redraw all member forms to display (or clear) currently visible errors, and
    // fire +link{valuesManager.handleHiddenValidationErrors()} to allow custom handling of
    // hidden errors.
    // @group errors
    // @visibility external
    //<
    // suppressHiddenErrorSynch parameter: indicates we know our stored hidden errors match the
    // currently visible set of fields [so we just ran validate() or setErrors()].
    // passed through to _handleHiddenValidationErrors()
    showErrors : function (suppressHiddenErrorSynch) {
        if (this.members) {
            for (var i= 0; i < this.members.length; i++) {
                if (!isc.isA.DynamicForm(this.members[i])) continue;
                if (!this.members[i].isDrawn() || !this.members[i].isVisible()) continue;
                this.members[i]._suppressAutoFocusOnErrors = true;
                this.members[i].showErrors();
            }
        }

        if (this.hiddenErrors != null) {
            this._handleHiddenValidationErrors(suppressHiddenErrorSynch);
        }
    },

    //> @method valuesManager.showFieldErrors()
    // Method to explicitly show the latest set of validation errors present on some field
    // within this ValuesManager.<br>
    // If the field in question is present as a visible item in a member form, the form item
    // will be redrawn to display the error message(s).
    // Otherwise +link{valuesManager.handleHiddenValidationErrors()} will be fired to allow
    // custom handling of hidden errors.
    // @group errors
    // @visibility external
    //<
    showFieldErrors : function (fieldName) {
        var form = this.getMemberForField(fieldName);
        if (form && isc.isA.DynamicForm(form) && form.isVisible() && form.isDrawn()) {
            var item = form.getItem(fieldName);
            if (item && item.visible) {
                item.redraw("Validation errors modified");
                return;
            }
        }

        // At this point we know we have a hidden error for the field - fire the
        // handleHiddenValidationErrors method. Of course that actually 'shows' the
        // errors for all hidden fields, not just this one.
        this._handleHiddenValidationErrors();
    },

    // Flow Methods:

    //> @method getFilterCriteria()
    // Return the set of filter criteria for this valuesManager based on its current set of
    // values
    // @return (object) set of name:values pairs to be used as filter criteria
    //<
    getFilterCriteria : function () {
        // get filter criteria from all my members

        var crit = {};
        if (this.members) {
            for (var i =0; i < this.members.length; i++) {
                isc.addProperties(crit, this.members[i].getFilterCriteria());
            }
        }

        // Mix in any values we have that didn't come from member forms
        var values = this.getValues(),
            undef;
        for (var field in values) {
            if (crit[field] !== undef) delete values[field];
        }
        // filterCriteriaForFormValues will clear out null values, and handle arrays with an
        // empty entry (Implies a null criterion)
        isc.addProperties(crit, isc.DataSource.filterCriteriaForFormValues(values));

        return crit;
    },


    // ========================================================================================
    //  Values Management APIs
    // ========================================================================================

    //> @method valuesManager.getValues()
    // Returns the current set of values for the values manager instance.  This includes the
    // values from any form managed by this manager, as well as any values explicitly applied
    // via +link{valuesManager.setValues()}.
    // @return (object) a map of the values for this manager
    // @group formValues
    // @visibility external
    //<
    getValues : function () {

        // if one of our member forms has focus, ensure its focus-item's value has been saved
        // out [which will update this.values]
        if (this.members != null) {
            var fc = isc.EH.getFocusCanvas();
            if (this.members.contains(fc) && fc.updateFocusItemValue) fc.updateFocusItemValue();
        }
        // Never let this.values be externally accessible.

        return isc.addProperties({}, this.values);
    },

    //> @method valuesManager.setValues()
    // Replaces the current values of the ValuesManager and all member components with the
    // values passed in.
    // <P>
    // Values should be provided as an Object containing the new values as properties, where
    // each propertyName is the name of a +link{items,form item} in one of the member forms,
    // and each property value is the value to apply to that form item via
    // +link{FormItem.setValue()}.
    // <P>
    // Values with no corresponding form item may also be passed, will be tracked by the
    // valuesManager and returned by subsequent calls to +link{getValues()}.
    // <P>
    // Any +link{FormItem} for which a value is not provided will revert to its
    // +link{formItem.defaultValue,defaultValue}.  To cause all FormItems to revert to default
    // values, pass null.
    // <P>
    // This method also calls +link{rememberValues()} so that a subsequent later call to
    // +link{resetValues()} will revert to the passed values.
    //
    // @param   values  (object)    new set of values for this values manager.
    // @group formValues
    // @visibility external
    //<
    setValues : function (values) {
        if (isc.isAn.Array(values)) {
            var useFirst = isc.isA.Object(values[0]);
            this.logWarn("values specified as an array." +
                        (useFirst ? " Treating the first item in the array as intended values."
                                  : " Ignoring specified values."));
            if (useFirst) values = values[0];
            else values = null;
        }



        // Duplicate the values object so we can manipulate it and apply it directly to
        // this.values and modify without interfering with external code.
        // _duplicateValues does a recursive duplication based on dataPaths
        var clonedVals = {};
        isc.DynamicForm._duplicateValues(this, values, clonedVals);
        values = clonedVals;

        this.values = values;
        if (this.members) {
            for (var i = 0; i < this.members.length; i++) {
                // setMemberValues will update the members' items to display the values passed in
                // Note that for DynamicForms, it also explicitly calls 'clearValue()' on items
                // for which we have no member - this re-evaluates default values
                this._setMemberValues(this.members[i]);
            }
        }
        // remember values for resetting
        this.rememberValues();

    },

    //> @method valuesManager.setData()
    // Set the data (values) on this valuesManager [synonym for <code>setValues()</code>].
    //<
    // setData() is used in dataBinding rather than setValues.
    setData : function (values) {
        return this.setValues(values);
    },

    //> @method valuesManager.clearValues()
    // Clear out all the values managed by this values manager.
    // @visibility external
    // @group formValues
    //<
    clearValues : function () {
        this.setValues({});
    },

    //> @method valuesManager.getMemberValues()
    // Returns the subset of this valuesManager's values associated with some member form.
    //
    // @param   ID  (string)    ID of the member form for which we want to retrieve the values.
    // @return (object) a map of the values for the appropriate member form.
    // @visibility external
    // @group formValues
    //<
    getMemberValues : function (ID) {
        var member = this.getMember(ID);
        if (member != null) return member.getValues();
    },

    //> @method valuesManager.setMemberValues()
    // Set the values for some member form.
    // @param   ID  (string)    ID of the member form to update
    // @param   values  (object)    new values for the form
    // @visibility external
    // @group formValues
    //<
    setMemberValues : function (ID, values) {
        var member = this.getMember(ID);
        if (member != null) return member.setValues(values);
    },


    //> @method valuesManager.rememberValues()
    // @include dynamicForm.rememberValues()
    //<
    // Values may change as a result of
    // - adding a new member and picking up values for fields for which we previously had no
    //   value
    // - the user modifying values in a field
    // - calls to 'setValue()' [not setValues as that will re-remember the values after setting]
    rememberValues : function () {

        var values = this.getValues();

        var oldVals = {},
            rememberedDefault = [];

        // Recursively duplicate values so further edits won't manipulate the remembered values
        // directly.
        isc.DynamicForm._duplicateValues(this, values, oldVals, rememberedDefault);

        // Remember the duplicated values object
        this._oldValues = oldVals;
        // rememberedDefault array will contain dataPaths for every item that had its value
        // set to the default in the 'values' object we passed in.
        // We need this information so 'resetValues' can set these items to null and
        // potentially re-evaluate a dynamicDefault rather than resetting to whatever the
        // value is at this moment.
        // [still store the current val for valuesHaveChanged() checks]
        this._rememberedDefault = rememberedDefault;

        return this._oldValues;
    },

    //> @method valuesManager.getOldValues()
    // @include dynamicForm.getOldValues()
    //<
    getOldValues : function () {
        var oldValues = {};
        isc.addProperties(oldValues, this._oldValues);
        return oldValues;
    },


    //> @method valuesManager.getChangedValues()
    // @include dynamicForm.getChangedValues()
    // @see getOldValues()
    // @visibility external
    //<
    getChangedValues : function () {
        return this.valuesHaveChanged(true);
    },


    //> @method valuesManager.resetValues()
    // @include dynamicForm.resetValues()
    //<
    resetValues : function () {
        // pull the values from form._oldValues into ValuesManager.values
        var values = {};
        isc.DynamicForm._duplicateValues(this, this._oldValues, values);
        // clear any remembered defaults so they get re-eval'd
        for (var i = 0; i < this._rememberedDefaults; i++) {
            isc.DynamicForm._clearFieldValue(this._rememberedDefaults[i], values, this);
        }

        this.setValues(values);

    },

    //> @method valuesManager.valuesHaveChanged()
    // @include dynamicForm.valuesHaveChanged()
    //<
    valuesHaveChanged : function (returnChangedVals) {
        var values = this.getValues();
        var oldValues = this._oldValues || {};

        return isc.DynamicForm.valuesHaveChanged(this,returnChangedVals,values,oldValues);
    },

    //> @method valuesManager.getValue()
    // Returns the value for some field.
    // @param   fieldName   (string)    Which value to be returned
    // @param   [component] (Canvas)    Optional, the component for which we are trying to
    //                                  retrieve a value.  This is used to identify which of
    //                                  the potential records to use when the ValuesManager
    //                                  is managing a complex structure involving nested Lists
    // @return  (any)   current value of the appropriate field
    // @visibility external
    // @group formValues
    //<
    getValue : function (fieldName, component) {
        return isc.DynamicForm._getFieldValue(fieldName, this.getField(fieldName),
                                                this.values, component, true);
    },

    //> @method valuesManager.setValue()
    // Set the value for some field.
    // @param   fieldName   (string)    Which field to set the value for
    // @param   newValue    (any)       New value for the field.
    // @visibility external
    // @group formValues
    //<
    setValue : function (fieldName, newValue) {

        var valueSet = false,
            member,
            undef;
        if (this.members) {
            var items = this.getItem(fieldName, true);
            if (items && items.length > 0) {
                for (var i = 0; i < items.length; i++) {
                    var item = items[i];
                    if (item && item.setValue) {

                        // Handle this being a field with an 'opaque' data type with a get/set atomic
                        // value method
                        // If this is the case, extract the atomic value and pass it to the item.

                        var type = item.type ? isc.SimpleType.getType(item.type) : null,
                            itemValue = newValue,
                            isUndef = (newValue === undef);
                        if (!isUndef && type && type.getAtomicValue && type.updateAtomicValue) {
                            // store the new atomic type on our values object

                            // getFullDataPath will prepend any member-widget level dataPath if necessary
                            fieldName = item.getFullDataPath();
                            isc.DynamicForm._saveFieldValue(fieldName, null, newValue, this.values, null, true);
                            // extract the atomic value which we'll pass to item.setValue()
                            itemValue = type.getAtomicValue(newValue);
                        }

                        if (isUndef) item.clearValue();
                        else item.setValue(itemValue);
                        valueSet = true;
                    }
                }
            }
        }
        if (!valueSet) {
            if (newValue === undef) {
                isc.DynamicForm._clearFieldValue(fieldName, this.values);
            } else {

                isc.DynamicForm._saveFieldValue(fieldName, null, newValue, this.values, null, true);
            }
        }
        var members = this._findMemberByField(fieldName, false, true);
        if (members) {
            for (var i = 0; i < members.length; i++) {
                if (member && member.setData) {
                    var dataObjPath = fieldName;
                    if (fieldName.indexOf(isc.Canvas._$slash) != -1) {
                        dataObjPath = fieldName.substring(0, fieldName.lastIndexOf(isc.Canvas._$slash));
                        member.setData(isc.DynamicForm._getFieldValue(dataObjPath, null,
                                                                      this.values, member, true));
                    }
                }
            }
        }
    },


    //> @method valuesManager.clearValue()
    // Clear the value for some field.
    // @param   fieldName   (string)    Which field to set the value for
    // @visibility external
    // @group formValues
    //<
    clearValue : function (fieldName) {
        this.setValue(fieldName);
    },

    // ========================================================================================
    //  Member Management
    // ========================================================================================

    //> @method valuesManager.addMember()
    //
    // Add a new member to this valuesManager.  Any +link{class:Canvas} can be a member of a
    // valuesManager, even components like +link{class:Layout} or +link{class:TabSet} that do
    // not actually have any values to manage.  When "valueless" components like these bind to
    // a ValuesManager, it is in order to provide their own child components with a shared
    // valuesManager so that complex data can be displayed and edited - see
    // +link{DataBoundComponent.dataPath} for more details.
    // <p>
    // For components like +link{class:DynamicForm} and +link{class:ListGrid}, which do have
    // a set of values to manage, the component's values will subsequently be available through
    // this valuesManager.
    // <p>
    // Note on pre-existent values when the member component is a +link{class:DynamicForm}:<br>
    // If the valuesManager has a value specified for some field, for which the member form has
    // an item, this value will be applied to the member form.  This is true whether the item
    // has a value or not.<br>
    // However if the member form has a value for some field, and the ValuesManager does not
    // have a specified value for the same field, we allow the valuesManager to pick up the
    // value from the member form.
    //
    // @param   member  (DynamicForm | String) component (or ID of component) to add to
    //                                          this valuesManager as a member.
    // @visibility external
    // @group members
    // @see method:ValuesManager.addMembers
    //<
    addMember : function (member, fromDataPath) {
        // If passed an ID, assume it's a pointer to the form.
        if (isc.isA.String(member)) member = window[member];

        if (!isc.isA.Canvas(member)) {
            this.logWarn("addMember() passed invalid object: " + this.echo(member) +
                         " - this should be a Canvas instance");
            return;
        }
        if (member.valuesManager != null) member.valuesManager.removeMember(member);

        if (this.members == null) this.members = [];

        // If the member has an explicit, different dataSource specified, log a warning.
        // Different dataSources are a problem, as datasource field properties (including
        // specified validators) will not be reflected in the form.
        // Don't catch the case where the member dataSource is unset, it may be using
        // datapath to pick up the appropriate dataSource field attributes.
        var memberDS = member.getDataSource();
        if (memberDS != null && !fromDataPath && memberDS != this.getDataSource()) {
            this.logWarn("addMember(): mismatched DataSources; new member form " + member +
                         " has dataSource: '" + memberDS.ID +
                         "', valuesManager has DataSource " +
                         (this.getDataSource() != null ? "'"+this.getDataSource().ID+"'" : "[NONE]"));
        }

        // If any member forms are multipart, warn the developer - this implies that
        // they need direct submission.
        if (this.getDataSource() != null && member.isMultipart &&
            member.isMultipart() && member.isMultipart())
        {
            this.logWarn("addMember(): new member form " + member +
                " is flagged as using multipart encoding. Multipart forms require direct form " +
                "submission to transfer uploaded files to the server - any uploaded files from " +
                "this member form will be dropped when saving values from this ValuesManager to " +
                "the server."
            );
        }

        // catch the case where the member is a dataArity singular component but is editing
        // a multiple:true field - in this case we auto attach a selectionComponent if possible
        if (member.dataArity == "single" && member.autoTrackSelection) {
            // Don't clobber a user-specified selectionComponent
            if (member.selectionComponent == null || member._autoSelectionComponent) {
                var dataPath = member.getFullDataPath(),
                    field = dataPath ? this.getDataSourceField(dataPath) : null,
                    newValues = isc.DynamicForm._getFieldValue(dataPath, null, this.values, member, true),
                    multiple = isc.isAn.Array(newValues) || (field && field.multiple);
                if (multiple) {
                    var selectionComponents = this.getMemberForField(dataPath, true);
                    if (selectionComponents && selectionComponents.length > 0) {
                        for (var i = 0; i < selectionComponents.length; i++) {
                            var selectionComponent = selectionComponents[i];
                            if (selectionComponent.dataArity == "multiple") {
                                member.setSelectionComponent(selectionComponent);
                                member._autoSelectionComponent = true;
                                break;
                            }
                        }
                    }
                }
            }
        // also catch the case where a singular item was already added for a multiple:true field
        // and the selection component is added after the fact
        } else {
            var dataPath = member.getFullDataPath(),
                singularComponents = this.getMemberForField(dataPath, true);
            if (singularComponents && singularComponents.length > 0) {
                for (var i = 0; i < singularComponents.length; i++) {
                    if (singularComponents[i].dataArity == "single" &&
                        singularComponents[i].autoTrackSelection &&
                        (singularComponents[i].selectionComponent == null ||
                         singularComponents[i]._autoSelectionComponent == true))
                    {
                        singularComponents[i].setSelectionComponent(member);
                        singularComponents[i]._autoSelectionComponent = true;
                    }
                }
            }
        }

        // We also need to bind to a selectionComponent for components of dataArity "multiple"
        // that are editing a field which is a descendant of a multiple: true field
        if (member.dataArity == "multiple" && member.autoTrackSelection) {
            var dataPath = member.getFullDataPath(),
                isDataPath = dataPath && dataPath.contains(isc.Canvas._$slash);

            if (isDataPath) {
                var elements = dataPath.split(isc.Canvas._$slash);
                // Skip the very last element - that's pointing at this member's dataPath, and
                // we're only interested in ancestors for this purpose
                dataPath = "/";
                for (var i = elements.length - 2; i >= 0; i--) {
                    for (var j = 0; j <= i; j++) {
                        dataPath += elements[j];
                        if (j != i) dataPath += "/";
                    }
                    var field = this.getDataSourceField(dataPath),
                        newValues = isc.DynamicForm._getFieldValue(dataPath, null, this.values, member, true),
                        multiple = isc.isAn.Array(newValues) || (field && field.multiple);
                    if (multiple) break;
                }
            }

            if (multiple) {
                var selectionComponents = this.getMemberForField(dataPath, true);
                if (selectionComponents && selectionComponents.length > 0) {
                    for (var i = 0; i < selectionComponents.length; i++) {
                        var selectionComponent = selectionComponents[i];
                        if (selectionComponent.dataArity == "multiple") {
                            member.setSelectionComponent(selectionComponent);
                            member._autoSelectionComponent = true;
                            break;
                        }
                    }
                }
            }
            // also catch the case where the selection component is added second
            var dataPath = member.getFullDataPath();
            if (dataPath && dataPath != "") {
                var members = this.members;
                for (var i = 0; i < members.length; i++) {
                    if (members[i] == member) continue;
                    if (members[i].dataArity == "single") continue;
                    var otherDataPath = members[i].getFullDataPath();
                    if (otherDataPath && otherDataPath != dataPath
                                      && otherDataPath.startsWith(dataPath))
                   {
                        // Ensure that the target component is not already bound to a better
                        // selectionComponent (ie, one between it and this component in the
                        // hierarchy), or one explicitly specified by the user
                        if (members[i].selectionComponent != null) {
                            if (members[i]._autoSelectionComponent) {
                                var existingDataPath = members[i].selectionComponent.getFullDataPath();
                                if (dataPath.length > existingDataPath.length) {
                                    members[i].setSelectionComponent(member);
                                    members[i]._autoSelectionComponent = true;
                                }
                            }
                        }
                    }
                }
            }
        }

        this.members.add(member);

        member.valuesManager = this;

        // If the member dataSource is null, bind it to the VM's dataSource; this step will be
        // done anyway by DBC.setDataPath(), but setDataSource is a destructive operation (it
        // wipes out the DBC's values to ensure that they cannot mismatch with the new
        // DataSource), and with certain orders of operation, we can end up with components
        // having their data cleared
        if (member.dataSource == null && this.dataSource != null && member.getFields) {
            var fields = isc.isA.DynamicForm(member) ? member._itemsConfig : member.getFields();
            fields = fields || member.getFields();
            var dataPath = member.getFullDataPath();
            var dataSource = this.getDataSource();
            // If the member has a 'dataPath', bind to the nested dataSource to which it refers
            if (dataPath) {
                var dataSource = dataSource.getDataSourceForDataPath(dataPath, true);
            }

            member.setDataSource(dataSource, fields);
        }

        // call _setMemberValues() to update the member data with values defined on this
        // VM.
        // Pass in the 'pickupValues' parameter - on initial add, we want to pick up any
        // values present in the form for which we don't already have values
        // (and warn / replace where there are collisions)
        this._setMemberValues(member, true);

        // set a flag so we know if this was auto-attached as part of setDataPath()
        // This allows us to respect explicitly specified valuesManager if the dataPath changes
        // later, but recalculate derived ones.
        member._valuesManagerFromDataPath = fromDataPath;

        // We have directly manipulated the values object, so we should re remember it.
        this.rememberValues();

    },

    // _setMemberValues - updates the values of a member (form or other dataBoundComponent) based
    // on the valuesManager values.
    // if 'pickupMemberValues' is passed - for cases where the member has existing values
    // (and the valuesManager doesn't) we pick up the field values from the member.
    // [if there are values specified on the vm and the member, the vm values will replace the
    //  members' values]
    // Called
    // - when a member is first added to this valuesManager
    // - from valuesManager.setValues()
    _setMemberValues : function (member, pickupMemberValues) {
        // Ignore inert members. Use the presence of 'getFields' as a rapid check for
        // data-aware components.
        if (member.getFields == null) return;

        // if a field is multiple, the values are expected to be an array.
        // Look at the dataArity of the databound member component to determine
        // whether we should display this array of values in the member
        var memberDataPath = member.getFullDataPath(),
            field = this.getField(memberDataPath),
            newValues = isc.DynamicForm._getFieldValue(memberDataPath, null, this.values, member, true),
            multiple = isc.isAn.Array(newValues) || (field && field.multiple),
            selComponent = member.selectionComponent;

        if (multiple) {
            if (member.dataArity == "single") {
                // Something that edits singular values is being assigned a multiple value.
                // - if a selectionComponent is set this means that this singular component is
                //   coordinating with a dataArity:multiple selectionComponent.  Ignore the
                //   update since the singular component is already observing the multiple
                //   component.
                if (selComponent != null) {

                    // NOTE: The value derived here is *not* used for DynamicForms; forms are
                    // populated item-by-item, using the _getFieldValue() API (whioch knows how
                    // to derive the correct record for a multiple:true field)
                    var record = member._selectionComponentRecordPKs,
                        foundRecord = false;
                    for (var i = 0; i < newValues.length; i++) {
                        if (newValues[i] == record) {
                            foundRecord = true;
                            break;
                        }
                    }
                    if (foundRecord) {
                        newValues = newValues[i];
                    } else {
                        newValues = null;
                        // This is a normal condition, encountered during initial values
                        // setting, so only log it at debug level
                        this.logDebug("Unable to locate selectionComponent's selected record in " +
                                    "_setMemberValues.  Record is: " + isc.Comm.serialize(record));
                    }
                } else {

                    // - if no selectionComponent is present it's tricky to know what the right behavior is
                    //   but default to editing the first record in the array of values.
                    if (isc.isAn.Array(newValues)) newValues = newValues[0];
                }
            }
            // else: multiple component editing multiple values, as expected
        } else {
            // singular value for a multiple component: upconvert to an Array
            if (newValues != null && member.dataArity == "multiple") newValues = [newValues]
        }

        // if the member is not a dynamicForm, we'll just use 'setData()' to apply the appropriate
        // values to the member
        // This will apply values for all fields that match the dataPath of the object
        // (or possibly our top level values object) - differs from logic for forms where
        // we selectively apply values only to fields present in the form

        if (!isc.isA.DynamicForm(member)) {
            if (!member.setData) return;

            var dataPath = member.getFullDataPath(),
                // if pickupMemberValues is unset we don't care what the old values were
                oldValues = pickupMemberValues ? member.getData() : null;

            if (newValues == null) {

                if (pickupMemberValues) {
                    isc.DynamicForm._saveFieldValue(dataPath, null, oldValues, this.values, member, true);
                }
            } else {
                // if oldValues is anything other than
                // null, {} or [], it "has meaning" - log a warning that we're clobbering it rather
                // than picking it up.
                if (pickupMemberValues &&
                    oldValues != null && !isc.isAn.emptyObject(oldValues) &&
                    !isc.isAn.emptyArray(oldValues))
                {
                    this.logInfo("ValuesManager member:" + member.getID() +
                        " has existing values:" + this.echo(oldValues) +
                        ", replacing with values from this valuesManager:" + this.echo(newValues));
                }
                member.setData(newValues);
            }

        } else {

            // for dynamicForms only apply values for which the form is actually displaying
            // items, since we can split the values for a record across multiple forms and we
            // don't want to be maintaining multiple values objects
            var items = member.getItems(),
                hasChanges = false,
                len = items.getLength(),
                undef;
            for (var i = 0; i < len; i++) {
                var item = items[i];



                var itemPath = item.getTrimmedDataPath() || item.getFieldName();

                // Item with no name - just ignore it.
                if (itemPath == null) continue;
                var memberDataPath = member.getFullDataPath(),
                    isAbsolute = itemPath.startsWith(isc.Canvas._$slash),
                    fullFieldPath = isAbsolute ? itemPath
                                        : this._combineDataPaths(memberDataPath, itemPath);

                // Figure out the value for the field

                var newFieldValue = isc.DynamicForm._getFieldValue(fullFieldPath, null,
                                                                   this.values,
                                                                   member, true);

                // for cases where we're looking inside the form - check for the
                // form already having a specified value for the field so we can notify
                // the developer we're clobbering it.

                if (!isAbsolute) {
                    var currentFieldValue = isc.DynamicForm._getFieldValue(fullFieldPath,
                                                           null,
                                                           member.values,
                                                           member, true);

                    if (currentFieldValue !== undef) {
                         this.logInfo("Member form " + member +
                                " has specified value for field '" + fullFieldPath +
                                "' which collides with an already specified value in this " +
                                "ValuesManager. Resetting the value on the member form.");
                    }
                }

                if (newFieldValue !== undef) {

                    member.setValue(itemPath, newFieldValue);
                } else {
                    // explicitly calling 'clearValues()' will cause dynamic defaults to be
                    // re-evaluated
                    if (!pickupMemberValues) member.clearValue(itemPath);
                }

                // Pick the value back up from the item
                // This will re-evaluate defaults on items, and potentially perform other
                // modification such as type-casting, so store the item's value again here
                if (item.shouldSaveValue != false) {


                    var updatedFieldVal = member.getValue(itemPath);

                    if (updatedFieldVal === undef) {
                        isc.DynamicForm._clearFieldValue(fullFieldPath, this.values, member, true);
                     } else {

                         isc.DynamicForm._saveFieldValue(fullFieldPath, null,
                                                         updatedFieldVal,
                                                         this.values, member, true);
                     }

                     hasChanges = true;
                }


                if (item.formValuesChanged && isc.isA.Function(item.formValuesChanged))
                    item.formValuesChanged();

            }

            // fire valuesChanged() on the form, if any values have actually changed
            if (hasChanges) {
                if (member.valuesChanged) member.valuesChanged();
                if (member.markForRedraw) member.markForRedraw();
            }

            if (pickupMemberValues) {
                //>DEBUG
                this._findItemlessFormValues(member);
                //<DEBUG
            }
        }
    },

    //>DEBUG
    // findItemlessFormValues
    // When we first add a DynamicForm to a ValuesManager it may have values for fields
    // with no associated items
    // This is a helper method to find any values from form.getValues() with no associated
    // items.
    // We don't currently add them to this.values - just log a warning and clear them on the form
    // to avoid future confusion
    _findItemlessFormValues : function (form, values, dataPath, itemlessValues, dontWarn) {
        if (values == null) values = form.getValues();
        if (itemlessValues == null) itemlessValues = [];
        for (var prop in values) {
            var fieldID = dataPath ? this._combineDataPaths(dataPath, prop) : prop;
            if (!form.getItem(fieldID)) {
                var value = values[prop];
                if (!isc.isAn.Object(value) || isc.isA.Date(value) || isc.isAn.Array(value)) {
                    itemlessValues.add(fieldID);
                    // clear the value from the form so we avoid future confusion
                    form.clearValue(fieldID);

                    // if we wanted to pick up these values and store them we could do so here
                    /*
                    var fullDataPath = form.dataPath
                                            ? this._combineDataPaths(form.dataPath, fieldID)
                                            : fieldID,
                        currentValue = isc.DynamicForm._getFieldValue(prop, null, this.values),
                        undef;
                    if (currentValue === undef) {
                        isc.DynamicForm._saveFieldValue(fullDataPath, null value, this.values);
                    }
                    */
                } else {

                    // this will recursively iterate into objects until it reaches a dataPath with
                    // an associated item, or an atomic value which we can store directly
                    this._findItemlessFormValues(form, value, dataPath, itemlessValues, true);
                }
            }
        }

        if (!dontWarn && itemlessValues.length > 0) {
            this._itemlessValueWarning(form, itemlessValues);
        }
    },
    //<DEBUG

    //> @method valuesManager.addMembers()
    //  Add multiple new member forms to this valuesManager.
    // @param   members  (Array of DynamicForm) array of forms to add to this valuesManager as members.
    // @group members
    // @see method:ValuesManager.addMember
    // @visibility external
    //<
    addMembers : function (members) {
        if (!isc.isAn.Array(members)) this.addMember(members);
        else {
            for (var i = 0; i< members.length; i++) {
                this.addMember(members[i]);
            }
        }
    },

    //> @method valuesManager.removeMember()
    //  Remove a member form from this valuesManager, so its values are no longer managed
    //  by this instance.
    //  This does not clear the values associated with the form from the valuesManager - they
    //  will still be available via <code>valuesManager.getValues()</code>, but will not be
    //  updated as the form is manipulated.
    // @param   member  (DynamicForm | String)
    //      form (or ID of form) to remove from this valuesManager
    // @group members
    // @see method:ValuesManager.removeMembers()
    // @visibility external
    //<
    removeMember : function (member) {

        if (isc.isA.String(member)) {
            member = isc.Class.getArrayItem(member, this.members);
            if (member == null) return;
        } else if (this.members && !this.members.contains(member)) return;

        if (this.members) this.members.remove(member);
        delete member.valuesManager;
    },

    //> @method valuesManager.removeMembers()
    // Remove multiple member forms from this valuesManager.
    // @param members (Array of DynamicForm) array of forms to remove
    // @group members
    // @see method:ValuesManager.removeMember()
    // @visibility external
    //<
    removeMembers : function (members) {
        if (!isc.isAn.Array(members)) this.removeMember(members);
        else {
            for (var i = 0; i< members.length; i++) {
                this.removeMember(members[i]);
            }
        }
    },


    // ----------------------------------------------------------------------------------------
    // Display
    // ----------------------------------------------------------------------------------------
    // valuesManagers don't usually display their values directly - but support
    // getPrintHTML() so we can build reports from them.
    getPrintHTML : function () {
        var values = this.getValues(),
            printHTML = isc.StringBuffer.create();

        printHTML.append("<TABLE border=1><TR><TD align='center' style='font-weight:bold;'>Field</TD>",
                         "<TD align='center' style='font-weight:bold;'>Value</TD>");
        for (var fieldName in values) {
            printHTML.append("<TR><TD>",fieldName,"</TD><TD>", values[fieldName], "</TD></TR>");
        }
        printHTML.append("</TABLE>");
        return printHTML.toString();
    },

    // recursively find all DataBoundComponents anywhere under any component bound to this VM
    getAllDBCs : function (child) {

        var dbcs = [];

        if (child == null) {
            for (var i = 0; i < this.members.length; i++) {
                dbcs.addAll(this.getAllDBCs(this.members[i]));
            }
            // It doesn't seem likely that this process could have created duplicates (generally
            // speaking, children can only belong to one parent), but let's get rid of any
            // just in case
            var unique = [];
            for (var i = 0; i < dbcs.length; i++) {
                if (!unique.contains(dbcs[i])) unique.add(dbcs[i]);
            }
            return unique;
        }

        if (isc.isA.DataBoundComponent(child)) dbcs.add(child);
        var children = child.children;
        if (!children) return dbcs;

        for (var i = 0; i < children.length; i++) {
            var child = children[i];
            if (isc.isA.DataBoundComponent(child)) dbcs.add(child);
            dbcs.addAll(this.getAllDBCs(child));
        }
        return dbcs;
    }

});



isc.ValuesManager.registerStringMethods ({

     //> @method valuesManager.handleHiddenValidationErrors (A)
    // Method to display validation error messages for a valuesManager when there is not
    // currently visible form item displaying the errors.
    // This will be called when validation fails for<br>
    // - a field in a hidden or undrawn member form<br>
    // - a hidden field in a visible member form<br>
    // - for databound ValuesManagers, a datasource field with specified validators, but not
    //   associated item in any member.<br>
    // Implement this to provide custom validation error handling for these fields.<br>
    // By default hidden validation errors will be logged as warnings in the developerConsole.
    // Return false from this method to suppress that behavior.
    // @param   errors (object) The set of errors returned - this is an object of the form<br>
    //                      &nbsp;&nbsp;<code>{fieldName:errors}</code><br>
    //                      Where the 'errors' object is either a single string or an array
    //                      of strings containing the error messages for the field.
    // @return (boolean) false from this method to suppress that behavior
    // @visibility external
    //<
    handleHiddenValidationErrors:"errors",

    //>    @method valuesManager.submitValues()
    // Optional +link{stringMethod} to fire when +link{valuesManager.submit()} is called
    // on this valuesManager (or any form included in this valuesManager).
    //
    // @param    values    (object)        the valuesManager values
    // @param    valuesManager      (ValuesManager)   the valuesManager being submitted
    // @group submitting
    // @see method:valuesManager.submit()
    // @visibility external
    //<
    submitValues : "values,valuesManager"
});

//!<Deferred
 
 
 
 


isc.ClassFactory.defineClass("RecordScrollbar", "Scrollbar");

isc.RecordScrollbar.addProperties({

    // initial position in the list
    position: 0
});

isc.RecordScrollbar.addMethods({

initWidget : function () {
    this.setData();
    this.Super(this._$initWidget);
    this._initialized = true;
    this.moveThumb();
},

setData : function (data) {

    // undo observation
    if (this.data != null && this.isObserving(this.data, "dataChanged")) {
        this.ignore(this.data, "dataChanged");
    }

    if (data != null) this.data = data;

    // whenever the data changes, reset the thumb
    this.observe(this.data, "dataChanged", "observer.setThumb()");
    this.setThumb();
},

setThumb : function () {
    if (!this._initialized) return;

    // make sure the thumb is above us (we avoid automatically redrawing the thumb, so it can end up
    // underneath the zIndex of the latest draw of the track/buttons)
    if (this.isDrawn() && this.thumb.isDrawn()) this.thumb.moveAbove(this);

    var size = this.trackSize();
    if (this.data != null && this.data.getLength() > 0) {
        size = size / this.data.getLength();
        size = Math.max(this.thumbMinSize, Math.round(size));
    }

    // Calculate size for thumb.
    if (this.vertical) {
        this.thumb.setHeight(size);
    } else {
        this.thumb.setWidth(size);
    }
},

moveThumb : function () {
    var trackSize = this.trackSize() - this.thumbSize();

    var coord;
    if (!this.data || this.data.getLength() == 0) {
        coord = 0;
    } else {
        // NOTE: position is zero-based, length is not
        coord = Math.round((this.position/(this.data.getLength()-1) * trackSize));
    }

    // move the thumb and fire position changed if it moved
    if (this.moveThumbTo(coord + this.trackStart())) this.positionChanged();
},

targetScrollSize : function () {
    return this.data.getLength();
},

targetClipSize : function () {
    return 1;
},

targetPageScrollDelta : function () {
    return Math.min(this.data.getLength(), 10);
},

scrollTargetTo : function (coord) {
    if (coord < 0) coord = 0;
    else if (coord >= this.data.getLength()) coord = this.data.getLength()-1;
    else this.position = Math.round(coord);
    this.moveThumb();
},

targetScrollAmount : function () {
    return this.position;
},

targetScrollDelta : function () {
    return 1;
},

// observable
positionChanged : function () { },

setPosition : function (newPosition) {
    this.position = newPosition;
    this.moveThumb();
},

getCurrentPosition : function () {
    return this.position;
}


//setScrollTarget : function () { }

});






//>@class   MultiRecordForm
//  Component to show multiple records in an editable form component.
// @visibility internal
//<
isc.ClassFactory.defineClass("MultiRecordForm", "HLayout");

isc.MultiRecordForm.addProperties({
    //> @attr multiRecordForm.dataSource (DataSource : null : I)
    // Datasource the dynamicform will be bound to
    //<
    //dataSource: null,

    initialRecord: 0

    //> @attr multiRecordForm.data (List : null : I)
    // ResultSet of data to display
    //<
    //data: null
});

isc.MultiRecordForm.addMethods({

initWidget : function () {

    this._form = this._createForm();
    this._scrollBar = this._createScrollBar();

    this.addMembers([this._form, this._scrollBar]);


    this.observe(this._scrollBar, "positionChanged", "observer.updateForm()");

    this.showRecord(this.initialRecord);
    this.updateForm();
},

// update the data in the form
updateForm : function () {
    //this.logWarn("updateForm: " + (this.data == null ? " null data" :
    //             " will show record: " + this._scrollBar.getCurrentPosition()) +
    //             this.getStackTrace());

    if (this.data == null) return;
    var recordNum = this._scrollBar.getCurrentPosition();
    this._form.setData(this.data.get(recordNum));
},

setDataSource : function (dataSource, fields) {
    this.Super("setDataSource", arguments);
    this._form.setDataSource(dataSource, fields);
},

bindToDataSource : function (fields, componentIsDetail) {
    this.Super("bindToDataSource", arguments);
    this._form.bindToDataSource(fields, componentIsDetail);
},

setData : function (data) {
    // undo old observation
    if (this.data != null && this.isObserving(this.data, "dataChanged")) {
        this.ignore(this.data, "dataChanged");
    }

    this.data = data;

    // update the form if the data changes
    this.observe(this.data, "dataChanged", "observer.updateForm()");

    this._scrollBar.setData(data);
    // move the thumb to the start position
    this.showRecord(this.initialRecord);
    // update the data in the form
    this.updateForm();
},

// scroll to a certain record and display it in the form.
showRecord : function (record) {
    // setPosition ultimately causes us to call updateForm, if the position actually changed
    if (isc.isA.Number(record)) {
        this._scrollBar.setPosition(record);
    } else {
        this._scrollBar.setPosition(this.data.indexOf(record));
    }
},

_createForm : function () {

    return isc.DynamicForm.create({
        ID: this.getID() + "_form",
        autoDraw: false,
        overflow: "auto",
        height: "100%",
        dataSource: this.dataSource
    }, this.formProperties);
},

_createScrollBar : function () {

    return isc.RecordScrollbar.create({
        ID: this.getID() + "_scroll",
        autoDraw: false,
        width: 16,
        height: "100%",
        data: this.data,
        position: this.initialRecord
    });
}

});




//> @class  MultiView
//
// MultiView is a compound UI component, based on the VLayout class, which allows switching
// and data-sharing between hosted +link{DataBoundComponent, data-bound components}.
//
// <ul>
// <li> ListGrid (default), with inline master/detail and editing support</li>
// <li> TileGrid, to display records as tiles, including an image as required</li>
// </ul>
//
// The MultiView manages data binding for its child components.
//
// @implements DataBoundComponent
// @visibility external
//<



// subclass of VLayout so we get stacking behavior
isc.ClassFactory.defineClass("MultiView", "VLayout");

isc.MultiView.addProperties({
    _views: {},  // internal list of views (created instances)

    // the views available to switch between - an array of names of autoChildren which are
    // created at switch-time and stored in this._views
    // FIXME: This hard-coded list should be replaced with one derived by asking the class
    // system for every class that implements the DataBoundComponent interface
    views: ["ListGrid", "TileGrid"
//            ,"Calendar", "DetailViewer", "TreeGrid"
    ],

    ListGridDefaults: {
        _constructor: "ListGrid",
        name: "ListGrid"
    },
    TileGridDefaults: {
        _constructor: "TileGrid",
        name: "TileGrid"
    },
    CalendarDefaults: {
        _constructor: "Calendar",
        name: "Calendar"
    },
    DetailViewerDefaults: {
        _constructor: "DetailViewer",
        name: "DetailViewer"
    },
    TreeGridDefaults: {
        _constructor: "TreeGrid",
        name: "TreeGrid"
    },


    //> @attr   multiView.dataSource  (DataSource or ID : null : IRW)
    // @include dataBoundComponent.dataSource
    //<

    //> @attr   multiView.currentView (String : "ListGrid" : IRW)
    // Class-name of the +link{DataBoundComponent} that represents the current View.  The
    // default is +link{class:ListGrid}.
    //
    // @visibility external
    //<
    currentView: "ListGrid",

    //> @attr multiView.currentDBC (DataBoundComponent : null : R)
    // The actual +link{DataBoundComponent} currently displaying it's UI.
    //
    // @visibility external
    //<

    //> @attr multiView.showFilterEditor    (Boolean : true : IR)
    // When true, show a UI for filtering the data in the current view.
    //
    // @visibility external
    //<
    showFilterEditor: true,

    autoFetchData: true,

    dbcDefaults: {
        showPanelHeader: true,
        tileWidth: 100, tileHeight: 150
    },

    dbcProperties: {}

});


isc.MultiView.addMethods({

//  Initialization
// --------------------------------------------------------------------------------------------

    initWidget : function () {
        this.Super(this._$initWidget);

        this.dataSource = isc.DataSource.getDataSource(this.dataSource);

        this.setShowFilter(this.showFilter);

        // show the initial view, whatever is the default (this.currentView)
        this.setCurrentView(this.currentView, true);
    },



//  DataSource Management
// --------------------------------------------------------------------------------------------

    setDataSource : function (dataSource, fields) {
        this.dataSource = isc.DS.getDataSource(dataSource);

        // bind all the views to the new DataSource
        for (var viewName in this._views) {
            var view = this._views[viewName];

            view.setDataSource(this.dataSource, fields);
            view.title = view.dataSource ? view.dataSource.ID : "";
        }

        if (this.currentDBC) {
            this.currentDBC.setDataSource(this.dataSource, fields);
            this.updateSortList(this.currentDBC);
            this.currentDBC.title = this.dataSource ? this.dataSource.ID : "";
            this.showView(this.currentDBC);
        }
    },

    bindToDataSource : function (fields, componentIsDetail) {
        for (var view in this._views) {
            this._views[view].bindToDataSource(fields, componentIsDetail);
        }
    },


//  Dynamic Filter Creation & Management
// --------------------------------------------------------------------------------------------

    setShowFilter : function (showFilter) {
        this.showFilter = showFilter;

        // hide the filter if it's showing
        if (!showFilter) {
            if (this.filterForm != null) this.filterForm.hide();
            return;
        }

        // just show the filter if it already exists
        if (this.filterForm != null) return this.filterForm.show();

        // create the filter and add it after the toolbar
        this.createFilter();
        this.addMember(this.filterForm);//, this.getMemberNumber(this.toolbar) + 1);
    },


    createFilter : function () {
        this.filterForm = isc.AdvancedFilter.create({
            ID:this.getID() + "_filter",
            autoDraw:false,
            dataSource:this.dataSource,

            // show advanced vs simple mode controls
            dualMode:true,
            currentMode:"simple",

            // have the filter form always filter whatever the current view is
            mv:this,
            filter : function (summary, context) {
                this.Super("filter", [this.mv.currentDBC, context]);
            }
        }, this.filterProperties);
    },



//  Main View Management
// --------------------------------------------------------------------------------------------

    setCurrentView : function (view, firstTime) {
        if (view == null) view = this.currentView;
        this.currentView = view;

        // hide the currently visible view
        if (this.currentDBC) this.currentDBC.hide();

        if (firstTime) {
            var dbc = this._views[view] = this.createAutoChild(
                view,
                this._getCombinedDBCProperties(view)
            );

            this.addSwitcher(dbc);
        } else {
            // create and/or re-init the new DBC with the current one's properties
            this._initView(view);
        }

        // add the view as the last Layout member
        this.currentDBC = this._views[view];
        this.addMember(this.currentDBC);
        this.currentDBC.show();

        this.showView(this.currentDBC);
    },

    _getCombinedDBCProperties : function (view) {
        var props = isc.addProperties({}, this.dbcDefaults, this.dbcProperties,
            {
                dataSource: this.dataSource,
                title: this.title,
                icon: this.icon,
                multiView:this,

                showFilterEditor: this.showFilterEditor,
                autoFetchData: this.autoFetchData,

                // passthrough APIs:
                recordClick:this._dbcRecordClick
            }
        );
        return props;
    },

    _initView : function (view) {
        var dbc = this.getView(view);

        // create the view if it doesn't exist yet
        if (!dbc) {
            var dbc = this._views[view] = this.createAutoChild(
                view,
                this._getCombinedDBCProperties(view)
            );
            this.addSwitcher(dbc);
        }

        if (dbc.panelHeader) dbc.panelHeader.setTitle(this.title);

        // and reconfigure it from the current one
        if (dbc) {
            // set the current view selection
            if (dbc.panelHeader) {
                var members = dbc.panelHeader.controlsLayout.getMembers();
                members.get(0).getField(0).setValue(view);
            }
            dbc.configureFrom(this.currentDBC);
            this.updateSortList(dbc);
        }
    },

    updateSortList : function (dbc) {
        var ds = dbc.getDataSource(),
            fields = ds ? isc.getValues(ds.getFields()) : [],
            panelHeader = dbc.panelHeader,
            members = panelHeader ? panelHeader.controlsLayout.getMembers() : [],
            valueMap={};

        for (var i=1; i<members.length; i++) {
            var item = members.get(i);
            if (item.actionObject.name == "sort") {
                for (var i=0; i<fields.length; i++) {
                    var field = fields.get(i);
                    valueMap[field.name] = field.title;
                }
                item.getField("sortField").setValueMap(valueMap);
                break;
            }
        }
    },

    addSwitcher : function (dbc) {
        if (!dbc.panelHeader) return;
        dbc.panelHeader.controlsLayout.addMember(this.getSwitcherControl(dbc), 0);
    },

    getSwitcherControl : function (dbc) {
        var _multiView = this,
            control = isc.DynamicForm.create({
                autoDraw: true,
                multiView: _multiView,
                fields: [
                    { name: "switcher", title: "View", type: "select", width: 80,
                        defaultValue: _multiView.currentView,
                        valueMap: this.views,

                        changed: function (form, item, value) {
                            _multiView.setCurrentView(value);
                        }
                    }
                ]
            })
        ;
        return control;
    },
    setTitle : function (title) {
        this.title = title;
        var dbc = this.currentDBC;
        if (dbc) {
            dbc.title = this.title;
            if (dbc.panelHeader) {
                dbc.panelHeader.setPanelTitleFromCanvas();
                dbc.panelHeader.redraw();
            }
        }
    },

    showView : function (view) {
        // Don't automatically fetch on 'showView' - this should be handled by an explicit
        // filter call if appropriate or by the autoFetchData setting
        //view.select();
    },

    getView : function (viewName) { return this._views[viewName] },

    // return the selected record in the current view
    getSelectedRecord : function () {
        var dbc = this.currentDBC;
        if (dbc && dbc.getSelectedRecord) return dbc.getSelectedRecord();
    },




// PassThrough APIs
// --------------------------------------------------------------------------------------------
//> @method multiView.recordClick()
// Handles a click on a record.
//
// @param record (ListGridRecord) Clicked record
// @param view (DataBoundComponent) component which received the click event
//
// @visibility external
//<
recordClick:function(record,view) {},

// _dbcRecordClick - assigned directly to our databound components *so fires in their scope!*
// will fire our recordClick handler.

_dbcRecordClick : function () {
    var record;
    if (isc.isA.ListGrid(this)) record = arguments[1];
    else if (isc.isA.TileGrid(this)) record = arguments[2];
    return this.multiView.recordClick(record, this);
}

// regular passthroughs

//> @method     multiView.fetchData()
// @include dataBoundComponent.fetchData()
// @group dataBoundComponentMethods
// @visibility external
//<

//> @method multiView.filterData()
// @include dataBoundComponent.filterData()
// @group dataBoundComponentMethods
// @visibility external
//<

//> @method multiView.fetchRelatedData()
// @include listGrid.fetchRelatedData()
// @group dataBoundComponentMethods
// @visibility external
//<

//> @method multiView.removeSelectedData()
// @include listGrid.removeSelectedData()
// @group dataBoundComponentMethods
// @visibility external
//<

//> @method multiView.anySelected()
// @include dataBoundComponent.anySelected()
// @group dataBoundComponentMethods
// @visibility external
//<

//> @method multiView.getSelection()
// @include dataBoundComponent.getSelection()
// @group dataBoundComponentMethods
// @visibility external
//<

//> @method multiView.selectRecord()
// @include dataBoundComponent.selectRecord()
// @group dataBoundComponentMethods
// @visibility external
//<

//> @method multiView.selectRecords()
// @include dataBoundComponent.selectRecords()
// @group dataBoundComponentMethods
// @visibility external
//<

//> @method multiView.selectAllRecords()
// @include dataBoundComponent.selectAllRecords()
// @group dataBoundComponentMethods
// @visibility external
//<

//> @method multiView.deselectRecord()
// @include dataBoundComponent.deselectRecord()
// @group dataBoundComponentMethods
// @visibility external
//<

//> @method multiView.deselectRecords()
// @include dataBoundComponent.deselectRecords()
// @group dataBoundComponentMethods
// @visibility external
//<

//> @method multiView.deselectAllRecords()
// @include dataBoundComponent.deselectAllRecords()
// @group dataBoundComponentMethods
// @visibility external
//<

});



// Expose some APIs of the currentDBC directly on MultiView
// --------------------------------------------------------------------------------------------

isc.ClassFactory.writePassthroughFunctions(isc.MultiView, "currentDBC",
                                           [
                                           "fetchData", "filterData",
                                           "fetchRelatedData", "removeSelectedData",
                                           "anySelected", "getSelection", "getSelectedRecord",
                                           "selectRecord", "selectRecords", "selectAllRecords",
                                           "deselectRecord", "deselectRecords", "deselectAllRecords"
                                           ]);

isc.MultiView.registerStringMethods({
    recordClick:"record,view"
});






//> @class ColorPicker
// The ColorPicker widget allows the user to select a color from anywhere in the
// color spectrum. It also supports selecting the alpha (opacity) value of the
// color.  The picker supports a simple mode - which allows for one-click selection
// from a standard palette of colors - and a complex mode which allow the user to
// define any conceivable color. It is possible for the user to switch from simple
// mode to complex by interacting with the widget.  In general, the widget provides
// very similar functionality to the color picker dialogs found in graphics packages
// and other desktop software.
// @treeLocation Client Reference/Forms
// @visibility external
//<

if (isc.Window) {

isc.ClassFactory.defineClass("ColorPicker", isc.Window);

isc.ColorPicker.addClassMethods({

//> @classMethod ColorPicker.getSharedColorPicker
// Returns the shared global ColorPicker.
// Many applications will only need one ColorPicker instance; for such use
// cases, it is a good idea to use the shared object for performance reasons.
// <p>
// The optional second parameter to this method indicates whether the shared picker
// should retain the state (mode, color and opacity) it was in last time it was used,
// or revert to defaults.
// Generally, you will want the picker to revert to default state; this gives the
// same user experience as creating a new instance without incurring the overhead.
// However, some use cases will benefit from the picker remembering what the user
// did last time.
// @param properties (Object) Properties to apply to the global ColorPicker object
// @param [keepCurrentState] (boolean) Should we keep the current state?
//                                    If false (or not provided), revert to default state
// @visibility external
//<
getSharedColorPicker : function (properties, keepCurrentState) {

    properties = properties || {};

    if (!isc.isA.ColorPicker(this._globalColorPicker)) {
        this._globalColorPicker = isc.ColorPicker.create(properties);
    } else {
        // Ensure previous colorSelected won't fire even if properties.colorSelected is undefined
        if (properties.colorSelected == null) delete this._globalColorPicker.colorSelected;
        if (properties.colorChanged == null) delete this._globalColorPicker.colorChanged;

        // Set properties so that RichTextEditor can assign this._globalColorPicker.creator to itself (RichTextEditor.js, chooseColor method, line 587)
        this._globalColorPicker.setProperties(properties);
    }


    if (!keepCurrentState) {
        var picker = this._globalColorPicker;

        if (picker._currentPickMode != picker.defaultPickMode) {
            picker._currentPickMode = picker.defaultPickMode;
            if (picker._currentPickMode == 'simple') {
                picker.removeComplexElements();
                if (picker.allowComplexMode) {
                    picker.modeToggleButton.setTitle(this.moreButtonTitle);
                }
            } else {
                if (!picker._rgbForm) {
                    picker.createComplexElements();
                }
                picker.addComplexElements();
                picker.modeToggleButton.setTitle(this.lessButtonTitle);
            }
        }
        // Note we already copied "properties" over to "picker" via the setProperties call
        // above.
        picker.setHtmlColor(picker.defaultColor);
        picker.setOpacity(picker.defaultOpacity);
    }
    return this._globalColorPicker;
}
});

// ColorChooser was an undocumented widget used by the RichTextEditor. This component
// supersedes it.
isc.addGlobal("ColorChooser", isc.ColorPicker);

isc.ColorPicker.addProperties({

// Default Window properties first

autoSize: true,
isModal: true,
autoCenter: true,
autoDraw: false,
showMinimizeButton: false,
closeClick: function () { this.hide(); },

layoutMargin: 2,

canFocus: false,

//> @attr colorPicker.okButton (AutoChild IButton : null : R)
// "OK" button for the ColorPicker
// @visibility external
//<

showOkButton: true,

//>    @attr colorPicker.okButtonConstructor    (Class : IButton : IRWA)
//      The class of the "OK" button. It is intended that you use either IButton or
//      Button - other classes are unlikely to work correctly.
//      @visibility external
//<
okButtonConstructor: isc.IButton,

okButtonDefaults: {
    width: 80,
    autoParent: "buttonLayout",
    click: function () {
        if (this.creator.colorSelected) {
            this.creator.colorSelected(this.creator.getHtmlColor(), this.creator.getOpacity());
        }
        this.creator.hide();
    }
},


//> @attr colorPicker.cancelButton (AutoChild IButton : null : R)
// Cancel button for the ColorPicker
// @visibility external
//<

showCancelButton: true,

//>    @attr colorPicker.cancelButtonConstructor    (Class : IButton : IRWA)
//      The class of the Cancel button. It is intended that you use either IButton or
//      Button - other classes are unlikely to work correctly.
//      @visibility external
//<
cancelButtonConstructor: isc.IButton,

cancelButtonDefaults: {
    title: "Cancel",
    width: 80,
    autoParent: "buttonLayout",
    click: function () {
        this.creator.hide();
    }
},

//> @attr colorPicker.modeToggleButton (AutoChild IButton : null : R)
// "More"/"Less" button for the ColorPicker
// @visibility external
//<

showModeToggleButton: true,

//>    @attr colorPicker.modeToggleButtonConstructor    (Class : IButton : IRWA)
//      The class of the mode toggle button. It is intended that you use either IButton or
//      Button - other classes are unlikely to work correctly.
//      @visibility external
//<
modeToggleButtonConstructor: isc.IButton,

modeToggleButtonDefaults: {
    width: 80,
    autoParent: "buttonLayout",
    click: function () {
        this.creator._togglePickMode();
    }
},

showButtonLayout: true,
buttonLayoutConstructor: "HLayout",
buttonLayoutDefaults: {
    autoParent: "contentLayout"
},

//> @attr colorPicker.defaultColor (text : #808080 : IR)
// The default color. This is the color that is selected when the picker first loads
// @visibility external
//<
defaultColor: "#808080",

//> @attr colorPicker.colorButtonSize (number : 20 : IR)
// Width and height of the basic color boxes (they are always square, and they are
// all the same size).
// @visibility external
//<
colorButtonSize: 20,

//> @attr colorPicker.colorButtonBaseStyle (String : "ColorChooserCell" : IR)
// Base CSS style applied to the basic color boxes
// @visibility external
//<
colorButtonBaseStyle : "colorChooserCell",

//> @attr colorPicker.colorArray (array : [...]  : IR)
// Array containing 40 HTML color strings, which will be shown as the basic color
// selection boxes.
// @visibility external
//<


colorArray: [

    "#000000",
    "#996100",
    "#636300",
    "#006300",
    "#006366",
    "#000080",
    "#636399",
    "#636363",

    "#800000",
    "#FF6600",
    "#808000",
    "#8000FF",
    "#008080",
    "#0000FF",
    "#666699",
    "#808080",

    "#FF0000",
    "#FF9900",
    "#99CC00",
    "#639966",
    "#63CCCC",
    "#6366FF",
    "#800080",
    "#999999",

    "#FF00FF",
    "#FFCC00",
    "#FFFF00",
    "#00FF00",
    "#00FFFF",
    "#00CCFF",
    "#996366",
    "#C0C0C0",

    "#FF99CC",
    "#FFCC99",
    "#FFFF99",
    "#CCFFCC",
    "#CCFFFF",
    "#99CCFF",
    "#CC99FF",
    "#FFFFFF"

],

//> @attr colorPicker.swatchWidth (number : 170 : IR)
// Displayed width of the color swatch image. The default width is approximately
// that used by the Windows&#174; XP color picking window
// @visibility external
//<
swatchWidth:170,
//> @attr colorPicker.swatchHeight (number : 170 : IR)
// Displayed height of the color swatch image. The default height is approximately
// that used by the Windows&#174; XP color picking window
// @visibility external
//<
swatchHeight:170,

//> @attr colorPicker.lumStep (number : 4 : IR)
// The Luminosity bar shows the selected color tone at numerous levels of brightness,
// from black to white. It is implemented as a stack of isc.Canvas objects. This attribute
// determines the height of each of those canvases.
//<
lumStep:4,           // The size of the steps in the Lum indicator

//> @attr colorPicker.lumWidth (number : 15 : IR)
// Width of the Luminosity bar
// @visibility external
//<
lumWidth:15,       // The width of the Lum indicator

//> @attr colorPicker.supportsTransparency (Boolean : true : IR)
// Determines whether to show the opacity slider. This allows the user to select colors
// with an alpha element (ie, semi-transparent colors). If this attribute is set to false,
// no opacity slider is shown, and all colors are completely opaque.
// @visibility external
//<
supportsTransparency:true,

//> @attr colorPicker.opacityText (String : "Lorem ipsum dolor sit amet, consectetuer adipiscing elit." : IR)
// The text to show underneath the selected color box, so that it can
// be seen through semi-transparent colors. If you do not want such text, set
// this value to blank. This value is irrelevant if
// +link{ColorPicker.supportsTransparency} is false.
// @visibility external
//<
opacityText: "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.",

//> @attr colorPicker.swatchImageURL (String : "[SKIN]ColorPicker/spectrum.png" : IR)
// The location of the color swatch image file
// @visibility external
//<
swatchImageURL: "[SKIN]ColorPicker/spectrum.png",

//> @attr colorPicker.crosshairImageURL (String : "[SKIN]ColorPicker/crosshair.png" : IR)
// The location of the crosshair image file
// @visibility external
//<
crosshairImageURL: "[SKIN]ColorPicker/crosshair.png",

//> @attr colorPicker.lessButtonTitle (String : "<< Less" : IR)
// The title for the button that switches to a less complex view.
// @group i18nMessages
// @visibility external
//<
lessButtonTitle: "<< Less",

//> @attr colorPicker.moreButtonTitle (String : "More >>" : IR)
// The title for the button that switches to a more complex view.
// @group i18nMessages
// @visibility external
//<
moreButtonTitle: "More >>",

//> @attr colorPicker.basicColorLabel (String : "Basic Colors:" : IR)
// The label shown above the basic color blocks.
// @group i18nMessages
// @visibility external
//<
basicColorLabel: "Basic Colors:",

//> @attr colorPicker.selectedColorLabel (String : "Selected Color" : IR)
// The label shown next to the selected color box.
// @group i18nMessages
// @visibility external
//<
selectedColorLabel: "Selected Color:",

//> @attr colorPicker.opacitySliderLabel (String : "Opacity" : IR)
// The label shown next to the opacity slider. Ignored if
// +link{ColorPicker.supportsTransparency} is false.
// @group i18nMessages
// @visibility external
//<
opacitySliderLabel: "Opacity:",

//> @attr colorPicker.defaultOpacity (number : 100 : IR)
// The initial opacity value for the component, as a percentage value between 0 and 100
// @visibility external
//<
defaultOpacity: 100,

//> @attr colorPicker.redFieldTitle (String : "Red" : IR)
// The title for the 'Red' field in the complex chooser.
// @group i18nMessages
// @visibility external
//<
redFieldTitle: "Red",

//> @attr colorPicker.redFieldPrompt (String : "The Red component of the selected color" : IR)
// The text to show when the mouse hovers over the 'Red' field in the complex chooser.
// @group i18nMessages
// @visibility external
//<
redFieldPrompt: "The Red component of the selected color",

//> @attr colorPicker.greenFieldTitle (String : "Green" : IR)
// The title for the 'Green' field in the complex chooser.
// @group i18nMessages
// @visibility external
//<
greenFieldTitle: "Green",

//> @attr colorPicker.greenFieldPrompt (String : "The Green component of the selected color" : IR)
// The text to show when the mouse hovers over the 'Green' field in the complex chooser.
// @group i18nMessages
// @visibility external
//<
greenFieldPrompt: "The Green component of the selected color",

//> @attr colorPicker.blueFieldTitle (String : "Blue" : IR)
// The title for the 'Blue' field in the complex chooser.
// @group i18nMessages
// @visibility external
//<
blueFieldTitle: "Blue",

//> @attr colorPicker.blueFieldPrompt (String : "The Blue component of the selected color" : IR)
// The text to show when the mouse hovers over the 'Blue' field in the complex chooser.
// @group i18nMessages
// @visibility external
//<
blueFieldPrompt: "The Blue component of the selected color",

//> @attr colorPicker.htmlFieldTitle (String : "HTML" : IR)
// The title for the 'HTML' field in the complex chooser.
// @group i18nMessages
// @visibility external
//<
htmlFieldTitle: "HTML",

//> @attr colorPicker.htmlFieldPrompt (String : "The selected color&#39;s HTML coding" : IR)
// The text to show when the mouse hovers over the 'HTML' field in the complex chooser.
// @group i18nMessages
// @visibility external
//<
htmlFieldPrompt: "The selected color&#39;s HTML coding",

//> @attr colorPicker.hueFieldTitle (String : "Hue" : IR)
// The title for the 'Hue' field in the complex chooser.
// @group i18nMessages
// @visibility external
//<
hueFieldTitle: "Hue",

//> @attr colorPicker.hueFieldPrompt (String : "The Hue (base tone) of the selected color" : IR)
// The text to show when the mouse hovers over the 'Hue' field in the complex chooser.
// @group i18nMessages
// @visibility external
//<
hueFieldPrompt: "The Hue (base tone) of the selected color",

//> @attr colorPicker.satFieldTitle (String : "Sat" : IR)
// The title for the 'Sat' field in the complex chooser.
// @group i18nMessages
// @visibility external
//<
satFieldTitle: "Sat",

//> @attr colorPicker.satFieldPrompt (String : "The Saturation (color purity) of the selected color" : IR)
// The text to show when the mouse hovers over the 'Saturation' field in the complex chooser.
// @group i18nMessages
// @visibility external
//<
satFieldPrompt: "The Saturation (color purity) of the selected color",

//> @attr colorPicker.lumFieldTitle (String : "Luminosity" : IR)
// The title for the 'Luminosity' field in the complex chooser.
// @group i18nMessages
// @visibility external
//<
lumFieldTitle: "Lum",

//> @attr colorPicker.lumFieldPrompt (String : "The Luminosity (brightness) of the selected color" : IR)
// The text to show when the mouse hovers over the 'Luminosity' field in the complex chooser.
// @group i18nMessages
// @visibility external
//<
lumFieldPrompt: "The Luminosity (brightness) of the selected color",

//> @attr colorPicker.okButtonTitle (String : "OK" : IR)
// The title for the 'OK' button.
// @group i18nMessages
// @visibility external
//<
okButtonTitle: "OK",

//> @attr colorPicker.cancelButtonTitle (String : "Cancel" : IR)
// The title for the 'Cancel' button.
// @group i18nMessages
// @visibility external
//<
cancelButtonTitle: "Cancel",

//> @attr colorPicker.autoPosition (Boolean : true : IR)
// If true, causes the ColorPicker to appear near where the mouse was last clicked.
// If false, the ColorPicker is centered on first show; depending on the value of
// +link{autoCenterOnShow}, it either reappears wherever it was last shown after hide/show(),
// or centered regardless of where it was last shown.
// @see ColorPicker.autoCenterOnShow
// @visibility external
//<
autoPosition: true,

//> @attr colorPicker.autoCenterOnShow (Boolean : true : IR)
// If +link{autoPosition} is false, this property controls whether to automatically center the
// colorPicker every time it is reshown with the show() method.
// @see ColorPicker.autoPosition
// @visibility external
//<
autoCenterOnShow: true,

//> @attr colorPicker.defaultPickMode (String : "simple" : IR)
// The ColorPicker can operate in either a "simple" mode (where it displays just the
// 40 basic colors and allows the user to click one), or a "complex" mode (where the
// user can specify a color from anywhere in the spectrum, with an optional alpha
// element). The defaultPickMode attribute specifies which of these two modes is
// in force when the picker first loads.
// @see ColorPicker.allowComplexMode()
// @visibility external
//<
defaultPickMode: "simple",

//> @attr colorPicker.allowComplexMode (Boolean : true : IR)
// Should the "complex" mode be allowed for this ColorPicker?
// If false, no "More" button is shown on the simple picker
// @visibility external
//<
allowComplexMode: true,

_updateColor: true
});

isc.ColorPicker.addMethods({

// Override show() to position near the mouse
show : function () {
    // place the picker immediately adjacent to the mouse pointer, keeping it onscreen
    if (this.autoPosition) {
        this.autoCenter = false;
        var event = isc.EH.getLastEvent();
        this.placeNear(event.x, event.y);
    } else {
        // Looks like DnD unsets autoCenter - that's great for the general case, but we need
        // the option to re-center every time after a hide/show.
        if (this.autoCenterOnShow) this.autoCenter = true;
    }

    this.Super("show", arguments);
},

// Check that the ColorPicker remains onscreen when it is displayed in complex mode
redraw : function () {
    this.Super("redraw", arguments);
    // Assure that it is onscreen
    if (this.autoPosition) {
        this.autoCenter = false;
        this.placeNear(this.left, this.top);
    }
},

// the window title is in the language packs as "selectTitle", but we want to also support
// title being set directly - make title null by default and then set a default selectTitle
// value - at init, update title to selectTitle if it isn't set
title: null,
selectTitle: "Select a Color",
initWidget : function () {
    this.title = this.title || this.selectTitle;

    this._currentPickMode = this.defaultPickMode;
    this.basicColorLayout = isc.VLayout.create({
        autoDraw: false
    });
    for (var i = 0; i < 5; i++) {
        var wk = isc.HLayout.create({
                autoDraw: false,
                layoutBottomMargin: 1,
                membersMargin: 1,
                height: this.colorButtonSize
        });
        for (var j = 0; j < 8; j++) {
            var wk2 = isc.StatefulCanvas.create({
                autoDraw: false,
                width: this.colorButtonSize,
                height: this.colorButtonSize,
                overflow: "hidden",
                title: "",
                backgroundColor: this.colorArray[i*8+j],
                baseStyle: this.colorButtonBaseStyle,
                showRollOver: true,
                picker: this,
                click: function () {
                    this.picker.setHtmlColor(this.backgroundColor);
                    if (this.picker._currentPickMode == "simple") {
                        this.picker._oneClickColorSelected(this.backgroundColor);
                    }
                }
            });
            wk.addMember(wk2);

        }
        this.basicColorLayout.addMember(wk);
    }

    this.leftHandLayout = isc.VLayout.create({autoDraw:false});

    this.leftHandLayout.addMember(this.basicColorLayout);

    this.innerContentLayout = isc.HLayout.create({
         autoDraw:false,
         align: "center",
         members: [this.leftHandLayout]
    });

    this.contentLayout = isc.VLayout.create({
        autoDraw:false,
        members: [ this.innerContentLayout ]
    });
    this.addItem(this.contentLayout);
    if (this._currentPickMode == "simple") {
        this.showOkButton = false;
        if (! this.allowComplexMode) {
            this.showModeToggleButton = false;
        }
    } else {
        this.showModeToggleButton = false;
    }

    this.addAutoChild("buttonLayout");
    this.addAutoChild("okButton", {title: this.okButtonTitle});
    this.addAutoChild("cancelButton", {title: this.cancelButtonTitle});
    this.addAutoChild("modeToggleButton", {title: this.moreButtonTitle});

    if (this._currentPickMode == "complex") {
            this.createComplexElements();
            this.addComplexElements();
        }

        this.setHtmlColor(this.defaultColor);
        this._setLumVals();
        this.setOpacity(this.defaultOpacity);

        this.Super("initWidget", arguments);
},


createComplexElements : function () {

    if (this._currentPickMode != 'complex') {
        return;
    }

    this._rgbForm = isc.DynamicForm.create({
        autoDraw: false,
        cellPadding:1,
        padding: 10,
        width:65,
        fields: [
            {name: "pickerRedVal", title:this.redFieldTitle, type: "text",
             width: "40", defaultValue: this._pickedRed,
             prompt: this.redFieldPrompt,
             picker: this,
             changed: function (form,item,value) { this.picker.setRed(value); } },

            {name: "pickerGrnVal", title:this.greenFieldTitle, type: "text",
             width: "40", defaultValue: this._pickedGrn,
             prompt: this.greenFieldPrompt,
             picker: this,
             changed: function (form,item,value) { this.picker.setGreen(value); } },

            {name: "pickerBluVal", title:this.blueFieldTitle, type: "text",
             width: "40", defaultValue: this._pickedBlu,
             prompt: this.blueFieldPrompt,
             picker: this,
             changed: function (form,item,value) { this.picker.setBlue(value); } },

            {name: "pickerHtmlVal", title:this.htmlFieldTitle, type: "text",
             width: "65", defaultValue: this._pickedHtml,
             prompt: this.htmlFieldPrompt,
             picker: this,
             changed: function (form,item,value) { this.picker.setHtmlColor(value); } }
        ]

    });

    this._hslForm = isc.DynamicForm.create({
        autoDraw: false,
        cellPadding:1,
        padding: 10,
        width:65,
        fields: [
            {name: "pickerHueVal", title:this.hueFieldTitle, type: "text",
             width: "40", defaultValue: this._pickedHue,
             prompt: this.hueFieldPrompt,
             picker: this,
             changed: function (form,item,value) { this.picker.setHue(value); } },

            {name: "pickerSatVal", title:this.satFieldTitle, type: "text",
             width: "40", defaultValue: this._pickedSat,
             prompt: this.satFieldPrompt,
             picker: this,
             changed: function (form,item,value) { this.picker.setSaturation(value); } },

            {name: "pickerLumVal", title:this.lumFieldTitle, type: "text",
             width: "40", defaultValue: this._pickedLum,
             prompt: this.lumFieldPrompt,
             picker: this,
             changed: function (form,item,value) { this.picker.setLuminosity(value); } }
        ]

    });

    this._crossHair = isc.Img.create({
        autoDraw: false,
        imageWidth: 16, imageHeight: 16, src: this.crosshairImageURL,
        width: 16, height: 16, imageType: "normal",
        canDrag: true,
        canDrop: true,
        dragAppearance: "target",
        picker: this,
        dragMove: function () {
            this.picker._dragging = true;
            this.picker._crosshairMoved(
                this.parentElement.getOffsetX(),
                this.parentElement.getOffsetY());
        }
    });

    this._colorBox = isc.Canvas.create({
        autoDraw: false,
        width: 100,
        height: 40,
        backgroundColor: this.getHtmlColor()
    });

    this._opacityBox = isc.Canvas.create({
        autoDraw: false, width: 60, height: 40,
        overflow: "hidden",
        border: "1px black solid",
        contents: this.opacityText,
        children: [
            this._colorBox
        ]
    });

    this._lumVals = isc.VStack.create({
        lumWidth: 15, height: this.swatchHeight, margin: 5, border: "1px solid black"
    });

    for (var i = 0; i < this.swatchHeight/this.lumStep; i++) {
        this._lumVals.addMember(isc.Canvas.create({
            width: this.lumWidth, height: this.lumStep,
            margin: 0, padding: 0, overflow: "hidden"
        }));
    }

    this._lumSlider = isc.Slider.create({
        minValue: 0,
        maxValue: 240,
        numValues: 240,
        margin: 5,
        length: this.swatchHeight,
        width: 10,
        showTitle: false,
        showValue: false,
        showRange: false
    });

    if (this.supportsTransparency) {

        this._opacitySlider = isc.Slider.create({
            autoDraw: false,
            vertical: false,
            margin: 5,
            minValue: 0,
            maxValue: 100,
            numValues: 100,
            length: 100,
            height: 12,
            width: 100,
            thumbThickWidth: 15,
            thumbThinWidth: 10,
            showTitle: false,
            showValue: false,
            showRange: false,
            value: 100
        });
        this._opacityLayout = isc.HLayout.create({
                                autoDraw: false,
                                layoutLeftMargin: 5,
                                layoutRightMargin: 5,
                                membersMargin: 5,
                                members: [
                                    isc.Label.create({
                                        autoDraw: false,
                                        margin: 5,
                                        contents: this.opacitySliderLabel,
                                        width: this.swatchWidth - 105, height: 10}),

                                    this._opacitySlider
                                ]
                            });
    }

    this._rightHandLayout = isc.VLayout.create({
        autoDraw: false,
        layoutLeftMargin: 5,
        layoutRightMargin: 5,
        membersMargin: 5,
        members: [
            isc.HLayout.create({
                autoDraw: false,
                height: this.swatchHeight,
                members: [
                    isc.Img.create({
                        autoDraw: false,
                        margin: 5,
                        // Note: width and height have 12 added to them here, to allow
                        // for the 5px margin and 1px border around the image
                        width: this.swatchWidth+12, height: this.swatchHeight+12,
                        src: this.swatchImageURL,
                        overflow: "hidden",
                        border: "1px black solid",
                        picker: this,
                        click: function () {
                            this.picker._crosshairMoved(this.getOffsetX(), this.getOffsetY());
                        },

                        children: [
                            this._crossHair
                        ]
                    }),
                    this._lumVals,
                    this._lumSlider
                ]
            }),
            isc.HLayout.create({
                autoDraw: false,
                layoutLeftMargin: 5,
                layoutRightMargin: 5,
                membersMargin: 5,
                members: [
                    isc.Label.create({
                        autoDraw: false,
                        margin: 5,
                        contents: this.selectedColorLabel,
                        width: this.swatchWidth - 63, height: 15}),

                    this._opacityBox
                 ]
            })
        ]
    });

    if (this._lumSlider) this.observe(this._lumSlider, "valueChanged", "observer._lumSliderChanged()");
    if (this._opacitySlider) this.observe(this._opacitySlider, "valueChanged", "observer._opSliderChanged()");

},

initComplexElements : function () {
    this._lumSlider.setValue(this._pickedLum);
    this._setLumVals();
    this._positionCrossHair(this._pickedHue, this._pickedSat);
    // run through setHtmlColor to update the RGB and HTML forms
    if (this._pickedHtml) this.setHtmlColor(this._pickedHtml);

    this._colorBox.setBackgroundColor(
        isc.ColorUtils.hslToHtml( this._pickedHue,
                                  this._pickedSat,
                                  this._pickedLum ) );
    if (this.supportsTransparency) {
        this._colorBox.setOpacity(this._pickedOpacity);
        this._opacitySlider.setValue(this._pickedOpacity);
    }
},

addComplexElements : function () {

    if (this._currentPickMode != 'complex') {
        return;
    }

    this.showOkButton = true;
    this.setAutoChild("okButton", {title: this.okButtonTitle});

    this.basicLabel = isc.Label.create({
            autoDraw: false,
            margin: 5,
            contents: this.basicColorLabel,
            width: 100, height: 15
        });

    this.formLayout = isc.HLayout.create({
            autoDraw: false,
            members: [
                this._rgbForm,
                this._hslForm
            ]
    });

    this.leftHandLayout.addMember(this.basicLabel, 0);
    this.leftHandLayout.addMember(this.formLayout);

    if (this.supportsTransparency) {
        this._rightHandLayout.addMember(this._opacityLayout);
    }

    this.innerContentLayout.addMember(this._rightHandLayout);

    // Initialise the complex elements
    this.initComplexElements();

},


removeComplexElements : function () {

    if (this._currentPickMode == 'complex') {
        return;
    }

    this.showOkButton = false;
    this.setAutoChild("okButton");

    if (this.formLayout) {
        this.leftHandLayout.removeMembers([this.basicLabel, this.formLayout]);

        this.innerContentLayout.removeMember(this._rightHandLayout);
    }

},

//> @method colorPicker.setSupportsTransparency
// Set the +link{supportsTransparency} flag.
// @param transparencyFlag (boolean) Set to true to enable transparency/opacity
// @visibility external
//<
setSupportsTransparency : function (transparencyFlag) {
    this.supportsTransparency = transparencyFlag;
    if (this._currentPickMode == 'complex') {
        if (this.supportsTransparency) {
            this._rightHandLayout.addMember(this._opacityLayout);
        } else {
            this._rightHandLayout.removeMember(this._opacityLayout);
        }
    }
},

//> @method colorPicker.getRed()
// Returns the Red element of the currently-selected color, as an integer from 0-255
// @see ColorPicker.setRed()
// @return (int) red color component
// @visibility external
//<
getRed : function () {
    return this._pickedRed;
},

//> @method colorPicker.getGreen()
// Returns the Green element of the currently-selected color, as an integer from 0-255
// @see ColorPicker.setGreen()
// @return (int) green color component
// @visibility external
//<
getGreen : function () {
    return this._pickedGrn;
},

//> @method colorPicker.getBlue()
// Returns the Blue element of the currently-selected color, as an integer from 0-255
// @see ColorPicker.setBlue()
// @return (int) blue color component
// @visibility external
//<
getBlue : function () {
    return this._pickedBlu;
},

//> @method colorPicker.getHue()
// Returns the Hue of the currently-selected color, as an integer from 0-239
// @see ColorPicker.setHue()
// @return (int) hue value
// @visibility external
//<
getHue : function () {
    return this._pickedHue;
},


//> @method colorPicker.getSaturation()
// Returns the Saturation of the currently-selected color, as an integer from 0-240
// @see ColorPicker.setSaturation()
// @return (int) saturation value
// @visibility external
//<
getSaturation : function () {
    return this._pickedSat;
},


//> @method colorPicker.getLuminosity()
// Returns the Luminosity (brightness) of the currently-selected color, as an
// integer from 0-240
// @see ColorPicker.setLuminosity()
// @return (int) luminosity value
// @visibility external
//<
getLuminosity : function () {
    return this._pickedLum;
},


//> @method colorPicker.getHtmlColor()
// Returns the currently-selected color, in HTML color representation form, as a string.
// HTML color representation is a hash sign, followed by the red, green and blue elements
// of the color in 2-digit hex form - for example "#F17F1D"
// @see ColorPicker.setHtmlColor()
// @return (String) HTML color value
// @visibility external
//<
getHtmlColor : function () {
    return this._pickedHtml;
},


//> @method colorPicker.getOpacity()
// Returns the opacity of the currently-selected color, as an integer from 0-100. If
// opacity is switched off, this is always 100.
// @return (int) opacity value
// @visibility external
//<
getOpacity : function () {
    return this._pickedOpacity;
},

//> @method colorPicker.setRed()
// Sets the Red element of the selected color
// @param newValue (Number) An integer between 0 and 255
// @see ColorPicker.getRed()
// @visibility external
//<
setRed : function (val) {
    if (val < 0) this._pickedRed = 0;
    else if (val > 255) this._pickedRed = 255;
    else this._pickedRed = val/1;

    if (this._currentPickMode == 'complex') {
        this._rgbForm.setValue("pickerRedVal", this._pickedRed);
    }
    if (this._updateColor === true)
        this._changeColor('rgb');
},


//> @method colorPicker.setGreen
// Sets the Green element of the selected color
// @param newValue (Number) An integer between 0 and 255
// @see ColorPicker.getGreen()
// @visibility external
//<
setGreen : function (val) {
    if (val < 0) this._pickedGrn = 0;
    else if (val > 255) this._pickedGrn = 255;
    else this._pickedGrn = val/1;

    if (this._currentPickMode == 'complex') {
        this._rgbForm.setValue("pickerGrnVal", this._pickedGrn);
    }
    if (this._updateColor === true)
        this._changeColor('rgb');
},

//> @method colorPicker.setBlue()
// Sets the Blue element of the selected color
// @param newValue (Number) An integer between 0 and 255
// @see ColorPicker.getBlue()
// @visibility external
//<
setBlue : function (val) {
    if (val < 0) this._pickedBlu = 0;
    else if (val > 255) this._pickedBlu = 255;
    else this._pickedBlu = val/1;

    if (this._currentPickMode == 'complex') {
        this._rgbForm.setValue("pickerBluVal", this._pickedBlu);
    }
    if (this._updateColor === true)
        this._changeColor('rgb');
},

//> @method colorPicker.setHue()
// Sets the Hue of the selected color
// @param newValue (Number) An integer between 0 and 239
// @see ColorPicker.getHue()
// @visibility external
//<
setHue : function (val) {
    if (val < 0) this._pickedHue = 0;
    else if (val > 239) this._pickedHue = 239;
    else this._pickedHue = val/1;

    if (this._currentPickMode == 'complex') {
        this._hslForm.setValue("pickerHueVal", this._pickedHue);
    }
    if (this._updateColor === true)
        this._changeColor('hsl');
},

//> @method colorPicker.setSaturation()
// Sets the Saturation of the selected color
// @param newValue (Number) An integer between 0 and 240
// @see ColorPicker.getSaturation()
// @visibility external
//<
setSaturation : function (val) {
    if (val < 0) this._pickedSat = 0;
    else if (val > 240) this._pickedSat = 240;
    else this._pickedSat = val/1;

    if (this._currentPickMode == 'complex') {
        this._hslForm.setValue("pickerSatVal", this._pickedSat);
    }
    if (this._updateColor === true) {
        this._changeColor('hsl');
    }
},

//> @method colorPicker.setLuminosity()
// Sets the Luminosity (brightness) of the selected color
// @param newValue (Number) An integer between 0 and 240
// @see ColorPicker.getLuminosity()
// @visibility external
//<
// additional dontPersist flag: If passed, we reset the luminosity to 50% when
// the user picks a new color via the swatch
// This is useful for the case where the user picks an HTML color value which
// includes luminosity implicitly.
// In this case if the user then picks from the swatch we don't want to retain this
// luminosity setting. This is most obvious if the user picks black (luminosity zero)
// - if we hung onto that setting it'd make subsequent choices in the swatch have no
// effect on the resultant color (still black!)
setLuminosity : function (val, dontPersist) {
    if (val < 0) this._pickedLum = 0;
    else if (val > 240) this._pickedLum = 240;
    else this._pickedLum = val/1;

    this._persistLum = !dontPersist;

    if (this._currentPickMode == 'complex') {
        this._hslForm.setValue("pickerLumVal", this._pickedLum);
    }
    if (this._updateColor === true)
        this._changeColor('hsl');
},

//> @method colorPicker.setHtmlColor()
// Changes the selected color to the one represented by the supplied HTML color
// string. Note that ths method only accepts the parameter if it represents a
// valid color (otherwise it is simply ignored).
// @param newValue (text) A string in HTML color representation format (#RRGGBB)
// @see ColorPicker.getHtmlColor()
// @visibility external
//<
setHtmlColor : function (val) {
    if (isc.ColorUtils.encodingIsValid(val) === true) {
        this._pickedHtml = val.toUpperCase();
        if (this._currentPickMode == 'complex') {
            this._rgbForm.setValue("pickerHtmlVal", this._pickedHtml);
        }
        if (this._updateColor === true)
            this._changeColor('html');
    }
},


//> @method colorPicker.setOpacity()
// Sets the Opacity of the selected color. Ignored if opacity is switched off.
// @param newValue (Number) An integer between 0 and 100
// @see ColorPicker.getOpacity()
// @visibility external
//<
setOpacity : function (val) {
    if (this._currentPickMode == 'complex' && this.supportsTransparency) {
        if (val < 0) this._pickedOpacity = 0;
        else if (val > 100) this._pickedOpacity = 100;
        else this._pickedOpacity = val/1;

        if (this._updateColor === true) {
            this._changeColor('opacity');
        }
    }
},

_changeColor : function (src) {

    if (src == 'rgb') {

        var hsl = isc.ColorUtils.rgbToHsl( this._pickedRed,
                                        this._pickedGrn,
                                        this._pickedBlu );
        this._updateColor = false;
        this.setHue(hsl.h);
        this.setSaturation(hsl.s);
        // pass in the param to forget the luminosity if the user changes positions in
        // the swatch
        this.setLuminosity(hsl.l, true);
        this.setHtmlColor(isc.ColorUtils.rgbToHtml(this._pickedRed,
                                                   this._pickedGrn,
                                                   this._pickedBlu));
        this._updateColor = true;
        // Move the crosshair
        this._positionCrossHair( this._pickedHue,
                                 this._pickedSat);
    } else if (src == 'hsl') {
        if (!this._persistLum) this._pickedLum = 120;
        var rgb = isc.ColorUtils.hslToRgb( this._pickedHue,
                                           this._pickedSat,
                                           this._pickedLum );
        this._updateColor = false;
        this.setRed(rgb.r);
        this.setGreen(rgb.g);
        this.setBlue(rgb.b);
        this.setHtmlColor(isc.ColorUtils.rgbToHtml(this._pickedRed,
                                                   this._pickedGrn,
                                                   this._pickedBlu));
        this._updateColor = true;
        // Move the crosshair, if necessary (if only the Luminosity
        // has changed, the crosshair does not need to move. If we move
        // it anyway, precision loss issues might cause it to choose
        // a pixel in the color swatch adjacent to the one we currently
        // have, rather than that exact pixel.  This is a problem if the
        // user is changing Lum with the slider - as they smoothly slide
        // the value up and down, we get a very annoying "wobbly cursor"
        // effect...)
        if (this._pickedHue != this._savHue || this._pickedSat != this._savSat) {
            this._positionCrossHair( this._pickedHue,
                                      this._pickedSat);
        }
    } else if (src == 'html') {

        var rgb = isc.ColorUtils.htmlToRgb( this._pickedHtml );

        this._updateColor = false;
        this.setRed(rgb.r);
        this.setGreen(rgb.g);
        this.setBlue(rgb.b);

        var hsl = isc.ColorUtils.rgbToHsl(  this._pickedRed,
                                        this._pickedGrn,
                                        this._pickedBlu );
        this.setHue(hsl.h);
        this.setSaturation(hsl.s);
        this.setLuminosity(hsl.l, true);
        this._updateColor = true;
        // Move the crosshair
        this._positionCrossHair( this._pickedHue,
                                 this._pickedSat);
    }


    // and the slider value
    if (this._currentPickMode == 'complex') {
        this._lumSlider.setValue(this._pickedLum);
        this._hslForm.setValue("pickerLumVal", this._pickedLum);
    }

    // Now set the color box - use HSL, though we could use either as they
    // are now the same...
    if (this._currentPickMode == 'complex') {
        this._colorBox.setBackgroundColor(
                            isc.ColorUtils.hslToHtml( this._pickedHue,
                                                      this._pickedSat,
                                                      this._pickedLum ) );
    }

    // Change the luminosity bar (for performance reasons, only do this
    // if the Hue or Sat have changed)
    if (this._pickedHue != this._savHue || this._pickedSat != this._savSat) {
        this._setLumVals();
    }

    // Set the opacity
    if (this._currentPickMode == 'complex' && this.supportsTransparency) {
        this._colorBox.setOpacity(this._pickedOpacity);
        if (this._opacitySlider != null) {
            var slider = this._opacitySlider,
                sliderVal = slider.getValue(),
                newSliderVal = this._pickedOpacity;
            if (newSliderVal === null) newSliderVal = 100;
            if (sliderVal != newSliderVal) this._opacitySlider.setValue(this._pickedOpacity);
        }
    }

    // Save the existing Hue and Saturation - we only want to reposition the
    // crosshair if either actually changes, otherwise we get an annoying
    // wobbling effect when we move the Luminosity slider
    this._savHue = this._pickedHue;
    this._savSat = this._pickedSat;

    if (this.colorChanged) this.colorChanged();
},

// Called when a color box is clicked in simple mode
_oneClickColorSelected : function (color) {
    this.hide();
    if (this.colorSelected) this.colorSelected(color);
},

_positionCrossHair : function (hue, sat) {

    if (this._currentPickMode != 'complex') {
        return;
    }


    if (this._dragging === true) {
        this._dragging = false;
        return;
    }

    var ph = hue / 239.0;
    var ps = sat / 240.0;

    ph *= this.swatchWidth;
    ps = this.swatchHeight - (ps * this.swatchHeight);

    ph = parseInt(ph) - 8;
    ps = parseInt(ps) - 8;

    this._crossHair.setLeft(ph);
    this._crossHair.setTop(ps);

},

_crosshairMoved : function (h, s) {

    h -= 5;  // Account for margin and border. Note that I'm using 5 rather than
    s -= 5;  // 6 here because it gives a more accurate result - maybe SmartClient
             // doesn't include the border...?

    h /= this.swatchWidth;
    s = 1.0 - s/this.swatchHeight;

    this._updateColor = false;  // Just to stop it updating the screen twice
    this.setHue(Math.floor(h * 239.0 + 0.5));
    this._updateColor = true;
    this.setSaturation(Math.floor(s * 240.0 + 0.5));
},

_setLumVals : function () {
    if (this._currentPickMode != 'complex') {
        return;
    }

    for (var i = 0; i < this.swatchHeight/this.lumStep; i++) {
        this._lumVals.members[i].setBackgroundColor(
                        isc.ColorUtils.hslToHtml(
                               this._pickedHue,
                               this._pickedSat,
                               240-(i * 240/(this.swatchHeight/this.lumStep))
                            ));
    }

},

_lumSliderChanged : function () {
    var wk = this._lumSlider.getValue();
    if (this._pickedLum != wk) {
        this.setLuminosity(wk);
    }
},

_opSliderChanged : function () {
    this.setOpacity(this._opacitySlider.getValue());
},

_togglePickMode : function () {
    if (this._currentPickMode == "simple") {
        this._currentPickMode = "complex";
        if (! this._rightHandLayout) {
            this.createComplexElements();
        }
        this.addComplexElements();
        this.modeToggleButton.setTitle(this.lessButtonTitle);
    } else {
        this._currentPickMode = "simple";
        this.removeComplexElements();
        this.modeToggleButton.setTitle(this.moreButtonTitle);
    }
    this.modeToggleButton.setState("");

    // Check that remains onscreen when redrawn
    this.markForRedraw();

}
});


isc.ColorPicker.registerStringMethods({

    //> @method colorPicker.colorChanged
    // Override this method to be kept informed when the ColorPicker changes in real-time
    // (for example, if you need to update your own GUI accordingly). Then use the
    // getXxxx() methods (for example, +link{getBlue,getBlue()} or
    // +link{getLuminosity,getLuminosity()})to obtain current state as required.
    // @see ColorPicker.colorSelected()
    // @visibility external
    //<
    colorChanged : "",

    //> @method colorPicker.colorSelected
    // Override this method to be notified when the user selects a color
    // either by clicking a basic color box in simple mode, or by clicking
    // the OK button in complex mode. It is not intended that client code
    // call this method.
    // @param color (String)    The color selected, in HTML format
    // @param opacity (Number)  The opacity selected, from 0-100.
    // @see ColorPicker.colorChanged()
    // @visibility external
    //<
    colorSelected : "color,opacity"
});


/*-----------------------------------------------------------------------------------*/

// The ColorUtils class contains class methods that are generally useful when you
// are working with colors - for example, conversion routines to convert between
// HTML, RGB and HSL color formats.
//
// Not documenting these for now - probably don't have much general-purpose
// usefulness

isc.ClassFactory.defineClass("ColorUtils", isc.Class).addClassMethods({

//*******************************************************
//  hexToDec
//  Returns the decimal equivalent of the passed-in hex string
//*******************************************************
hexToDec : function (hex) {
    return parseInt(hex, 16);
},

//*******************************************************
//  decToHex
//  Returns the hexadecimal equivalent of the passed-in decimal number
//*******************************************************
decToHex : function (dec) {
    var d = dec/1;
    var h = d.toString(16);
    if (h.length == 1) {
        h = "0" + h;
    }
    return h;
},

//*******************************************************
//  brightness
//  Returns the brightness (luminosity) of the supplied RGB values
//*******************************************************
brightness : function (r, g, b) {
    var hsl = isc.ColorUtils.rgbToHsl(r, g, b);
    return (hsl.l / 240.0);
},

//*******************************************************
//  encodingIsValid
//  Returns true if the supplied string is a valid HTML color
//*******************************************************
encodingIsValid : function (html) {
    return (html.substring(0, 1) == '#' && isc.isA.color(html));
},


//*******************************************************
//  rgbToHtml
//  Converts an RGB triplicate to an HTML color string
//*******************************************************
rgbToHtml : function (r, g, b) {
    var htmlCol = '#' + isc.ColorUtils.decToHex(r) +
                        isc.ColorUtils.decToHex(g) +
                        isc.ColorUtils.decToHex(b);
    return htmlCol;
},

//*******************************************************
//  hslToHtml
//  Converts an HSL triplicate to an HTML color string
//*******************************************************
hslToHtml : function (h, s, l) {
    var rgb = isc.ColorUtils.hslToRgb(h, s, l);
    var htmlCol = '#' + isc.ColorUtils.decToHex(rgb.r) +
                        isc.ColorUtils.decToHex(rgb.g) +
                        isc.ColorUtils.decToHex(rgb.b);
    return htmlCol;
},

//*******************************************************
//  htmlToRgb
//  Converts an HTML color string to an RGB triplicate
//*******************************************************
htmlToRgb : function (htmlString) {
    var r = htmlString.substring(1, 3);
    var g = htmlString.substring(3, 5);
    var b = htmlString.substring(5, 7);
    return {
        r: isc.ColorUtils.hexToDec(r),
        g: isc.ColorUtils.hexToDec(g),
        b: isc.ColorUtils.hexToDec(b)
    };
},

//*******************************************************
//  htmlToHsl
//  Converts an HTML color string to an HSL triplicate
//*******************************************************
htmlToHsl : function (htmlString) {
    var r = htmlString.substring(1, 3);
    var g = htmlString.substring(3, 5);
    var b = htmlString.substring(5, 7);
    return isc.ColorUtils.rgbToHsl( isc.ColorUtils.hexToDec(r),
                                    isc.ColorUtils.hexToDec(g),
                                    isc.ColorUtils.hexToDec(b) );
},

//*******************************************************
//  rgbToHsl
//  Converts an RGB triplicate to an HSL triplicate
//*******************************************************
rgbToHsl : function (r, g, b) {
    var wr = r / 255.0;
    var wg = g / 255.0;
    var wb = b / 255.0;

    var min  = Math.min(Math.min(wr, wg), wb);
    var max  = Math.max(Math.max(wr, wg), wb);
    var delta = max - min;

    var h = 0, s = 0, l = 0;

    l = (max + min) / 2.0;

    if (max == min) {   // ie, a grey shade
        s = 0;
        h = 0;
    } else {
        if (l < 0.5) {
            s = (max - min) / (max + min);
        } else {
            s = (max - min) / (2.0 - max - min)
        }


        // Calculate hue
        if ( wr == max )
            h = ( wg - wb ) / delta;    // between yellow & magenta
        else if( wg == max )
            h = 2 + ( wb - wr ) / delta;    // between cyan & yellow
        else
            h = 4 + ( wr - wg ) / delta;    // between magenta & cyan
    }

    // Scale the results
    h = Math.floor(h * 40 + 0.5);
    if (h < 0) h += 240;
    s = Math.floor(s * 240 + 0.5);
    l = Math.floor(l * 240 + 0.5);

    return { h: h, s: s, l: l};
},

//*******************************************************
//  hslToRgb
//  Converts an HSL triplicate to an RGB triplicate
//  Note that, in keeping with Microsoft's color picking tool,
//  we expect the HSL values to be between 0 and 240 (239 for
//  Hue, because of the way the maths works out).
//*******************************************************
hslToRgb : function (h, s, l) {

    var wh = h / 239.0;
    var ws = s / 240.0;
    var wl = l / 240.0;
    var t1, t2, tr3, tg3, tb3;

    var r = 0, g = 0, b = 0;

    if (ws == 0) {   // ie, a completely neutral grey shade
        r = wl;
        g = wl;
        b = wl;
    } else {

        if (wl < 0.5) {
            t2 = wl * (1.0 + ws);
        } else {
            t2 = (wl + ws) - (wl * ws);
        }

        t1 = (2.0 * wl) - t2;

        tr3 = wh + 0.3333;
        tg3 = wh;
        tb3 = wh - 0.3333;

        if (tr3 < 0) tr3 += 1.0;
        if (tg3 < 0) tg3 += 1.0;
        if (tb3 < 0) tb3 += 1.0;

        if (tr3 > 1) tr3 -= 1.0;
        if (tg3 > 1) tg3 -= 1.0;
        if (tb3 > 1) tb3 -= 1.0;

        if (tr3 * 6.0 < 1)
            r = t1 + (t2-t1) * 6.0 * tr3;
        else if (tr3 * 2.0 < 1)
            r = t2;
        else if (tr3 * 3.0  < 2)
            r = t1 + (t2-t1) * (0.6667 - tr3) * 6.0;
        else
            r = t1;

        if (tg3 * 6.0 < 1)
            g = t1 + (t2-t1) * 6.0 * tg3;
        else if (tg3 * 2.0 < 1)
            g = t2;
        else if (tg3 * 3.0  < 2)
            g = t1 + (t2-t1) * (0.6667 - tg3) * 6.0;
        else
            g = t1;

        if (tb3 * 6.0 < 1)
            b = t1 + (t2-t1) * 6.0 * tb3;
        else if (tb3 * 2.0 < 1)
            b = t2;
        else if (tb3 * 3.0  < 2)
            b = t1 + (t2-t1) * (0.6667 - tb3) * 6.0;
        else
            b = t1;
    }

    // And scale...
    r = Math.floor(r * 255.0 + 0.5);
    g = Math.floor(g * 255.0 + 0.5);
    b = Math.floor(b * 255.0 + 0.5);

    return { r: r, g: g, b: b};
}
});

}







//> @class NestedEditorItem
// Form item which renders a single complex sub-object in an embedded component.  By default,
// the embedded component is a +link{class:DynamicForm}
// @treeLocation Client Reference/Forms/Form Items
// @visibility internal
//<
isc.ClassFactory.defineClass("NestedEditorItem", "CanvasItem");
isc.NestedEditorItem.addProperties({

    shouldSaveValue: true,


    isCriteriaEditor:false,

      //> @attr    nestedEditorItem.editor        (AutoChild DynamicForm : null : [IRW])
    //
    // The editor that will be rendered inside this item.  Unless overridden, the editor will be
    // an instance of +link{class:DynamicForm}. It will be created using the overrideable
    // defaults standard to the +link{group:autoChildren,AutoChild} subsystem - editorConstructor
    // and editorProperties.
    //
    //  @visibility internal
    //<
    editorConstructor: "DynamicForm",
    editorDefaults: {
        itemChanged : function (item, newValue) {
            var values = this.creator.isCriteriaEditor ? this.getValuesAsCriteria()
                            : this.getValues();
            this.creator.updateValue(values);
        }
    }

});

isc.NestedEditorItem.addMethods({
    init : function () {
        this._createEditor();
        this.Super("init", arguments);
    },

    isEditable : function () {
        return true;
    },

    _createEditor: function(){

        var ds;
        var dynProps = {};

        if (this.form.dataSource) { // Should be, otherwise how have we ended up with a complex field?
            ds = isc.DataSource.getDataSource(this.form.dataSource);
            var field = ds.getField(this.name);
            if (field) {
                dynProps.dataSource = ds.getFieldDataSource(field);
            }
        }

        if (this.form && this.form.showComplexFieldsRecursively) {
            dynProps.showComplexFields = true;
            dynProps.showComplexFieldsRecursively = true;
        } else {
            dynProps.showComplexFields = false;
        }

        this.addAutoChild("editor", dynProps);
        this.canvas = this.editor;

    },

    setValue : function(value) {
        this.editor.setValues(value);
    },

    updateValue : function(data) {
        this._updateValue(data);
    },

    _shouldAllowExpressions : function () {
        return false;
    }

});









//> @class NestedListEditorItem
// Form item which renders a list of complex sub-objects in an embedded component.  By default,
// the embedded component is a +link{class:ListEditor}
// @treeLocation Client Reference/Forms/Form Items
// @visibility internal
//<
isc.ClassFactory.defineClass("NestedListEditorItem", "CanvasItem");
isc.NestedListEditorItem.addProperties({

    shouldSaveValue: true,

      //> @attr    nestedListEditorItem.editor        (AutoChild ListEditor : null : [IRW])
    //
    // The editor that will be rendered inside this item.  Unless overridden, the editor will be
    // an instance of +link{class:ListEditor}. It will be created using the overrideable defaults
    // standard to the +link{group:autoChildren,AutoChild} subsystem - editorConstructor and
    // editorProperties.
    //
    //  @visibility internal
    //<
    editorConstructor: "ListEditor",
    editorDefaults: {

        inlineEdit: false,
        height: 155,

        saveRecord : function () {
            if (!this.form.validate()) return false;
            var values = this.form.getValues();

            this.showList();

            if (this.inlineEdit) {
                // User clicked "Save" in "More" mode
                this.grid.setEditValues(this.grid.getEditRow(), values);
            } else {
                if (this.form.saveOperationType == "add") { // new record
                    this.grid.addData(values);
                } else {
                    // Avoid updating via updateData, it requires primary keys
                    isc.addProperties(this.currentRecord, values);
                    this.grid.markForRedraw();
                }

                if (!this.inlineEdit) {
                    this.form.clearValues();
                }

                this.creator.updateValue(this.grid.data);
            }

            return true;
        }
    }

});

isc.NestedListEditorItem.addMethods({
    init : function () {
        this._createEditor();
        this.Super("init", arguments);
    },

    isEditable : function () {
        return true;
    },

    _createEditor : function() {

        var ds;
        var dynProps = {};

        if (this.form.dataSource) { // Should be, otherwise how have we ended up with a complex field?
            ds = isc.DataSource.getDataSource(this.form.dataSource);
            var field = ds.getField(this.name);
            if (field) {
                dynProps.dataSource = ds.getFieldDataSource(field);
            }
        }

        if (this.form && this.form.showComplexFieldsRecursively) {
            dynProps.formProperties = {
                showComplexFields: true,
                showComplexFieldsRecursively: true
            };
            dynProps.gridProperties = {
                showComplexFields: true,
                showComplexFieldsRecursively: true,
                canRemoveRecords: true,
                saveLocally: true,
                data: []
            };
        } else {
            dynProps.formProperties = {
                showComplexFields: false
            };
            dynProps.gridProperties = {
                showComplexFields: false,
                canRemoveRecords: true,
                saveLocally: true
            };
        }

        this.addAutoChild("editor", dynProps);
        this.canvas = this.editor;
    },

    updateValue : function(data) {
        this.editor.setData(data);
        this._updateValue(data);
    },

    setValue : function(data) {
        this.editor.setData(data);
        this.Super("setValue", arguments);
    },

    _shouldAllowExpressions : function () {
        return false;
    }

    // Value comparison needs to be deep for this FormItem - the default one on DyanmicForm
    // only looks one level deep (ie, it compares Arrays and Objects, but it checks for pointer
    // identity on each entry / property)
    // FIXME - Commenting this out for now because something about it is breaking the
    // stripifier or module packager
    /* compareValues(value1, value2) {
        if (value1 == value2) return true;
        if (isc.isA.Date(value1) && isc.isA.Date(value2)) {
            return (Date.compareDates(value1, value2) == 0);
        } else if (isc.isAn.Array(value1) && isc.isAn.Array(value2)) {
            if (value1.length != value2.length) return false;
            for (var i = 0; i < value1.length; i++) {
                if (!this.compareValues(value1[i], value2[i])) return false;
            }
            return true;
        } else if (isc.isAn.Object(value1) && isc.isAn.Object(value2)) {
            if (value1.getKeys().length != value2.getKeys().length) return false;
            for (var key in value1) {
                if (!this.compareValues(value1[key], value2[key])) return false;
            }
            return true;
        }
        return false;
    } */
});






//> @groupDef viewFile
//<

//>    @class ViewFileItem
//
// Item for displaying the contents of "imageFile" fields in DynamicForms.
// <P>
// Displays one of two UIs, according to the value of
// +link{viewFileItem.showFileInline, showFileInline}.  If showFileInline is false, this Item
// displays the View and Download icons and the filename.  Otherwise, it streams the image-file
// and displays it inline.
//
// @group upload
// @treeLocation Client Reference/Forms/Form Items
// @visibility external
//<
isc.ClassFactory.defineClass("ViewFileItem", "CanvasItem");

isc.ViewFileItem.addProperties({

    shouldSaveValue: false,
    colSpan: "*",
    height: 20,
    width: "*",
    overflow: "visible",

    //> @attr viewFileItem.showFileInline    (boolean : null : [IR])
    // Indicates whether to stream the image and display it
    // inline or to display the View and Download icons.
    //
    // @visibility external
    //<

    canvasDefaults: {
        _constructor: "Canvas",
        height: 10, width: "100%"
    },

    isEditable : function () {
        return false;
    },

    init : function () {
        this.addAutoChild("canvas");
        this.Super('init', arguments);
    },

    formValuesChanged : function () {
        this.setCanvasContent(null);
    },

    setValue : function(data) {
        this.setCanvasContent(data);
        this.Super("setValue", arguments);
    },

    setCanvasContent : function (data) {

        var record = this.getFormRecord();


        if ((this.type == "imageFile" || this.type == "viewFile") && this.showFileInline != false) {
            this.canvas.setHeight("*");
            this.canvas.setWidth("*");
            this.canvas.setContents(this.getImageHTML() || "&nbsp;");
        } else {
            if (this.showFileInline == true) { // non-imageFile field
                this.logWarn("setValue(): Unsupported field-type for showFileInline: " +this.type);
            }
            this.canvas.setHeight(20);
            this.canvas.setWidth("*");
            this.canvas.setContents(this.getViewDownloadHTML(data, record) || "&nbsp;");
        }
    },

    getViewDownloadHTML : function (value, record) {

        //if (isc.isA.String(value)) return value;
        if (record == null) return null;

        var form = this.form,
            ds = form.getDataSource(),
            field = ds ? ds.getField(this.name) : null,
            filenameField = (ds ? ds.getFilenameField(this.name) : null) || this.name + "_filename",
            name = record[filenameField],
            // see if the form has a value for the pk-field
            pkFields = ds ? ds.getPrimaryKeyFieldNames() : null,
            missingPkValues = (pkFields == null)
        ;


        if (field && !field.filenameSuppressed && (name == null || isc.isAn.emptyString(name))) {
            return this.emptyCellValue;
        }

        if (pkFields) {
            var values = form.getValues();
            for (var i = 0; i < pkFields.length; i++) {
                var pk = pkFields[i];
                if (isc.DynamicForm._getFieldValue(pk, this, values, form, true) == null) {
                    missingPkValues = true;
                    break;
                }
            }
        }


        if (missingPkValues) {
            // never show view/download if the record has no PK
            return this.emptyCellValue;
        }

        var viewIconHTML = isc.Canvas.imgHTML({
                src: "[SKIN]actions/view.png",
                width: 16,
                height: 16,
                extraCSSText: "cursor:" + isc.Canvas.HAND,
                extraStuff: " onclick='" + this.getID() + ".viewFile()'"
            }),
            downloadIconHTML = isc.Canvas.imgHTML({
                src: "[SKIN]actions/download.png",
                width: 16,
                height: 16,
                extraCSSText: "cursor:" + isc.Canvas.HAND,
                extraStuff: " onclick='" + this.getID() + ".downloadFile()'"
            })
        ;

        return "<nobr>" + viewIconHTML + "&nbsp;" + downloadIconHTML +
            (name ? "&nbsp;" + name : "") + "</nobr>";
    },

    getFormDataSource : function () {
        // get the DS from either the parent form or it's VM
        var ds = this.form.getDataSource() ||
                (this.form.valuesManager ? this.form.valuesManager.getDataSource() : null)
        ;
        return ds;
    },

    getFormRecord : function () {
        // get the data from either the VM or the parent form
        var record = this.form.valuesManager ? this.form.valuesManager.getValues() : null;
        if (!record || isc.isAn.emptyObject(record)) record = this.form.getValues();
        return record;
    },

    getImageHTML : function () {
        var record = this.getFormRecord(),
            field = this.form.getField(this.name),
            urlProperty = this.name + "_imgURL",
            value;

        if (!record || isc.isAn.emptyObject(record)) return;

        var formDS = this.getFormDataSource();
        if (!record[urlProperty] && formDS != null) {
            var dimensions = isc.Canvas.getFieldImageDimensions(field, record);

            value = record[urlProperty] =
                isc.Canvas.imgHTML(formDS.streamFile(record, field.name),
                    dimensions.width, dimensions.height);
        } else
            value = record[urlProperty];

        return value;
    },

    viewFile : function () {
        isc.DS.get(this.getFormDataSource()).viewFile(this.getFormRecord(), this.name);
    },

    downloadFile : function () {
        isc.DS.get(this.getFormDataSource()).downloadFile(this.getFormRecord(), this.name);
    },

    _shouldAllowExpressions : function () {
        return false;
    }
});


isc.defineClass("DOMView", "VLayout").addMethods({
    overflow:"hidden",
    initWidget : function () {
        this.Super(this._$initWidget);

        this.pickForm = isc.DynamicForm.create({
            autoDraw:false,
            height:10,
            cellPadding:0,
            numCols:5,
            colWidths:[90, 90, 40, "*", 40],
            items:[
                { name:"viewType", title:"View Type", type:"select", width:"*",
                  valueMap:{ tree:"Tree", text:"Text", none:"None" },
                  defaultValue:this.viewType,
                  change:"this.form.parentElement.setViewType(value)" },
                { name:"url", title:"URL", type:"text", width:"*" },
                { type:"button", title:"Fetch", width:"*", startRow:false, endRow:false,
                  click:"this.form.parentElement.fetchXML()" }
            ]
        });
        this.addMember(this.pickForm);

        this.textView = isc.DynamicForm.create({
            autoDraw:false,
            overflow: "hidden",
            cellPadding:0,
            items:[
                {showTitle:false, name:"xmlText", type:"textArea", height:"*", width:"*", colSpan: "*",
                 defaultValue:this.xmlText }
            ]
        });
        this.addMember(this.textView);

        this.treeView = isc.DOMGrid.create({
            autoDraw:false,
            wrapCells:true,
            fixedRecordHeights:false,
            showHeader:false
        });
        this.addMember(this.treeView);

        this.setViewType(this.viewType);

        if (this.url) this.setURL(this.url);
        else this.setXMLText(this.xmlText);
    },

    setURL : function (url) {
        this.url = url;
        this.pickForm.setValue("url", url);
        this.fetchXML();
    },
    getURL : function () {
        return this.pickForm.getValue("url");
    },

    viewType: "text",
    setViewType : function (viewType) {
        viewType = viewType || this.pickForm.getValue("viewType");
        this.viewType = viewType;
        this.pickForm.setValue("viewType", viewType);

        // parse the (possibly edited) text
        this.setXMLText(this.textView.getValue("xmlText"));

        if (this.viewType == "none") {
            this.treeView.hide();
            this.textView.hide();
            var height = 21;
            this.setHeight(height);
            // HACK: layouts don't respect programmatic height change before draw
            this._userHeight = height;
            return;
        }

        // HACK: get rid of non-user-specified height that would be acquired if we minimized
        this._userHeight = null;
        if (viewType == "tree") {
            this.textView.hide();
            if (this.xmlDocument) {
                this.treeView.setRootElement(this.xmlDocument.documentElement);
            }
            this.treeView.show();
        } else { // (viewType == "text")
            // no editing could have taken place in the non-Text views, so just switch back
            this.treeView.hide();
            this.textView.show();
        }
    },

    fetchXML : function () {
        isc.xml.loadXML(this.getURL(),
                        this.getID() + "._fetchXMLReply(xmlText)");
    },
    _fetchXMLReply : function (xmlText) {
        this.setXMLText(xmlText);
    },


    getXMLDocument : function () {
        if (this.viewType == "text") {
            // parse the (possibly edited) text
            return isc.xml.parseXML(this.textView.getValue("xmlText"));
        } else {
            return this.xmlDocument;
        }
    },

    setXMLText : function (xmlText) {
        this.xmlText = xmlText;
        this.textView.setValue("xmlText", xmlText);
        if (xmlText != null) {
            this.xmlDocument = isc.xml.parseXML(xmlText);
            this.treeView.setRootElement(this.xmlDocument.documentElement);
        }
    },

    setXMLDocument : function (doc) {
        // TODO: serialize the structure so we can show it in text mode
        this.xmlDocument = doc;
        this.setViewType("tree");
    }

});

isc.defineClass("TransformBuilder", "SectionStack").addMethods({
    styleViewDefaults : {
        xmlText:
'<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"\r' +
'                              xmlns:xs="http://www.w3.org/2001/XMLSchema"\r' +
'                              xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"\r' +
'                              xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"\r' +
'                              xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"\r' +
'                              xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/"\r' +
'>\r' +
'   <!-- get rid of builtin rule to pass through text -->\r' +
'   <xsl:template match="text()"/>\r\r' +
'   <xsl:template match="/">\r' +
'      <anElement/>\r' +
'   </xsl:template>\r' +
'</xsl:stylesheet>\r'
    },
    visibilityMode:"multiple",
    initWidget : function () {
        this.Super(this._$initWidget);

        this.inputView = this.createAutoChild("inputView", null, isc.DOMView);

        this.addSection({
            title : "Input XML",
            autoShow:true,
            items : [ this.inputView ]
        });

        this.styleView = this.createAutoChild("styleView", null, isc.DOMView);

        this.addSection({
            title : "StyleSheet",
            autoShow:true,
            items : [
                this.styleView,
                isc.Button.create({
                    autoDraw:false,
                    resizeable: false,
                    title:"Apply",
                    click : "this.parentElement.doTransform()"
                })
            ]
        });

        this.outputView = this.createAutoChild("outputView", {
            viewType:"tree"
        }, isc.DOMView);

        this.addSection({
            title : "Output",
            ID : "outputSection",
            items : [ this.outputView ]
        });
    },

    doTransform : function () {

        var styleView = this.styleView;
        if (styleView.viewType == "none" &&
            !isc.isAn.emptyString(styleView.getURL()))
        {
            styleView.fetchXML();
            if (!this.isObserving(styleView, "_fetchXMLReply")) {
                this.observe(styleView, "_fetchXMLReply", function () {
                    this._doTransform();
                });
            }

        } else this._doTransform();

    },
    _doTransform : function () {
        var input = this.inputView.getXMLDocument(),
            styleSheet = this.styleView.getXMLDocument();

        this.logWarn("input is: " + this.echoLeaf(input) +
                     " stylesheet: " + this.echoLeaf(styleSheet));

        var output = isc.xml.transformNodes(input, styleSheet);

        this.logWarn("output is: " + this.echoLeaf(output));

        this.outputView.setXMLText(output);
        this.expandSection("outputSection");
    }
});





isc.defineClass("XMLBinder", "SectionStack").addMethods({
    visibilityMode:"multiple",
    protocol:"soap",
    messageStyle:"inputElement",
    initWidget : function () {
        this.Super(this._$initWidget);

        // ---------------------------------------------------------------------------------------
        this.urlForm = isc.DynamicForm.create({
            autoDraw:false,
            numCols:4,
            items:[
                {name:"dataURL", title:"Service URL", type:"selectOther", width:"*", colSpan:3,
                 valueMap:[
                    "http://webservices.amazon.com/onca/xml",
                    "http://webservices.amazon.com/AWSECommerceService/AWSECommerceService.wsdl",
                    "http://api.google.com/search/beta2"
                 ]},
                {name:"protocol", startRow:true, endRow:true, type:"select", width:"*",
                 valueMap:{
                    "getParams" : "Send Data as URL Params (GET)",
                    "postParams" : "Post Data as Form Params (POST)",
                    "soap" : "Post Data as XML Document (SOAP)"
                 }, defaultValue:this.dataProtocol, redrawOnChange:true},

                {name:"soapAction", startRow:true,
                 showIf:"values.dataProtocol == 'soap'"},

                {name:"messageStyle", defaultValue:this.messageStyle, type:"select", width:"*",
                 valueMap:{
                    "inputElement" : "Serialize Data as named XML Element",
                    "template" : "Merge Data with Template"
                 }
                },

                // Element
                {name:"inputElement",
                 showIf:"values.messageStyle == 'inputElement'"},
                {name:"inputElementNamespace",
                 showIf:"values.messageStyle == 'inputElement'"},
                {name:"qualifyAll", title:"Qualify All Tags", type:"boolean",
                 showIf:"values.messageStyle == 'inputElement'"},

                // template
                {name:"messageTemplate", startRow:true,
                 showIf:"values.messageStyle == 'template'",
                 type:"textArea",
                 icons:[{
                    click:"this.parentElement.showTransformBuilder()"
                 }]},

                {type:"button", title:"Send", click:"form.parentElement.send()", startRow:false}
            ]
        });

        this.paramGrid = isc.ListGrid.create({
            autoDraw:false,
            canEdit:true,
            listEndEditAction:"next",
            overflow:"visible",
            bodyOverflow:"visible",
            inherentHeight:true,
            bodyKeyPress : function (event, eventInfo) {
                if (event.keyName == "Delete") {
                    this.data.remove(this.getSelectedRecord());
                    return;
                }
                this.Super("bodyKeyPress", arguments);
            },
            fields:[
                { name:"paramName", title:"Parameter" },
                { name:"paramValue", title:"Value" }
            ]
        });

        this.addSection({
            title:"SendRequest",
            items:[this.urlForm, this.paramGrid],
            autoShow:true
        });

        // ---------------------------------------------------------------------------------------
        this.displayArea = isc.DOMGrid.create({
            autoDraw:false,
            showHeader:false,
            wrapCells:true,
            fixedRecordHeights:false
        });

        this.selectorForm = isc.DynamicForm.create({
            autoDraw:false,
            height:20,
            numCols:3,
            colWidths:[150, "*", 40],
            inherentWidth:false,
            items:[
                { name:"expression", title:"XPath Expression", type:"text", width:"*" },
                //{ type:"submit", title:"Apply", startRow:false }
                { type:"button", title:"Apply", startRow:false, click:"form.handleSubmit()" }
            ],
            handleSubmit : function () {
                this.parentElement.applySelector();
                return false;
            }
        });

        this.addSection({
            title:"Raw Results",
            autoShow:true,
            items:[this.displayArea, this.selectorForm]
        });

        // ---------------------------------------------------------------------------------------
        this.transformGrid = isc.DOMGrid.create({
            autoDraw:false,
            showHeader:false,
            // don't show root because root is just the NodeList returned by selectNodes
            showRoot:false,
            wrapCells:true,
            fixedRecordHeights:false
        });
        this.addSection({
            ID:"selectedResults",
            title:"Selected Results",
            items:[this.transformGrid]
        });

        // ---------------------------------------------------------------------------------------
        this.finalGrid = isc.ListGrid.create({
            autoDraw:false
        });

        this.addSection({
            title:"List Results",
            items:[this.finalGrid]
        });

        // ---------------------------------------------------------------------------------------
        this.setURL(this.url);
        this.setParams(this.params);
    },

    // Setters
    // ---------------------------------------------------------------------------------------
    setURL : function (url) {
        this.urlForm.setValue("dataURL", url);
    },

    setParams : function (params) {
        var records = [];
        for (var param in params) {
            records.add({paramName:param, paramValue:params[param]});
        }
        this.paramGrid.setData(records);
    },

    // XSLT design
    // ---------------------------------------------------------------------------------------
    showTransformBuilder : function () {
        isc.Window.create({
            autoCenter:true,
            width:"80%",
            height:"80%",
            items : [
                isc.TransformBuilder.create({
                    inputViewProperties : {
                        viewType : "tree",
                        xmlText : "<criteria>" +
                            "<sampleField1>sampleValue1</sampleField1>" +
                            "<sampleField2>sampleValue2</sampleField2>" +
                                  "</criteria>"
                    },
                    styleViewProperties : {
                        xmlText : this.urlForm.getValue("messageTemplate")
                    }
                }),
                isc.AutoFitButton.create({
                    title : "Save",
                    click : function () {
                        this.urlForm.setValue("messageTemplate", "");
                    }
                }),
                isc.AutoFitButton.create({
                    title : "Cancel",
                    click : "this.parentElement.parentElement.close()"
                })
            ]
        });
    },

    // XML retrieval
    // ---------------------------------------------------------------------------------------

    send : function () {
        var params = this.paramGrid.getData(),
            fields = {};

        for (var i = 0; i < params.length; i++) {
            var param = params[i];
            fields[param.paramName] = param.paramValue;
        }

        var values = this.urlForm.getValues();
        var ds = isc.DataSource.create(values);

        var request = ds.getServiceInputs({data:fields});
        request.callback = this.getID() + "._sendReply(xmlDoc);";
        isc.xml.sendXMLRequest(request);
    },

    _sendReply : function (xmlDoc) {
        this.rawResults = xmlDoc;
        this.displayArea.setRootElement(xmlDoc.documentElement);
    },

    // XML Transform
    // ---------------------------------------------------------------------------------------
    applySelector : function () {
        var expression = this.selectorForm.getValue("expression"),
            nodes = isc.xml.selectNodes(this.rawResults, expression);

        this.transformGrid.setData(isc.DOMTree.create({
            rootElement : { childNodes : nodes }
        }));

        this.expandSection("selectedResults");
    }


});




// DOMEditor Class
// --------------------------------------------------------------------------------------------
isc.ClassFactory.defineClass("DOMEditor", "DynamicForm");

isc.DOMEditor.addMethods({
    numCols:4,
    items: [
        { type:"header", name:"elementSummary", defaultValue:"[NONE]" },

        // Sizes
        // -------------------------------------------------------------------------
        { type:"section", name:"sizes", defaultValue:"Sizes",
          sectionExpanded:false,
          itemIds:["style.width", "style.height", "width", "height", "boxSizes"]
        },

        { type:"text", name:"style.width", changeOnKeypress:false },
        { type:"text", name:"style.height", changeOnKeypress:false },

        { type:"text", name:"width", title:"element.width", changeOnKeypress:false },
        { type:"text", name:"height", title:"element.height", changeOnKeypress:false },

        { type:"staticText", name:"boxSizes", title:"Box Sizes", colSpan:"*" },

        // Position
        // -------------------------------------------------------------------------
        { type:"section", name:"positions", defaultValue:"Positions",
          sectionExpanded:false,
          itemIds:["style.position", "style.left", "style.top"]
        },

        { type:"text", name:"style.position", changeOnKeypress:false },
        { type:"text", name:"style.left", changeOnKeypress:false },
        { type:"text", name:"style.top", changeOnKeypress:false },

        // Styling
        // -------------------------------------------------------------------------

        { type:"section", name:"styling", defaultValue:"Styling",
          itemIds:["style.margin", "style.border", "style.padding", "className",
                   "style.cssText"],
          sectionExpanded:false
        },

        { type:"text", name:"style.margin", changeOnKeypress:false },
        { type:"text", name:"style.border", changeOnKeypress:false },
        { type:"text", name:"style.padding", changeOnKeypress:false },
        { type:"text", name:"className", changeOnKeypress:false },
        { type:"text", name:"style.cssText", changeOnKeypress:false,
          colSpan:"*", startRow:true, width:"*" },

        // Clipping
        // -------------------------------------------------------------------------
        { type:"section", name:"clipScroll", defaultValue:"Clipping / Scrolling",
          itemIds:["style.overflow", "scrollLeft", "scrollTop"],
          sectionExpanded:false
        },
        { type:"text", name:"style.overflow", changeOnKeypress:false },
        { type:"text", name:"scrollLeft", changeOnKeypress:false },
        { type:"text", name:"scrollTop", changeOnKeypress:false },

        // Misc
        // -------------------------------------------------------------------------
        { type:"text", name:"style.visibility", changeOnKeypress:false },
        { type:"text", name:"style.zIndex", changeOnKeypress:false },

        // element-specific
        // -------------------------------------------------------------------------
        { type:"text", name:"cellPadding", showIf:"form.isTableElement()",
          changeOnKeypress:false },
        { type:"text", name:"cellSpacing", showIf:"form.isTableElement()",
          changeOnKeypress:false },

        { type:"textArea", name:"innerHTML", width:"*", height:"*",
          startRow:true, colSpan:"*", changeOnKeypress:false }

        /*
        // or instead, option to show inner and outer HTML
        { type:"radioGroup", name:"htmlShown", startRow:true, showTitle:false,
          defaultValue:"innerHTML", options:["innerHTML", "outerHTML"] },

        { type:"textArea", name:"innerHTML", width:"*", height:"*", showTitle:false,
          colSpan:3, changeOnKeypress:false }
        */
    ],

    isEmpty : function (value) {
        return value == null || isc.isAn.emptyString(value);
    },

    editElement : function (element) {
        this._element = element;


        var values = isc.addProperties({}, element);

        // hide the _cachedReportedEtc properties we put on elements for page coordinate
        // caching
        var html = element.innerHTML;
        values.innerHTML =
            html.replace(/ _cached(Reported|Calculated)Offset(Left|Top)="\d{1,4}"/g, "");
        //if (values.innerHTML != html) {
        //    this.logWarn("removed cache properties: orig html: " + html +
        //                 " new html: " + values.html);
        //}

        // treat names beginning with "style." as referring to the style subobject
        if (element.style != null) {
            var items = this.getItems();
            for (var i = 0; i < items.length; i++) {
                var item = items[i],
                    fieldName = item.getFieldName();
                if (fieldName.startsWith("style.")) {
                    values[fieldName] = this.getElementValue(fieldName);
                    //this.logWarn("for fieldName: " + fieldName +
                    //             ", got value: " + values[fieldName]);
                }
            }
        }

        // generate various summaries
        this.setValue("elementSummary",
            "&lt;" + (element.tagName || element.nodeName) +
                   (element.id ? " ID=" + element.id : "") +
            "&gt;");

        // sizes summary
        this.setValue("sizes",
                "Sizes <SPAN class=normal STYLE='color:black'>" +
                    "<SPAN STYLE='color:darkgray'>offset</SPAN>: " +
                 values.offsetWidth + "w x " + values.offsetHeight + "h" +
                    ", <SPAN STYLE='color:darkgray'>scroll</SPAN>: " +
                 values.scrollWidth + "w x " + values.scrollHeight + "h" +
                    ", <SPAN STYLE='color:darkgray'>client</SPAN>: " +
                 values.clientWidth + "w x " + values.clientHeight + "h</SPAN>");

        // position summary
        this.setValue("positions",
                "Positions <SPAN class=normal STYLE='color:black'>" +
                    "<SPAN STYLE='color:darkgray'>offset</SPAN>: " +
                 values.offsetLeft + "px left, " + values.offsetTop + "px top" +
                    ", <SPAN STYLE='color:darkgray'>client</SPAN>: " +
                 values.clientLeft + "px left, x " + values.clientTop + "px top" +
                "</SPAN>");

        // clip/scroll summary
        var overflow = values["style.overflow"];
        this.setValue("clipScroll",
                "Clip / Scroll <SPAN class=normal STYLE='color:black'>" +
                    "<SPAN STYLE='color:darkgray'>overflow</SPAN>: " +
                        (this.isEmpty(overflow) ? "visible" : overflow) +
                    ", <SPAN STYLE='color:darkgray'>scroll</SPAN>: " +
                                   values.scrollLeft + "px left, " +
                                   values.scrollTop + "px top" +
                "</SPAN>");

        // inline styling summary
        /*
        // showing style.cssText in summary disabled: IE will expand out a simple border:5px
        // gold setting into BORDER-LEFT.. et al - too spammy
        this.setValue("styling", "Styling <SPAN class=normal " +
                 "STYLE='color:black;text-overflow:ellipsis;'>inline CSS: " +
                 values["style.cssText"] + "</SPAN>");
        */
        var bgColor = values["style.backgroundColor"],
            margin = values["style.margin"],
            border = values["style.border"],
            padding = values["style.padding"];
        this.setValue("styling", "Styling <SPAN class=normal " +
                 "STYLE='color:black;'>" +
                 (this.isEmpty(values.className) ? "" : " class=" + values.className) +
                 (this.isEmpty(bgColor) ? "" : " bgColor: " + bgColor) +
                 (this.isEmpty(margin) ? "" : " margin: " + margin) +
                 (this.isEmpty(border) ? "" : " border: " + border) +
                 (this.isEmpty(padding) ? "" : " padding: " + padding) +
                 "</SPAN>");

        // detected size of boxes
        /*
        // this is currently factored so we could find out the box sizes from a className or a
        // Canvas only - a minor refactoring would make it possible to work with any element
        var scope = this.scope || window, // allow cross-frame usage
            Element = scope.isc.Element,
            boxWidths = [Element._calculateBorderSize(element),
                         Element._calculatePadding(element),
                         Element._calculateMargins(element)],
            boxNames = ["borders", "padding", "margins"],
            summary = "";

        for (var i = 0; i < 3; i++) {
            var edges = boxWidths[i];
            if (i > 0) summary += "\n";
            if (edges.left == edges.top && edges.top == edges.bottom &&
                edges.bottom == edges.right)
            {
                summary += boxNames[i] + ": " + edges.left + "px";
            } else if (edges.left == edges.right && edges.top == edges.bottom) {
                summary += boxNames[i] + ": " + edges.left + "px width x "
                                              + edges.top + "px height";
            } else {
                summary += boxNames[i] + ": " + edges.left + "px left, " +
                                              + edges.right + "px right, " +
                                              + edges.top + "px top, " +
                                              + edges.bottom + "px bottom";
            }
        }
        this.logWarn("summary is: " + summary);
        values["boxSizes"] = summary;
        */

        this.setValues(values);
    },

    isTableElement : function () {
        if (this._element == null) return false;
        var element = this._element,
            tagName = element.tagName;
        return tagName != null && tagName.toLowerCase() == "table";
    },

    // we have to use isc.eval() to get and set element values because you can't use bracket
    // notation (element["innerHTML"]).  NOTE: get/setAttribute may work, hasn't been tried
    getElementValue : function (fieldName) {
        //!DONTOBFUSCATE
        var element = this._element;
        if (element == null) return null;

        if (fieldName.startsWith("style.")) {
            element = element.style;
            if (element == null) return null;
            fieldName = fieldName.substring(6);
        }
        var oldValue;
        try {
            isc.eval("oldValue = " + this.getID() + "._element." + fieldName + ";");
        } catch (e) {
            // VML elements in IE throw quite a few errors
            oldValue = "Error: " + e.description;
        }

        return oldValue;
    },

    setElementValue : function (fieldName, newValue) {
        //!DONTOBFUSCATE
        var element = this._element;
        if (element == null) return;
        isc.eval("element." + fieldName + "=newValue");
    },

    // Handling updates
    // --------------------------------------------------------------------------
    itemChange : function (item, newValue, oldValue) {
        if (item == null) return;
        this.saveToElement(item.getFieldName(), newValue);
    },

    saveToElement : function (fieldName, value) {
        var element = this._element;
        if (element == null) return;

        var oldValue = this.getElementValue(fieldName);

        if (oldValue != value) {

            this.logWarn("changed " + fieldName + " from " +
                         this.echoLeaf(oldValue) + " to " + this.echoLeaf(value));

            // NOTE: in IE, many elements (eg TR, TBODY) don't allow innerHTML change.
            if (fieldName == "innerHTML" && isc.Browser.isIE) {
                return this._replaceIEInnerHTML(element, oldValue, value);
            }

            this.setElementValue(fieldName, value);

            this.editElement(element); // re-edit to pick up changes

            // re-inspect the subtree: innerHTML may make a completely new child set, or
            // various sizes may change in reaction to parent changes..
            var inspector = this.inspector;
            if (inspector) {
                var record = inspector.treeGrid.getSelectedRecord();
                inspector.treeGrid.data.reloadChildren(record);
            }

        } else {
            this.logWarn("value unchanged: " + fieldName + " from " +
                         this.echoLeaf(oldValue) + " to " + this.echoLeaf(value));
        }
    },
    _replaceIEInnerHTML : function (element, oldValue, value) {
        /*
        // outerHTML replacement caused "Unknown runtime error" for a
        // straightforward TR replacement
        var outerHTML = element.outerHTML;
        outerHTML = outerHTML.replace(oldValue, value);
        this.logWarn("new outerHTML: " + outerHTML);
        element.outerHTML = outerHTML;
        */

        /*
        // Node replacement bails saying "Not implemented" on createRange(), possibly
        // because of cross-frame call
        parent = element.parentNode;
        var range = element.ownerDocument.createRange();
        fragment = range.createContextualFragment(outerHTML);
        parent.replaceNode(element, fragment);
        */

        // look for a parent where we know assigning to innerHTML actually works.
        // Not viable because it's very common for the same HTML to exist in multiple
        // spots within some ancestor, eg a series of cells
        var safeParent = element;
        while (safeParent != null) {
            var tagName = safeParent.tagName;
            if (tagName == null) continue;
            tagName = tagName.toLowerCase();
            if (tagName == "div" || tagName == "td") break;

            safeParent = safeParent.parentNode;
        }

        if (safeParent == null) {
            this.logWarn("Couldn't change innerHTML, no safe parent found");
            return;
        }

        // NOTE: imperfect: if the parent has several elements with identical HTML,
        // only the first will be replaced
        var parentHTML = safeParent.innerHTML;
        parentHTML = parentHTML.replace(oldValue, value);
        safeParent.innerHTML = parentHTML;
        this.logWarn("replaced innerHTML on ancestor: " + this.echoLeaf(safeParent));

        var inspector = this.inspector,
            tree = inspector.treeGrid.data,
            parentRecord = tree.find("_element", safeParent);
        tree.reloadChildren(parentRecord);
        tree.loadSubtree(parentRecord);

        // edit at the changed parent (FIXME: ideally we're re-edit the same element, but it
        // doesn't necessarily have an ID)
        this.editElement(safeParent);
    }
});

// DOMInspector Class
// --------------------------------------------------------------------------------------------
isc.ClassFactory.defineClass("DOMInspector", "VLayout");

isc.DOMInspector.addMethods({
    initWidget : function () {
        this.Super("initWidget", arguments);
        this.createUI();
        if (this.rootElement) this.setRootElement(this.rootElement);
    },
    inspectTarget : function () {
        var target = this.domTargetForm.getValue("target"),
            element = this.scope.document.getElementById(target);
        this.setRootElement(element);
    },
    createUI : function () {

        this.domTargetForm = isc.DynamicForm.create({
            autoDraw:false,
            cellPadding:0,
            inspector:this,
            items:[
                {type:"text", name:"target", showTitle:false,
                 keyPress : function (item, form, keyName) {
                     if (keyName == "Enter") this.form.inspector.inspectTarget();
                 }
                }
            ]
        });
        this.toolbar = isc.HLayout.create({
            autoDraw:false,
            height:20,

            overflow:"hidden",
            members : [
                isc.Button.create({
                    autoDraw:false,
                    inspector:this,
                    scope:this.scope,
                    extraSpace:10,
                    title:"Inspect BODY",
                    click:function () {
                        this.inspector.setRootElement(this.scope.document.body);
                    }
                }),
                this.domTargetForm,
                isc.Button.create({
                    autoDraw:false,
                    inspector:this,
                    scope:this.scope,
                    title:"Inspect",
                    click:function () {
                        this.inspector.inspectTarget();
                    }
                })
            ]
        });
        this.addMember(this.toolbar);

        this.treeGrid = isc.DOMGrid.create({
            autoDraw:false,
            inspector:this,
            htmlMode:true,
            showResizeBar:true,
            scope : this.scope,

            nodeClick : function (viewer, node) {
                var element = this.data.getElement(node),
                    domEditor = this.inspector.domEditor;
                domEditor.editElement(element);

                if (element.id != null) {
                    this.inspector.domTargetForm.setValue("target", element.id);
                }


                this.scope.isc.Log.elementToHilite = element;
                this.scope.isc.Timer.setTimeout(
                        "isc.Log.hiliteElement()", 0);
            },
            contextMenu:this.getMenuConstructor().create({
                autoDraw:false,
                inspector:this,
                data:[
                    {title:"Open All",
                     click:"target.data.loadSubtree(target.getSelectedRecord())"},
                    {title:"Remove Node",
                     click:"menu.inspector.removeSelectedElement()"}
                ]
            })
        });
        this.addMember(this.treeGrid);

        this.domEditor = isc.DOMEditor.create({
            autoDraw:false,
            scope : this.scope,
            inspector : this,
            height:"50%",
            overflow:"hidden",
            showResizeBar:true
        });
        this.addMember(this.domEditor);
        this.addMember(isc.Button.create({
            autoDraw : false,
            title : "Remove Node",
            inspector : this,
            click : function () {
                this.inspector.removeSelectedElement();
            }
        }));
    },

    removeSelectedElement : function () {
        var record = this.treeGrid.getSelectedRecord();
        if (record == null) return;

        var tree = this.treeGrid.data;
        var parentRecord = tree.getParent(record);

        var element = record._element;
        if (element && element.parentNode) element.parentNode.removeChild(element);

        this.domEditor.clearValues();

        tree.reloadChildren(parentRecord);
    },

    getRootElement : function () { return this.rootElement },
    setRootElement : function (rootElement) {
        this.rootElement = rootElement;
        this.treeGrid.setRootElement(rootElement, true);
    }
});










//>    @class    PanelHeader
// A PanelHeader
//
// @visibility external
//<

isc.defineClass("PanelHeader", "ImgSectionHeader").addProperties({

    width: "100%",
    height: 22,

    // baseStyle
    //----------
    //> @attr panelHeader.baseStyle (CSSStyleName : "sectionHeader" : IRW)
    // @visibility external
    //<
    baseStyle:"sectionHeader"

    //> @method panelHeader.setBaseStyle()
    // @include statefulCanvas.setBaseStyle
    // @visibility external
    //<

    //>    @attr panelHeader.titleStyle (CSSStyleName : null : IRW)
    // CSS style applied to the title text only.  Defaults to +link{baseStyle} when unset.
    // <P>
    // With a separate <code>titleStyle</code> and +link{baseStyle} set, you can provide a
    // backgroundColor via <code>baseStyle</code> that will allow translucent .png media to be
    // "tinted" by the underlying background color, so that a single set of media can provide
    // range of color options.  In this usage, the <code>titleStyle</code> should generally not
    // specify a background color as this would block out the media that appears behind the
    // title.
    //
    // @visibility external
    //<

    //>    @method panelHeader.setTitleStyle()
    // Sets the +link{titleStyle}, which is applied to the title text.
    // @param style (CSSStyleName) new title style
    // @visibility external
    //<

});

isc.PanelHeader.addMethods({
    showSelectedIcon: false,
    showRollOverIcon: false,
    showDisabledIcon: false,
    showDownIcon: false,
    showFocusedIcon: false,
    initWidget : function () {
        this.setPanelTitleFromCanvas();
    },
    setPanelTitleFromCanvas : function () {
        if (this.canvas) {
            if (this.canvas.title) this.title = this.canvas.title;
            if (this.canvas.icon) this.icon = this.canvas.icon;
        }
    }
});

isc.PanelHeader.registerStringMethods({
    //> @method panelHeader.iconClick()
    // If this button is showing an +link{panelHeader.icon, icon}, a separate click
    // handler for the icon may be defined as <code>this.iconClick</code>.
    // Returning false will suppress the standard button click handling code.
    // @group buttonIcon
    // @visibility external
    //<
    iconClick:""
});


isc.Canvas.addProperties({
    //> @attr canvas.panelHeader (AutoChild PanelHeader : null : IRW)
    //
    //<

    //> @attr canvas.showPanelHeader (boolean : false : IRW)
    // Should a PanelHeader be shown attached to this canvas?
    //<

    //> @attr canvas.panelHeaderDefaults (PanelHeader Properties : null : IRW)
    //
    //<
    panelHeaderDefaults: {
        _constructor: "PanelHeader"
    },

    //> @attr canvas.panelHeaderProperties (PanelHeader Properties : null : IRW)
    //
    //<
    panelHeaderProperties: {
    },

    //> @type PanelHeaderPlacement
    //
    // @value "peer"
    // Add the +link{PanelHeader} as a peer of this Canvas.
    //
    // @value "member"
    // Add the PanelHeader as the first member of this Canvas.
    //
    // @value "custom"
    // Don't attempt to place the PanelHeader, assuming that the component will do so itself.
    //
    // @visibility external
    //<

    panelHeaderPlacement: "peer",

    setupPanelHeader : function () {
        if (!this.showPanelHeader) return;

        this.panelHeader = this.createAutoChild("panelHeader", { canvas: this, snapTo: "T"});

        if (isc.isA.Layout(this)) this.panelHeaderPlacement = "member";
        else this.panelHeaderPlacement = "peer";

        // wangle this to always add the panelHeader as a peer for now (addMember
        // in an HLayout looks terrible!)
        this.panelHeaderPlacement = "peer";

        if (this.panelHeaderPlacement == "member") {
            this.addMember(this.panelHeader, 0);
        }
        else if (this.panelHeaderPlacement == "peer") {
            this.addPeer(this.panelHeader);
            this.panelHeader.moveAbove(this);

            this._registerAttachedPeer(this.panelHeader, isc.Canvas.TOP);
        }
        else if (this.panelHeaderPlacement == "custom") {}
    }
});


// Actions for panelHeaders

isc.Canvas.addMethods({
    panelActionControls: [],

    refreshPanelControls : function () {
        var controls = this.panelControls || [];

        if (!this.panelHeader.controls) this.panelHeader.controls = [];
        else this.panelHeader.controls.setLength(0);

        for (var i=0; i<controls.length; i++) {
            var item = controls.get(i),
                itemToAdd = null;

            if (isc.isAn.Object(item)) {
                // item is a control
                itemToAdd = isc.addProperties({}, item);
            } else if (item.startsWith("action:")) {
                // item is an action-def
                var actionName = item.substring(7, item.length),
                    action = isc.Canvas.getRegisteredAction(actionName);

                if (this.canPerformAction(action)) {
                    if (this.showActionInPanel(action))
                        itemToAdd = this.getPanelActionControl(action);
                }
            } else {
                // item is an autochild-name
                itemToAdd = this.createAutoChild(item);
            }

            if (itemToAdd) {
                this.panelHeader.controls.add(itemToAdd);
            } else {
            }
        }
//        this.panelHeader.controlsLayout.members = [];
        var header = this.panelHeader;
        header.addControls();
//        this.panelHeader.markForRedraw();
    },

    canPerformAction : function (action) {
        var actionName = action.name,
            propertyName = action.enableProperty ||
                "can"+ actionName.substring(0,1).toUpperCase() +
                actionName.substring(1,actionName.length)
        ;

        return this[propertyName] && this[propertyName] == true ? true : false;
    },
    showActionInPanel : function (action) {
        return action.showInPanel;
    },
    getPanelActionControl : function (action) {
        var control = this.panelActionControls[action.name],
            className = action.controlConstructor || "ImgButton";

// TODO: this seems to return the same control instances across multiple canvii,
// like the cached _panelActionControls array is static, which it isn't
//        if (control) return control;

        control = isc.ClassFactory.newInstance(
            className, {
                ID: this.getID()+"_"+action.name,
                width: 18, height:18,
                src: action.icon,
                showRollOver:false,
                showDown:false,
                showDisabled:false,
                showFocused:false,
                actionTarget: this,
                actionObject: action,
                prompt: action.tooltip,
                click : function () {
                    this.actionTarget[this.actionObject.methodName]();
                }
            }
        );

        this.panelActionControls[action.name] = control;
        return this.panelActionControls[action.name];
    },

    // helper method to allow Canvas instances to print themselves - means the
    // method can be called as just canvas.showPrintPreview() without params if
    // necessary (like in the MultiView)
    showPrintPreview : function (printProperties, previewProperties, callback,
                                  separator)
    {
        isc.Canvas.showPrintPreview(this, printProperties, previewProperties,
            callback, separator);
    }

});

isc.Canvas.addClassProperties({
    _registeredActions : {}                 // internal array of  registered actions
});

isc.Canvas.addClassMethods({
    registerAction : function (newAction) {
        if (!this._registeredActions[newAction.name]) {
            this._registeredActions[newAction.name] = newAction;
        }
    },
    getRegisteredActionNames : function () {
        return isc.getKeys(this._registeredActions);
    },
    getRegisteredActions : function () {
        return isc.getValues(this._registeredActions);
    },
    getRegisteredActionIndex : function () {
        var actions = this.getRegisteredActions(),
            actionIndex = actions.makeIndex("name", false);
        return actionIndex;
    },
    getRegisteredAction : function (actionName) {
        return this._registeredActions[actionName];
    },
    isActionRegistered : function (actionName) {
        return !this._registeredActions[actionName] ? false : true;
    }
});

isc.defineClass("Action", "Class").addProperties({
    name: null,
    title: null,
    icon: null,
    tooltip: null,
    methodName: null,
    controlConstructor: "ImgButton",
    enableProperty: null,
    showInPanel: true
});

isc.Canvas.registerAction(
    isc.Action.create({
        name: "edit",
        title: "Edit",
        icon: "[SKINIMG]/actions/edit.png",
        tooltip: "Put the component into Edit mode",
        methodName: "startEditing",
        showInPanel: false
    })
);

isc.Canvas.registerAction(
    isc.Action.create({
        name: "editNew",
        title: "Edit New",
        icon: "[SKINIMG]/SectionHeader/opener_closed.png",
        tooltip: "Add a new Record to the component",
        methodName: "startEditingNew",
        showInPanel: false
    })
);

isc.Canvas.registerAction(
    isc.Action.create({
        name: "sort",
        title: "Sort",
        icon: "[SKINIMG]/actions/sort_ascending.png",
        tooltip: "Sort the records in the component",
        methodName: "sort",
        controlConstructor: "SortActionSelector",
        enableProperty: "canSortFields",
        showInPanel: false
    })
);

isc.Canvas.registerAction(
    isc.Action.create({
        name: "export",
        title: "Export",
        icon: "[SKINIMG]/actions/redo.png",
        tooltip: "Export the data in the component",
        methodName: "exportData",
        showInPanel: true
    })
);

isc.Canvas.registerAction(
    isc.Action.create({
        name: "print",
        title: "Print",
        icon: "[SKINIMG]/actions/print.png",
        tooltip: "Print the data in the component",
        methodName: "showPrintPreview",
        showInPanel: true
    })
);

isc.defineClass("SortActionSelector", "DynamicForm").addProperties({
    width: 100,
    height: 20,
    numCols: 4,
    fields: [
        {
            name: "sortField", showTitle: false, colSpan: 2,
            type: "select",
            width: 80,
            prompt: "Sort Field",
            startRow: false, endRow: false,
            changed: function (form, item, value) {
                var up = form.getField("sortDirection").getValue(),
                    sortDirection = up ? "ascending" : "descending";
                if (form.actionTarget.sort) form.actionTarget.sort(value, sortDirection);
                else form.actionTarget.data.sortByProperty(value, up);
            }
        },{
            name: "sortDirection", showTitle: true, showLabel: false,
            type: "checkbox",
            width: 20,
            prompt: "Sort Direction: Checked is Ascending",
            startRow: false, endRow: false,
            changed: function (form, item, value) {
                var up = value,
                    sortDirection = up ? "ascending" : "descending",
                    sortField = form.getField("sortField").getValue();
                if (form.actionTarget.sort) form.actionTarget.sort(sortField, sortDirection);
                else form.actionTarget.data.sortByProperty(sortField, up);
            }
        }
    ],
    initWidget : function () {
        this.Super("initWidget", arguments);
    },
    draw : function () {
        this.Super("draw", arguments);
        var ds = this.actionTarget.getDataSource(),
            fields = ds ? isc.getValues(ds.getFields()) : [],
            valueMap = {};

        for (var i=0; i<fields.length; i++) {
            var item = fields.get(i);
            valueMap[item.name] = item.title;
        }
        this.getField("sortField").setValueMap(valueMap);
    }
});








//>    @class    DataPathItem
//
// TextItem subclass for managing a DataPath
//
// @visibility external
//<
isc.defineClass("DataPathItem", "TextItem").addProperties({
    operationsTreeDefaults: {
        _constructor: "TTreeGrid",
        autoDraw: false,
        recordDoubleClick: function () {
            this.creator.operationSelected();
        },
        getIcon : function (node) {
            var serviceContainer = this.creator.form.creator,
                icon = (serviceContainer && serviceContainer.getServiceElementIcon) ?
                    serviceContainer.getServiceElementIcon(node) : null
            ;
            if (icon) return icon;
            return this.Super("getIcon", arguments);
        }
    },

    operationsTreeSelectButtonDefaults: {
        _constructor: "TButton",
        autoDraw: false,
        title: "Select",
        click: function () {
            if (this.creator.operationsTree.anySelected())
                this.creator.operationSelected();
        }
    },

    defaultIcons: [
        {
            src: "[SKINIMG]/actions/edit.png",
            click: "item.showOperationsTreeData()",
            width:16, height:16
        },
        {
            src: "[SKINIMG]/actions/remove.png",
            click: "item.clearFormValues()",
            width:16, height:16
        }
    ],

    baseManagedProperties: [
        "dataPath", "schemaDataSource", "serviceName", "serviceNamespace"
    ],

    // methods
    getPropertyName : function (prop) {
        if (this.isInput)
            return "input" + prop.substring(0,1).toUpperCase() + prop.substring(1);
        else return prop;
    },
    initManagedProperties : function () {
        this.managedProperties = [];
        var baseProps = this.baseManagedProperties;
        for (var i = 0; i < baseProps.length; i++) {
            this.managedProperties.add(this.getPropertyName(baseProps[i]));
        }
    },

    keyPress : function (item, form, keyName) {
        if (keyName != "Arrow_Left" && keyName != "Arrow_Right" &&
            keyName != "Home" && keyName != "End") return false;
        this.Super("keyPress", arguments);
    },

    init : function () {
        this.icons = isc.clone(this.defaultIcons);

        this.initManagedProperties();

        this.Super("init", arguments);

        if (this.operationsTreeData) {
            this.addAutoChildren(["operationsTree", "operationsTreeSelectButton"]);
        }
    },

    showOperationsTreeData : function () {
        if (!this.operationsTreeData) return;
        if (!this.operationsTree)
            this.addAutoChildren(["operationsTree", "operationsTreeSelectButton"]);

        var theTree;
        if (isc.isA.Tree(this.operationsTreeData)) {
            theTree = this.operationsTreeData;
        } else {
            theTree = isc.Tree.create({
                modelType: "children",
                root: {children: this.operationsTreeData},
                nameProperty: "name",
                childrenProperty: "children"
            });
            theTree.openAll();
        }
        this.operationsTree.setData(theTree);

        // find and select the node corresponding to the current dataPath value, if there is one
        var prefix = this.isInput ? "formInputs" : "formOutputs";

        var dataPath = prefix + "/" + this.getValue(),
            node = theTree.find(dataPath)
        ;

        if (node) this.operationsTree.selectRecord(node);

        if (!this.schemaDialog) {
            this.schemaDialog = isc.TWindow.create({
                title:"Select element from message",
                autoCenter:true,
                height:"90%", width:"60%",
                isModal: true,
                showModalMask: true,
                items: [
                    isc.VLayout.create({
                        width: "100%",
                        height: "100%",
                        members: [this.operationsTree, this.operationsTreeSelectButton ]
                    })
                ]
            });
        } else this.schemaDialog.show();
    },

    // the user has picked a paletteNode generated from one of the available input messages
    operationSelected : function () {
        var palette = this.operationsTree,
            tree = palette.data,
            paletteNode = palette.getSelectedRecord()
        ;

        this.schemaDialog.hide();
        this.setDataPathProperties(paletteNode);
    },

    setDataPathProperties : function (editNode) {
        // handle either an editNode (live editing) or a paletteNode (picked from schema tree)
        var defaults = editNode.initData || editNode.defaults;

        this.dataPathProps = isc.applyMask(defaults, this.managedProperties);
        if (this.logIsInfoEnabled()) {
            this.logInfo("setDPProps, editNode: " + this.echoAll(editNode) +
                         " defaults: " + this.echo(defaults) +
                         ", managedProps: " + this.managedProperties +
                         ", props: " + this.echo(this.dataPathProps));
        }
        this.saveFormValues();
    },

    saveFormValues : function () {
        for (var i = 0; i < this.managedProperties.length; i++) {
            var prop = this.managedProperties[i],
                value = this.dataPathProps[prop];

            this.form.setValue(prop, value);
        }
        // mild hack: only active with ComponentEditor as specialized by VisualBuilder,
        // where saveProperties exists.
        if (this.form.saveProperties) {
            this.form.saveProperties(this.dataPathProps, this.form.currentComponent);
        }
    },
    clearFormValues : function () {
        for (var i = 0; i < this.managedProperties.length; i++) {
            this.form.setValue(this.managedProperties[i], null);
        }
        // mild hack: only active with ComponentEditor as specialized by VisualBuilder,
        // where saveProperties exists.
        if (this.form.saveProperties) {
            this.form.saveProperties(this.dataPathProps, this.form.currentComponent);
        }
    }


});



// Class will not work without the ListGrid
if (isc.ListGrid) {




//> @class RelativeDateItem
// A FormItem for entering a date relative to today or relative to some other date, or a specific
// date.  Typically used for filtering data by date.
// <P>
// The RelativeDateItem consists of a +link{ComboBoxItem} where the user may directly choose
// one of several +link{relativeDateItem.presetOptions, preset options}, choose to enter a
// +link{relativeDateItem.quantityField, quantity} and +link{type:TimeUnit, time unit}
// (eg "4 months ago" or "3 years from now") or directly type in
// an absolute date value (7/18/2009).
// @visibility external
//<
isc.defineClass("RelativeDateItem", "CanvasItem");

isc.RelativeDateItem.addClassMethods({

    //> @classMethod relativeDateItem.getAbsoluteDate()
    // @include DateUtil.getAbsoluteDate()
    // @param relativeDate (RelativeDate) the relative date to convert
    // @param [baseDate] (Date) base value for conversion.  Defaults to today
    // @return (Date) resulting absolute date value
    // @visibility external
    //<
    getAbsoluteDate : function (relativeDate, baseDate, type, rangePosition) {

        var isLogicalDate = false;
        if (isc.SimpleType.inheritsFrom(type, "date")
            && !isc.SimpleType.inheritsFrom(type, "datetime"))
        {
            isLogicalDate = true;
        }
        var absoluteDate = isc.DateUtil.getAbsoluteDate(relativeDate, baseDate, rangePosition,
                                                        isLogicalDate);
        return absoluteDate;
    },



    getPeriodName : function (periodKey) {
        return isc.DateUtil.getTimeUnitName(periodKey);
    },

    getPeriodKey : function (periodName) {
        return isc.DateUtil.getTimeUnitKey(periodName);
    },

    getRelativeDateParts : function (relativeDateString) {
        return isc.DateUtil.getRelativeDateParts(relativeDateString);
    },
    isRelativeDate : function (value) {
        return isc.DateUtil.isRelativeDate(value);
    }
});


isc.RelativeDateItem.addProperties({


//    titleOrientation: "top",
    height: 20,
    cellHeight: 20,
    canFocus: true,

    //> @attr relativeDateItem.rangePosition (RelativeDateRangePosition : null : IRWA)
    // Does this items relative date value refer to the start or end of the chosen date? Useful
    // when using this item to generate filter criteria, such as the from or to value for an
    // inclusive range.
    // <P>
    // If unset "start" is assumed.
    //
    // @see relativeDateItem.operator
    // @see relativeDateItem.rangeRoundingGranularity
    // @visibility external
    //<
    // Note that 'getAbsoluteDate()' [falls through to the method on the DateUtil class] actually
    // implements rounding the chosen value to the range position.
    // This has no effect if we're dealing with logical-dates (field is of
    // type "date" rather than "datetime"), since logical dates theoretically have no time component.
    // Of course they're implemented as JS Date objects so do have a time value but it should
    // always be set to the same static fixed value and we won't be modifying this.


    //> @type TimeUnit
    //   An enum of time-units available for use with the +link{RelativeDateItem},
    // +link{TimeItem} and +link{Calendar} widgets.
    //
    // @value "millisecond"    a millisecond time-unit
    // @value "second"    a second time-unit
    // @value "minute"    a minute time-unit
    // @value "hour"    an hour time-unit
    // @value "day"    a day time-unit
    // @value "week"    a week time-unit
    // @value "month"    a month time-unit
    // @value "quarter"    a quarter (3 month) time-unit
    // @value "year"    a year time-unit
    //
    // @visibility external
    //<

    //> @attr relativeDateItem.timeUnitOptions (Array of TimeUnit : ["day", "week", "month"] : IR)
    // List of time units that will be offered for relative dates.
    // <P>
    // Each available time unit option will cause two options to appear in the
    // +link{valueField}.  For example, if "day" is an available +link{type:TimeUnit,time unit}
    // option, there will be +link{relativeDateItem.daysAgoTitle, "N days ago"} and
    // +link{relativeDateItem.daysFromNowTitle, "N days from now"}.
    //
    // @see relativeDateItem.showPastOptions
    // @see relativeDateItem.showFutureOptions
    // @see relativeDateitem.rangeRoundingGranularity
    //
    // @visibility external
    //<
    timeUnitOptions: ["day", "week", "month"],

    //> @attr relativeDateItem.showPastOptions (Boolean : true : IR)
    // Should we show time-unit options in the past? If set to false, for each
    // +link{timeUnitOptions,timeUnitOption} we will show only future options [for example
    // "N weeks from now"].
    // <P>
    // Note: this does not change the +link{relativeDateItem.presetOptions}, which show up in
    // addition to the time-unit options (<i>"N days from now"</i>, etc). The default
    // preset options include both past and future presets so developers may wish to modify the
    // presets to ensure only past options are available.
    //
    // @visibility external
    //<
    showPastOptions:true,

    //> @attr relativeDateItem.showFutureOptions (Boolean : true : IR)
    // Should we show time-unit options in the future? If set to false, for each
    // +link{timeUnitOptions,timeUnitOption} we will show only past options [for example
    // "N weeks ago"].
    // <P>
    // Note: this does not change the +link{relativeDateItem.presetOptions}, which show up in
    // addition to the time-unit options (<i>"N days from now"</i>, etc). The default
    // preset options include both past and future presets so developers may wish to modify the
    // presets to ensure only future options are available.
    //
    // @visibility external
    //<
    showFutureOptions:true,

    //> @attr relativeDateItem.rangeRoundingGranularity (Object : {...} ; IRWA)
    // A map from a granularity of time specified by a user to the granularity of time used for
    // rounding.
    // <P>
    // A relative date such as "n days from now" is normally shifted to the end of the day when
    // used as a range endpoint, and the beginning of the day when used as the beginning of a range.
    // (The rounding direction on some item can be specified via
    // +link{relativeDateItem.rangePosition}).
    // This causes the intuitive behavior that "from yesterday to today" is from the beginning of
    // yesterday to the end of today, and that "from today until 5 days from now" includes the
    // entirety of Friday if today is Monday.
    // <P>
    // This same rule <i>can</i> be applied to any time granularity, such that "from now until
    // 20 minutes from now" is up to 5:32 if it is now 5:11:34, and
    // "from now until 2 months from now" means end of June if it is mid-April.
    // <P>
    // User intuitions about where this rounding is expected for any given time period tend to
    // vary based on what kind of event is being discussed and subtle phrasing differences
    // (consider "up to one year from now", "until next year", "within the next couple of years").
    // The defaults behaviors are:
    // <ul>
    // <li> for days, weeks and months round to <b>day</b> end/beginning
    // <li> for hours, round to <b>minute</b> end/beginning
    // <li> for minutes and seconds, round to <b>second</b> end/beginning
    // </ul>
    // To customize this rounding behavior, this attribute may be set to a simple javascript object
    // mapping each timeUnit to the granularity for that timeUnit.<br>
    // For example the following config code would produce an item where the user could select
    // only day or week values, and the selected value would be rounded to the beginning of the day
    // if a day was selected, or the beginning of the week if a week was selected:
    // <pre>
    //  {
    //      name:"fromDate", type:"RelativeDateItem",
    //      timeUnitOptions:["day", "week"],
    //      rangePosition:"start",
    //      rangeRoundingGranularity:{
    //          "day":"day",
    //          "week":"week"
    //      }
    //  }
    // </pre>
    //
    // @visibility external
    //<


    rangeRoundingGranularity:{
        "year":"day",
        "month":"day",
        "week":"day",
        "day":"day",
        "hour":"minute",
        "minute":"second",
        "second":"second"
    },

    // i18n attributes

    //> @attr relativeDateItem.millisecondsAgoTitle (string : "N milliseconds ago" : IR)
    // The title to show for historical periods when the +link{type:TimeUnit} is "millisecond".
    // @visibility external
    // @group i18nMessages
    //<
    millisecondsAgoTitle: "N milliseconds ago",

    //> @attr relativeDateItem.secondsAgoTitle (string : "N seconds ago" : IR)
    // The title to show for historical periods when the +link{type:TimeUnit} is "second".
    // @visibility external
    // @group i18nMessages
    //<
    secondsAgoTitle: "N seconds ago",

    //> @attr relativeDateItem.minutesAgoTitle (string : "N minutes ago" : IR)
    // The title to show for historical periods when the +link{type:TimeUnit} is "minute".
    // @visibility external
    // @group i18nMessages
    //<
    minutesAgoTitle: "N minutes ago",

    //> @attr relativeDateItem.hoursAgoTitle (string : "N hours ago" : IR)
    // The title to show for historical periods when the +link{type:TimeUnit} is "hour".
    // @visibility external
    // @group i18nMessages
    //<
    hoursAgoTitle: "N hours ago",

    //> @attr relativeDateItem.daysAgoTitle (string : "N days ago" : IR)
    // The title to show for historical periods when the +link{type:TimeUnit} is "day".
    // @visibility external
    // @group i18nMessages
    //<
    daysAgoTitle: "N days ago",

    //> @attr relativeDateItem.weeksAgoTitle (string : "N weeks ago" : IR)
    // The title to show for historical periods when the +link{type:TimeUnit} is "week".
    // @visibility external
    // @group i18nMessages
    //<
    weeksAgoTitle: "N weeks ago",

    //> @attr relativeDateItem.monthsAgoTitle (string : "N months ago" : IR)
    // The title to show for historical periods when the +link{type:TimeUnit} is "month".
    // @visibility external
    // @group i18nMessages
    //<
    monthsAgoTitle: "N months ago",

    //> @attr relativeDateItem.quartersAgoTitle (string : "N quarters ago" : IR)
    // The title to show for historical periods when the +link{type:TimeUnit} is "quarter".
    // @visibility external
    // @group i18nMessages
    //<
    quartersAgoTitle: "N quarters ago",

    //> @attr relativeDateItem.yearsAgoTitle (string : "N years ago" : IR)
    // The title to show for historical periods when the +link{type:TimeUnit} is "year".
    // @visibility external
    // @group i18nMessages
    //<
    yearsAgoTitle: "N years ago",

    //> @attr relativeDateItem.millisecondsFromNowTitle (string : "N milliseconds from now" : IR)
    // The title to show for future periods when the +link{type:TimeUnit} is "millisecond".
    // @visibility external
    // @group i18nMessages
    //<
    millisecondsFromNowTitle: "N milliseconds from now",

    //> @attr relativeDateItem.secondsFromNowTitle (string : "N seconds from now" : IR)
    // The title to show for future periods when the +link{type:TimeUnit} is "second".
    // @visibility external
    // @group i18nMessages
    //<
    secondsFromNowTitle: "N seconds from now",

    //> @attr relativeDateItem.minutesFromNowTitle (string : "N minutes from now" : IR)
    // The title to show for future periods when the +link{type:TimeUnit} is "minute".
    // @visibility external
    // @group i18nMessages
    //<
    minutesFromNowTitle: "N minutes from now",

    //> @attr relativeDateItem.hoursFromNowTitle (string : "N hours from now" : IR)
    // The title to show for future periods when the +link{type:TimeUnit} is "hour".
    // @visibility external
    // @group i18nMessages
    //<
    hoursFromNowTitle: "N hours from now",

    //> @attr relativeDateItem.daysFromNowTitle (string : "N days from now" : IR)
    // The title to show for future periods when the +link{type:TimeUnit} is "day".
    // @visibility external
    // @group i18nMessages
    //<
    daysFromNowTitle: "N days from now",

    //> @attr relativeDateItem.weeksFromNowTitle (string : "N weeks from now" : IR)
    // The title to show for future periods when the +link{type:TimeUnit} is "week".
    // @visibility external
    // @group i18nMessages
    //<
    weeksFromNowTitle: "N weeks from now",

    //> @attr relativeDateItem.monthsFromNowTitle (string : "N months from now" : IR)
    // The title to show for future periods when the +link{type:TimeUnit} is "month".
    // @visibility external
    // @group i18nMessages
    //<
    monthsFromNowTitle: "N months from now",

    //> @attr relativeDateItem.quartersFromNowTitle (string : "N quarters from now" : IR)
    // The title to show for future periods when the +link{type:TimeUnit} is "quarter".
    // @visibility external
    // @group i18nMessages
    //<
    quartersFromNowTitle: "N quarters from now",

    //> @attr relativeDateItem.yearsFromNowTitle (string : "N years from now" : IR)
    // The title to show for future periods when the +link{type:TimeUnit} is "year".
    // @visibility external
    // @group i18nMessages
    //<
    yearsFromNowTitle: "N years from now",

    //> @attr relativeDateItem.defaultValue (Date or RelativeDateString or TimeUnit : "$today" : IR)
    // Default value to show.  Can be a concrete Date, a +link{RelativeDateString} that matches
    // one of the +link{relativeDateItem.presetOptions}, or one of the available
    // +link{relativeDateItem.timeUnitOptions, time units}.  If setting a +link{type:TimeUnit},
    // use +link{relativeDateItem.defaultQuantity, defaultQuantity} to establish a default
    // value for the +link{relativeDateItem.quantityField, quantityField}.
    //
    // @visibility external
    //<
    defaultValue: "$today",

    //> @attr relativeDateItem.operator (OperatorId : "greaterThan" : IR)
    // What operator to use when <var class="smartclient"/>+link{getCriterion()}</var>
    // <var class="smartgwt">the <code>FormItemCriterionGetter</code>'s <code>getCriterion()</code> method</var>
    // is called.
    //
    // @visibility external
    //<
    operator: "greaterThan",

    //> @attr relativeDateItem.presetOptions (Object : see below : IR)
    // Preset relative dates, such as "today" or "tomorrow", that the user can choose directly
    // from the +link{valueField}.
    // <P>
    // Format is an Object mapping user-visible titles to +link{RelativeDateShortcut} or
    // +link{RelativeDateString}s. The default value (expressed in JSON) is:
    // <pre>
    // {
    //     "$today" : "Today",
    //     "$yesterday" : "Yesterday",
    //     "$tomorrow" : "Tomorrow",
    //     "$weekAgo" : "Current day of last week",
    //     "$weekFromNow" : "Current day of next week",
    //     "$monthAgo" : "Current day of last month",
    //     "$monthFromNow" : "Current day of next month"
    // }
    // </pre>
    // In addition to these presets, options are shown for each of the
    // +link{type:TimeUnit, timeUnit options}.
    //
    // @visibility external
    //<

    presetOptions: {
        "$today" : "Today",
        "$yesterday" : "Yesterday",
        "$tomorrow" : "Tomorrow",
        "$weekAgo" : "Current day of last week",
        "$weekFromNow" : "Current day of next week",
        "$monthAgo" : "Current day of last month",
        "$monthFromNow" : "Current day of next month"
    },

    // default implementation of formItem.shouldSaveOnEnter() returns this
    saveOnEnter: true,

    //> @attr relativeDateItem.valueField (AutoChild ComboBoxItem : null : IR)
    // +link{ComboBoxItem} field where a user may choose among
    // +link{relativeDateItem.presetOptions, presets},
    // +link{type:TimeUnit, time unit} plus +link{relativeDateItem.quantityField,quantity}, or
    // direct entry of a date as text.
    //
    // @visibility external
    //<
    valueFieldDefaults: {
        editorType: "ComboBoxItem",
        name: "valueField",
        showTitle: false,
        shouldSaveValue: false,
        validateOnChange: false,
        // Override getInnerWidth() - the inner form has cellPadding set to zero to ensure the
        // left edge of this item lines up with the left edge of other select/combo items in the
        // form - however we'll also render *wider* since our inner width won't have cellPadding
        // to take into account. Deal with this by explicitly knocking off the cellPadding from
        // the form the RelativeDateItem is written into!
        getInnerWidth : function (adjustForIcons) {
            var iw = this.Super("getInnerWidth", arguments);
            var rdi = this.form.canvasItem;
            if (rdi == null || this._absPos() || !rdi._writtenIntoCell() || !isc.isA.Number(iw)) {
                return Math.max(0, iw);
            }
            return Math.max(0, iw - rdi._getCellHBorderPadSpacing());
        },
        keyPress : function (item, form, keyName, characterValue) {
            if (keyName == "Enter") {
                var parentItem = form.canvasItem;
                if (parentItem && parentItem.form)
                    parentItem.form.handleKeyPress(isc.EH.lastEvent);
            }
        },
        pickValue : function () {
            this.form.canvasItem.setBaseDate();
            this.Super("pickValue", arguments);
        }
    },

    //> @attr relativeDateItem.defaultQuantity (int : 1 : IR)
    // Default quantity to show in the +link{quantityField}.
    //
    // @visibility external
    //<
    defaultQuantity: 1,

    //> @attr relativeDateItem.quantityField (AutoChild SpinnerItem : null : IR)
    // Field allowing user to pick units of time, eg, number of days.
    //
    // @visibility external
    //<
    quantityFieldDefaults: {
        editorType: "SpinnerItem",
        name: "quantityField",
        width: 50,
        min: 0,
        step: 1,
        showTitle: false,
        shouldSaveValue: false,
        selectOnFocus: true,
        keyPress : function (item, form, keyName, characterValue) {
            if (keyName == "Enter") {
                var parentItem = form.canvasItem;
                if (parentItem && parentItem.form) {
                    parentItem.updateValue();
                    parentItem.form.handleKeyPress(isc.EH.lastEvent);
                }
            }
        }
    },

    //> @attr relativeDateItem.showChooserIcon (Boolean : true : IRW)
    //      Should we show the icon that shells a date-chooser?
    // @visibility external
    //<
    showChooserIcon:true,

    //> @attr relativeDateItem.pickerIcon (AutoChild FormItemIcon : null : IR)
    // Icon that launches a +link{DateChooser} for choosing an absolute date.
    //
    // @visibility external
    //<
    pickerIconDefaults: {
        name: "chooserIcon",
        showOver: false,
        showFocused: false,
        showFocusedWithItem: false,
        neverDisable: true,
        width: 16, height: 16,
        src:"[SKIN]/DynamicForm/DatePicker_icon.gif"
    },

    iconVAlign: "center",

    //> @attr relativeDateItem.pickerIconPrompt (string : "Show Date Chooser" : IR)
    // Prompt to show when the user hovers the mouse over the picker icon for this
    // RelativeDateItem. May be overridden for localization of your application.
    // @visibility external
    // @group i18nMessages
    //<
    pickerIconPrompt : "Show Date Chooser",

    //> @attr relativeDateItem.pickerConstructor (string : "DateChooser" : [IR])
    // SmartClient class for the +link{picker, dateChooser} autoChild displayed to allow the user
    // to directly select dates.
    // @visibility external
    //<
    pickerConstructor: "DateChooser",

    //> @attr relativeDateItem.baseDate (Date : null : IR)
    // Base date for calculating the relative date entered by the user.
    // <P>
    // The default is to use the current date.
    //
    // @visibility external
    //<

    //> @attr relativeDateItem.showCalculatedDateField (Boolean : true : IRW)
    //  Should the Calculated-Date be displayed to the right of the +link{pickerIcon}.
    // @visibility external
    //<
    showCalculatedDateField:true,

    //> @attr relativeDateItem.calculatedDateField (AutoChild BlurbItem : null : IR)
    // Field that shows the current calculated date by adding the user-entered relative date to
    // the +link{baseDate}.
    //
    // @visibility external
    //<
    invalidCalculatedDatePrompt: "",
    calculatedDateFieldDefaults: {
        editorType: "BlurbItem",
        name: "calculatedDateField",
        border:"1px solid black;",
        width: "*",

        setValue : function (value) {
            if (value == null || value == "") value = this.defaultValue;
            return this.Super("setValue", arguments);
        },
        startRow: false,
        showTitle: false,
        shouldSaveValue: false
    },



    //> @method relativeDateItem.formatEditorValue() [A]
    // RelativeDateItems do not make use of the standard +link{formItem.formatEditorValue()} and
    // +link{formItem.parseEditorValue()} methods. Developers can customize the display values
    // for these items in the following ways:<ul>
    // <li>The +link{presetOptions} map allows standard preset RelativeDateString
    //     and RelativeDateShortcut values to be mapped to custom display values</li>
    // <li>The text displayed for each of the +link{timeUnitOptions} (e.g:"N days ago") may be
    //     customized via the per-time unit title attributes (+link{daysFromNowTitle},
    //     +link{daysAgoTitle}, etc)</li>
    // <li>The +link{dateFormatter} and +link{inputFormat} may be used modify how date values
    //     are displayed (both in the text entry box and in the
    //    +link{showCalculatedDateField,calculatedDateField}</li>
    // </ul>
    // @visibility external
    //<

    //> @method relativeDateItem.parseEditorValue() [A]
    // RelativeDateItems do not make use of the standard +link{formItem.formatEditorValue()} and
    // +link{formItem.parseEditorValue()} methods. Developers can customize the display values
    // for these items in the following ways:<ul>
    // <li>The +link{presetOptions} map allows standard preset RelativeDateString
    //     and RelativeDateShortcut values to be mapped to custom display values</li>
    // <li>The text displayed for each of the +link{timeUnitOptions} (e.g:"N days ago") may be
    //     customized via the per-time unit title attributes (+link{daysFromNowTitle},
    //     +link{daysAgoTitle}, etc)</li>
    // <li>The +link{dateFormatter} and +link{inputFormat} may be used modify how date values
    //     are displayed (both in the text entry box and in the
    //    +link{showCalculatedDateField,calculatedDateField}</li>
    // </ul>
    // @visibility external
    //<

    //> @attr relativeDateItem.inputFormat (DateInputFormat : null : IR)
    // Format for direct user input of date values.
    // <P>
    // If unset, the input format will be determined based on the specified
    // +link{displayFormat} if possible, otherwise picked up from the Date class (see
    // +link{Date.setInputFormat()}).
    // <var class="smartclient">
    // <P>
    // Note: if entirely custom date formatting/parsing logic is required for this item,
    // this attribute may be set to a function which takes a single parameter (the formatted
    // date string) and returns a JavaScript date object.
    // </var>
    // @visibility external
    //<

    //> @attr relativeDateItem.displayFormat (DateDisplayFormat : null : IR)
    // Format for displaying dates in the +link{valueField} and +link{calculatedDateField}.
    // Defaults to the system-wide default established by +link{Date.setShortDisplayFormat()}, or
    // if this item has its type specified as datetime, +link{Date.setShortDatetimeDisplayFormat()}.
    // @deprecated in favor of RelativeDateItem.dateFormatter
    // @visibility external
    //<

    //> @attr relativeDateItem.dateFormatter (DateDisplayFormat : null : IR)
    // Format for displaying dates in the +link{valueField} and +link{calculatedDateField}.
    // If unset a default DateDisplayFormat will be picked up from +link{dyamicForm.dateFormatter}
    // (or +link{dynamicForm.datetimeFormatter} for datetime fields} or otherwise from
    // the system-wide default established by +link{Date.setShortDisplayFormat()}, or
    // if this item has its type specified as datetime, +link{Date.setShortDatetimeDisplayFormat()}.
    // <var class="smartclient">
    // <P>
    // Note: if entirely custom date formatting/parsing logic is required for this item, this
    // attribute may be set to a custom formatting function. In this case the function will be
    // applied to the Date being formatted (for example <code>this.getFullYear()</code> would
    // give you back the full year), and should return the formatted date string.
    // </var>
    //
    // @visibility external
    //<

    //> @attr relativeDateItem.startDate (Date : 1/1/1995 : IRW)
    // @include dateItem.startDate
    // @group appearance
    // @visibility external
    //<
    //startDate:isc.DateItem.DEFAULT_START_DATE,

    //> @attr relativeDateItem.endDate (Date : 12/31/2020 : IRW)
    // @include dateItem.endDate
    // @group appearance
    // @visibility external
    //<
    //endDate:isc.DateItem.DEFAULT_END_DATE,

    //> @attr relativeDateItem.centuryThreshold (number : 25 : IRW)
    // @include dateItem.centuryThreshold
    // @visibility external
    //<
    centuryThreshold:isc.DateItem.DEFAULT_CENTURY_THRESHOLD,

    shouldSaveValue: true,

    editCriteriaInInnerForm:false,

    //> @attr relativeDateItem.editor (AutoChild DynamicForm : null : [IRW])
    //
    // The editor that will be rendered inside this item.  Unless overridden, the editor will be
    // an instance of +link{class:DynamicForm}. It will be created using the overrideable
    // defaults standard to the +link{group:autoChildren,AutoChild} subsystem - editorConstructor
    // and editorProperties.
    //
    //  @visibility internal
    //<
    editorConstructor: "DynamicForm",
    editorDefaults: {
        numCols: 4,
        // Don't specify an explicit width - we'll expand/contract based on whether we're
        // showing the "n day" picker / whether we're showing the calculated date field, etc.
        // If we render the form at a fixed size, any icons for the item get mispositioned.
        cellPadding:0,
        colWidths: [130, "*", "*", "*"],
        itemChanged : function (item, newValue) {
            this.creator.updateValue();
        },
        // Pass keyPress events through to this form item

        itemKeyPress : function (item,keyName,characterValue) {
            // Fire item.keyPress and item.form.itemKeyPress on the
            // RelativeDateItem instance.
            var item = this.canvasItem;
            if (item && item.form) {
                return item._fireKeyPressHandlers(item,item.form,keyName,characterValue);
            }
        }
    },

    //> @attr relativeDateItem.useSharedPicker (Boolean : true : [IR])
    // When set to true (the default), use a single shared date-picker across all widgets that
    // use one.  When false, create a new picker using the autoChild system.  See
    // +link{dateItem.pickerDefaults, picker} and
    // +link{dateItem.pickerProperties, pickerProperties} for details on setting up an unshared
    // picker.
    // @visibility external
    //<
    useSharedPicker: false,

    //> @attr relativeDateItem.pickerDefaults (DateChooser : see below : [IR])
    // Defaults for the +link{DateChooser} created by this form item.
    //<
    pickerDefaults: {
        width: isc.DateItem.chooserWidth,
        height: isc.DateItem.chooserHeight,
        closeOnEscapeKeypress:true,
//        border: "none",
        // show a cancel button that closes the window
        showCancelButton: true,
        autoHide: true
    },

    //> @attr RelativeDateItem.showChooserFiscalYearPicker (Boolean : false : IRW)
    // When set to true, show a button that allows the calendar to be navigated by fiscal year.
    // @visibility external
    //<
    showChooserFiscalYearPicker: false,
    //> @attr RelativeDateItem.showChooserWeekPicker (Boolean : false : IRW)
    // When set to true, show a button that allows the calendar to be navigated by week or
    // fiscal week, depending on the value of +link{showChooserFiscalYearPicker}.
    // @visibility external
    //<
    showChooserWeekPicker: false

    //> @attr relativeDateItem.pickerProperties (DateChooser : see below : [IR])
    // Properties for the +link{DateChooser} created by this form item.
    //<

/*

    //> @attr relativeDateItem.pickerIconProperties (object : {...} : IRW)
    // Properties for the pickerIcon.
    // @visibility pickerIcon
    //<
    pickerIconProperties:{
    },
*/


});

isc.RelativeDateItem.addMethods({

    init : function () {
        if (!this.startDate) this.startDate = isc.DateItem.getPrototype().startDate;
        if (!this.endDate) this.endDate = isc.DateItem.getPrototype().endDate;
        this._createEditor();
        this.Super("init", arguments);
    },

    isEditable : function () {
        return true;
    },

    // if we get destroyed, wipe out our editor too
    autoDestroy:true,

    _createEditor: function(){
        var ds;
        var dynProps = { _suppressColWidthWarnings: true };
        this.addAutoChild("editor", dynProps);
        this.canvas = this.editor;

        var _this = this,
            items = [],
            blurbIndex=2
        ;

        items[0] = isc.addProperties({}, this.valueFieldDefaults, this.valueFieldProperties,
            {
                valueMap: this.getValueFieldOptions()
            }
        );
        items[1] = isc.addProperties({}, this.quantityFieldDefaults,
            this.quantityFieldProperties,
            {
                defaultValue: this.defaultQuantity
            }
        );

        if (this.showChooserIcon) {
            blurbIndex = 3;
            items[2] = { name: "iconPlaceholder", type: "staticText", width: 1,
                showTitle: false,
                canFocus:true, _canFocusInTextBox:function () { return false },
                // The synthetic tab-order management stuff used when the clickmask is
                // up needs to have focusInItem et al behave (never attempting to
                // focus on the text-box)
                focusInItem:function () {
                    this.focusInIcon(this.icons[0]);
                },
                _moveFocusWithinItem : function (forward) {
                    return false;
                },
                iconVAlign: "center",
                icons: [
                    isc.addProperties({ prompt: this.pickerIconPrompt },
                        this.pickerIconDefaults, this.pickerIconProperties,
                        {
                            click : function () {
                                if (!_this.renderAsStatic() && !_this.renderAsReadOnly())
                                    _this.showPicker();
                            }
                        }
                    )
                ]
            };
        }

        // set a default baseDate is one wasn't provided
        var type = this.getType(),
            isLogicalDate = false;
        if (isc.SimpleType.inheritsFrom(type, "date") &&
            !isc.SimpleType.inheritsFrom(type, "datetime"))
        {
            isLogicalDate = this.isLogicalDate = true;
        }

        this.setBaseDate();

        if (this.showCalculatedDateField) {
            items[blurbIndex] = isc.addProperties({}, this.calculatedDateFieldDefaults,
                this.calculatedDateFieldProperties,
                { cellStyle: this.getHintStyle(), defaultValue: this.invalidCalculatedDatePrompt });
        }

        this.canvas.setFields(items);

        this.valueField = this.canvas.getField("valueField");
        this.quantityField = this.canvas.getField("quantityField");
        if (this.showCalculatedDateField)
            this.calculatedDateField = this.canvas.getField("calculatedDateField");
        if (this.showChooserIcon) {
            this.iconPlaceholder = this.canvas.getField("iconPlaceholder");
            this.pickerIcon = this.iconPlaceholder.icons.find("name", "chooserIcon");
        }

        this.setValue(this.value || this.defaultValue);
    },

    getBaseDate : function () {
        return this.baseDate;
    },

    setBaseDate : function (baseDate) {
        this.baseDate = baseDate ||
                    (this.isLogicalDate ? isc.Date.createLogicalDate() : new Date());
    },

    // updateEditor() Fired when the value changes (via updateValue or setValue)
    // Shows or hides the quantity box and updates the hint to reflect the current value.
    updateEditor : function () {

        if (!this.valueField || !this.quantityField) return;

        var focusItem,
            selectionRange,
            mustRefocus = false;

        if (this.valueField.hasFocus) {
            focusItem = this.valueField;
            selectionRange = this.valueField.getSelectionRange();
        } else if (this.quantityField.hasFocus) {
            focusItem = this.quantityField;
            selectionRange = this.quantityField.getSelectionRange();
        }

        var value = this.valueField.getValue(),
            quantity = this.quantityField.getValue();

        var showQuantity = (value && isc.isA.String(value) && this.relativePresets[value]);

        if (!showQuantity) {
            if (this.quantityField.isVisible()) {
                mustRefocus = true;
                this.editor.colWidths = [130, 22, "*", "*"];
                this.quantityField.hide();
            }
        } else {
            if (!this.quantityField.isVisible()) {
                mustRefocus = true;
                this.editor.colWidths = [130, 50, 22, "*"];
                this.quantityField.show();
            }
        }

        if (this.calculatedDateField) {
            var value = this.getAbsoluteDate();
            var visibleDate = this.calculatedDateField.getValue();
            var newDate = !value ? "" :
                "("+this.formatDate(value)+")";
            if (visibleDate != newDate) {

                mustRefocus = true;
                this.calculatedDateField.setValue(newDate);
            }
        }
        // If we redrew the form to show or hide the qty field, we may need to refocus and
        // reset the selection range

        if (mustRefocus && focusItem != null) {

            if (!showQuantity && focusItem == this.quantityField) {
                this.valueField.focusInItem();
            }
        }
    },

    _valueFieldOptions: null,
    getValueFieldOptions : function (useStored) {
        if (useStored && this._valueFieldOptions) return this._valueFieldOptions;

        var options = isc.addProperties({}, this.presetOptions);

        this.relativePresets = {};

        // add two entries for each available time-unit, one historical, the other futuristic
        for (var i=0; i< this.timeUnitOptions.length; i++) {
            var key = this.timeUnitOptions[i];
            if (this.showPastOptions) {
                options[key+"_ago"] = this[key+"sAgoTitle"];
                this.relativePresets[key+"_ago"] = true;
            }
            if (this.showFutureOptions)  {
                options[key+"_fromNow"] = this[key+"sFromNowTitle"];
                this.relativePresets[key+"_fromNow"] = true;
            }
        }

        this._valueFieldOptions = options;
        return options;
    },

    setValue : function (value, allowNullValue) {


        if (!this.valueField) return this.Super("setValue", arguments);

        // If we have a defaultValue, pick it up now so we can convert from rel to abs date etc.
        // Check for allowNullvalue matches the superclass implementation so we get the same
        // default as we would calling 'super'
        if (value == null && !allowNullValue) {
            var defaultVal = this.getDefaultValue();
            if (defaultVal != null) {
                value = defaultVal;
            }
        }

        // Overridden to:
        // - update the valueField / quantity field to reflect the value passed in
        // - convert to an absolute date (if a relative date was passed in) and store that out so
        //   getValue() always returns an absolute date.

        // Note - if passed a relative date we *also* store out the relative date value, and
        // hang it on the derived absolute date as an attribute.
        // This allows us to display the relative date and return it from getRelativeDate()
        // calls even if a setValue(getValue()) cycle occurs [can happen on initial draw with
        // setItemValues for example].

        // If this is a date that was derived from our current relativeDate value, we're basically
        // resetting to our current value -- retain the relative date information.
        if (isc.isA.Date(value) && value._fromRelativeDate != null &&
            this.compareValues(this._relativeDate,value._fromRelativeDate))
        {
            // We hung onto a timestamp when we converted from relative-date - if it's
            // changed this implies the date has subsequently been modified, so don't
            // make use of this relative value.
            if (value.getTime() == value._relativeDateTimestamp) {
                value = value._fromRelativeDate;
            }
        }

        var isRelativeDate = false;

        // reset the baseDate
        this.setBaseDate();

        if (value == null) {
            this.valueField.setValue(null);

        } else if (isc.isA.Date(value) || this.valueField.valueMap[value] ||
                (value.value && this.valueField.valueMap[value.value]))
        {
            var absDate = isc.isA.Date(value);
            isRelativeDate = !absDate;
            // the defaultValue is a preset or a date, just set the value
            this.valueField.setValue(absDate ? this.formatDate(value) :
                    value.value ? value.value : value);


        } else if (this.timeUnitOptions.contains(value)) {
            isRelativeDate = true;
            // the defaultValue is a timeUnit - select the future version of it
            value += "_fromNow";
            this.valueField.setValue(value);
        } else if (isc.isA.String(value) && isc.isA.Date(this.parseDate(value, this.getInputFormat()))) {
            // convert the text input to a date, since it is one
            value = this.parseDate(value, this.getInputFormat());
            this.valueField.setValue(this.formatDate(value));
        } else {
            // a defaultValue was provided, but it's none of preset, timeUnit or date
            var absoluteDate,
                isRelativeDate = isc.RelativeDateItem.isRelativeDate(value);

            // Handle being passed a relative date object or a shortcut string
            if (isRelativeDate || isc.isA.String(value)) {
                absoluteDate = isc.RelativeDateItem.getAbsoluteDate(
                        value, this.getBaseDate(), this.getType(), this.rangePosition);
            }
            // Unable to convert to a date - we were passed something other than a valid
            // RelativeDate - just ignore it.
            if (!isc.isA.Date(absoluteDate)) {
                isRelativeDate = false;
                this.valueField.setValue(null);
//                this.valueField.setValue("$today");
            } else {

                var relativeDateString = isRelativeDate ? value.value : value;

                // at this point we know we were passed either a relative date shortcut
                // ($now etc) which isn't present in our presetOptions (we checked for being present
                // in the valueMap earlier in this method), or we were passed a true relative date
                // string like +1w
                // If possible, set both value and quantity to preserve the relative date -- if
                // we're not showing options that allow us to do this, just store out the
                // absolute date.
                relativeDateString = isc.DateUtil.mapRelativeDateShortcut(relativeDateString,
                                        this.rangePosition);
                var key, quantity,
                    parts = isc.RelativeDateItem.getRelativeDateParts(relativeDateString),
                    period = isc.RelativeDateItem.getPeriodName(parts.period),
                    suffix = (parts.direction == "+" ? "fromNow" : "ago")
                ;

                quantity = parts ? parts.countValue : null;
                key = period ? period.toLowerCase()+"_"+suffix : null;


                if (key && this.valueField.valueMap[key]) {
                    this.valueField.setValue(key);
                    this.quantityField.setValue(quantity);
                    isRelativeDate = true;
                } else {
                    // the period to which this relativeDate applies is not in the list
                    // just resolve to an absolute date and store that value
                    isRelativeDate = false;
                    this.valueField.setValue(this.formatDate(absoluteDate));
                }
            }
        }

        if (isRelativeDate) {
            // we know we were passed a relative date. getDataValue() will normalize to
            // the standard RelativeDateObject format.
            // convertToAbsoluteDate() will convert to an actual date, and store the '_fromRelativeDate'
            // marker on that absolute date.
            this._relativeDate = this.getDataValue();
            value = this._convertToAbsoluteDate(this._relativeDate);
        } else {
            this._relativeDate = null;
        }

        this.Super("setValue", [value, allowNullValue], arguments);

        this.updateEditor();
    },

    _convertToAbsoluteDate : function (relativeDate) {
        var relativeDateValue = relativeDate.value ? relativeDate.value : relativeDate;
        var absDate = isc.RelativeDateItem.getAbsoluteDate(
                        relativeDateValue, this.getBaseDate(), this.getType(), this.rangePosition);

        if (isc.isA.Date(absDate)) {
            // Hang flags on the converted date so we can tell what relative date it was
            // derived from.
            // We use this to detect the case where setValue(getValue()) is called on an item
            // showing a relative date value so we can hang onto the relative date information.
            absDate._fromRelativeDate = relativeDate;
            absDate._relativeDateTimestamp = absDate.getTime();
        }

        return absDate;
    },

    //> @method relativeDateItem.getAbsoluteDate()
    // Returns the current value as a Date. If the user entered a relative date value, this
    // will be normalized to a Date and returned.
    //
    // @return Date value
    // @visibility internal
    //<
    // Currently getValue() always normalizes to a real date.
    getAbsoluteDate : function () {
        return this.getDataValue(true);
    },

    //> @method relativeDateItem.getRelativeDate()
    // Returns the current +link{RelativeDate} object for this item. Only applies if the user entered
    // a relative date value (such as "Today") - if an absolute date was entered, this method
    // returns null.<br>
    // Relative date objects have the following format:
    // <pre>
    //     { _constructor: "RelativeDate", value: "$today" }
    // </pre>
    //
    // @return object an object containing the relativeDate string for the current value
    // @visibility external
    //<
    getRelativeDate : function () {
        var value = this.valueField.getValue(),
            quantity = this.quantityField.getValue()
        ;
        if (!value || !isc.isA.String(value)) return null;

        var firstChar = value.substring(0,1);

        if (firstChar == "+" || firstChar == "-" ||
                (firstChar == "$" && this.getValueFieldOptions(true)[value]))
        {
            // this is a relativeDate anyway, just return it
            return this.getRelativeDateObject(value);
        }

        // check for one of the other built-in types (in the format [period]_ago, [period]_fromNow
        // This is what gets created when the user picks a quantity and a value from the timePeriod
        // drop downs.
        var underscoreIndex = value.indexOf("_");

        if (underscoreIndex >= 0) {
            var periodName = value.substring(0, underscoreIndex),
                negative = (value.substring(underscoreIndex+1) == "ago"),
                key = isc.RelativeDateItem.getPeriodKey(periodName)
            ;
            if (key) {
                // Return a relative date string representing the period, rounded by the appropriate
                // offset - example for 3 weeks from now when rounding to the beginning of the day,
                // return "+3w[-0D]"
                var string = (negative ? "-" : "+") + quantity + key;

                var offsetDirection = this.rangePosition == "end" ? "+" : "-",
                    offset = this.rangeRoundingGranularity[periodName];
                if (offset != null && offset.toLowerCase() != "millisecond") {
                    offset = isc.RelativeDateItem.getPeriodKey(offset).toUpperCase();
                    string += "[" + offsetDirection + "0" + offset + "]";
                }
                return this.getRelativeDateObject(string);
            }
        }

        return null;
    },

    getRelativeDateObject : function (relativeDate) {
        var result = { _constructor: "RelativeDate", value: relativeDate };
        if (this.rangePosition) result.rangePosition = this.rangePosition;
        return result;
    },

    // Helper method to return the current value as an absolute or relative date object.
    // Returns null if the entered value can't be parsed into either absolute or relative date
    getDataValue : function (returnAbsoluteDate) {
        var enteredVal = this.valueField.getValue(),
            dateValue;

        if (enteredVal == null || isc.isAn.emptyString(enteredVal)) {
            dateValue = null;
        } else {
            var relativeDate = this.getRelativeDate();

            if (relativeDate) {
                if (returnAbsoluteDate) {
                    if (!relativeDate.value.startsWith("$") ||
                            this.getValueFieldOptions(true)[relativeDate.value]) {
                        dateValue = this._convertToAbsoluteDate(relativeDate);
                    }
                } else {
                    dateValue = relativeDate;
                }
            } else {
                dateValue = this.parseDate(enteredVal, this.getInputFormat());
            }
            if (isc.isA.Date(dateValue)) {
                var type = this.getType();
                if (type == null ||
                    (!isc.SimpleType.inheritsFrom(type, "datetime") &&
                     !isc.SimpleType.inheritsFrom(type, "time")))
                {
                    dateValue.logicalDate = true;
                }
                if (this.rangePosition && dateValue.logicalDate) {
                    dateValue.rangePosition = this.rangePosition;
                    this.setToZeroTime(dateValue);
                    if (this.rangePosition == "end")
                        dateValue = isc.DateUtil.getEndOf(dateValue, "D");
                }
            }
        }
        return dateValue;
    },

    // This formItem returns a Date from getValue(), so we want to convert to an absolute date
    // for storage.
    // If a relative date was entered we also hang onto that (on both the item and the date object)
    // so we can maintain it across item.setValue(item.getValue()) cycles (EG form validation/redraw)
    updateValue : function() {
        if (!this.valueField || !this.quantityField) return;

        var oldValue = this._value,
            oldRelativeDate = this._relativeDate,
            dataValue = this.getDataValue(false)
        ;

        if (!oldValue && dataValue) this.setBaseDate();

        var absDateValue = (dataValue == null || isc.isA.Date(dataValue))
                            ? dataValue
                            : this._convertToAbsoluteDate(dataValue);

        if (dataValue != absDateValue) {
            this._relativeDate = dataValue;
        } else {
            this._relativeDate = null;
        }

        // Note: We compare both the absolute date and the relative date - this means
        // we *will* fire change/changed handler if the user changes from "Yesterday" to
        // the absolute date for yesterday, or "1 day ago". Seems desirable since
        // getRelativeDate() will return different results in the second case.

        if (this.compareValues(oldValue,absDateValue) &&
            this.compareValues(oldRelativeDate,this._relativeDate)) return;

        this._updateValue(absDateValue);
        this.updateEditor();
    },

    // We always return advanced criteria if we have a value.
    hasAdvancedCriteria : function () {
        return (this.valueField && this.valueField.getValue() != null);
    },

    //> @method relativeDateItem.getCriterion()
    // Get the criterion based on the values the user has entered.
    // @param [absolute] (boolean) whether to use an absolute date in the Criterion produced.
    //                             By default a +link{RelativeDate} will be used if the user
    //                             entered a relative date value
    // @return Criterion
    //
    // @visibility external
    //<
    getCriterion : function (absolute) {
        var date = this.getDataValue(absolute);
        if (this.rangePosition && date && date.isLogicalDate) {
            this.setToZeroTime(date);
            // Respect rangePosition on the date picked (note that zero time is the default)
            if (this.rangePosition == "end") date = isc.DateUtil.getEndOf(date, "D");
        }

        if (date == null) return null;
        var field = this.getCriteriaFieldName();
        return { operator: this.operator, value: date, fieldName:field };
    },

    getCellHeight : function () {
        var cellHeight = this.Super("getCellHeight", arguments);
        if (isc.Browser.isIE && this.useTextField && isc.isA.Number(cellHeight)) cellHeight += 2;
        return cellHeight;
    },

    //> @method RelativeDateItem.getFiscalCalendar()
    // Returns the +link{FiscalCalendar} object that will be used by this item's DateChooser.
    //
    // @return (FiscalCalendar) the fiscal calendar for this chooser, if set, or the global
    //            one otherwise
    // @visibility external
    //<
    getFiscalCalendar : function () {
        return this.fiscalCalendar || Date.getFiscalCalendar();
    },

    //> @method RelativeDateItem.setFiscalCalendar()
    // Sets the +link{FiscalCalendar} object that will be used by this item's DateChooser.  If
    // unset, the _link{Date.getFiscalCalendar, global fiscal calendar} is used.
    //
    // @param [fiscalCalendar] (FiscalCalendar) the fiscal calendar for this chooser, if set, or the global
    //            one otherwise
    // @visibility external
    //<
    setFiscalCalendar : function (fiscalCalendar) {
        this.fiscalCalendar = fiscalCalendar;
    },

    //> @attr RelativeDateItem.showPickerTimeItem (Boolean : true : IRWA)
    // If this item is editing a field of type <code>"datetime"</code>, should the
    // +link{class:DateChooser} display the +link{dateChooser.showTimeItem,time field},
    // allowing the user to select a time?
    // <P>
    // One case where developers will wish to suppress this time-field from being displayed
    // is if a custom +link{dateFormatter} has been specified which does not display the
    // time portion of the selected date. In this case any value selected from the
    // DateChooser's time field will be discarded when the picker is dismissed, making
    // it a confusing UI for the end user.
    // <P>
    // Has no effect if the field type is <code>"date"</code> - in this case the
    // picker will never show the time field.
    //
    // @visibility external
    //<
    showPickerTimeItem:true,

    //> @attr RelativeDateItem.pickerTimeItemProperties (TimeItem Properties : null : IRWA)
    // A set of properties to apply to the +link{class:TimeItem} displayed in the picker when
    // +link{showPickerTimeItem} is true.
    // <P>
    // Has no effect for fields of type <code>"date"</code>.
    // @visibility external
    //<

    //> @attr RelativeDateItem.use24HourTime (Boolean : true : IRW)
    // When showing the +link{class:DateChooser}, should the
    // +link{dateChooser.showTimeItem,time field} be set to use 24-hour time?
    // Has no effect for fields of type <code>"date"</code> rather than
    // <code>"datetime"</code>, or if +link{showPickerTimeItem} is <code>false</code>.
    // <P>
    // Default is true.
    // @visibility external
    //<
    use24HourTime: true,

    // override 'showPicker' - instead of creating a picker instance we're reusing a shared
    // one.
    showPicker : function () {

        if (!this.picker) {
            if (this.useSharedPicker) this.picker = isc.DateChooser.getSharedDateChooser();
            else {
                this.picker = isc[this.pickerConstructor].create(
                    isc.addProperties({}, this.pickerDefaults, this.pickerProperties,
                        {
                            _generated:true,
                            // When re-using a DateChooser, we're almost certainly displaying it as a
                            // floating picker rather than an inline element. Apply the common options for
                            // a floating picker
                            autoHide:true,
                            showCancelButton:true
                        }
                    )
                );
            }
        }

        var picker = this.picker;

        var oldItem = picker.callingFormItem;
        if (oldItem != this) {
            if (oldItem) oldItem.ignore(picker, "dataChanged");
            this.observe(picker, "dataChanged", "observer.pickerDataChanged(observed)");

            picker.callingFormItem = this;
            picker.callingForm = this.canvas; // this.form;

            picker.locatorParent = this.canvas; //this.form;
        }

        if (this.inputFormat) picker.inputFormat = this.inputFormat;

        picker.startYear = this.getStartDate().getFullYear();
        picker.endYear = this.getEndDate().getFullYear();

        // set the year and week attributes
        picker.fiscalCalendar = this.getFiscalCalendar();
        picker.showFiscalYearChooser = this.showChooserFiscalYearPicker;
        picker.showWeekChooser = this.showChooserWeekPicker;
        // show a TimeItem in the picker if type is datetime
        var type = this.type, isLogicalDate = false;
        if (isc.SimpleType.inheritsFrom(type, "date")
            && !isc.SimpleType.inheritsFrom(type, "datetime"))
        {
            isLogicalDate = true;
        }
        var showTimeItem = isLogicalDate ? false : this.showPickerTimeItem;
        picker.showTimeItem = showTimeItem
        picker.use24HourTime = this.use24HourTime;
        if (this.pickerTimeItemProperties)
            picker.timeItemProperties = isc.addProperties({}, picker.timeItemProperties,
                this.pickerTimeItemProperties);

        var absoluteDate = this.getAbsoluteDate();
        if (picker.setData) {
            if (isc.isA.Date(absoluteDate) && !isNaN(absoluteDate.getTime())) {
                // this item has a value, pass it to the DateChooser
                picker.setData(absoluteDate);
            } else {
                var chosenDate = new Date();
                // this item has no value - if it has a rangePosition, set the time
                // appropriately on the default chosenDate
                if (this.rangePosition == "start") {
                    chosenDate = isc.DateUtil.getStartOf(chosenDate, "D");
                } else if (this.rangePosition == "end") {
                    chosenDate = isc.DateUtil.getEndOf(chosenDate, "D");
                }
                picker.setData(chosenDate);
                // prevent a second call to setData(), from the Super() call, that passes
                // item.getValue(), which returns null at that point
                picker._ignorePickerSetData = true;
            }
        }
        if (picker.updateUI) picker.updateUI();

        return this.Super("showPicker", arguments);

    },

    // custom code to center the picker over the picker icon
    getPickerRect : function () {
        // we want the date chooser to float centered over the picker icon.
        var left = this.getPageLeft(),
            top = this.getPageTop(),

            chooserWidth = isc.DateItem.chooserWidth + 3,
            chooserHeight = isc.DateItem.chooserHeight + 3,
            form = this.canvas,
            item
        ;

        item = form.getItem("iconPlaceholder");

        left += item.getLeft();
        left += Math.round((item.getVisibleWidth() - (this.getPickerIconWidth() /2)) -
                (chooserWidth/2));
        top += Math.round((this.getPickerIconHeight() / 2) - (chooserHeight/2));

        // NOTE: don't return chooserWidth/Height as part of the rect, which would cause the
        // picker to actually be resized to those dimensions, and they may match the natural
        // size at which the chooser draws given skinning properties.
        return [left, top];
    },

    updateDisabled : function () {
        this.Super("updateDisabled", arguments);
        if (this.iconPlaceholder && this.iconPlaceholder.isVisible()) {
            this.iconPlaceholder.redraw();
        }
    },

    //> @method relativeDateItem.pickerDataChanged()
    //      Store the date passed in, and fire the change handler for this item.
    //      Called when the user selects a date from the date-chooser window.
    //  @visibility internal
    //<
    pickerDataChanged : function (picker) {

        var date = picker.getData();
        var type = this.getType(),
            isDate = isc.SimpleType.inheritsFrom(type, "date"),
            isDatetime = isc.SimpleType.inheritsFrom(type, "datetime");
        if (!this.showPickerTimeItem && (!isDate || isDatetime)) {
            this.setToZeroTime(date);
            // Respect rangePosition on the date picked (note that zero time is the default)
            if (this.rangePosition == "end") date = isc.DateUtil.getEndOf(date, "D");

        }

        // avoid firing 'updateValue' while setting the values of sub items
        this._suppressUpdates = true;

        this.valueField.setValue(this.formatDate(date));

        this._suppressUpdates = false;

        // Explicitly call 'updateValue' to save the new date (handles firing change
        // handlers, etc. too)
        this.updateValue();

        // Ensure we have focus

        if (!this.hasFocus) this.focusInItem();
    },

    //> @method relativeDateItem.getStartDate() (A)
    //    use this method, rather than referring to this.startDate, to guarantee that it
    //    returns a date
    //      Note - Does not update this.startDate - should it?
    //<
    getStartDate : function () {
        var startDate = this.parseDate(this.startDate);
        if(!isc.isA.Date(startDate)) {
            //>DEBUG
            this.logWarn("startDate was not in valid date format - using default start date");
            //<DEBUG
            startDate = isc.DateItem.DEFAULT_START_DATE;
        }
        return startDate;
    },

    //> @method relativeDateItem.getEndDate() (A)
    //    use this method, rather than referring to this.endDate, to guarantee that it
    //      returns a date
    //<
    getEndDate : function () {
        var endDate = this.parseDate(this.endDate);
        if(!isc.isA.Date(endDate)) {
            //>DEBUG
            this.logWarn("endDate was not in valid date format - using default end date");
            //<DEBUG
            endDate = isc.DateItem.DEFAULT_END_DATE;
        }
        return endDate;
    },

    //> @method relativeDateItem.parseDate()
    // Parse a date passed in as a string.
    //
    // @param dateString (string) date value as a string
    // @param inputFormat (DateInputFormat) format for date strings to be parsed
    // @return (date) date value
    // @group elements
    //<
    parseDate : function (dateString, inputFormat) {
        if (dateString == null || isc.isAn.emptyString(dateString)) return null;
        if (inputFormat == null) inputFormat = this.getInputFormat();
        var dataType = this.getType(),
            isDate = isc.SimpleType.inheritsFrom(dataType, "date"),
            isDatetime = isc.SimpleType.inheritsFrom(dataType, "datetime"),
            isLogicalDate =  isDate && !isDatetime;

        var date = Date.parseInput(dateString, inputFormat,
                                this.centuryThreshold, true, !isLogicalDate);
        // If it's a datetime, we may not actually be showing a time portion in the string.
        // In this case we'll want to clamp to the start or end of day!
        if (isc.isA.Date(date) && !isLogicalDate) {
            var enteredVal = this.getEnteredValue();

            if (enteredVal != null && !isc.isA.Function(inputFormat)) {
                var validTime = isc.Date.isDatetimeString(enteredVal, inputFormat);

                //var validTime = isc.Time.parseInput(enteredVal, true);

                if (!validTime) {
                    this.setToZeroTime(date);
                    // Respect rangePosition on the date picked (note that zero time is the default)
                    if (this.rangePosition == "end") date = isc.DateUtil.getEndOf(date, "D");
                }
            }
        }
        return date;
    },


    // formatDate() - given a live date object, returns the formatted date string to display
    formatDate : function (date) {
        if (!isc.isA.Date(date)) return date;

        var displayFormat = this._getDateFormatter();

        var type = this.getType(),
            isDatetime = false,
            isDate = isc.SimpleType.inheritsFrom(type, "date");
        if (!isDate || isc.SimpleType.inheritsFrom(type, "datetime")) isDatetime = true;


        if (isDatetime) {
            return date.toShortDatetime(displayFormat, true);
        } else {
            return date.toShortDate(displayFormat, !isDate);
        }
    },

    //> @method relativeDateItem.getInputFormat() (A)
    // @include dateItem.getInputFormat
    // @visibility external
    //<
    getInputFormat : function () {
        // developer may explicitly specify an inputFormat (this used to be the only way to change
        // input/display format for text-based date items)
        if (this.inputFormat) return this.inputFormat;
        // If a display format, but no inputFormat is specified attempt to derive the inputFormat
        // from the displayFormat. This works for the standard shortDate display formatters but
        // you'll still need to specify an explicit input format for anything more exotic
        var displayFormat = this._getDateFormatter();
        if (displayFormat) {
            return Date.mapDisplayFormatToInputFormat(displayFormat);
        }
        // couldn't get an input format - rely on the standard global Date inputFormat
        return null;
    },


    //> @method relativeDateItem.getEnteredValue()
    // Returns the raw text value typed into this items value text field
    // @visibility external
    //<
    getEnteredValue : function () {
        if (this.valueField) return this.valueField.getValue();
        return this.getValue();
    },

    getValue : function () {
        var value = this.Super("getValue", arguments);
        return value || (this.valueField && this.valueField.getValue());
    },

    validators: [
        { type: "isDate" }
    ]

});

}




// Class will not work without the ListGrid
if (isc.ListGrid) {





//> @class DateRangeItem
// Allows a user to select an absolute or relative range of dates via two
// +link{RelativeDateItem}s (if +link{DateRangeItem.allowRelativeDates} is true) or two
// +link{DateItems}.
// <P>
// DateRangeItem is just a convenience relative to using two +link{RelativeDateItem} or
// +link{DateItem} controls in a form, then using +link{FormItem.operator} and
// +link{FormItem.criteriaField} to cause them to produce a date range.  If you need more
// control over layout, validation, event handling or any other aspect of appearance or
// behavior, stop using DateRangeItem and use two DateItem/RelativeDateItem controls directly
// instead.
//
// @visibility external
//<
isc.defineClass("DateRangeItem", "CanvasItem");

isc.DateRangeItem.addProperties({

//> @attr dateRangeItem.shouldSaveValue (Boolean : true : IR)
// Allow dateRangeItems' values to show up in the form's values array, or if
// +link{dynamicForm.getValuesAsCriteria()} is called, for the criterion to be included
// in the returned AdvancedCriteria object
// @visibility external
//<
shouldSaveValue:true,

//> @object DateRange
// A JavaScript object specifying a range of dates.  Values are +link{dateRange.start, start}
// and +link{dateRange.end, end}.  If either value is omitted, the range is assumed to be
// open-ended in that direction - so if dateRange.start is omitted, the range will include any
// date earlier than the value specified in dateRange.end.
//
// @visibility external
//<

//> @attr dateRange.start (RelativeDate or Date : null : IR)
// The start of this DateRange.
// @visibility external
//<

//> @attr dateRange.end (RelativeDate or Date : null : IR)
// The end of this DateRange.
// @visibility external
//<

//> @attr dateRangeItem.fromTitle (string : "From" : IR)
// The title for the +link{dateRangeItem.fromField, from} part of the range.
// @visibility external
// @group i18nMessages
//<
fromTitle: "From",

//> @attr dateRangeItem.toTitle (string : "To" : IR)
// The title for the +link{dateRangeItem.toField, to} part of the range.
// @visibility external
// @group i18nMessages
//<
toTitle: "To",

//> @attr dateRangeItem.innerTitleOrientation (TitleOrientation : null : IR)
// The title orientation for the to / from sub-items. If unset this will be derived from
// +link{FormItem.titleOrientation,this.titleOrientation} or
// +link{DynamicForm.titleOrientation,this.form.titleOrientation}.
// @visibility external
//<
//innerTitleOrientation:null,

//> @attr dateRangeItem.allowRelativeDates (Boolean : false : IR)
// Whether to allow the user to specify relative dates (via +link{RelativeDateItem}s) or whether
// dates are absolute (via +link{DateItem}s).
// @visibility external
//<
allowRelativeDates: false,

//> @attr dateRangeItem.fromField (AutoChild FormItem : null : IR)
// The field for the "from" date - a +link{RelativeDateItem} or +link{DateItem} according to
// +link{allowRelativeDates}.
// @visibility external
//<

//> @attr dateRangeItem.fromFieldProperties (FormItem Properties: null : IR)
// Additional property-block to customize the +link{dateRangeItem.fromField, fromField}
// AutoChild.
// @visibility internal
//<

//> @attr dateRangeItem.toField (AutoChild FormItem : null : IR)
// The field for the "to" date - a +link{RelativeDateItem} or +link{DateItem} according to
// +link{allowRelativeDates}.
// @visibility external
//<

//> @attr dateRangeItem.toFieldProperties (FormItem Properties: null : IR)
// Additional property-block to customize the +link{dateRangeItem.toField, toField}
// AutoChild.
// @visibility internal
//<

//> @attr dateRangeItem.fromDate (Date or RelativeDateString or TimeUnit : today : IRW)
// Initial value for the "from" date.
// @setter setFromDate
// @visibility external
//<
//fromDate: "$today",

//> @method dateRangeItem.setFromDate()
// Sets the +link{fromDate} for this DateRangeItem.
// @param fromDate (Date) the date from which this item should start it's range
// @visibility external
//<
setFromDate : function (fromDate) {
    this.fromDate = fromDate;
    if (this.fromField) this.fromField.setValue(this.fromDate);
},

//> @attr dateRangeItem.toDate (Date or RelativeDateString or TimeUnit : today : IRW)
// Initial value for the "to" date.
// @setter setToDate
// @visibility external
//<
//toDate: "$today",

//> @method dateRangeItem.setToDate()
// Sets the +link{toDate} for this DateRangeItem.
// @param fromDate (Date) the date at which this item should end it's range
// @visibility external
//<
setToDate : function (toDate) {
    this.toDate = toDate;
    if (this.toField) this.toField.setValue(this.toDate);
},

//> @attr dateRangeItem.dateInputFormat (DateInputFormat : null : IR)
// Format for direct user input of date values.
// <P>
// If unset, the input format will be determined based on the specified
// +link{dateDisplayFormat} if possible, otherwise picked up from the Date class (see
// +link{Date.setInputFormat()}).
//
// @deprecated This property is supported but
// the standard +link{dateFormatter} and +link{inputFormat}
// may now be used to specify date formatting parsing behavior for dateRangeItems
//
// @visibility external
//<

//> @attr dateRangeItem.inputFormat (DateInputFormat : null : IR)
// @include FormItem.inputFormat
//<

//> @attr dateRangeItem.dateDisplayFormat (DateDisplayFormat : null : IR)
// Format for displaying dates in to the user.
// Defaults to the system-wide default established by +link{Date.setDefaultDisplayFormat()}.
//
// @visibility external
//
// @deprecated This property is supported but
// the standard +link{dateFormatter} and +link{inputFormat}
// may now be used to specify date formatting parsing behavior for dateRangeItems
//
//<

//> @method dateRangeItem.hasAdvancedCriteria()
// Overridden to return true: dateRangeItems always generate AdvancedCriteria.
// @return (Boolean) true
// @visibility external
// @group criteriaEditing
//<
hasAdvancedCriteria : function () {
    return this.fromField && this.toField &&
        (this.fromField.getValue() != null || this.toField.getValue() != null);
},

//> @method dateRangeItem.getCriterion()
// Returns the Criterion entered in the date field.
// <P>
// A Criterion with an "and" +link{type:OperatorId,operator} will be
// returned with both a "greaterOrEqual" and "lessOrEqual" sub-criteria.  If either date is
// omitted, only the "greaterOrEqual" (from date) or "lessOrEqual" (to date) Criterion is
// included.
//
// @return (Criterion)
//
// @group criteriaEditing
// @visibility external
//<

getCriterion : function (absolute) {

    if (this.validateCriteria) {
        if (!this.validateRange()) return null;
    }

    absolute = absolute || !this.allowRelativeDates;

    var fromValue = absolute ? this.fromField.getValue() :
            this.fromField.getRelativeDate() || this.fromField.getValue(),

        hasFromValue = fromValue != null,

        toValue = absolute ? this.toField.getValue() :
            this.toField.getRelativeDate() || this.toField.getValue(),

        hasToValue = toValue != null,
        result = null
    ;

    if (hasFromValue || hasToValue) {
        // return an AdvanvedCriteria with one or two subCriteria
        result = { _constructor: "AdvancedCriteria", operator: "and", criteria: [ ] };

        if (hasFromValue) {
            if (isc.DateUtil.isRelativeDate(fromValue)) {
                fromValue.rangePosition = "start";
            }
            if (this.fromField.isLogicalDate) {
                fromValue.logicalDate = true;
            }
            result.criteria.add({
                fieldName: this.getCriteriaFieldName(),
                operator: "greaterOrEqual",
                value: fromValue
            });
        }
        if (hasToValue) {
            if (isc.DateUtil.isRelativeDate(toValue)) {
                toValue.rangePosition = "end";
            }
            if (this.toField.isLogicalDate) {
                toValue.logicalDate = true;
            }
            result.criteria.add({
                fieldName: this.getCriteriaFieldName(),
                operator: "lessOrEqual",
                value: toValue
            });
        }
    }

    return result;
},

//> @attr dateRangeItem.validateCriteria (Boolean : false :IRW)
// If this attribute is set to <code>true</code> when +link{getCriteria()} is
// called, the item will validate the <i>"to"</i> and <i>"from"</i> fields and
// return null if either field fails validation.
// See +link{dateRangeItem.validateRange()}
// @visibility external
//<
validateCriteria:true,

//> @method dateRangeItem.validateRange()
// Validate both <i>"to"</i> and <i>"from"</i> date-fields.
// @return (Boolean) false if either <i>to</i> or <i>from</i>
//   field contains an invalid date value.
// @visibility external
//<
validateRange : function () {
    var success = true;
    if (this.fromField && !this.fromField.validate()) success = false;
    if (this.toField && !this.toField.validate()) success = false;
    return success;
},

//> @method dateRangeItem.canEditCriterion()
// Returns true if the specified criterion contains:
// <ul><li>A single "lessOrEqual" or "greaterOrEqual" criterion on this field</li>
//     <li>An "and" type criterion containing a "lessOrEqual" and a "greaterOrEqual" criterion
//         on this field</li>
//     <li>A single "equals" criterion.  Internally, this will be converted into a range
//         by constructing an "and" type criterion containing both a "lessOrEqual" and
//         a "greaterOrEqual" criterion on this field.  Note that subsequent calls to
//         +link{dateRangeItem.getCriterion(), getCriterion()} will return this more complex
//         criterion.</li>
// </ul>
// @param criterion (Criterion) criterion to test
// @return (boolean) returns true if this criterion can be edited by this item
// @group criteriaEditing
// @visibility external
//<
canEditCriterion : function (criterion) {

    if (criterion == null) return false;
    var dateField = this.getCriteriaFieldName();
    if (criterion.operator == "and") {
        var innerCriteria = criterion.criteria;
        // we always produce one or 2 criteria only (to and from date range) - note, however,
        // that we can also edit an "equals" criterion, by first converting it into a range

        if (innerCriteria.length == 0 || innerCriteria.length > 2) {
            return false;
        } else if (innerCriteria.length == 1) {
            var crit = innerCriteria[0];
            if (crit.fieldName != dateField) return false;
            if (crit.operator == "equals") {
                this.logWarn("DynamicForm editing Advanced criteria. Includes criterion for " +
                    "field " +  dateField + ". A dateRange editor is showing for this field and " +
                    "the existing criteria has operator: " + crit.operator + ". DateRange " +
                    "items can only edit criteria greaterThan/greaterOrEqual or lessThan/lessOrEqual. "+
                    "However, for the 'equals' operator, a dateRange will be constructed for you, " +
                    "as greaterOrEqual to [value] and lessOrEqual to [value], ie, one day.");
                return true;
            }
        }
        for (var i = 0; i < innerCriteria.length; i++) {
            var innerCriterion = innerCriteria[i];

            // other field - just bail
            if (innerCriterion.fieldName != dateField) return false;

            // wrong operator - bail, but with a warning since this could confuse a
            // developer
            if (innerCriterion.operator != "greaterThan" && innerCriterion.operator != "greaterOrEqual"
                && innerCriterion.operator != "lessThan" && innerCriterion.operator != "lessOrEqual")
            {
                this.logWarn("DynamicForm editing Advanced criteria. Includes criterion for " +
                    "field " +  dateField + ". A dateRange editor is showing for this field but " +
                    "the existing criteria has operator:" + innerCriterion.operator + ". DateRange " +
                    "items can only edit criteria greaterThan/greaterOrEqual or lessThan/lessOrEqual " +
                    "so leaving this unaltered.");
                return false;
            }
        }
        // Only contains a range, with one or 2 values, so we'll allow editing of that.
        return true;

    // single criterion matching to or from of range.. We support that..
    } else if (criterion.fieldName == dateField) {
        var message = "DynamicForm editing Advanced criteria. Includes criterion for " +
                "field " +  dateField + ". A dateRange editor is showing for this field and " +
                "the existing criteria has operator:" + criterion.operator + ". DateRange " +
                "items can only edit criteria greaterThan/greaterOrEqual or lessThan/lessOrEqual";

        if (criterion.operator == "equals") {
            this.logWarn(message + ". However, for the 'equals' operator, a dateRange will be " +
                "constructed for you, as greaterOrEqual to [value] and lessOrEqual to [value], " +
                "ie, one day.");
            return true;
        }

        if (criterion.operator != "greaterThan" && criterion.operator != "greaterOrEqual"
            && criterion.operator != "lessThan" && criterion.operator != "lessOrEqual")
        {
            this.logWarn(message +  " so leaving this unaltered.");
            return false;
        }
        return true;
    }

    // in this case it's not on our field at all
    return false;
},

//> @method dateRangeItem.setCriterion()
// Applies the specified criterion to this item for editing. Applies any specified
// "greaterOrEqual" operator criterion or sub-criterion to our +link{fromField} and any
// specified "lessOrEqual" operator criterion or sub-criterion to our +link{toField}.
// <P>
// Note that a single "equals" criterion can also be passed.  See
// +link{dateRangeItem.canEditCriterion, canEditCriterion()} for more detail.
// @param criterion (Criterion) criterion to edit
// @group criteriaEditing
// @visibility external
//<
setCriterion : function (criterion) {
    if (!criterion) return;

    if (criterion.operator == "equals") {
        // handle "equals" criterion by constructing an AdvancedCriteria that represents a
        // genuine range
        var newCrit = { _constructor: "AdvancedCriteria", operator: "and",
            criteria: [
                { fieldName: criterion.fieldName, operator: "greaterOrEqual", value: criterion.value },
                { fieldName: criterion.fieldName, operator: "lessOrEqual", value: criterion.value }
            ]};
        criterion = newCrit;
    }

    var fromCrit, toCrit;
    if (criterion.operator == "and") {
        fromCrit = criterion.criteria.find("operator", "greaterThan");
        if (!fromCrit) fromCrit = criterion.criteria.find("operator", "greaterOrEqual");
        toCrit = criterion.criteria.find("operator", "lessThan");
        if (!toCrit) toCrit = criterion.criteria.find("operator", "lessOrEqual");
    } else {
        if (criterion.operator == "greaterThan") fromCrit = criterion;
        else if (criterion.operator == "greaterOrEqual") fromCrit = criterion;
        else if (criterion.operator == "lessThan") toCrit = criterion;
        else if (criterion.operator == "lessOrEqual") toCrit = criterion;
    }

    // just call setValue on the relevant items.
    // If we're showing relative date items they should handle being passed an absolute
    // date value
    if (fromCrit != null) {
        this.fromField.setValue(fromCrit.value);
    }
    if (toCrit != null) {
        this.toField.setValue(toCrit.value);
    }
},

//> @attr dateRangeItem.dateRangeForm (AutoChild DynamicForm : null : R)
// +link{DynamicForm} +link{AutoChild} automatically created by the dateRangeItem and applied
// to the item as +link{canvasItem.canvas,this.canvas}.<P>
// This DynamicForm contains the "from" and "to" fields the user will interact with to actually
// select a date-range. Note that as a standard autoChild, developers may customize this form
// by modifying <code>dateRangeProperties</code>.
//
// @visibility external
//<
dateRangeFormDefaults: {
    _constructor: "DynamicForm",
    margin: 0,
    padding: 0,
    itemChanged : function (item, newValue) {
        var values = this.getValues(),
            dateRange = {_constructor:"DateRange"};
        if (values.fromField != null) dateRange.start = values.fromField;
        if (values.toField != null) dateRange.end = values.toField;

        this.creator._updateValue(dateRange);
    }
}

});

isc.DateRangeItem.addMethods({
    init : function () {
        this._createEditor();
        this.Super("init", arguments);
    },

    isEditable : function () {
        return true;
    },

    // If we get destroyed also wipe out our canvas
    autoDestroy:true,

    //> @attr DateRangeItem.invalidRangeErrorMessage (String : '"To" field value cannot be earlier than "From" field value.' : IR)
    // Error message to display if the user enters a date range where the "To" field value
    // is earlier than the "From" field value.
    // @visibility external
    // @group i18nMessages
    //<
    invalidRangeErrorMessage:"'To' field value cannot be earlier than 'From' field value.",

    _createEditor: function () {
        var ds;
        var dynProps = { _suppressColWidthWarnings: true };

        if (this.form.dataSource) { // Should be, otherwise how have we ended up with a complex field?
            ds = isc.DataSource.getDataSource(this.form.dataSource);
            var field = ds.getField(this.name);
            if (field) {
                dynProps.dataSource = ds.getFieldDataSource(field);
            }
        }

        if (this.form && this.form.showComplexFieldsRecursively) {
            dynProps.showComplexFields = true;
            dynProps.showComplexFieldsRecursively = true;
        } else {
            dynProps.showComplexFields = false;
        }

        dynProps.height = 22;

        var titleOrientation = this.innerTitleOrientation || this.titleOrientation ||
                this.form.titleOrientation || "left";

        dynProps.titleOrientation = titleOrientation;
        if (titleOrientation == "left" || titleOrientation == "right") {
            dynProps.numCols = 2;
            dynProps.colWidths = [50, "*"];
        } else {
            dynProps.numCols = 1;
            dynProps.colWidths = ["*"];
        }

        this.addAutoChild("dateRangeForm", dynProps);
        this.canvas = this.dateRangeForm;

        // set a default baseDate is one wasn't provided
        var type = this.getType(),
            isLogicalDate = false;
        if (isc.SimpleType.inheritsFrom(type, "date") &&
            !isc.SimpleType.inheritsFrom(type, "datetime"))
        {
            isLogicalDate = true;
        }
        this.baseDate = this.baseDate ||
                        (isLogicalDate ? isc.Date.createLogicalDate() : new Date());

        var _this = this,
            _constructor = this.allowRelativeDates ? "RelativeDateItem" : "DateItem",
            items = []
        ;

        items[0] = isc.addProperties({}, this.fromFieldDefaults, this.fromFieldProperties,
            {
                name: "fromField", _constructor: _constructor, baseDate: this.baseDate,
                // dateDisplayFormat and dateInputFormat are basically there for back-compat only
                dateFormatter:(this.dateDisplayFormat || this.dateFormatter),

                type:this.getType(),

                inputFormat: (this.dateInputFormat || this.inputFormat),
                rangePosition: "start",
                title: this.fromTitle,
                defaultValue: this.fromDate,
                useTextField: (_constructor == "DateItem" ? true : null)
            }
        );
        items[1] = isc.addProperties({}, this.toFieldDefaults, this.toFieldProperties,
            {
                name: "toField", _constructor: _constructor, baseDate: this.baseDate,
                dateFormatter:(this.dateDisplayFormat || this.dateFormatter),
                type:this.getType(),
                inputFormat: (this.dateInputFormat || this.inputFormat),
                rangePosition: "end",
                // Add a greater-than validator to enforce valid ranges
                validators:[
                    {type:"custom",
                     errorMessage:this.invalidRangeErrorMessage,
                     condition:function (item, validator, value, record) {
                        if (value != null && isc.isA.Date(value)) {
                            var fromDate = record.fromField;
                            if (fromDate != null && isc.isA.Date(fromDate) &&
                                Date.compareDates(fromDate, value) < 0)
                            {
                                return false;
                            }
                        }
                        return true;
                     }
                    }
                ],
                title: this.toTitle,
                defaultValue: this.toDate,
                useTextField: (_constructor == "DateItem" ? true : null)
            }
        );

        this.canvas.setFields(items);

        this.toField = this.canvas.getField("toField");
        this.fromField = this.canvas.getField("fromField");

        if (this.allowRelativeDates) {
            this.fromField.canvas._nextTabWidget = this.toField.canvas;
            this.toField.canvas._previousTabWidget = this.fromField.canvas;
        }

        if (this.defaultValue) {
            this.setValue(this.defaultValue);
        } else {
            if (this.fromDate) this.setFromDate(this.fromDate);
            if (this.toDate) this.setToDate(this.toDate);
        }
    },

    fieldChanged : function () {
    },

    //> @method dateRangeItem.setValue()
    // Sets the value for this dateRangeItem.  The value parameter is a
    // +link{object:DateRange} object that optionally includes both start and end values.  If
    // passed null, both start- and end-range values are cleared.
    // @param value (DateRange) the new value for this item
    // @visibility external
    //<
    setValue : function (value) {

        var start = value ? value.start : null,
            end = value ? value.end : null,
            RDI = isc.RelativeDateItem;

        if (!this.allowRelativeDates && RDI.isRelativeDate(start)) this.setFromDate(null);
        else this.setFromDate(start);
        if (!this.allowRelativeDates && RDI.isRelativeDate(end)) this.setToDate(null);
        else this.setToDate(end);
        this.Super("setValue", arguments);
    },

    //> @method dateRangeItem.getValue()
    // Retrieves the current value of this dateRangeItem.  The return value is a
    // +link{object:DateRange} object that excludes start and end values if they aren't
    // set.
    // @return (DateRange) the current value of this item
    // @visibility external
    //<
    getValue : function () {
        if (!this.fromField || !this.toField) return;
        var isRelative = this.allowRelativeDates,
            fromValue = isRelative && this.fromField.getRelativeDate() ?
                this.fromField.getRelativeDate() : this.fromField.getValue(),
            toValue = isRelative && this.toField.getRelativeDate() ?
                this.toField.getRelativeDate() : this.toField.getValue(),
            result = {_constructor:"DateRange"};
        if (fromValue == null && toValue == null) return null;
        if (fromValue != null) result.start = fromValue;
        if (toValue != null) result.end = toValue;

        return result;
    },
    destroy : function () {
        if (this.dateRangeForm) this.dateRangeForm.destroy();
        this.Super("destroy", arguments);
    }

});


if (isc.Window) {
// dateRangeDialog and miniDateRangeItem require isc.Window

//> @class DateRangeDialog
// Simple modal dialog for collecting a date range from the end user.
//
// @visibility external
//<

isc.defineClass("DateRangeDialog", "Window");


isc.DateRangeDialog.addClassMethods({
//> @classMethod DateRangeDialog.askForRange()
// Helper method to launch a DateRangeDialog to have a date range input by the user.
// @param allowRelativeDates (boolean) whether to allow relative date entry via
//                                    +link{RelativeDateItem}s, default true
// @param rangeItemProperties (DateRangeItem Properties) properties for the DateRangeItem
// @param windowProperties (DateRangeDialog Properties) properties for the Window
// @param callback (Callback) method to fire once user has input values, with a single parameter
//                           "criterion" of type +link{Criterion}
//
// @visibility external
//<
askForRange : function (allowRelativeDates, rangeItemProperties, windowProperties, callback) {
    var drd = isc.DateRangeDialog.create({
        allowRelativeDates: allowRelativeDates != null ? allowRelativeDates : true,
        rangeItemProperties: rangeItemProperties,
        callback: callback
    }, windowProperties);

    drd.show();
}
});

isc.DateRangeDialog.addProperties({
isModal: true,
showModalMask: true,
dismissOnEscape: true,
autoCenter: true,
autoSize: true,
vertical: "true",
showMinimizeButton: false,
headerIconProperties: {
    src: "[SKIN]/DynamicForm/DatePicker_icon.gif"
},

returnCriterion: false,

//> @attr dateRangeDialog.headerTitle (String : "Select Date Range" : IR)
// The title to display in the header-bar of this Dialog.
//
// @visibility external
// @group i18nMessages
//<
headerTitle: "Select Date Range",

mainLayoutDefaults: {
    _constructor: "VLayout",
    width: 380,
    height: 105,
    layoutMargin: 5
},

rangeFormDefaults: {
    _constructor: "DynamicForm",
    numCols: 1,
    height: "100%",
    autoParent: "mainLayout"
},

//> @attr dateRangeDialog.rangeItem (AutoChild DateRangeItem : null : IR)
//
// @visibility external
//<
rangeItemDefaults: {
    _constructor: "DateRangeItem",
    allowRelativeDates: true,
    showTitle: false
},

buttonLayoutDefaults: {
    _constructor: "HLayout",
    width: "100%",
    height: 22,
    layoutAlign: "right",
    align: "right",
    membersMargin: 5,
    autoParent: "mainLayout"
},

//> @attr dateRangeDialog.clearButtonTitle (string : "Clear" : IR)
// The title for the "Clear" button on this dialog.
// @visibility external
// @group i18nMessages
//<
clearButtonTitle: "Clear",
//> @attr dateRangeDialog.clearButton (AutoChild IButton : null : IR)
// Button used for clearing the dialog's values.  Note that, since this is an +link{AutoChild},
// it can be configured using clearButtonDefaults and clearButtonProperties.
// @visibility external
//<
clearButtonDefaults: {
    _constructor: "IButton",
    height: 22,
    width: 80,
    canFocus:true,
    autoParent: "buttonLayout",
    click : function () {
        this.creator.clearValues();
    }
},

//> @attr dateRangeDialog.okButtonTitle (string : "OK" : IR)
// The title for the "OK" button on this dialog.
// @visibility external
// @group i18nMessages
//<
okButtonTitle: "OK",
//> @attr dateRangeDialog.okButton (AutoChild IButton : null : IR)
// Button used for accepting the values entered into the dialog.  Note that, since this is an
// +link{AutoChild}, it can be configured using okButtonDefaults and okButtonProperties.
// @visibility external
//<
okButtonDefaults: {
    _constructor: "IButton",
    height: 22,
    width: 80,
    canFocus:true,
    autoParent: "buttonLayout",
    click : function () {
        this.creator.accept();
    }
},

//> @attr dateRangeDialog.cancelButtonTitle (string : "Cancel" : IR)
// The title for the "Cancel" button on this dialog.
// @visibility external
// @group i18nMessages
//<
cancelButtonTitle: "Cancel",
//> @attr dateRangeDialog.cancelButton (AutoChild IButton : null : IR)
// Button used for cancelling the dialog.  Note that, since this is an +link{AutoChild}, it can
// be configured using cancelButtonDefaults and cancelButtonProperties.
// @visibility external
//<
cancelButtonDefaults: {
    _constructor: "IButton",
    height: 22,
    width: 80,
    canFocus:true,
    autoParent: "buttonLayout",
    click : function () {
        this.creator.cancel();
    }
},

// Pipe the standard top-left close icon through to our cancel method
closeClick : function () {
    this.cancel();
},

destroyOnClose: true,

destroy : function () {
    if (this.rangeForm) {
        this.rangeForm.markForDestroy();
    }
    this.Super("destroy", arguments);
}

});

isc.DateRangeDialog.addMethods({
    initWidget : function () {
        this.title = this.headerTitle;

        this.Super("initWidget", arguments);
        this.addAutoChild("mainLayout");

        if (!this.type) this.type = "date";

        var itemProps = isc.addProperties(
            {_constructor: this.rangeItemConstructor},
            this.rangeItemDefaults, this.rangeItemProperties,
            { name: "rangeItem", fromDate: this.fromDate, toDate: this.toDate,
                type: this.type,
                dateDisplayFormat: this.dateDisplayFormat
            }
        );

        this.addAutoChild("rangeForm",
            {
                _suppressColWidthWarnings: true,
                items: [ itemProps ]
            }
        );

        var rangeItem = this.rangeItem = this.rangeForm.getField("rangeItem");

        var titleOrientation = rangeItem.innerTitleOrientation || rangeItem.titleOrientation ||
                "left";

        rangeItem.canvas.titleOrientation = titleOrientation;
        if (titleOrientation == "left" || titleOrientation == "right") {
            rangeItem.canvas.numCols = 2;
            rangeItem.canvas.colWidths = [50, "*"];
        } else {
            rangeItem.canvas.numCols = 1;
            rangeItem.canvas.colWidths = ["*"];
        }

        this.addAutoChild("buttonLayout");

        this.addAutoChild("clearButton", { canFocus:true, title: this.clearButtonTitle});
        this.addAutoChild("okButton", { canFocus:true, title: this.okButtonTitle});
        this.addAutoChild("cancelButton", { canFocus:true, title: this.cancelButtonTitle});
        this.addItem(this.mainLayout);
    },

    clearValues : function () {
        if (this.rangeItem) {
            this.rangeItem.setValue(null);
            // clear validation errors!
            if (this.autoValidate) this.rangeItem.validateRange();
        }

    },

    accept : function () {
        if (this.autoValidate && !this.rangeItem.validateRange()) return;
        this.finished(
            this.rangeItem.returnCriterion ? this.rangeItem.getCriterion() : this.rangeItem.getValue()
        );
    },

    cancel : function () {
        this.hide();
        if (this.destroyOnClose) this.markForDestroy();
    },

    finished : function (value) {
        if (this.callback) this.fireCallback(this.callback, "value", [value]);
        this.hide();
        if (this.destroyOnClose) this.markForDestroy();
    }

});

//> @class MiniDateRangeItem
// Provides a compact interface for editing a date range, by providing a formatted, read-only
// display of the current selected date range with an icon to launch a +link{DateRangeDialog}
// to edit the range.
//
// @visibility external
//<
isc.defineClass("MiniDateRangeItem", "StaticTextItem");

isc.MiniDateRangeItem.addProperties({

//> @attr miniDateRangeItem.textBoxStyle (FormItemBaseStyle : "textItem" : IRW)
// @include formItem.textBoxStyle
//<
textBoxStyle:"textItem",

applyHeightToTextBox:true,

clipValue: true,
wrap: false,
iconVAlign: "top",
height: 20,
width: 100,

//> @attr miniDateRangeItem.shouldSaveValue (Boolean : true : IR)
// Allow miniDateRangeItems' values to show up in the form's values array, or if
// +link{dynamicForm.getValuesAsCriteria()} is called, for the criterion to be included
// in the returned AdvancedCriteria object
// @visibility external
//<
shouldSaveValue:true,

//> @attr miniDateRangeItem.rangeDialog (AutoChild DateRangeDialog : null : IR)
// Pop-up +link{DateRangeDialog} for entering a date range.
//
// @visibility external
//<
rangeDialogDefaults: {
    _constructor: "DateRangeDialog",
    autoDraw: false,
    destroyOnClose: false
},

//> @attr miniDateRangeItem.canFocus (Boolean : true : IR)
// MiniDateRangeItems are marked as canFocus:true, and set up with properties such that focus
// will always go to the icon to launch the dateRange dialog. Set canFocus to false to
// suppress this behavior.
// @visibility external
//<
canFocus:true,
// these overrides ensure focus goes to the picker icon (actually this.icons[0]) rather than
// us writing tab-order properties into the static div.
getFocusElement : function () {
    return this._getIconLinkElement(this.icons[0]);
},
_canFocusInTextBox : function () {
    return false;
},

canTabToIcons:true,

//> @attr miniDateRangeItem.fromDateOnlyPrefix (string : "Since" : IR)
// The text to prepend to the formatted date when only a +link{fromDate, fromDate} is supplied.
// @visibility external
// @group i18nMessages
//<
fromDateOnlyPrefix: "Since",

//> @attr miniDateRangeItem.toDateOnlyPrefix (string : "Before" : IR)
// The text to prepend to the formatted date when only a +link{toDate, toDate} is supplied.
// @visibility external
// @group i18nMessages
//<
toDateOnlyPrefix: "Before",

//> @attr miniDateRangeItem.pickerIconPrompt (String : "Show Date Chooser" : IR)
// The prompt to show when the mouse is hovered over the +link{pickerIcon}.
//
// @visibility external
// @group i18nMessages
//<
pickerIconPrompt: "Show Date Chooser",

//> @attr miniDateRangeItem.pickerIcon (FormItemIcon Properties : null : IR)
// Icon that launches a +link{DateChooser} for choosing an absolute date.
//
// @visibility external
//<
pickerIconDefaults: {
    name: "showDateRange",
    src: "[SKIN]/DynamicForm/DatePicker_icon.gif",
    width: 16, height: 16,
    showOver: false,
    showFocused: false,
    showFocusedWithItem: false,
    hspace: 0,
    click : function (form, item, icon) {
        if (!item.isReadOnly()) item.showRangeDialog();
    }
},

iconVAlign: "center",

//> @attr miniDateRangeItem.allowRelativeDates (Boolean : true : IR)
// Whether the +link{DateRangeDialog} opened when the
// +link{miniDateRangeItem.pickerIcon, pickerIcon} is clicked should display
// +link{RelativeDateItem}s or +link{DateItem}s.
//
// @visibility external
//<
allowRelativeDates: true,

//> @attr miniDateRangeItem.dateDisplayFormat (DateDisplayFormat : null : IR)
// Format for displaying dates to the user.
// <P>
// If this attribute is unset, the display value is formatted intelligently according to the
// dates involved.  For example, if both dates appear in the same month, the value will be
// formatted as
// <P><code>Month date1 - date2, Year</code>
// <P>and, if in different months of the same year,
// <P><code>Month1 date1 - Month2 date2, Year</code>.
// <P>If either date-value is unset, the display-value is formatted according to
// +link{miniDateRangeItem.fromDateOnlyPrefix, fromDateOnlyPrefix} and
// +link{miniDateRangeItem.toDateOnlyPrefix, toDateOnlyPrefix}.
//
// @visibility external
//<

//> @attr miniDateRangeItem.fromDate (Date or RelativeDateString or TimeUnit : today : IRW)
// Initial value for the "from" date.
// @setter setFromDate
// @visibility external
//<
//fromDate: "$today",

//> @attr miniDateRangeItem.toDate (Date or RelativeDateString or TimeUnit : today : IRW)
// Initial value for the "to" date.
// @setter setFromDate
// @visibility external
//<
//toDate: "$today",


handleClick : function () {
    if (!this.isReadOnly()) this.showRangeDialog();
}

});

isc.MiniDateRangeItem.addMethods({
    init : function () {

        var type = this.getType();

        // combine any specified rangeItemProperties with the ones we need to pass
        var rangeItemProps = isc.addProperties({},
            this.rangeDialogProperties ? this.rangeDialogProperties.rangeItemProperties : null,
            {
                allowRelativeDates: this.allowRelativeDates,
                criteriaField: this.criteriaField,
                type: type
            }
        );

        this.addAutoChild("rangeDialog",
            {
                type: type,
                autoValidate:this.autoValidate,
                fromDate: this.fromDate,
                toDate: this.toDate,
                rangeItemProperties: rangeItemProps,
                dateDisplayFormat: this.dateDisplayFormat,
                callback: this.getID()+".rangeDialogCallback(value)"
            }
        );


        this.icons = [
            isc.addProperties({ prompt: this.pickerIconPrompt },
                this.pickerIconDefaults, this.pickerIconProperties
            )
        ];

        this.canTabToIcons = true;

        this.rangeItem = this.rangeDialog.rangeItem;
        this.rangeItem.name = this.name;

        if (this.defaultValue) {
            this.setValue(this.defaultValue);
        }
    },



    //> @attr miniDateRangeItem.autoValidate (Boolean : true : IRW)
    // If this attribute is set to true, the pop up date range dialog will automatically
    // validate the user-entered <i>"to"</i> and <i>"from"</i> values on
    // <code>"OK"</code>-click, and refuse to dismiss if these items contain invalid values.
    // @visibility external
    //<
    autoValidate:true,

    //> @method miniDateRangeItem.setAutoValidate()
    // Setter for +link{miniDateRangeItem.autoValidate}
    // @param autoValidate (boolean) New auto-validate setting.
    // @visibility external
    //<
    setAutoValidate : function (autoValidate) {
        this.autoValidate = autoValidate;
        if (this.rangeDialog) this.rangeDialog.autoValidate = this.autoValidate;
    },

    showRangeDialog : function () {
        this.rangeDialog.rangeItem.setFromDate(this.fromDate);
        this.rangeDialog.rangeItem.setToDate(this.toDate);
        this.rangeDialog.show();
    },

    rangeDialogCallback : function (value) {
        if (!this._updateValue(value)) return;
        this.displayValue(value);
    },

    //> @method miniDateRangeItem.hasAdvancedCriteria()
    // @include dateRangeItem.hasAdvancedCriteria()
    // @group criteriaEditing
    //<
    hasAdvancedCriteria : function () {
        return this.rangeItem != null && this.rangeItem.hasAdvancedCriteria();
    },

    //> @method miniDateRangeItem.getCriterion()
    // Returns the Criterion entered in the fields shown in the
    // +link{miniDateRangeItem.rangeDialog}.
    // <P>
    // If both dates are entered, a Criterion with an "and" +link{type:OperatorId,operator}
    // and both "greaterOrEqual" and "lessOrEqual" sub-criteria will be returned.  If either
    // date is omitted, only the "greaterOrEqual" (from date) or "lessOrEqual" (to date)
    // Criterion is returned.
    //
    // @return (Criterion)
    //
    // @group criteriaEditing
    // @visibility external
    //<
    getCriterion : function () {
        var criteria = this.rangeItem ? this.rangeItem.getCriterion() : null;
        return criteria;
    },

    //> @method miniDateRangeItem.setCriterion()
    // @include dateRangeItem.setCriterion()
    // @visibility external
    //<
    setCriterion : function (criterion) {
        if (this.rangeItem) {
            this.rangeItem.setCriterion(criterion);
            var value = this.rangeItem.getValue();

            // call superclass 'setValue()' to update display and store
            // the new DateRange value derived from the criterion passed in
            // Pass flag to suppress updating the dateRangeItem again
            this.setValue(value, null, true);
        }

    },

    //> @method miniDateRangeItem.canEditCriterion()
    // @include dateRangeItem.canEditCriterion()
    // @visibility external
    //<
    canEditCriterion : function (criterion) {
        return this.rangeItem ? this.rangeItem.canEditCriterion(criterion) : false;
    },

    //> @method miniDateRangeItem.setValue()
    // Sets the value for this miniDateRangeItem.  The value parameter is a
    // +link{object:DateRange} object that optionally includes both start and end values.
    // @param value (DateRange) the new value for this item
    // @visibility external
    //<
    setValue : function (value, allowNull, fromRangeItem) {
        // update this.fromDate / this.toDate
        this.updateStoredDates(value);

        // update the rangeItem
        if (!fromRangeItem) {
            this.rangeItem.setFromDate(this.fromDate);
            this.rangeItem.setToDate(this.toDate);
        }
        // this will store the value in the DynamicForm values object, and
        // refresh to display the value
        var newArgs = [this.getValue()];
        this.Super("setValue", newArgs, arguments);
    },

    updateStoredDates : function (value) {

        if (value != null) {
            if (isc.DataSource.isAdvancedCriteria(value)) {
                // value has come back as an AdvancedCriteria!
                var newValue = {};

                for (var i=0; i<value.criteria.length; i++) {
                    var criterion = value.criteria[i];
                    if (criterion.operator == "greaterThan" || criterion.operator == "greaterOrEqual")
                        newValue.start = criterion.value;
                    else if (criterion.operator == "lessThan" || criterion.operator == "lessOrEqual")
                        newValue.end = criterion.value;
                }
                value = newValue
            }

            this.fromDate = value.start;
            this.toDate = value.end;
        } else {
            this.fromDate = null;
            this.toDate = null;
            // the prompt is pre-built, based on the stored dates, so clear it now
            this.prompt = "";
        }
    },

    saveValue : function () {
        this.Super("saveValue", arguments);
        this.updateStoredDates(this._value);
    },

    // show the current value in our text box (called from setValue / updateValue)
    displayValue : function (value) {
        var displayValue = this.mapValueToDisplay(value) || "";
        this.setElementValue(displayValue, value);

    },

    mapValueToDisplay : function (value) {
        if (value == null) return "";
        var fromDate = value.start,
            toDate = value.end,
            RDI = isc.RelativeDateItem,
            start = (RDI.isRelativeDate(fromDate) ?
                RDI.getAbsoluteDate(fromDate.value, null, null, "start") : fromDate),
            end = (RDI.isRelativeDate(toDate) ?
                RDI.getAbsoluteDate(toDate.value, null, null, "end") : toDate)
        ;

        var prompt;
        if (start || end) {
            if (this.dateDisplayFormat) {
                if (start) prompt = this.formatDate(start);
                if (end) {
                    if (prompt) prompt += " - " + this.formatDate(end);
                    else prompt = this.formatDate(end);
                }
            } else prompt = Date.getFormattedDateRangeString(start, end);
            if (!start) prompt = this.toDateOnlyPrefix + " " + prompt;
            else if (!end) prompt = this.fromDateOnlyPrefix + " " + prompt;
        }
        this.prompt = prompt || "";
        return this.prompt;
    },

    //> @method miniDateRangeItem.getValue()
    // Retrieves the current value of this dateRangeItem.  The return value is a
    // +link{object:DateRange} object that excludes start and end values if they aren't
    // set.
    // @return (DateRange) the current value of this item
    // @visibility external
    //<
    getValue : function () {
        if (!this.rangeItem) return;
        return this.rangeItem.getValue();
    },

    // formatDate() - given a live date object, returns the formatted date string to display
    formatDate : function (date) {
        if (!isc.isA.Date(date)) return date;

        var type = this.getType(),
            isLogicalDate = isc.SimpleType.inheritsFrom(type, "date") &&
                            !isc.SimpleType.inheritsFrom(type, "datetime");
        return date.toShortDate(this._getDateFormatter(), !isLogicalDate);
    },

    getCriteriaValue : function () {
        return this.getCriterion();
    },

    destroy : function () {
        if (this.rangeDialog) this.rangeDialog.markForDestroy();
        this.Super("destroy", arguments);
    }


});


}


}


// NOTE: This class does not support composite primary keys




if (isc.Portal) {

isc.defineClass("EntityEditorHeader", "VLayout");

isc.EntityEditorHeader.addProperties({
    height: 1,
    padding: 10,
    border: "2px solid black",

    headerLayoutDefaults: {
        _constructor: "VLayout",
        width: "100%",
        height: 1,
        membersMargin: 5
    },

    headerLabelTitle: "<B><H2>Editing ${entityType}</H2><br>"+
        "<H3>This UI lets you edit the entire data-structure for this Entity-type</H3></B>",
    headerLabelDefaults: {
        _constructor: "Label",
        width: "100%",
        height: 30,
        autoParent: "headerLayout"
    },
    showDetailLabel: false,
    defaultDetailLabelTitle: "<B><H3>This UI lets you edit the entire data-structure for this Entity-type</H3></B>",
    detailLabelTitle: "<B><H3>$entityComment</H3></B>",
    detailLabelDefaults: {
        _constructor: "Label",
        width: "100%",
        height: 20,
        autoParent: "headerLayout"
    },

    unknownEntityTitle: "[Unknown Entity-type]",

    initWidget : function () {
        var headerTitle = this.headerLabelTitle;
        var detailTitle = this.detailLabelTitle;

        if (this.dataSource) this.getDataSource(this.dataSource);
        if (!this.entityName) this.entityName = this.getEntityName(this.dataSource);
        if (!this.entityComment) this.entityComment = this.getEntityComment(this.dataSource);

        if (this.entityName)
            headerTitle = headerTitle.evalDynamicString(this, { entityType: this.entityName });
        if (this.entityComment)
            detailTitle = detailTitle.evalDynamicString(this, { entityType: this.entityComment });

        this.headerLayout = this.addAutoChild("headerLayout");
        this.headerLabel = this.addAutoChild("headerLabel", {contents: headerTitle});
        this.detailLabel = this.addAutoChild("detailLabel", {contents: detailTitle});

        this.headerLayout.addMembers([this.headerLabel, this.detailLabel]);
        //this.headerLayout.addMember(this.detailLabel);

        this.addMember(this.headerLayout);
    },

    getEntityName : function (dataSource) {
        var result = this.unknownEntityTitle;

        if (dataSource) {
            if (isc.isA.Function(dataSource.getEntityName))
                result = dataSource.getEntityName();
            else result = dataSource.ID;
        }

        return result;
    },

    getEntityComment : function (dataSource) {
        var result = this.defaultDetailLabelTitle;

        if (dataSource) {
            if (isc.isA.Function(dataSource.getEntityComment))
                result = dataSource.getEntityComment();
            else result = "Allows hierarchical editing of data in "+dataSource.ID+" DataSource";
        }

        return result;
    }


});

isc.defineClass("EntityEditorForm", "Portlet").addProperties({
//isc.defineClass("EntityEditorForm", "VLayout").addProperties({
    //height: 1,
    //padding: 10,

    addButtonDefaults: {
        _constructor: "IButton",
        title: "Add",
        autoFit: true,
        layoutAlign: "right",
        click : function () {
            this.creator.addRecord();
        }
    },

    formDefaults: {
        _constructor: "DynamicForm",
        numCols: 6,
        colWidths: ["*", "*", "*", "*", "*", "*"],
        width: "100%",
        implicitSave: true,
        initWidget : function () {
            this.Super("initWidget", arguments);

            if (this.record && this.relation) {
                var criteria = {};
                if (!this.relation.direction) {
                        // top-level entity - fetch via direct pk criteria
                    criteria[this.relation.baseFieldName] = this.record[this.relation.baseFieldName];
                } else {
                    // we're a child-entity of some parent which refers to us by PK - fetch via a link
                    criteria[this.relation.baseFieldName] = this.record[this.relation.relatedFieldName];
                }
                this.fetchData(criteria);
            }
        },
        implicitSaveCallback : function (data) {
            if (!this.record || !this.record[this.relation.baseFieldName]) {
                this.creator.setRecord(data);
                this.setValues(data);
                this.saveOperationType = "update";
            }
        }
    },

    initWidget : function () {
        this.Super("initWidget", arguments);

        this.addAutoChild("addButton");

        this.addAutoChild("form",
            isc.addProperties({}, this.formProperties,
                {
                    dataSource: this.dataSource,
                    title: this.title,
                    record: this.record,
                    relation: this.relation
                }
            )
        );

        if (isc.isA.Portlet(this)) this.addItems([this.addButton, this.form]);
        else this.addMembers([this.addButton, this.form]);

    },

    fetchData : function () {
        var criteria = {};
        if (!this.relation.direction) {
            if (this.isTopLevel())
            // top-level entity - fetch via direct pk criteria
                criteria[this.relation.baseFieldName] = this.record[this.relation.baseFieldName];
        } else {
            // we're a child-entity of some parent which refers to us by PK - fetch via a link
            criteria[this.relation.baseFieldName] = this.record[this.relation.relatedFieldName];
        }
        this.form.fetchData(criteria);
    },

    addRecord : function () {
        var criteria = {};
        if (!this.relation.direction) {
            if (!this.isTopLevel())
                criteria[this.relation.baseFieldName] = this.record[this.relation.baseFieldName];
        } else {
            // we're a child-entity of some parent which refers to us by PK - fetch via a link
            criteria[this.relation.baseFieldName] = this.record[this.relation.relatedFieldName];
        }

        this.record = criteria;
        this.form.editNewRecord(criteria);
        //this.creator.updateTopLevel();
    },

    isTopLevel : function () {
        return this.relation.relatedDS == null;
    },

    setRecord : function (record) {
        if (this.isTopLevel()) {
            this.creator.updateTopLevel();
        } else {
            this.fetchData();
        }
        this.record = record;
        this.form.record = record;
        if (this.addButton) this.addButton.setDisabled(this.record == null);
    },

    getData : function () {
        return null;
    },

    getCriteria : function () {
        return this.form.getValuesAsCriteria();
    },

    enterSearchMode : function (criteria) {
        this.addButton.setDisabled(true);

        this.record = this.form.record = null;
        this.form.implicitSave = false;
        this.form.setData([]);
        if (criteria) this.form.setValues(criteria);
    },

    exitSearchMode : function () {
    }

});

isc.defineClass("EntityEditorGrid", "Portlet").addProperties({
//isc.defineClass("EntityEditorGrid", "VLayout").addProperties({
//    overflow: "visible",
//    autoSize: true,
    //padding: 10,

    addButtonDefaults: {
        _constructor: "IButton",
        title: "Add",
        autoFit: true,
        layoutAlign: "right",
        click : function () {
            this.creator.addRecord();
        }
    },

    gridDefaults: {
        _constructor: "ListGrid",
        width: "100%",
        height: "100%",
//        autoFitData: "vertical",
        autoFitMaxRecords: 4,
        autoFetchData: false,
        canEdit: true,
        autoSaveEdits: true,
        initWidget : function () {
            if (this.record && this.relation) {
                this.initialCriteria = {};
                this.initialCriteria[this.relation.baseFieldName] = this.record[this.relation.relatedFieldName];
            }

            this.Super("initWidget", arguments);
        }
    },

    initWidget : function () {

        this.addAutoChild("addButton");

        if (!isc.isA.DataSource(this.dataSource))
            this.dataSource = isc.DS.get(this.dataSource);

        this.addAutoChild("grid",
            isc.addProperties({}, this.gridProperties,
                {
                    dataSource: this.dataSource,
                    title: this.title,
                    record: this.record,
                    relation: this.relation
                }
            )
        );

        if (this.record) this.fetchData();

        if (this.addButton) this.addButton.setDisabled(this.record == null);

        if (isc.isA.Portlet(this)) this.addItems([this.addButton, this.grid]);
        else this.addMembers([this.addButton, this.grid]);

        this.Super("initWidget", arguments);
    },

    setData : function (data) {
    },

    fetchData : function () {
        var criteria = {};
        if (this.record) {
            // we're a child-entity of some parent which refers to us by PK - fetch via a link
            criteria[this.relation.baseFieldName] = this.record[this.relation.relatedFieldName];
        }
        this.grid.fetchData(criteria);
    },

    addRecord : function () {
        var criteria = {};
        // we're a child-entity of some parent which refers to us by PK - fetch via a link
        criteria[this.relation.baseFieldName] = this.record[this.relation.relatedFieldName];

        this.grid.startEditingNew(criteria);
    },

    setRecord : function (record) {
        this.record = record;
        this.fetchData();
        if (this.addButton) this.addButton.setDisabled(this.record == null);
    },

    getData : function () {
        return null;
    },

    getCriteria : function () {
        return this.grid.getFilterEditorCriteria();
    },

    enterSearchMode : function (criteria) {
        this.addButton.setDisabled(true);

        this.record = this.grid.record = null;
        //this.grid.clearCriteria();
        this.grid.setData([]);
        this.grid.setShowFilterEditor(true);
        //this.grid.redraw();
        //this.redraw();
        if (criteria) this.grid.setCriteria(criteria);
    },

    exitSearchMode : function () {
    }

});


// Entity Editor
// ---------------------------------------------------------------------------------------
// Interface for defining and editing a complete data-structure for a database entity.

//>    @class EntityEditor
// Interface for defining and editing a complete data-structure for a database entity.
//
// @visibility entityEditor
//<
isc.defineClass("EntityEditor", "VLayout");

isc.EntityEditor.addProperties({
    //height: 1,
    membersMargin: 10,
    padding: 10,

    //> @attr entityEditor.dataSource (DataSource : null : IR)
    // The dataSource providing the initial top-level Entity, which we'll check for
    // relational links and also display the data from those tables in appropriate UIs.
    //
    // @visibility entityEditor
    //<
    dataSource: "",

    modeFormDefaults: {
        _constructor: "DynamicForm",
        height: 1,
        width: "100%",
        fields: [
            {name: "edit", title: "options", defaultToFirstOption: true,
                valueMap: ["Edit", "Search"],
                changed : function (form, item, value) {
                    form.creator.setMode(value);
                }
            }
        ]
    },

    formEntityDefaults: {
        _constructor: "EntityEditorForm",
        height: 1,
        width: "100%"
    },

    gridEntityDefaults: {
        _constructor: "EntityEditorGrid",
        height: 1,
        width: "100%"
    },

    showTabset: false,
    tabsetDefaults: {
        _constructor: "TabSet",
        width: "100%",
        height: "100%"
    },

    //showPortal: false,
    portalDefaults: {
        _constructor: "PortalLayout",
        width: "100%",
        height: "100%",
        showColumnMenus: false,
        numColumns: 1
    },

    getDataSourceHierarchy : function (ds, doneList) {
        var result = [];

        if (!isc.isA.DataSource(ds))
            ds = isc.DS.getDataSource(ds);

        if (!isc.isA.DataSource(ds)) {
            this.logWarn("No datasource provided..");
            return result;
        }

        if (!doneList) doneList = [];

        if (doneList.contains(ds.ID)) {
            // already processed this DS - skip it now
            return null;
        }

        var rawFields = isc.getValues(ds.getFields()),
            dsFields = []
        ;

        for (var i=0; i<rawFields.length; i++) {
            if (rawFields[i].foreignKey != null) {
                dsFields.add(rawFields[i]);
            }
        }

        var dsName = ds.ID,
            localDoneList = [],
            fields = [];

        isc.DS.registerDataSource(ds);

        var childDSArray = ds.getChildDataSources();

        if (childDSArray) {
            for (var i=0; i<childDSArray.length; i++) {
                var foreignDS = childDSArray[i],
                    foreignFields = isc.getValues(foreignDS.getFields())
                ;

                for (var j=0; j<foreignFields.length; j++) {
                    var foreignField = foreignFields[j];

                    if (!foreignField.foreignKey ||
                        isc.DS.getForeignDSName(foreignField, foreignDS) != ds.ID) continue;

                    var localFieldName = isc.DS.getForeignFieldName(foreignField, foreignDS);
                    var field = rawFields.find("name", localFieldName);

                    if (field && foreignField.entityEditMode != "picker") {
                        // this is a relationship from another table into this entity
                        // (eg, from orderLine.orderId to order.orderId)
                        var thisDSName = dsName,
                            thisFieldName = field.name,
                            baseDSName = foreignDS.ID,
                            baseFieldName = foreignField.name,
                            relationArity = field.relationArity,
                            direction = "out"
                        ;

                        if (!doneList.contains(baseDSName)) {
                            //doneList.add(baseDSName);

                            result.add({
                                baseDS: baseDSName,
                                baseFieldName: baseFieldName,
                                relatedDS: thisDSName,
                                relatedFieldName: thisFieldName,
                                relationArity: field.relationArity
                                //,
                                //relations: this.getDataSourceHierarchy(baseDSName, doneList)
                            });
                        }
                    }
                }
            }
        }

        //doneList = [];

        for (var j=0; j<dsFields.length; j++) {
            var field = dsFields[j];

            if (!field.foreignKey) continue;

            var foreignFieldName = isc.DS.getForeignFieldName(field, ds),
                foreignDS = isc.DS.getDataSource(isc.DS.getForeignDSName(field, ds)),
                foreignFields = isc.getValues(foreignDS.getFields());

            var foreignField = foreignFields.find("name", foreignFieldName);

            if (foreignField && field.entityEditMode != "picker") {
                // this is a relationship from this table out to another entity
                // (eg, from order.customerId to customer.customerId)
                var thisDSName = dsName,
                    thisFieldName = field.name,
                    baseDSName = this.getDSName(foreignDS),
                    baseFieldName = foreignField.name,
                    relationArity = field.relationArity,
                    direction = "out"
                ;

                if (!doneList.contains(baseDSName)) {
                    //doneList.add(baseDSName);

                    result.add({
                        baseDS: baseDSName,
                        baseFieldName: baseFieldName,
                        relatedDS: thisDSName,
                        relatedFieldName: thisFieldName,
                        relationArity: relationArity,
                        direction: direction
                        //,
                        //relations: this.getDataSourceHierarchy(baseDSName, doneList)
                    });
                }
            }
        }

        return result;
    },

    getDSName : function (ds) {
        if (isc.isA.String(ds)) return ds;
        if (isc.isA.DataSource(ds)) return ds.ID;
        return null;
    }



});

isc.EntityEditor.addMethods({

    initWidget: function () {
        this.vertical = true;

        if (!isc.isA.DataSource(this.dataSource))
            this.dataSource = isc.DS.getDataSource(this.dataSource);

        if (!this.dataSource) this.logWarn("No dataSource provided - no entity to edit");
        else this.entityTree = this.getEntityTree();

        this.addAutoChild("modeForm");

        this.addAutoChild("tabset");
        this.addAutoChild("portal");

        this.addMember(this.modeForm);
        if (this.tabset) this.addMember(this.tabset);
        if (this.portal) this.addMember(this.portal);

        //this.Super("initWidget", arguments);

        //this.showEntity();
    },

    getEntityTree : function () {
        if (!isc.isA.DataSource(this.dataSource))
            return { baseDS: "NoDSProvided", relations: [] };

        var result = {
            baseDS: this.dataSource.ID,
            baseFieldName: this.dataSource.getPrimaryKeyFieldNames()[0],
            relations: []
        };

        result.relations = this.getDataSourceHierarchy(this.dataSource);

        return result;
    },

    fetchDataByPK : function (criteria) {
        if (!this.dataSource) return;

        var _this = this;
        this.dataSource.fetchData( criteria,
            function (dsResponse, data) {
                _this.fetchDataReply(data);
            }
        );
    },

    fetchDataReply : function (data) {
        this.clearEntity();
        this.record = data[0];

        this.showEntity();
    },

    clearEntity : function () {
        if (this.entities && this.entities.length > 0) {
            if (this.showTabset) {
                for (var i=this.tabset.tabs.length-1; i>=0; i--) {
                    this.entities[i].markForDestroy();
                    this.tabset.removeTab(i);
                }
            } else if (this.portal) {
                this.portal.members.removeAll();
                for (var i=this.entities.length-1; i>=0; i--) {
                    this.entities[i].markForDestroy();
                }
            } else {
                for (var i=this.members.length-1; i>=1; i--) {
                    this.removeMember(i);
                    this.entities[i-1].markForDestroy();
                }
            }
        }

        this.entities = [];
        this.record = null;
    },

    showEntity : function () {
        var tree = this.entityTree;

        if (!this.entities) this.entities = [];

        if (!this.entityTree) return;

        this.addEditor(tree);

        this.topLevelComponent = this.entities[0];

        if (tree && tree.relations && tree.relations.length > 0) {
            for (var i=0; i<tree.relations.length; i++) {
                var relation = tree.relations[i];
                if (this.shouldShowEntity(relation.baseDS)) {
                    if (relation.relationArity == "one") {
                        this.addEditor(relation);
                    } else {
                        this.addGrid(relation);
                    }
                }
            }
        }
    },

    updateTopLevel : function () {
        for (var i=1; i<this.entities.length; i++) {
            this.entities[i].setRecord(this.topLevelComponent.record);
        }
    },

    getData : function () {
        return [];
    },

    getDataSourceSpec : function (dsName, baseFieldName) {
        if (this.dataSources) {
            var dsProps = this.dataSources[dsName];
            if (!dsProps) dsProps = this.dataSources[dsName + "!" + baseFieldName];
            return dsProps;
        }
        return null;
    },

    getRelatedEditorProperties : function (dsName, baseFieldName) {
        var dsProps = this.getDataSourceSpec(dsName, baseFieldName);
        if (dsProps) return dsProps.editorProperties;
        return null;
    },

    shouldShowEntity : function (dsName) {
        if (this.dataSources) {
            var dsProps = this.dataSources[dsName],
                undef;
            if (dsProps === undef) return true;
            return (dsProps !== null);
        }
        return true;
    },

    addEditor : function (relation) {
        var entity = this.createAutoChild("formEntity", {
            height: "100%",
            width: "100%",
            dataSource: relation.baseDS,
            title: this.getEntityTitle(relation),
            record: this.record,
            relation: relation,
            formProperties: this.getRelatedEditorProperties(relation.baseDS, relation.relatedFieldName)
        });

        this.addEntityLink(entity);
        this.logWarn("adding linked single-record entity");
    },

    addGrid : function (relation) {
        var entity = this.createAutoChild("gridEntity", {
            height: "100%",
            width: "100%",
            //overflow: "visible",
            dataSource: relation.baseDS,
            title: this.getEntityTitle(relation),
            record: this.record,
            relation: relation,
            gridProperties: this.getRelatedEditorProperties(relation.baseDS, relation.relatedFieldName)
        });

        this.addEntityLink(entity);
        //entity.markForRedraw();

        this.logWarn("added linked multiple-record entity");
    },

    addEntityLink : function (widget) {
        // add the entity to a local array of entities
        this.entities.add(widget);

        if (this.showTabset) {
            this.addEntityTab(widget);
        } else {
            var rel = widget.relation,
                dsProps = this.getDataSourceSpec(rel.baseDS, rel.relatedFieldName),
                rowNum = (dsProps && dsProps.rowNum != null ? dsProps.rowNum : -1),
                offsetInRow = (dsProps && dsProps.offsetInRow != null ? dsProps.offsetInRow : -1)
            ;

            if (this.portal) {
                if (dsProps && dsProps.userHeight != null) widget._userHeight = dsProps.userHeight;
                if (rowNum != -1) {
                    this.portal.getColumn(0).addPortletToExistingRow(widget, rowNum, offsetInRow);
                } else {
                    this.portal.getColumn(0).addPortlet(widget);
                }
            }
            else this.addMember(widget);
        }
    },

    getEntityName : function (relation) {
        var baseDSName = this.getDSName(relation.baseDS),
            relatedDSName = this.getDSName(relation.relatedDS),
            fieldName = relation.relatedFieldName || relation.baseFieldName
        ;

        return baseDSName + "_" + fieldName;
    },

    getEntityTitle : function (relation) {
        var dsProps = this.getDataSourceSpec(relation.baseDS, relation.baseFieldName),
            baseDSName = this.getDSName(relation.baseDS),
            relatedDSName = this.getDSName(relation.relatedDS),
            fieldName = relation.relatedFieldName || relation.baseFieldName,
            title
        ;

        if (dsProps && dsProps.entityTitle) {
            title = dsProps.entityTitle;
        } else if (!relatedDSName) {
            title = isc.DS.getAutoTitle(baseDSName);
        } else if (baseDSName != relatedDSName) {
            title = isc.DS.getAutoTitle(baseDSName);
        } else {
            title = isc.DS.getAutoTitle(fieldName);
        }

        return title;
    },

    addEntityTab : function (paneWidget) {
        var name = this.getEntityName(paneWidget.relation),
            title = this.getEntityTitle(paneWidget.relation);

        if (paneWidget) {
            paneWidget.setWidth("100%");
            paneWidget.setHeight("100%");
        }

        this.tabset.addTab({
            name: name,
            title: title,
            pane: paneWidget,
            relation: paneWidget.relation
        });
    },

    setMode : function (newMode) {
        //alert("setMode called with new mode: "+newMode);
        var i, entity;

        if (newMode == "Search") {
            if (this.entities && this.entities.length > 0) {
                for (i=0; i<this.entities.length; i++) {
                    entity = this.entities[i];
                    if (isc.isA.EntityEditorGrid(entity)) {
                        entity.enterSearchMode(this.getEntityCriteria(entity));
                        entity.markForRedraw();
                    } else if (isc.isA.EntityEditorForm(entity)) {
                        entity.enterSearchMode(this.getEntityCriteria(entity));
                        entity.markForRedraw();
                    }
                }
            }
        } else {
            // gather up the criteria from the various forms and grids
            if (!this.entityCriteria) this.entityCriteria = {};

            if (this.entities && this.entities.length > 0) {
                for (i=0; i<this.entities.length; i++) {
                    entity = this.entities[i];
                    var criteria = entity.getCriteria();

                    if (criteria) {
                        this.entityCriteria[entity.getID()] =
                            {
                                relation: entity.relation,
                                criteria: criteria
                            }
                        ;
                    }
                }
                //alert("Search Criteria: \n\n" + isc.echoFull(this.entityCriteria));
            }
        }
    },

    getEntityCriteria : function (entity) {
        if (this.entityCriteria) {
            return this.entityCriteria[entity.getID()];
        }

        return null;
    }

});

}




// Class will not work without the ListGrid
if (isc.ListGrid) {





//> @class PresetCriteriaItem
// A FormItem for use with the +link{FilterBuilder}, allows the user to pick from a set of
// pre-configured search criteria such as specific ranges in numeric or date data, and provide
// user friendly titles for such criteria, such as "within the next two weeks" or
// "High (0.75 - 0.99)".
//
// @visibility external
//<
isc.defineClass("PresetCriteriaItem", "SelectItem");

isc.PresetCriteriaItem.addClassMethods({

});


isc.PresetCriteriaItem.addProperties({
    height: 20,
    cellHeight: 20,
    canFocus: true,

    //> @attr presetCriteriaItem.valueMap (Object : null : IR)
    // This attribute is not applicable to the PresetCriteriaItem.  See
    // +link{presetCriteriaItem.options} instead.
    //
    // @visibility external
    //<

    //> @attr presetCriteriaItem.options (Object : null : IR)
    // An object whose properties are user-visible titles for the preset ranges, and whose
    // values are Criteria / AdvancedCriteria objects representing the criteria to be used if
    // the user selects that choice.
    //
    // @visibility external
    //<

    // i18n attributes

    //> @attr presetCriteriaItem.showCustomOption (boolean : false : IR)
    // If set, an additional option will be shown with the title +link{customOptionTitle},
    // which will cause +link{getCustomCriteria()} to be called.
    //
    // @visibility external
    //<

    //> @attr presetCriteriaItem.customOptionTitle (String : "Custom..." : IR)
    // The title to show for the +link{presetCriteriaItem.showCustomOption, custom option}.
    //
    // @visibility external
    // @group i18nMessages
    //<
    customOptionTitle: "Custom...",

    shouldSaveValue: true

});

isc.PresetCriteriaItem.addMethods({

    init : function () {
        if (!this.options) this.options = {};

        this.valueMap = {};
        for (var key in this.options) {
            this.valueMap[key] = key;
        }

        if (this.showCustomOption) {
            this.options["customOption"] = null;
            this.valueMap["customOption"] = this.customOptionTitle;
        }

        this.Super("init", arguments);
    },

    changed : function (form, item, value) {
        //this.Super("changed", arguments);
        if (value == "customOption") {
            if (this.getCustomCriteria && isc.isA.Function(this.getCustomCriteria))
                this.getCustomCriteria(this.getID()+".getCustomCriteriaReply(criteria,title)");
        }
    }

    ,

    //> @method presetCriteriaItem.getCustomCriteria() [A]
    // This method is called when +link{showCustomOption} is true and the user selects the
    // custom option.  Implement this method by allowing the user to enter custom criteria, for
    // example, by opening a modal dialog.  Once the user has input customer criteria, fire the
    // callback method with the resulting criteria.
    //
    // @param callback (Callback) callback to fire when custom criteria has been gathered.
    //              Expects parameters "criteria,title".  The "title" will be displayed as the
    //              currently selected value when custom criteria have been chosen.
    //
    // @visibility external
    //<
    getCustomCriteria : function (callback) {
    },

    getCustomCriteriaReply : function (criteria, title) {
        this.valueMap["customOption"] = title;
        this.options["customOption"] = criteria;
    },

    getCriteriaValue : function () {
        return this.getCriterion();
    },

    hasAdvancedCriteria : function () {
        return true;
    }

    ,

    //> @method presetCriteriaItem.getCriterion()
    // Get the criterion based on the value selected by the user.
    //
    // @return (Criterion or AdvancedCriteria) the criteria for the selected option
    //
    // @visibility external
    //<
    getCriterion : function () {
        var key = this.getValue(),
            crit = this.options[key]
        ;

        //var result = this.updateCriteriaFieldNames(crit);
        var result = crit;

        return result;
    },

    updateCriteriaFieldNames : function (crit) {
        for (var key in crit) {
            var subCrit = crit[key];

            if (isc.DataSource.isAdvancedCriteria(subCrit)) {
                subCrit = this.updateCriteriaFieldNames(subCrit);
            } else {
                if (!subCrit.fieldName) {
                    subCrit.fieldName = this.getCriteriaFieldName();
                }
            }
        }

        return crit;
    },

    setValue : function (value) {
        value = this.matchCriteria(value);
        this.Super("setValue", arguments);
    },

    setValueMap : function (valueMap) {
        // valueMap is not supported for this FormItem type - we use this.options instead -
        // ignore this call
        return null;
    },

    canEditCriterion : function (criterion) {
        var fieldNames = isc.DS.isAdvancedCriteria(criterion) ?
                isc.DS.getCriteriaFields(criterion).getUniqueItems() :
                criterion.criteria ? criterion.criteria.getProperty("fieldName").getUniqueItems() :
                [criterion.fieldName]
        ;

        var fieldName = this.getCriteriaFieldName();
        return fieldNames.contains(fieldName);
    },

    setCriterion : function (criterion) {
        this.setValue(isc.shallowClone(criterion));
    },

    getCriterion : function () {
        var value = this.getValue(),
            crit = this.options[value]
        ;
        return crit;
    },

    // find the entry in this.options that maps to the criteria object passed in
    matchCriteria : function (criteria, passedOptions) {
        var options = passedOptions || this.options,
            keys = isc.getKeys(options)
        ;

        for (var i=0; i<keys.length; i++) {
            var option = options[keys[i]];

            if (this.objectsAreEqual(option, criteria)) {
                return keys[i];
            }
        }

        return criteria;
    },

    // helper method to compare the properties on two objects, not dissimilar to
    // DS.compareCriteria
    objectsAreEqual : function (object1, object2) {
        if (!object1 && !object2) return true;
        if (!object1 || !object2) return false;

        var keys1 = isc.getKeys(object1);

        for (var i=0; i<keys1.length; i++) {
            var key = keys1[i];

            if (key == "_constructor") continue;

            var prop1 = object1[key],
                prop2 = object2[key]
            ;

            if (isc.isAn.Array(prop1)) {
                for (var j=0; j<prop1.length; j++) {
                    if (isc.isAn.Object(prop1[j])) {
                        if (!this.objectsAreEqual(prop1[j], prop2[j])) return false;
                    } else {
                        if (prop1[j] != prop2[j]) return false;
                    }
                }
            } else if (isc.isA.Date(prop1)) {
                if (Date.compareDates(prop1, prop2) != 0) return false;
            } else if (isc.isAn.Object(prop1)) {
                if (!this.objectsAreEqual(prop1, prop2)) return false;
            } else {
                if (object1[key] != object2[key]) return false;
            }
        }
        return true;
    }

});

//> @class PresetDateRangeItem
// Allows the user to pick from pre-set date ranges or choose a custom date range via a
// +link{DateRangeDialog}.
// <P>
// To use this item in the +link{listGrid.showFilterEditor,FilterEditor} or
// +link{FilterBuilder}, create a trivial +link{ClassFactory.defineClass,subclass} which
// defines +link{PresetDateRangeItem.options,preset options}, then set
// +link{listGridField.filterEditorType} to use this class with the FilterEditor, or define a
// custom operator and set +link{operator.editorType} to use it with the FilterBuilder.
// <P>
// See the +explorerExample{dateRangeFilterPresets,Date Range (Presets)} example for sample code.
// @visibility external
//<
isc.defineClass("PresetDateRangeItem", "PresetCriteriaItem");

isc.PresetDateRangeItem.addProperties({

    customOptionTitle: "Custom Date Range",

    getCustomCriteria : function (callback) {
        this._callback = callback;
        isc.DateRangeDialog.askForRange(
            true, { returnCriterion: true }, null, this.getID()+".showDateRangeDialogReply(value)"
        );
    },

    showDateRangeDialogReply : function (value) {
        var callback = this._callback;
        delete this._callback;
        this.fireCallback(callback, "criteria,title", [value,this.customOptionTitle]);
    }

});

}
isc._debugModules = (isc._debugModules != null ? isc._debugModules : []);isc._debugModules.push('Forms');isc.checkForDebugAndNonDebugModules();isc._moduleEnd=isc._Forms_end=(isc.timestamp?isc.timestamp():new Date().getTime());if(isc.Log&&isc.Log.logIsInfoEnabled('loadTime'))isc.Log.logInfo('Forms module init time: ' + (isc._moduleEnd-isc._moduleStart) + 'ms','loadTime');delete isc.definingFramework;}else{if(window.isc && isc.Log && isc.Log.logWarn)isc.Log.logWarn("Duplicate load of module 'Forms'.");}

/*

  SmartClient Ajax RIA system
  Version v9.0p_2021-05-01/LGPL Deployment (2021-05-01)

  Copyright 2000 and beyond Isomorphic Software, Inc. All rights reserved.
  "SmartClient" is a trademark of Isomorphic Software, Inc.

  LICENSE NOTICE
     INSTALLATION OR USE OF THIS SOFTWARE INDICATES YOUR ACCEPTANCE OF
     ISOMORPHIC SOFTWARE LICENSE TERMS. If you have received this file
     without an accompanying Isomorphic Software license file, please
     contact licensing@isomorphic.com for details. Unauthorized copying and
     use of this software is a violation of international copyright law.

  DEVELOPMENT ONLY - DO NOT DEPLOY
     This software is provided for evaluation, training, and development
     purposes only. It may include supplementary components that are not
     licensed for deployment. The separate DEPLOY package for this release
     contains SmartClient components that are licensed for deployment.

  PROPRIETARY & PROTECTED MATERIAL
     This software contains proprietary materials that are protected by
     contract and intellectual property law. You are expressly prohibited
     from attempting to reverse engineer this software or modify this
     software for human readability.

  CONTACT ISOMORPHIC
     For more information regarding license rights and restrictions, or to
     report possible license violations, please contact Isomorphic Software
     by email (licensing@isomorphic.com) or web (www.isomorphic.com).

*/

