All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.smartclient.debug.public.sc.client.widgets.TreeGrid.js Maven / Gradle / Ivy

The newest version!
/*
 * Isomorphic SmartClient
 * Version SC_SNAPSHOT-2011-08-08 (2011-08-08)
 * Copyright(c) 1998 and beyond Isomorphic Software, Inc. All rights reserved.
 * "SmartClient" is a trademark of Isomorphic Software, Inc.
 *
 * [email protected]
 *
 * http://smartclient.com/license
 */




//>	@class	TreeGrid
//
// The SmartClient system supports hierarchical data (also referred to as tree data
// due to its "branching" organization) with:
// 
    //
  • the +link{class:Tree} class, which manipulates hierarchical data sets //
  • the TreeGrid widget class, which extends the ListGrid class to visually // present tree data in an expandable/collapsible format. //
// For information on DataBinding Trees, see +link{group:treeDataBinding}. //

// A TreeGrid works just like a +link{ListGrid}, except one column (specified by // +link{TreeGridField.treeField}) shows a hierarchical +link{Tree}. A TreeGrid is not limited // to displaying just the +link{Tree} column - you can define additional columns (via // +link{TreeGrid.fields}) which will render just like the columns of a +link{ListGrid}, and // support all of the functionality of ListGrid columns, such as // +link{listGridField.formatCellValue,formatters}. //

// Except where explicitly overridden, +link{ListGrid} methods, callbacks, and properties // apply to TreeGrids as well. The +link{ListGrid} defines some methods as taking/returning // +link{ListGridField} and +link{ListGridRecord}. When using those methods in a TreeGrid, // those types will be +link{TreeGridField} and +link{TreeNode}, respectively. // // @implements DataBoundComponent // @treeLocation Client Reference/Grids // @visibility external //< // define us as a subclass of the ListGrid isc.ClassFactory.defineClass("TreeGrid", "ListGrid"); // Synonym for backCompat. NOTE: define an alias rather than make a subclass, otherwise, // attempts to skin the class using the old name would only affect the subclass! isc.addGlobal("TreeViewer", isc.TreeGrid); //> @class TreeGridBody // // GridRenderer displayed as the body of a TreeGrid. Includes various overrides for // specialized event handling and display. // // @treeLocation Client Reference/Grids/TreeGrid/ // @visibility internal //< isc.defineClass("TreeGridBody", isc.GridBody).addProperties({ // Override the internal _updateCellStyle() method to style the tree-field's internal // table (without regening the HTML) _$TABLE:"TABLE", _$zeroBorderPadding:"padding:0px;border:0px;", _updateCellStyle : function (record, rowNum, colNum, cell, className) { if (cell == null) cell = this.getTableElement(rowNum, colNum); if (cell == null) return; // cell not currently drawn if (!this.showHiliteInCells && colNum == this.grid.getLocalFieldNum(this.grid.getTreeFieldNum())) { if (record == null) record = this.getCellRecord(rowNum, colNum); // determine the CSS style className if not provided if (className == null) className = this.getCellStyle(record, rowNum, colNum); // There will be a clip-div around the table. // In IE the NOBR also counts as a node. var table = cell.childNodes[0]; while (table && table.tagName != this._$TABLE) table = table.childNodes[0]; if (table) { var customCSSText; // Apply custom css text from getCellCSSText to the table element and any cells. // Note: this is not required in most cases - we write out no-style-doubling css // on these elements so we'll ignore bg color, images, border etc. // This is really just required to pick up changes to the text color / weight etc if (this.getCellCSSText) { customCSSText = this.getCellCSSText(record, rowNum, colNum); if (customCSSText != null && !isc.isAn.emptyString(customCSSText)) { // we always append no-style-doubling css to the custom css to avoid // doubled borders etc customCSSText += isc.Canvas._$noStyleDoublingCSS; } else customCSSText = null; } table.className = className; if (customCSSText != null) table.cssText = customCSSText; var innerRows = table.rows, innerCells = innerRows[0].cells; if (innerCells && innerCells.length > 0) { for (var i = 0; i < innerCells.length; i++) { innerCells[i].className = className; if (customCSSText) { // Title is the last cell in the tree-title table - this cell // requires additional specification for the icon padding if (i == innerCells.length-1) { customCSSText += "paddingLeft:" + this.iconPadding; } innerCells[i].cssText = customCSSText; } } } } } // Actually style the cell itself return isc.GridRenderer.getPrototype()._updateCellStyle.apply( this, [record, rowNum, colNum, cell, className]); }, //> @method treeGridBody.click() (A) // Handle a click in the "open" area of a record specially // @group event handling // // @return (boolean) false == cancel further event processing //< // Note: We override click rather than putting this logic into rowClick / cellClick, as // we don't want folder toggling to occur if the user's mouse is hovering over the open // area while the user triggers standard record click handling by hitting the Space-bar. click : function (event, eventInfo) { if (!this._suppressEventHandling()) { var tg = this.grid, recordNum = tg.getEventRecordNum(), node = tg.getRecord(recordNum); // if what they clicked on is a folder, toggle it's state. The 'open' observation // will automaticaly redraw for us if (tg.data.isFolder(node) && tg.clickInOpenArea(node)) { if (isc.screenReader) { this._putNativeFocusInRow(recordNum); } tg.toggleFolder(node); // clear out the pointer to the last record clicked, and the last row selected // by keyboard navigation. (Prevents index-based keyboard navigation from // jumping around due to the number of rows changing as folders toggle) tg.clearLastHilite(); tg._lastRecordClicked = null; // Note: if you click in the open area to toggle a folder, no nodeClick or // folderClick events will be fired, since we've already taken action in // response to your click by toggling a folder // Return EH.STOP_BUBBLING to stop propogation. return isc.EH.STOP_BUBBLING; } } return this.Super("click", arguments); }, // Override mouseDown and mouseUp in the body to avoid selecting when clicking in open // area by default. //> @method treeGridBody.mouseDown() (A) // Handle a click in the open area on mouseDown specially // @group event handling // // @return (boolean) false == cancel further event processing //< mouseDown : function () { // get the item that was clicked on -- bail if not found var rowNum = this.getEventRow(), node = this.grid.data.get(rowNum); if (node != null && this.grid.clickInOpenArea(node)) { // if they clicked in the open area of the record, // just bail because we're supposed to open the folder instead // TreeGrid.click() will actually open the folder return isc.EH.STOP_BUBBLING; } else if (this.grid.clickInCheckboxArea(node) && this.canSelectRecord(node)) { // Toggle node selection state var selectionType = this.grid.selectionType; if (selectionType == isc.Selection.SINGLE) { this.deselectAllRecords(); this.selectRecord(node); } else if (selectionType == isc.Selection.SIMPLE || selectionType == isc.Selection.MULTIPLE) { if (this.selection.isSelected(node)) this.deselectRecord(node); else this.selectRecord(node); } // Note: if you click in the checkbox area to select a node, no nodeClick or // folderClick events will be fired, since we've already taken action in // response to your click by selecting/deselected the node. // Return EH.STOP_BUBBLING to stop propogation. return isc.EH.STOP_BUBBLING; } else { return this.Super("mouseDown", arguments); } }, //> @method treeGridBody.mouseUp() (A) // Handle a click in the open area on mouseUp specially // @group event handling // // @return (boolean) false == cancel further event processing //< mouseUp : function () { // get the item that was clicked on -- bail if not found var rowNum = this.getEventRow(), node = this.grid.data.get(rowNum); if (node != null && (this.grid.clickInOpenArea(node) || this.grid.clickInCheckboxArea(node))) { // don't select the row; on click() we'll open the folder return isc.EH.STOP_BUBBLING; } else { // proceed to super (select the row) return this.Super("mouseUp", arguments); } }, // Override to place embedded components for the tree field indented as a title // would be if TG.indentRecordComponents == true. placeEmbeddedComponent : function (component) { if (this.grid.indentRecordComponents) { var colNum = component._currentColNum; if (colNum == this.grid.getTreeFieldNum() && !component.snapOffsetLeft) { var record = component.embeddedRecord; if (record != null) { component.snapOffsetLeft = this.grid.getOpenAreaWidth(record) + this.grid.iconPadding; } } } return this.Super("placeEmbeddedComponent", arguments); } }); isc.TreeGrid.addClassProperties({ //= @const TreeGrid.TREE_FIELD default field to display a tree TREE_FIELD : {name:"nodeTitle", treeField:true, getCellValue : function (list,record,recordNum,colNum) { if (!list.getNodeTitle) { var fieldName = colNum == null ? null : list.getFieldName(colNum); return record == null || fieldName == null ? null : record[fieldName]; } return list.getNodeTitle(record,recordNum, this) }, canFilter: false, // return the title for the Header Button over the tree column. getFieldTitle : function (viewer, fieldNum) { var field = viewer.getField(fieldNum); if (field.name == "nodeTitle") return viewer.treeFieldTitle; // otherwise just return the title of the field, or failing that, the field's name return field.title || field.name; } }, // _getTreeCellTemplate - returns the HTML template array used for the start of // tree grid cells. // This is a dynamic method - it incorporates the standard 'noDoublingCSS' string into the // returned HTML template. That string can change at runtime due to setNeverUseFilters() // so we need to react to this and regenerate the template. _getTreeCellTemplate : function () { if (!this._observingDoublingStrings) { isc.Canvas._doublingStringObservers.add({ target:this, methodName:"_doublingStringsChanged" }); this._observingDoublingStrings = true; } if (this._$treeCellTemplate == null) { this._$treeCellTemplate = [ "

" // [10] // (we'll write the title cell out using _$treeCellTitleTemplate) ]; } return this._$treeCellTemplate; }, _getTreeCellTitleTemplate : function () { if (!this._observingDoublingStrings) { isc.Canvas._doublingStringObservers.add({ target:this, methodName:"_doublingStringsChanged" }); this._observingDoublingStrings = true; } if (this._$treeCellTitleTemplate == null) { this._$treeCellTitleTemplate = [ "" // [17] ]; } return this._$treeCellTitleTemplate; }, _doublingStringsChanged : function () { this._$treeCellTemplate = null; this._$treeCellTitleTemplate = null } }); isc.TreeGrid.addProperties({ //> @attr treeGrid.dataSource (DataSource or ID : null : IRW) // @include dataBoundComponent.dataSource //< //> @attr treeGrid.data (Tree : null : IRW) // A +link{class:Tree} object containing of nested +link{object:TreeNode}s to // display as rows in this TreeGrid. // The data property will typically not be explicitly specified for // databound TreeGrids, where the data is returned from the server via databound component // methods such as fetchData() // @visibility external // @group data //< //> @attr treeGrid.initialData (List of TreeNode : null : IRA) // You can specify the initial set of data for a databound TreeGrid using this property. // The value of this attribute should be a list of parentId linked // +link{TreeNode}s in a format equivalent to that documented on +link{Tree.data}. // // @see TreeNode // @see Tree.data // @visibility external // @example initialData //< //> @attr treeGrid.loadDataOnDemand (boolean : null : IRW) // For databound treeGrid instances, should the entire tree of data be loaded on initial // fetch, or should folders load their children as they are opened. //

// If unset, calling +link{fetchData()} will default it to true, otherwise, if a ResultTree // is passed to +link{setData()}, the +link{resultTree.loadDataOnDemand} setting is // respected. //

// Note that when using loadDataOnDemand, every node returned by the server is // assumed be a folder which may load further children. See // +link{resultTree.defaultIsFolder} for how to control this behavior. // // @group databinding // @visibility external // @example initialData //< //> @attr treeGrid.autoFetchTextMatchStyle (TextMatchStyle : "exact" : IR) // With +link{loadDataOnDemand}:true, TreeGrids fetch data by selecting the child nodes of // each parent, which should be exact match, so we default to // autoFetchTextMatchStyle:"exact". // See +link{listGrid.autoFetchTextMatchStyle} for details. // // @group dataBinding // @visibility external //< autoFetchTextMatchStyle:"exact", //> @attr treeGrid.cascadeSelection (boolean : false : [IR]) // Should children be selected when parent is selected? And should parent be // selected when any child is selected? // @visibility external //< cascadeSelection:false, //> @attr treeGrid.showPartialSelection (boolean : false : [IRW]) // Should partially selected parents be shown with special icon? // @visibility external //< showPartialSelection:false, //> @attr treeGrid.selectionProperty (string : null : [IRA]) // @include listGrid.selectionProperty // @visibility external //< //> @attr treeGrid.treeRootValue (any : null : IRA) // For databound trees, use this attribute to supply a +link{DataSourceField.rootValue} for this // component's generated data object. //

// This property allows you to have a particular component navigate a tree starting from any // given node as the root. // @group databinding // @visibility external //< //> @attr treeGrid.fields (Array of TreeGridField: null : IRW) // An array of field objects, specifying the order, layout, dynamic calculation, and // sorting behavior of each field in the treeGrid object. In TreeGrids, the fields // array specifies columns. Each field in the fields array is TreeGridField object. //

// If +link{TreeGrid.dataSource} is also set, this value acts as a set of overrides as // explained in +link{attr:DataBoundComponent.fields}. // // @group databinding // @see TreeGridField // @visibility external //< //> @object TreeGridField // // An object literal with a particular set of properties used to configure the display of // and interaction with the columns of a +link{TreeGrid}. // +link{TreeGrid} is a subclass of +link{ListGrid} and as a result, for all fields except // the field containing the +link{Tree} itself (specified by // +link{treeGridField.treeField}, all properties settable on // +link{ListGridField} apply to TreeGridField as well. //

// This class documents just those properties that are specific to TreeGridFields - see // +link{ListGridField} for the set of inherited properties. // // @see ListGridField // @see TreeGrid.fields // @see ListGrid.setFields // // @treeLocation Client Reference/Grids/TreeGrid // @visibility external //< // Tree Field // --------------------------------------------------------------------------------------- //> @attr treeGridField.treeField (boolean : see below : IRW) // // The field containing treeField: true will display the +link{Tree}. If no // field specifies this property, if a field named after the +link{Tree.titleProperty} of // the Tree is present in +link{TreeGrid.fields}, that field will show the tree. Note that // when using a DataSource, you typically define the title field via // +link{DataSource.titleField} and the generated +link{ResultTree} automatically uses this // field. // // If none of the above rules apply, the first field in +link{TreeGrid.fields} is assigned to // display the +link{Tree}. // // @group treeField // @visibility external //< //> @attr treeGridField.canExport (Boolean : null : IR) // Dictates whether the data in this field be exported. Explicitly set this // to false to prevent exporting. Has no effect if the underlying // +link{dataSourceField.canExport, dataSourceField} is explicitly set to // canExport: false. // // @visibility external //< //> @attr treeGrid.treeFieldTitle (string : "Name" : IR) // Visible title for the tree column (field). // @group treeField // @visibility external //< treeFieldTitle:"Name", //> @attr treeGrid.autoAssignTreeField (boolean : true : IR) // If no field was specified as the "tree-field" (showing indentations for tree hierarchy // and tree icons), should we assign one of the other fields to be the tree-field? // Useful when we want to display a tree or partial tree as a flattened list. // @group treeField //< autoAssignTreeField:true, //> @attr treeGrid.showRoot (boolean : false : IR) // Specifies whether the root node should be displayed in the treeGrid. //

