You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
625 lines
21 KiB
625 lines
21 KiB
dojo.provide("dojox.data.ItemExplorer"); |
|
dojo.require("dijit.Tree"); |
|
dojo.require("dijit.Dialog"); |
|
dojo.require("dijit.Menu"); |
|
dojo.require("dijit.form.ValidationTextBox"); |
|
dojo.require("dijit.form.Textarea"); |
|
dojo.require("dijit.form.Button"); |
|
dojo.require("dijit.form.RadioButton"); |
|
dojo.require("dijit.form.FilteringSelect"); |
|
|
|
(function(){ |
|
var getValue = function(store, item, prop){ |
|
var value = store.getValues(item, prop); |
|
if(value.length < 2){ |
|
value = store.getValue(item, prop); |
|
} |
|
return value; |
|
} |
|
|
|
dojo.declare("dojox.data.ItemExplorer", dijit.Tree, { |
|
useSelect: false, |
|
refSelectSearchAttr: null, |
|
constructor: function(options){ |
|
dojo.mixin(this, options); |
|
var self = this; |
|
var initialRootValue = {}; |
|
var root = (this.rootModelNode = {value:initialRootValue,id:"root"}); |
|
|
|
this._modelNodeIdMap = {}; |
|
this._modelNodePropMap = {}; |
|
var nextId = 1; |
|
this.model = { |
|
getRoot: function(onItem){ |
|
onItem(root); |
|
}, |
|
mayHaveChildren: function(modelNode){ |
|
return modelNode.value && typeof modelNode.value == 'object' && !(modelNode.value instanceof Date); |
|
}, |
|
getChildren: function(parentModelNode, onComplete, onError){ |
|
var keys, parent, item = parentModelNode.value; |
|
var children = []; |
|
if(item == initialRootValue){ |
|
onComplete([]); |
|
return; |
|
} |
|
var isItem = self.store && self.store.isItem(item, true); |
|
if(isItem && !self.store.isItemLoaded(item)){ |
|
// if it is not loaded, do so now. |
|
self.store.loadItem({ |
|
item:item, |
|
onItem:function(loadedItem){ |
|
item = loadedItem; |
|
enumerate(); |
|
} |
|
}); |
|
}else{ |
|
enumerate(); |
|
} |
|
function enumerate(){ |
|
// once loaded, enumerate the keys |
|
if(isItem){ |
|
// get the properties through the dojo data API |
|
keys = self.store.getAttributes(item); |
|
parent = item; |
|
}else if(item && typeof item == 'object'){ |
|
parent = parentModelNode.value; |
|
keys = []; |
|
// also we want to be able to drill down into plain JS objects/arrays |
|
for(var i in item){ |
|
if(item.hasOwnProperty(i) && i != '__id' && i != '__clientId'){ |
|
keys.push(i); |
|
} |
|
} |
|
} |
|
if(keys){ |
|
for(var key, k=0; key = keys[k++];){ |
|
children.push({ |
|
property:key, |
|
value: isItem ? getValue(self.store, item, key) : item[key], |
|
parent: parent}); |
|
} |
|
children.push({addNew:true, parent: parent, parentNode : parentModelNode}); |
|
} |
|
onComplete(children); |
|
} |
|
}, |
|
getIdentity: function(modelNode){ |
|
if(!modelNode.id){ |
|
if(modelNode.addNew){ |
|
modelNode.property = "--addNew"; |
|
} |
|
modelNode.id = nextId++; |
|
if(self.store){ |
|
if(self.store.isItem(modelNode.value)){ |
|
var identity = self.store.getIdentity(modelNode.value); |
|
(self._modelNodeIdMap[identity] = self._modelNodeIdMap[identity] || []).push(modelNode); |
|
} |
|
if(modelNode.parent){ |
|
identity = self.store.getIdentity(modelNode.parent) + '.' + modelNode.property; |
|
(self._modelNodePropMap[identity] = self._modelNodePropMap[identity] || []).push(modelNode); |
|
} |
|
} |
|
} |
|
return modelNode.id; |
|
}, |
|
getLabel: function(modelNode){ |
|
return modelNode === root ? |
|
"Object Properties" : |
|
modelNode.addNew ? (modelNode.parent instanceof Array ? "Add new value" : "Add new property") : |
|
modelNode.property + ": " + |
|
(modelNode.value instanceof Array ? "(" + modelNode.value.length + " elements)" : modelNode.value); |
|
}, |
|
onChildrenChange: function(modelNode){ |
|
}, |
|
onChange: function(modelNode){ |
|
} |
|
}; |
|
}, |
|
postCreate: function(){ |
|
this.inherited(arguments); |
|
// handle the clicking on the "add new property item" |
|
dojo.connect(this, "onClick", function(modelNode, treeNode){ |
|
this.lastFocused = treeNode; |
|
if(modelNode.addNew){ |
|
//this.focusNode(treeNode.getParent()); |
|
this._addProperty(); |
|
}else{ |
|
this._editProperty(); |
|
} |
|
}); |
|
var contextMenu = new dijit.Menu({ |
|
targetNodeIds: [this.rootNode.domNode], |
|
id: "contextMenu" |
|
}); |
|
dojo.connect(contextMenu, "_openMyself", this, function(e){ |
|
var node = dijit.getEnclosingWidget(e.target); |
|
if(node){ |
|
var item = node.item; |
|
if(this.store.isItem(item.value, true) && !item.parent){ |
|
dojo.forEach(contextMenu.getChildren(), function(widget){ |
|
widget.attr("disabled", (widget.label != "Add")); |
|
}); |
|
this.lastFocused = node; |
|
// TODO: Root Node - allow Edit when mutli-value editing is possible |
|
}else if(item.value && typeof item.value == 'object' && !(item.value instanceof Date)){ |
|
// an object that's not a Date - could be a store item |
|
dojo.forEach(contextMenu.getChildren(), function(widget){ |
|
widget.attr("disabled", (widget.label != "Add") && (widget.label != "Delete")); |
|
}); |
|
this.lastFocused = node; |
|
// TODO: Object - allow Edit when mutli-value editing is possible |
|
}else if(item.property && dojo.indexOf(this.store.getIdentityAttributes(), item.property) >= 0){ // id node |
|
this.focusNode(node); |
|
alert("Cannot modify an Identifier node."); |
|
}else if(item.addNew){ |
|
this.focusNode(node); |
|
}else{ |
|
dojo.forEach(contextMenu.getChildren(), function(widget){ |
|
widget.attr("disabled", (widget.label != "Edit") && (widget.label != "Delete")); |
|
}) |
|
// this won't focus the node but gives us a way to reference the node |
|
this.lastFocused = node; |
|
} |
|
} |
|
}); |
|
contextMenu.addChild(new dijit.MenuItem({label: "Add", onClick: dojo.hitch(this, "_addProperty")})); |
|
contextMenu.addChild(new dijit.MenuItem({label: "Edit", onClick: dojo.hitch(this, "_editProperty")})); |
|
contextMenu.addChild(new dijit.MenuItem({label: "Delete", onClick: dojo.hitch(this, "_destroyProperty")})); |
|
contextMenu.startup(); |
|
}, |
|
store: null, |
|
setStore: function(store){ |
|
this.store = store; |
|
var self = this; |
|
if(this._editDialog){ |
|
this._editDialog.destroyRecursive(); |
|
delete this._editDialog; |
|
} |
|
// i think we should just destroy this._editDialog and let _createEditDialog take care of everything |
|
// once it gets called again by either _editProperty or _addProperty - it will create everything again |
|
// using the new store. this way we don't need to keep track of what is in the dialog if we change it. |
|
/*if(this._editDialog && this.useSelect){ |
|
dojo.query(".reference [widgetId]", this._editDialog.containerNode).forEach(function(node){ |
|
dijit.getEnclosingWidget(node).attr("store", store); |
|
}); |
|
}*/ |
|
dojo.connect(store, "onSet", function(item, attribute, oldValue, newValue){ |
|
var nodes, i, identity = self.store.getIdentity(item); |
|
nodes = self._modelNodeIdMap[identity]; |
|
|
|
if(nodes && |
|
(oldValue === undefined || newValue === undefined || |
|
oldValue instanceof Array || newValue instanceof Array || typeof oldValue == 'object' || typeof newValue == 'object')){ |
|
for(i = 0; i < nodes.length; i++){ |
|
(function(node){ |
|
self.model.getChildren(node, function(children){ |
|
self.model.onChildrenChange(node, children); |
|
}); |
|
})(nodes[i]); |
|
} |
|
} |
|
nodes = self._modelNodePropMap[identity + "." + attribute]; |
|
|
|
if(nodes){ |
|
for(i = 0; i < nodes.length; i++){ |
|
nodes[i].value = newValue; |
|
self.model.onChange(nodes[i]); |
|
} |
|
} |
|
}); |
|
this.rootNode.setChildItems([]); |
|
}, |
|
setItem: function(item){ |
|
// this is called to show a different item |
|
|
|
// reset the maps, for the root getIdentity is not called, so we pre-initialize it here |
|
(this._modelNodeIdMap = {})[this.store.getIdentity(item)] = [this.rootModelNode]; |
|
this._modelNodePropMap = {}; |
|
|
|
this.rootModelNode.value = item; |
|
var self = this; |
|
this.model.getChildren(this.rootModelNode, function(children){ |
|
self.rootNode.setChildItems(children); |
|
}); |
|
|
|
}, |
|
refreshItem: function(){ |
|
this.setItem(this.rootModelNode.value); |
|
}, |
|
_createEditDialog: function(){ |
|
this._editDialog = new dijit.Dialog({ |
|
title: "Edit Property", |
|
execute: dojo.hitch(this, "_updateItem"), |
|
preload: true |
|
}); |
|
this._editDialog.placeAt(dojo.body()); |
|
this._editDialog.startup(); |
|
|
|
// handle for dialog content |
|
var pane = dojo.doc.createElement('div'); |
|
|
|
// label for property |
|
var labelProp = dojo.doc.createElement('label'); |
|
dojo.attr(labelProp, "for", "property"); |
|
dojo.style(labelProp, "fontWeight", "bold"); |
|
dojo.attr(labelProp, "innerHTML", "Property:") |
|
pane.appendChild(labelProp); |
|
|
|
// property name field |
|
var propName = new dijit.form.ValidationTextBox({ |
|
name: "property", |
|
value: "", |
|
required: true, |
|
disabled: true |
|
}).placeAt(pane); |
|
|
|
pane.appendChild(dojo.doc.createElement("br")); |
|
pane.appendChild(dojo.doc.createElement("br")); |
|
|
|
// radio button for "value" |
|
var value = new dijit.form.RadioButton({ |
|
name: "itemType", |
|
value: "value", |
|
onClick: dojo.hitch(this, function(){this._enableFields("value");}) |
|
}).placeAt(pane); |
|
|
|
// label for value |
|
var labelVal = dojo.doc.createElement('label'); |
|
dojo.attr(labelVal, "for", "value"); |
|
dojo.attr(labelVal, "innerHTML", "Value (JSON):") |
|
pane.appendChild(labelVal); |
|
|
|
// container for value fields |
|
var valueDiv = dojo.doc.createElement("div"); |
|
dojo.addClass(valueDiv, "value"); |
|
|
|
// textarea |
|
var textarea = new dijit.form.Textarea({ |
|
name: "jsonVal" |
|
}).placeAt(valueDiv); |
|
pane.appendChild(valueDiv); |
|
|
|
// radio button for "reference" |
|
var reference = new dijit.form.RadioButton({ |
|
name: "itemType", |
|
value: "reference", |
|
onClick: dojo.hitch(this, function(){this._enableFields("reference");}) |
|
}).placeAt(pane); |
|
|
|
// label for reference |
|
var labelRef = dojo.doc.createElement('label'); |
|
dojo.attr(labelRef, "for", "_reference"); |
|
dojo.attr(labelRef, "innerHTML", "Reference (ID):") |
|
pane.appendChild(labelRef); |
|
pane.appendChild(dojo.doc.createElement("br")); |
|
|
|
// container for reference fields |
|
var refDiv = dojo.doc.createElement("div"); |
|
dojo.addClass(refDiv, "reference"); |
|
|
|
if(this.useSelect){ |
|
// filteringselect |
|
// TODO: see if there is a way to sort the items in this list |
|
var refSelect = new dijit.form.FilteringSelect({ |
|
name: "_reference", |
|
store: this.store, |
|
searchAttr: this.refSelectSearchAttr || this.store.getIdentityAttributes()[0], |
|
required: false, |
|
value: null, // need to file a ticket about the fetch that happens when declared with value: null |
|
pageSize: 10 |
|
}).placeAt(refDiv); |
|
}else{ |
|
var refTextbox = new dijit.form.ValidationTextBox({ |
|
name: "_reference", |
|
value: "", |
|
promptMessage: "Enter the ID of the item to reference", |
|
isValid: dojo.hitch(this, function(isFocused){ |
|
// don't validate while it's focused |
|
return true;//isFocused || this.store.getItemByIdentity(this._editDialog.attr("value")._reference); |
|
}) |
|
}).placeAt(refDiv); |
|
} |
|
pane.appendChild(refDiv); |
|
pane.appendChild(dojo.doc.createElement("br")); |
|
pane.appendChild(dojo.doc.createElement("br")); |
|
|
|
// buttons |
|
var buttons = document.createElement('div'); |
|
buttons.setAttribute("dir", "rtl"); |
|
var cancelButton = new dijit.form.Button({type: "reset", label: "Cancel"}).placeAt(buttons); |
|
cancelButton.onClick = dojo.hitch(this._editDialog, "onCancel"); |
|
var okButton = new dijit.form.Button({type: "submit", label: "OK"}).placeAt(buttons); |
|
pane.appendChild(buttons); |
|
|
|
this._editDialog.attr("content", pane); |
|
}, |
|
_enableFields: function(selection){ |
|
// enables/disables fields based on whether the value in this._editDialog is a reference or a primitive value |
|
switch(selection){ |
|
case "reference": |
|
dojo.query(".value [widgetId]", this._editDialog.containerNode).forEach(function(node){ |
|
dijit.getEnclosingWidget(node).attr("disabled", true); |
|
}); |
|
dojo.query(".reference [widgetId]", this._editDialog.containerNode).forEach(function(node){ |
|
dijit.getEnclosingWidget(node).attr("disabled", false); |
|
}); |
|
break; |
|
case "value": |
|
dojo.query(".value [widgetId]", this._editDialog.containerNode).forEach(function(node){ |
|
dijit.getEnclosingWidget(node).attr("disabled", false); |
|
}); |
|
dojo.query(".reference [widgetId]", this._editDialog.containerNode).forEach(function(node){ |
|
dijit.getEnclosingWidget(node).attr("disabled", true); |
|
}); |
|
break; |
|
} |
|
}, |
|
_updateItem: function(vals){ |
|
// a single "execute" function that handles adding and editing of values and references. |
|
var node, item, val, storeItemVal, editingItem = this._editDialog.attr("title") == "Edit Property"; |
|
var editDialog = this._editDialog; |
|
var store = this.store; |
|
function setValue(){ |
|
try{ |
|
var itemVal, propPath = []; |
|
var prop = vals.property; |
|
if(editingItem){ |
|
while(!store.isItem(item.parent, true)){ |
|
node = node.getParent(); |
|
propPath.push(item.property); |
|
item = node.item; |
|
} |
|
if(propPath.length == 0){ |
|
// working with an item attribute already |
|
store.setValue(item.parent, item.property, val); |
|
}else{ |
|
// need to walk back down the item property to the object |
|
storeItemVal = getValue(store, item.parent, item.property); |
|
if(storeItemVal instanceof Array){ |
|
// create a copy for modification |
|
storeItemVal = storeItemVal.concat(); |
|
} |
|
itemVal = storeItemVal; |
|
while(propPath.length > 1){ |
|
itemVal = itemVal[propPath.pop()]; |
|
} |
|
itemVal[propPath] = val; // this change is reflected in storeItemVal as well |
|
store.setValue(item.parent, item.property, storeItemVal); |
|
} |
|
}else{ |
|
// adding a property |
|
if(store.isItem(value, true)){ |
|
// adding a top-level property to an item |
|
if(!store.isItemLoaded(value)){ |
|
// fetch the value and see if it is an array |
|
store.loadItem({ |
|
item: value, |
|
onItem: function(loadedItem){ |
|
if(loadedItem instanceof Array){ |
|
prop = loadedItem.length; |
|
} |
|
store.setValue(loadedItem, prop, val); |
|
} |
|
}); |
|
}else{ |
|
if(value instanceof Array){ |
|
prop = value.length; |
|
} |
|
store.setValue(value, prop, val); |
|
} |
|
}else{ |
|
// adding a property to a lower level in an item |
|
if(item.value instanceof Array){ |
|
propPath.push(item.value.length); |
|
}else{ |
|
propPath.push(vals.property); |
|
} |
|
while(!store.isItem(item.parent, true)){ |
|
node = node.getParent(); |
|
propPath.push(item.property); |
|
item = node.item; |
|
} |
|
storeItemVal = getValue(store, item.parent, item.property); |
|
itemVal = storeItemVal; |
|
while(propPath.length > 1){ |
|
itemVal = itemVal[propPath.pop()]; |
|
} |
|
itemVal[propPath] = val; |
|
store.setValue(item.parent, item.property, storeItemVal); |
|
} |
|
} |
|
}catch(e){ |
|
alert(e); |
|
} |
|
} |
|
|
|
if(editDialog.validate()){ |
|
node = this.lastFocused; |
|
item = node.item; |
|
var value = item.value; |
|
// var property = null; |
|
if(item.addNew){ |
|
// we are adding a property to the parent item |
|
// the real value of the parent is in the parent property of the lastFocused item |
|
// this.lastFocused.getParent().item.value may be a reference to an item |
|
value = node.item.parent; |
|
node = node.getParent(); |
|
item = node.item; |
|
} |
|
val = null; |
|
switch(vals.itemType){ |
|
case "reference": |
|
this.store.fetchItemByIdentity({identity:vals._reference, |
|
onItem:function(item){ |
|
val = item; |
|
setValue(); |
|
}, |
|
onError:function(){ |
|
alert("The id could not be found"); |
|
} |
|
}); |
|
break; |
|
case "value": |
|
var jsonVal = vals.jsonVal; |
|
val = dojo.fromJson(jsonVal); |
|
// ifit is a function we want to preserve the source (comments, et al) |
|
if(typeof val == 'function'){ |
|
val.toString = function(){ |
|
return jsonVal; |
|
} |
|
} |
|
setValue(); |
|
break; |
|
} |
|
}else{ |
|
// the form didn't validate - show it again. |
|
editDialog.show(); |
|
} |
|
}, |
|
_editProperty: function(){ |
|
// this mixin stops us polluting the tree item with jsonVal etc. |
|
// FIXME: if a store identifies items by instanceof checks, this will fail |
|
var item = dojo.mixin({}, this.lastFocused.item); |
|
// create the dialog or reset it if it already exists |
|
if(!this._editDialog){ |
|
this._createEditDialog(); |
|
}else{ |
|
this._editDialog.reset(); |
|
} |
|
// not allowed to edit an item's id - so check for that and stop it. |
|
if(dojo.indexOf(this.store.getIdentityAttributes(), item.property) >= 0){ |
|
alert("Cannot Edit an Identifier!"); |
|
}else{ |
|
this._editDialog.attr("title", "Edit Property"); |
|
// make sure the property input is disabled |
|
dijit.getEnclosingWidget(dojo.query("input", this._editDialog.containerNode)[0]).attr("disabled", true); |
|
if(this.store.isItem(item.value, true)){ |
|
// root node || Item reference |
|
if(item.parent){ |
|
// Item reference |
|
item.itemType = "reference"; |
|
this._enableFields(item.itemType); |
|
item._reference = this.store.getIdentity(item.value); |
|
this._editDialog.attr("value", item); |
|
this._editDialog.show(); |
|
} // else root node |
|
}else{ |
|
if(item.value && typeof item.value == 'object' && !(item.value instanceof Date)){ |
|
// item.value is an object but it's NOT an item from the store - no-op |
|
// only allow editing on a property not on the node that represents the object/array |
|
}else{ |
|
// this is a primitive |
|
item.itemType = "value"; |
|
this._enableFields(item.itemType); |
|
item.jsonVal = typeof item.value == 'function' ? |
|
// use the plain toString for functions, dojo.toJson doesn't support functions |
|
item.value.toString() : |
|
item.value instanceof Date ? |
|
// A json-ish form of a date: |
|
'new Date("' + item.value + '")' : |
|
dojo.toJson(item.value); |
|
this._editDialog.attr("value", item); |
|
this._editDialog.show(); |
|
} |
|
} |
|
} |
|
}, |
|
_destroyProperty: function(){ |
|
var node = this.lastFocused; |
|
var item = node.item; |
|
var propPath = []; |
|
// we have to walk up the tree to the item before we can know if we're working with the identifier |
|
while(!this.store.isItem(item.parent, true) || item.parent instanceof Array){ |
|
node = node.getParent(); |
|
propPath.push(item.property); |
|
item = node.item; |
|
} |
|
// this will prevent any part of the identifier from being changed |
|
if(dojo.indexOf(this.store.getIdentityAttributes(), item.property) >= 0){ |
|
alert("Cannot Delete an Identifier!"); |
|
}else{ |
|
try{ |
|
if(propPath.length > 0){ |
|
// not deleting a top-level property of an item so get the top-level store item to change |
|
var itemVal, storeItemVal = getValue(this.store, item.parent, item.property); |
|
itemVal = storeItemVal; |
|
// walk back down the object if needed |
|
while(propPath.length > 1){ |
|
itemVal = itemVal[propPath.pop()]; |
|
} |
|
// delete the property |
|
if(dojo.isArray(itemVal)){ |
|
// the value being deleted represents an array element |
|
itemVal.splice(propPath, 1); |
|
}else{ |
|
// object property |
|
delete itemVal[propPath]; |
|
} |
|
// save it back to the store |
|
this.store.setValue(item.parent, item.property, storeItemVal); |
|
}else{ |
|
// deleting an item property |
|
this.store.unsetAttribute(item.parent, item.property); |
|
} |
|
}catch(e){ |
|
alert(e); |
|
} |
|
} |
|
}, |
|
_addProperty: function(){ |
|
// item is what we are adding a property to |
|
var item = this.lastFocused.item; |
|
// value is the real value of the item - not a reference to a store item |
|
var value = item.value; |
|
var showDialog = dojo.hitch(this, function(){ |
|
var property = null; |
|
if(!this._editDialog){ |
|
this._createEditDialog(); |
|
}else{ |
|
this._editDialog.reset(); |
|
} |
|
// are we adding another item to an array? |
|
if(value instanceof Array){ |
|
// preset the property to the next index in the array and disable the property field |
|
property = value.length; |
|
dijit.getEnclosingWidget(dojo.query("input", this._editDialog.containerNode)[0]).attr("disabled", true); |
|
}else{ |
|
// enable the property TextBox |
|
dijit.getEnclosingWidget(dojo.query("input", this._editDialog.containerNode)[0]).attr("disabled", false); |
|
} |
|
this._editDialog.attr("title", "Add Property"); |
|
// default to a value type |
|
this._enableFields("value"); |
|
this._editDialog.attr("value", {itemType: "value", property: property}); |
|
this._editDialog.show(); |
|
}); |
|
|
|
if(item.addNew){ |
|
// we are adding a property to the parent item |
|
item = this.lastFocused.getParent().item; |
|
// the real value of the parent is in the parent property of the lastFocused item |
|
// this.lastFocused.getParent().item.value may be a reference to an item |
|
value = this.lastFocused.item.parent; |
|
} |
|
if(item.property && dojo.indexOf(this.store.getIdentityAttributes(), item.property) >= 0){ |
|
alert("Cannot add properties to an ID node!"); |
|
}else{ |
|
// ifthe value is an item then we need to get the item's value |
|
if(this.store.isItem(value, true) && !this.store.isItemLoaded(value)){ |
|
// fetch the value and see if it is an array |
|
this.store.loadItem({ |
|
item: value, |
|
onItem: function(loadedItem){ |
|
value = loadedItem; |
|
showDialog(); |
|
} |
|
}); |
|
}else{ |
|
showDialog(); |
|
} |
|
} |
|
} |
|
}); |
|
})(); |
|
|
|
|