source: Dev/branches/rest-dojo-ui/client/dojox/data/ItemExplorer.js @ 256

Last change on this file since 256 was 256, checked in by hendrikvanantwerpen, 13 years ago

Reworked project structure based on REST interaction and Dojo library. As
soon as this is stable, the old jQueryUI branch can be removed (it's
kept for reference).

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})();
626
Note: See TracBrowser for help on using the repository browser.