// This property is only available for "children" modelType trees, hence is not allowed for // trees that load data from the server dynamically via +link{fetchData()}. //

// To get the equivalent of a visible "root" node in a tree that loads data dynamically, // add a singular, top-level parent to the data. However, note that this top-level parent // will technically be the only child of root, and the implicit root object will be // returned by +link{tree.getRoot,this.data.getRoot()}. // // @group treeField // @visibility external //< showRoot:false, //> @attr treeGrid.separateFolders (boolean : null : IR) // If specified, this attribute will override +link{Tree.separateFolders} on the // data for this treeGrid. //

// Specifies whether folders and leaves should be segregated in the treeGrid display. // Use +link{treeGrid.sortFoldersBeforeLeaves} to customize whether folders appear before // or after their sibling leaves. //

// If unset, at the treeGrid level, the property can be set directly on // +link{treeGrid.data,the tree data object} or for dataBound TreeGrids on the // +link{treeGrid.dataProperties,dataProperties configuration object}. // // @group treeField // @visibility external //< // separateFolders:false, //> @attr treeGrid.sortFoldersBeforeLeaves (boolean : null : IR) // If specified, this attribute will override +link{tree.sortFoldersBeforeLeaves} on // the data for this treeGrid. //

// Specifies whether when +link{tree.separateFolders} is true, folders should be displayed // before or after their sibling leaves in a sorted tree. If set to true, with // sortDirection set to Array.ASCENDING, folders are displayed before their sibling leaves // and with sort direction set to Array.DESCENDING they are displayed after. To invert // this behavior, set this property to false. // @group treeField // @see treeGrid.separateFolders // @visibility external //< // sortFoldersBeforeLeaves:null, //> @attr treeGrid.displayNodeType (DisplayNodeType : isc.Tree.FOLDERS_AND_LEAVES : [IRW]) // Specifies the type of nodes displayed in the treeGrid. // @see type:DisplayNodeType for options // @group treeField // @visibility external //< displayNodeType:isc.Tree.FOLDERS_AND_LEAVES, // Drag and Drop // -------------------------------------------------------------------------------------------- //> @attr treeGrid.canDragRecordsOut (boolean : false : IRW) // @include ListGrid.canDragRecordsOut // @group dragdrop // @see TreeNode.canDrag // @see TreeNode.canAcceptDrop // @visibility external // @example treeDropEvents //< canDragRecordsOut:false, //> @attr treeGrid.canAcceptDroppedRecords (boolean : false : IRW) // @include ListGrid.canAcceptDroppedRecords // @see TreeNode.canDrag // @see TreeNode.canAcceptDrop // @group dragdrop // @visibility external // @example dragReparent //< //canAcceptDroppedRecords:false, //> @attr treeGrid.canReorderRecords (boolean : false : IRWA) // @include ListGrid.canReorderRecords // @see TreeNode.canDrag // @see TreeNode.canAcceptDrop // @group dragdrop // @visibility external // @example dragReparent //< //canReorderRecords:false, //> @attr treeGrid.canDropOnLeaves (boolean : false : IRWA) // Whether drops are allowed on leaf nodes. //

// Dropping is ordinarily not allowed on leaf nodes unless +link{canReorderRecords} is // set. //

// The default action for a drop on a leaf node is to place the node in that leaf's parent // folder. This can be customized by overriding +link{folderDrop()}. //

// Note that enabling canDropOnLeaves is usually only appropriate where you // intend to add a custom +link{folderDrop()} implementation that does not add a // child node under the leaf. If you want to add a child nodes to a leaf, instead of // enabling canDropOnLeaves, use empty folders instead - see +link{Tree.isFolder} for how // to control whether a node is considered a folder. // // @visibility external //< //> @attr treeGrid.canReparentNodes (boolean : null : IRW) // If set this property allows the user to reparent nodes by dragging them from their // current folder to a new folder.
// Backcompat: For backwards compatibility with versions prior to SmartClient 1.5, // if this property is unset, but this.canAcceptDroppedRecords is true, we // allow nodes to be dragged to different folders. // @see TreeNode.canDrag // @see TreeNode.canAcceptDrop // @group dragdrop // @visibility external //< //canReparentNodes:null, //> @attr treeGrid.dragDataAction (DragDataAction : isc.ListGrid.MOVE : IRWA) // // Specifies what to do with data dragged from this TreeGrid (into another component, or // another node in this TreeGrid. The default action is to move the data. A setting of // "none" is not recommended for trees because Trees maintain the node open state on the nodes // themselves, and hence having multiple Tree objects share a reference to a node can have // unintended consequences (such as opening a folder in one tree also triggering an open in // another tree that shares the same node). //

// For DataBound trees (+link{class:ResultTree}), the expectation is that // +link{method:TreeGrid.folderDrop} will be overridden to perform whatever action took // place as the result of the drag and drop interaction. // // @see group:sharingNodes // @visibility external //< dragDataAction:isc.ListGrid.MOVE, // dragRecategorize:true, //> @attr treeGrid.openDropFolderDelay (integer : 600 : IRWA) // When dragging something over a closed folder, delay in milliseconds before the folder // automatically opens. //< openDropFolderDelay:600, // D&D Error Messages // error messages for invalid drag and drop situations. Can be customized on a per // instance basis so something more application-specific can be said, eg "a manager cannot // become his own employee" //> @attr treeGrid.parentAlreadyContainsChildMessage (String : "This item already contains a child item with that name." : IR) // Message displayed when user attempts to drag a node into a parent that already contains // a child of the same name. // @see attr:treeGrid.canDragRecordsOut // @see attr:treeGrid.canAcceptDroppedRecords // @see attr:treeGrid.canReorderRecords // @group i18nMessages // @visibility external //< parentAlreadyContainsChildMessage:"This item already contains a child item with that name.", //> @attr treeGrid.cantDragIntoSelfMessage (String : "You can't drag an item into itself." : IR) // Message displayed when user attempts to drop a dragged node onto itself. // @see attr:treeGrid.canDragRecordsOut // @see attr:treeGrid.canAcceptDroppedRecords // @see attr:treeGrid.canReorderRecords // @group i18nMessages // @visibility external //< cantDragIntoSelfMessage:"You can't drag an item into itself.", //> @attr treeGrid.cantDragIntoChildMessage (String : "You can't drag an item into one of it's children." : IR) // Message displayed when user attempts to drop a node into a child of itself. // @see attr:treeGrid.canDragRecordsOut // @see attr:treeGrid.canAcceptDroppedRecords // @see attr:treeGrid.canReorderRecords // @group i18nMessages // @visibility external //< cantDragIntoChildMessage:"You can't drag an item into one of it's children.", // Body Rendering // -------------------------------------------------------------------------------------------- //> @attr treeGrid.fixedFieldWidths (boolean : true : IRWA) // make trees fixedFieldWidths by default // @group appearance //< fixedFieldWidths:true, //> @attr treeGrid.wrapCells (boolean : false : IRWA) // don't wrap, as that will mess up the look of the trees // @group appearance //< wrapCells:false, //> @attr treeGrid.showHiliteInCells (boolean : false : IRWA) // Should the hilite show across the entire record or just in the text of the item itself //< showHiliteInCells:false, // Images: locations, sizes, and names // -------------------------------------------------------------------------------------------- //> @attr treeGrid.indentSize (number : 20 : IRW) // The amount of indentation (in pixels) to add to a node's icon/title for each level // down in this tree's hierarchy. //

// This value is ignored when +link{treeGrid.showConnectors,showConnectors} is // true because fixed-size images are used to render the connectors. // @visibility external // @group appearance //< indentSize:20, //> @attr treeGrid.extraIconGap (integer : 2 : IR) // The amount of gap (in pixels) between the extraIcon (see +link{treeGrid.getExtraIcon()}) // or checkbox icon and the +link{treeGrid.nodeIcon,nodeIcon}/ // +link{treeGrid.folderIcon,folderIcon} or node text. // @group appearance // @visibility external //< extraIconGap:2, //> @attr treeGrid.iconSize (number : 16 : [IRW]) // The standard size (same height and width, in pixels) of node icons in this // treeGrid. // @group treeIcons // @visibility external //< iconSize:16, //> @attr treeGrid.openerIconSize (number : null : [IRW]) // Width and height in pixels of the opener icons, that is, the icons which show the open // or closed state of the node, typically a [+] or [-] symbol. //

// If +link{showConnectors} is true, the opener icon includes the connector line, and // defaults to +link{listGrid.cellHeight,cellHeight}. //

// Otherwise, openerIconSize defaults to +link{iconSize}. // // @group treeIcons // @visibility external //< //> @attr treeGrid.skinImgDir (URL : "images/TreeGrid/" : IRWA) // Where do 'skin' images (those provided with the class) live? // This is local to the Page.skinDir // @group appearance, images //< skinImgDir:"images/TreeGrid/", //> @attr treeGrid.folderIcon (SCImgURL : "[SKIN]folder.gif" : [IRW]) // The URL of the base icon for all folder nodes in this treeGrid. Note that this URL will // have +link{treeGrid.openIconSuffix}, +link{treeGrid.closedIconSuffix} or // +link{treeGrid.dropIconSuffix} appended to indicate state changes if appropriate - // see documentation on +link{treeGrid.showOpenIcons} and +link{treeGrid.showDropIcons}. // @group treeIcons // @visibility external // @example nodeTitles //< folderIcon:"[SKIN]/folder.gif", //> @attr treeGrid.dropIconSuffix (String : "drop" : [IRW]) // If +link{treeGrid.showDropIcons} is true, this suffix will be appended to the // +link{treeGrid.folderIcon} when the user drop-hovers over some folder. // @group treeIcons // @visibility external //< dropIconSuffix:"drop", //> @attr treeGrid.openIconSuffix (String : "open" : [IRW]) // If +link{showOpenIcons} is true, this suffix will be appended to the // +link{folderIcon} for open folders in this grid. // @group treeIcons // @visibility external //< openIconSuffix:"open", //> @attr treeGrid.closedIconSuffix (String : "closed" : [IRW]) // This suffix will be appended to the +link{folderIcon} for closed folders. // If +link{showOpenIcons} is set to false this suffix will also be // appended to open folders' icons. // @group treeIcons // @visibility external //< closedIconSuffix:"closed", //> @attr treeGrid.nodeIcon (SCImgURL : "[SKIN]file.gif" : [IRW]) // The filename of the default icon for all leaf nodes in this grid. To specify a // custom image for an individual node, set the +link{customIconProperty} directly on // the node. // @group treeIcons // @visibility external // @example nodeTitles //< nodeIcon:"[SKIN]/file.gif", //>@attr treeGrid.showOpenIcons (boolean : true : IRW) // If true, show a different icon for open folders than closed folders. // This is achieved by appending the +link{openIconSuffix} onto the // +link{folderIcon} URL [for example "[SKIN]/folder.gif" might be // replaced by "[SKIN]/folder_open.gif".
// Note If this property is set to false the same icon is shown for // open folders as for closed folders, unless a custom folder icon was specified. This will be // determined by +link{folderIcon} plus the +link{closedIconSuffix}. // @group treeIcons // @visibility external // @example nodeTitles //< showOpenIcons:true, //>@attr treeGrid.showDropIcons (boolean : true : IRW) // If true, when the user drags a droppable target over a folder in this TreeGrid, show // a different icon folder icon. // This is achieved by appending the +link{treeGrid.dropIconSuffix} onto the // +link{TreeGrid.folderIcon} URL (for example "[SKIN]/folder.gif" may be // replaced by "[SKIN]/folder_drop.gif"). // @group treeIcons // @visibility external // @example nodeTitles //< showDropIcons:true, //> @attr treeGrid.customIconProperty (String : "icon" : [IRW]) // This property allows the developer to rename the // +link{TreeNode.icon, default node.icon} property. // @group treeIcons // @visibility external //< customIconProperty:"icon", //> @attr treeGrid.customIconOpenProperty (string : "showOpenIcon" : [IRWA]) // This property allows the developer to rename the // +link{TreeNode.showOpenIcon, default node.showOpenIcon} property. // @see treeGrid.customIconProperty // @see treeGrid.showCustomIconOpen // @visibility external // @group treeIcons //< customIconOpenProperty:"showOpenIcon", //> @attr treeGrid.customIconDropProperty (string : "showDropIcon" : [IRWA]) // This property allows the developer to rename the // +link{TreeNode.showDropIcon, default node.showDropIcon} property. // @see treeGrid.customIconProperty // @see treeGrid.showCustomIconDrop // @visibility external // @group treeIcons //< customIconDropProperty:"showDropIcon", //> @attr treeGrid.showCustomIconOpen (boolean : false : [IRWA]) // Should folder nodes showing custom icons (set via the +link{customIconProperty}), // show open state images when the folder is opened. // If true, the +link{openIconSuffix} will be appended to the image URL // (so "customFolder.gif" might be replaced with // "customFolder_open.gif").
// Note that the +link{closedIconSuffix} is never appended to custom folder icons.
// Can be overridden at the node level via the default property +link{treeNode.showOpenIcon} // and that property can be renamed via +link{treeGrid.customIconOpenProperty}. // @group treeIcons // @visibility external //< showCustomIconOpen:false, //> @attr treeGrid.showCustomIconDrop (boolean : false : [IRWA]) // Should folder nodes showing custom icons (set via the +link{treeGrid.customIconProperty}, // default +link{treeNode.icon}), // show drop state images when the user is drop-hovering over the folder. // If true, the +link{treeGrid.dropIconSuffix} will be appended to the image URL // (so "customFolder.gif" might be replaced with // "customFolder_drop.gif").
// Can be overridden at the node level via the default property +link{treeNode.showDropIcon} // and that property can be renamed via +link{treeGrid.customIconDropProperty}. // @group treeIcons // @visibility external //< showCustomIconDrop:false, //> @attr treeGrid.showDisabledSelectionCheckbox (boolean : false : [IR]) // Should tree nodes show a disabled checkbox instead of a blank space // when +link{ListGrid.selectionAppearance, selectionAppearance}:"checkbox" // is set on the treegrid, and a node can't be selected? // @see ListGrid.recordCanSelectProperty // @group treeIcons // @visibility external //< // --------------------------------- // DEPRECATED ICON PROPERTIES: // //> @attr treeGrid.folderOpenImage (String : null : [IRW]) // The filename of the default icon for all open folder nodes in this treeGrid. // @visibility external // @deprecated as part of SmartClient release 5.5 in favor of +link{TreeGrid.folderIcon} //< //> @attr treeGrid.folderClosedImage (string : null : [IRW]) // The filename of the default icon for all closed folder nodes in this treeGrid. Use // the node.icon property (null by default) to specify a custom image for an individual // folder node. The same custom image will be used for both the open and closed folder // images. // @visibility external // @deprecated as part of SmartClient release 5.5 in favor of +link{TreeGrid.folderIcon} //< //> @attr treeGrid.folderDropImage (String : null : [IRW]) // The filename of the icon displayed for a folder node that will accept drag-and-drop // data when the mouse is released. // @visibility external // @deprecated as part of SmartClient release 5.5 in favor of +link{TreeGrid.folderIcon} //< //> @attr treeGrid.fileImage (SCImgURL : "[SKIN]file.gif" : [IRW]) // The filename of the default icon for all leaf nodes in this treeGrid. Use the // node.icon property (null by default) to specify a custom image for an individual // node. // @visibility external // @deprecated as part of SmartClient release 5.5 in favor of +link{TreeGrid.nodeIcon} //< // -------------------- //> @attr treeGrid.manyItemsImage (SCImgURL : "[SKIN]folder_file.gif" : [IRW]) // The filename of the icon displayed use as the default drag tracker when for multiple // files and/or folders are being dragged. // @group dragdrop // @visibility external //< manyItemsImage:"[SKIN]folder_file.gif", //> @attr treeGrid.showConnectors (boolean : false : [IRW]) // Should this treeGrid show connector lines illustrating the tree's hierarchy? //

