source: Dev/trunk/src/client/dojox/data/ItemExplorer.js @ 532

Last change on this file since 532 was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

File size: 20.6 KB
Line 
1dojo.provide("dojox.data.ItemExplorer");
2dojo.require("dijit.Tree");
3dojo.require("dijit.Dialog");
4dojo.require("dijit.Menu");
5dojo.require("dijit.form.ValidationTextBox");
6dojo.require("dijit.form.Textarea");
7dojo.require("dijit.form.Button");
8dojo.require("dijit.form.RadioButton");
9dojo.require("dijit.form.FilteringSelect");
10
11(function(){
12        var getValue = function(store, item, prop){
13                var value = store.getValues(item, prop);
14                if(value.length < 2){
15                        value = store.getValue(item, prop);
16                }
17                return value;
18        }
19
20dojo.declare("dojox.data.ItemExplorer", dijit.Tree, {
21        useSelect: false,
22        refSelectSearchAttr: null,
23        constructor: function(options){
24                dojo.mixin(this, options);
25                var self = this;
26                var initialRootValue = {};
27                var root = (this.rootModelNode = {value:initialRootValue,id:"root"});
28
29                this._modelNodeIdMap = {};
30                this._modelNodePropMap = {};
31                var nextId = 1;
32                this.model = {
33                        getRoot: function(onItem){
34                                onItem(root);
35                        },
36                        mayHaveChildren: function(modelNode){
37                                return modelNode.value && typeof modelNode.value == 'object' && !(modelNode.value instanceof Date);
38                        },
39                        getChildren: function(parentModelNode, onComplete, onError){
40                                var keys, parent, item = parentModelNode.value;
41                                var children = [];
42                                if(item == initialRootValue){
43                                        onComplete([]);
44                                        return;
45                                }
46                                var isItem = self.store && self.store.isItem(item, true);
47                                if(isItem && !self.store.isItemLoaded(item)){
48                                        // if it is not loaded, do so now.
49                                        self.store.loadItem({
50                                                item:item,
51                                                onItem:function(loadedItem){
52                                                        item = loadedItem;
53                                                        enumerate();
54                                                }
55                                        });
56                                }else{
57                                        enumerate();
58                                }
59                                function enumerate(){
60                                        // once loaded, enumerate the keys
61                                        if(isItem){
62                                                // get the properties through the dojo data API
63                                                keys = self.store.getAttributes(item);
64                                                parent = item;
65                                        }else if(item && typeof item == 'object'){
66                                                parent = parentModelNode.value;
67                                                keys = [];
68                                                // also we want to be able to drill down into plain JS objects/arrays
69                                                for(var i in item){
70                                                        if(item.hasOwnProperty(i) && i != '__id' && i != '__clientId'){
71                                                                keys.push(i);
72                                                        }
73                                                }
74                                        }
75                                        if(keys){
76                                                for(var key, k=0; key = keys[k++];){
77                                                        children.push({
78                                                                property:key,
79                                                                value: isItem ? getValue(self.store, item, key) : item[key],
80                                                                parent: parent});
81                                                }
82                                                children.push({addNew:true, parent: parent, parentNode : parentModelNode});
83                                        }
84                                        onComplete(children);
85                                }
86                        },
87                        getIdentity: function(modelNode){
88                                if(!modelNode.id){
89                                        if(modelNode.addNew){
90                                                modelNode.property = "--addNew";
91                                        }
92                                        modelNode.id = nextId++;
93                                        if(self.store){
94                                                if(self.store.isItem(modelNode.value)){
95                                                        var identity = self.store.getIdentity(modelNode.value);
96                                                        (self._modelNodeIdMap[identity] = self._modelNodeIdMap[identity] || []).push(modelNode);
97                                                }
98                                                if(modelNode.parent){
99                                                        identity = self.store.getIdentity(modelNode.parent) + '.' + modelNode.property;
100                                                        (self._modelNodePropMap[identity] = self._modelNodePropMap[identity] || []).push(modelNode);
101                                                }
102                                        }
103                                }
104                                return modelNode.id;
105                        },
106                        getLabel: function(modelNode){
107                                return modelNode === root ?
108                                                "Object Properties" :
109                                                        modelNode.addNew ? (modelNode.parent instanceof Array ? "Add new value" : "Add new property") :
110                                                                modelNode.property + ": " +
111                                                                        (modelNode.value instanceof Array ? "(" + modelNode.value.length + " elements)" : modelNode.value);
112                        },
113                        onChildrenChange: function(modelNode){
114                        },
115                        onChange: function(modelNode){
116                        }
117                };
118        },
119        postCreate: function(){
120                this.inherited(arguments);
121                // handle the clicking on the "add new property item"
122                dojo.connect(this, "onClick", function(modelNode, treeNode){
123                        this.lastFocused = treeNode;
124                        if(modelNode.addNew){
125                                //this.focusNode(treeNode.getParent());
126                                this._addProperty();
127                        }else{
128                                this._editProperty();
129                        }
130                });
131                var contextMenu = new dijit.Menu({
132                        targetNodeIds: [this.rootNode.domNode],
133                        id: "contextMenu"
134                        });
135                dojo.connect(contextMenu, "_openMyself", this, function(e){
136                        var node = dijit.getEnclosingWidget(e.target);
137                        if(node){
138                                var item = node.item;
139                                if(this.store.isItem(item.value, true) && !item.parent){
140                                        dojo.forEach(contextMenu.getChildren(), function(widget){
141                                                widget.attr("disabled", (widget.label != "Add"));
142                                        });
143                                        this.lastFocused = node;
144                                        // TODO: Root Node - allow Edit when mutli-value editing is possible
145                                }else if(item.value && typeof item.value == 'object' && !(item.value instanceof Date)){
146                                        // an object that's not a Date - could be a store item
147                                        dojo.forEach(contextMenu.getChildren(), function(widget){
148                                                widget.attr("disabled", (widget.label != "Add") && (widget.label != "Delete"));
149                                        });
150                                        this.lastFocused = node;
151                                        // TODO: Object - allow Edit when mutli-value editing is possible
152                                }else if(item.property && dojo.indexOf(this.store.getIdentityAttributes(), item.property) >= 0){ // id node
153                                        this.focusNode(node);
154                                        alert("Cannot modify an Identifier node.");
155                                }else if(item.addNew){
156                                        this.focusNode(node);
157                                }else{
158                                        dojo.forEach(contextMenu.getChildren(), function(widget){
159                                                widget.attr("disabled", (widget.label != "Edit") && (widget.label != "Delete"));
160                                        })
161                                        // this won't focus the node but gives us a way to reference the node
162                                        this.lastFocused = node;
163                                }
164                        }
165                });
166                contextMenu.addChild(new dijit.MenuItem({label: "Add", onClick: dojo.hitch(this, "_addProperty")}));
167                contextMenu.addChild(new dijit.MenuItem({label: "Edit", onClick: dojo.hitch(this, "_editProperty")}));
168                contextMenu.addChild(new dijit.MenuItem({label: "Delete", onClick: dojo.hitch(this, "_destroyProperty")}));
169                contextMenu.startup();
170        },
171        store: null,
172        setStore: function(store){
173                this.store = store;
174                var self = this;
175                if(this._editDialog){
176                        this._editDialog.destroyRecursive();
177                        delete this._editDialog;
178                }
179                // i think we should just destroy this._editDialog and let _createEditDialog take care of everything
180                // once it gets called again by either _editProperty or _addProperty - it will create everything again
181                // using the new store. this way we don't need to keep track of what is in the dialog if we change it.
182                /*if(this._editDialog && this.useSelect){
183                        dojo.query(".reference [widgetId]", this._editDialog.containerNode).forEach(function(node){
184                                dijit.getEnclosingWidget(node).attr("store", store);
185                        });
186                }*/
187                dojo.connect(store, "onSet", function(item, attribute, oldValue, newValue){
188                        var nodes, i, identity = self.store.getIdentity(item);
189                        nodes = self._modelNodeIdMap[identity];
190
191                        if(nodes &&
192                                        (oldValue === undefined || newValue === undefined ||
193                                        oldValue instanceof Array || newValue instanceof Array || typeof oldValue == 'object' || typeof newValue == 'object')){
194                                for(i = 0; i < nodes.length; i++){
195                                        (function(node){
196                                                self.model.getChildren(node, function(children){
197                                                        self.model.onChildrenChange(node, children);
198                                                });
199                                        })(nodes[i]);
200                                }
201                        }
202                        nodes = self._modelNodePropMap[identity + "." + attribute];
203
204                        if(nodes){
205                                for(i = 0; i < nodes.length; i++){
206                                        nodes[i].value = newValue;
207                                        self.model.onChange(nodes[i]);
208                                }
209                        }
210                });
211                this.rootNode.setChildItems([]);
212        },
213        setItem: function(item){
214                // this is called to show a different item
215
216                // reset the maps, for the root getIdentity is not called, so we pre-initialize it here
217                (this._modelNodeIdMap = {})[this.store.getIdentity(item)] = [this.rootModelNode];
218                this._modelNodePropMap = {};
219
220                this.rootModelNode.value = item;
221                var self = this;
222                this.model.getChildren(this.rootModelNode, function(children){
223                        self.rootNode.setChildItems(children);
224                });
225
226        },
227        refreshItem: function(){
228                this.setItem(this.rootModelNode.value);
229        },
230        _createEditDialog: function(){
231                this._editDialog = new dijit.Dialog({
232                        title: "Edit Property",
233                        execute: dojo.hitch(this, "_updateItem"),
234                        preload: true
235                });
236                this._editDialog.placeAt(dojo.body());
237                this._editDialog.startup();
238
239                // handle for dialog content
240                var pane = dojo.doc.createElement('div');
241
242                // label for property
243                var labelProp = dojo.doc.createElement('label');
244                dojo.attr(labelProp, "for", "property");
245                dojo.style(labelProp, "fontWeight", "bold");
246                dojo.attr(labelProp, "innerHTML", "Property:")
247                pane.appendChild(labelProp);
248
249                // property name field
250                var propName = new dijit.form.ValidationTextBox({
251                        name: "property",
252                        value: "",
253                        required: true,
254                        disabled: true
255                }).placeAt(pane);
256
257                pane.appendChild(dojo.doc.createElement("br"));
258                pane.appendChild(dojo.doc.createElement("br"));
259
260                // radio button for "value"
261                var value = new dijit.form.RadioButton({
262                        name: "itemType",
263                        value: "value",
264                        onClick: dojo.hitch(this, function(){this._enableFields("value");})
265                }).placeAt(pane);
266
267                // label for value
268                var labelVal = dojo.doc.createElement('label');
269                dojo.attr(labelVal, "for", "value");
270                dojo.attr(labelVal, "innerHTML", "Value (JSON):")
271                pane.appendChild(labelVal);
272
273                // container for value fields
274                var valueDiv = dojo.doc.createElement("div");
275                dojo.addClass(valueDiv, "value");
276
277                // textarea
278                var textarea = new dijit.form.Textarea({
279                        name: "jsonVal"
280                }).placeAt(valueDiv);
281                pane.appendChild(valueDiv);
282
283                // radio button for "reference"
284                var reference = new dijit.form.RadioButton({
285                        name: "itemType",
286                        value: "reference",
287                        onClick: dojo.hitch(this, function(){this._enableFields("reference");})
288                }).placeAt(pane);
289
290                // label for reference
291                var labelRef = dojo.doc.createElement('label');
292                dojo.attr(labelRef, "for", "_reference");
293                dojo.attr(labelRef, "innerHTML", "Reference (ID):")
294                pane.appendChild(labelRef);
295                pane.appendChild(dojo.doc.createElement("br"));
296
297                // container for reference fields
298                var refDiv = dojo.doc.createElement("div");
299                dojo.addClass(refDiv, "reference");
300
301                if(this.useSelect){
302                        // filteringselect
303                        // TODO: see if there is a way to sort the items in this list
304                        var refSelect = new dijit.form.FilteringSelect({
305                                name: "_reference",
306                                store: this.store,
307                                searchAttr: this.refSelectSearchAttr || this.store.getIdentityAttributes()[0],
308                                required: false,
309                                value: null,            // need to file a ticket about the fetch that happens when declared with value: null
310                                pageSize: 10
311                        }).placeAt(refDiv);
312                }else{
313                        var refTextbox = new dijit.form.ValidationTextBox({
314                                name: "_reference",
315                                value: "",
316                                promptMessage: "Enter the ID of the item to reference",
317                                isValid: dojo.hitch(this, function(isFocused){
318                                        // don't validate while it's focused
319                                        return true;//isFocused || this.store.getItemByIdentity(this._editDialog.attr("value")._reference);
320                                })
321                        }).placeAt(refDiv);
322                }
323                pane.appendChild(refDiv);
324                pane.appendChild(dojo.doc.createElement("br"));
325                pane.appendChild(dojo.doc.createElement("br"));
326
327                // buttons
328                var buttons = document.createElement('div');
329                buttons.setAttribute("dir", "rtl");
330                var cancelButton = new dijit.form.Button({type: "reset", label: "Cancel"}).placeAt(buttons);
331                cancelButton.onClick = dojo.hitch(this._editDialog, "onCancel");
332                var okButton = new dijit.form.Button({type: "submit", label: "OK"}).placeAt(buttons);
333                pane.appendChild(buttons);
334
335                this._editDialog.attr("content", pane);
336        },
337        _enableFields: function(selection){
338                // enables/disables fields based on whether the value in this._editDialog is a reference or a primitive value
339                switch(selection){
340                        case "reference":
341                                dojo.query(".value [widgetId]", this._editDialog.containerNode).forEach(function(node){
342                                        dijit.getEnclosingWidget(node).attr("disabled", true);
343                                });
344                                dojo.query(".reference [widgetId]", this._editDialog.containerNode).forEach(function(node){
345                                        dijit.getEnclosingWidget(node).attr("disabled", false);
346                                });
347                                break;
348                        case "value":
349                                dojo.query(".value [widgetId]", this._editDialog.containerNode).forEach(function(node){
350                                        dijit.getEnclosingWidget(node).attr("disabled", false);
351                                });
352                                dojo.query(".reference [widgetId]", this._editDialog.containerNode).forEach(function(node){
353                                        dijit.getEnclosingWidget(node).attr("disabled", true);
354                                });
355                                break;
356                }
357        },
358        _updateItem: function(vals){
359                // a single "execute" function that handles adding and editing of values and references.
360                var node, item, val, storeItemVal, editingItem = this._editDialog.attr("title") == "Edit Property";
361                var editDialog = this._editDialog;
362                var store = this.store;
363                function setValue(){
364                        try{
365                                var itemVal, propPath = [];
366                                var prop = vals.property;
367                                if(editingItem){
368                                        while(!store.isItem(item.parent, true)){
369                                                node = node.getParent();
370                                                propPath.push(item.property);
371                                                item = node.item;
372                                        }
373                                        if(propPath.length == 0){
374                                                // working with an item attribute already
375                                                store.setValue(item.parent, item.property, val);
376                                        }else{
377                                                // need to walk back down the item property to the object
378                                                storeItemVal = getValue(store, item.parent, item.property);
379                                                if(storeItemVal instanceof Array){
380                                                        // create a copy for modification
381                                                        storeItemVal = storeItemVal.concat();
382                                                }
383                                                itemVal = storeItemVal;
384                                                while(propPath.length > 1){
385                                                        itemVal = itemVal[propPath.pop()];
386                                                }
387                                                itemVal[propPath] = val; // this change is reflected in storeItemVal as well
388                                                store.setValue(item.parent, item.property, storeItemVal);
389                                        }
390                                }else{
391                                        // adding a property
392                                        if(store.isItem(value, true)){
393                                                // adding a top-level property to an item
394                                                if(!store.isItemLoaded(value)){
395                                                        // fetch the value and see if it is an array
396                                                        store.loadItem({
397                                                                item: value,
398                                                                onItem: function(loadedItem){
399                                                                        if(loadedItem instanceof Array){
400                                                                                prop = loadedItem.length;
401                                                                        }
402                                                                        store.setValue(loadedItem, prop, val);
403                                                                }
404                                                        });
405                                                }else{
406                                                        if(value instanceof Array){
407                                                                prop = value.length;
408                                                        }
409                                                        store.setValue(value, prop, val);
410                                                }
411                                        }else{
412                                                // adding a property to a lower level in an item
413                                                if(item.value instanceof Array){
414                                                        propPath.push(item.value.length);
415                                                }else{
416                                                        propPath.push(vals.property);
417                                                }
418                                                while(!store.isItem(item.parent, true)){
419                                                        node = node.getParent();
420                                                        propPath.push(item.property);
421                                                        item = node.item;
422                                                }
423                                                storeItemVal = getValue(store, item.parent, item.property);
424                                                itemVal = storeItemVal;
425                                                while(propPath.length > 1){
426                                                        itemVal = itemVal[propPath.pop()];
427                                                }
428                                                itemVal[propPath] = val;
429                                                store.setValue(item.parent, item.property, storeItemVal);
430                                        }
431                                }
432                        }catch(e){
433                                alert(e);
434                        }
435                }
436
437                if(editDialog.validate()){
438                        node = this.lastFocused;
439                        item = node.item;
440                        var value = item.value;
441                        // var property = null;
442                        if(item.addNew){
443                                // we are adding a property to the parent item
444                                // the real value of the parent is in the parent property of the lastFocused item
445                                // this.lastFocused.getParent().item.value may be a reference to an item
446                                value = node.item.parent;
447                                node = node.getParent();
448                                item = node.item;
449                        }
450                        val = null;
451                        switch(vals.itemType){
452                                case "reference":
453                                        this.store.fetchItemByIdentity({identity:vals._reference,
454                                                onItem:function(item){
455                                                        val = item;
456                                                        setValue();
457                                                },
458                                                onError:function(){
459                                                        alert("The id could not be found");
460                                                }
461                                        });
462                                        break;
463                                case "value":
464                                        var jsonVal = vals.jsonVal;
465                                        val = dojo.fromJson(jsonVal);
466                                        // ifit is a function we want to preserve the source (comments, et al)
467                                        if(typeof val == 'function'){
468                                                val.toString = function(){
469                                                        return jsonVal;
470                                                }
471                                        }
472                                        setValue();
473                                        break;
474                        }
475                }else{
476                        // the form didn't validate - show it again.
477                        editDialog.show();
478                }
479        },
480        _editProperty: function(){
481                // this mixin stops us polluting the tree item with jsonVal etc.
482                // FIXME: if a store identifies items by instanceof checks, this will fail
483                var item = dojo.mixin({}, this.lastFocused.item);
484                // create the dialog or reset it if it already exists
485                if(!this._editDialog){
486                        this._createEditDialog();
487                }else{
488                        this._editDialog.reset();
489                }
490                // not allowed to edit an item's id - so check for that and stop it.
491                if(dojo.indexOf(this.store.getIdentityAttributes(), item.property) >= 0){
492                        alert("Cannot Edit an Identifier!");
493                }else{
494                        this._editDialog.attr("title", "Edit Property");
495                        // make sure the property input is disabled
496                        dijit.getEnclosingWidget(dojo.query("input", this._editDialog.containerNode)[0]).attr("disabled", true);
497                        if(this.store.isItem(item.value, true)){
498                                // root node || Item reference
499                                if(item.parent){
500                                        // Item reference
501                                        item.itemType = "reference";
502                                        this._enableFields(item.itemType);
503                                        item._reference = this.store.getIdentity(item.value);
504                                        this._editDialog.attr("value", item);
505                                        this._editDialog.show();
506                                } // else root node
507                        }else{
508                                if(item.value && typeof item.value == 'object' && !(item.value instanceof Date)){
509                                        // item.value is an object but it's NOT an item from the store - no-op
510                                        // only allow editing on a property not on the node that represents the object/array
511                                }else{
512                                        // this is a primitive
513                                        item.itemType = "value";
514                                        this._enableFields(item.itemType);
515                                        item.jsonVal = typeof item.value == 'function' ?
516                                                        // use the plain toString for functions, dojo.toJson doesn't support functions
517                                                        item.value.toString() :
518                                                                item.value instanceof Date ?
519                                                                        // A json-ish form of a date:
520                                                                        'new Date("' + item.value + '")' :
521                                                                        dojo.toJson(item.value);
522                                        this._editDialog.attr("value", item);
523                                        this._editDialog.show();
524                                }
525                        }
526                }
527        },
528        _destroyProperty: function(){
529                var node = this.lastFocused;
530                var item = node.item;
531                var propPath = [];
532                // we have to walk up the tree to the item before we can know if we're working with the identifier
533                while(!this.store.isItem(item.parent, true) || item.parent instanceof Array){
534                        node = node.getParent();
535                        propPath.push(item.property);
536                        item = node.item;
537                }
538                // this will prevent any part of the identifier from being changed
539                if(dojo.indexOf(this.store.getIdentityAttributes(), item.property) >= 0){
540                        alert("Cannot Delete an Identifier!");
541                }else{
542                        try{
543                                if(propPath.length > 0){
544                                        // not deleting a top-level property of an item so get the top-level store item to change
545                                        var itemVal, storeItemVal = getValue(this.store, item.parent, item.property);
546                                        itemVal = storeItemVal;
547                                        // walk back down the object if needed
548                                        while(propPath.length > 1){
549                                                itemVal = itemVal[propPath.pop()];
550                                        }
551                                        // delete the property
552                                        if(dojo.isArray(itemVal)){
553                                                // the value being deleted represents an array element
554                                                itemVal.splice(propPath, 1);
555                                        }else{
556                                                // object property
557                                                delete itemVal[propPath];
558                                        }
559                                        // save it back to the store
560                                        this.store.setValue(item.parent, item.property, storeItemVal);
561                                }else{
562                                        // deleting an item property
563                                        this.store.unsetAttribute(item.parent, item.property);
564                                }
565                        }catch(e){
566                                alert(e);
567                        }
568                }
569        },
570        _addProperty: function(){
571                // item is what we are adding a property to
572                var item = this.lastFocused.item;
573                // value is the real value of the item - not a reference to a store item
574                var value = item.value;
575                var showDialog = dojo.hitch(this, function(){
576                        var property = null;
577                        if(!this._editDialog){
578                                this._createEditDialog();
579                        }else{
580                                this._editDialog.reset();
581                        }
582                        // are we adding another item to an array?
583                        if(value instanceof Array){
584                                // preset the property to the next index in the array and disable the property field
585                                property = value.length;
586                                dijit.getEnclosingWidget(dojo.query("input", this._editDialog.containerNode)[0]).attr("disabled", true);
587                        }else{
588                                // enable the property TextBox
589                                dijit.getEnclosingWidget(dojo.query("input", this._editDialog.containerNode)[0]).attr("disabled", false);
590                        }
591                        this._editDialog.attr("title", "Add Property");
592                        // default to a value type
593                        this._enableFields("value");
594                        this._editDialog.attr("value", {itemType: "value", property: property});
595                        this._editDialog.show();
596                });
597
598                if(item.addNew){
599                        // we are adding a property to the parent item
600                        item = this.lastFocused.getParent().item;
601                        // the real value of the parent is in the parent property of the lastFocused item
602                        // this.lastFocused.getParent().item.value may be a reference to an item
603                        value = this.lastFocused.item.parent;
604                }
605                if(item.property && dojo.indexOf(this.store.getIdentityAttributes(), item.property) >= 0){
606                        alert("Cannot add properties to an ID node!");
607                }else{
608                        // ifthe value is an item then we need to get the item's value
609                        if(this.store.isItem(value, true) && !this.store.isItemLoaded(value)){
610                                // fetch the value and see if it is an array
611                                this.store.loadItem({
612                                        item: value,
613                                        onItem: function(loadedItem){
614                                                value = loadedItem;
615                                                showDialog();
616                                        }
617                                });
618                        }else{
619                                showDialog();
620                        }
621                }
622        }
623});
624})();
625
Note: See TracBrowser for help on using the repository browser.