source: Dev/trunk/src/client/dojox/atom/widget/FeedViewer.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: 21.0 KB
Line 
1define([
2        "dojo/_base/kernel",
3        "dojo/_base/lang",
4        "dojo/_base/array",
5        "dojo/_base/connect",
6        "dojo/_base/declare",
7        "dojo/dom-class",
8        "dijit/_Widget",
9        "dijit/_Templated",
10        "dijit/_Container",
11        "../io/Connection",
12        "dojo/text!./templates/FeedViewer.html",
13        "dojo/text!./templates/FeedViewerEntry.html",
14        "dojo/text!./templates/FeedViewerGrouping.html",
15        "dojo/i18n!./nls/FeedViewerEntry"
16], function (dojo, lang, arrayUtil, connect, declare, domClass, _Widget, _Templated, _Container, Connection, template, entryTemplate, groupingTemplate, i18nViewer) {
17dojo.experimental("dojox.atom.widget.FeedViewer");
18
19var FeedViewer = declare("dojox.atom.widget.FeedViewer", [_Widget, _Templated, _Container],{
20        // summary:
21        //              An ATOM feed viewer that allows for viewing a feed, deleting entries, and editing entries.
22
23        feedViewerTableBody: null,      //The body of the feed viewer table so we can access it and populate it.  Will be assigned via template.
24        feedViewerTable: null,          //The overal table container which contains the feed viewer table.  Will be assigned via template.
25        entrySelectionTopic: "",        //The topic to broadcast when any entry is clicked so that a listener can pick up it and display it.
26        url: "",                                        //The URL to which to connect to initially on creation.
27        xmethod: false,
28        localSaveOnly: false,
29
30        //Templates for the HTML rendering.  Need to figure these out better, admittedly.
31        templateString: template,
32
33        _feed: null,
34        _currentSelection: null, // Currently selected entry
35
36        _includeFilters: null,
37
38        alertsEnabled: false,
39
40        postCreate: function(){
41                // summary:
42                //              The postCreate function.
43                // description:
44                //              The postCreate function.  Creates our AtomIO object for future interactions and subscribes to the
45                //              event given in markup/creation.
46                this._includeFilters = [];
47
48                if(this.entrySelectionTopic !== ""){
49                        this._subscriptions = [dojo.subscribe(this.entrySelectionTopic, this, "_handleEvent")];
50                }
51                this.atomIO = new Connection();
52                this.childWidgets = [];
53        },
54       
55        startup: function(){
56                // summary:
57                //              The startup function.
58                // description:
59                //              The startup function.  Parses the filters and sets the feed based on the given url.
60                this.containerNode = this.feedViewerTableBody;
61                var children = this.getDescendants();
62                for(var i in children){
63                        var child = children[i];
64                        if(child && child.isFilter){
65                                this._includeFilters.push(new FeedViewer.CategoryIncludeFilter(child.scheme, child.term, child.label));
66                                child.destroy();
67                        }
68                }
69               
70                if(this.url !== ""){
71                        this.setFeedFromUrl(this.url);
72                }
73        },
74
75        clear: function(){
76                // summary:
77                //              Function clearing all current entries in the feed view.
78                // returns:
79                //              Nothing.
80                this.destroyDescendants();
81        },
82
83        setFeedFromUrl: function(/*string*/url){
84                // summary:
85                //              Function setting the feed from a URL which to get the feed.
86                // url:
87                //              The URL to the feed to load.
88                // returns:
89                //              Nothing.
90                if(url !== ""){
91                        if(this._isRelativeURL(url)){
92                                var baseUrl = "";
93                                if(url.charAt(0) !== '/'){
94                                        baseUrl = this._calculateBaseURL(window.location.href, true);
95                                }else{
96                                        baseUrl = this._calculateBaseURL(window.location.href, false);
97                                }
98                                this.url = baseUrl + url;
99                        }
100
101                        this.atomIO.getFeed(url,lang.hitch(this,this.setFeed));
102                }
103        },
104
105
106        setFeed: function(/*object*/feed){
107                // summary:
108                //              Function setting the dojox.atom.io.model.Feed data into the view.
109                // entry:
110                //              The dojox.atom.io.model.Feed object to process
111                // returns:
112                //              Nothing.
113                this._feed = feed;
114                this.clear();
115                var entrySorter=function(a,b){
116                        var dispA = this._displayDateForEntry(a);
117                        var dispB = this._displayDateForEntry(b);
118                        if(dispA > dispB){return -1;}
119                        if(dispA < dispB){return 1;}
120                        return 0;
121                };
122
123                // This function may not be safe in different locales.
124                var groupingStr = function(dateStr){
125                        var dpts = dateStr.split(',');
126                       
127                        dpts.pop(); // remove year and time
128                        return dpts.join(",");
129                };
130                var sortedEntries = feed.entries.sort(lang.hitch(this,entrySorter));
131                if(feed){
132                        var lastSectionTitle = null;
133                        for(var i=0;i<sortedEntries.length;i++){
134                               
135                                var entry = sortedEntries[i];
136
137                                if(this._isFilterAccepted(entry)){
138                                        var time = this._displayDateForEntry(entry);
139                                        var sectionTitle = "";
140
141                                        if(time !== null){
142                                                sectionTitle = groupingStr(time.toLocaleString());
143
144                                                if(sectionTitle === "" ){
145                                                        //Generally an issue on Opera with how its toLocaleString() works, so do a quick and dirty date construction M/D/Y
146                                                        sectionTitle = "" + (time.getMonth() + 1) + "/" + time.getDate() + "/" + time.getFullYear();
147                                                }
148                                        }
149                                        if((lastSectionTitle === null) || (lastSectionTitle != sectionTitle)){
150                                                this.appendGrouping(sectionTitle);
151                                                lastSectionTitle = sectionTitle;
152                                        }
153                                        this.appendEntry(entry);
154                                }
155                        }
156                }
157        },
158
159        _displayDateForEntry: function(/*object*/entry){
160                // summary:
161                //              Internal function for determining the appropriate date to display.
162                // entry:
163                //              The dojox.atom.io.model.Entry object to examine.
164                // returns:
165                //              An appropriate date for the feed viewer display.
166                if(entry.updated){return entry.updated;}
167                if(entry.modified){return entry.modified;}
168                if(entry.issued){return entry.issued;}
169                return new Date();
170        },
171
172        appendGrouping: function(/*string*/titleText){
173                // summary:
174                //              Function for appending a new grouping of entries to the feed view.
175                // entry:
176                //              The title of the new grouping to create on the view.
177                // returns:
178                //              Nothing.
179                var entryWidget = new FeedViewerGrouping({});
180                entryWidget.setText(titleText);
181                this.addChild(entryWidget);
182                this.childWidgets.push(entryWidget);
183        },
184
185        appendEntry: function(/*object*/entry){
186                // summary:
187                //              Function for appending an entry to the feed view.
188                // entry:
189                //              The dojox.atom.io.model.Entry object to append
190                // returns:
191                //              Nothing.
192                var entryWidget = new FeedViewerEntry({"xmethod": this.xmethod});
193                entryWidget.setTitle(entry.title.value);
194                entryWidget.setTime(this._displayDateForEntry(entry).toLocaleTimeString());
195                entryWidget.entrySelectionTopic = this.entrySelectionTopic;
196                entryWidget.feed = this;
197                this.addChild(entryWidget);
198                this.childWidgets.push(entryWidget);
199                this.connect(entryWidget, "onClick", "_rowSelected");
200                entry.domNode = entryWidget.entryNode;
201               
202                //Need to set up a bi-directional reference here to control events between the two.
203                entry._entryWidget = entryWidget;
204                entryWidget.entry = entry;
205        },
206       
207        deleteEntry: function(/*object*/entryRow){
208                // summary:
209                //              Function for deleting a row from the view
210                if(!this.localSaveOnly){
211                        this.atomIO.deleteEntry(entryRow.entry, lang.hitch(this, this._removeEntry, entryRow), null, this.xmethod);
212                }else{
213                        this._removeEntry(entryRow, true);
214                }
215                dojo.publish(this.entrySelectionTopic, [{ action: "delete", source: this, entry: entryRow.entry }]);
216        },
217
218        _removeEntry: function(/*FeedViewerEntry*/ entry, /* boolean */success){
219                // summary:
220                //              callback for when an entry is deleted from a feed.
221                if(success){
222                        /* Check if this is the last Entry beneath the given date */
223                        var idx = arrayUtil.indexOf(this.childWidgets, entry);
224                        var before = this.childWidgets[idx-1];
225                        var after = this.childWidgets[idx+1];
226                        if( before.isInstanceOf(widget.FeedViewerGrouping) &&
227                                (after === undefined || after.isInstanceOf(widget.FeedViewerGrouping))){
228                                before.destroy();
229                        }
230                       
231                        /* Destroy the FeedViewerEntry to remove it from the view */
232                        entry.destroy();
233                }else{}
234        },
235       
236        _rowSelected: function(/*object*/evt){
237                // summary:
238                //              Internal function for handling the selection of feed entries.
239                // evt:
240                //              The click event that triggered a selection.
241                // returns:
242                //              Nothing.
243                var selectedNode = evt.target;
244                while(selectedNode){
245                        if(domClass.contains(selectedNode, 'feedViewerEntry')) {
246                                break;
247                        }
248                        selectedNode = selectedNode.parentNode;
249                }
250
251                for(var i=0;i<this._feed.entries.length;i++){
252                        var entry = this._feed.entries[i];
253                        if( (selectedNode === entry.domNode) && (this._currentSelection !== entry) ){
254                                //Found it and it isn't already selected.
255                                domClass.add(entry.domNode, "feedViewerEntrySelected");
256                                domClass.remove(entry._entryWidget.timeNode, "feedViewerEntryUpdated");
257                                domClass.add(entry._entryWidget.timeNode, "feedViewerEntryUpdatedSelected");
258
259                                this.onEntrySelected(entry);
260                                if(this.entrySelectionTopic !== ""){
261                                        dojo.publish(this.entrySelectionTopic, [{ action: "set", source: this, feed: this._feed, entry: entry }]);
262                                }
263                                if(this._isEditable(entry)){
264                                        entry._entryWidget.enableDelete();
265                                }
266
267                                this._deselectCurrentSelection();
268                                this._currentSelection = entry;
269                                break;
270                        }else if( (selectedNode === entry.domNode) && (this._currentSelection === entry) ){
271                                //Found it and it is the current selection, we just want to de-select it so users can 'unselect rows' if they want.
272                                dojo.publish(this.entrySelectionTopic, [{ action: "delete", source: this, entry: entry }]);
273                                this._deselectCurrentSelection();
274                                break;
275                        }
276                }
277        },
278
279        _deselectCurrentSelection: function(){
280                // summary:
281                //              Internal function for unselecting the current selection.
282                // returns:
283                //              Nothing.
284                if(this._currentSelection){
285                        domClass.add(this._currentSelection._entryWidget.timeNode, "feedViewerEntryUpdated");
286                        domClass.remove(this._currentSelection.domNode, "feedViewerEntrySelected");
287                        domClass.remove(this._currentSelection._entryWidget.timeNode, "feedViewerEntryUpdatedSelected");
288                        this._currentSelection._entryWidget.disableDelete();
289                        this._currentSelection = null;
290                }
291        },
292
293
294        _isEditable: function(/*object*/entry){
295                // summary:
296                //              Internal function for determining of a particular entry is editable.
297                // description:
298                //              Internal function for determining of a particular entry is editable.
299                //              This is used for determining if the delete action should be displayed or not.
300                // entry:
301                //              The dojox.atom.io.model.Entry object to examine
302                // returns:
303                //              Boolean denoting if the entry seems editable or not..
304                var retVal = false;
305                if(entry && entry !== null && entry.links && entry.links !== null){
306                        for(var x in entry.links){
307                                if(entry.links[x].rel && entry.links[x].rel == "edit"){
308                                        retVal = true;
309                                        break;
310                                }
311                        }
312                }
313                return retVal;
314        },
315
316        onEntrySelected: function(/*object*/entry){
317                // summary:
318                //              Function intended for over-riding/replacement as an attachpoint to for other items to recieve
319                //              selection notification.
320                // entry:
321                //              The dojox.atom.io.model.Entry object selected.
322                // returns:
323                //              Nothing.
324        },
325
326        _isRelativeURL: function(/*string*/url){
327                // summary:
328                //              Method to determine if the URL is relative or absolute.
329                // description:
330                //              Method to determine if the URL is relative or absolute.  Basic assumption is if it doesn't start
331                //              with http:// or file://, it's relative to the current document.
332                // url:
333                //              The URL to inspect.
334                // returns:
335                //              boolean indicating whether it's a relative url or not.
336                var isFileURL = function(url){
337                        var retVal = false;
338                        if(url.indexOf("file://") === 0){
339                                retVal = true;
340                        }
341                        return retVal;
342                };
343
344                var isHttpURL = function(url){
345                        var retVal = false;
346                        if(url.indexOf("http://") === 0){
347                                retVal = true;
348                        }
349                        return retVal;
350                };
351
352                var retVal = false;
353                if(url !== null){
354                        if(!isFileURL(url) && !isHttpURL(url)){
355                                retVal = true;
356                        }
357                }
358                return retVal;
359        },
360
361        _calculateBaseURL: function(/*string*/fullURL, /*boolean*/currentPageRelative){
362                // summary:
363                //              Internal function to calculate a baseline URL from the provided full URL.
364                // fullURL:
365                //              The full URL as a string.
366                // currentPageRelative:
367                //              Flag to denote of the base URL should be calculated as just the server base, or relative to the current page/location in the URL.
368                // returns:
369                //              String of the baseline URL
370                var baseURL = null;
371                if(fullURL !== null){
372                        //Check to see if we need to strip off any query parameters from the URL.
373                        var index = fullURL.indexOf("?");
374                        if(index != -1){
375                                fullURL = fullURL.substring(0,index);
376                                //console.debug("Removed query parameters.  URL now: " + fullURL);
377                        }
378
379                        if(currentPageRelative){
380                                //Relative to the 'current page' in the URL, so we need to trim that off.
381                                //Now we need to trim if necessary.  If it ends in /, then we don't have a filename to trim off
382                                //so we can return.
383                                index = fullURL.lastIndexOf("/");
384                                if((index > 0) && (index < fullURL.length) && (index !== (fullURL.length -1))){
385                                        //We want to include the terminating /
386                                        baseURL = fullURL.substring(0,(index + 1));
387                                }else{
388                                        baseURL = fullURL;
389                                }
390                        }else{
391                                //We want to find the first occurance of / after the <protocol>://
392                                index = fullURL.indexOf("://");
393                                if(index > 0){
394                                        index = index + 3;
395                                        var protocol = fullURL.substring(0,index);
396                                        var fragmentURL = fullURL.substring(index, fullURL.length);
397                                        index = fragmentURL.indexOf("/");
398                                        if((index < fragmentURL.length) && (index > 0) ){
399                                                baseURL = protocol + fragmentURL.substring(0,index);
400                                        }else{
401                                                baseURL = protocol + fragmentURL;
402                                        }
403                                }
404                        }
405                }
406                return baseURL;
407        },
408
409        _isFilterAccepted: function(/*object*/entry) {
410                // summary:
411                //              Internal function to do matching of category filters to widgets.
412                // returns:
413                //              boolean denoting if this entry matched one of the accept filters.
414                var accepted = false;
415                if (this._includeFilters && (this._includeFilters.length > 0)) {
416                        for (var i = 0; i < this._includeFilters.length; i++) {
417                                var filter = this._includeFilters[i];
418                                if (filter.match(entry)) {
419                                        accepted = true;
420                                        break;
421                                }
422                        }
423                }
424                else {
425                        accepted = true;
426                }
427                return accepted;
428        },
429
430        addCategoryIncludeFilter: function(/*object*/filter) {
431                // summary:
432                //              Function to add a filter for entry inclusion in the feed view.
433                // filter:
434                //              The basic items to filter on and the values.
435                //              Should be of format: {scheme: ``some text or null``, term: ``some text or null``, label: ``some text or null``}
436                // returns:
437                //              Nothing.
438                if (filter) {
439                        var scheme = filter.scheme;
440                        var term = filter.term;
441                        var label = filter.label;
442                        var addIt = true;
443
444                        if (!scheme) {
445                                scheme = null;
446                        }
447                        if (!term) {
448                                scheme = null;
449                        }
450                        if (!label) {
451                                scheme = null;
452                        }
453                       
454                        if (this._includeFilters && this._includeFilters.length > 0) {
455                                for (var i = 0; i < this._includeFilters.length; i++) {
456                                        var eFilter = this._includeFilters[i];
457                                        if ((eFilter.term === term) && (eFilter.scheme === scheme) && (eFilter.label === label)) {
458                                                //Verify we don't have this filter already.
459                                                addIt = false;
460                                                break;
461                                        }
462                                }
463                        }
464
465                        if (addIt) {
466                                this._includeFilters.push(widget.FeedViewer.CategoryIncludeFilter(scheme, term, label));
467                        }
468                }
469        },
470
471        removeCategoryIncludeFilter: function(/*object*/filter) {
472                // summary:
473                //              Function to remove a filter for entry inclusion in the feed view.
474                // filter:
475                //              The basic items to identify the filter that is present.
476                //              Should be of format: {scheme: ``some text or null``, term: ``some text or null``, label: ``some text or null``}
477                // returns:
478                //              Nothing.
479                if (filter) {
480                        var scheme = filter.scheme;
481                        var term = filter.term;
482                        var label = filter.label;
483
484                        if (!scheme) {
485                                scheme = null;
486                        }
487                        if (!term) {
488                                scheme = null;
489                        }
490                        if (!label) {
491                                scheme = null;
492                        }
493                       
494                        var newFilters = [];
495                        if (this._includeFilters && this._includeFilters.length > 0) {
496                                for (var i = 0; i < this._includeFilters.length; i++) {
497                                        var eFilter = this._includeFilters[i];
498                                        if (!((eFilter.term === term) && (eFilter.scheme === scheme) && (eFilter.label === label))) {
499                                                //Keep only filters that do not match
500                                                newFilters.push(eFilter);
501                                        }
502                                }
503                                this._includeFilters = newFilters;
504                        }
505                }
506        },
507
508        _handleEvent: function(/*object*/entrySelectionEvent) {
509                // summary:
510                //              Internal function for listening to a topic that will handle entry notification.
511                // entrySelectionEvent:
512                //              The topic message containing the entry that was selected for view.
513                // returns:
514                //              Nothing.
515                if(entrySelectionEvent.source != this) {
516                        if(entrySelectionEvent.action == "update" && entrySelectionEvent.entry) {
517                var evt = entrySelectionEvent;
518                                if(!this.localSaveOnly){
519                                        this.atomIO.updateEntry(evt.entry, lang.hitch(evt.source,evt.callback), null, true);
520                                }
521                                this._currentSelection._entryWidget.setTime(this._displayDateForEntry(evt.entry).toLocaleTimeString());
522                                this._currentSelection._entryWidget.setTitle(evt.entry.title.value);
523                        } else if(entrySelectionEvent.action == "post" && entrySelectionEvent.entry) {
524                                if(!this.localSaveOnly){
525                                        this.atomIO.addEntry(entrySelectionEvent.entry, this.url, lang.hitch(this,this._addEntry));
526                                }else{
527                                        this._addEntry(entrySelectionEvent.entry);
528                                }
529                        }
530                }
531        },
532       
533        _addEntry: function(/*object*/entry) {
534                // summary:
535                //              callback function used when adding an entry to the feed.
536                // description:
537                //              callback function used when adding an entry to the feed.  After the entry has been posted to the feed,
538                //              we add it to our feed representation (to show it on the page) and publish an event to update any entry viewers.
539                this._feed.addEntry(entry);
540                this.setFeed(this._feed);
541                dojo.publish(this.entrySelectionTopic, [{ action: "set", source: this, feed: this._feed, entry: entry }]);
542        },
543
544        destroy: function(){
545                // summary:
546                //              Destroys this widget, including all descendants and subscriptions.
547                this.clear();
548                arrayUtil.forEach(this._subscriptions, dojo.unsubscribe);
549        }
550});
551
552var FeedViewerEntry = FeedViewer.FeedViewerEntry = declare("dojox.atom.widget.FeedViewerEntry", [_Widget, _Templated],{
553        // summary:
554        //              Widget for handling the display of an entry and specific events associated with it.
555
556        templateString: entryTemplate,
557
558        entryNode: null,
559        timeNode: null,
560        deleteButton: null,
561        entry: null,
562        feed: null,
563
564        postCreate: function(){
565                var _nlsResources = i18nViewer;
566                this.deleteButton.innerHTML = _nlsResources.deleteButton;
567        },
568
569        setTitle: function(/*string*/text){
570                // summary:
571                //              Function to set the title of the entry.
572                // text:
573                //              The title.
574                // returns:
575                //              Nothing.
576                if (this.titleNode.lastChild){this.titleNode.removeChild(this.titleNode.lastChild);}
577               
578                var titleTextNode = document.createElement("div");
579                titleTextNode.innerHTML = text;
580                this.titleNode.appendChild(titleTextNode);
581        },
582
583        setTime: function(/*string*/timeText){
584                // summary:
585                //              Function to set the time of the entry.
586                // timeText:
587                //              The string form of the date.
588                // returns:
589                //              Nothing.
590                if (this.timeNode.lastChild){this.timeNode.removeChild(this.timeNode.lastChild);}
591                var timeTextNode = document.createTextNode(timeText);
592                this.timeNode.appendChild(timeTextNode);
593        },
594
595        enableDelete: function(){
596                // summary:
597                //              Function to enable the delete action on this entry.
598                // returns:
599                //              Nothing.
600                if (this.deleteButton !== null) {
601                        //TODO Fix this
602                        this.deleteButton.style.display = 'inline';
603                }
604        },
605
606        disableDelete: function(){
607                // summary:
608                //              Function to disable the delete action on this entry.
609                // returns:
610                //              Nothing.
611                if (this.deleteButton !== null) {
612                        this.deleteButton.style.display = 'none';
613                }
614        },
615
616        deleteEntry: function(/*object*/event) {
617                // summary:
618                //              Function to handle the delete event and delete the entry.
619                // returns:
620                //              Nothing.
621                event.preventDefault();
622                event.stopPropagation();
623                this.feed.deleteEntry(this);
624        },
625
626        onClick: function(/*object*/e){
627                // summary:
628                //              Attach point for when a row is clicked on.
629                // e:
630                //              The event generated by the click.
631        }
632});
633
634var FeedViewerGrouping = FeedViewer.FeedViewerGrouping = declare("dojox.atom.widget.FeedViewerGrouping", [_Widget, _Templated],{
635        // summary:
636        //              Grouping of feed entries.
637
638        templateString: groupingTemplate,
639       
640        groupingNode: null,
641        titleNode: null,
642
643        setText: function(text){
644                // summary:
645                //              Sets the text to be shown above this grouping.
646                // text:
647                //              The text to show.
648                if (this.titleNode.lastChild){this.titleNode.removeChild(this.titleNode.lastChild);}
649                var textNode = document.createTextNode(text);
650                this.titleNode.appendChild(textNode);
651        }
652});
653
654FeedViewer.AtomEntryCategoryFilter = declare("dojox.atom.widget.AtomEntryCategoryFilter",  null,{
655        // summary:
656        //              A filter to be applied to the list of entries.
657        scheme: "",
658        term: "",
659        label: "",
660        isFilter: true
661});
662
663FeedViewer.CategoryIncludeFilter = declare("dojox.atom.widget.FeedViewer.CategoryIncludeFilter", null,{
664        constructor: function(scheme, term, label){
665                // summary:
666                //              The initializer function.
667                this.scheme = scheme;
668                this.term = term;
669                this.label = label;
670        },
671
672        match: function(entry) {
673                // summary:
674                //              Function to determine if this category filter matches against a category on an atom entry
675                // returns:
676                //              boolean denoting if this category filter matched to this entry.
677                var matched = false;
678                if (entry !== null) {
679                        var categories = entry.categories;
680                        if (categories !== null) {
681                                for (var i = 0; i < categories.length; i++) {
682                                        var category = categories[i];
683
684                                        if (this.scheme !== "") {
685                                                if (this.scheme !== category.scheme) {
686                                                        break;
687                                                }
688                                        }
689                                       
690                                        if (this.term !== "") {
691                                                if (this.term !== category.term) {
692                                                        break;
693                                                }
694                                        }
695
696                                        if (this.label !== "") {
697                                                if (this.label !== category.label) {
698                                                        break;
699                                                }
700                                        }
701                                        //Made it this far, everything matched.
702                                        matched = true;
703                                }
704                        }
705                }
706                return matched;
707        }
708});
709
710return FeedViewer;
711});
Note: See TracBrowser for help on using the repository browser.