// For the set of images used to show connectors, see +link{connectorImage}. //

// Note: in order for connector images to be perfectly connected, all styles for // cells must have no top or bottom border or padding. If you see small gaps in connector // lines, check your CSS files. See the example below for an example of correct // configuration, including example CSS. // // @group treeIcons // @example connectors // @visibility external //< showConnectors : false, //> @attr treeGrid.showFullConnectors (boolean : true : [IRW]) // If +link{treeGrid.showConnectors} is true, this property determines whether we should show // vertical continuation lines for each level of indenting within the tree. Setting to // false will show only the hierarchy lines are only shown for the most indented path ("sparse" // connectors). // @group treeIcons // @visibility external //< // Default to false since older skins won't have all the media required to render full // connector lines out. // The logic to show full connectors involves iterating through the parents for each node // being written out. This is a potential performance hit. We could improve this performance // by adding caching logic to the Tree when calculating where the continuation lines should // appear if this is a problem. showFullConnectors:true, //> @attr treeGrid.showOpener (boolean : true : [IRW]) // Should the an opener icon be displayed next to folder nodes? // @visibility external //< showOpener:true, //> @attr treeGrid.openerImage (SCImgURL : "[SKIN]opener.gif" : [IR]) // The base filename of the opener icon for the folder node when 'showConnectors' is false // for this TreeGrid.
// The opener icon is displayed beside the folder icon in the Tree column for folder nodes. // Clicking on this icon will toggle the open state of the folder.
// The filenames for these icons are assembled from this base filename and the state of the // node, as follows:
// If the openerImage is set to {baseName}.{extension}, // {baseName}_opened.{extension} will be displayed next to opened folders, and // {baseName}_closed.{extension} will be displayed next to closed folders, or // if this page is in RTL mode, {baseName}_opened_rtl.{extension} and // {baseName}_closed_rtl.{extension} will be used. // // @group treeIcons // @visibility external //< openerImage:"[SKIN]opener.gif", //> @attr treeGrid.connectorImage (SCImgURL : "[SKIN]connector.gif" : [IR]) // The base filename for connector icons shown when +link{TreeGrid.showConnectors} is true. // Connector icons are rendered into the title field of each row and show the dotted // hierarchy lines between siblings of the same parent node. For each node, a connector icon // may be shown:

    //
  • As an opener icon for folder nodes, next to the folder icon
  • //
  • In place of an opener icon for leaf nodes, next to the leaf icon
  • //
  • As a standalone vertical continuation line in the indent to the left of the node, to show // a connection between some ancestor node's siblings (only relevant if // +link{TreeGrid.showFullConnectors} is true).
  • //
// Note that +link{TreeGrid.showFullConnectors} governs whether connector lines will be // displayed for all indent levels, or just for the innermost level of the tree. //

// The filenames for these icons are assembled from this base filename and the state of the // node. Assuming the connectorImage is set to {baseName}.{extension}, the // full set of images to be displayed will be: //

// {baseName}_ancestor[_rtl].{extension} if +link{TreeGrid.showFullConnectors} // is true, this is the URL for the vertical continuation image to be displayed at the // appropriate indent levels for ancestor nodes with subsequent children. //

// For nodes with no children: //

    //
  • {baseName}_single[_rtl].{extension}: Shown when there is no connector line // attached to the parent or previous sibling, and no connector line to the next sibling. For // +link{TreeGrid.showFullConnectors,showFullConnectors:true} trees, there will always be a // connector leading to the parent or previous sibling if its present in the tree so this // icon can only be displayed for the first row.
  • //
  • {baseName}_start[_rtl].{extension}: Shown when the there is no connector // line attached to the parent or previous sibling, but there is a connector to the next // sibling. As with _single this will only ever be used for the first row if // +link{TreeGrid.showFullConnectors} is true
  • //
  • {baseName}_end[_rtl].{extension}: Shown if we are not showing a connector // line attached to the next sibling of this node (but are showing a connection to the previous // sibling or parent).
  • //
  • {baseName}_middle[_rtl].{extension}: Shown where the we have a connector // line leading to both the previous sibling (or parent) and the next sibling. //
// For folders with children. Note that if +link{TreeGrid.showFullConnectors} is false, open // folders will never show a connector to subsequent siblings: //
    //
  • {baseName}_opened_single[_rtl].{extension} opened folder node with // children when no connector line is shown attaching to either the folder's previous sibling // or parent, or to any subsequent siblings.
  • //
  • {baseName}_opened_start[_rtl].{extension}: opened folder with children // when the there is no connector line attached to the parent or previous sibling, but there // is a connector to the next sibling.
  • //
  • {baseName}_opened_end[_rtl].{extension}: opened folder with children // if we are not showing a connector line attached to the next sibling of this node (but are // showing a connection to the previous sibling or parent).
  • //
  • {baseName}_opened_middle[_rtl].{extension}: opened folder with children // where the we have a connector line leading to both the previous sibling (or parent) and the // next sibling. //
//
    //
  • {baseName}_closed_single[_rtl].{extension} closed folder node with // children when no connector line is shown attaching to either the folder's previous sibling // or parent, or to any subsequent siblings.
  • //
  • {baseName}_closed_start[_rtl].{extension}: closed folder with children // when the there is no connector line attached to the parent or previous sibling, but there // is a connector to the next sibling.
  • //
  • {baseName}_closed_end[_rtl].{extension}: closed folder with children // if we are not showing a connector line attached to the next sibling of this node (but are // showing a connection to the previous sibling or parent).
  • //
  • {baseName}_closed_middle[_rtl].{extension}: closed folder with children // where the we have a connector line leading to both the previous sibling (or parent) and the // next sibling. //
// (Note '[_rtl]' means that "_rtl" will be attached if isRTL() is true for this widget). // @group treeIcons // @visibility external //< connectorImage:"[SKIN]connector.gif", //> @attr treeGrid.offlineNodeMessage (String : "This data not available while offline" : [IRW]) // For TreeGrids with loadDataOnDemand: true, a message to show the user if an attempt is // made to open a folder, and thus load that node's children, while we are offline and // there is no offline cache of that data. The message will be presented to the user in // in a pop-up dialog box. // // @visibility external // @group offlineGroup, i18nMessages // @see dataBoundComponent.offlineMessage //< offlineNodeMessage: "This data not available while offline", //> @attr treeGrid.indentRecordComponents (boolean : true : IRW) // For record components placed "within" the +link{TreeGridField.treeField,treeField} // column, should the component be indented to the position where a title would normally // show? //

