com.smartclient.debug.public.sc.client.widgets.EditMode.js Maven / Gradle / Ivy
Show all versions of smartgwt Show documentation
/*
* Isomorphic SmartClient
* Version SC_SNAPSHOT-2011-08-08 (2011-08-08)
* Copyright(c) 1998 and beyond Isomorphic Software, Inc. All rights reserved.
* "SmartClient" is a trademark of Isomorphic Software, Inc.
*
* [email protected]
*
* http://smartclient.com/license
*/
// Resize thumbs
// --------------------------------------------------------------------------------------------
isc.Canvas.addClassProperties({
resizeThumbConstructor:isc.Canvas,
resizeThumbDefaults:{
width:8,
height:8,
overflow:"hidden",
styleName:"resizeThumb",
canDrag:true,
canDragResize:true,
// resizeEdge should be the edge of the target, not the thumb
getEventEdge : function () { return this.edge; },
autoDraw:false
}
});
isc.Canvas.addClassMethods({
// NOTE: Canvas thumbs vs one-piece mask?
// - since we reuse the same set of thumbs, there's no real performance issue
// - one-piece mask implementations:
// - if an image with transparent regions, thumbs would scale
// - if a table
// - event handling iffy - transparent table areas may or may not intercept
// - would have to redraw on resize
// - transparent Canvas with absolutely-positioned DIVs as content
// - event handling might be iffy
// - would have bug: when thumbs are showing, should be able to click between them to hit
// something behind the currently selected target
// - when thumbs are not showing, mask still needs to be there, but would need to shrink and not
// show thumbs
_makeResizeThumbs : function () {
var edgeCursors = isc.Canvas.getInstanceProperty("edgeCursorMap"),
thumbs = {},
thumbClass = isc.ClassFactory.getClass(this.resizeThumbConstructor);
for (var thumbPosition in edgeCursors) {
// NOTE: can't use standard autoChild creation because we are in static scope -
// thumbs are globally shared
thumbs[thumbPosition] = thumbClass.create({
ID:"isc_resizeThumb_" + thumbPosition,
edge:thumbPosition
}, this.resizeThumbDefaults, this.resizeThumbProperties)
}
isc.Canvas._resizeThumbs = thumbs;
},
showResizeThumbs : function (target) {
if (!target) return;
if (!isc.Canvas._resizeThumbs) isc.Canvas._makeResizeThumbs();
var thumbSize = isc.Canvas.resizeThumbDefaults.width,
thumbs = isc.Canvas._resizeThumbs;
// place the thumbs along the outside of the target
var rect = target.getPageRect(),
left = rect[0],
top = rect[1],
width = rect[2],
height = rect[3],
midWidth = Math.floor(left + (width/2) - (thumbSize/2)),
midHeight = Math.floor(top + (height/2) - (thumbSize/2));
thumbs.T.moveTo(midWidth, top - thumbSize);
thumbs.B.moveTo(midWidth, top + height);
thumbs.L.moveTo(left - thumbSize, midHeight);
thumbs.R.moveTo(left + width, midHeight);
thumbs.TL.moveTo(left - thumbSize, top - thumbSize);
thumbs.TR.moveTo(left + width, top - thumbSize);
thumbs.BL.moveTo(left - thumbSize, top + height);
thumbs.BR.moveTo(left + width, top + height);
for (var thumbName in thumbs) {
var thumb = thumbs[thumbName];
// set all the thumbs to drag resize the canvas we're masking
thumb.dragTarget = target;
// show all the thumbs
thumb.bringToFront();
thumb.show();
}
this._thumbTarget = target;
},
hideResizeThumbs : function () {
var thumbs = this._resizeThumbs;
for (var thumbName in thumbs) {
thumbs[thumbName].hide();
}
this._thumbTarget = null;
}
});
// Edit Mask
// --------------------------------------------------------------------------------------------
// At the Canvas level the Edit Mask provides moving, resizing, and standard context menu items.
// The editMask should be extended on a per-widget basis to add things like drop behaviors or
// additional context menu items. Any such extensions should be delineated with
//>EditMode
// 0) { // multi-select
menuItems =
(edited.editMultiMenuItems || []).concat(this.multiSelectionMenuItems);
} else {
menuItems = (edited.editMenuItems || []).concat(this.standardMenuItems);
}
if (!this.contextMenu) this.contextMenu = this.getMenuConstructor().create({});
this.contextMenu.setData(menuItems);
// NOTE: show the menu on our masterElement (the widget we're masking) so that "target"
// in click methods will be the masterElement and not the mask.
this.contextMenu.showContextMenu(edited);
return false;
},
standardMenuItems:[
{title:"Remove", click:"target.destroy()"},
{title:"Bring to Front", click:"target.bringToFront()"},
{title:"Send to Back", click:"target.sendToBack()"}
],
multiSelectionMenuItems:[
{title:"Remove Selected Items", click:"target.editContext.removeSelection(target)"}
]
},
// Enabling EditMode
// ---------------------------------------------------------------------------------------
// A hook which subclasses can use if they need to know when they have been added to an editContext
addedToEditContext : function (editContext, editNode, parentNode, index) {
},
// A hook called from addComponent, allowing the liveParent to wrap a newNode in some additional
// structure. Return the parentNode that the newNode should be added to. By default, just
// returns the parentNode supplied.
wrapChildNode : function (editContext, newNode, parentNode, index) {
return parentNode;
},
useEditMask:true,
setEditMode : function (editingOn, editContext, editNode) {
if (editingOn == null) editingOn = true;
if (this.editingOn == editingOn) return;
this.editingOn = editingOn;
if (this.editingOn) {
this.editContext = editContext;
} else {
this.hideEditMask();
}
this.editNode = editNode;
// If we're going into edit mode, re-route various methods
if (this.editingOn) {
this.saveToOriginalValues(["click", "doubleClick", "willAcceptDrop",
"clearNoDropIndicator", "setNoDropCursor", "canAcceptDrop",
"canDropComponents", "drop", "dropMove", "dropOver",
"setDataSource"]);
this.setProperties({
click: this.editModeClick,
doubleClick: this.editModeDoubleClick,
willAcceptDrop: this.editModeWillAcceptDrop,
clearNoDropIndicator: this.editModeClearNoDropIndicator,
setNoDropIndicator: this.editModeSetNoDropIndicator,
canAcceptDrop: true,
canDropComponents: true,
drop: this.editModeDrop,
dropMove: this.editModeDropMove,
dropOver: this.editModeDropOver,
baseSetDataSource: this.setDataSource,
setDataSource: this.editModeSetDataSource
});
} else {
this.restoreFromOriginalValues(["click", "doubleClick", "willAcceptDrop",
"clearNoDropIndicator", "setNoDropCursor", "canAcceptDrop",
"canDropComponents", "drop", "dropMove", "dropOver",
"setDataSource"]);
}
// In case anything visual has changed, or the widget has different drag-and-drop
// behavior in edit mode (register/unregisterDroppableItem is called from redraw)
this.markForRedraw();
},
showEditMask : function () {
var svgID = this.getID() + ":
" + this.src;
// create an edit mask if we've never created one
if (!this._editMask) {
// special SVG handling
// FIXME: move all SVG-specific handling to SVG.js
var svgProps = { };
if (isc.SVG && isc.isA.SVG(this) && isc.Browser.isIE) {
isc.addProperties(svgProps, {
backgroundColor : "gray",
mouseOut : function () { this._maskTarget.Super("_hideDragMask"); },
contents : isc.Canvas.spacerHTML(10000,10000, svgID)
});
}
var props = isc.addProperties({}, this.editMaskDefaults, this.editMaskProperties,
// assume the editContext is the parent if none is
// provided
{editContext:this.editContext || this.parentElement,
keepInParentRect: this.keepInParentRect},
svgProps);
this._editMask = isc.EH.makeEventMask(this, props);
}
this._editMask.show();
// SVG-specific
if (isc.SVG && isc.isA.SVG(this)) {
if (isc.Browser.isIE) this.showNativeMask();
else {
this.setBackgroundColor("gray");
this.setContents(svgID);
}
}
},
hideEditMask : function () {
if (this._editMask) this._editMask.hide();
},
editModeClick : function () {
if (this.editNode) {
isc.EditContext.selectCanvasOrFormItem(this, true);
return isc.EH.STOP_BUBBLING;
}
},
editModeDoubleClick : function () {
// No default impl
},
// XXX - Need to do something about Menus in the drop hierarchy - they aren't Class-based
editModeWillAcceptDrop : function (changeObjectSelection) {
this.logInfo("editModeWillAcceptDrop for " + this.ID, "editModeDragTarget");
var dragData = this.ns.EH.dragTarget.getDragData(),
dragType,
draggingFromPalette = true;
// If dragData is null, this is probably because we are drag-repositioning a component
// in a layout - the dragData is the component itself
if (dragData == null || (isc.isAn.Array(dragData)) && dragData.length == 0) {
draggingFromPalette = false;
this.logInfo("dragData is null - using the dragTarget itself", "editModeDragTarget");
dragData = this.ns.EH.dragTarget;
if (isc.isA.FormItemProxyCanvas(dragData)) {
this.logInfo("The dragTarget is a FormItemProxyCanvas for " + dragData.formItem,
"editModeDragTarget");
dragData = dragData.formItem;
}
dragType = dragData._constructor || dragData.Class;
} else {
if (isc.isAn.Array(dragData)) dragData = dragData[0];
dragType = dragData.className || dragData.type;
}
this.logInfo("Using dragType " + dragType, "editModeDragTarget");
if (!this.canAdd(dragType)) {
this.logInfo(this.ID + " does not accept drop of type " + dragType, "editModeDragTarget");
// Can't drop on this widget, so check its ancestors
var ancestor = this.parentElement;
while (ancestor && !ancestor.editorRoot) {
if (ancestor.editingOn) {
var ancestorAcceptsDrop = ancestor.editModeWillAcceptDrop();
if (!ancestorAcceptsDrop) {
this.logInfo("No ancestor accepts drop", "editModeDragTarget");
if (changeObjectSelection != false) {
if (draggingFromPalette) isc.EditContext.hideDragHandle();
isc.SelectionOutline.hideOutline();
this.setNoDropIndicator();
}
return false;
}
this.logInfo("An ancestor accepts drop", "editModeDragTarget");
return true;
}
// Note that the effect of the return statements in the
// condition above is that we'll stop walking
// the ancestor tree at the first parent where editingOn is true ...
// at that point, we'll re-enter editModeWillAcceptDrop
ancestor = ancestor.parentElement;
}
// Given the return statements in the while condition above, we'll only get
// here if no ancestor had editingOn: true
this.logInfo(this.ID + " has no parentElement in editMode", "editModeDragTarget");
if (changeObjectSelection != false) {
if (draggingFromPalette) isc.EditContext.hideDragHandle();
isc.SelectionOutline.hideOutline();
this.setNoDropIndicator();
}
return false;
}
// This canvas can accept the drop, so select its top-level parent (in case it's a
// sub-component like a TabSet's PaneContainer)
this.logInfo(this.ID + " is accepting the " + dragType + " drop", "editModeDragTarget");
var hiliteCanvas = this.findEditNode(dragType);
if (hiliteCanvas) {
if (changeObjectSelection != false) {
this.logInfo(this.ID + ": selecting editNode object " + hiliteCanvas.ID);
if (draggingFromPalette) isc.EditContext.hideDragHandle();
isc.SelectionOutline.select(hiliteCanvas, false);
hiliteCanvas.clearNoDropIndicator();
}
return true;
} else {
this.logInfo("findEditNode() returned null for " + this.ID, "editModeDragTarget");
}
if (changeObjectSelection != false) {
this.logInfo("In editModeWillAcceptDrop, '" + this.ID + "' was willing to accept a '" +
dragType + "' drop but we could not find an ancestor with an editNode");
}
},
// Override to provide special editNode canvas selection (note that this impl does not
// care about dragType, but some special implementations - eg, TabSet - return different
// objects depending on what is being dragged)
findEditNode : function (dragType) {
if (!this.editNode) {
this.logInfo("Skipping '" + this + "' - has no editNode", "editModeDragTarget");
if (this.parentElement && this.parentElement.findEditNode) {
return this.parentElement.findEditNode(dragType);
} else {
return null;
}
}
return this;
},
// tests whether this Canvas can accept a child of type "type". If it can't, and "type"
// names some kind of FormItem, then we'll accept it if this Canvas is willing to accept
// a child of type "Canvas" - we'll cope with this downstream by auto-wrapping the dropped
// FormItem inside a DynamicForm that we create for that very purpose
canAdd : function (type) {
if (this.getObjectField(type) == null) {
var clazz = isc.ClassFactory.getClass(type);
if (isc.isA.FormItem(clazz)) {
return (this.getObjectField("Canvas") != null);
} else {
return false;
}
} else {
return true;
}
},
// Canvas.clearNoDropindicator no-ops if the internal _noDropIndicator flag is null. This
// isn't good enough in edit mode because a canvas can be dragged over whilst the no-drop
// cursor is showing, and we want to revert to a droppable cursor regardless of whether
// _noDropIndicatorSet has been set on this particular canvas.
editModeClearNoDropIndicator : function (type) {
if (this._noDropIndicatorSet) delete this._noDropIndicatorSet;
this._updateCursor();
// XXX May need to add support for no-drop drag tracker here if we ever implement
// such a thing in Visual Builder
},
// Special editMode version of setNoDropCursor - again, because the base version no-ops in
// circumstances where we need it to refresh the cursor.
editModeSetNoDropIndicator : function () {
this._noDropIndicatorSet = true;
this._applyCursor(this.noDropCursor);
},
dropMargin: 15,
shouldPassDropThrough : function () {
var source = isc.EH.dragTarget,
paletteNode,
dropType;
if (!source.isA("Palette")) {
dropType = source.isA("FormItemProxyCanvas") ? source.formItem.Class
: source.Class;
} else {
paletteNode = source.getDragData();
if (isc.isAn.Array(paletteNode)) paletteNode = paletteNode[0];
dropType = paletteNode.className || paletteNode.type;
}
this.logInfo("Dropping a " + dropType, "formItemDragDrop");
if (isc.isA.TabBar(this)) {
if (dropType != "Tab") {
return true;
}
return false;
}
if (!this.canAdd(dropType)) {
this.logInfo("This canvas cannot accept a drop of a " + dropType, "formItemDragDrop");
return true;
}
if (this.parentElement && !this.parentElement.editModeWillAcceptDrop(false)) {
this.logInfo(this.ID + " is not passing drop through - no ancestor is willing to " +
"accept the drop", "editModeDragTarget");
return false;
}
var x = isc.EH.getX(),
y = isc.EH.getY(),
work = this.getPageRect(),
rect = {
left: work[0],
top: work[1],
right: work[0] + work[2],
bottom:work[1] + work[3]
}
if (!this.orientation || this.orientation == "vertical") {
if (x < rect.left + this.dropMargin || x > rect.right - this.dropMargin) {
this.logInfo("Close to right or left edge - passing drop through to parent for " +
this.ID, "editModeDragTarget");
return true;
}
}
if (!this.orientation || this.orientation == "horizontal") {
if (y < rect.top + this.dropMargin || y > rect.bottom - this.dropMargin) {
this.logInfo("Close to top or bottom edge - passing drop through to parent for " +
this.ID, "editModeDragTarget");
return true;
}
}
this.logInfo(this.ID + " is not passing drop through", "editModeDragTarget");
return false;
},
editModeDrop : function () {
if (this.shouldPassDropThrough()) {
return;
}
var source = isc.EH.dragTarget,
paletteNode,
dropType;
if (!source.isA("Palette")) {
if (source.isA("FormItemProxyCanvas")) {
source = source.formItem;
}
dropType = source._constructor || source.Class;
} else {
paletteNode = source.transferDragData();
if (isc.isAn.Array(paletteNode)) paletteNode = paletteNode[0];
paletteNode.dropped = true;
dropType = paletteNode.className || paletteNode.type;
}
// if the source isn't a Palette, we're drag/dropping an existing component, so remove the
// existing component and re-create it in its new position
if (!source.isA("Palette")) {
if (isc.EditContext._dragHandle) isc.EditContext._dragHandle.hide();
if (source == this) return; // Can't drop a component onto itself
var tree = this.editContext.data,
oldParent = tree.getParent(source.editNode);
this.editContext.removeComponent(source.editNode);
var node;
if (source.isA("FormItem")) {
if (source.isA("CanvasItem")) {
node = this.editContext.addNode(source.canvas.editNode, this.editNode);
} else {
node = this.editContext.addWithWrapper(source.editNode, this.editNode);
}
} else {
node = this.editContext.addNode(source.editNode, this.editNode);
}
if (node && node.liveObject) {
isc.EditContext.selectCanvasOrFormItem(node.liveObject, true);
}
} else {
// loadData() operates asynchronously, so we'll have to finish the item drop off-thread
if (paletteNode.loadData && !paletteNode.isLoaded) {
var thisCanvas = this;
paletteNode.loadData(paletteNode, function (loadedNode) {
loadedNode = loadedNode || paletteNode;
loadedNode.isLoaded = true;
thisCanvas.completeItemDrop(loadedNode)
loadedNode.dropped = paletteNode.dropped;
});
return isc.EH.STOP_BUBBLING;
}
this.completeItemDrop(paletteNode);
return isc.EH.STOP_BUBBLING;
}
},
completeItemDrop : function (paletteNode) {
if (!this.editContext) return;
var nodeType = paletteNode.className || paletteNode.type;
var clazz = isc.ClassFactory.getClass(nodeType);
if (clazz && isc.isA.FormItem(clazz)) {
this.editContext.addWithWrapper(paletteNode, this.editNode);
} else {
this.editContext.addComponent(paletteNode, this.editNode);
}
},
editModeDropMove : function () {
if (!this.editModeWillAcceptDrop()) return false;
if (!this.shouldPassDropThrough()) {
this.Super("dropMove", arguments);
if (this.parentElement && this.parentElement.hideDropLine) {
this.parentElement.hideDropLine();
if (isc.isA.FormItem(this.parentElement)) {
this.parentElement.form.hideDragLine();
}
}
return isc.EH.STOP_BUBBLING;
}
},
editModeDropOver : function () {
if (!this.editModeWillAcceptDrop()) return false;
if (!this.shouldPassDropThrough()) {
this.Super("dropOver", arguments);
if (this.parentElement && this.parentElement.hideDropLine) {
this.parentElement.hideDropLine();
if (isc.isA.FormItem(this.parentElement)) {
this.parentElement.form.hideDragLine();
}
}
return isc.EH.STOP_BUBBLING;
}
},
// DataBoundComponent functionality
// ---------------------------------------------------------------------------------------
// In editMode, when setDataSource is called, generate editNodes for each field so that the
// user can modify the generated fields.
// On change of DataSource, remove any auto-gen field that the user has not changed.
editModeSetDataSource : function (dataSource, fields, forceRebind) {
//this.logWarn("editMode setDataSource called" + isc.Log.getStackTrace());
// _loadingNodeTree is a flag set by Visual Builder - its presence indicates that we are
// loading a view from disk. In this case, we do NOT want to perform the special
// processing in this function, otherwise we'll end up with duplicate components in the
// componentTree. So we'll just fall back to the base impl in that case.
if (isc._loadingNodeTree) {
this.baseSetDataSource(dataSource, fields);
return;
}
if (dataSource == null) return;
if (dataSource == this.dataSource && !forceRebind) return;
var fields = this.getFields(),
keepFields = [],
removeFields = [];
// remove all automatically generated fields that have not been edited by the user
if (fields) {
for (var i = 0; i < fields.length; i++) {
var field = fields[i];
if (field.editNode && field.editNode.autoGen && !this.fieldEdited(field)) {
removeFields.add(field);
} else {
keepFields.add(field);
}
}
this.setFields(keepFields);
for (var i = 0; i < removeFields.length; i++) {
this.editContext.removeComponent(removeFields[i].editNode, true);
}
}
// If this dataSource has a single complex field, use the schema of that field in lieu
// of the schema that was dropped.
var schema,
fields = dataSource.fields;
if (fields && isc.getKeys(fields).length == 1 &&
dataSource.fieldIsComplexType(fields[isc.firstKey(fields)].name))
{
schema = dataSource.getSchema(fields[isc.firstKey(fields)].type);
} else {
schema = dataSource;
}
// add one editNode for every field in the DataSource that the component would normally
// display or use.
var allFields = schema.getFields();
fields = {};
for (var key in allFields) {
var field = allFields[key];
if (!this.shouldUseField(field, dataSource)) continue;
// duplicate the field on the DataSource - we don't want to have the live component
// sharing actual field objects with the DataSource.
fields[key] = isc.addProperties({},allFields[key]);
}
// Merge the list of fields to keep (because they were manually added, or changed after
// generation) with the list of fields on the new DataSource. Of course, the "list of
// fields to keep" could well be the empty list (and always will be if this is the first
// time we're binding this DataBoundComponent and the user has not manually added fields)
keepFields.addList(isc.getValues(fields));
this.baseSetDataSource(dataSource, keepFields);
for (var key in fields) {
var field = fields[key];
// What constitutes a "field" varies by DBC type
var fieldConfig = this.getFieldEditNode(field, schema);
var editNode = this.editContext.makeEditNode(fieldConfig);
//this.logWarn("editMode setDataSource adding field: " + field.name);
this.editContext.addNode(editNode, this.editNode, null, null, true);
}
//this.logWarn("editMode setDataSource done adding fields");
},
// whether a field has been edited
fieldEdited : function (field) {
return field.editNode._edited; // flag set in setNodeProperties
},
// get an editNode from a DataSourceField
getFieldEditNode : function (field, dataSource) {
// works for ListGrid, TreeGrid, DetailViewer, etc. DynamicForm overrides
var fieldType = this.Class + "Field";
var editNode = {
type: fieldType,
autoGen: true,
defaults: {
name: field.name,
// XXX this makes the code more verbose since the title could be left blank and be
// inherited from the DataSource. However if we don't supply one here, currently
// the process of creating an editNode and adding to the editTree generates a title
// anyway, and without using getAutoTitle().
title: field.title || dataSource.getAutoTitle(field.name)
}
}
return editNode;
}
});
isc.Class.addMethods({
getSchema : function () {
// NOTE: this.schemaName allows multiple classes to share a single role within editing,
// eg the various possible implementations of tabs, section headers, etc
return isc.DS.get(this.schemaName || this.Class);
},
getSchemaField : function (fieldName) {
return this.getSchema().getField(fieldName);
},
getObjectField : function (type) {
// cache lookups, but only on Canvases. FIXME: we should really cache lookups only for
// framework DataSources
var objectFields = this._objectFields;
if (isc.isA.Canvas(this)) {
var undef;
if (objectFields && objectFields[type] !== undef) {
//this.logWarn("cache hit: " + type);
return objectFields[type];
}
}
var schema = this.getSchema();
if (!schema) {
this.logWarn("getObjectField: no schema exists for: " + this);
return;
}
var fieldName = schema.getObjectField(type);
if (isc.isA.Canvas(this)) {
if (!objectFields) this._objectFields = objectFields = {};
objectFields[type] = fieldName;
}
return fieldName;
},
addChildObject : function (newChildType, child, index, parentProperty) {
return this._doVerbToChild("add", newChildType, child, index, parentProperty);
},
removeChildObject : function (childType, child, parentProperty) {
return this._doVerbToChild("remove", childType, child, parentProperty);
},
_doVerbToChild : function (verb, childType, child, index, parentProperty) {
var fieldName = parentProperty || this.getObjectField(childType);
var field = this.getSchemaField(fieldName);
// for fields that aren't set to multiple, call setProperties to add the object, which
// will look up and use the setter if there is one
// (eg field "contextMenu", "setContextMenu")
if (!field.multiple) {
var props = {};
if (verb == "remove") {
props[fieldName] = null;
} else {
props[fieldName] = child;
}
this.logInfo(verb + "ChildObject calling setProperties for fieldName '" + fieldName +
"'", "editing");
this.setProperties(props);
return true;
}
var methodName = this.getFieldMethod(childType, fieldName, verb);
if (methodName != null) {
this.logInfo("calling " + methodName + "(" + this.echoLeaf(child) +
(index != null ? "," + index + ")" : ")"),
"editing");
this[methodName](child, index);
return true;
}
return false;
},
getChildObject : function (type, id, parentProperty) {
var fieldName = parentProperty || this.getObjectField(type),
field = this.getSchemaField(fieldName);
if (field == null) {
if (parentProperty) {
this.logWarn("getChildObject: no such field '" + parentProperty +
"' in schema: " + this.getSchema());
} else {
this.logWarn("getChildObject: schema for Class '" + this.Class +
"' does not have a field accepting type: " + type);
}
return null;
}
// if the field is not array-valued, just use getProperty, which will auto-discover
// getters
if (!field.multiple) return this.getProperty(fieldName);
// otherwise, look for a getter method and call it with the id
var methodName;
if (isc.isA.ListGrid(this) && fieldName == "fields") {
methodName = "getSpecifiedField";
} else {
methodName = this.getFieldMethod(type, fieldName, "get");
}
if (methodName == null) var methodName = this.getFieldMethod(type, fieldName, "get");
if (methodName && this[methodName]) {
this.logInfo("getChildObject calling: " + methodName + "('"+id+"')", "editing");
return this[methodName](id);
} else {
// if there's no getter method, search the Array directly for something with
// matching id
this.logInfo("getChildObject calling getArrayItem('"+id+"',this." + fieldName + ")",
"editing");
return isc.Class.getArrayItem(id, this[fieldName]);
}
},
// get a method that can perform verb "verb" for an object of type "type" being added to a
// field named "fieldName", eg, "add" (verb) a "Tab" (type) to field "tabs".
// Uses naming conventions to auto-discover methods. Subclasses may need to override for
// non-discoverable methods, eg, canvas.addChild() is not discoverable from the field name
// ("children") or type ("Canvas").
getFieldMethod : function (type, fieldName, verb) {
// NOTE: number of args checks: whether it's an add, remove or get, we're looking for
// something takes arguments, and we don't want to be fooled by eg Class.getWindow()
var funcName = verb+type;
// look for add[type] method, e.g. addTab
if (isc.isA.Function(this[funcName]) &&
isc.Func.getArgs(this[funcName]).length > 0)
{
return funcName;
}
// look for add[singular form of field name] method, e.g. addMember
if (fieldName.endsWith("s")) {
funcName = verb + fieldName.slice(0,-1).toInitialCaps();
if (isc.isA.Function(this[funcName]) &&
isc.Func.getArgs(this[funcName]).length > 0)
{
return funcName;
}
}
},
// EditMode OriginalValues
// ---------------------------------------------------------------------------------------
// When a component enters editMode it may change appearance or change interactive
// behavior, for example, a Tab becomes closable via setting canClose. However if the tab
// is not intended to be closeable in the actual application, when we edit the tab we want
// to show canClose as false and if the user changes the value, we want to track that they
// have changed the value separately from it's temporary setting due to editMode.
//
// get/setEditableProperties allows the component to provide specialized properties to a
// component editor, and saveTo/restoreFromOriginalValues are helpers for a component to
// track it true, savable state from it's temporary editMode settings
getEditableProperties : function (fieldNames) {
var properties = {},
undef;
if (!this.editModeOriginalValues) this.editModeOriginalValues = {};
if (!isc.isAn.Array(fieldNames)) fieldNames = [fieldNames];
for (var i = 0; i < fieldNames.length; i++) {
// Just in case we're passed fields rather than names
var fieldName = isc.isAn.Object(fieldNames[i]) ? fieldNames[i].name : fieldNames[i];
var value = null;
if (this.editModeOriginalValues[fieldName] === undef) {
this.logInfo("Field " + fieldName + " - value [" + this[fieldName] + "] is " +
"coming from live values", "editModeOriginalValues");
value = this[fieldName];
// If this is an observation notification function, pick up the thing being observed,
// not the notification function!
if (isc.isA.Function(value) && value._isObservation) {
value = this[value._origMethodSlot];
}
} else {
this.logInfo("Field " + fieldName + " - value [" +
this.editModeOriginalValues[fieldName] + "] is coming from " +
"original values", "editModeOriginalValues");
value = this.editModeOriginalValues[fieldName];
}
properties[fieldName] = value;
}
return properties;
},
// called to apply properties to an object when it is edited in an EditContext (eg Visual
// Builder) via EditContext.setNodeProperties().
setEditableProperties : function (properties) {
var undef;
if (!this.editModeOriginalValues) this.editModeOriginalValues = {};
for (var key in properties) {
if (this.editModeOriginalValues[key] === undef) {
this.logInfo("Field " + key + " - value is going to live values",
"editModeOriginalValues");
this.setProperty(key, properties[key]);
} else {
this.logInfo("Field " + key + " - value is going to original values",
"editModeOriginalValues");
this.editModeOriginalValues[key] = properties[key];
}
}
this.editablePropertiesUpdated();
},
// called when a child object that is not itself an SC class is having properties applied
// to it in an EditContext. Enables cases like a ListGrid handling changes to it's
// ListGridFields
setChildEditableProperties : function (liveObject, properties, editNode, editContext) {
isc.addProperties(liveObject, properties);
},
saveToOriginalValues : function (fieldNames) {
var undef;
if (!this.editModeOriginalValues) this.editModeOriginalValues = {};
for (var i = 0; i < fieldNames.length; i++) {
// Just in case we're passed fields rather than names
var fieldName = isc.isAn.Object(fieldNames[i]) ? fieldNames[i].name : fieldNames[i];
if (this[fieldName] === undef) {
// We'll have to store it as explicit null, otherwise the downstream code won't
// realize we took a copy
this.editModeOriginalValues[fieldName] = null;
} else {
if (this[fieldName] && this[fieldName]._isObservation) {
// Pick up the original method, not the notification function set up by
// observation.
// If we ever restore the method we want to be restoring the underlying functionality
// and not restoring a notification function which may no longer be valid.
var origMethodName = isc._obsPrefix + fieldName;
this.editModeOriginalValues[fieldName] = this[origMethodName];
} else {
this.editModeOriginalValues[fieldName] = this[fieldName];
}
}
}
},
restoreFromOriginalValues : function (fieldNames) {
var undef;
if (!this.editModeOriginalValues) this.editModeOriginalValues = {};
var logString = "Retrieving fields from original values:"
var changes = {};
for (var i = 0; i < fieldNames.length; i++) {
// Just in case we're passed fields rather than names
var fieldName = isc.isAn.Object(fieldNames[i]) ? fieldNames[i].name : fieldNames[i];
if (this.editModeOriginalValues[fieldName] !== undef) {
changes[fieldName] = this.editModeOriginalValues[fieldName];
// Zap the editModeOriginalValues copy so that future queries will return
// the live value
delete this.editModeOriginalValues[fieldName];
} else {
}
}
// Note use setProperties() rather than just hanging the attributes onto the live
// widget blindly.
// Required because:
// - StringMethods need to be converted to live methods
// - Observation will be left intact (setProperties/addProperties will correctly update
// the renamed underlying method rather than the notification method sitting in its slot)
// - setProperties will fire propertyChanged which we use in some cases (For example
// to update "canDrag" when "canDragRecordsOut" is updated on a ListGrid)
this.setProperties(changes);
},
propertyHasBeenEdited : function (fieldName) {
var undef;
if (!this.editModeOriginalValues) return false;
// Just in case we're passed a field rather than a field name
if (isc.isAn.Object(fieldName)) fieldName = fieldName.name;
if (this.editModeOriginalValues[fieldName] !== undef) {
if (isc.isA.Function(this.editModeOriginalValues[fieldName])) return false;
if (this.editModeOriginalValues[fieldName] != this[fieldName]) return true;
}
return false;
},
// Override if you have a class that needs to be notified when editor properties have
// potentially changed
editablePropertiesUpdated : function () {
if (this.parentElement) this.parentElement.editablePropertiesUpdated();
}
});
isc.DataSource.addClassMethods({
// Given a parent object and child type, use schema to find out what field children
// of that type are kept under
// ---------------------------------------------------------------------------------------
getSchema : function (object) {
if (isc.isA.Class(object)) return object.getSchema();
return isc.DS.get(object.schemaName || object._constructor || object.Class);
},
getObjectField : function (object, type) {
if (object == null) return null;
if (isc.isA.Class(object)) return object.getObjectField(type);
var schema = isc.DS.getSchema(object);
if (schema) return schema.getObjectField(type);
},
getSchemaField : function (object, fieldName) {
var schema = isc.DS.getSchema(object);
if (schema) return schema.getField(fieldName);
},
// Add/remove an object to another object, automatically detecting the appropriate field,
// and calling add/remove functions if they exist on the parent
// ---------------------------------------------------------------------------------------
addChildObject : function (parent, newChildType, child, index, parentProperty) {
return this._doVerbToChild(parent, "add", newChildType, child, index, parentProperty);
},
removeChildObject : function (parent, childType, child, parentProperty) {
return this._doVerbToChild(parent, "remove", childType, child, parentProperty);
},
_doVerbToChild : function (parent, verb, childType, child, index, parentProperty) {
var fieldName = parentProperty || isc.DS.getObjectField(parent, childType);
if (fieldName == null) {
this.logWarn("No field for child of type " + childType);
return false;
}
this.logInfo(verb + " object " + this.echoLeaf(child) +
" in field: " + fieldName +
" of parentObject: " + this.echoLeaf(parent), "editing");
var field = isc.DS.getSchemaField(parent, fieldName);
// if it's a Class, call doVerbToChild on it, which will look for a method that
// modifies the field
if (isc.isA.Class(parent)) {
// if that worked, we're done
if (parent._doVerbToChild(verb, childType, child, index, parentProperty)) return true;
}
// either it's not a Class, or no appropriate method was found, we'll just directly
// manipulate the properties
if (!field.multiple) {
// simple field: "add" is assignment, "remove" is deletion
if (verb == "add") parent[fieldName] = child;
else if (verb == "remove") {
// NOTE: null check avoids creating null slots on no-op removals
if (parent[fieldName] != null) delete parent[fieldName];
} else {
this.logWarn("unrecognized verb: " + verb);
return false;
}
return true;
}
this.logInfo("using direct Array manipulation for field '" + fieldName + "'", "editing");
// Array field: add or remove at index
var fieldArray = parent[fieldName];
if (verb == "add") {
if (fieldArray != null && !isc.isAn.Array(fieldArray)) {
this.logWarn("unexpected field value: " + this.echoLeaf(fieldArray) +
" in field '" + fieldName +
"' when trying to add child: " + this.echoLeaf(child));
return false;
}
if (fieldArray == null) parent[fieldName] = fieldArray = [];
if (index != null) fieldArray.addAt(child, index);
else fieldArray.add(child);
} else if (verb == "remove") {
if (!isc.isAn.Array(fieldArray)) return false;
if (index != null) fieldArray.removeAt(child, index);
else fieldArray.remove(child);
} else {
this.logWarn("unrecognized verb: " + verb);
return false;
}
return true;
},
getChildObject : function (parent, type, id, parentProperty) {
if (isc.isA.Class(parent)) return parent.getChildObject(type, id, parentProperty);
var fieldName = isc.DS.getObjectField(parent, type),
field = isc.DS.getSchemaField(parent, fieldName);
var value = parent[fieldName];
//this.logWarn("getting type: " + type + " from field: " + fieldName +
// ", value is: " + this.echoLeaf(value));
if (!field.multiple) return value;
if (!isc.isAn.Array(value)) return null;
return isc.Class.getArrayItem(id, value);
},
// AutoId: field that should have some kind of automatically assigned ID to make the object
// referenceable in a builder environment
// ---------------------------------------------------------------------------------------
getAutoIdField : function (object) {
var schema = this.getNearestSchema(object);
return schema ? schema.getAutoIdField() : "ID";
},
getAutoId : function (object) {
var fieldName = this.getAutoIdField(object);
return fieldName ? object[fieldName] : null;
}
});
isc.DataSource.addMethods({
getAutoIdField : function () {
return this.getInheritedProperty("autoIdField") || "ID";
},
// In the Visual Builder, whether a component should be create()d before being added to
// it's parent.
// ---------------------------------------------------------------------------------------
shouldCreateStandalone : function () {
if (this.createStandalone != null) return this.createStandalone;
if (!this.superDS()) return true;
return this.superDS().shouldCreateStandalone();
}
});
// Overrides for components that support in-place editing of title
var sharedEditModeFunctions = {
editModeClick : function () {
if (isc.VisualBuilder && isc.VisualBuilder.titleEditEvent == "click") this.editClick();
return this.Super("editModeClick", arguments);
},
editModeDoubleClick : function () {
if (isc.VisualBuilder && isc.VisualBuilder.titleEditEvent == "doubleClick") this.editClick();
return this.Super("editModeDoubleClick", arguments);
}
}
isc.Button.addProperties(sharedEditModeFunctions);
isc.ImgButton.addMethods(sharedEditModeFunctions);
isc.StretchImgButton.addMethods(sharedEditModeFunctions);
isc.SectionHeader.addMethods(sharedEditModeFunctions);
isc.ImgSectionHeader.addMethods(sharedEditModeFunctions);
isc.ImgSectionHeader.addMethods({
setEditMode : function(editingOn, editContext, editNode) {
if (editingOn == null) editingOn = true;
if (editingOn == this.editingOn) return;
this.invokeSuper(isc.TabSet, "setEditMode", editingOn, editContext, editNode);
if (this.editingOn) {
// "background" doesn't yet exist - is presumably created asynchronously
var sectionHeader = this;
isc.Timer.setTimeout(function () {
sectionHeader.saveToOriginalValues(["background"]);
sectionHeader.background.setProperties({
iconClick: sectionHeader.editModeIconClick
})
}, 0);
} else {
this.restoreFromOriginalValues(["background"]);
}
},
editModeIconClick : function () {
var header = this.creator;
if (header) {
var stack = header.layout;
if (stack.sectionIsExpanded(header)) stack.collapseSection(header);
else stack.expandSection(header);
var ctx = header.editContext;
if (ctx) {
ctx.setNodeProperties(header.editNode,
{"expanded" : stack.sectionIsExpanded(header)});
}
}
return this.Super("editModeClick", arguments);
},
editClick : function () {
var left = this.getPageLeft() + this.getLeftBorderSize() + this.getLeftMargin() + 1
- this.getScrollLeft(),
width = this.getVisibleWidth() - this.getLeftBorderSize() - this.getLeftMargin()
- this.getRightBorderSize() - this.getRightMargin() - 1;
isc.Timer.setTimeout({target: isc.EditContext,
methodName: "manageTitleEditor",
args: [this, left, width]}, 100);
}
});
isc.StatefulCanvas.addMethods({
editClick : function () {
var left, width;
if (isc.isA.Button(this)) { // This includes Labels and SectionHeaders
left = this.getPageLeft() + this.getLeftBorderSize() + this.getLeftMargin() + 1
- this.getScrollLeft();
width = this.getVisibleWidth() - this.getLeftBorderSize() - this.getLeftMargin()
- this.getRightBorderSize() - this.getRightMargin() - 1;
} else if (isc.isA.StretchImgButton(this)) {
left = this.getPageLeft() + this.capSize;
width = this.getVisibleWidth() - this.capSize * 2;
} else {
isc.logWarn("Ended up in editClick with a StatefulCanvas of type '" +
this.getClass() + "'. This is neither a Button " +
"nor a StretchImgButton - editor will work, but will hide the " +
"entire component it is editing");
left = this.getPageLeft();
width = this.getVisibleWidth();
}
isc.Timer.setTimeout({target: isc.EditContext,
methodName: "manageTitleEditor",
args: [this, left, width]}, 0);
},
// This function is only called for ImgTabs that need to be scrolled into view
repositionTitleEditor : function () {
var left = this.getPageLeft() + this.capSize,
width = this.getVisibleWidth() - this.capSize * 2;
isc.EditContext.positionTitleEditor(this, left, width);
}
});
// Edit Mode impl for TabSet
// -------------------------------------------------------------------------------------------
if (isc.TabSet) {
isc.TabSet.addClassProperties({
addTabEditorHint: "Enter tab titles (comma separated)"
});
isc.TabSet.addProperties({
defaultPaneDefaults: {
_constructor: "VLayout"
},
setEditMode : function(editingOn, editContext, editNode) {
if (editingOn == null) editingOn = true;
if (editingOn == this.editingOn) return;
this.invokeSuper(isc.TabSet, "setEditMode", editingOn, editContext, editNode);
// If we're going into edit mode, add close icons to every tab
if (this.editingOn) {
for (var i = 0; i < this.tabs.length; i++) {
var tab = this.tabs[i];
this.saveOriginalValues(tab);
this.setCanCloseTab(tab, true);
}
this.closeClick = function(tab) {
this.editContext.removeComponent(tab.editNode);
var tabSet = this;
isc.Timer.setTimeout(function() {tabSet.manageAddIcon()}, 200);
}
} else {
// If we're coming out of edit mode, revert to whatever was on the init data
for (var i = 0; i < this.tabs.length; i++) {
var tab = this.tabs[i];
this.restoreOriginalValues(tab);
var liveTab = this.getTab(tab);
this.setCanCloseTab(tab, liveTab.editNode.initData.canClose);
}
}
// Set edit mode on the TabBar and PaneContainer. Note that we deliberately pass null as
// the editNode - this allows the components to pick up the special editMode method
// overrides, but prevents them from actually being edited
this.tabBar.setEditMode(editingOn, editContext, null);
this.paneContainer.setEditMode(editingOn, editContext, null);
this.manageAddIcon();
},
saveOriginalValues : function (tab) {
var liveTab = this.getTab(tab);
if (liveTab) {
liveTab.saveToOriginalValues(["closeClick", "canClose", "icon", "iconSize",
"iconOrientation", "iconAlign", "disabled"]);
}
},
restoreOriginalValues : function (tab) {
var liveTab = this.getTab(tab);
if (liveTab) {
liveTab.restoreFromOriginalValues(["closeClick", "canClose", "icon", "iconSize",
"iconOrientation", "iconAlign", "disabled"]);
}
},
showAddTabEditor : function () {
var pos = this.tabBarPosition,
align = this.tabBarAlign,
top, left,
height, width,
bar = this.tabBar;
if (pos == isc.Canvas.TOP || pos == isc.Canvas.BOTTOM) {
// Horizontal tabBar
top = this.tabBar.getPageTop();
height = this.tabBar.getHeight();
if (align == isc.Canvas.LEFT) {
left = this.addIcon.getPageLeft();
width = this.tabBar.getVisibleWidth() - this.addIcon.left;
if (width < 150) width = 150;
} else {
width = this.tabBar.getVisibleWidth();
width = width - (width - (this.addIcon.left + this.addIcon.width));
if (width < 150) width = 150;
left = this.addIcon.getPageLeft() + this.addIcon.width - width;
}
} else {
// Vertical tabBar
left = this.tabBar.getPageLeft();
width = 150;
top = this.addIcon.getPageTop();
height = 20;
}
this.manageAddTabEditor(left, width, top, height);
},
manageAddIcon : function () {
if (this.editingOn) {
if (this.addIcon == null) {
this.addIcon = isc.Img.create({
autoDraw: false, width: 16, height: 16,
cursor: "hand",
tabSet: this,
src: "[SKIN]/actions/add.png",
click: function() {this.tabSet.showAddTabEditor();}
});
this.tabBar.addChild(this.addIcon);
}
var lastTab = this.tabs.length == 0 ? null : this.getTab(this.tabs[this.tabs.length-1]);
var pos = this.tabBarPosition,
align = this.tabBarAlign,
addIconLeft,
addIconTop;
if (lastTab == null) {
// Empty tabBar
if (pos == isc.Canvas.TOP || pos == isc.Canvas.BOTTOM) {
// Horizontal tabBar
if (align == isc.Canvas.LEFT) {
addIconLeft = this.tabBar.left + 10;
addIconTop = this.tabBar.top + (this.tabBar.height/2) - (8);
} else {
addIconLeft = this.tabBar.left + this.tabBar.width - 10 - (16); // 16 = icon width
addIconTop = this.tabBar.top + (this.tabBar.height/2) - (8);
}
} else {
// Vertical tabBar
if (align == isc.Canvas.TOP) {
addIconLeft = this.tabBar.left + (this.tabBar.width/2) - (8);
addIconTop = this.tabBar.top + 10;
} else {
addIconLeft = this.tabBar.left + (this.tabBar.width/2) - (8);
addIconTop = this.tabBar.top + this.tabBar.height - 10 - (16)
}
}
} else {
if (pos == isc.Canvas.TOP || pos == isc.Canvas.BOTTOM) {
// Horizontal tabBar
if (align == isc.Canvas.LEFT) {
addIconLeft = lastTab.left + lastTab.width + 10;
addIconTop = lastTab.top + (lastTab.height/2) - (8);
} else {
addIconLeft = lastTab.left - 10 - (16); // 16 = icon width
addIconTop = lastTab.top + (lastTab.height/2) - (8); // 8 = half icon height
}
} else {
// Vertical tabBar
if (align == isc.Canvas.TOP) {
addIconLeft = lastTab.left + (this.width/2) - (8);
addIconTop = lastTab.top + (lastTab.height) + 10;
} else {
addIconLeft = lastTab.left + (this.width/2) - (8);
addIconTop = lastTab.top + (lastTab.height/2) - (8);
}
}
}
this.addIcon.setTop(addIconTop);
this.addIcon.setLeft(addIconLeft);
this.addIcon.show();
} else {
if (this.addIcon && this.addIcon.hide) this.addIcon.hide();
}
},
manageAddTabEditor : function (left, width, top, height) {
if (!isc.isA.DynamicForm(isc.TabSet.addTabEditor)) {
isc.TabSet.addTabEditor = isc.DynamicForm.create({
autoDraw: false,
margin: 0, padding: 0, cellPadding: 0,
fields: [
{
name: "addTabString", type: "text",
hint: isc.TabSet.addTabEditorHint,
showHintInField: true,
showTitle: false,
keyPress : function (item, form, keyName) {
if (keyName == "Escape") {
form.discardUpdate = true;
form.hide();
return
}
if (keyName == "Enter") item.blurItem();
},
blur : function (form, item) {
if (!form.discardUpdate) {
form.targetComponent.editModeAddTabs(item.getValue());
}
form.hide();
}
}
]
});
}
var editor = isc.TabSet.addTabEditor;
editor.addProperties({targetComponent: this});
editor.discardUpdate = false;
var item = editor.getItem("addTabString");
item.setHeight(height);
item.setWidth(width);
item.setValue(item.hint);
editor.setTop(top);
editor.setLeft(left);
editor.show();
item.focusInItem();
item.delayCall("selectValue", [], 100);
},
editModeAddTabs : function (addTabString) {
if (!addTabString || addTabString == isc.TabSet.addTabEditorHint) return;
var titles = addTabString.split(",");
for (var i = 0; i < titles.length; i++) {
var defaultPane = isc.addProperties({}, this.defaultPaneDefaults);
if (!defaultPane.type && !defaultPane.className) {
defaultPane.type = defaultPane._constructor;
}
var tab = {
type: "Tab",
initData: {
title: titles[i]
}
};
var node = this.editContext.addComponent(this.editContext.makeEditNode(tab),
this.editNode);
if (node) {
this.editContext.addComponent(this.editContext.makeEditNode(defaultPane), node);
}
}
},
// Extra stuff to do when tabSet.addTabs() is called when the tabSet is in an editable context
// (though not necessarily actually in editMode)
addTabsEditModeExtras : function (newTabs) {
// Put this on a delay, to give the new tab chance to draw before we start querying its
// drawn size and position
this.delayCall("manageAddIcon");
// If the TabSet is in editMode, put the new tab(s) into edit mode too
if (this.editingOn) {
for (var i = 0; i < newTabs.length; i++) {
this.saveOriginalValues(newTabs[i]);
this.setCanCloseTab(newTabs[i], true);
}
}
},
// Extra stuff to do when tabSet.removeTabs() is called when the tabSet is in an editable
// context (though not necessarily actually in editMode)
removeTabsEditModeExtras : function () {
// Put this on a delay, to give the new tab chance to draw before we start querying its
// drawn size and position
this.delayCall("manageAddIcon");
},
// Override editablePropertiesUpdated() to invoke manageAddIcon() when things change
editablePropertiesUpdated : function () {
this.delayCall("manageAddIcon");
this.invokeSuper(isc.TabSet, "editablePropertiesUpdated");
},
tabScrolledIntoView : function () {
if (!this.editingOn) return;
for (var i = 0; i < this.tabs.length; i++) {
var liveTab = this.getTab(this.tabs[i]);
if (liveTab.titleEditor && liveTab.titleEditor.isVisible()) {
liveTab.repositionTitleEditor();
}
}
},
// Override of Canvas.findEditNode. If the item being dragged is a Tab, falls back to the
// Canvas impl (which will return the TabSet itself). If the item being dragged is not a
// Tab, returns the currently selected Tab if it has an editNode, otherwise the first Tab
// with an editNode, otherwise returns the result of calling the parent element's
// findEditNode(), because this is a TabSet with no tabs in edit mode
findEditNode : function (dragType) {
this.logInfo("In TabSet.findEditNode, dragType is " + dragType, "editModeDragTarget");
if (dragType != "Tab") {
var tab = this.getTab(this.getSelectedTabNumber());
if (tab && tab.editNode) return tab;
for (var i = 0; i < this.tabs.length; i++) {
tab = this.getTab(i);
if (tab.editNode) return tab;
}
if (this.parentElement) return this.parentElement.findEditNode(dragType);
}
return this.Super("findEditNode", arguments);
}
});
isc.TabBar.addMethods({
findEditNode : function (dragType) {
if (dragType == "Tab") {
// Delegate to the TabSet's findEditNode()
return this.parentElement.findEditNode(dragType);
} else if (this.parentElement && isc.isA.Layout(this.parentElement.parentElement)) {
return this.parentElement.parentElement.findEditNode(dragType);
}
return this.Super("findEditNode", arguments);
}
});
}
// Edit Mode impl for Layout
// -------------------------------------------------------------------------------------------
isc.Layout.addMethods({
// Note that setEditMode() at the Canvas level applies editModeDrop et al to the live canvas.
editModeDrop : function () {
if (this.shouldPassDropThrough()) {
this.hideDropLine();
return;
}
isc.EditContext.hideAncestorDragDropLines(this);
var source = isc.EH.dragTarget,
paletteNode,
dropType;
if (!source.isA("Palette")) {
if (source.isA("FormItemProxyCanvas")) {
source = source.formItem;
}
dropType = source._constructor || source.Class;
} else {
paletteNode = source.transferDragData();
if (isc.isAn.Array(paletteNode)) paletteNode = paletteNode[0];
paletteNode.dropped = true;
dropType = paletteNode.className || paletteNode.type;
}
// Establish the actual drop node (this may not be the canvas accepting the drop - for a
// composite component like TabSet, the dropped-on canvas will be the tabBar or
// paneContainer)
var dropTargetNode = this.findEditNode(dropType);
if (dropTargetNode) {
dropTargetNode = dropTargetNode.editNode;
}
// modifyEditNode() is a late-modify hook for components with unusual drop requirements
// that don't fit in with the normal scheme of things (SectionStack only, as of August 09).
// This method can be used to modify the editNode that is going to be the parent - or
// replace it with a whole different one
if (this.modifyEditNode) {
dropTargetNode = this.modifyEditNode(paletteNode, dropTargetNode, dropType);
if (!dropTargetNode) {
this.hideDropLine();
return isc.EH.STOP_BUBBLING;
}
}
// if the source isn't a Palette, we're drag/dropping an existing component, so remove the
// existing component and re-create it in its new position
if (!source.isA("Palette")) {
if (isc.EditContext._dragHandle) isc.EditContext._dragHandle.hide();
if (source == this) return; // Can't drop a component onto itself
var tree = this.editContext.data,
oldParent = tree.getParent(source.editNode),
oldIndex = tree.getChildren(oldParent).indexOf(source.editNode),
newIndex = this.getDropPosition(dropType);
this.editContext.removeComponent(source.editNode);
// If we've moved the child component to a slot further down in the same parent,
// indices will now be off by one because we've just removeed it from its old slot
if (oldParent == this.editNode && newIndex > oldIndex) newIndex--;
var node;
if (source.isA("FormItem")) {
// If the source is a CanvasItem, unwrap it and insert the canvas into this Layout
// directly; otherwise, we would end up with teetering arrangments of Canvases in
// inside CanvasItems inside DynamicForms inside CanvasItems inside DynamicForms...
if (source.isA("CanvasItem")) {
node = this.editContext.addNode(source.canvas.editNode, dropTargetNode, newIndex);
} else {
// Wrap the FormItem in a DynamicForm
node = this.editContext.addWithWrapper(source.editNode, dropTargetNode);
}
} else {
node = this.editContext.addNode(source.editNode, dropTargetNode, newIndex);
}
if (isc.isA.TabSet(dropTargetNode.liveObject)) {
dropTargetNode.liveObject.selectTab(source);
} else if (node && node.liveObject) {
isc.EditContext.delayCall("selectCanvasOrFormItem", [node.liveObject, true], 200);
}
} else {
var nodeAdded;
var clazz = isc.ClassFactory.getClass(dropType);
if (clazz && clazz.isA("FormItem")) {
// create a wrapper form to allow the FormItem to be added to this Canvas
nodeAdded = this.editContext.addWithWrapper(paletteNode, dropTargetNode);
} else {
nodeAdded = this.editContext.addComponent(paletteNode, dropTargetNode,
this.getDropPosition(dropType));
}
// FIXME - this is almost hackery, needs to be factored more cleanly
if (nodeAdded != null) {
var liveObj = paletteNode.liveObject;
if (isc.isA.TabSet(dropTargetNode.liveObject)) {
var defaultPane = isc.addProperties({}, dropTargetNode.liveObject.defaultPaneDefaults);
if (!defaultPane.type && !defaultPane.className) {
defaultPane.type = defaultPane._constructor;
}
this.editContext.addComponent(
this.editContext.makeEditNode(defaultPane, nodeAdded));
dropTargetNode.liveObject.selectTab(liveObj);
}
if (isc.isA.TabSet(liveObj)) {
liveObj.delayCall("showAddTabEditor");
} else if (isc.isA.ImgTab(liveObj) ||
isc.isA.Button(liveObj) ||
isc.isA.StretchImgButton(liveObj) ||
isc.isA.SectionHeader(liveObj) ||
isc.isA.ImgSectionHeader(liveObj)) {
// Give the object a chance to draw before we start the edit, otherwise the
// editor co-ordinates will be wrong
liveObj.delayCall("editClick");
}
}
}
this.hideDropLine();
return isc.EH.STOP_BUBBLING;
},
editModeDropMove : function () {
if (!this.editModeWillAcceptDrop()) return false;
if (!this.shouldPassDropThrough()) {
this.Super("dropMove", arguments);
if (this.parentElement && this.parentElement.hideDropLine) {
this.parentElement.hideDropLine();
if (isc.isA.FormItem(this.parentElement)) {
this.parentElement.form.hideDragLine();
}
}
return isc.EH.STOP_BUBBLING;
} else {
this.hideDropLine();
}
},
editModeDropOver : function () {
if (!this.editModeWillAcceptDrop()) return false;
if (!this.shouldPassDropThrough()) {
this.Super("dropOver", arguments);
if (this.parentElement && this.parentElement.hideDropLine) {
this.parentElement.hideDropLine();
if (isc.isA.FormItem(this.parentElement)) {
this.parentElement.form.hideDragLine();
}
}
return isc.EH.STOP_BUBBLING;
} else {
this.hideDropLine();
}
}
});
// Edit Mode impl for PortalLayout and friends
// -------------------------------------------------------------------------------------------
//
// Note that PortalLayout and friends have some special features with respect to EditMode.
//
// 1. Even in "live" mode (rather than just "edit" mode), you can drag nodes from a Palette to
// a PortalLayout and it will do the right thing -- it will create the liveObject from the node,
// and, if necessary, wrap it in a Portlet. Of course, you have to be in "edit" mode to edit
// the contents of a Portlet.
//
// 2. The normal user interface of PortalLayout allows the user to adjust the number of columns,
// move columns around, move Portlets around, etc. Even in "live" mode, the code will adjust
// the editNodes so that they correspond to the user's actions. You can see this in
// Visual Builder, for instance, by creating a PortalLayout with some Portlets in "edit" mode,
// and then switching to "live" mode and moving the Portlets around -- the editNodes will follow.
//
// In order to make this work, there are some bits of code in Portal.js that take account of
// edit mode, but the larger pieces that can be broken out separately are here.
isc.Portlet.addProperties({
// Keep track of our editContext and editNode even if editMode is not on yet
addedToEditContext : function (editContext, editNode) {
this.editContext = editContext;
this.editNode = editNode;
},
canAdd : function (type) {
// Don't let Portlets be added directly to Portlets, because it is almost never what
// would be wanted.
if (type == "Portlet") return false;
return this.Super("canAdd", arguments);
}
});
isc.PortalRow.addProperties({
// Keep track of our editContext and editNode even if editMode is not on yet
addedToEditContext : function (editContext, editNode) {
this.editContext = editContext;
this.editNode = editNode;
if (editNode.generatedType) delete editNode.generatedType;
},
wrapChildNode : function (editContext, newNode, parentNode, index) {
var liveObject = newNode.liveObject;
if (isc.isA.Portlet(liveObject)) {
// If it's a portlet, then we're fine
return parentNode;
} else {
// If it's something else, we'll wrap it in a Portlet
var portletNode = editContext.makeEditNode({
type: "Portlet",
defaults: {
title: newNode.title,
destroyOnClose: true
}
});
editContext.addNode(portletNode, parentNode, index);
return portletNode;
}
},
// Called from getDropComponent to deal with drops from palettes
handleDroppedEditNode : function (dropComponent, dropPosition) {
var editContext = this.editContext;
var editNode = this.editNode;
if (isc.isA.Palette(dropComponent)) {
// Drag and drop from palette
var data = dropComponent.transferDragData(),
component = (isc.isAn.Array(data) ? data[0] : data);
if (editContext && editNode) {
// If we have an editContext and editNode, just use them. The wrapping
// is handled by wrapChildNode in this case. We return false to cancel the drop,
// since addNode will have taken care of it.
editContext.addNode(component, editNode, dropPosition);
return false;
} else {
// If we don't have an editContext and editNode. then we'll wrap the liveObject
// in a Portlet if necessary.
if (isc.isA.Portlet(component.liveObject)) {
// If it's a Portlet, we're good
dropComponent = component.liveObject;
} else {
// If not, we'll wrap it in one
dropComponent = isc.Portlet.create({
autoDraw: false,
title: component.title,
items: [component.liveObject],
destroyOnClose: true
});
}
}
}
return dropComponent;
}
});
isc.PortalColumnBody.addProperties({
// Called from getDropComponent to deal with drops from palettes
handleDroppedEditNode : function (dropComponent, dropPosition) {
var editContext = this.creator.editContext;
var editNode = this.creator.editNode;
if (isc.isA.Palette(dropComponent)) {
// Drag and drop from palette
var data = dropComponent.transferDragData(),
component = (isc.isAn.Array(data) ? data[0] : data);
if (editContext && editNode) {
// If we have an editContext and editNode, just use them. The wrapping
// is handled by wrapChildNode in this case. We return false to cancel the drop,
// since addNode will have taken care of it.
editContext.addNode(component, editNode, dropPosition);
return false;
} else {
// If we don't have an editContext and editNode, then wrap the liveObject
// in a Portlet if necessary.
if (isc.isA.Portlet(component.liveObject)) {
// If it's a Portlet, we're good
dropComponent = component.liveObject;
} else {
// If not, we'll wrap it in one
dropComponent = isc.Portlet.create({
autoDraw: false,
title: component.title,
items: [component.liveObject],
destroyOnClose: true
});
}
}
}
if (dropComponent) {
// We need to check whether the dropComponent is already the only portlet
// in an existing row. If so, we can simplify by just dropping
// the row -- that is what the user will have meant.
var currentRow = dropComponent.portalRow;
if (currentRow && currentRow.parentElement == this && currentRow.getMembers().length == 1) {
// Check whether we need to adjust the editNodes
if (editContext && editNode && currentRow.editNode) {
var currentIndex = this.getMemberNumber(currentRow);
// Check if we're not really changing position
if (dropPosition == currentIndex || dropPosition == currentIndex + 1) return;
editContext.removeNode(currentRow.editNode);
// Adjust dropPosition if we are dropping after the currentIndex
if (currentIndex < dropPosition) dropPosition -= 1;
editContext.addNode(currentRow.editNode, editNode, dropPosition);
return null;
}
} else {
// If we're not moving a whole current row, then we add the new portlet, creating a new row
if (editContext && editNode && dropComponent.editNode) {
editContext.addNode(dropComponent.editNode, editNode, dropPosition);
return null;
}
}
}
// We'll get here if we're not doing something special with the dropComponent's editNode ...
// in that case, we can return it and getDropComponent can handle it.
return dropComponent;
}
});
isc.PortalColumn.addProperties({
wrapChildNode : function (editContext, newNode, parentNode, index) {
var liveObject = newNode.liveObject;
if (isc.isA.PortalRow(liveObject) || newNode.type == "PortalRow") {
// If it's a PortalRow, then we're fine
return parentNode;
} else if (isc.isA.Portlet(liveObject)) {
// If it's a portlet, then we'll wrap it in a row
var rowNode = editContext.makeEditNode({
type: this.rowConstructor,
defaults: {}
});
editContext.addNode(rowNode, parentNode, index);
return rowNode;
} else {
// If it's something else, we'll wrap it in a Portlet
var portletNode = editContext.makeEditNode({
type: "Portlet",
defaults: {
title: newNode.title,
destroyOnClose: true
}
});
// Note that when we add the Portlet node, we'll eventually
// get back here to wrap it in a PortalRow, so we don't need
// to take care of that explicitly (though we could).
editContext.addNode(portletNode, parentNode, index);
return portletNode;
}
},
// We don't actually want to add anything via drag & drop ... that will be
// handled by PortalColumnBody
canAdd : function (type) {
return false;
},
// Keep track of our editContext and editNode even if editMode is not on yet
addedToEditContext : function (editContext, editNode) {
this.editContext = editContext;
this.editNode = editNode;
if (editNode.generatedType) delete editNode.generatedType;
},
setEditMode : function (editingOn, editContext, editNode) {
this.Super("setEditMode", arguments);
// We need to put the body in editMode, because the drag/drop behaviours belong there.
this.rowLayout.setEditMode(editingOn, editContext, null);
}
});
isc.PortalLayout.addProperties({
// We need to do some special things when we learn of our EditContext and EditNode
addedToEditContext : function (editContext, editNode) {
// Keep track of editContext and editNode even if editMode s not on yet
this.editContext = editContext;
this.editNode = editNode;
// We may need to add our PortalColumns to the EditContext, since they may have already been created.
for (var i = 0; i < this.getNumColumns(); i++) {
var column = this.getPortalColumn(i);
if (!column.editContext) {
// Create the editNode, supplying the liveObject
var node = editContext.makeEditNode({
type: this.columnConstructor,
liveObject: column,
defaults: {
ID: column.ID,
_constructor: this.columnConstructor
}
});
node.initData = node.defaults;
// Add it to the EditContext, without adding the liveObject to the parent, since it's
// already there.
editContext.addNode(node, editNode, i, null, true);
}
}
// And we should change our defaults to specify numColumns: 0, because otherwise we'll
// initialize the default 2 columns when restored, which isn't what will be wanted
editNode.defaults.numColumns = 0;
}
});
// Edit Mode impl for DynamicForm
// -------------------------------------------------------------------------------------------
if (isc.DynamicForm) {
isc.DynamicForm.addProperties({
setEditMode : function(editingOn, editContext, editNode) {
if (editingOn == null) editingOn = true;
if (editingOn == this.editingOn) return;
this.invokeSuper(isc.DynamicForm, "setEditMode", editingOn, editContext, editNode);
// Canvas level implementation already switched on ability to drop components.
// Add ability to drop items / add columns
if (this.editingOn) {
this.saveToOriginalValues(["canDropItems", "canAddColumns", "dropOut"]);
this.setProperties({
canDropItems: true,
canAddColumns: true,
dropOut: this.editModeDropOut
});
// Throw away anything the user might have typed in live mode
this.resetValues();
// Fix up the dropMargin, to prevent not-very-tall DFs from passing *every* drop
// through to parent layouts
var dropMargin = this.dropMargin;
if (dropMargin * 2 > this.getVisibleHeight() - 10) {
dropMargin = Math.round((this.getVisibleHeight() - 10) / 2);
if (dropMargin < 2) dropMargin = 2;
this.dropMargin = dropMargin;
}
} else {
// If we're coming out of edit mode, revert to whatever we saved
this.restoreFromOriginalValues(["canDropItems", "canAddColumns", "dropOut"]);
// Set default values from whatever the user typed into the formItems in edit mode
this.resetValues();
}
},
// editModeDropOver et al picked up by Canvas level setEditMode() implementation.
editModeDropOver : function () {
if (this.canDropItems != true) return false;
if (!this.editModeWillAcceptDrop()) return false;
this._lastDragOverItem = null;
// just to be safe
this.hideDragLine();
return isc.EH.STOP_BUBBLING;
},
dropMargin: 10,
editModeDropMove : function () {
if (!this.ns.EH.getDragTarget()) return false;
if (this.canDropItems != true) return false;
if (!this.editModeWillAcceptDrop()) return false;
// DataSource is a special case - we accept drop, but show no drag line
var item = this.ns.EH.getDragTarget().getDragData();
if (isc.isAn.Array(item)) item = item[0];
if (item != null && (item.type == "DataSource" || item.className == "DataSource")) {
this.hideDragLine();
return isc.EH.STOP_BUBBLING;
}
// If the form has no items, indicate insertion at the left of the form
if (this.getItems().length == 0) {
if (this.shouldPassDropThrough()) {
this.hideDragLine();
return;
}
isc.EditContext.hideAncestorDragDropLines(this);
this.showDragLineForForm();
return isc.EH.STOP_BUBBLING;
}
var event = this.ns.EH.lastEvent,
overItem = this.getItemAtPageOffset(event.x, event.y),
dropItem = this.getNearestItem(event.x, event.y);
//if (this._lastDragOverItem && this._lastDragOverItem != dropItem) {
// still over an item but not the same one
//}
// We only consider passing the drop through if the cursor is not over an actual item
if (overItem) {
isc.EditContext.hideAncestorDragDropLines(this);
this.showDragLineForItem(dropItem, event.x, event.y);
} else {
if (this.shouldPassDropThrough()) {
this.hideDragLine();
return;
}
if (dropItem) {
isc.EditContext.hideAncestorDragDropLines(this);
this.showDragLineForItem(dropItem, event.x, event.y);
} else {
this.hideDragLine();
}
}
this._lastDragOverItem = dropItem;
return isc.EH.STOP_BUBBLING;
},
editModeDropOut : function () {
this.hideDragLine();
return isc.EH.STOP_BUBBLING;
},
editModeDrop : function () {
// DataSource is a special case - it's the only non-visual property that users can drag
// and drop and a position within the form doesn't make sense
var dropItem = this.ns.EH.getDragTarget().getDragData();
if (isc.isAn.Array(dropItem)) dropItem = dropItem[0];
if ((dropItem && dropItem.className == "DataSource") ||
this.getItems().length == 0) // Empty form is also a special case
{
if (this.shouldPassDropThrough()) {
this.hideDragLine();
return;
}
this.itemDrop(this.ns.EH.getDragTarget(), 0, 0, 0);
return isc.EH.STOP_BUBBLING;
}
if (!this._lastDragOverItem) {
isc.logWarn("lastDragOverItem not set, cannot drop", "dragDrop");
return;
}
var item = this._lastDragOverItem,
dropOffsets = this.getItemTableOffsets(item),
side = item.dropSide,
index = item._dragItemIndex,
insertIndex = this.getItemDropIndex(item, side);
this._lastDragOverItem = null;
if (this.shouldPassDropThrough()) {
this.hideDragLine();
return;
}
if (insertIndex != null && insertIndex >= 0) {
if (this.parentElement) {
if (this.parentElement.hideDragLine) this.parentElement.hideDragLine();
if (this.parentElement.hideDropLine) this.parentElement.hideDropLine();
}
// Note that we cache a copy of _rowTable because the modifyFormOnDrop() method may
// end up invalidating the table layout, and thus clearing _rowTable in the middle of
// its processing
var rowTable = this.items._rowTable.duplicate();
this.modifyFormOnDrop(item, dropOffsets.top, dropOffsets.left, side, rowTable);
}
this.hideDragLine();
return isc.EH.STOP_BUBBLING;
},
itemDrop : function (item, itemIndex, rowNum, colNum, side, callback) {
var source = item.getDragData();
// If source is null, this is probably because we are drag-repositioning an existing
// item within a DynamicForm (or from one DF to another) - the source is the component
// itself
if (source == null) {
source = isc.EH.dragTarget;
if (isc.isA.FormItemProxyCanvas(source)) {
this.logInfo("The dragTarget is a FormItemProxyCanvas for " +
source.formItem, "editModeDragTarget");
source = source.formItem;
}
}
if (!item.isA("Palette")) {
if (isc.EditContext._dragHandle) isc.EditContext._dragHandle.hide();
var tree = this.editContext.data,
oldParent = tree.getParent(source.editNode),
oldIndex = tree.getChildren(oldParent).indexOf(source.editNode),
editNode = source.editNode;
if (isc.isA.Function(this.itemDropping)) {
editNode = this.itemDropping(editNode, itemIndex, true);
if (!editNode) return;
}
this.editContext.removeComponent(editNode);
// If we've moved the child component to a slot further down in the same parent,
// indices will now be off by one because we've just removed it from its old slot
if (oldParent == this.editNode && itemIndex > oldIndex) itemIndex--;
var node = this.editContext.addNode(source.editNode, this.editNode, itemIndex);
if (node && node.liveObject) {
isc.EditContext.delayCall("selectCanvasOrFormItem", [node.liveObject, true], 200);
}
return node;
} else {
// We're dealing with a drag of a new item from a component palette
var paletteNode = item.transferDragData();
if (isc.isAn.Array(paletteNode)) paletteNode = paletteNode[0];
// loadData() operates asynchronously, so we'll have to finish the item drop off-thread
if (paletteNode.loadData && !paletteNode.isLoaded) {
var thisForm = this;
paletteNode.loadData(paletteNode, function (loadedNode) {
loadedNode = loadedNode || paletteNode
loadedNode.isLoaded = true;
thisForm.completeItemDrop(loadedNode, itemIndex, rowNum, colNum, side, callback)
loadedNode.dropped = paletteNode.dropped;
});
return;
}
this.completeItemDrop(paletteNode, itemIndex, rowNum, colNum, side, callback)
}
},
completeItemDrop : function (paletteNode, itemIndex, rowNum, colNum, side, callback) {
var liveObject = paletteNode.liveObject,
canvasEditNode;
if (!isc.isA.FormItem(liveObject)) {
if (isc.isA.Button(liveObject) || isc.isAn.IButton(liveObject)) {
// Special case - Buttons become ButtonItems
paletteNode = this.editContext.makeEditNode({
type: "ButtonItem",
title: liveObject.title,
defaults : paletteNode.defaults
})
} else if (isc.isA.Canvas(liveObject)) {
canvasEditNode = paletteNode;
paletteNode = this.editContext.makeEditNode({type: "CanvasItem"});
isc.addProperties(paletteNode.initData, {
showTitle: false,
startRow: true,
endRow: true,
width: "*",
colSpan: "*"
});
}
}
paletteNode.dropped = true;
if (isc.isA.Function(this.itemDropping)) {
paletteNode = this.itemDropping(paletteNode, itemIndex, true);
if (!paletteNode) return;
}
var nodeAdded = this.editContext.addComponent(paletteNode, this.editNode, itemIndex);
if (nodeAdded) {
isc.EditContext.clearSchemaProperties(nodeAdded);
if (canvasEditNode) {
nodeAdded = this.editContext.addComponent(canvasEditNode, nodeAdded, 0);
// FIXME: Need a cleaner factoring here (see also Layout.dropItem())
if (isc.isA.TabSet(liveObject)) {
liveObject.delayCall("showAddTabEditor", [], 1000);
}
}
// If we've just dropped a palette node that contained a reference to a dataSource,
// do a forced set of that dataSource on the liveObject. This will take it through
// any special editMode steps - for example, it will cause a DynamicForm to have a
// set of fields generated for it and added to the project tree
if (nodeAdded.liveObject.dataSource) {
//this.logWarn("calling setDataSource on: " + nodeAdded.liveObject);
nodeAdded.liveObject.setDataSource(nodeAdded.liveObject.dataSource, null, true);
}
isc.EditContext.delayCall("selectCanvasOrFormItem", [paletteNode.liveObject, true], 200);
if (nodeAdded.showTitle != false) {
paletteNode.liveObject.delayCall("editClick");
}
}
if (callback) this.fireCallback(callback, "node", [nodeAdded]);
},
// Modifies the form to accommodate the pending drop by adding columns and/or SpacerItems as
// necessary, then performs the actual drop
modifyFormOnDrop : function (item, rowNum, colNum, side, rowTable) {
if (this.canAddColumns == false) return;
var dropItem = this.ns.EH.getDragTarget().getDragData(),
dropItemCols,
draggingFromRow,
draggingFromIndex,
_this = this;
if (!dropItem) {
// We're drag-positioning an existing item
dropItem = this.ns.EH.getDragTarget();
if (!isc.isA.FormItemProxyCanvas(dropItem)) {
this.logWarn("In modifyFormOnDrop the drag target was not a FormItemProxyCanvas");
return;
}
dropItem = dropItem.formItem;
var lastIndex = -1;
// If the item we're dragging is in this form, note its location so that we can clean
// up where it came from
for (var i = 0; i < rowTable.length; i++) {
for (var j = 0; j < rowTable[i].length; j++) {
if (rowTable[i][j] == lastIndex) continue;
lastIndex = rowTable[i][j];
if (this.items[lastIndex] == dropItem) {
draggingFromRow = i;
draggingFromIndex = lastIndex;
break;
}
}
}
var dragPositioning = true;
} else {
// Manually create a FormItem using the config that will be used to create the real
// object. We need to do this because we need to know things about that object that
// can only be easily discovered by creating and then inspecting it - eg, colSpan,
// title attributes and whether startRow or endRow are set
if (isc.isAn.Array(dropItem)) dropItem = dropItem[0];
var type = dropItem.type || dropItem.className;
var theClass = isc.ClassFactory.getClass(type);
if (isc.isA.FormItem(theClass)) {
dropItem = this.createItem(dropItem, type);
} else {
// This is not completely accurate, but it gives us enough info for placement and
// column occupancy calculation. dropItem() differentiates between Buttons and
// other types of Canvas, but for our purposes here it's enough to know that non-
// FormItem items will occupy one cell and don't have endRow/startRow set
dropItem = this.createItem({type: "CanvasItem", showTitle: false}, "CanvasItem");
}
var dragPositioning = false;
}
dropItemCols = this.getAdjustedColSpan(dropItem);
// If we've previously set startRow or endRow on the item we're dropping, clear them
if ((dropItem.startRow && dropItem._startRowSetByBuilder) ||
(dropItem.endRow && dropItem._endRowSetByBuilder)) {
dropItem.editContext.setNodeProperties(dropItem.editNode, {
startRow: null,
_startRowSetByBuilder: null,
endRow: null,
_endRowSetByBuilder: null
});
}
// If we're in drag-reposition mode and the rowNum we're dropping on is not the row we're
// dragging from, we could end up with a situation where a row contains nothing but spacers.
// Detect when this situation is about to arise and mark the spacers for later deletion
var spacersToDelete = [];
if (dragPositioning && draggingFromRow) {
var fromRow = rowTable[draggingFromRow],
lastIndex = -1;
for (var i = 0; i < fromRow.length; i++) {
if (fromRow[i] != lastIndex) {
lastIndex = fromRow[i];
if (this.items[lastIndex] == dropItem) continue;
if (isc.isA.SpacerItem(this.items[lastIndex]) &&
this.items[lastIndex]._generatedByBuilder)
{
this.logDebug("Marking spacer " + this.items[lastIndex].name + " for removal",
"formItemDragDrop");
spacersToDelete.add(this.items[lastIndex]);
continue;
}
this.logDebug("Found a non-spacer item on row " + draggingFromRow +
", no spacers will be deleted", "formItemDragDrop");
spacersToDelete = null;
break;
}
}
}
var delta = 0;
if (side == "L" || side == "R") {
var addColumns = true;
// If the item is flagged startRow: true, we don't need to add columns
if (dropItem.startRow) addColumns = false;
// If the item is flagged endRow: true and we're not dropping in the rightmost
// column, we don't need to add columns (NOTE: this isn't strictly true, we need
// to revisit this to cope with the case of an item with a larger colSpan than
// the number of columns remaining to the right)
if (dropItem.endRow && (side == "L" || colNum < rowTable[rowNum].length)) {
addColumns = false;
}
// If we're repositioning an item and it came from this row in this form, we don't
// need to add columns
if (dragPositioning && draggingFromRow == rowNum) addColumns = false;
// Need to add column(s) and move the existing items around accordingly
if (addColumns) {
var cols = dropItemCols;
// If we're dropping onto a SpacerItem that we created in the first place, we only
// need to add columns if the colSpan of the dropped item is greater than the
// colSpan of the spacer (FIXME: and any adjacent spacers)
var insertIndex = rowTable[rowNum][colNum];
//if (side == "R") insertIndex++;
if (rowTable[rowNum].contains(insertIndex)) {
var existingItem = this.items[insertIndex];
// If the item being dropped upon is not a spacer, check the item immediately
// adjacent on the side of the drop
if (!isc.isA.SpacerItem(existingItem) || !existingItem._generatedByBuilder) {
insertIndex += side =="L" ? -1 : 1;
existingItem = this.items[insertIndex];
}
if (rowTable[rowNum].contains(insertIndex)) {
if (isc.isA.SpacerItem(existingItem) && existingItem._generatedByBuilder) {
if (existingItem.colSpan && existingItem.colSpan > cols) {
existingItem.editContext.setNodeProperties(existingItem.editNode,
{colSpan: existingItem.colSpan - cols});
cols = 0;
} else {
cols -= existingItem.colSpan;
existingItem.editContext.removeComponent(existingItem.editNode);
if (side == "R") delta = -1;
}
}
}
}
if (cols <= 0) {
addColumns = false;
// If we get this far, we are going to insert "dropItemCols" columns to the form.
// It may be that the form is already wide enough to accommodate those columns in
// this particular row (the grid has a ragged right edge because we use endRow and
// startRow to control row breaking rather than unnecessary spacers)
} else if (rowTable[rowNum].length + dropItemCols <= this.numCols) {
addColumns = false;
} else {
// Otherwise widen the entire form
this.editContext.setNodeProperties(this.editNode, {numCols: this.numCols + cols});
}
}
// We're inserting a whole new column to the "grid" that the user sees. This may not
// be the desired action - maybe the user just wanted to insert an extra cell in this
// row? Leaving as is for now - prompting the user would make this and everything
// downstream of it asynchronous
for (var i = 0; i < rowTable.length; i++) {
var insertIndex = rowTable[i][colNum];
if (insertIndex == null) insertIndex = this.items.length;
else insertIndex += delta + (side == "L" ? 0 : 1);
if (i != rowNum) {
if (!addColumns) continue;
// If we're dragging an item to a row higher up the form, we'll have stepped the
// delta forward when we inserted the dragged item; when we reach the row it
// used to be on, we need to retard the delta by one to get the insert index
// back in line
if (dragPositioning && draggingFromRow &&
rowNum < draggingFromRow && i == draggingFromRow)
{
delta--;
}
// If spacersToDelete contains anything, we detected up front that this drop-
// reposition will leave the from row empty of everything except spacer items
// that we added in the first place. Those spacers are marked for deletion at
// the end of this process; we certainly don't want to add any more!
if (spacersToDelete && spacersToDelete.length > 0 && i == draggingFromRow) {
continue;
}
// Look to see if the new column is to the right of an item with endRow: true,
// because in that circumstance the spacer will break the layout
if (insertIndex > 0) {
var existingItem = this.items[insertIndex - 1];
if (!existingItem || existingItem == dropItem || existingItem.endRow) {
continue;
}
}
// If the column just added is the rightmost one, we should retain form
// coherence by marking the right-hand item on each row as endRow: true instead
// of creating unnecessary spacers
var existingItemCols = this.getAdjustedColSpan(existingItem);
if (side == "R" && colNum + existingItemCols >= rowTable[i].length) {
if (!existingItem.endRow) {
existingItem.editContext.setNodeProperties(existingItem.editNode,
{endRow: true, _endRowSetByBuilder: true});
}
continue;
}
var paletteNode = this.editContext.makeEditNode({type: "SpacerItem"});
isc.addProperties(paletteNode.initData, {
colSpan: cols,
height: 0,
_generatedByBuilder: true
});
var nodeAdded = this.editContext.addComponent(paletteNode, this.editNode,
insertIndex);
// Keep track of how many new items we've added to the form, because we need
// to step the insert point on for any later adds
delta++;
} else {
if (side == "L") {
// We're dropping to the left of an item, so we know there is an item to
// our right. If it specifies startRow, clear that out
var existingItem = this.items[insertIndex];
if (existingItem && existingItem.startRow && existingItem._startRowSetByBuilder) {
existingItem.editContext.setNodeProperties(existingItem.editNode,
{startRow: null, _startRowSetByBuilder: null});
}
} else {
// We're dropping to the right of an item, so we know there is an item to
// our left. If it specifies endRow, clear that out
var existingItem = this.items[insertIndex - 1];
if (existingItem && existingItem.endRow && existingItem._endRowSetByBuilder) {
existingItem.editContext.setNodeProperties(existingItem.editNode,
{endRow: null, _endRowSetByBuilder: null});
}
}
this.itemDrop(this.ns.EH.getDragTarget(), insertIndex, i, colNum, side,
function (node) {
_this._nodeToSelect = node;
});
if (draggingFromRow == null || rowNum < draggingFromRow) delta++;
}
}
} else { // side was "T" or "B"
var row,
currentItemIndex;
// We don't want to drop "above" or "below" a spacer we put in place; we want to
// replace it
if (isc.isA.SpacerItem(item) && item._generatedByBuilder) {
row = rowNum;
} else {
row = rowNum + (side == "B" ? 1 : 0);
}
if (rowTable[row]) currentItemIndex = rowTable[row][colNum];
var rowStartIndex;
if (row >= rowTable.length) rowStartIndex = this.items.length;
else rowStartIndex = rowTable[row][0];
var currentItem = currentItemIndex == null ? null : this.items[currentItemIndex];
if (currentItem == null ||
(isc.isA.SpacerItem(currentItem) && currentItem._generatedByBuilder)) {
if (row > rowTable.length - 1 || row < 0) {
// Dropping past the end or before the beginning of the form - in both cases
// rowStartIndex will already have been set correctly, so we can just go
// ahead and add the component, plus any spacers we need
if (colNum != 0 && !dropItem.startRow) {
var paletteNode = this.editContext.makeEditNode({type: "SpacerItem"});
isc.addProperties(paletteNode.initData, {
colSpan: colNum,
height: 0,
_generatedByBuilder : true
});
this.editContext.addComponent(paletteNode, this.editNode, rowStartIndex);
}
this.itemDrop(this.ns.EH.getDragTarget(),
rowStartIndex + (colNum != 0 ? 1 : 0), row, colNum, side,
function (node) {
_this._nodeToSelect = node;
});
// We have just created an empty line for this item, so we know for sure that
// it is the only item on the line (except for any spacers we created).
// Therefore, we mark it endRow: true
} else if (currentItem == null) {
// This can only happen if we're dropping on an existing row to the right of
// a component that specifies endRow: true, or where the first item in the
// next row specifies startRow: true. If the reason is a trailing startRow,
// that's fine and we don't need to do anything special. If the reason is a
// leading endRow, that presents a problem. For now, we assume that the
// endRow was set by VB, and just change it to suit ourselves. This will
// change so that we look to see whether the startRow/endRow attr was set by
// VB or the user. If it was set by VB, we just can it as now; if it was set
// by the user we attempt to honor that by inserting a whole new row and
// padding on the left, such that the item is dropped immediately above or
// below the item hilited by the dropline, and the item that specified endRow
// remains as the last item in its row.
var leftCol = rowTable[row].length - 1;
if (leftCol < 0) {
isc.logWarn("Found completely empty row in DynamicForm at position (" +
row + "," + (colNum) + ")");
return;
}
var existingItemIndex = rowTable[row][leftCol];
var existingItem = this.items[existingItemIndex];
if (existingItem == null) {
isc.logWarn("Null item in DynamicForm at position (" + row + "," + (colNum-1) + ")");
return;
}
// Special case - don't remove the endRow flag from the existing item if the
// existing item is also the item we're dropping (as would be the case if the
// if the user piacks up a field and drops it further to the right in the
// same column)
if (existingItem.endRow && existingItem != dropItem) {
existingItem.editContext.setNodeProperties(existingItem.editNode, {endRow: false});
}
var padding = (colNum - leftCol) - 1;
// Special case - the item to our left is actually the item we're dropping,
// so we need to replace it with a spacer or the drop won't appear to have
// have had any effect
if (dragPositioning && existingItem == dropItem) {
padding += dropItemCols;
}
if (padding > 0) {
var paletteNode = this.editContext.makeEditNode({type: "SpacerItem"});
isc.addProperties(paletteNode.initData, {
colSpan: padding,
height: 0,
_generatedByBuilder: true
});
this.editContext.addComponent(paletteNode, this.editNode, existingItemIndex + 1);
}
this.itemDrop(this.ns.EH.getDragTarget(),
existingItemIndex + (padding > 0 ? 2 : 1), row, colNum, side,
function (node) {
_this._nodeToSelect = node;
});
} else {
// Where the user wants to drop there is currently a SpacerItem that we created
// to maintain form coherence. So we do the following:
// - If the item being dropped is narrower than the spacer, we adjust the
// spacer's colSpan accordingly and drop the item in before it
// - If the item and the spacer are the same width, we remove the spacer and
// insert the item in its old position
// - If the item is wider than the spacer then for now we just replace the
// spacer with the item, like we would if they were the same width. This
// may well cause the form to reflow in an ugly way. To fix this, we will
// change this code to look for other spacers in the target row, and
// attempt to remove them to make space for the item; if all else fails, we
// must add columns to the form and fix up as required to ensure that we
// don't get any reflows that break the form's coherence
var oldColSpan = currentItem.colSpan ? currentItem.colSpan : 1,
newColSpan = dropItemCols;
if (oldColSpan > newColSpan) {
currentItem.editContext.setNodeProperties(currentItem.editNode,
{colSpan: oldColSpan - newColSpan});
this.itemDrop(this.ns.EH.getDragTarget(), currentItemIndex, row,
colNum, side,
function (node) {
_this._nodeToSelect = node;
});
} else {
this.itemDrop(this.ns.EH.getDragTarget(), currentItemIndex, row,
colNum, side,
function (node) {
_this._nodeToSelect = node;
});
currentItem.editContext.removeComponent(currentItem.editNode);
}
}
} else {
// Something is in the way. We could either insert an entire new row or just push
// the contents of this one column down a row. Both of these seem like valid use
// cases; for now, we're just going with inserting a whole new row
if (colNum != 0) {
var paletteNode = this.editContext.makeEditNode({type: "SpacerItem"});
isc.addProperties(paletteNode.initData, {
colSpan: colNum,
height: 0,
_generatedByBuilder : true
});
this.editContext.addComponent(paletteNode, this.editNode, rowStartIndex);
}
this.itemDrop(this.ns.EH.getDragTarget(), rowStartIndex + (colNum == 0 ? 0 : 1),
row, colNum, side, function (node) {
if (node && node.liveObject && node.liveObject.editContext) {
node.liveObject.editContext.setNodeProperties(node,
{endRow: true, _endRowSetByBuilder: true});
}
_this._nodeToSelect = node;
});
}
}
if (dragPositioning && spacersToDelete) {
for (var i = 0; i < spacersToDelete.length; i++) {
this.logDebug("Removing spacer item " + spacersToDelete[i].name, "formItemDragDrop");
spacersToDelete[i].editContext.removeComponent(spacersToDelete[i].editNode);
}
}
if (!dragPositioning) dropItem.destroy();
if (this._nodeToSelect && this._nodeToSelect.liveObject) {
isc.EditContext.delayCall("selectCanvasOrFormItem(", [this._nodeToSelect.liveObject], 200);
}
},
getAdjustedColSpan : function(item) {
if (!item) return 0;
var cols = item.colSpan != null ? item.colSpan : 1;
// colSpan of "*" makes no sense for the purposes of this calculation, which is trying to
// work out how many columns an item we're dropping needs to take up. So we'll call it 1.
if (cols == "*") cols = 1;
if (item.showTitle != false && (item.titleOrientation == "left" ||
item.titleOrientation == "right" ||
item.titleOrientation == null))
{
cols++
}
return cols;
},
// Override of Canvas.canAdd - DynamicForm will accept a drop of a Canvas in addition to the
// FormItems advertised in its schema
canAdd : function (type) {
if (this.getObjectField(type) != null) return true;
var classObject = isc.ClassFactory.getClass(type);
if (classObject && classObject.isA("Canvas")) return true;
return false;
},
setEditorType : function (item, editorType) {
if (!item.editContext) return;
var tree = item.editContext.data,
parent = tree.getParent(item.editNode),
index = tree.getChildren(parent).indexOf(item.editNode),
ctx = item.editContext,
paletteNode = { className: editorType, defaults: item.editNode.defaults },
editNode = ctx.makeEditNode(paletteNode);
ctx.removeComponent(item.editNode);
ctx.addComponent(editNode, parent, index);
},
// This undocumented method is called from DF.itemDrop() just before the editNode is
// inserted into the editContext. This function should return the editNode to actually
// insert - either the passed node if no change is required, or some new value. Note that
// the "isAdded" parameter will be false if the item was dropped after being dragged from
// elsewhere, as opposed to a drop of a new item from a component palette
itemDropping : function (editNode, insertIndex, isAdded) {
var item = editNode.liveObject,
schemaInfo = isc.EditContext.getSchemaInfo(editNode);
// Case 0: there is no schema information to compare, so nothing to do
if (!schemaInfo.dataSource) return editNode;
// Case 1: this is an unbound (so presumably empty) form. Bind it to the top-level
// schema associated with this item
if (!this.dataSource) {
this.setDataSource(schemaInfo.dataSource);
this.serviceNamespace = schemaInfo.serviceNamespace;
this.serviceName = schemaInfo.serviceName;
return editNode;
}
// Case 2: this form is already bound to the top-level schema associated with this item,
// so we don't need to do anything
if (schemaInfo.dataSource == isc.DataSource.getDataSource(this.dataSource).ID &&
schemaInfo.serviceNamespace == this.serviceNamespace &&
schemaInfo.serviceName == this.serviceName) {
return editNode;
}
// Case 3: this form is already bound to some other schema. We need to wrap this item
// in its own sub-form
var canvasItemNode = this.editContext.makeEditNode({
className: "CanvasItem",
defaults: {
cellStyle: "nestedFormContainer"
}
});
isc.addProperties(canvasItemNode.initData, {showTitle: false, colSpan: 2});
canvasItemNode.dropped = true;
this.editContext.addComponent(canvasItemNode, this.editNode, insertIndex);
var dfNode = this.editContext.makeEditNode({
className: "DynamicForm",
defaults: {
numCols: 2,
canDropItems: false,
dataSource: schemaInfo.dataSource,
serviceNamespace: schemaInfo.serviceNamespace,
serviceName: schemaInfo.serviceName,
doNotUseDefaultBinding: true
}
});
dfNode.dropped = true;
this.editContext.addComponent(dfNode, canvasItemNode, 0);
var nodeAdded = this.editContext.addComponent(editNode, dfNode, 0);
isc.EditContext.clearSchemaProperties(nodeAdded);
},
getFieldEditNode : function (field, dataSource) {
var editorType = this.getEditorType(field);
editorType = editorType.substring(0,1).toUpperCase() + editorType.substring(1) + "Item";
var editNode = {
type: editorType,
autoGen: true,
defaults: {
name: field.name,
title: field.title || dataSource.getAutoTitle(field.name)
}
}
return editNode;
}
});
// Edit Mode extras for FormItem and its children
// -------------------------------------------------------------------------------------------
isc.FormItem.addMethods({
// Note: this impl contains code duplicated from Canvas.setEditMode because FormItem does not
// extend Canvas.
setEditMode : function(editingOn, editContext, editNode) {
if (editingOn == null) editingOn = true;
if (this.editingOn == editingOn) return;
this.editingOn = editingOn;
if (this.editingOn) {
this.editContext = editContext;
}
this.editNode = editNode;
// If we're going into edit mode, re-route various methods
if (this.editingOn) {
this.saveToOriginalValues(["click", "doubleClick", "changed"]);
this.setProperties({
click: this.editModeClick,
doubleClick: this.editModeDoubleClick,
changed: this.editModeChanged
});
} else {
this.restoreFromOriginalValues(["click", "doubleClick", "changed"]);
}
},
editModeChanged : function (form, item, value) {
this.editContext.setNodeProperties(this.editNode, {defaultValue: value});
},
setEditorType : function (editorType) {
if (this.form) this.form.setEditorType(this, editorType);
}
});
isc.ButtonItem.addMethods({
editClick : function () {
var left = this.canvas.getPageLeft(),
width = this.canvas.getVisibleWidth(),
top = this.canvas.getPageTop(),
height = this.canvas.getHeight();
isc.EditContext.manageTitleEditor(this, left, width, top, height);
}
});
}
// Edit Mode impl for SectionStack
// -------------------------------------------------------------------------------------------
isc.SectionStack.addMethods({
canAdd : function (type) {
// SectionStack is a special case for DnD - although it is a VLayout, its schema marks
// children, peers and members as inapplicable. However, anything can be put into a
// SectionStackSection. Therefore, we accept drop of any canvas, and handle adding it
// to the appropriate section in the drop method. We also accept a drop of a FormItem;
// this will be detected downstream and handled by wrapping the FormItem inside an auto-
// created DynamicForm
if (type == "SectionStackSection") return true;
var classObject = isc.ClassFactory.getClass(type);
if (classObject && classObject.isA("Canvas")) return true;
if (classObject && classObject.isA("FormItem")) return true;
return false;
},
// Return the modified editNode (or a completely different one); return false to abandon
// the drop
modifyEditNode : function (paletteNode, newEditNode, dropType) {
if (dropType == "SectionStackSection") return newEditNode;
var dropPosition = this.getDropPosition();
if (dropPosition == 0) {
isc.warn("Cannot drop before the first section header");
return false;
}
var headers = this._getHeaderPositions();
for (var i = headers.length-1; i >= 0; i--) {
if (dropPosition > headers[i]) {
// Return the edit node off the section header
return this.getSectionHeader(i).editNode;
}
}
// Shouldn't ever get here
return newEditNode;
},
// getEditModeDropPosition() - explicitly called from getDropPosition if the user isn't doing
// a drag reorder of sections.
getEditModeDropPosition : function (dropType) {
var pos = this.invokeSuper(isc.SectionStack, "getDropPosition");
if (!dropType || dropType == "SectionStackSection") {
return pos;
}
var headers = this._getHeaderPositions();
for (var i = headers.length-1; i >= 0; i--) {
if (pos > headers[i]) {
return pos - headers[i] - 1;
}
}
return 0;
},
_getHeaderPositions : function () {
var headers = [],
j = 0;
for (var i = 0; i < this.getMembers().length; i++) {
if (this.getMember(i).isA(this.sectionHeaderClass)) {
headers[j++] = i;
}
}
return headers;
}
});
// Edit Mode impl for ListGrid
// -------------------------------------------------------------------------------------------
if (isc.ListGrid != null) {
isc.ListGrid.addMethods({
setEditMode : function(editingOn, editContext, editNode) {
if (editingOn == null) editingOn = true;
if (editingOn == this.editingOn) return;
this.invokeSuper(isc.ListGrid, "setEditMode", editingOn, editContext, editNode);
if (this.editingOn) {
this.saveToOriginalValues(["setNoDropIndicator", "clearNoDropIndicator",
"headerClick"]);
this.setProperties({
setNoDropIndicator: this.editModeSetNoDropIndicator,
clearNoDropIndicator: this.editModeClearNoDropIndicator,
headerClick: this.editModeHeaderClick
});
} else {
// If we're coming out of edit mode, revert to whatever we saved
this.restoreFromOriginalValues(["setNoDropIndicator", "clearNoDropIndicator",
"headerClick"]);
}
},
// Canvas.clearNoDropindicator no-ops if the internal _noDropIndicator flag is null. This
// isn't good enough in edit mode because a canvas can be dragged over whilst the no-drop
// cursor is showing, and we want to revert to a droppable cursor regardless of whether
// _noDropIndicatorSet has been set on this particular canvas.
editModeClearNoDropIndicator : function (type) {
this.Super("clearNoDropIndicator", arguments);
this.body.editModeClearNoDropIndicator();
},
// Special editMode version of setNoDropCursor - again, because the base version no-ops in
// circumstances where we need it to refresh the cursor.
editModeSetNoDropIndicator : function () {
this.Super("setNoDropIndicator", arguments);
this.body.editModeSetNoDropIndicator();
},
editModeHeaderClick : function (fieldNum) {
// Select the corresponding ListGridField
var tree = this.editContext.data,
children = tree.getChildren(tree.findById(this.ID)),
node = children[fieldNum];
node.liveObject._visualProxy = this.header.getButton(fieldNum);
isc.EditContext.selectCanvasOrFormItem(node.liveObject);
this._headerClickFired = true;
return isc.EH.STOP_BUBBLING;
},
// HACK: We ideally want a header click to stop event bubbling at that point, but it seems
// that returning STOP_BUBBLING from the headerClick() method does not prevent the ListGrid's
// click event from firing, so the object selection is superseded. To work around this, we
// maintain a flag on the LG that headerClick has been fired, which this click() impl tests
// and then clears
editModeClick : function () {
if (this.editNode) {
if (this._headerClickFired) delete this._headerClickFired;
else isc.EditContext.selectCanvasOrFormItem(this, true);
return isc.EH.STOP_BUBBLING;
}
}
});
}
// Edit Mode impl for TreeGrid
// -------------------------------------------------------------------------------------------
if (isc.TreeGrid != null) {
isc.TreeGrid.addMethods({
setEditMode : function(editingOn, editContext, editNode) {
if (editingOn == null) editingOn = true;
if (editingOn == this.editingOn) return;
this.invokeSuper(isc.TreeGrid, "setEditMode", editingOn, editContext, editNode);
if (this.editingOn) {
// If the TG was not databound and had an empty fieldset at initWidget time, it
// will have created a default treeField which now appears in its fields property
// as if it were put there by user code. We need to detect this circumstance and
// create a TreeGridField node in the projectComponents tree so the user can
// manipulate this auto-generated field
this.editModeCreateDefaultTreeFieldEditNode();
this.saveToOriginalValues(["addField"]);
this.setProperties({
addField: this.editModeAddField
});
} else {
// If we're coming out of edit mode, revert to whatever we saved
this.restoreFromOriginalValues(["addField"]);
}
},
editModeCreateDefaultTreeFieldEditNode : function () {
// If we're loading a view, the default nodeTitle is going to be destroyed before the
// user sees it, so just bail
if (isc._loadingNodeTree) return;
// If this TG is databound, we presumably haven't created a default nodeTitle; this
// being the case, let's bail now so that we don't remove a real user field just
// because it happens to be called "nodeTitle"
if (this.dataSource) return;
var fields = this.fields;
for (var i = 0; i < fields.length; i++) {
if (fields[i].name == "nodeTitle") {
var config = {
type: "TreeGridField",
autoGen: true,
defaults: {
name: fields[i].name,
title: fields[i].title
}
};
var editNode = this.editContext.makeEditNode(config);
this.editContext.addNode(editNode, this.editNode, null, null, true);
return;
}
}
},
// Overriding the DBC implementation because we need to treat the field being added as a
// special case if it has treeField set - there can only be one treeField, so we must
// remove the extant one. This could only really happen during Load View (unless we were
// to change the default for treeField to true in the component palette), so we will just
// hand the call on if we're not in loading mode
editModeAddField : function (field, index) {
this.Super("addField", arguments);
if (isc._loadingNodeTree) {
if (field.treeField) {
var fields = this.getFields();
for (var i = 0; i < fields.length; i++) {
if (fields[i].name != field.name && fields[i].treeField) {
this.removeField(fields[i]);
break;
}
}
}
}
},
// TreeGrid needs a special implementation of this method because binding a TreeGrid really
// means binding the one field in the DataSource that represents the tree; with other DBC's,
// we bind all the visible fields
editModeSetDataSource : function (dataSource, fields, forceRebind) {
//this.logWarn("editMode setDataSource called" + isc.Log.getStackTrace());
// _loadingNodeTree is a flag set by Visual Builder - its presence indicates that we are
// loading a view from disk. In this case, we do NOT want to perform the special
// processing in this function, otherwise we'll end up with duplicate components in the
// componentTree.
// However, TreeGrid needs special treatment because it auto-creates a treeField if it
// is not passed a list of fields to use. Since we'll be adding the fields one at a
// time during View Load, we start out with no fields, so a default will be created.
//
if (isc._loadingNodeTree) {
this.baseSetDataSource(dataSource, fields);
return;
}
if (dataSource == null) return;
if (dataSource == this.dataSource && !forceRebind) return;
var fields = this.getFields();
// remove just the field currently marked treeField: true - in many use cases, this
// will be the only field in the TreeGrid anyway
if (fields) {
for (var i = 0; i < fields.length; i++) {
var field = fields[i];
if (field.treeField) {
field.treeField = null;
var nodeToRemove = field.editNode;
break;
}
}
}
var existingFields = this.getFields();
existingFields.remove(field);
// If this dataSource has a single complex field, use the schema of that field in lieu
// of the schema that was dropped.
var schema,
fields = dataSource.fields;
if (fields && isc.getKeys(fields).length == 1 &&
dataSource.fieldIsComplexType(fields[isc.firstKey(fields)].name))
{
schema = dataSource.getSchema(fields[isc.firstKey(fields)].type);
} else {
schema = dataSource;
}
// add one editNode for the single field in the DataSource that is named as the
// "titleField"; if there is no such field, just use the first
var fields = schema.getFields(),
titleFieldName = dataSource.titleField;
if (!isc.isAn.Array(fields)) fields = isc.getValues(fields);
for (var ix = 0; ix < fields.length; ix++) {
if (!this.shouldUseField(fields[ix], dataSource)) continue;
if (titleFieldName == null || titleFieldName == fields[ix].name) {
var titleField = fields[ix];
break;
}
}
if (titleField) existingFields.addAt(titleField, 0);
this.baseSetDataSource(dataSource, existingFields);
var fieldConfig = this.getFieldEditNode(titleField, schema);
fieldConfig.defaults.treeField = true;
var editNode = this.editContext.makeEditNode(fieldConfig);
this.editContext.addNode(editNode, this.editNode, 0, null);
// Deferred node removal to here as it avoids leaving the TG with an empty fieldset,
// because this situation triggers the creation of a default treeField in various
// places in the TG code
if (nodeToRemove) this.editContext.removeNode(nodeToRemove, true);
//this.logWarn("editMode setDataSource done adding fields");
}
});
}
// Edit Mode impl for ServiceOperation and ValuesMap. Both of these are non-visual classes
// that can nevertheless appear in a VB app - kind of like DataSources, but they're added to
// the project as a side effect of adding a web service binding.
// -------------------------------------------------------------------------------------------
var basicSetEditMode = function (editingOn, editContext, editNode) {
if (editingOn == null) editingOn = true;
if (this.editingOn == editingOn) return;
this.editingOn = editingOn;
if (this.editingOn) this.editContext = editContext;
this.editNode = editNode;
}
isc.ServiceOperation.addMethods({
setEditMode : basicSetEditMode,
getActionTargetTitle : function () {
return "Operation: [" + this.operationName + "]";
}
});
if (isc.ValuesManager != null) {
isc.ValuesManager.addMethods({
setEditMode : basicSetEditMode
});
}
// EditContext
// --------------------------------------------------------------------------------------------
//> @interface EditContext
// An EditContext provides an editing environment for a set of components.
//
// An EditContext is typically populated by adding a series of EditNodes created via a
// +link{Palette}, either via drag and drop creation, or when loading from a saved version,
// via +link{addFromPaletteNode}.
//
// An EditContext then provides interfaces for further editing of the components represented
// by EditNodes.
//
// @group devTools
// @visibility devTools
//<
isc.ClassFactory.defineInterface("EditContext");
// EditNode
// ---------------------------------------------------------------------------------------
//> @object EditNode
// An object representing a component that is currently being edited within an
// +link{EditContext}.
//
// An EditNode is essentially a copy of a +link{PaletteNode}, initially with the same properties
// as the PaletteNode from which it was generated. However unlike a PaletteNode, an EditNode
// always has a +link{editNode.liveObject,liveObject} - the object created from the
// +link{paletteNode.defaults} or other properties defined on a paletteNode.
//
// Like a Palette, an EditContext may use properties such as +link{paletteNode.icon} or
// +link{paletteNode.title} to display EditNodes.
//
// An EditContext generally offers some means of editing EditNodes and, as edits are made,
// updates +link{editNode.defaults} with the information required to re-create the component.
//
// @inheritsFrom PaletteNode
// @treeLocation Client Reference/Tools
// @visibility devTools
//<
//> @attr editNode.defaults (Properties : null : IR)
// Properties required to recreate the current +link{editNode.liveObject}.
// @visibility devTools
//<
//> @attr editNode.type (SCClassName : null : IR)
// +link{SCClassName} of the +link{liveObject}, for example, "ListGrid".
// @visibility devTools
//<
//> @attr editNode.liveObject (Object : null : IR)
// Live version of the object created from the +link{editNode.defaults}. For example,
// if +link{editNode.type} is "ListGrid", liveObject
would be a ListGrid.
// @visibility devTools
//<
//> @attr editNode.editDataSource (DataSource : null : IR)
// DataSource to use when editing the properties of this component. Defaults to
// +link{editContext.dataSource}, or the DataSource named after the component's type.
//
// @visibility internal
//<
//> @attr EditContext.editDataSource (DataSource : null : IR)
// Default DataSource to use when editing any component in this context. Defaults to the
// DataSource named after the component's type. Can be overridden per-component via
// +link{editedItem.editDataSource}.
//
// @group devTools
//<
isc.EditContext.addClassProperties({
_dragHandleHeight: 18,
_dragHandleWidth: 18,
_dragHandleXOffset: -18,
_dragHandleYOffset: 0
});
isc.EditContext.addClassMethods({
// Title Editing (for various components: buttons, tabs, etc)
// ---------------------------------------------------------------------------------------
manageTitleEditor : function (targetComponent, left, width, top, height) {
if (!isc.isA.DynamicForm(this.titleEditor)) {
this.titleEditor = isc.DynamicForm.create({
autoDraw: false,
margin: 0, padding: 0, cellPadding: 0,
fields: [
{
name: "title", type: "text",
showTitle: false,
keyPress : function (item, form, keyName) {
if (keyName == "Escape") {
form.discardUpdate = true;
form.hide();
return
}
if (keyName == "Enter") item.blurItem();
},
blur : function (form, item) {
// WWW this.logWarn("Blurring...");
//this.logWarn(this.getStackTrace());
if (!form.discardUpdate) {
var widget = form.targetComponent,
ctx = widget.editContext;
if (ctx) {
ctx.setNodeProperties(widget.editNode,
{"title" : item.getValue()});
ctx.nodeClick(ctx, widget.editNode);
}
}
form.hide();
}
}
]
});
}
var editor = this.titleEditor;
editor.setProperties({targetComponent: targetComponent});
editor.discardUpdate = false;
var item = editor.getItem("title");
var title = targetComponent.title;
if (!title) {
title = targetComponent.name;
}
item.setValue(title);
this.positionTitleEditor(targetComponent, left, width, top, height);
editor.show();
item.focusInItem();
item.delayCall("selectValue", [], 100);
// WWW this.logWarn("Showing editor...");
},
positionTitleEditor : function (targetComponent, left, width, top, height) {
if (top == null) top = targetComponent.getPageTop();
if (height == null) height = targetComponent.height;
if (left == null) left = targetComponent.getPageLeft();
if (width == null) width = targetComponent.getVisibleWidth();
var editor = this.titleEditor;
var item = editor.getItem("title");
item.setHeight(height);
item.setWidth(width);
editor.setTop(top);
editor.setLeft(left);
},
// Selection and Dragging of EditNodes
// ---------------------------------------------------------------------------------------
deselect : function () {
isc.SelectionOutline.deselect();
this.hideDragHandle();
},
setEditMode : function (editingOn) {
var selectedComponent = isc.SelectionOutline.getSelectedObject();
if (selectedComponent == null) return;
if (editingOn) {
this.setupDragProperties(selectedComponent);
this.showSelectedObjectDragHandle();
isc.SelectionOutline.showOutline();
} else {
this.resetDragProperties(selectedComponent);
this.hideDragHandle();
isc.SelectionOutline.hideOutline();
}
},
// In editMode, we allow dragging the selected canvas using the drag-handle
// This involves overriding some default behaviors at the widget level.
setupDragProperties : function (component) {
component.saveToOriginalValues([
"canDrag",
"canDrop",
"dragAppearance",
"dragStart",
"dragMove",
"dragStop",
"setDragTracker"
]);
component.setProperties({
canDrop: true,
dragAppearance: "outline",
// These method overrides are to clobber special record-based drag handling
// implemented by ListGrid and its children
dragStart : function () { return true; },
dragMove : function () { return true; },
setDragTracker : function () {isc.EH.setDragTracker(""); return false; },
dragStop : function () {
isc.EditContext.hideProxyCanvas();
isc.EditContext.positionDragHandle();
}
});
},
resetDragProperties : function (component) {
if (this.observer) this.observer.ignore(component, "dragMove");
component.restoreFromOriginalValues([
"canDrag",
"canDrop",
"dragAppearance",
"dragStart",
"dragMove",
"dragStop",
"setDragTracker"
]);
},
selectCanvasOrFormItem : function (object, hideLabel) {
// Make sure we're not being asked to select a non-visual object like a DataSource
// or ServiceOperation. We also support the idea of a visual proxy for a non-widget
// object - for example, ListGridFields are represented visually by the corresponding
// button in the ListGrid header.
if (!isc.isA.Canvas(object) && !isc.isA.FormItem(object) && !object._visualProxy) {
return;
}
// Or a Menu (ie, a context menu which has no visibility until an appropriate object
// is right-clicked by the user)
if (isc.isA.Menu(object)) {
return;
}
if (this._dragHandle) this._dragHandle.hide();
var selectedObject = isc.SelectionOutline.getSelectedObject();
if (selectedObject) this.resetDragProperties(selectedObject);
var underlyingObject,
overrideLabel;
if (object._visualProxy) {
var type = object.type || object._constructor;
overrideLabel = "[" + type + " " + (object.name ? "name:" : "ID");
overrideLabel += object.name || object.ID;
overrideLabel += "]"
underlyingObject = object;
object = object._visualProxy;
}
var editContext = underlyingObject ? underlyingObject.editContext : object.editContext;
if (!editContext) return;
var vb = editContext.creator;
isc.SelectionOutline.select(object, false,
!(hideLabel && vb.hideLabelWhenSelecting),
overrideLabel);
// For conceptual objects that needed a visual proxy, now we've done the physical
// on-screen selection we need to flip the object back to the underlying one
if (underlyingObject) object = underlyingObject;
if (object.editingOn) {
this.setupDragProperties(object);
this.showSelectedObjectDragHandle();
var ctx = object.editContext;
if (ctx.selectRecord) {
ctx.deselectAllRecords();
if (isc.isA.Canvas(object)) {
// Canvas objects are created with reasonably friendly IDs that appear
// as visual identifiers in the component tree
// (Except for section header objects, that is...)
if (isc.isA.SectionHeader(object) || isc.isA.ImgSectionHeader(object)) {
ctx.selectRecord(ctx.data.findById(object._ID));
} else {
ctx.selectRecord(ctx.data.findById(object.ID));
}
} else {
// FormItems have standard system-assigned IDs like isc_TextItem_1234.
// They are identified in the component tree by name.
ctx.selectRecord(ctx.data.find({ID: object.name}));
}
}
ctx.creator.editComponent(object.editNode, object);
}
},
showSelectedObjectDragHandle : function () {
if (!this._dragHandle) {
var _this = this;
this._dragHandle = isc.Img.create({
src: "[SKIN]/../../ToolSkin/images/controls/dragHandle.gif",
prompt:"Grab here to drag component",
width: this._dragHandleWidth, height: this._dragHandleHeight,
cursor:"move",
backgroundColor:"white",
opacity: 80,
canDrag: true,
canDrop: true,
isMouseTransparent: true,
mouseDown : function () {
// Remember the offset from the top-left corner of the target widget when
// a drag starts (OK, this is mouseDown, but all drags start from the
// co-ords of the most recent mouseDown, so it works)
this.dragIconOffsetX = isc.EH.getX() -
isc.EditContext.draggingObject.getPageLeft();
this.dragIconOffsetY = isc.EH.getY() -
isc.EditContext.draggingObject.getPageTop();
_this._mouseDown = true;
this.Super("mouseDown", arguments);
},
mouseUp : function () {
_this._mouseDown = false;
}
});
}
if (this.draggingObject) {
this.observer.ignore(this.draggingObject, "dragMove");
this.observer.ignore(this.draggingObject, "dragStop");
this.observer.ignore(this.draggingObject, "hide");
this.observer.ignore(this.draggingObject, "destroy");
}
var dragTarget = isc.SelectionOutline.getSelectedObject();
if (isc.isA.FormItem(dragTarget)) {
// dragTarget must be a canvas, so wrap the formItem in a proxy canvas
if (!this._dragTargetProxy) {
this._dragTargetProxy = isc.FormItemProxyCanvas.create();
}
this._dragTargetProxy.delayCall("setFormItem", [dragTarget]);
dragTarget = this._dragTargetProxy;
}
this._dragHandle.setProperties({dragTarget: dragTarget});
isc.Timer.setTimeout("isc.EditContext.positionDragHandle()", 0);
if (!this.observer) this.observer = isc.Class.create();
this.draggingObject = dragTarget;
this.observer.observe(this.draggingObject, "dragMove",
"isc.EditContext.positionDragHandle(true)");
this.observer.observe(this.draggingObject, "dragStop",
"isc.EditContext._mouseDown = false");
this.observer.observe(this.draggingObject, "hide",
"isc.EditContext._dragHandle.hide()");
this.observer.observe(this.draggingObject, "destroy",
"isc.EditContext._dragHandle.hide()");
this._dragHandle.show();
},
hideProxyCanvas : function () {
if (this._dragTargetProxy) this._dragTargetProxy.hide();
},
positionDragHandle : function (offset) {
if (!this._dragHandle) return;
var selected = this.draggingObject;
if (selected.destroyed || selected.destroying) {
this.logWarn("target of dragHandle: " + isc.Log.echo(selected) + " is invalid: " +
selected.destroyed ? "already destroyed"
: "currently in destroy()");
return;
}
var height = selected.getVisibleHeight();
if (height < this._dragHandleHeight * 2) {
// Center the drag handle next to the item (the -1 makes it look slightly more
// correct, because the image has a completely white line 1px thick at the top,
// giving the impression on a white background that it's lower down than it
// actually is)
this._dragHandleYOffset = Math.round((height - this._dragHandle.height) / 2) - 1;
} else {
// Place the drag handle at the top-left corner of a taller item
this._dragHandleYOffset = -1;
}
if (selected.isA("FormItemProxyCanvas") && !this._mouseDown) {
selected.syncWithFormItemPosition();
}
if (!selected) return;
var left = selected.getPageLeft() + this._dragHandleXOffset;
if (offset) {
left += selected.getOffsetX() - this._dragHandle.dragIconOffsetX;
}
this._dragHandle.setPageLeft(left);
var top = selected.getPageTop() + this._dragHandleYOffset;
if (offset) {
top += selected.getOffsetY() - this._dragHandle.dragIconOffsetY;
}
this._dragHandle.setPageTop(top);
this._dragHandle.bringToFront();
},
hideDragHandle : function () {
if (this._dragHandle) this._dragHandle.hide();
},
showDragHandle : function () {
if (this._dragHandle) this._dragHandle.show();
},
hideAncestorDragDropLines : function (object) {
while (object && object.parentElement) {
if (object.parentElement.hideDragLine) object.parentElement.hideDragLine();
if (object.parentElement.hideDropLine) object.parentElement.hideDropLine();
object = object.parentElement;
if (isc.isA.FormItem(object)) object = object.form;
}
},
getSchemaInfo : function (editNode) {
var schemaInfo = {},
liveObject = editNode.liveObject;
if (!liveObject) return schemaInfo;
if (isc.isA.FormItem(liveObject)) {
if (liveObject.form && liveObject.form.dataSource) {
var form = liveObject.form;
schemaInfo.dataSource = isc.DataSource.getDataSource(form.dataSource).ID;
schemaInfo.serviceName = form.serviceName;
schemaInfo.serviceNamespace = form.serviceNamespace;
} else {
schemaInfo.dataSource = liveObject.schemaDataSource;
schemaInfo.serviceName = liveObject.serviceName;
schemaInfo.serviceNamespace = liveObject.serviceNamespace;
}
} else if (isc.isA.Canvas(liveObject)) {
schemaInfo.dataSource = isc.DataSource.getDataSource(liveObject.dataSource).ID;
schemaInfo.serviceName = liveObject.serviceName;
schemaInfo.serviceNamespace = liveObject.serviceNamespace;
} else {
// If it's not a FormItem or a Canvas, then we must presume it's a config object.
// This can happen on drop of new components
schemaInfo.dataSource = liveObject.schemaDataSource;
schemaInfo.serviceName = liveObject.serviceName;
schemaInfo.serviceNamespace = liveObject.serviceNamespace;
}
return schemaInfo;
},
clearSchemaProperties : function (node) {
if (node && node.initData && isc.isA.FormItem(node.liveObject)) {
delete node.initData.schemaDataSource;
delete node.initData.serviceName;
delete node.initData.serviceNamespace;
var form = node.liveObject.form;
if (form && form.inputSchemaDataSource &&
isc.DataSource.get(form.inputSchemaDataSource).ID == node.initData.inputSchemaDataSource &&
form.inputServiceName == node.initData.inputServiceName &&
form.inputServiceNamespace == node.initData.inputServiceNamespace)
{
delete node.initData.inputSchemaDataSource;
delete node.initData.inputServiceName;
delete node.initData.inputServiceNamespace;
}
}
},
// XML source code generation
// ---------------------------------------------------------------------------------------
// serialize a set of component definitions to XML code, that is, essentially the
// editNode.defaults portion ( { _constructor:"Something", prop1:value, ... } )
serializeInitData : function (initData) {
if (initData == null) return null;
if (!isc.isAn.Array(initData)) initData = [initData];
var output = isc.SB.create();
isc.Comm.omitXSI = true;
for (var i = 0; i < initData.length; i++) {
var schema = isc.DS.getNearestSchema(initData[i]);
output.append(schema.xmlSerialize(initData[i]), "\n\n");
}
isc.Comm.omitXSI = null;
return output.toString();
}
});
isc.EditContext.addInterfaceMethods({
//> @attr editContext.defaultPalette (Palette : null : IRW)
// Palette to use when an +link{EditNode} is being created directly by this EditContext,
// instead of being created due to a user interaction with a palette (eg dragging from
// a TreePalette, or clicking on MenuPalette).
//
// If no defaultPalette is provided, the EditContext uses an automatically created
// +link{HiddenPalette}.
//
// @visibility devTools
//<
//> @method editContext.addFromPaletteNode()
// Creates a new EditNode from a PaletteNode, using the +link{defaultPalette}.
//
// @param paletteNode (PaletteNode) the palette node to use to create the new node
// @param [parentNode] (EditNode) optional the parent node if the new node should appear
// under a specific parent
// @return (EditNode) the EditNode created from the paletteNode
// @visibility devTools
//<
addFromPaletteNode : function (paletteNode, parentNode) {
var editNode = this.makeEditNode(paletteNode, parentNode);
return this.addNode(editNode, parentNode);
},
//> @method editContext.makeEditNode()
// Creates and returns an EditNode using the +link{defaultPalette}. Does not add the newly
// created EditNode to the EditContext.
//
// @param paletteNode (PaletteNode) the palette node to use to create the new node
// @return (EditNode) the EditNode created from the paletteNode
//<
makeEditNode : function (paletteNode) {
var palette = this.getDefaultPalette();
return palette.makeEditNode(paletteNode);
},
getDefaultPalette : function () {
if (this.defaultPalette) return this.defaultPalette;
return (this.defaultPalette = isc.HiddenPalette.create());
},
// wizard handling
requestLiveObject : function (newNode, callback, palette) {
var _this = this;
// handle deferred nodes (don't load or create their liveObject until they are actually
// added). NOTE: arguably the palette should handle this, and makeEditNode()
// should be asynchronous in this case.
if (newNode.loadData && !newNode.isLoaded) {
newNode.loadData(newNode, function (loadedNode) {
loadedNode = loadedNode || newNode
loadedNode.isLoaded = true;
// preserve the "dropped" flag
loadedNode.dropped = newNode.dropped;
_this.fireCallback(callback, "node", [loadedNode]);
}, palette);
return;
}
if (newNode.wizardConstructor) {
this.logInfo("creating wizard with constructor: " + newNode.wizardConstructor);
var wizard = isc.ClassFactory.newInstance(newNode.wizardConstructor,
newNode.wizardDefaults);
// ask the wizard to go through whatever steps
wizard.getResults(newNode, function (results) {
// accept either a paletteNode or editNode (detect via liveObject)
if (!results.liveObject) {
results = palette.makeEditNode(results);
}
_this.fireCallback(callback, "node", [results]);
}, palette);
return;
}
this.fireCallback(callback, "node", [newNode]);
},
getEditComponents : function () {
if (!this.editComponents) this.editComponents = [];
return this.editComponents;
},
// EditFields : optional lists of fields that can be edited in an EditContext
// ---------------------------------------------------------------------------------------
getEditDataSource : function (canvas) {
return isc.DataSource.getDataSource(canvas.editDataSource || canvas.Class ||
this.editDataSource);
},
// fields to edit:
// - application-specific: two different editing applications may edit the same type of
// component (eg a ListViewer) exposing different sets of properties
// - the DataSource may not even represent the full set of properties, but regardless,
// can act as a default list of fields and reference properties for those fields
// - on an application-specific basis, should be able to have a base set of fields, plus
// additions
// get list of editable fields for a component. May be a mix of string field names and
// field objects
_getEditFields : function (canvas) {
// combine the baseEditFields and editFields properties
var fields = [];
fields.addList(canvas.baseEditFields);
fields.addList(canvas.editFields);
// HACK: set any explicitly specified fields to be visible, since many fields in the
// current widget DataSources are set to visible=false to suppress them in editing
// demos. If a field is explicitly specified in editFields, we want it to be shown
// unless they've set "visible" explicitly
for (var i = 0; i < fields.length; i++) {
var field = fields[i];
if (field.visible == null) field.visible = true;
}
// if this is an empty list, take all the fields from the DataSource
if (fields.length == 0) {
fields = this.getEditDataSource(canvas).getFields();
fields = isc.getValues(fields);
}
return fields;
},
// get the list of editable fields as an Array of Strings
getEditFieldsList : function (canvas) {
var fieldList = [],
fields = this._getEditFields(canvas);
// return just the name for any fields specified as objects
for (var i = 0; i < fields.length; i++) {
var field = fields[i];
if (isc.isAn.Object(field)) {
fieldList.add(field.name);
} else {
fieldList.add(field);
}
}
return fieldList;
},
// get the edit fields, suitable for passing as "fields" to a dataBinding-aware component
getEditFields : function (canvas) {
var fields = this._getEditFields(canvas);
// make any string fields into objects
for (var i = 0; i < fields.length; i++) {
var field = fields[i];
if (isc.isA.String(field)) field = {name:field};
// same hack as above to ensure visibility of explicitly specified fields, for
// fields specified as just Strings
if (field.visible == null) field.visible = true;
fields[i] = field;
}
return fields;
},
// get serializable data as an Array of Objects for the editNodes in this context, via
// getting properties from the liveObjects and stripping it down to editFields (fields that
// are allowed to be edited in the context), or the DataSource fields if no editFields were
// declared.
serializeEditComponents : function () {
// get all the widgets being edited
var widgets = this.getEditComponents(),
output = [];
if (!widgets) return [];
for (var i = 0; i < widgets.length; i++) {
var child = widgets[i].liveObject,
// get all properties that don't have default value
props = child.getUniqueProperties(),
editFields = this.getEditFieldsList(child);
// add in the Class, which will be needed to recreate the widget, but which could never
// have non-default value
props._constructor = child.Class;
// limit the data to just the fields listed in the DataSource
props = isc.applyMask(props, editFields);
output.add(props);
}
return output;
},
// ---------------------------------------------------------------------------------------
enableEditing : function (editNode) {
var liveObject = editNode.liveObject;
if (liveObject.setEditMode) {
liveObject.setEditMode(true, this, editNode);
} else {
// We're trying enable editing on something that isn't a Canvas or a FormItem.
// Assume that it needs no special logic beyond setting the editNode, editContext
// and editingOn flag
liveObject.editContext = this;
liveObject.editNode = editNode;
liveObject.editingOn = true;
}
},
// Applying Properties to EditNodes
// ---------------------------------------------------------------------------------------
// apply properties to an editNode. Used by eg ComponentEditor
setNodeProperties : function (editNode, properties) {
if (this.logIsDebugEnabled("editing")) {
this.logDebug("with editNode: " + this.echoLeaf(editNode) +
" applying properties: " + this.echo(properties), "editing");
}
// Set the builder's dirty flag, in case we didn't get here via VB.saveProperties
this.creator.setIsDirty(true);
// update the initialization / serializeable data
isc.addProperties(editNode.initData, properties);
editNode._edited = true;
var targetObject = editNode.liveObject;
// FormItems name change: various FormItems would probably have to implement a fairly
// involved setName() (considering subItems and such), so do this via a remove/add
// cycle instead
var schema = isc.DS.get(editNode.type);
if (properties.name != null &&
(isc.isA.FormItem(targetObject) ||
(schema &&
(schema.inheritsSchema("ListGridField") || schema.inheritsSchema("DetailViewerField")))))
{
var theTree = this.data,
parentComponent = theTree.getParent(editNode),
index = theTree.getChildren(parentComponent).findIndex(editNode);
this.logInfo("using remove/re-add cycle to modify liveObject: " +
isc.echoLeaf(targetObject) + " within parent node " +
isc.echoLeaf(parentComponent));
this.removeComponent(editNode);
// update the node with the new name and add it
editNode.name = editNode.ID = properties.name;
delete properties.name;
this.addComponent(editNode, parentComponent, index);
// collect the newly created live object
targetObject = this.getLiveObject(editNode);
}
// update the component node with the new ID
if (editNode.initData.ID != null) editNode.ID = editNode.initData.ID;
// update the live object
if (targetObject.setEditableProperties) {
// instance of an SC class (or something else that implements a
// setEditableProperties API)
targetObject.setEditableProperties(properties);
if (targetObject.markForRedraw) targetObject.markForRedraw();
// NOTE: for FormItems, causes parent redraw
else if (targetObject.redraw) targetObject.redraw();
} else {
// for objects that never become ISC classes (MenuItems, ListGrid fields),
// call an overridable method on the parent if it exists
var parentComponent = this.data.getParent(editNode),
parentLiveObject = parentComponent ? parentComponent.liveObject : null;
if (parentLiveObject && parentLiveObject.setChildEditableProperties)
{
parentLiveObject.setChildEditableProperties(targetObject, properties,
editNode, this);
} else {
// fall back to just applying the properties
isc.addProperties(targetObject, properties);
}
}
this.markForRedraw();
},
// ---------------------------------------------------------------------------------------
// The "wrapperForm" is a DynamicForm that we auto-create as a container for a FormItem dropped
// directly onto a Canvas, Layout or whatever. We're using autoChild-like semantics here so
// that you can provide your own settings for the generated form.
addWithWrapper : function (childNode, parentNode) {
var defaults = isc.addProperties({}, this.wrapperFormDefaults),
paletteNode = {
type: this.wrapperFormDefaults._constructor,
defaults : defaults
};
// if this FormItem belongs to a DataSource, the wrapper form needs to use it too
if (childNode.liveObject.schemaDataSource) {
var item = childNode.liveObject;
defaults.doNotUseDefaultBinding = true;
defaults.dataSource = item.schemaDataSource;
defaults.serviceNamespace = item.serviceNamespace;
defaults.serviceName = item.serviceName;
}
var wrapperNode = this.makeEditNode(paletteNode);
// add the wrapper to the parent
this.addComponent(wrapperNode, parentNode);
// add the child node to the wrapper
return this.addComponent(childNode, wrapperNode);
},
wrapperFormDefaults: {
_constructor: "DynamicForm"
}
});
//> @groupDef devTools
// The Portals & Tools framework enables you to build interfaces in which a set of UI
// components can be edited by end users, saved and later restored.
//
// This includes interfaces such as:
//
// - Portals: where a library of possible portlets can be created & configured then stored
// for future use and shared with other users
//
- Diagramming & FlowChart tools: tools similar to Visio™ which allows users
// to use shapes and connectors to create a flowchart or diagram
//
- Development Tools: tools similar to Visual Builder, in which a user can build new UI
// interfaces and save them
//
// The basic building blocks of the Portals & Tools framework are:
//
// - Palettes: palettes create UI components on the fly from stored data. Palettes
// come in a variety of flavors which implement different UI gestures for creating components
// (e.g. drag from tree, pick from menu, etc)
//
- EditContexts: an edit context tracks a set of components that are being edited.
// Different EditContexts offer different built-in user interactions for editing the
// components they track, for example, a tree-based EditContext can provide an interface for
// managing relationships between components via drag and drop, and a Canvas-based EditContext
// can automatically store changes to position and size.
//
// Defaults vs LiveObject
// The Portals & Tools framework is careful to maintain a distinction between the current state
// of the live UI component and the data that should actually be persisted and used to
// re-create the component. For example:
//
// - a portlet may be showing a border or brightened background color to hilite it within
// the tool, but this should not be saved as part of the portlet state
//
- a component may have a current width of 510 pixels when viewed within a tool, but what
// should persist is the configured width of 40% of available space
//
- in a development tool, a component such a Window automatically creates subcomponents
// such as a header - these should not persist because the Window knows how to create them
// without any additional data. Only components specifically dragged into the Window by the
// end user should be persisted
//
// The data that will be saved and used to re-create the component is called the
// defaults. A Palette, which creates a component from stored data, minimally requires
// the +link{SCClassName,className} of the component to create, and the defaults. This
// information is captured by a +link{PaletteNode} (along with other options) - all types of
// Palettes work with a set or tree of paletteNodes.
//
// When a component is created from a paletteNode, a "live object" is created from the
// defaults. One PaletteNode may generate any number of live objects. An +link{EditNode}
// combines the +link{editNode.liveObject,live object} along with the className and defaults
// needed to recreate the live object, and is used to track the created live object as it is
// further edited. Essentially an EditNode is a copy of the PaletteNode in which a live object
// has been created and the editNode.defaults may now begin to change as the user edits the
// object they have created.
//
// @title Portals & Tools Framework Overview
// @treeLocation Client Reference/Tools
// @visibility devTools
//<
//> @interface Palette
// An interface that provides the ability to create components from a +link{PaletteNode}.
//
// @treeLocation Client Reference/Tools
// @group devTools
// @visibility devTools
//<
//> @attr palette.defaultEditContext (EditContext : null : IRW)
// Default EditContext that this palette should use. Palettes generally create components via
// drag and drop, but may also support creation via double-click or other UI idioms when a
// defaultEditContext is set.
// @visibility external
//<
// providing static methods, but then they couldn't be piece however, making it an inteface
isc.ClassFactory.defineInterface("Palette");
//> @object PaletteNode
// An object representing a component which the user may create dynamically within an
// application.
//
// A PaletteNode expresses visual properties for how the palette will display it (eg
// +link{paletteNode.title,title}, +link{paletteNode.icon,icon}) as well as instructions for
// creating the component the paletteNode represents (+link{paletteNode.type},
// +link{paletteNode.defaults}).
//
// Various types of palettes (+link{ListPalette}, +link{TreePalette}, +link{MenuPalette},
// +link{TilePalette}) render a PaletteNode in different ways, and allow the user to trigger
// creation in different ways (eg drag and drop, or just click). All share a common pattern
// for how components are created from palettes.
//
// Note that in a TreePalette, a PaletteNode is essentially a +link{TreeNode} and can have
// properties expected for a TreeNode (eg,
// +link{TreeGrid.customIconDropProperty,showDropIcon}. Likewise
// a PaletteNode in a MenuPalette can have the properties of a +link{MenuItem}, such as
// +link{MenuItem.enableIf}.
//
// @treeLocation Client Reference/Tools
// @visibility devTools
//<
//> @attr paletteNode.icon (SCImgURL : null : IR)
// Icon for this paletteNode.
//
// @visibility devTools
//<
//> @attr paletteNode.title (String : null : IR)
// Textual title for this paletteNode.
//
// @visibility devTools
//<
//> @attr paletteNode.type (SCClassName : null : IR)
// +link{SCClassName} this paletteNode creates, for example, "ListGrid".
//
// @visibility devTools
//<
//> @attr paletteNode.defaults (Properties : null : IR)
// Defaults for the component to be created from this palette.
//
// For example, if +link{paletteNode.type} is "ListGrid", properties valid to pass to
// +link{ListGrid.create()}.
//
// @visibility devTools
//<
//> @attr paletteNode.liveObject (Object : null : IR)
// For a paletteNode which should be a "singleton", that is, always provides the exact same
// object (==) rather than a dynamically created copy, provide the singleton object as
// liveObject
.
//
// Instead of dynamically creating an object from defaults, the liveObject
will
// simply be assigned to +link{editNode.liveObject} for the created editNode.
//
// @visibility devTools
//<
//> @attr paletteNode.wizardConstructor (Object : null : IR)
// A paletteNode that requires user input before component creation can occur
// may provide a wizardConstructor
and +link{wizardDefaults} for the creation of
// a "wizard" component.
//
// Immediately after creation, the wizard will then have the +link{paletteWizard.getResults()}
// method called on it, dynamically produced defaults.
//
// @visibility devTools
//<
//> @attr paletteNode.wizardDefaults (Properties : null : IR)
// Defaults for the wizard created to gather user input before a component is created from
// this PaletteNode. See +link{wizardConstructor}.
//
// @visibility devTools
//<
// PaletteWizard
// ---------------------------------------------------------------------------------------
//> @interface PaletteWizard
// Interface to be fulfilled by a "wizard" specified on a +link{PaletteNode} via
// +link{paletteNode.wizardConstructor}.
// @visibility devTools
//<
//> @method paletteWizard.getResults()
// Single function invoked on paletteWizard. Expects defaults to be asynchronously returned,
// after user input is complete, by calling the +link{Callback} provided as a parameter.
//
// @param callback (Callback) callback to be fired once this wizard completes. Expects a
// single argument: the defaults
// @param paletteNode (PaletteNode) the paletteNode that specified this wizard
// @param palette (Palette) palette where creation is taking place
//
// @visibility devTools
//<
isc.Palette.addInterfaceMethods({
//> @method palette.makeEditNode()
// Given a +link{PaletteNode}, make an +link{EditNode} from it by creating a
// +link{editNode.liveObject,liveObject} from the +link{paletteNode.defaults}
// and copying presentation properties (eg +link{paletteNode.title,title}
// to the editNode. If editNodeProperties
is specified as an object on
// on the paletteNode, each property in this object will also be copied across to
// the editNode.
// @param paletteNode (PaletteNode) paletteNode to create from
// @return (EditNode) created EditNode
//
// @visibility devTools
//<
makeEditNode : function (paletteNode) {
return this.makeNewComponent(paletteNode);
},
makeNewComponent : function (sourceData) {
if (!sourceData) sourceData = this.getDragData();
if (isc.isAn.Array(sourceData)) sourceData = sourceData[0];
var type = sourceData.className || sourceData.type;
var componentNode = {
type : type,
_constructor : type, // this is here just to match the initData
// for display in the target Tree
title : sourceData.title,
icon : sourceData.icon,
iconSize : sourceData.iconSize,
showDropIcon : sourceData.showDropIcon,
useEditMask : sourceData.useEditMask,
autoGen : sourceData.autoGen
};
// support arbitrary properties on the generated edit node
// This allows 'loadData' to get at properties that might not otherwise be copied
// across to the editNode from the paletteNode
if (isc.isAn.Object(sourceData.editNodeProperties)) {
for (var prop in sourceData.editNodeProperties) {
componentNode[prop] = sourceData.editNodeProperties[prop];
}
}
// allow a maker function on the source data (synchronous)
if (sourceData.makeComponent) {
componentNode.liveObject = sourceData.makeComponent(componentNode);
return componentNode;
}
// NOTE: IDs
// - singletons may have an ID on the palette node.
// - an ID may appear in defaults because palette-based construction is used to reload
// views, and in this case the palette node will be used once ever
var defaults = sourceData.defaults;
componentNode.ID = sourceData.ID ||
(defaults ? isc.DS.getAutoId(defaults) : null);
var clobberDefaults = true;
if (sourceData.loadData) {
// deferred load node. No creation happens for now; whoever receives this node is
// expected to call the loadData function
componentNode.loadData = sourceData.loadData;
} else if (sourceData.wizardConstructor) {
// wizard-based deferred construction
componentNode.wizardConstructor = sourceData.wizardConstructor;
componentNode.wizardDefaults = sourceData.wizardDefaults;
} else if (sourceData.liveObject) {
// singleton, or already created component. This means that rather than a new
// object being instantiated each time, the same "liveObject" should be reused,
// because multiple components will be accessing a shared object.
var liveObject = sourceData.liveObject;
// handle global IDs
if (isc.isA.String(liveObject)) liveObject = window[liveObject];
componentNode.liveObject = liveObject
} else {
// create a live object from defaults
componentNode = this.createLiveObject(sourceData, componentNode);
clobberDefaults = false;
}
// also pass the defaults. Note that this was overwriting a more detailed set of defaults
// derived by the createLiveObject method; hence the introduction of the condition
if (clobberDefaults) componentNode.defaults = sourceData.defaults;
return componentNode;
},
//> @attr palette.generateNames (boolean : true : IR)
// Whether created components should have their "ID" or "name" property automatically set
// to a unique value based on the component's type, eg, "ListGrid0".
//
// @group devTools
// @visibility devTools
//<
generateNames : true,
typeCount : {},
// get an id for the object we're creating, by type
getNextAutoId : function (type) {
if (type == null) type = "Object";
var autoId;
this.typeCount[type] = this.typeCount[type] || 0;
while (window[(autoId = type + this.typeCount[type]++)] != null) {}
return autoId;
},
createLiveObject : function (sourceData, componentNode) {
// put together an initialization data block
var type = sourceData.className || sourceData.type,
classObject = isc.ClassFactory.getClass(type),
schema = isc.DS.getNearestSchema(type),
initData = {},
// assume we should create standalone if there's no schema (won't happen anyway if
// there's no class)
createStandalone = (schema ? schema.shouldCreateStandalone() : true),
sourceInitData = sourceData.initData || sourceData.defaults || {};
// suppress drawing for widgets
if (classObject && classObject.isA("Canvas")) initData.autoDraw = false;
// If a title was explicitly passed in the sourceData, use it
if (sourceData.initData && sourceData.initData.title) {
initData.title = sourceData.initData.title;
}
if (this.generateNames) {
// generate an id if one wasn't specified
var ID = componentNode.ID = componentNode.ID ||
sourceInitData[schema.getAutoIdField()] ||
this.getNextAutoId(type);
// give the object an autoId in initData
initData[schema.getAutoIdField()] = ID;
// don't supply a title for contexts where the ID or name will automatically be
// used as a title (currently just formItems), otherwise, it will be necessary to
// change both ID/name and title to get rid of the auto-gen'd id
if (schema && schema.getField("title") && !isc.isA.FormItem(classObject) &&
!initData.title) {
initData.title = ID;
}
}
initData = componentNode.initData =
componentNode.defaults = // HACK: alias to defaults; only defaults is doc'd
isc.addProperties(initData,
this.componentDefaults,
sourceData.defaults);
initData._constructor = type;
// create the live object from the init data
// NOTE: components generated from config by parents (tabs, sectionStack sections,
// formItems). These objects:
// - are created as an ISC Class by adding to a parent, and not before
// - in makeEditNode, don't create if there is no class or if the schema sets
// createStandalone=false
// - destroyed by removal from the parent, then re-created by a re-add
// - re-add handled by addComponent by checking for destruction
// - serialized as sub-objects rather than independent components
// - handled by checking for _generated during serialization
// - should be a default consequence of not having a class or setting
// createStandalone=false
// The various checks mentioned above are redundant and should be unified and make able
// to be declared in component schema
// if there's no class for the item, or schema.createStandalone has been set false,
// don't auto-create the component - assume the future parent of the component will
// create it from data. The explicit flag (createStandalone:false) is needed for
// FormItems. In particular, canvasItems require item.containerWidget to be defined
// during init.
var liveObject;
if (classObject && createStandalone) {
liveObject = isc.ClassFactory.newInstance(initData);
} else {
// for the live object, just create a copy (NOTE: necessary because widgets
// generally assume that it is okay to add properties to pseudo-objects provided as
// init data)
componentNode.generatedType = true;
liveObject = isc.shallowClone(initData);
}
// store the new live object
componentNode.liveObject = liveObject;
this.logInfo("palette created component, type: " + type +
", ID: " + ID +
(this.logIsDebugEnabled("editing") ?
", initData: " + this.echo(initData) : "") +
", liveObject: " + this.echoLeaf(liveObject), "editing");
return componentNode;
}
});
//> @class HiddenPallete
// A Pallete with no visible representation that handles programmatic creation of components.
//
// @group devTools
// @treeLocation Client Reference/Tools
// @visibility devTools
//<
isc.defineClass("HiddenPalette", "Class", "Palette");
//> @attr treePalette.componentDefaults (Object : null : IR)
// Defaults to apply to all components originating from this palette.
// @group devTools
// @visibility devTools
//<
// ---------------------------------------------------------------------------------------
//> @class TreePalette
// A TreeGrid that implements the Palette behavior, so it can be used as the source for
// drag and drop instantiation of components when combined with an +link{EditContext} as
// the drop target.
//
// Each +link{TreeNode} within +link{treeGrid.data} can be a +link{PaletteNode}.
//
// @group devTools
// @treeLocation Client Reference/Tools
// @visibility devTools
//<
// Class will not work without TreeGrid
if (isc.TreeGrid) {
isc.defineClass("TreePalette", "TreeGrid", "Palette").addMethods({
canDragRecordsOut:true,
// add to defaultEditContext (if any) on double click
recordDoubleClick : function () {
var target = this.defaultEditContext;
if (target) {
if (isc.isA.String(target)) target = this.creator[target];
if (isc.isAn.EditContext(target)) {
var node = this.makeEditNode(this.getDragData());
if (node) {
if (target.getDefaultParent(node, true) == null) {
isc.warn("No default parent can accept a component of this type");
} else {
target.addNode(node);
isc.EditContext.selectCanvasOrFormItem(node.liveObject, true);
}
}
}
}
},
// NOTE: we can't factor this up to the Palette interface because it wouldn't override the
// built-in implementation of transferDragData.
transferDragData : function (targetFolder) {
return [this.makeEditNode(this.getDragData())];
}
});
}
// --------------------------------------------------------------------------------------------
//> @class ListPalette
// A ListGrid that implements the +link{Palette} behavior, so it can be used as the source for
// drag and drop instantiation of components when combined with an +link{EditContext} as
// the drop target.
//
// Each +link{ListGridRecord} can be a +link{PaletteNode}.
//
// @group devTools
// @treeLocation Client Reference/Tools
// @visibility devTools
//<
// Class will not work without ListGrid
if (isc.ListGrid) {
isc.defineClass("ListPalette", "ListGrid", "Palette").addMethods({
canDragRecordsOut:true,
defaultFields : [ { name:"title", title:"Title" } ],
// add to defaultEditContext (if any) on double click
recordDoubleClick : function () {
// NOTE: dup'd in TreePalette
var target = this.defaultEditContext;
if (target) {
if (isc.isA.String(target)) target = isc.Canvas.getById(target);
if (isc.isAn.EditContext(target)) {
target.addNode(this.makeEditNode(this.getDragData()));
}
}
},
// NOTE: we can't factor this up to the Palette interface because it wouldn't override the
// built-in implementation of transferDragData.
transferDragData : function () {
return [this.makeEditNode(this.getDragData())];
}
});
}
// --------------------------------------------------------------------------------------------
//> @class TilePalette
// A +link{TileGrid} that implements the +link{Palette} behavior, so it can be used as the source for
// drag and drop instantiation of components when combined with an +link{EditContext} as
// the drop target.
//
// Each +link{TileGrid.tile} can be a +link{PaletteNode}.
//
// @group devTools
// @treeLocation Client Reference/Tools
// @visibility devTools
//<
// Class will not work without TileGrid
if (isc.TileGrid) {
isc.defineClass("TilePalette", "TileGrid", "Palette").addMethods({
canDragRecordsOut: true,
defaultFields: [
{name: "title", title: "Title"}
],
// add to defaultEditContext (if any) on double click
recordDoubleClick : function () {
var target = this.defaultEditContext;
if (target) {
if (isc.isA.String(target)) target = isc.Canvas.getById(target);
if (isc.isAn.EditContext(target)) {
target.addNode(this.makeEditNode(this.getDragData()));
}
}
},
// NOTE: we can't factor this up to the Palette interface because it wouldn't override the
// built-in implementation of transferDragData.
transferDragData : function () {
return [this.makeEditNode(this.getDragData())];
}
});
}
// --------------------------------------------------------------------------------------------
//> @class MenuPalette
// A Menu that implements the +link{Palette} behavior, so it can be used as the source for
// drag and drop instantiation of components when combined with an +link{EditContext} as
// the drop target.
//
// Each +link{MenuItem} can be a +link{PaletteNode}.
//
// @group devTools
// @treeLocation Client Reference/Tools
// @visibility devTools
//<
// Class will not work without Menu
if (isc.Menu) {
isc.defineClass("MenuPalette", "Menu", "Palette").addMethods({
canDragRecordsOut:true,
// needed because the selection is what's dragged, and menus do not normally track a
// selection
selectionType: "single",
// add to defaultEditContext (if any) on click
itemClick : function (item) {
var target = this.defaultEditContext;
if (target) {
if (isc.isA.String(target)) target = isc.Canvas.getById(target);
if (isc.isAn.EditContext(target)) {
target.addNode(this.makeEditNode(this.getDragData()));
}
}
},
// NOTE: we can't factor this up to the Palette interface because it wouldn't override the
// built-in implementation of transferDragData.
transferDragData : function () {
return [this.makeEditNode(this.getDragData())];
}
});
}
// ---------------------------------------------------------------------------------------
//> @class EditPane
// A container that allows drag and drop instantiation of visual components from a
// +link{Palette}, and direct manipulation of the position and size of those components.
//
// Any drag onto an EditPane from a Palette will add an EditNode created from the dragged
// PaletteNode.
//
// If an EditNode containing a +link{editNode.liveObject,liveObject} that is a subclass
// of Canvas is added to an EditPane,
//
// @group devTools
// @treeLocation Client Reference/Tools
// @visibility devTools
//<
isc.ClassFactory.defineClass("EditPane", "Canvas", "EditContext");
isc.EditPane.addProperties({
canAcceptDrop:true,
contextMenu : {
autoDraw:false,
data : [{title:"Clear", click: "target.removeAllChildren()"}]
},
editingOn:true,
//> @attr EditPane.persistCoordinates (boolean : true : IRW)
// If enabled, changes to components position and size will be persisted to their
// EditNodes. This applies to both programmatic calls and user interaction (drag
// reposition or drag resize).
//
// @visibility devTools
//<
persistCoordinates:true,
// drag selection properties
canDrag:true,
dragAppearance:"none",
overflow:"hidden",
selectedComponents: []
});
isc.EditPane.addMethods({
// Component creation
// ---------------------------------------------------------------------------------------
// on drop from a palette, add a new component
drop : function () {
var source = isc.EH.dragTarget;
// if the source isn't a Palette, do standard drop interaction
if (!source.isA("Palette")) return this.Super("drop", arguments);
var data = source.transferDragData(),
editNode = (isc.isAn.Array(data) ? data[0] : data);
if (!editNode) return false;
var editContext = this;
this.requestLiveObject(editNode, function (editNode) {
if (editNode) editContext.addComponentAtCursor(editNode);
}, source)
return isc.EH.STOP_BUBBLING;
},
//> @method editPane.addNode()
// Adds a new +link{editNode} to the pane.
// @param editNode (EditNode) new editNode to add
//
// @visibility devTools
//<
addNode : function (editNode) {
return this.addComponent(editNode);
},
addComponent : function (component) {
this.logInfo("EditPane adding component: " + this.echoLeaf(component), "editing");
if (this.editComponents == null) this.editComponents = [];
this.editComponents.add(component);
// add the component as a child
var liveObject = component.liveObject;
this.addChild(liveObject);
// Flip it into edit mode depending on the setting on the VB instance
if (this.creator && this.creator.editingOn) this.enableEditing(component);
// Add an event mask if so configured
if (component.useEditMask) liveObject.showEditMask();
// Call hook in case the live object wants to know about being added
if (liveObject.addedToEditContext) liveObject.addedToEditContext(this, component);
},
// add a new component at the current mouse position
addComponentAtCursor : function (component) {
this.addNode(component);
var liveObject = component.liveObject;
liveObject.moveTo(this.getOffsetX(), this.getOffsetY());
},
// The EditPane itself is the default parent for added nodes
getDefaultParent : function (newNode, returnNullIfNoSuitableParent) {
return this;
},
// Component removal / destruction
// ---------------------------------------------------------------------------------------
// if a child is removed that is being edited, remove it from the list of edit components
removeChild : function (child, name) {
this.Super("removeChild", arguments);
if (this.editComponents == null) this.editComponents = [];
this.editComponents.removeWhere("ID", child.getID());
this.selectedComponents.remove(child);
},
//<
removeAllChildren : function () {
if (!this.children) return;
var destroyTargets = [];
for (var i = 0; i < this.children.length; i++) {
if (this.children[i]._eventMask) destroyTargets.add(this.children[i]);
}
for (var i = 0; i < destroyTargets.length; i++) {
destroyTargets[i].destroy();
}
},
removeSelection : function (target) {
if (this.selectedComponents.length > 0) {
while (this.selectedComponents.length > 0) {
this.selectedComponents[0].destroy();
}
} else {
target.destroy();
}
},
// Thumbs, drag move and resize
// ---------------------------------------------------------------------------------------
click : function () {
isc.Canvas.hideResizeThumbs();
},
// enable editing mode for the entire EditPane: turn editing on for all edit components
setEditMode : function (editingOn) {
if (editingOn == null) editingOn = true;
if (this.editingOn == editingOn) return;
this.editingOn = editingOn;
var liveObjects = this.editComponents.getProperty("liveObject");
liveObjects.map("setEditMode", editingOn, this);
},
// save new coordinates to initDate on resize or move
childResized : function (liveObject) {
var result = this.Super("childResized", arguments);
this.saveCoordinates(liveObject);
return result;
},
childMoved : function (liveObject, deltaX, deltaY) {
var result = this.Super("childMoved", arguments);
this.saveCoordinates(liveObject);
// if this component is part of a selection, move the rest of the selected
// components by the same amount
var selection = this.selectedComponents;
if (selection.length > 0 && selection.contains(liveObject)) {
for (var i = 0; i < selection.length; i++) {
if (selection[i] != liveObject) {
selection[i].moveBy(deltaX, deltaY);
}
}
}
return result;
},
saveCoordinates : function (liveObject) {
// eg, no components yet, hoop selector moved
if (!this.editComponents) return;
if (!this.persistCoordinates) return;
//this.logWarn("saveCoordinates for: " + liveObject +
// ", editComponents are: " + this.echoAll(this.editComponents));
var component = this.editComponents.find("liveObject", liveObject);
// can happen if we get a resized or moved notification while a component is being
// added or removed
if (!component) return;
component.initData = isc.addProperties(component.initData, {
left:liveObject.getLeft(),
top:liveObject.getTop(),
width:liveObject.getWidth(),
height:liveObject.getHeight()
})
},
// Serialization
// ---------------------------------------------------------------------------------------
//> @method editPane.getSaveData()
// Returns an Array of +link{PaletteNode}s representing all current +link{EditNodes} in this
// pane, suitable for saving and restoring via passing each paletteNode to +link{addNode()}.
// @return (Array of PaletteNode) paletteNodes suitable for saving for subsequent restoration
//
// @visibility devTools
//<
getSaveData : function () {
// get all the components being edited
var editComponents = this.getEditComponents(),
allSaveData = [];
for (var i = 0; i < editComponents.length; i++) {
var component = editComponents[i],
liveObject = component.liveObject;
// save off just types and initialization data, not the live objects themselves
var saveData = {
type : component.type,
defaults : component.defaults
};
// let the object customize it
if (liveObject.getSaveData) saveData = liveObject.getSaveData(saveData);
allSaveData.add(saveData);
}
return allSaveData;
},
// Hoop selection
// --------------------------------------------------------------------------------------------
// create selector hoop
canMultiSelect:true,
mouseDown : function () {
if (!this.editingOn || !this.canMultiSelect ||
// don't start hoop selection unless the mouse went down on the EditPane itself, as
// opposed to on one of the live objects
isc.EH.getTarget() != this) return;
var target = isc.EH.getTarget();
if (this.selector == null) {
this.selector = isc.Canvas.create({
autoDraw:false,
keepInParentRect: true,
left: isc.EH.getX(),
top: isc.EH.getY(),
redrawOnResize:false,
overflow: "hidden",
border: "1px solid blue"
});
this.addChild(this.selector);
}
this.startX = this.getOffsetX();
this.startY = this.getOffsetY();
this.resizeSelector();
this.selector.show();
this.updateCurrentSelection();
},
// resize hoop on dragMove
dragMove : function() {
if (this.selector) this.resizeSelector();
},
// hide selector hoop on mouseUp or dragStop
mouseUp : function () {
if (this.selector) this.selector.hide();
},
dragStop : function() {
if (this.selector) this.selector.hide();
},
outlineBorderStyle : "2px dashed red",
// add an outline, indicating selection to a set of components
setOutline : function (components) {
if (!components) return;
if (!isc.isAn.Array(components)) components = [components];
for (var i = 0; i < components.length; i++) {
components[i]._eventMask.setBorder(this.outlineBorderStyle);
}
},
// clear outline on a set of components
clearOutline : function (components) {
if (!components) return;
if (!isc.isAn.Array(components)) components = [components];
for (var i = 0; i < components.length; i++) {
components[i]._eventMask.setBorder("none");
}
},
// figure out which components intersect the selector hoop, and show the selected outline on
// those
updateCurrentSelection : function () {
if (!this.children) return;
var oldSelection = this.selectedComponents;
// make a list of all the children which currently intersect the selection hoop
this.selectedComponents = [];
for (var i = 0; i < this.children.length; i++) {
var child = this.children[i];
if (this.selector.intersects(child)) {
child = this.deriveSelectedComponent(child);
if (child && !this.selectedComponents.contains(child)) {
this.selectedComponents.add(child);
}
}
}
// set outline on components currently within the hoop
this.setOutline(this.selectedComponents);
// de-select anything that is no longer within the hoop
oldSelection.removeList(this.selectedComponents);
this.clearOutline(oldSelection);
// show selection in window.status
var selection = this.selectedComponents.getProperty("ID");
window.status = selection.length ? "Current Selection: " + selection : "";
},
// given a child in the editPane, derive the editComponent if there is one
deriveSelectedComponent : function (comp) {
// if the component has a master, it's either an editMask or a peer of some editComponent
if (comp.masterElement) return this.deriveSelectedComponent(comp.masterElement);
if (!comp.parentElement || comp.parentElement == this) {
// if it has an event mask, it's an edit component
if (comp._eventMask) return comp;
// otherwise it's a mask
return null;
}
// XXX does this case exist? how can a direct child have a parent element other than its
// parent?
return this.deriveSelectedComponent(comp.parentElement);
},
// resize selector to current mouse coordinates
resizeSelector : function () {
var x = this.getOffsetX(),
y = this.getOffsetY();
if (this.selector.keepInParentRect) {
if (x < 0) x = 0;
var parentHeight = this.selector.parentElement.getVisibleHeight();
if (y > parentHeight) y = parentHeight;
}
// resize to the distances from the start coordinates
this.selector.resizeTo(Math.abs(x-this.startX), Math.abs(y-this.startY));
// if we are above/left of the origin set top/left to current mouse coordinates,
// otherwise to start coordinates.
if (x < this.startX) this.selector.setLeft(x);
else this.selector.setLeft(this.startX);
if (y < this.startY) this.selector.setTop(y);
else this.selector.setTop(this.startY);
// figure out which components are now in the selector hoop
this.updateCurrentSelection();
},
// external, safe getter for selected components
getSelectedComponents : function () {
return this.selectedComponents.duplicate()
}
});
//> @class EditTree
// A TreeGrid that allows drag and drop creation and manipulation of a tree of
// object sdescribed by DataSources.
//
// Nodes can be added via drag and drop from a +link{Palette} or may be programmatically
// added via +link{editTree.addNode()}. Nodes may be dragged within the tree to reparent
// them.
//
// Eligibility to be dropped on any given node is determined by inspecting the
// DataSource of the parent node. Drop is allowed only if the parent schema has
// a field which accepts the type of the dropped node.
//
// On successful drop, the newly created component will be added to the parent node under the
// detected field. Array fields, declared by setting
// dataSourceField.multiple:true
, are supported.
//
// An EditTree is initialized by setting +link{EditTree.rootComponent}. EditTree.data (the
// Tree instance) should never be directly set or looked at.
//
// @treeLocation Client Reference/Tools
// @group devTools
// @visibility devTools
//<
// Class will not work without TreeGrid
if (isc.TreeGrid) {
isc.ClassFactory.defineClass("EditTree", "TreeGrid", "EditContext");
isc.EditTree.addProperties({
//> @attr EditTree.rootComponent (Object : null : IR)
// Root of data to edit. Must contain the "_constructor" property, with the name of a
// valid +link{DataSource,schema} or nothing will be able to be dropped on the this
// EditTree.
//
// Can be retrieved at any time.
//
// @group devTools
// @visibility devTools
//<
canDragRecordsOut: false,
canAcceptDroppedRecords: true,
canReorderRecords: true,
fields:[
{name:"ID", title:"ID", width:"*"},
{name:"type", title:"Type", width:"*"}
],
selectionType:isc.Selection.SINGLE,
// whether to automatically show parents of an added node (if applicable)
autoShowParents:true
});
isc.EditTree.addMethods({
initWidget : function () {
this.Super("initWidget", arguments);
// NOTE: there really is no reasonable default for rootComponent, since its type
// determines what can be dropped. This default will create a tree that won't accept
// any drops, but won't JS error
var rootComponent = this.rootComponent || { _constructor: "Object" },
rootType = isc.isA.Class(rootComponent) ? rootComponent.Class :
rootComponent._constructor,
rootLiveObject = this.rootLiveObject || rootComponent;
var rootNode = {
type: rootType,
_constructor: rootType,
initData : rootComponent,
liveObject: rootLiveObject
};
this.setData(isc.Tree.create({
idField:"ID",
root : rootNode,
// HACK: so that all nodes can be targetted for D&D
isFolder : function () { return true; }
}));
},
// Adding / Removing components in the tree
// --------------------------------------------------------------------------------------------
// tests whether the targetNode can accept a newNode of type "type"
canAddToParent : function (targetNode, type) {
var liveObject = targetNode.liveObject;
if (isc.isA.Class(liveObject)) {
return (liveObject.getObjectField(type) != null);
}
// still required for MenuItems and ListGridFields, where the live object is not a Class
return (isc.DS.getObjectField(targetNode, type) != null);
},
willAcceptDrop : function () {
if (!this.Super("willAcceptDrop",arguments)) return false;
var recordNum = this.getEventRow(),
dropTarget = this.getDropFolder(),
dragData = this.ns.EH.dragTarget.getDragData()
;
if (dragData == null) return false;
if (isc.isAn.Array(dragData)) {
if (dragData.length == 0) return false;
dragData = dragData[0];
}
if (dropTarget == null) dropTarget = this.data.getRoot();
var dragType = dragData.className || dragData.type;
this.logInfo("checking dragType: " + dragType +
" against dropLiveObject: " + dropTarget.liveObject, "editing");
return this.canAddToParent(dropTarget, dragType)
},
folderDrop : function (nodes, parent, index, sourceWidget) {
if (sourceWidget != this && !sourceWidget.isA("Palette")) {
// if the source isn't a Palette, do standard drop interaction
return this.Super("folderDrop", arguments);
}
if (sourceWidget != this) {
// this causes component creation since the drop is from a Palette
nodes = sourceWidget.transferDragData();
}
var newNode = (isc.isAn.Array(nodes) ? nodes[0] : nodes);
// flag that this node was dropped by a user
newNode.dropped = true;
this.logInfo("sourceWidget is a Palette, dropped node of type: " + newNode.type,
" editing");
var editTree = this;
this.requestLiveObject(newNode, function (node) {
if (node == null) return;
// self-drop: remove component from old location before re-adding
if (sourceWidget == editTree) {
// If we're self-dropping to a slot further down in the same parent, this will
// cause the index to become off by one
var oldParent = this.data.getParent(newNode);
if (parent == oldParent) {
var oldIndex = this.data.getChildren(oldParent).indexOf(newNode);
if (oldIndex != null && oldIndex <= index) index--;
}
editTree.removeComponent(newNode);
}
editTree.addNode(node, parent, index);
}, sourceWidget)
},
//> @method editTree.addNode()
// Add a new +link{EditNode} to the tree, under the specified parent.
//
// The EditTree will interrogate the parent and new nodes to determine what field
// within the parent allows a child of this type, and to find a method to add the newNode's
// liveObject to the parentNode's liveObject. The new relationship will then be stored
// in the +link{editNode.defaults} of the parentNode.
//
// For example, when a Tab is dropped on a TabSet, the field TabSet.tabs is discovered as
// the correct target field via naming conventions, and the method TabSet.addTab() is likewise
// discovered as the correct method to add a Tab to a TabSet.
//
// @param newNode (EditNode) new node to be added
// @param parentNode (EditNode) parent to add the new node under
// @param index (integer) index within the parent's children array
// @visibility devTools
//<
addNode : function (newNode, parentNode, index, parentProperty, skipParentComponentAdd) {
return this.addComponent(newNode, parentNode, index, parentProperty, skipParentComponentAdd);
},
addComponent : function (newNode, parentNode, index, parentProperty, skipParentComponentAdd) {
if (parentNode == null) parentNode = this.getDefaultParent(newNode);
var liveParent = this.getLiveObject(parentNode);
this.logInfo("addComponent will add newNode of type: " + newNode.type +
" to: " + this.echoLeaf(liveParent), "editing");
if (liveParent.wrapChildNode) {
parentNode = liveParent.wrapChildNode(this, newNode, parentNode, index);
if (!parentNode) return;
liveParent = this.getLiveObject(parentNode);
}
// find what field in the parent can accommodate a child of this type (prefer the
// passed-in name over a looked-up one, so the user can override in the case of
// multiple valid parent fields)
var fieldName = parentProperty || isc.DS.getObjectField(liveParent, newNode.type),
field = isc.DS.getSchemaField(liveParent, fieldName);
if (!field) {
this.logWarn("can't addComponent: can't find a field in parent: " + liveParent +
" for a new child of type: " + newNode.type + ", newNode is: " +
this.echo(newNode));
return;
}
// for a singular field (eg listGrid.dataSource), remove the old node first
if (!field.multiple) {
var existingChild = isc.DS.getChildObject(liveParent, newNode.type, parentProperty);
if (existingChild) {
var existingChildNode =
this.data.getChildren(parentNode).find("ID", isc.DS.getAutoId(existingChild));
this.logWarn("destroying existing child: " + this.echoLeaf(existingChild) +
" in singular field: " + fieldName);
this.data.remove(existingChildNode);
if (isc.isA.Class(existingChild) &&
!isc.isA.DataSource(existingChild)) existingChild.destroy();
}
}
// NOTE: generated components and remove/add cycles: some widgets convert config
// objects into live objects (eg formItem properties to live FormItem, tab -> ImgTab,
// section -> SectionHeader, etc). When we are doing an add/remove cycle for these
// kinds of generated objects:
// - rebuild based on initData, rather than trying to re-add the liveObject, which will
// be a generated component that the parent will have destroyed
// - preserve Canvas children of the generated component, such as tab.pane,
// section.items, which have not been added to the initData. We do this by using
// part of the serialization logic (addChildData)
// - ensure removal of the tab, item, or section does not destroy these Canvas children
// (a special flag is passed to at least TabSets to avoid this)
// Optimization for add/remove cycles: check for methods like "reorderMember" first.
// Note this doesn't remove the complexity discussed above because a generated
// component might be moved between two parents.
var childObject;
if (newNode.generatedType) {
// copy to avoid property scribbling that is currently done by TabSets and
// SectionStacks at least
childObject = isc.addProperties({}, newNode.initData);
this.addChildData(childObject, this.data.getChildren(newNode));
} else {
childObject = newNode.liveObject;
}
if (!skipParentComponentAdd) {
var result = isc.DS.addChildObject(liveParent, newNode.type, childObject, index,
parentProperty);
if (!result) {
this.logWarn("addChildObject failed, returning");
return;
}
}
// fetch the liveObject back from the parent to handle it's possible conversion from
// just properties to a live instance.
// NOTE: fetch object by ID, not index, since on a reorder when a node is dropped after
// itself the index is one too high
newNode.liveObject = isc.DS.getChildObject(liveParent, newNode.type,
isc.DS.getAutoId(newNode.initData), parentProperty);
this.logDebug("for new node: " + this.echoLeaf(newNode) +
" liveObject is now: " + this.echoLeaf(newNode.liveObject),
"editing");
if (newNode.liveObject == null) {
this.logWarn("wasn't able to retrieve live object after adding node of type: " +
newNode.type + " to liveParent: " + liveParent +
", does liveParent have an appropriate getter() method?");
}
// add the node representing the component to the project tree
this.data.add(newNode, parentNode, index);
// gets rid of the spurious opener icon that appears because all nodes are regarded as
// folders and dropped node is unloaded, hence might have children
this.data.openFolder(newNode);
this.logInfo("added node " + this.echoLeaf(newNode) +
" to EditTree at path: " + this.getData().getPath(newNode) +
" with live object: " + this.echoLeaf(newNode.liveObject), "editing");
this.selection.selectSingle(newNode);
if (this.autoShowParents) this.showParents(newNode);
// Flip it into edit mode depending on the setting on the VB instance
if (this.creator.editingOn) this.enableEditing(newNode);
// Call hook in case the live object wants to know about being added
if (newNode.liveObject.addedToEditContext) newNode.liveObject.addedToEditContext(this, newNode, parentNode, index);
return newNode;
},
// for a node being added without a parent, find a plausible default node to add to.
// In combination with palette.defaultEditContext, allows double-click (tree, list
// palettes) as an alternative to drag and drop.
getDefaultParent : function (newNode, returnNullIfNoSuitableParent) {
// rules:
// Start with the selected node. We select on drop / create, so this is typically
// the last added node, but the user can select something else to take control of
// where the double-click add goes
// If this node accepts this type as a child, use that.
// - handles most layout nesting, DataSource for last form, etc
// Otherwise, go up hierarchy from this node
// - handles a series of components that should not nest being placed adjacent instead,
// eg ListGrid then DynamicForm
var type = newNode.className || newNode.type,
node = this.getSelectedRecord();
while (node && !this.canAddToParent(node, type)) node = this.data.getParent(node);
var root = this.data.getRoot()
if (returnNullIfNoSuitableParent) {
if (!node && this.canAddToParent(root, type)) return root;
return node;
}
return node || root;
},
// alternative to just using node.liveObject
// exists because forms used to rebuild *all* items when any single item is added, hence
// making the liveObject stale for siblings of an added item
getLiveObject : function (node) {
var parentNode = this.data.getParent(node);
// at root, just use the cached liveObject (a formItem can never be at root)
if (parentNode == null) return node.liveObject;
var liveParent = parentNode.liveObject;
var liveObject = isc.DS.getChildObject(liveParent, node.type, isc.DS.getAutoId(node));
if (liveObject) node.liveObject = liveObject;
return node.liveObject;
},
// remove all editNodes in the tree (does not destroy live objects, just removes nodes
// from tree)
removeAll : function () {
var rootChildren = this.data.getChildren(this.data.getRoot()).duplicate()
for (var i = 0; i < rootChildren.length; i++) {
this.removeComponent(rootChildren[i]);
}
},
// destroy all editNodes in the tree and their liveObjects
destroyAll : function () {
var rootChildren = this.data.getChildren(this.data.getRoot()).duplicate()
for (var i = 0; i < rootChildren.length; i++) {
this.destroyComponent(rootChildren[i]);
}
},
// remove an editNode from the tree
removeNode : function (editNode, skipLiveRemoval) {
return this.removeComponent(editNode, skipLiveRemoval);
},
removeComponent : function (editNode, skipLiveRemoval) { // old name
// remove the node from the tree
this.data.remove(editNode);
if (skipLiveRemoval) return;
// remove the corresponding component from the object model
var parentNode = this.data.getParent(editNode),
liveParent = this.getLiveObject(parentNode),
liveChild = this.getLiveObject(editNode);
//this.logWarn("removing with initData: " + this.echo(editNode.initData));
isc.DS.removeChildObject(liveParent, editNode.type, liveChild);
},
// destroy an editNode in the tree, including it's liveObject
destroyNode : function (editNode) {
return this.destroyComponent(editNode);
},
destroyComponent : function (editNode) { // old name
var liveObject = this.getLiveObject(editNode);
this.removeNode(editNode);
// if it has a destroy function, call it. Otherwise we assume garbage collection will
// work
if (liveObject.destroy) liveObject.destroy();
},
// give a newNode, ensure all of it's parents are visible
showParents : function (newNode) {
// if something is dropped under a tab, ensure that tab gets selected
var parents = this.data.getParents(newNode),
tabNodes = parents.findAll("type", "Tab");
//this.logWarn("detected tab parents: " + tabNodes);
if (tabNodes) {
for (var i = 0; i < tabNodes.length; i++) {
var tabNode = tabNodes[i],
tabSetNode = this.data.getParent(tabNode),
tab = this.getLiveObject(tabNode),
tabSet = this.getLiveObject(tabSetNode);
tabSet.selectTab(tab);
}
}
},
// Serializing
// --------------------------------------------------------------------------------------------
// Take a tree of editNodes and produce a data structure that can be serialized to produce
// actual XML or JSON source code
// we flatten the Tree of objects into a flat list of top-level items to serialize.
// Nesting (eg grid within Layout) is accomplished by having the Layout refer to the grid's
// ID.
serializeComponents : function (serverless, includeRoot) {
var nodes = includeRoot ?
[this.data.root] : this.data.getChildren(this.data.root).duplicate();
return this.serializeEditNodes(nodes, serverless);
},
// NOTE: the "nodes" passed to this function need to be part of the Tree that's available
// as this.data. TODO: generalized this so that it takes a Tree, optional nodes, and
// various mode flags like serverless.
serializeEditNodes : function (nodes, serverless) {
// add autoDraw to all non-hidden top-level components
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i] = isc.addProperties({}, nodes[i]),
iscClass = isc.ClassFactory.getClass(node.type),
initData = node.initData = isc.addProperties({}, node.initData);
//this.logWarn("considering node: " + this.echo(topNode) +
// " with initData: " + this.echo(initData));
if (iscClass && iscClass.isA("Canvas") && initData &&
initData.visibility != isc.Canvas.HIDDEN && initData.autoDraw !== false)
{
initData.autoDraw = true;
}
}
// if serverless is set we will actually output DataSources in their entirety.
// Otherwise, we'll just output a special tag that causes the DataSource to be loaded
// as the server processes the XML format.
this.serverless = serverless;
this.initDataBlocks = [];
this.map("getSerializeableTree", nodes);
this.serverless = null;
var xmlCode = isc.EditContext.serializeInitData(this.initDataBlocks);
return xmlCode;
},
// arrange the initialization data into a structure suitable for XML serialization. This
// will:
// - grab just the initData portion of each editNode (what we serialize)
// - flatten hierarchies: all Canvas-derived components will be at top-level,
// members/children arrays will contain references to these children
// - ensure DataSources are only listed once since multiple components may refer to the
// same DataSource
getSerializeableTree : function (node, dontAddGlobally) {
var type = node.type,
// copy initData for possible modification
initData = isc.addProperties({}, node.initData);
// if this node is a DataSource (or subclass of DataSource)
var classObj = isc.ClassFactory.getClass(type);
this.logInfo("node: " + this.echoLeaf(node) + " with type: " + type);
if (classObj && classObj.isA("DataSource")) {
// check for this same DataSource already being saved out
if (this.initDataBlocks) {
var existingDS = this.initDataBlocks.find("ID", initData.ID) ||
this.initDataBlocks.find("loadID", initData.ID);
if (existingDS && existingDS.$schemaId == "DataSource") return;
}
if (!this.serverless) {
// when serializing a DataSource, just output the loadID tag so that the
// server outputs the full definition during XML processing on JSP load
initData = { _constructor:"DataSource",
$schemaId : "DataSource",
loadID : initData.ID };
} else {
// if running serverless, we can't rely on the server to fetch the definition
// as part of XML processing during JSP load, so we have to write out a full
// definition. This works only for DataSources that don't require the server
// to fetch and update data.
// NOTE: since all DataSources in Visual Builder are always saved to the
// server, an alternative approach would be to load the DataSource and capture
// its initData, as we do when we edit an existing DataSource. However we
// would still depend on getSerializeableFields() being correct, as we also use
// it to obtain clean data when we begin editing a dynamically created
// DataSource obtained from XML Schema (eg SFDataSource)
var liveDS = node.liveObject;
initData = liveDS.getSerializeableFields();
initData._constructor = liveDS.Class;
initData.$schemaId = "DataSource";
}
}
// Actions
// By default these will be defined as simple objects in JS, but for saving in XML
// we need to enclose them in ... tags
// (ensures that any specified mappings are rendered out as an array)
// Catch these cases and store as a StringMethod object rather than the raw action
// object - this will serialize correctly.
this.convertActions(node, initData, classObj);
var treeChildren = this.data.getChildren(node);
if (!treeChildren) {
if (this.initDataBlocks) this.initDataBlocks.add(initData); // add as a top-level node
return;
}
this.addChildData(initData, treeChildren);
// if we're not supposed to be global, return out initData
if (dontAddGlobally) return initData;
// otherwise add this node's data globally (we list top-most parents last)
if (this.initDataBlocks) this.initDataBlocks.add(initData);
},
addChildData : function (parentData, childNodes) {
var ds = isc.DS.get(parentData._constructor);
for (var i = 0; i < childNodes.length; i++) {
var child = childNodes[i],
childType = child.initData._constructor,
// copy initData for possible modification
childData = isc.addProperties({}, child.initData),
parentFieldName = childData.parentProperty || ds.getObjectField(childType),
parentField = ds.getField(parentFieldName);
this.logInfo("serializing: child of type: " + childType +
" goes in parent field: " + parentFieldName,
"editing");
// all Canvii output individually, and their parents just output the Canvas ID.
// NOTE: don't do this for _generated components, which include TabSet tabs and
// SectionStack sections.
if ((isc.isA.Canvas(child.liveObject) && !child.liveObject._generated) ||
isc.isA.DataSource(child.liveObject))
{
childData = "ref:" + childData.ID;
this.getSerializeableTree(child);
} else {
// otherwise, serialize this child without adding it globally
childData = this.getSerializeableTree(child, true);
}
var existingValue = parentData[parentFieldName];
if (parentField.multiple) {
// force multiple fields to Arrays
if (!existingValue) existingValue = parentData[parentFieldName] = [];
existingValue.add(childData);
} else {
parentData[parentFieldName] = childData;
}
}
},
convertActions : function (node, initData, classObj) {
// Convert actions defined as a raw object to StringMethods so they can be
// serialized correctly.
// This is a bit of a pain to achieve as there's nothing in the component's initData
// that makes it clear that this is a StringMethod object rather than some other
// simple object and there are no dataSource field definitions for most stringMethods
// - We could examine the registered stringMethod for the class, but this wouldn't
// work for non instance object fields, such as stringMethods on ListGridFields
// - We could just examine the object - if it's a valid format (has target, name attrs)
// we could assume it's an action - but this would catch false positives in some
// cases
// For now - look at the value on the live-instance and see if it's a function produced
// from an Action (check for function.iscAction).
// This will work as long as the live-object has actually been instantiated (may not be
// a valid test in all cases - EG anything that's lazily created on draw or when called
// may not yet have converted it to a function).
for (var field in initData) {
var value = initData[field];
// if it's not an object or is already a StringMethod no need to convert to one
if (!isc.isAn.Object(value) || isc.isA.StringMethod(value)) continue;
// If it has a specified field-type, other than StringMethod - we don't need
// to convert
// Note: type Action doesn't need conversion to a StringMethod as when we serialize
// to XML, the ActionDataSource will do the right thing
var fieldType;
if (classObj.getField) fieldType = classObj.getField(field).type;
if (fieldType && (fieldType != "StringMethod")) continue;
var liveValue = node.liveObject[field],
liveAction = liveValue ? liveValue.iscAction : null,
convertToSM
;
if (liveAction) convertToSM = true;
/*
// We could add a sanity check that the value will convert to a function successfully
// in case a function has been added since init or something.
try {
isc.Func.expressionToFunction("", initData[field]);
} catch (e) {
convertToSM = false;
}
*/
if (convertToSM) initData[field] = isc.StringMethod.create({value:value});
}
// no need to return anything we've modified the initData object directly.
}
});
//> @groupDef visualBuilder
// The SmartClient Visual Builder tool, accessible from the SDK Explorer as Tools->Visual
// Builder, is intended for:
//
// - business analysts and others doing functional application design, who want to create
// functional prototypes in a codeless, "what you see is what you get" environment
//
- developers new to SmartClient who want to get a basic familiarity with component
// layout, component properties and SmartClient code structure
//
- developers building simple applications that can be completed entirely within Visual
// Builder
//
//
//
Using Visual Builder
//
// Basic usage instructions are embedded in Visual Builder itself, in the "About Visual
// Builder" pane. Click on it to open it.
//
// Visual Builder for Functional Design
//
// Visual Builder has several advantages over other tools typically used for functional design:
//
// - Visual Builder allows simple drag and drop manipulation of components, form-based
// editing of component properties, and simple connection of events to actions - all without
// requiring any code to be written. It is actually simpler to use than
// DreamWeaver or other code-oriented prototyping tools
//
- because Visual Builder generates clean code, designs will not have to be converted to
// another technology before development can proceed. This reduces both effort and the
// potential for miscommunication
//
- developers can add custom skinning, components with custom behaviors, and custom
// DataSources with sample datasets to Visual Builder so that the design environment is an even
// closer match to the final application. This helps eliminate many types of unimplementable
// designs
//
- because Visual Builder is built in SmartClient itself, Visual Builder is simply a
// web page, and does not require installation. Visual Builder can be deployed to
// an internal network to allow teams with a mixture of technical and semi-technical
// users to collaboratively build and share prototypes of SmartClient-based applications.
//
//
// Loading and Saving
//
// The "File" menu within Visual Builder allows screens to be saved and reloaded for further
// editing. Saved screens can be edited outside of Visual Builder and successfully
// reloaded, however, as with any design tool that provides a drag and drop, dialog-driven
// approach to screen creation, Visual Builder cannot work with entirely free-form code. In
// particular, when a screen is loaded and then re-saved:
//
// - any indenting or spacing changes are not preserved
//
- order of property or method definitions will revert to Visual Builder's default
//
- while method definitions on components are preserved, any code outside of
// component definitions will be dropped (in some cases adding such code will cause
// loading to fail)
//
- each Canvas-based component will be output separately, in the order these components
// appear in the project tree, deepest first
//
// Generally speaking, screen definitions that you edit within Visual Builder should consist of
// purely declarative code. Rather than appearing in screen definitions, custom components and
// JavaScript libraries should be added to Visual Builder itself via the customization
// facilities described below.
//
//
Installing Visual Builder
//
// Visual Builder comes already installed and working in the SDK, and can be used from there out
// of the box. This is the simplest thing to do during initial prototyping.
//
// Further on in the development cycle, it may be advantageous to have Visual Builder available
// outside the SDK, for example in your test environment. Installing Visual Builder into
// such an environment is very easy:
//
// - Perform a normal installation procedure, as discussed +link{group:iscInstall,here}
// - Copy the following .jar files from the SDK
lib
folder to the target
// WEB-INF/lib
folder:
//
// isomorphic_tools.jar
// isomorphic_sql.jar
// isomorphic_hibernate.jar
//
// - Copy the SDK
tools
folder to the target application root
// - Add the following line to the end of
WEB-INF/server.properties
:
//
// FilesystemDataSource.enabled: true
//
//
//
// Note that it is safe to include Visual Builder even in a production environment, so long
// as you ensure that the tools
folder is protected with any normal HTTP
// authentication/authorization mechanism - for example, an authentication filter.
//
//
Customizing Visual Builder
//
// The rest of this topic focuses on how Visual Builder can be customized and deployed by
// developers to make it more effective as a functional design tool for a particular
// organization.
//
// Adding Custom DataSources to Visual Builder
//
// DataSources placed in the project dataSources directory ([webroot]/shared/ds by default)
// will be detected by Visual Builder whenever it is started, and appear in the DataSource
// listing in the lower right-hand corner automatically.
//
// If you have created a custom subclass of DataSource (eg, as a base class for several
// DataSources that contact the same web service), you can use it with Visual Builder by:
//
// - creating an XML version of the DataSource using the XML tag <DataSource> and the
//
constructor
property set to the name of your custom DataSource subclass (as
// described +link{group:componentXML} under the heading Custom Components)
// - modifying [webroot]/tools/visualBuilder/globalDependencies.xml to load the JavaScript
// code for your custom DataSource class. See examples in that file.
//
//
// Adding Custom Components to Visual Builder
//
// The Component Library on the right hand side of Visual Builder loads component definitions
// from two XML files in the [webroot]/tools/visualBuilder directory: customComponents.xml and
// defaultComponents.xml. customComponents.xml is empty and is intended for developers to add
// their own components. defaultComponents.xml can also be customized, but the base version
// will change between SmartClient releases.
//
// As can be seen by looking at defaultComponents.xml, components are specified using a tree
// structure similar to that shown in the
// +explorerExample{treeLoadXML,tree XML loading example}. The properties that can be set on
// nodes are:
//
// -
className
: name of the SmartClient Class on which +link{Class.create,create()} will be
// called in order to construct the component. className
can be omitted to create
// a folder that cannot be dropped
// -
title
: title for the node
// -
defaults
: an Object specifying defaults to be passed to
// +link{Class.create,create()}.
// For example, you could add an "EditableGrid" node by using className:"ListGrid"
// and specifying:
//
// <defaults canEdit="true"/>
// NOTE: if you set any defaults that are not Canvas properties, you need to provide explicit
// type as documented under Custom Properties for +link{group:componentXML}.
// -
children
: components that should appear as children in the tree under this
// node
// -
icon
: icon to show in the Visual Builder component tree (if desired)
// -
iconWidth/Height/Size
: dimensions of the icon in pixels ("iconSize" sets
// both)
// -
showDropIcon
: for components that allow children, whether to show a
// special drop icon on valid drop (like +link{treeGrid.showDropIcons}).
//
//
// In order to use custom classes in Visual Builder, you must modify
// [webroot]/tools/visualBuilder/globalDependencies.xml
to include:
//
// - the JavaScript class definition for the custom class (in other words, the
// +link{classMethod:isc.defineClass(),defineClass()} call)
//
- a +link{group:componentSchema,component schema} for the custom component
//
// See globalDependencies.xml for examples.
//
//
Component Schema and Visual Builder
//
// When you provide +link{group:componentSchema,custom schema} for a component, Visual Builder
// uses that schema to drive component editing (Component Properties pane) and to drive drag
// and drop screen building functionality.
//
// Component Editing
//
// Newly declared fields will appear in the Component Editor in the "Other" category at the
// bottom by default. You can create your own category by simply setting field.group to the
// name of a new group and using this on multiple custom fields.
//
// The ComponentEditor will pick a FormItem for a custom field by the
// +link{type:FormItemType,same rules} used for ordinary databinding, including the ability to
// set field.editorType to use a custom FormItem.
//
// When the "Apply" button is clicked, Visual Builder will look for an appropriate "setter
// function" for the custom field, for example, for a field named "myProp", Visual Builder will
// look for "setMyProp". The target component will also be +link{canvas.redraw,redrawn}.
//
// Event -> Action Bindings
//
// The Component Properties pane contains an Events tab that allows you wire components events
// to actions on any other component currently in the project.
//
// Events are simply +link{group:stringMethods,StringMethods} defined on the component. In
// order to be considered events, method definitions must have been added to the class via
// +link{Class.registerStringMethods} and either be publicly documented SmartClient methods or,
// for custom classes, have a methods definition in the +link{group:componentSchema,component
// schema}.
// Examples of events are: +link{listGrid.recordClick} and +link{dynamicForm.itemChange}.
//
// Actions are methods on any component that have a method definition in the
// +link{group:componentSchema,component schema} and specify action="true".
//
// All available events (stringMethods) on a component are shown in the Events tab of the
// Component Editor. Clicking the plus (+) sign next to the event name brings up a menu that
// shows a list of all components currently in the project and their available actions.
// Selecting an action from this submenu binds the action to the selected event. When an event
// is bound to an action in this manner, automatic type matching is performed to pass arguments
// from the event to the action as follows:
//
// - Only non-optional parameters of the action are bound.
//
- For each non-optional parameter of the action method, every parameter of the
// event method is inspected in order to either directly match the type (for non-object types)
// or to match an isAssignableFrom type check via a SmartClient schema inheritance check.
//
- The 'type' of a parameter is determined from the type documented in the SmartClient
// reference for built-in components, or from the
type
attribute on the method
// param in the +link{group:componentSchema,component schema} definition of a custom component.
// - When a matching parameter is found, it is assigned to the current slot of the action and
// not considered for further parameter matching.
//
- The above pattern is repeated until all non-optional parameters are exhausted, all
// event parameters are exhausted, or until no further type matches can be inferred.
//
// The "actionBinding" log category can be enabled in the Developer Console to troubleshoot
// issues with automatic binding for custom methods.
//
// Component Drag and Drop
//
// Visual Builder uses component schema to determine whether a given drop is allowed and what
// methods should be called to accomplish the drop. For example, any Canvas-based component
// can be dropped on a VLayout because VLayout has a "members" field of type "Canvas", and an
// +link{Layout.addMember,addMember()} function.
//
// Because of these rules, any subclass of Canvas will be automatically eligible to be dropped
// into any container that accepts a Canvas (eg, a Layout or Tab). Any subclass of a FormItem
// will be, likewise, automatically eligible to be dropped into a DynamicForm.
//
// You can declare custom containment relations, such as a custom class "Wizard" that accepts
// instances of the custom class "Pane" by simply declaring a
// +link{group:componentSchema,component schema} that says that Wizard has a property called
// "panes" of type "Pane". Then, provide methods that allow components to be added and removed:
//
// - for a +link{dataSourceField.multiple,multiple} field, provide "add" and "remove"
// functions based on the name of the field. For example, for a field "panes" of type "Pane",
// provide "addPane()" that takes a Pane instance, and "removePane()" that takes a pane
// instance or pane ID
//
- for a singular field (such as +link{Canvas.contextMenu} or +link{Tab.pane}), provide a
// setter method named after the field (eg setContextMenu()) that takes either an instance of
// the component or null for removal
//
//
// The "editing" log category can be enabled in the Developer Console to troubleshoot issues
// with schema-driven drag and drop and automatic lookup of getter/setter and adder/remover
// methods.
//
// NOTE: after modifying component schema, it may be necessary to restart the servlet
// engine and reload Visual Builder
//
// Presenting simplified components
//
// SmartClient components expose many methods and properties. For some environments, it is
// more appropriate to provide a simplified list of properties, events, and actions on either
// built-in SmartClient components or your custom components. This can be done by providing a
// custom +link{group:componentSchema,component schema} for an existing component that exposes
// your minimal set. You also need to provide a trivial subclass of the class you're exposing
// so that it can be instantiated.
//
// For example, let's say you want to make a simplified button called EButton that exposes only
// the 'title' property and the 'click' event of a standard Button. The following steps will
// accomplish this:
//
// 1. Edit /tools/visualBuilder/customComponents.xml and add a block similar to the following
// to make your custom component appear in the Component Library:
//
// <PaletteNode>
// <title>EButton</title>
// <className>EButton</className>
// <icon>button.gif</icon>
// </PaletteNode>
//
// 2. Next, create a custom schema: /isomorphic/system/schema/EButton.ds.xml as follows:
//
// <DataSource ID="EButton" inheritsFrom="Button" Constructor="EButton"
// showLocalFieldsOnly="true" showSuperClassActions="false"
// showSuperClassEvents="false">
// <fields>
// <field name="title" type="HTML"/>
// </fields>
// <methods>
// <method name="click">
// <description>Fires when this button is clicked.</description>
// </method>
// </methods>
// </DataSource>
//
// See documentation above and also +link{group:componentSchema,component schema} for what the
// properties above do.
// 3. Finally, you'll need to define an EButton class as a simple subclass of Button, as
// follows:
//
// isc.defineClass("EButton", "Button");
//
// To make sure that the Visual Builder will load the above definition, you'll need to place it
// into a JavaScript file being loaded by the Visual Builder. If you do not already have
// such a file, you can create one and add it to the list of Visual Builder dependencies by
// adding an entry in /tools/visualBuilder/globalDependencies.xml. See examples in that file
// for specifics.
//
//
Deploying Visual Builder for Functional Designers
//
// The normal +link{group:iscInstall} deployment instructions apply to Visual Builder except
// that the "BuiltinRPCs", which are configured via server.properties, must be enabled
// in order for Visual Builder to load and save files to the SmartClient server. This also
// means that Visual Builder should only be deployed within trusted environments.
//
// Note that the Visual Builder provides a "live" interface to the provided DataSources. In
// other words, if a DataSource supports saving and a designer enables inline editing in a grid,
// real saves will be initiated. The Visual Builder tool should be configured to use the same
// sample data that developers use during development.
//
//
// @treeLocation Concepts
// @title Visual Builder
// @visibility external
//<
} // end if (isc.TreeGrid)
// -----------------------------------------------------------------------------------------
// DynamicForm.rolloverControls
// INCOMPLETE IMPLEMENTATION - commented out for now
/*
isc.DynamicForm.addProperties({
rolloverControlsLayoutDefaults: [],
rolloverControls: []
});
isc.DynamicForm.addMethods({
showRolloverControls : function (item) {
var controls = this.getRolloverControls(item),
layout = this.rolloverControlsLayout;
layout.item = item;
layout.setPageLeft();
layout.moveTo(item.getPageLeft()+item.getPageWidth(), item.getPageTop());
},
hideRolloverControls : function (item) {
this.rolloverControlsLayout.hide();
},
getRolloverControls : function (item) {
if (!this.rolloverControlsLayout) {
this.createRolloverControls(item);
}
return this.rolloverControls;
},
createRolloverControls : function (item) {
this.addAutoChild("rolloverControlsLayout");
this.createRolloverControls(item);
}
});
*/
// This is a marker class for FormItem drag-and-drop in edit mode. We use an instance of
// this class (for efficiency, we just keep one cached against the EditContext class) so
// that the DnD code knows we're really dragging a FormItem, which will be present on this
// proxy canvas as property "formItem".
isc.ClassFactory.defineClass("FormItemProxyCanvas", "Canvas").addProperties({
autoDraw: false,
canDrop: true,
setFormItem : function (formItem) {
this.formItem = formItem;
this.syncWithFormItemPosition();
this.sendToBack();
this.show();
},
syncWithFormItemPosition : function () {
if (!this.formItem || !this.formItem.form) return; // formItem not yet part of a form?
this.setPageLeft(this.formItem.getPageLeft());
this.setPageTop(this.formItem.getPageTop());
this.setWidth(this.formItem.getVisibleWidth());
this.setHeight(this.formItem.getVisibleHeight());
}
});