// For more general placement of embedded components, see // +link{ListGrid.addEmbeddedComponent, addEmbeddedComponent}. // // @visibility external //< indentRecordComponents: true, // Disble groupBy for TreeGrids altogether - we're already showing data-derived hierarchy! canGroupBy: false, ignoreEmptyCriteria: false, // users tend to navigate trees by opening and closing nodes more often than by scrolling, // so optimize for that use case. drawAllMaxCells:50, drawAheadRatio:1.0, // heavily used strings for templating _openIconIDPrefix: "open_icon_", _extraIconIDPrefix:"extra_icon_", _iconIDPrefix: "icon_", _titleField: "nodeTitle" }); isc.TreeGrid.addMethods({ initWidget : function () { this.invokeSuper(isc.TreeGrid, this._$initWidget); // if no dataSource is specified on this TG, pick up the dataSource off the data model if (!this.dataSource && this.data != null && this.data.dataSource) { this.dataSource = this.data.dataSource; } // if the fields are not set or of zero length, initialize with a single TREE_FIELD // NB: it is not safe to try to determine the tree field before setFields has been run, // since fields in this.fields might not be shown if they have a showIf:false if (!this.fields || this.fields.length == 0) { this.fields = [isc.TreeGrid.TREE_FIELD]; } }, // override setDataSource - if no fields were passed in, default to showing the tree field. // This matches the behavior if a datbound treeGrid is initialized with no fields. setDataSource : function (ds, fields) { if (fields == null || fields.length == 0) { fields = [isc.TreeGrid.TREE_FIELD]; } return this.Super("setDataSource", [ds, fields]); }, // make sure one of the fields has been set up as the special "tree field" _initTreeField : function () { // if the fields are not set or of zero length, initialize with a single TREE_FIELD if (!this.fields || this.fields.length == 0) { this.fields = [isc.TreeGrid.TREE_FIELD]; } else { // see which field is the tree field. Note this handles both the case that the special // constant TreeGrid.TREE_FIELD was provided as a field, and the case that the caller // marked a field as a the treeField. // if none of the fields is specified as the treeField, we look for a "title" field, // then we default to the first field in the array; we use this.completeFields so that // the treeField property of hidden fields will be checked as well--otherwise we would // default another field to be the tree field, and end up with more than one treeField // if the hidden treeField became visible again. var completeFields = this.completeFields, fields = this.fields, treeFieldNum; for (var i = 0; i < completeFields.length; i++) { if (completeFields[i].treeField) { treeFieldNum = fields.indexOf(completeFields[i]); break; } } if (treeFieldNum == null) { // if autoAssignTreeField has been set false, don't assign a default tree field in // the absence of an explicit marker if (!this.autoAssignTreeField) return; // if there is no explicit marker, look for the field that matches the // titleProperty declared on the Tree var titleProp = this.data.titleProperty, fieldNum = fields.findIndex(this.fieldIdProperty, titleProp); if (fieldNum != -1) treeFieldNum = fieldNum; } // use the first field if none were marked as the tree field if (treeFieldNum == null) treeFieldNum = 0; if (this.isCheckboxField(this.fields[treeFieldNum])) treeFieldNum +=1; // store the chosen fieldNum this._treeFieldNum = treeFieldNum; // use the properties of TREE_FIELD as defaults for the field // Note: We're manipulating the field object in the fields array. // this.completeFields also contains a pointer to this object. // We don't want to replace the slot in either array with a different object as // that would make them out of synch (causes errors sorting, etc.) // - instead just copy any unset properties across from the TREE_FIELD field. var treeField = fields[treeFieldNum], fieldDefaults = isc.TreeGrid.TREE_FIELD; for (var property in fieldDefaults) { if (treeField[property] == null) { treeField[property] = fieldDefaults[property] } } } }, // because we store _treeFieldNum as a number, we need to recalc when fields are changed or // their numbering changes. This include setFields(), reorderFields(), showField() and hideField(). // // Note that the chosen treeField won't shift on reorder, because we install the TREE_FIELD // properties into the chosen field, and the TREE_FIELD properties includes the treeField:true // marker. deriveVisibleFields : function (a,b,c,d) { this.invokeSuper(isc.TreeGrid, "deriveVisibleFields", a,b,c,d); this._initTreeField(); }, getEmptyMessage : function () { if (this.isOffline()) { return this.offlineMessage; } // can't just check for data != null because ListGrid initWidget sets data to [] if unset // and we must make sure we have a tree. if (isc.isA.Tree(this.data) && this.data.getLoadState(this.data.getRoot()) == isc.Tree.LOADING) return this.loadingDataMessage == null ? " " : this.loadingDataMessage.evalDynamicString(this, { loadingImage: this.imgHTML(isc.Canvas.loadingImageSrc, isc.Canvas.loadingImageSize, isc.Canvas.loadingImageSize) }); return this.emptyMessage.evalDynamicString(this, { loadingImage: this.imgHTML(isc.Canvas.loadingImageSrc, isc.Canvas.loadingImageSize, isc.Canvas.loadingImageSize) }); }, isEmpty : function () { // can't just check for data != null because ListGrid initWidget sets data to [] if unset // and we must make sure we have a tree. if (!isc.isA.Tree(this.data)) return true; var root = this.data.getRoot(); if (root == null) return true; var rootHasChildren = this.data.hasChildren(root); if (rootHasChildren || this.showRoot || this.data.showRoot) return false; return true; }, // Folder Animation // --------------------------------------------------------------------------------------- // Because of grouping, the implementation of all of these properties is actually on ListGrid, but is // re-doc'd here for clarity //> @attr treeGrid.animateFolders (boolean : true : IRW) // If true, when folders are opened / closed children will be animated into view. // @group animation // @visibility animation // @example animateTree //< //> @attr treeGrid.animateFolderMaxRows (integer : null : IRW) // If +link{animateFolders} is true for this grid, this number can be set to designate // the maximum number of rows to animate at a time when opening / closing a folder. // @see getAnimateFolderMaxRows() // @group animation // @visibility external //< //> @attr treeGrid.animateFolderTime (number : 100 : IRW) // When animating folder opening / closing, if +link{treeGrid.animateFolderSpeed} is not // set, this property designates the duration of the animation in ms. // @group animation // @visibility animation // @see treeGrid.animateFolderSpeed //< //> @attr treeGrid.animateFolderSpeed (number : 3000 : IRW) // When animating folder opening / closing, this property designates the speed of the // animation in pixels shown (or hidden) per second. Takes precedence over the // +link{treeGrid.animateFolderTime} property, which allows the developer to specify a // duration for the animation rather than a speed. // @group animation // @visibility animation // @see treeGrid.animateFolderTime // @example animateTree //< //> @attr treeGrid.animateFolderEffect (AnimationAcceleration : null : IRW) // When animating folder opening / closing, this property can be set to apply an // animated acceleration effect. This allows the animation speed to be "weighted", for // example expanding or collapsing at a faster rate toward the beginning of the animation // than at the end. // @group animation // @visibility animation //< //> @attr treeGrid.animateRowsMaxTime (number : 1000 : IRW) // If animateFolderSpeed is specified as a pixels / second value, this property will cap // the duration of the animation. // @group animation // @visibility animation_advanced //< //> @method treeGrid.shouldAnimateFolder () // Should this folder be animated when opened / closed? Default implementation will // return true if +link{treeGrid.animateFolders} is true, the folder being actioned // has children and the child-count is less than the result of // +link{treeGrid.getAnimateFolderMaxRows}. // @group animation // @param folder (TreeNode) folder being opened or closed. // @return (boolean) returns true if folders should be animated when opened / closed. // @visibility external //< //> @method treeGrid.getAnimateFolderMaxRows() [A] // If +link{animateFolders} is true for this treeGrid, this method returns the // the maximum number of rows to animate at a time when opening / closing a folder. // This method will return +link{treeGrid.animateFolderMaxRows} if set. Otherwise // the value will be calculated as 3x the number of rows required to fill a viewport, // capped at a maximum value of 75. // @return (integer) maximum number of rows to be animated when opening or closing folders. // @group animation // @visibility external //< // View State // --------------------------------------------------------------------------------------- //> @type treeGridOpenState // An object containing the open state for a treeGrid. // Note that this object is not intended to be interrogated directly, but may be stored // (for example) as a blob on the server for state persistence across sessions. // // @group viewState // @visibility external //< // treeGridOpenState object is implemented as an array of strings, each of which is the path // to a currently open folder (all other folders are closed) //> @method treeGrid.getOpenState() // Returns a snapshot of the current open state of this grid's data as // a +link{type:treeGridOpenState} object.
// This object can be passed to +link{treeGrid.setOpenState()} to open the same set of folders // within the treeGrid's data (assuming the nodes are still present in the data). // @return (treeGridOpenState) current sort state for the grid. // @group viewState // @see treeGrid.setOpenState() // @visibility external //< getOpenState : function () { var tree = this.data; if (tree == null) { this.logWarn("getOpenState() called for a treeGrid with no data"); return []; } var root = tree.getRoot(), openState = []; this._addNodeToOpenState(tree, root, openState); return isc.Comm.serialize(openState); }, // _addNodeToOpenState implemented in ListGrid // Used for groupTree open/closed state maintenance logic //> @method treeGrid.setOpenState() // Reset this set of open folders within this grid's data to match the // +link{type:treeGridOpenState} object passed in.
// Used to restore previous state retrieved from the grid by a call to // +link{treeGrid.getOpenState()}. // // @param openState (treeGridOpenState) Object describing the desired set of open folders. // @group viewState // @see treeGrid.getOpenState() // @visibility external //< setOpenState : function (openState) { openState = this.evalViewState(openState, "openState") if (!openState) return; if (!this.data) { this.logWarn("unable to set open state for this treeGrid as this.data is unset"); return; } this.data.closeAll(); this.data.openFolders(openState); }, //> @method treeGrid.getSelectedPaths() // Returns a snapshot of the current selection within this treeGrid as // a +link{type:listGridSelectedState} object.
// This object can be passed to +link{treeGrid.setSelectedPaths()} to reset this grid's selection // the current state (assuming the same data is present in the grid).
// @group viewState // @see treeGrid.setSelectedPaths(); // @visibility external // @return (listGridSelectedState) current state of this grid's selection //< getSelectedPaths : function () { if (!this.selection) return null; var selection = this.selection.getSelection() || [], selectedPaths = []; // store paths only. for (var i = 0; i < selection.length; i++) { selectedPaths[i] = this.data.getPath(selection[i]); } return isc.Comm.serialize(selectedPaths); }, // ---------------------------------------------------------------------------- // panelHeader related methods showActionInPanel : function (action) { return this.Super("showActionInPanel", arguments); }, //> @method treeGrid.setSelectedPaths() // Reset this grid's selection to match the +link{type:listGridSelectedState} object passed in.
// Used to restore previous state retrieved from the grid by a call to // +link{treeGrid.getSelectedPaths()}. // // @group viewState // @param selectedPaths (listGridSelectedState) Object describing the desired selection state of // the grid // @see treeGrid.getSelectedPaths() // @visibility external //< setSelectedPaths : function (selectedPaths) { selectedPaths = this.evalViewState(selectedPaths, "selectedPaths") if (!selectedPaths) return; var selection = this.selection, data = this.data; if (data && selection) { selection.deselectAll(); var nodes = []; // use find to look up node by path for (var i = 0; i < selectedPaths.length; i++) { var node = data.find(selectedPaths[i]); if (node) nodes.add(node); } this.selection.selectList(nodes); } }, //> @type treeGridViewState // An object containing the "view state" information for a treeGrid. In addition to the // state data contained by a +link{type:listGridViewState} object, this will also contain the // current open state of the treeGrid in question.
// Note that this object is not intended to be interrogated directly, but may be stored // (for example) as a blob on the server for view state persistence across sessions. // // @group viewState // @visibility external //< // treeGridViewState object is implemented as a simple JS object containing the following // fields: // - selected [an (undocumented) treeGridSelectedState object - an array of selected nodes' paths] // - field [a listGridFieldState object] // - sort [a listGridSortState object] // - open [a treeGridOpenState object] //> @method treeGrid.getViewState() // Overridden to return a +link{type:treeGridViewState} object for the grid. // @return (treeGridViewState) current view state for the grid. // @group viewState // @see type:treeGridViewState // @see treeGrid.setViewState(); // @visibility external //< getViewState : function () { var state = this.Super("getViewState", [true]); state.open = this.getOpenState(); return "(" + isc.Comm.serialize(state) + ")"; }, //> @method treeGrid.setViewState() // Overridden to take a +link{type:treeGridViewState} object. // // @param viewState (treeGridViewState) Object describing the desired view state for the grid // @group viewState // @see treeGrid.getViewState() // @visibility external //< setViewState : function (state) { // Ensure we set open state after setting sort state this.Super("setViewState", arguments); // don't bother warning on error - Super() will have done that already state = this.evalViewState(state, "viewState", true) if (!state) return; if (state.open) this.setOpenState(state.open); // Re-apply selection so that nodes just opened can be found. if (state.selected) this.setSelectedState(state.selected); }, // if data is not specified, use an empty Tree. getDefaultData : function () { // NOTE: initializing to a ResultTree would effectively trigger fetch on draw. Don't want // to do this unless fetchData() is called (possibly via autoFetchData property), in which // case the empty starter Tree will be discarded and replaced by a ResultTree //if (this.dataSource) return this.createResultTree(); return isc.Tree.create({_autoCreated:true}); }, //> @method treeGrid.setData() // Set the +link{class:Tree} object this TreeGrid will view and manipulate. // // @param newData (Tree) Tree to show // @visibility external //< setData : function (newData,a,b,c) { this.invokeSuper(isc.TreeGrid, "setData", newData,a,b,c); if (!this.data) return; // set the separateFolders and showRoot options of the tree as well if (this.separateFolders != null) this.data.separateFolders = this.separateFolders; if (this.sortFoldersBeforeLeaves != null) this.data.sortFoldersBeforeLeaves = this.sortFoldersBeforeLeaves; if (this.showRoot && isc.ResultTree && isc.isA.ResultTree(this.data)) { this.logWarn("showRoot may not be set with a databound treeGrid, unexpected " + "results may occur"); } this.data.showRoot = this.showRoot; // should we show only branches or leaves this.data.openDisplayNodeType = this.displayNodeType; }, draw : function (a,b,c,d) { if (this.initialData && (!isc.ResultSet || !isc.isA.ResultSet(this.data))) { this.setData(this.createResultTree()); } this.invokeSuper(isc.TreeGrid, "draw", a,b,c,d); }, bodyConstructor:"TreeGridBody", // Override bodyKeyPress to handle open and closing of trees // Note: standard windows behavior with Left and Right arrow key presses in a treeGrid is: // - multiple selection seems to *always* be disallowed, so doesn't come into play // - arrow right on a closed folder will open the folder // - arrow right on an open folder (with content) will move selection to the first child node // - arrow left on an open folder will close the folder // - arrow left on a node within a folder will move selection to the node's parent folder bodyKeyPress : function (event) { // if exactly one record is selected, mimic windows LV behaviors for arrow left and right var selection = this.selection; if (this.selectionType != isc.Selection.NONE && this.data.getLength() > 0 && selection.anySelected() && !selection.multipleSelected()) { var node = this.selection.getSelectedRecord(); if (event.keyName == "Arrow_Left") { if (this.data.isFolder(node) && this.data.isOpen(node)) { this.closeFolder(node); } else { this._generateRecordClick(this.data.getParent(node), true); } return false; } else if (event.keyName == "Arrow_Right") { if (this.data.isFolder(node)) { if (!this.data.isOpen(node)) { this.openFolder(node); return false; } else { var nextNode = this.getRecord(this.data.indexOf(node) + 1); if (nextNode != null && this.data.getParent(nextNode) == node) { this._generateRecordClick(nextNode, true); return false; } } } } } return this.Super("bodyKeyPress", arguments); }, // fire synthetic context menu events for nodes _cellContextClick : function (record, recordNum, fieldNum) { if (recordNum < 0 || fieldNum < 0) return true; // not in body, allow native context menu var isFolder = this.data.isFolder(record); // fire synthetic context click events. Note any of these can cancel further processing by // returning an explicit false, which presumably indicates they've shown a context menu if (this.nodeContextClick && this.nodeContextClick(this, record, recordNum) == false) { return false; } if (isFolder) { if (this.folderContextClick && this.folderContextClick(this, record, recordNum) == false) { return false; } } else { if (this.leafContextClick && this.leafContextClick(this, record, recordNum) == false) { return false; } } // fire the superclass implementation of this method to fire 'cellContextClick', if defined, // and show the default context menu if appropriate return this.Super("_cellContextClick", arguments); }, //> @method treeGrid.handleEditCellEvent() // @group event handling // Override handleEditCellEvent to not allow editing if the click / doubleClick event // occurred over the open area of the treeGrid // // @return (boolean) false == cancel further event processing //< handleEditCellEvent : function (recordNum, fieldNum) { var record = this.getRecord(recordNum); // if they're clicking in the open or checkbox area of the list, // don't allow editing to proceed if (this.clickInOpenArea(record) || this.clickInCheckboxArea(record)) return false; // return the results of a call to the superclass method return this.Super("handleEditCellEvent",arguments); }, //> @method treeGrid.canEditCell() // Overridden to disallow editing of the +link{treeNode.name, name} field of this grid's data // tree. Also disallows editing of the auto-generated tree field, which displays the result // of +link{method:Tree.getTitle} on the node. // @return (boolean) Whether to allow editing this cell // @visibility external //< canEditCell : function (rowNum, colNum) { if (this.Super("canEditCell", arguments) == false) return false; if (this.getField(colNum)[this.fieldIdProperty] == this.data.nameProperty) return false; if (this.getField(colNum)[this.fieldIdProperty] == this._titleField) return false; return true; }, //> @method treeGrid.startEditingNew() // This inherited +link{listGrid.startEditingNew,ListGrid API} is not supported by the TreeGrid // since adding a new tree node arbitrarily at the end of the tree is usually not useful. // Instead, to add a new tree node and begin editing it, use either of these two strategies: //

    //
  1. add a new node to the client-side Tree model via +link{Tree.add()}, then use // +link{startEditing()} to begin editing this node. Note that if using a DataSource, when the // node is saved, an "update" operation will be used since adding a node directly to the // client-side +link{ResultTree} effectively means a new node has been added server side. //
  2. use +link{DataSource.addData()} to immediately save a new node. Automatic cache sync // by the +link{ResultTree} will cause the node to be integrated into the tree. When the // callback to addData() fires, locate the new node by matching primary key and call // +link{startEditing()} to begin editing it. //
// // @group editing // // @param [newValues] (object) Optional initial set of properties for the new record // @param [suppressFocus] (boolean) Whether to suppress the default behavior of moving focus // to the newly shown editor. // @visibility external //< // Override the method to determine the widths of the form items displayed while editing to // account for the tree-field indents getEditFormItemFieldWidths : function (record) { var level = this.data.getLevel(record); if (!this.showRoot) level--; var openerIconSize = this.getOpenerIconSize(record), indentSize = level * (this.showConnectors ? openerIconSize : this.indentSize) ; indentSize += this.iconSize + openerIconSize; if (this._getCheckboxIcon(record)) { indentSize += (this._getCheckboxFieldImageWidth() + this.extraIconGap); } else if (this.getExtraIcon(record)) { indentSize += (this.iconSize + this.extraIconGap); } var widths = this.Super("getEditFormItemFieldWidths", arguments), treeFieldNum = this.getTreeFieldNum(); widths[treeFieldNum] -= indentSize; return widths; }, // return the DataSource for the current record, to allow embedded editing getRecordDataSource : function (record) { return this.data.getNodeDataSource(record); }, //> @method treeGrid.rowClick() // // This override to +link{ListGrid.rowClick()}. This implementation calls through to the // +link{TreeGrid.nodeClick}, +link{TreeGrid.folderClick}, +link{TreeGrid.leafClick} methods, as // appropriate unless the click was on the expand/collapse control of a folder - in which case // those callbacks are not fired. //

// Do not override this method unless you need a rowClick callback that fires even when the // user clicks on the expand/collapse control. If you do override this method, be sure to call // return this.Super("rowClick", arguments); at the end of your override to // preserver other handler that are called from the superclass (for example, // +link{ListGrid.recordClick()}. //

// // @param record (TreeNode) record that was clicked on // @param recordNum (number) index of the row where the click occurred // @param fieldNum (number) index of the col where the click occurred // // @see TreeGrid.nodeClick() // @see TreeGrid.folderClick() // @see TreeGrid.leafClick() // @see ListGrid.recordClick() // // @visibility external //< rowClick : function (record, recordNum, fieldNum) { var node = record; // if the're clicking in the open or checkbox area of the list, // it's already been processed properly on mouseDown so just bail if (this.clickInOpenArea(node) || this.clickInCheckboxArea(node)) return false; this._lastRecordClicked = recordNum; if (recordNum < 0 || fieldNum < 0) return false; // not in body var node = this.getRecord(recordNum), isFolder = this.data.isFolder(node); if (this.nodeClick) this.nodeClick(this, node, recordNum); if (isFolder) { if (this.folderClick) this.folderClick(this, node, recordNum); } else { if (this.leafClick) this.leafClick(this, node, recordNum); } // execute the super class click method - to pick up field click and recordClick // Note: be sure not to call any handlers the ListGrid will call so as not to get a dup return this.Super("rowClick",arguments); }, //> @method treeGrid.recordDoubleClick() // // Handle a doubleClick on a tree node - override of ListGrid stringMethod of same name. If // the node is a folder, this implementation calls +link{TreeGrid.toggleFolder()} on it. If // the node is a leaf, calls +link{TreeGrid.openLeaf()} on it. // // @see listGrid.recordDoubleClick() // @visibility external //< recordDoubleClick : function (viewer, record, recordNum, field, fieldNum, value, rawValue) { // if the're clicking in the open or checkbox area of the list, // it's already been processed properly on mouseDown so just bail if (this.clickInOpenArea(record) || this.clickInCheckboxArea(record)) return false; // If this is an editable grid, don't toggle the folder, but do return true to allow // editing to proceed. if (this.isEditable() && this.editEvent == isc.EH.DOUBLE_CLICK && this.canEditCell(recordNum,fieldNum)) { return true; } if (this.data.isFolder(record)) { return this.toggleFolder(record); } else return this.openLeaf(record); }, dataChanged : function () { this.Super("dataChanged", arguments); // For a load-on-demand TreeGrid push a full selection to newly loaded child nodes. if (this.cascadeSelection && isc.ResultTree && isc.isA.ResultTree(this.data) && this.data.loadDataOnDemand) { var nodes = this.data.getNodeList(); for (var i = 0; i < nodes.length; i++) { var node = nodes[i]; if (this.data.isFolder(node) && this.data.getLoadState(node) == isc.Tree.LOADED && this.selection.isSelected(node) && !this.selection.isPartiallySelected(node)) { var children = this.data.getChildren(node); for (var j = 0; j < children.length; j++) { var child = children[j]; if (this.data.getLoadState(child) == isc.Tree.UNLOADED && !this.selection.isSelected(child)) { this.selection.setSelected(child, true); } } } } } var folder = this._pendingFolderAnim; if (folder && this.data.isOpen(folder) && this.data.getLoadState(folder) == isc.Tree.LOADED) { this._startFolderAnimation(folder); this._pendingFolderAnim = null; } }, //> @method treeGrid.openLeaf() ([A]) // Executed when a leaf node receives a 'doubleClick' event. This handler must be // specified as a function, whose single parameter is a reference to the relevant leaf node in // the tree's data.
// See the ListGrid Widget Class for inherited recordClick and recordDoubleClick events. // // @visibility external // @param node (TreeNode) node to open // @see class:ListGrid //< openLeaf : function (node) {}, // Drag and Drop // ---------------------------------------------------------------------------------------- //> @method treeGrid.transferDragData() // @include dataBoundComponent.transferDragData() //< // ---------------------------------------------------------------------------------- // Customizations of the drag-tracker for tree grids //> @method treeGrid.getDragTrackerIcon() // Return an icon to display as a drag tracker when the user drags some node(s).
// Default implementation:
// If multiple nodes are selected and +link{TreeGrid.manyItemsImage} is defined, this // image will be returned.
// Otherwise returns the result of +link{TreeGrid.getIcon()} for the first node being // dragged. //

// Note: Only called if +link{listGrid.dragTrackerMode} is set to "icon". // @param records (Array of ListGridRecord) Records being dragged // @return (string) Image URL of icon to display // @group dragTracker // @visibility external //< getDragTrackerIcon : function (records) { var icon; if (records && records.length > 1 && this.manyItemsImage !=null) icon = this.manyItemsImage; else if (records && records[0]) icon = this.getIcon(records[0], true); return icon; }, // Override getDragTrackerTitle() to just return the icon and title of the row, not // the indent, opener icon, etc. // Override not currently documented as it's essentially the same as the superclass // implementation - we just reformat the title cell value to avoid it showing the // indent and opener icon. getDragTrackerTitle : function (record, rowNum, a,b,c,d) { var fieldNum = this.getFieldNum(this.getTitleField()); if (fieldNum != this.getTreeFieldNum()) return this.invokeSuper(isc.TreeGrid, "getDragTrackerTitle", record, rowNum, a,b,c,d); // We need to apply the base (non selected) standard cellStyle/cssText to the drag tracker // table. var cellStyle = this.getCellStyle(record, rowNum, fieldNum), cellCSSText = this.getCellCSSText(record,rowNum,fieldNum); if (this.selection.isSelected(record)) { var styleIndex = this.body.getCellStyleIndex(record, rowNum, fieldNum), standardSelectedStyle = this.body.getCellStyleName(styleIndex, record, rowNum, fieldNum); if (standardSelectedStyle == cellStyle) { styleIndex -= 2; cellStyle = this.body.getCellStyleName(styleIndex, record, rowNum, fieldNum); } } // Call the standard listGrids 'getCellValue()' method to give us the formatted title // of the cell being dragged, excluding the TreeGrid folder/file icons, etc. var value = this.invokeSuper(isc.TreeGrid, "getCellValue", record, rowNum, fieldNum); // Now use _getTreeCellTitleArray() to tack on the icon for the node. var titleCell = this._getTreeCellTitleArray( value, record, rowNum, fieldNum, false, cellStyle, cellCSSText ).join(isc.emptyString); return ["

", titleCell, "
"].join(isc.emptyString); }, //> @method treeGrid.willAcceptDrop() (A) // // This method overrides +link{ListGrid.willAcceptDrop()} and works as follows: //

// First, +link{ListGrid.willAcceptDrop()} (the superclass definition) is consulted. If it // returns false, then this method returns false immediately.
// This handles the following cases:
// - reordering of records withing this TreeGrid when +link{ListGrid.canReorderRecords} is true
// - accepting dropped records from another dragTarget when +link{ListGrid.canAcceptDroppedRecords} // is true and the dragTarget gives us a valid set of records to drop into place.
// - disallowing drop over disabled nodes, or nodes with canAcceptDrop:false //
// This method will also return false if the drop occurred over a leaf node whos immediate // parent has canAcceptDrop set to false
// If +link{TreeGrid.canReparentNodes} is true, and the user is dragging a node from one folder // to another, this method will return true to allow the change of parent folder.
//

// Otherwise this method returns true. // // @group event handling // @return (boolean) true if this component will accept a drop of the dragData // // @visibility external //< willAcceptDrop : function () { // Bail if Superclass willAcceptDrop fails // (Checks that the record is enabled, etc.) if (!this.Super("willAcceptDrop",arguments)) return false; isc._useBoxShortcut = true; // get the record being dropped on var recordNum = this.getEventRecordNum(), newParent = this.data.get(recordNum); isc._useBoxShortcut = false; // dropping in the body in open space means add to root if (newParent == null) newParent = this.data.getRoot(); // if we can't get the new parent, or it can't accept drops, return false if (!newParent || newParent.canAcceptDrop == false) return false; // don't allow drop over non-folder nodes, unless we're allowing record reordering or // canDropOnLeaves is set var isFolder = this.data.isFolder(newParent); if (!isFolder && !(this.canReorderRecords || this.canDropOnLeaves)) return false; // check for dropErrors (dropping record over self, etc.) var moveList = isc.EH.dragTarget.getDragData(); if (!isc.isAn.Object(moveList) || this.getDropError(moveList, newParent) != null) { return false } // Even if we are allowing record reordering, don't allow the user to drop into a // parent with canAcceptDrop explicitly set to false if (!isFolder) { newParent = this.data.getParent(newParent); if (newParent.canAcceptDrop == false) return false; } // If we're dragging data in from another listGrid we're done here // (this relies on canAcceptDropRecords getting checked by the superClass implementation // for this case). if (isc.EH.dragTarget != this) return true; // If we can reorder records, but not reparent, we need to catch the cases where // - records selected currently come from multiple folders // - the drop folder doesn't match the source folder for the node[s] var canReparent = this.canReparentNodes; //>!BackCompat 2006.06.27 if (canReparent == null && this.canAcceptDroppedRecords) canReparent = true; //!BackCompat 2006.06.27 if (canReparentNodes == null && this.canAcceptDroppedRecords) { if (!this._canReparentBackcompatNotified) { this.logInfo("'canReparentNodes' is unset. Allowing node reparenting as " + "'canAcceptDroppedRecords' is set to true. For explicit control, " + "use 'canReparentNodes' instead.", "dragDrop"); this._canReparentBackcompatNotified = true; } canReparentNodes = this.canAcceptDroppedRecords; } // @method treeGrid.dropMove() (A) // mouse is moving over the list while dragging is happening // show a hilite in the appropriate record if necessary // @group event handling // // @return (boolean) false == cancel further event processing //< dropMove : function () { var eventRow = this.getEventRow(); // before the beginning of the rendered area, aka over the header; do nothing if (eventRow == -1) return; // if after the end of the list, choose root var eventNode = (eventRow == -2 ? this.data.getRoot() : this.data.get(eventRow)), dropFolder = this.getDropFolder(), position = (this.canReorderRecords ? this.getReorderPosition(eventRow) : null); // We used to check willAcceptDrop() here, but that prevented spring-loaded folders // from working in the case where the folder being hovered over is will not accept the // drop, but one of its children might accept the drop. So now, we always set the // timer to open a folder being hovered on and updateDropFolder() logic checks for // willAcceptDrop(). // suppress duplicate runs, but updateDropFolder() whenever the lastDropFolder, eventNode // or lastPosition have changed because event though we may still be within the same // dropFolder, we may want to change the dropFolder icon state based on whether the tree // willAcceptDrop() at the new location. if (dropFolder != this.lastDropFolder || eventNode != this._lastEventNode || position != this._lastPosition) { // Set up a function to be executed in the global scope to open the drop folder. if (!this._openDropFolder) { this._openDropFolder = this.getID() + ".openDropFolder()"; } // If there's a running openDropFolderTimer, clear it if (this.openDropFolderTimer) isc.Timer.clear(this.openDropFolderTimer); // If the dropFolder is closed, set up a new openDropFolderTimer if (!this.data.isOpen(dropFolder)) { this.openDropFolderTimer = isc.Timer.setTimeout(this._openDropFolder, this.openDropFolderDelay); } // remember the new drop-folder as this.lastDropFolder, and update its icon. // [note this calls 'willAcceptDrop()'] this.updateDropFolder(dropFolder); } // If the drop is disallowed, show the 'no drop' cursor if (!this.willAcceptDrop()) { this.body.setNoDropIndicator(); } else { this.body.clearNoDropIndicator(); } // Show the drag line if appropriate if (this.canReorderRecords) { if (this.data.isOpen(dropFolder)) this.showDragLineForRecord(eventRow, position); else this.hideDragLine(); } this._lastEventNode = eventNode; this._lastPosition = position; }, //> @method treeGrid.getEventRow() // @include gridRenderer.getEventRow() // @group events // @visibility external //< //> @method treeGrid.getDropFolder() // When the user is dragging a droppable element over this grid, this method returns the folder // which would contain the item if dropped. This is the current drop node if the user is hovering // over a folder, or the node's parent if the user is hovering over a leaf. // @group events // @return (node) target drop folder // @visibility external //< getDropFolder : function () { var eventRow = this.getEventRow(), data = this.data, // before the beginning of the list (over header), or after the end, use root eventNode = (eventRow < 0 ? data.getRoot() : data.get(eventRow)); // if we're over the root, we're going to drop into the root (no choice) if (data.isRoot(eventNode)) return data.getRoot(); var isFolder = data.isFolder(eventNode); // if we can't reorder records, it's easy if (!this.canReorderRecords) return (isFolder ? eventNode : data.getParent(eventNode)); var position = this.getReorderPosition(eventNode); // if we're over a leaf (anywhere), or // we're over the "before" or "after" part (top / bottom 1/4) of any folder, or // we're over the "after" part (bottom 1/4) of a closed or empty folder, return the // parent of the node, if (!isFolder || position == isc.ListGrid.BEFORE || (position == isc.ListGrid.AFTER && (!data.isOpen(eventNode) || !data.hasChildren(eventNode)) ) ) { return data.getParent(eventNode); } else { // In this case we're either over the "over" position of a closed folder, or the // "below" position for an open folder. In either case we'll want to drop into this // folder, before the first child return eventNode; } }, //> @method treeGrid.openDropFolder() (A) // Method to open the folder we're currently hovering over (about to drop) // Called on a timer set up by this.dropMove // @group event handling //< openDropFolder : function () { var dropFolder = this.lastDropFolder; // if we're not over a closed folder, bail! if (!dropFolder || !this.data.isFolder(dropFolder) || this.data.isOpen(dropFolder)) return false; // Open the folder this.openFolder(dropFolder); // show the drag line if we can reorder if (this.canReorderRecords) this.showDragLineForRecord(this.data.indexOf(dropFolder), isc.ListGrid.OVER) }, // Override getReorderPosition to return "over" when over the middle part of a closed folder. getReorderPosition : function (recordNum, y, a,b,c) { // If a y-coordinate was not passed, get it from the offset of the last event if (y == null) y = this.body.getOffsetY(); // which row is the mouse over? if (recordNum == null) recordNum = this.getEventRow(y); var data = this.data; if (!isc.isA.Number(recordNum)) recordNum = data.indexOf(recordNum); var record = data.get(recordNum); if (record && data.isFolder(record)) { var localY = y - this.body.getRowTop(recordNum), recordHeight = this.body.getRowSize(recordNum); // Top 1/4, drop above, // Bottom 1/4, drop below // Middle - drop into folder if (localY < (recordHeight / 4)) { return isc.ListGrid.BEFORE; } else if (localY > (3*recordHeight/4) ) { return isc.ListGrid.AFTER; } else { return isc.ListGrid.OVER; } } // If we're over a leaf, allow the super method to take care of it. return this.invokeSuper(isc.TreeGrid, "getReorderPosition", recordNum, y, a,b,c); }, // Override showDragLineFor record - if the drop will occur inside a folder, we'll show the // drag line after the folder (before the first child) showDragLineForRecord : function (recordNum, position, a,b,c) { if (recordNum == null) recordNum = this.getEventRecordNum(); if (position == null) position = this.getReorderPosition(recordNum); // If dropping over an open folder, show the drag line before the first child (after the // folder) if (position == isc.ListGrid.OVER) { var node = this.getRecord(recordNum), data = this.data; if (data.isFolder(node) && data.isOpen(node)) position = isc.ListGrid.AFTER; } // Have the default implementation actually show the drag line. return this.invokeSuper(isc.TreeGrid, "showDragLineForRecord", recordNum, position, a,b,c); }, //> @method treeGrid.dropOut() (A) // mouse just moved out of the range of the list while dragging is going on // remove the hilite // @group event handling // // @return (boolean) false == cancel further event processing //< dropOut : function () { // Hide drag line this.hideDragLine(); // clear no-drop indicator this.body.clearNoDropIndicator(); // Clear any remembered drop folder this._lastEventNode = null; this.updateDropFolder(); // If we have a timer waiting to open a drop folder, clear it // (Note - if it did fire it would bail anyway because lastDropMoveRow has gone, but // this is more efficient) if (this.openDropFolderTimer) isc.Timer.clear(this.openDropFolderTimer); }, //> @method treeGrid.updateDropFolder() (A) // Takes a record (or record index) as a parameter // Applies the folderDropImage icon to the parameter (or parent folder, if passed a leaf) // Clears out any folderDropImage applied to another folder. // Remembers the folder passed in as this.lastDropFolder. // @group drawing, event handling // // @param newFolder (object or index) //< updateDropFolder : function (newFolder) { var LDF = this.lastDropFolder; this.lastDropFolder = newFolder; // Set the icons on both the previous and current drop folder // // Special _willAcceptDrop flag: set for getIcon() and only update to drop state if the // body willAcceptDrop() the new folder - see comments in dropMove() if (newFolder) { newFolder._willAcceptDrop = this.body.willAcceptDrop(newFolder) this.setRowIcon(newFolder, this.getIcon(newFolder)); } if (LDF && LDF != newFolder) { delete LDF._willAcceptDrop; this.setRowIcon(LDF, this.getIcon(LDF)); } }, //> @method treeGrid.transferSelectedData() // Simulates a drag / drop type transfer of the selected records in some other grid to this // treeGrid, without requiring any user interaction.
// See the +link{group:dragging} documentation for an overview of grid drag/drop data // transfer. // @param sourceGrid (ListGrid) source grid from which the records will be transferred // @param [folder] (TreeNode) parent node into which records should be dropped - if null // records will be transferred as children of the root node. // @param [index] (integer) target index (drop position) within the parent folder // @param [callback] (Callback) optional callback to be fired when the transfer process has // completed. The callback will be passed a single parameter "records", // the list of nodes actually transferred to this component (it is called // "records" because this logic is shared with +link{class:ListGrid}). // @group dragging // @example dragTree // @visibility external //< transferSelectedData : function (source, folder, index, callback) { if (!this.isValidTransferSource(source)) { if (callback) this.fireCallback(callback); return; } // don't check willAcceptDrop() this is essentially a parallel mechanism, so the developer // shouldn't have to set that property directly. if (index == null) index = 0; if (folder == null) folder = this.data.getRoot(); // Call cloneDragData() to pull the records out of the source's dataSet // Note we don't need to call 'transferDragData' here - that is all handled after // transferNodes now, potentially by a server callback var nodes = source.cloneDragData(); this.transferNodes(nodes, folder, index, source, callback); }, //> @method treeGrid.drop() (A) // @group event handling // handle a drop in the list // if possible, move or copy the items automatically // NOTE: at this point, we should be assured that we can accept whatever was dragged... // @return (boolean) false == cancel further event processing //< drop : function () { if (!this.willAcceptDrop()) return false; // NOTE: we perform some redundant checks with willAcceptDrop(), but this is not a time // critical method, and the errors being checked for would corrupt the Tree and so should // never be allowed, so it makes sense to check them here as well since willAcceptDrop() // might be incorrectly overidden. // get what was dropped and where it was dropped var moveList = isc.EH.dragTarget.cloneDragData(), recordNum = this.getEventRecordNum(), position = this.getReorderPosition(recordNum), // dropping in the body in open space means add to root dropItem = this.data.get(recordNum) || this.data.getRoot(), newParent = this.getDropFolder(); //this.logWarn("valid drop with parent: " + this.echo(newParent)); // figure out if this is a drag within the same Tree data model. This can happen within the // same TreeGrid or across two TreeGrids. var dragTree = isc.EH.dragTarget.getData(), dragWithinTree = ( isc.isA.Tree(dragTree) && isc.isA.Tree(this.data) && dragTree.getRoot() == this.data.getRoot() ); // make sure that they're not trying to drag into parent containing child with same name. // NOTE: this particular check is postponed until drop() because it's not self-evident why // the widget won't accept drop, so we want to warn() the user for (var i = 0; i < moveList.length; i++) { var child = moveList[i]; // NOTE: If dragging in from another tree - set dragDataAction to "copy" to test the // code below, otherwise you end up with 2 trees pointing at the same object // name collision: see if there's already a child under the newParent that has the same // name as the child we're trying to put under that parent var collision = (this.data.findChildNum(newParent, this.data.getName(child)) != -1); // this collision is not a problem if we're reordering under the same parent var legalReorder = dragWithinTree && this.canReorderRecords && newParent == this.data.getParent(child); if (collision && !legalReorder) { this.logInfo("already a child named: " + this.data.getName(child) + " under parent: " + this.data.getPath(newParent)); isc.warn(this.parentAlreadyContainsChildMessage); return false; } } // At this point, everything looks OK and we are accepting the drop // figure out where the dropped should be placed in the parent's children var index = null; if (this.canReorderRecords) { if (recordNum < 0) { // already set dropItem to root newParent = dropItem; // special case: dropped in empty area of body, make last child of root index = this.data.getChildren(newParent).getLength(); } else if (dropItem == newParent) { // if dropped directly on a folder, place at beginning of children index = 0; } else { // otherwise place before or after leaf's index within parent index = (position == isc.ListGrid.AFTER ? 1 : 0) + this.data.getChildren(newParent).indexOf(dropItem); } } // if onFolderDrop exists - allow it to cancel the drop if (this.onFolderDrop != null && (this.onFolderDrop(moveList,newParent,index,isc.EH.dragTarget) == false)) return false; this.folderDrop(moveList, newParent, index, isc.EH.dragTarget); // open the folder the nodes were dropped into this.data.openFolder(newParent); // return false to cancel further event processing return false; }, //> @method treeGrid.recordDrop() // The superclass event +link{listGrid.recordDrop} does not fire on a TreeGrid, use // +link{folderDrop} instead. // // @visibility external //< //> @method treeGrid.folderDrop() [A] // // This method processes the drop on a folder in the TreeGrid. The default implementation // works as follows: //

// If the nodes originate in this tree and the +link{TreeGrid.dragDataAction} is "none" or // "move", then the nodes are simply reordered in this TreeGrid. Otherwise (if // +link{TreeGrid.dragDataAction} is "copy" or "none"), this method calls // +link{ListGrid.transferDragData()} on the sourceWidget and adds the returned // rows to this TreeGrid. //

// In either case, the new row(s) appear in the folder at the index // specified by the arguments of the same name. //

// For dataBound treeGrids, folderDrop() will initiate +link{DSRequest,DataSource requests} // to update remote datasets. For nodes moved within the tree, an "update" operation will // be submitted to update the +link{tree.parentIdField,parentId} field of the moved node(s). // For nodes added to a tree, "add" DataSource requests will be submitted with the dropped // node(s) as dsRequest.data. For all drops onto databound treeGrids from other databound // components, if +link{DataBoundComponent.addDropValues,addDropValues} is true, // +link{DataBoundComponent.getDropValues,getDropValues} will be called for every item // being dropped. //

// As a special case, if the sourceWidget is also databound, and a // +link{dataSourceField.foreignKey,foreignKey} relationship is declared from the // sourceWidget's DataSource to this TreeGrid's DataSource, the interaction will // be treated as a "drag recategorization" use case such as files being placed in folders, // employees being assigned to teams, etc. "update" DSRequests will be submitted that // change the foreignKey field in the dropped records to point to the tree folder that was the // target of the drop. In this case no change will be made to the Tree data as such, only to // the dropped records. //

// For multi-record drops, Queuing is automatically used to combine all DSRequests into a // single HTTP Request (see QuickStart Guide, Server Framework chapter). This allows the // server to persist all changes caused by the drop in a single transaction (and this is // automatically done when using the built-in server DataSources with Power Edition and // above). //

// If these default persistence behaviors are undesirable, return false to cancel them, then // and implement your own behavior, typically by using grid.updateData() or addData() to add // new records. //

NOTE: the records you receive in this event are the actual Records from the source // component. Use +link{DataSource.copyRecords()} to create a copy before modifying the records // or using them with updateData() or addData(). // // @param nodes (List of TreeNode) List of nodes being dropped // @param folder (TreeNode) The folder being dropped on // @param index (integer) Within the folder being dropped on, the index at which the drop is // occurring. Only passed if +link{canReorderRecords} is true. // @param sourceWidget (Canvas) The component that is the source of the nodes (where the nodes // were dragged from). // @param [callback] (Callback) optional callback to be fired when the transfer process has // completed. The callback will be passed a single parameter "records", // the list of nodes actually transferred to this component (it is called // "records" because this is logic shared with +link{class:ListGrid}) // // @visibility external // @example treeDropEvents //< folderDrop : function (nodes, folder, index, sourceWidget, callback) { this.transferNodes(nodes, folder, index, sourceWidget, callback); }, // Helper for folderDrop and transferSelectedData transferNodes : function (nodes, folder, index, sourceWidget, callback) { // storeTransferState returns false if a prior transfer is still running, in which case // we just bail out (transferNodes() will be called again when the first transfer // completes, so we aren't abandoning this transfer, just postponing it) if (!this._storeTransferState("transferNodes", nodes, folder, index, sourceWidget, callback)) { return; } // If parent folder is null, we're dropping into the TreeGrid body, which implies root folder = folder || this.data.root; // figure out if this is a drag within the same Tree (even if from another TreeGrid) var dragTree = sourceWidget.getData(), dragWithinTree = ( isc.isA.Tree(dragTree) && isc.isA.Tree(this.data) && dragTree.getRoot() == this.data.getRoot() ); // if we're dropping an item from one tree to another that both share the same root, perform a // move instead. Note that this ignores dragType (eg clone vs copy) completely. var dataSource = this.getDataSource(), sourceDS = sourceWidget.getDataSource(); if (dragWithinTree && (this.dragDataAction != isc.TreeGrid.COPY && this.dragDataAction != isc.TreeGrid.CLONE)) { if (dataSource != null && this.data != null && isc.ResultTree && isc.isA.ResultTree(this.data)) { this._dropRecords[0].noRemove = true; var wasAlreadyQueuing = isc.rpc.startQueue(); // NOTE: We are possibly going to do some client-side reordering here. Depending // on whether we're moving nodes forwards or backwards within their siblings, or // neither (if we're reparenting) or both (if we have multiple selected), we'll be // changing which index within the parent is the correct one to insert at. Thus // we'll establish upfront which is the correct sibling node to insert before, and // always the actual index by reference to that node's current location as the // loop progresses var currentChildren = dragTree.getChildren(folder); var insertBeforeNode, undef; if (index != null) { if (index < currentChildren.length) { insertBeforeNode = currentChildren[index]; } } if (insertBeforeNode == undef) { insertBeforeNode = currentChildren[currentChildren.length - 1]; } for (var i = 0; i < nodes.length; i++) { var node = nodes[i]; if (this.saveLocally || node[this.data.parentIdField] == folder[this.data.idField]) { // The user has dragged a node to a different location within the the same // parent. This change cannot be automatically persisted, so we'll just // reflect the change locally so it doesn't appear to the user that nothing // has happened (though, in fact, nothing *has* happened - some kind of // index update on the underlying persistent store needs to be performed in // order for a user interaction of this type to persist beyond the current // UI session). // If index is null, it's unclear what we should do. We could either leave // the node where it is, or move it to the end of the list (as we would if // we were adding to the parent). This may change, but right now we just // leave it where it is if (index != null) { currentChildren = dragTree.getChildren(folder); dragTree.move(node, folder, currentChildren.indexOf(insertBeforeNode)); } } else { // NOTE: getCleanNodeData() scrubs off the isOpen flag if it was auto- // generated, but we need to hang onto it, otherwise dragging an open // folder from one parent to another causes it to snap shut. var saveIsOpenFlag = nodes[i]["_isOpen_" + this.data.ID]; var node = isc.addProperties({}, this.data.getCleanNodeData(nodes[i], true, false)), oldValues = isc.addProperties({}, node); if (saveIsOpenFlag != null) node["_isOpen_" + this.data.ID] = saveIsOpenFlag; node[this.data.parentIdField] = folder[this.data.idField]; var dropNeighbor = null, children = this.data.getChildren(folder); if (index == null) { dropNeighbor = children.get(children.length - 1); } else if (index > 0) { dropNeighbor = children.get(index - 1); } // We pass a number of parameters relating to this drop up to the server, // so that they are available in the callback. This allows us to give // the impression that a drop has taken place at a particular position // within the parent. This isn't what has actually happened - see the // above comment about dragging nodes to different locations within the // same parent in a databound TreeGrid. this.updateDataViaDataSource(node, dataSource, { oldValues : oldValues, parentNode : this.data.getParent(nodes[i]), newParentNode : folder, dragTree : dragTree, draggedNode : node, draggedNodeList: nodes, dropNeighbor: dropNeighbor, dropIndex : index }, sourceWidget); } } } else { // move the nodes within the tree dragTree.moveList(nodes, folder, index); } } else if (dataSource != null) { var canRecat; if (this.dragRecategorize || (sourceDS != null && sourceDS != dataSource && this.data != null && isc.ResultTree && isc.isA.ResultTree(this.data) && sourceWidget.dragDataAction == isc.TreeGrid.MOVE)) { // check for a foreign key relationship between some field in the source DS to some // field in the treeGrid DS var relationship = sourceDS.getTreeRelationship(dataSource); if (relationship != null && relationship.parentIdField) { var cannotRecat = false, pkFields = sourceDS.getPrimaryKeyFields(); // If the detected foreignKeyField is a Primary Key, we can't modify it. // Catch this case and log a warning for (var pk in pkFields) { if (pk == relationship.parentIdField) { this.logWarn("dragRecategorize: data source has dataSource:" + sourceDS.getID() + ". foreignKey relationship with " + "target dataSource " + dataSource.getID() + " is based on primary key which cannot be modified."); cannotRecat = true; } } if (!cannotRecat) canRecat = true; //>DEBUG this.logInfo("Recategorizing dropped nodes in dataSource:" + sourceDS.getID()); // nodePosition) break; index++; } } if (index === undef) { isc.logWarn("Could not order dropped node by reference to neighbor; trying absolute index"); index = dsRequest.dropIndex; } // If index is still undefined, something's gone wrong. Log a warning and bail if (index === undef) { isc.logWarn("Unable to determine drop location in TreeGrid cache sync"); return; } // dragTree.move(dsRequest.draggedNode, dsRequest.newParentNode, index); var nodeToMove = this.data.find(idField, dsRequest.draggedNode[idField]); dragTree.move(nodeToMove, this.data.getParent(nodeToMove), index); this.Super("_updateComplete", arguments); }, // Tree-specific HTML generation // -------------------------------------------------------------------------------------------- //> @method treeGrid.getTreeCellValue() // Returns the HTML to display a cell with //

    //
  • Indentation
  • //
  • Open / Close Icon (folders only)
  • //
  • Optional extra icon
  • //
  • Folder / Node Icon
  • //
  • Value for the cell
  • //
// OVERRIDE in your subclass for a more complicated presentation // // @param value (string) value to display in the cell // @param record (TreeNode) tree node in question // @param recordNum (number) number of that tree node // @param fieldNum (number) number of the field being output as treeField // // @return (HTML) HTML output for the cell // @visibility internal //< // iconPadding - padding between the folder open/close icon and text. // Make this customizable, but not exposed - very unlikely to be modified iconPadding:3, _$closeTreeCellTable:"", _$semi:";", getTreeCellValue : function (value, record, recordNum, fieldNum) { // This returns HTML to achieve // - an indent equal to what level of the tree you're viewing // - open / close icon // - an optional additional icon // - Folder / Record icon // - title for the cell. // If passed a null record just return the value passed in. if (record == null) { return value; } // get the level of the node var level = this.data.getLevel(record), template = isc.TreeGrid._getTreeCellTemplate(), cssText = this.getCellCSSText(record, recordNum, fieldNum), styleName = this.getCellStyle(record, recordNum, fieldNum); template[1] = styleName template[3] = cssText // catch custom css text with no closing ";" if (template[3] != null && !template[3].endsWith(this._$semi)) template[3] += this._$semi; // styling for indent cell template[5] = cssText; template[7] = styleName; template[9] = this.getIndentHTML(level, record); // Get the HTML for the icons and title from _getTreeCellTitleArray(), and fold them // into our template var titleCellTemplate = this._getTreeCellTitleArray(value, record, recordNum, fieldNum, true, styleName, cssText); for (var i = 0, j = 11; i < titleCellTemplate.length; i++) { template[j] = titleCellTemplate[i]; j++; } template[j] = this._$closeTreeCellTable return template.join(isc.emptyString); }, // _getTreeCellTitleArray() - helper method for getTreeCellValue() to return the // "title" portion of the treeCell value - that is: the icons and the title, without // any indent _getTreeCellTitleArray : function (value, record, recordNum, fieldNum, showOpener, cellStyle,cellCSSText) { if (cellCSSText == null) cellCSSText = this.getCellCSSText(record, recordNum, fieldNum); if (cellStyle == null) cellStyle = this.getCellStyle(record, recordNum, fieldNum); var template = isc.TreeGrid._getTreeCellTitleTemplate(); template[1] = cellCSSText; template[3] = cellStyle; if (showOpener) { // opener icon (or small indent) var openIcon = this.getOpenIcon(record), openIconSize = this.openerIconSize || (this.showConnectors ? this.cellHeight : null), openerID = (recordNum != null ? this._openIconIDPrefix+recordNum : null); if (openIcon) { template[5] = this.getIconHTML(openIcon, openerID, openIconSize); } else { template[5] = this._indentHTML(openIconSize || this.iconSize); } } else template[5] = null; var checkboxIcon = this._getCheckboxIcon(record), extraIcon = checkboxIcon || this.getExtraIcon(record), extraIconID = (recordNum != null ? this._extraIconIDPrefix+recordNum : null), extraIconSize = (checkboxIcon != null ? this._getCheckboxFieldImageWidth() : this.iconSize), extraIconGap = this.extraIconGap, icon = this.getIcon(record), iconID = (recordNum != null ? this._iconIDPrefix+recordNum : null) ; // extra icon if there is one template[6] = (extraIcon ? this.getIconHTML(extraIcon, extraIconID, extraIconSize, extraIconGap) : null); // folder or file icon template[7] = this.getIconHTML(icon, iconID, record.iconSize); template[9] = cellCSSText; template[11] = this.iconPadding; template[13] = cellStyle; template[15] = this.wrapCells ? null : "" template[16] = value; return template; }, //> @method TreeGrid.getCellAlign() // Return the horizontal alignment for cell contents. Default implementation will always // left-align the special +link{treeGridField.treeField} [or right-align if the page is in // RTL mode] - otherwise will return +link{listGridField.cellAlign} if specified, otherwise // +link{listGridField.align}. // // // @param record (ListGridRecord) this cell's record // @param rowNum (number) row number for the cell // @param colNum (number) column number of the cell // @return (Alignment) Horizontal alignment of cell contents: 'right', 'center', or 'left' // @visibility external //< getCellAlign : function (record, rowNum, colNum) { var field = this.getField(colNum); if (field && field.treeField) { return this.isRTL() ? "right" : "left"; } return this.Super("getCellAlign", arguments); }, // Override getCellValue() to return custom HTML for the tree-field // Note: Developers are always advised to override formatCellValue rather than this method // directly (which could lead to certain conflicts). getCellValue : function (record, rowNum, colNum, a, b, c, d) { var value = this.invokeSuper(isc.TreeGrid, "getCellValue", record, rowNum, colNum, a,b,c,d); if (colNum == this.getTreeFieldNum()) { value = this.getTreeCellValue(value, record, rowNum, colNum); } return value; }, // overridden to create/clear draw cache // (Fired for both frozen and normal body) bodyDrawing : function (body,a,b,c,d) { this._drawCache = {}; return this.invokeSuper(isc.TreeGrid, "bodyDrawing", a,b,c,d); }, //> @method TreeGrid.getNodeTitle() // // Returns the title to show for a node in the tree column. If the field specifies the // name attribute, then the current node[field.name] is returned. // Otherwise, the result of calling +link{method:Tree.getTitle} on the node is called. //

// You can override this method to return a custom title for node titles in the tree column. // // @param node (TreeNode) The node for which the title is being requested. // @param recordNum (Number) The index of the node. // @param field (DSField) The field for which the title is being requested. // // @return (HTML) the title to display. // // @see method:Tree.getTitle // // @visibility external //< getNodeTitle : function (record, recordNum, field) { if (field.name && field.name != this._titleField) { if (recordNum == -1) return record[field.name]; return this.getEditedRecord(recordNum)[field.name]; } return this.data.getTitle(record); }, //> @method TreeGrid.getTitleField() // Method to return the fieldName which represents the "title" for records in this // TreeGrid.
// If this.titleField is explicitly specified for this treeGrid, respect it - // otherwise always return the tree-field (+link{TreeGrid.treeField}) for this grid. // @return (string) fieldName for title field for this grid. //< getTitleField : function () { if (this.titleField != null) return this.titleField; return this.getFieldName(this.getTreeFieldNum()); }, //> @method treeGrid.getTreeFieldNum() (A) // Return the number of the tree field for this treeGrid. // // @return (number) Number for the tree node. //< getTreeFieldNum : function () { return this._treeFieldNum; }, //> @method treeGrid.getOpenAreaWidth() (A) // // @param node (TreeNode) tree node clicked on // // @return (number) Return the width of the open area (relative to wherever the tree field is) //< getOpenAreaWidth : function (node) { var openerIconSize = this.getOpenerIconSize(node), indentSize = (this.showConnectors ? openerIconSize : this.indentSize) ; return ((this.data.getLevel(node)-(this.showRoot?0:1)) * indentSize) + openerIconSize; }, getOpenerIconSize : function (node) { return (this.openerIconSize || (this.showConnectors ? this.cellHeight : this.iconSize)); }, //> @method treeGrid.clickInOpenArea() (A) // for a given click, was it in the open/close area or on the main part of the item // OVERRIDE in your subclasses for different open/close schemes // @param node (TreeNode) tree node clicked on // // @return (boolean) true == click was in open area, false == normal click //< clickInOpenArea : function (node) { if (!this.data.isFolder(node)) return false; // get some dimensions var treeFieldNum = this.getTreeFieldNum(), body = this.getFieldBody(treeFieldNum), localFieldNum = this.getLocalFieldNum(treeFieldNum), fieldLeft = body.getColumnLeft(localFieldNum), fieldWidth = body.getColumnWidth(localFieldNum), openAreaWidth = this.getOpenAreaWidth(node), x = body.getOffsetX() ; // textDirection: switch based on drawing in left-to-right (default) or right-to-left order if (this.isRTL()) { var fieldRight = fieldLeft + fieldWidth; return x >= (fieldRight - openAreaWidth) && x <= fieldRight; } else { return x >= fieldLeft && x < fieldLeft + openAreaWidth; } }, //> @method treeGrid.clickInCheckboxArea() (A) // For a given click, was it in the checkbox area? // @param node (TreeNode) tree node clicked on // // @return (boolean) true == click was in checkbox area, false == normal click //< clickInCheckboxArea : function (node) { if (this.selectionAppearance != this._$checkbox) return false; // get some dimensions var treeFieldNum = this.getTreeFieldNum(), body = this.getFieldBody(treeFieldNum), localFieldNum = this.getLocalFieldNum(treeFieldNum), fieldLeft = body.getColumnLeft(localFieldNum), fieldWidth = body.getColumnWidth(localFieldNum), openAreaWidth = this.getOpenAreaWidth(node), checkboxAreaWidth = this._getCheckboxFieldImageWidth(), x = body.getOffsetX() ; // textDirection: switch based on drawing in left-to-right (default) or right-to-left order if (this.isRTL()) { var fieldRight = fieldLeft + fieldWidth; return (x >= (fieldRight - openAreaWidth - checkboxAreaWidth) && x <= (fieldRight - openAreaWidth)); } else { return (x >= (fieldLeft + openAreaWidth) && x < (fieldLeft + openAreaWidth + checkboxAreaWidth)); } }, //> @method treeGrid.getIndentHTML() (A) // Return the HTML to indent a record // @param level (number) indent level (0 == root, 1 == first child, etc) // @param record (treeNode) record for which we're returning indent HTML // // @return (HTML) HTML to indent the child //< getIndentHTML : function (level, record) { var drawLevel = level; if (!this.showRoot) drawLevel--; var indentSize = (this.showConnectors ? this.getOpenerIconSize(record) : this.indentSize); // If showFullConnectors is true we need to write out vertical connector lines between // ancestors who are siblings. if (this.showConnectors && this.showFullConnectors) { // assume the level passed in is correct //var level = this.data.getLevel(record), var levels = this.data._getFollowingSiblingLevels(record); // we don't care about the innermost level (connector written out as part of opener icon) levels.remove(level); if (!this.showRoot) levels.remove(0); if (levels.length != 0) { if (!this._ancestorConnectorURL) { var connectorURL = isc.Img.urlForState(this.connectorImage, null, null, "ancestor"), connectorHTML = this.getIconHTML(connectorURL, null, this.cellHeight); this._ancestorConnectorHTML = connectorHTML; } var singleIndent = this._indentHTML(indentSize), indent = isc.StringBuffer.create(isc.emptyString) ; // explicit NOBR tag required in IE6 to ensure the indents don't wrap // when they run out of horizontal space indent.append("
"); for (var i = (this.showRoot ? 0 : 1); i < level; i ++) { if (levels.contains(i)) indent.append(this._ancestorConnectorHTML); else indent.append(singleIndent); } indent = indent.release(); return indent; } } var indentHTML = this._indentHTML(drawLevel * indentSize); if (isc.Browser.isIE9 || (isc.Browser.isStrict && (isc.Browser.isIE7 || isc.Browser.isIE8))) { indentHTML = "" + indentHTML + ""; } return indentHTML; }, _indentHTML : function (numPixels) { if (numPixels == 0) return isc.emptyString; var cache = isc.TreeGrid._indentHTMLCache; if (cache == null) cache = isc.TreeGrid._indentHTMLCache = {}; if (cache[numPixels] == null) cache[numPixels] = isc.Canvas.spacerHTML(numPixels, 1); return cache[numPixels]; }, //> @method treeGrid.getOpenIcon() (A) // Get the appropriate open/close opener icon for a node. Returns null if +link{showOpener} is // set to false. // // @param node (TreeNode) tree node in question // @return (URL) URL for the icon to show the node's open state // // @visibility external //< getOpenIcon : function (record) { if (this.showOpener == false) return null; if (!this.data) return null; if (isc.isA.Number(record)) record = this.data.get(record); if (record == null) return null; // if the record has a specific openIcon, use that if (record.openIcon) { return record.openIcon; } else { var isFolder = this.data.isFolder(record), // non-folders can never have children, or be open hasChildren = isFolder, isOpen = isFolder, // does this node have adjacent siblings above or below, or is there a gap // between it's next sibling at the same level in either direction. start, end; if (isFolder) { // If the folder doesn't have it's data fully loaded show the // folder as being closed var loadState = this.data.getLoadState(record); if (loadState == isc.Tree.UNLOADED || (loadState == isc.Tree.FOLDERS_LOADED && this.displayNodeType != isc.Tree.FOLDERS_ONLY)) { hasChildren = true; isOpen = false; // If the data is loaded for the folder, use the data APIs to determine // whether this has children or not. } else { hasChildren = this.data.hasChildren(record, this.displayNodeType); isOpen = hasChildren && this.data.isOpen(record); } } // if we're an open folder, showing sparse connectors, we have a gap below us if (isOpen && !this.showFullConnectors) end = true else { end = !this._shouldShowNextLine(record); } start = !this._shouldShowPreviousLine(record); // punt it over to getOpenerImageURL which will assmble the URL from the state info. return this.getOpenerImageURL(hasChildren, isOpen, start, end); } }, // _shouldShowPreviousLine // Internal method - should we show a continuation connector line going up to the previous row // for some record? // True if the previous row is a sibling of this record, or if this is the first record in // some folder (so the previous row contains parent of this record) _shouldShowPreviousLine : function (record) { var rowNum = this.data.indexOf(record); if (rowNum == 0) return false; // always show a previous line if we're showing "full connectors" if (this.showFullConnectors) return true; var previousRecord = this.getRecord(rowNum -1), parent = this.data.getParent(record); if (previousRecord == null) return false; return (parent == previousRecord || parent == this.data.getParent(previousRecord)); }, // _shouldShowNextLine // Internal method - should we show a continuation connector line going down to the next row for // some record? // True only if the next row is a sibling of this record. _shouldShowNextLine : function (record) { if (this.showFullConnectors) { var data = this.data, parent = data.getParent(record), children = data.getChildren(parent); return children.indexOf(record) != children.length-1; } var rowNum = this.data.indexOf(record), nextRecord = this.getRecord(rowNum +1); if (nextRecord == null) return false; return (this.data.getParent(record) == this.data.getParent(nextRecord)); }, //> @method treeGrid.getOpenerImageURL() (A) // Helper method called from getOpenIcon to retrieve the appropriate image URL string for // the opener. // // @param hasChildren (boolean) Is the node in question a folder with children? // @param isOpen (boolean) Is the node an open folder? // @param startLine (boolean) True if the previous row in the TreeGrid is not a sibling // or the parent of the node in question. (Node effectively // starts a new hierarchy continuation line). // @param endLine (boolean) True if the next row in the TreeGrid is not a sibling // of the node in question. (Node effectively ends a // hierarchy continuation line). // @return (string) URL for the icon to show the node's state // // @visibility internal //< getOpenerImageURL : function (hasChildren, isOpen, startLine, endLine) { // Assemble the appropriate filename based on the base filename for connector / opener // images // Do this once per TreeGrid since each TreeGrid can have a different tg.openerImage / // tg.connectorImage. if (!this._openerImageMap) { // Assemble the various file names based on the possible states of these // images - the "opened" state (opened, closed, or has no children) // - the position within the parent folder state // (start, middle, end, separate) // - RTL if appropriate. var img = this.openerImage; this._openerImageMap = { // use Img.urlForState // This handles splitting the base name into base + extension, and plugging in // the state name parameter (third parameter). // opener opened. NOTE: doesn't switch with RTL, which is an assumption that it // will generally be a downward pointing glyph. opened:isc.Img.urlForState(img, null, null, "opened"), // opener closed closed:isc.Img.urlForState(img, null, null, (this.isRTL() ? "closed_rtl" : "closed")), // opener opening opening:isc.Img.urlForState(img, null, null, "opening") } } // generate connector / opener w/connector images if (this.showConnectors && !this._connectorImageMap) { // for connectors, we have start/end/middle for position on a line of connectors, // "single" for a node with no peers, and opener/leaf variations. var img = this.connectorImage, states = ["single", "start", "end", "middle", "opened_single", "opened_start", "opened_middle", "opened_end", "closed_single", "closed_start", "closed_middle", "closed_end"], map = {}, isRTL = this.isRTL(), rtl = "_rtl"; for (var i = 0; i < states.length; i++) { var state = states[i], suffix = state; if (isRTL) suffix += rtl; map[state] = isc.Img.urlForState(img, null, null, suffix); } this._connectorImageMap = map; } if (this.showConnectors) { var imageMap = this._connectorImageMap; if (hasChildren) { if (isOpen) { if (!this.showFullConnectors) { // if we're showing sparse connectors, no need to check for 'end' - if it's // open this is always true. if (startLine) return imageMap.opened_single; return imageMap.opened_end; } if (startLine && endLine) return imageMap.opened_single; else if (startLine) return imageMap.opened_start; else if (endLine) return imageMap.opened_end; else return imageMap.opened_middle; } else { if (startLine && endLine) return imageMap.closed_single; if (startLine) return imageMap.closed_start; if (endLine) return imageMap.closed_end; return imageMap.closed_middle; } } else { // leaf if (startLine && endLine) return imageMap.single; if (startLine) return imageMap.start; if (endLine) return imageMap.end; return imageMap.middle; } } else { var imageMap = this._openerImageMap; // we don't return any image if we're not showing connectors, and this is not a folder // with children. if (!hasChildren) return null; if (isOpen) return imageMap.opened; return imageMap.closed; } }, _$checkbox:"checkbox", _getCheckboxIcon : function (record) { var icon = null; if (this.selectionAppearance == this._$checkbox) { var isSel = this.selection.isSelected(record) ? true : false; var isPartSel = (isSel && this.showPartialSelection && this.selection.isPartiallySelected(record)) ? true : false; // checked if selected, otherwise unchecked icon = isPartSel ? (this.checkboxFieldPartialImage || this.booleanPartialImage) : isSel ? (this.checkboxFieldTrueImage || this.booleanTrueImage) : (this.checkboxFieldFalseImage || this.booleanFalseImage); if (!this.body.canSelectRecord(record)) { if (this.showDisabledSelectionCheckbox) { // show the disabled checkbox, making sure to capture the // disabled state icon = isc.Img.urlForState(icon, null, null, "Disabled"); } else { // record cannot be selected but we want the space allocated for the checkbox anyway. icon = "[SKINIMG]/blank.gif"; } } } return icon; }, //> @method treeGrid.getExtraIcon() (A) // Get an additional icon to show between the open icon and folder/node icon for a particular // node. //

// NOTE: If +link{listGrid.selectionAppearance} is "checkbox", this method will // NOT be called. Extra icons cannot be shown for that appearance. // // @param node (TreeNode) tree node in question // @return (URL) URL for the extra icon (null if none required) // // @visibility external //< getExtraIcon : function (record) { // Default trees don't make use of this. return null; }, //> @method treeGrid.getIcon() // Get the appropriate icon for a node. //

// By default icons are derived from +link{folderIcon} and +link{nodeIcon}. // Custom icons for individual nodes can be overridden by setting the +link{customIconProperty} // on a node. //

// If you want to suppress icons altogether, provide an override of this method that simply // returns null. //

// Note that the full icon URL will be derived by applying +link{Canvas.getImgURL()} to the // value returned from this method. // // @param node (TreeNode) tree node in question // @return (URL) URL for the icon to show for this node // @visibility external //< getIcon : function (node, defaultState) { if (isc.isA.Number(node)) node = this.data.get(node); if (!node) return null; var icon = node[this.customIconProperty], customIcon = (icon != null), isFolder = this.data.isFolder(node); if (!customIcon) { if (isFolder) icon = this.folderIcon; else icon = this.nodeIcon; } var state; if (isFolder) { // Default folder icon is the 'closed' icon. This will be used for dragTrackers, etc // Note: check for the special _willAcceptDrop flag set by updateDropFolder() - when a // user hovers over a folder for a while, we spring it open, and that causes a redraw, // but the folder is not necessarily droppable. var isDrop = defaultState ? false : (this.lastDropFolder == node && node._willAcceptDrop), isOpen = defaultState ? false : !!this.data.isOpen(node); if (isDrop) { // backCompat - respect old dropIcon / folderDropImage if specified if (node.dropIcon != null) icon = node.dropIcon; else if (!customIcon && this.folderDropImage != null) icon = this.folderDropImage; else { var showDrop; if (customIcon) { showDrop = node[this.customIconDropProperty]; if (showDrop == null) showDrop = this.showCustomIconDrop; } else { showDrop = this.showDropIcons; } if (showDrop) state = this.dropIconSuffix; } } else if (isOpen) { // backCompat - respect old openIcon / folderOpenImage if specified if (node.openedIcon != null) icon = node.openedIcon; else if (!customIcon && this.folderOpenImage != null) icon = this.folderOpenImage; // Don't override already set drop state else { var showOpen; if (customIcon) { showOpen = node[this.customIconOpenProperty]; if (showOpen == null) showOpen = this.showCustomIconOpen; } else { showOpen = this.showOpenIcons; } if (showOpen) state = this.openIconSuffix; else if (!customIcon) state = this.closedIconSuffix; } } else { // Respect old 'folderClosedImage' if specified // Otherwise - if the icon is not custom, append "_closed" state if (!customIcon) { if (this.folderClosedImage) icon = this.folderClosedImage; else state = this.closedIconSuffix; } } // not a folder: } else { // Pick up the old 'fileImage' for back compat, if specified. if (!customIcon && this.fileImage) icon = this.fileImage; } return isc.Img.urlForState(icon, false, false, state); }, // helper method - caches generated image templates on a per-draw basis for faster html generation. _$absMiddle : "absmiddle", _$iconExtraStuffTemplate : [ "style='margin-right:", // [0] , // [1] padding on right of icon "px;'" // [2] ], _imgParams : {}, getIconHTML : function (icon, iconID, iconSize, extraRightMargin) { if (icon == null) return isc.emptyString; if (iconSize == null) iconSize = this.iconSize; // make sure the iconHTML cache exists var cache = this._drawCache.iconHTML; if (cache == null) cache = this._drawCache.iconHTML = {}; // if not in cache, generate and store - keyed by the image src if (cache[icon] == null) { var extraStuff; if (extraRightMargin) { var extraStuffTemplate = this._$iconExtraStuffTemplate; extraStuffTemplate[1] = extraRightMargin; extraStuff = extraStuffTemplate.join(isc.emptyString); } var imgParams = this._imgParams; imgParams.src = icon; imgParams.width = imgParams.height = iconSize; imgParams.name = iconID; imgParams.align = this._$absMiddle; imgParams.extraStuff = extraStuff; cache[icon] = this._getImgHTMLTemplate(imgParams); } // now there's guaranteed to be a cached version - grab it var template = cache[icon]; // Note: We need to update the image ID for each icon - this is in the 14'th slot in the // array of strings used as a template (see Canvas.imgHTML()) template[14] = iconID; return template.join(isc._emptyString); }, //> @method treeGrid.setRowIcon() (A) // Set the icon for a particular record to a specified URL (relative to Page.imgDir + this.imgDir // // @param record (TreeNode) tree node // @param URL (URL) URL for the record icon //< setRowIcon : function (record, URL) { // normalize the record from a number if necessary if (!isc.isA.Number(record)) record = this.data.indexOf(record); // set the image if (record != -1 && this.getIcon(record) != null) { this.setImage(this._iconIDPrefix + record, URL); } }, //> @method treeGrid.setNodeIcon() // Set the icon for a particular treenode to a specified URL // // @param node (TreeNode) tree node // @param icon (SCImgUrl) path to the resource // @group treeIcons // @visibility external //< setNodeIcon : function (node, icon) { //make the change persist across redraws node[this.customIconProperty] = icon; //efficiently refresh the image this.setImage(this._iconIDPrefix + this.getRecordIndex(node), icon); }, // ------------------- // Printing getPrintHTML : function (printProperties, callback) { var expand = this.printExpandTree; if (expand == null) expand = printProperties ? printProperties.expandTrees : null; if (expand && this.data) { if (isc.ResultTree && isc.isA.ResultTree(this.data) && this.data.loadDataOnDemand) { this.logWarn("Printing TreeGrid with option to expand folders on print not supported " + "for load on demand trees."); } else { this.data.openAll(); } } return this.Super("getPrintHTML", arguments); }, // Multiple copies of this string are prepended to the tree field, in order to indent it, // when exporting tree data via +link{DataBoundComponent.getClientExportData}. //exportIndentString:null, getExportFieldValue : function (record, fieldName, fieldIndex) { var val = this.Super("getExportFieldValue", arguments); // Prepend tree depth indent string, ensuring that children of root are not indented if (fieldIndex == this.getTreeFieldNum() && this.exportIndentString) { var level = this.data.getLevel(record); while (--level > 0) val = this.exportIndentString + val; } return val; } }); // Register "stringMethods" for this class isc.TreeGrid.registerStringMethods({ // folderDropMove:"viewer,folder,childIndex,child,position", //> @method treeGrid.folderOpened() // // This method is called when a folder is opened either via the user manipulating the // expand/collapse control in the UI or via +link{TreeGrid.openFolder()}. You can return // false to cancel the open. // // @param node (TreeNode) the folder (record) that is being opened // // @return (boolean) false to cancel the open, true to all it to proceed // // @visibility external //< folderOpened : "node", //> @method treeGrid.folderClosed() // // This method is called when a folder is closed either via the user manipulating the // expand/collapse control in the UI or via +link{TreeGrid.closeFolder()}. You can return // false to cancel the close. // // @param node (TreeNode) the folder (record) that is being closed // // @return (boolean) false to cancel the close, true to all it to proceed // // @visibility external //< folderClosed : "node", //> @method treeGrid.folderClick() // // This method is called when a folder record is clicked on. // // @param viewer (TreeGrid) The TreeGrid on which folderClick() occurred. // @param folder (TreeNode) The folder (record) that was clicked // @param recordNum (number) Index of the row where the click occurred. // // @see treeGrid.nodeClick() // // @visibility external //< folderClick : "viewer,folder,recordNum", //> @method treeGrid.leafClick() // // This method is called when a leaf record is clicked on. // // @param viewer (TreeGrid) The TreeGrid on which leafClick() occurred. // @param leaf (TreeNode) The leaf (record) that was clicked // @param recordNum (number) Index of the row where the click occurred. // // @see treeGrid.nodeClick() // // @visibility external //< leafClick : "viewer,leaf,recordNum", //> @method treeGrid.nodeClick() // // This method is called when a leaf or folder record is clicked on. Note that if you set // up a callback for nodeClick() and e.g. +link{treeGrid.leafClick()}, then // both will fire (in that order) if a leaf is clicked on. // // @param viewer (TreeGrid) The TreeGrid on which leafClick() occurred. // @param node (TreeNode) The node (record) that was clicked // @param recordNum (number) Index of the row where the click occurred. // // @see treeGrid.folderClick() // @see treeGrid.leafClick() // // @visibility external // @example treeDropEvents //< nodeClick : "viewer,node,recordNum", //> @method treeGrid.folderContextClick() // // This method is called when a context click occurs on a folder record. // // @param viewer (TreeGrid) The TreeGrid on which the contextclick occurred. // @param folder (TreeNode) The folder (record) on which the contextclick occurred. // @param recordNum (number) Index of the row where the contextclick occurred. // // @return (boolean) whether to cancel the event // // @see treeGrid.nodeContextClick(); // // @visibility external //< folderContextClick : "viewer,folder,recordNum", //> @method treeGrid.leafContextClick() // // This method is called when a context click occurs on a leaf record. // // @param viewer (TreeGrid) The TreeGrid on which the contextclick occurred. // @param leaf (TreeNode) The leaf (record) on which the contextclick occurred. // @param recordNum (number) Index of the row where the contextclick occurred. // // @return (boolean) whether to cancel the event // // @see treeGrid.nodeContextClick(); // // @visibility external //< leafContextClick : "viewer,leaf,recordNum", //> @method treeGrid.nodeContextClick() // // This method is called when a context click occurs on a leaf or folder record. Note that // if you set up a callback for nodeContextClick() and // e.g. +link{treeGrid.leafContextClick}, then both will fire (in that order) if a leaf // is contextclicked - unless nodeContextClick() returns false, in which case // no further contextClick callbacks will be called. // // @param viewer (TreeGrid) The TreeGrid on which the contextclick occurred. // @param node (TreeNode) The node (record) on which the contextclick occurred. // @param recordNum (number) Index of the row where the contextclick occurred. // // @return (boolean) whether to cancel the event // // @see treeGrid.folderContextClick(); // @see treeGrid.leafContextClick(); // // @visibility external //< nodeContextClick : "viewer,node,recordNum", //> @method treeGrid.dataArrived // Notification method fired whenever this TreeGrid receives new data nodes from the // dataSource. Only applies to databound TreeGrids where +link{treeGrid.data} is a // +link{ResultTree} - either explicitly created and applied via +link{treeGrid.setData()} or // automatically generated via a +link{treeGrid.fetchData(),fetchData()} call. // @param parentNode (TreeNode) The parentNode for which children were just loaded // @visibility external //< dataArrived:"parentNode", //> @method treeGrid.onFolderDrop // Notification method fired when treeNode(s) are dropped into a folder of this TreeGrid. // This method fires before the standard +link{method:treeGrid.folderDrop} processing occurs // and returning false will suppress that default behavior. // @param nodes (List of TreeNode) List of nodes being dropped // @param folder (TreeNode) The folder being dropped on // @param index (integer) Within the folder being dropped on, the index at which the drop is // occurring. // @param sourceWidget (Canvas) The component that is the source of the nodes (where the nodes // were dragged from). // @return (boolean) return false to cancel standard folder drop processing // @visibility sgwt //< onFolderDrop:"nodes,folder,index,sourceWidget" });





© 2015 - 2024 Weber Informatics LLC | Privacy Policy