703690e0ff48f68093b0464fe44c752fd60d7e05
[tine20] / tine20 / Tinebase / js / widgets / tags / TagsPanel.js
1 /*
2  * Tine 2.0
3  * 
4  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
5  * @author      Cornelius Weiss <c.weiss@metaways.de>
6  * @copyright   Copyright (c) 2007-2008 Metaways Infosystems GmbH (http://www.metaways.de)
7  *
8  * TODO make initial color work again in Ext.menu.ColorMenu
9  */
10  
11 Ext.ns('Tine.widgets', 'Tine.widgets.tags');
12
13 /**
14  * Class for a single tag panel
15  * 
16  * @namespace   Tine.widgets.tags
17  * @class       Tine.widgets.tags.TagPanel
18  * @extends     Ext.Panel
19  */
20 Tine.widgets.tags.TagPanel = Ext.extend(Ext.Panel, {
21     /**
22      * @cfg {Tine.Tinebase.Application} app Application which uses this panel
23      */
24     app: null,
25     /**
26      * @cfg {String} recordId Id of record this panel is displayed for
27      */
28     recordId: '',
29     /**
30      * @cfg {Array} tags Initial tags
31      */
32     tags: null,
33     /**
34      * @var {Ext.data.JsonStore}
35      * Holds tags of the record this panel is displayed for
36      */
37     recordTagsStore: null,
38     /**
39      * @var {Ext.data.JsonStore} Store for available tags
40      */
41     availableTagsStore: false,
42     /**
43      * @var {Ext.form.ComboBox} live search field to search tags to add
44      */
45     searchField: null,
46     
47     iconCls: 'action_tag',
48     layout: 'fit',
49     bodyStyle: 'padding: 2px 2px 2px 2px',
50     collapsible: true,
51     border: false,
52     
53     /**
54      * @private
55      */
56     initComponent: function(){
57         this.title =  _('Tags') + ' (0)';
58         this.app = Ext.isString(this.app) ? Tine.Tinebase.appMgr.get(this.app) : this.app;
59         
60         // init recordTagsStore
61         this.tags = [];
62         var that = this;
63         
64         this.recordTagsStore = new Ext.data.JsonStore({
65             id: 'id',
66             fields: Tine.Tinebase.Model.Tag,
67             data: this.tags,
68             scope: that,
69             listeners: {
70                 add: that.onChange,
71                 load: that.onChange,
72                 remove: that.onChange,
73                 scope: that
74             }
75         });
76         
77         // init availableTagsStore
78         this.availableTagsStore = new Ext.data.JsonStore({
79             id: 'id',
80             root: 'results',
81             totalProperty: 'totalCount',
82             fields: Tine.Tinebase.Model.Tag,
83             baseParams: {
84                 method: 'Tinebase.searchTags',
85                 filter: {
86                     application: this.app.appName,
87                     grant: 'use'
88                 },
89                 paging : {}
90             }
91         });
92         
93         this.searchField = new Tine.widgets.tags.TagCombo({
94             app: this.app,
95             onlyUsableTags: true,
96             disableClearer: true
97         });
98         this.searchField.on('select', function(searchField, selectedTag){
99             if(this.recordTagsStore.getById(selectedTag.id) === undefined) {
100                 this.recordTagsStore.add(selectedTag);
101             }
102             searchField.blur();
103             searchField.reset();
104         },this);
105         
106         this.bottomBar = new Ext.Container({
107             layout: 'column',
108             items: [
109                 Ext.apply(this.searchField, {columnWidth: .99}),
110                 new Ext.Button({
111                     text: '',
112                     width: 16,
113                     iconCls: 'action_add',
114                     tooltip: _('Add a new personal tag'),
115                     scope: this,
116                     handler: function() {
117                         Ext.Msg.prompt(_('Add New Personal Tag'),
118                                        _('Please note: You create a personal tag. Only you can see it!') + ' <br />' + _('Enter tag name:'), 
119                             function(btn, text) {
120                                 if (btn == 'ok'){
121                                     this.onTagAdd(text);
122                                 }
123                             }, 
124                         this, false, this.searchField.lastQuery);
125                     }
126                 })
127             ]
128         
129         });
130         
131         var tagTpl = new Ext.XTemplate(
132             '<tpl for=".">',
133                '<div class="x-widget-tag-tagitem" id="{id}">',
134                     '<div class="x-widget-tag-tagitem-color" style="background-color: {color};">&#160;</div>', 
135                     '<div class="x-widget-tag-tagitem-text" ext:qtip="', 
136                         '{[this.encode(values.name)]}', 
137                         '<tpl if="type == \'personal\' ">&nbsp;<i>(' + _('personal') + ')</i></tpl>',
138                         '</i>&nbsp;[{occurrence}]',
139                         '<tpl if="description != null && description.length &gt; 1"><hr>{[this.encode(values.description)]}</tpl>" >',
140                         
141                         '&nbsp;{[this.encode(values.name)]}',
142                     '</div>',
143                 '</div>',
144             '</tpl>' ,{
145                 encode: function(value) {
146                     return Tine.Tinebase.common.doubleEncode(value);
147                 }
148             }
149         );
150         
151         this.dataView = new Ext.DataView({
152             store: this.recordTagsStore,
153             tpl: tagTpl,
154             autoHeight:true,
155             multiSelect: true,
156             overClass:'x-widget-tag-tagitem-over',
157             selectedClass:'x-widget-tag-tagitem-selected',
158             itemSelector:'div.x-widget-tag-tagitem',
159             emptyText: _('No Tags to display')
160         });
161         this.dataView.on('contextmenu', function(dataView, selectedIdx, node, event){
162             if (!this.dataView.isSelected(selectedIdx)) {
163                 this.dataView.clearSelections();
164                 this.dataView.select(selectedIdx);
165             }
166             event.preventDefault();
167             
168             var selectedTags = this.dataView.getSelectedRecords();
169             var selectedTag = selectedTags.length == 1 ? selectedTags[0] : null;
170             
171             var allowDelete = true;
172             for (var i=0; i<selectedTags.length; i++) {
173                 if (selectedTags[i].get('type') == 'shared') {
174                     allowDelete = false;
175                 }
176             }
177             
178             var menu = new Ext.menu.Menu({
179                 items: [
180                     new Ext.Action({
181                         scope: this,
182                         text: Tine.Tinebase.translation.ngettext('Detach tag', 'Detach tags', selectedTags.length),
183                         iconCls: 'x-widget-tag-action-detach',
184                         handler: function() {
185                             for (var i=0,j=selectedTags.length; i<j; i++){
186                                 this.recordTagsStore.remove(selectedTags[i]);
187                             }
188                         }
189                     }),
190                     '-',
191                     {
192                         text: _('Edit tag'),
193                         disabled: !(selectedTag && allowDelete),
194                         menu: {
195                             items: [
196                                 new Ext.Action({
197                                     text: _('Rename Tag'),
198                                     selectedTag: selectedTag,
199                                     scope: this,
200                                     handler: function(action) {
201                                         var tag = action.selectedTag;
202                                         Ext.Msg.prompt(_('Rename Tag') + ' "'+ tag.get('name') +'"', _('Please enter a new name:'), function(btn, text){
203                                             if (btn == 'ok'){
204                                                 tag.set('name', text);
205                                                 this.onTagUpdate(tag);
206                                             }
207                                         }, this, false, tag.get('name'));
208                                     }
209                                 }),
210                                 new Ext.Action({
211                                     text: _('Edit Description'),
212                                     selectedTag: selectedTag,
213                                     scope: this,
214                                     handler: function(action) {
215                                         var tag = action.selectedTag;
216                                         Ext.Msg.prompt(_('Description for tag') + ' "'+ tag.get('name') +'"', _('Please enter new description:'), function(btn, text){
217                                             if (btn == 'ok'){
218                                                 tag.set('description', text);
219                                                 this.onTagUpdate(tag);
220                                             }
221                                         }, this, 30, tag.get('description'));
222                                     }
223                                 }),
224                                 new Ext.Action({
225                                     text: _('Change Color'),
226                                     iconCls: 'action_changecolor',
227                                     scope: this,
228                                     menu: new Ext.menu.ColorMenu({
229                                         // not working any longer ->
230                                         //value: selectedTag ? selectedTag.get('color') : '#FFFFFF',
231                                         // something like this should work -> 
232                                         // (from extjs api doc: (value) The initial color to highlight (should be a valid 6-digit color hex code without the # symbol). Note that the hex codes are case-sensitive.)
233                                         //value: selectedTag ? Ext.util.Format.lowercase(selectedTag.get('color').substr(1)) : 'ffffff',
234                                         scope: this,
235                                         listeners: {
236                                             select: function(menu, color) {
237                                                 color = '#' + color;
238                                                 
239                                                 if (selectedTag.get('color') != color) {
240                                                     selectedTag.set('color', color);
241                                                     this.onTagUpdate(selectedTag);
242                                                 }
243                                             },
244                                             scope: this
245                                         }
246                                     })                                        
247                                 })                                    
248                             ]
249                         }
250                     },
251                     new Ext.Action({
252                         disabled: !allowDelete,
253                         scope: this,
254                         text: Tine.Tinebase.translation.ngettext('Delete Tag', 'Delete Tags', selectedTags.length),
255                         iconCls: 'action_delete',
256                         handler: function() {
257                             var tagsToDelete = [];
258                             for (var i=0,j=selectedTags.length; i<j; i++){
259                                 // don't request to delete non existing tags
260                                 if (selectedTags[i].id.length > 20) {
261                                     tagsToDelete.push(selectedTags[i].id);
262                                 }
263                             }
264                             
265                             // @todo use correct strings: Realy -> Really / disapear -> disappear
266                             Ext.MessageBox.confirm(
267                                 Tine.Tinebase.translation.ngettext('Realy Delete Selected Tag?', 'Realy Delete Selected Tags?', selectedTags.length), 
268                                 Tine.Tinebase.translation.ngettext('the selected tag will be deleted and disapear for all entries', 
269                                                         'The selected tags will be removed and disapear for all entries', selectedTags.length), 
270                                 function(btn) {
271                                     if (btn == 'yes'){
272                                         Ext.MessageBox.wait(_('Please wait a moment...'), Tine.Tinebase.translation.ngettext('Deleting Tag', 'Deleting Tags', selectedTags.length));
273                                         Ext.Ajax.request({
274                                             params: {
275                                                 method: 'Tinebase.deleteTags', 
276                                                 ids: tagsToDelete
277                                             },
278                                             success: function(_result, _request) {
279                                                 // reset avail tag store
280                                                 this.availableTagsStore.lastOptions = null;
281                                                 
282                                                 for (var i=0,j=selectedTags.length; i<j; i++){
283                                                     this.recordTagsStore.remove(selectedTags[i]);
284                                                 }
285                                                 Ext.MessageBox.hide();
286                                             },
287                                             failure: function ( result, request) {
288                                                 Ext.MessageBox.alert(_('Failed'), _('Could not delete Tag(s).'));
289                                             },
290                                             scope: this 
291                                         });
292                                     }
293                             }, this);
294                         }
295                     })
296                 ]
297             });
298             menu.showAt(event.getXY());
299         },this);
300         
301         this.formField = {
302             layout: 'form',
303             items: new Tine.widgets.tags.TagFormField({
304                 recordTagsStore: this.recordTagsStore
305             })
306         };
307         
308         this.items = [{
309             xtype: 'panel',
310             layout: 'fit',
311             bbar: this.bottomBar,
312             items: [
313                 this.dataView,
314                 this.formField
315             ]
316         }];
317         
318         Tine.widgets.dialog.MultipleEditDialogPlugin.prototype.registerSkipItem(this);
319         Tine.widgets.tags.TagPanel.superclass.initComponent.call(this);
320     },
321     
322     getFormField: function() {
323         return this.formField.items;
324     },
325     
326     /**
327      * @private
328      */
329     onTagAdd: function(tagName) {
330         if (tagName.length < 3) {
331             Ext.Msg.show({
332                title: _('Notice'),
333                msg: _('The minimum tag length is three.'),
334                buttons: Ext.Msg.OK,
335                animEl: 'elId',
336                icon: Ext.MessageBox.INFO
337             });
338         } else {
339             var isAttached = false;
340             this.recordTagsStore.each(function(tag){
341                 if(tag.data.name == tagName) {
342                     isAttached = true;
343                 }
344             },this);
345             
346             if (!isAttached) {
347                 var tagToAttach = false;
348                 this.availableTagsStore.each(function(tag){
349                     if(tag.data.name == tagName) {
350                         tagToAttach = tag;
351                     }
352                 }, this);
353                 
354                 if (!tagToAttach) {
355                     tagToAttach = new Tine.Tinebase.Model.Tag({
356                         name: tagName,
357                         type: 'personal',
358                         description: '',
359                         color: '#FFFFFF'
360                     });
361                     
362                     if (! Ext.isIE) {
363                         this.el.mask();
364                     }
365                     Ext.Ajax.request({
366                         params: {
367                             method: 'Tinebase.saveTag', 
368                             tag: tagToAttach.data
369                         },
370                         success: function(_result, _request) {
371                             var tagData = Ext.util.JSON.decode(_result.responseText);
372                             var newTag = new Tine.Tinebase.Model.Tag(tagData, tagData.id);
373                             this.recordTagsStore.add(newTag);
374                             
375                             // reset avail tag store
376                             this.availableTagsStore.lastOptions = null;
377                             this.el.unmask();
378                         },
379                         failure: function ( result, request) {
380                             Ext.MessageBox.alert(_('Failed'), _('Could not create tag.'));
381                             this.el.unmask();
382                         },
383                         scope: this 
384                     });
385                 } else {
386                     this.recordTagsStore.add(tagToAttach);
387                 }
388             }
389         }
390     },
391     onTagUpdate: function(tag) {
392         if (tag.get('name').length < 3) {
393             Ext.Msg.show({
394                title: _('Notice'),
395                msg: _('The minimum tag length is three.'),
396                buttons: Ext.Msg.OK,
397                animEl: 'elId',
398                icon: Ext.MessageBox.INFO
399             });
400         } else {
401             this.el.mask();
402             Ext.Ajax.request({
403                 params: {
404                     method: 'Tinebase.saveTag', 
405                     tag: tag.data
406                 },
407                 success: function(_result, _request) {
408                     // reset avail tag store
409                     this.availableTagsStore.lastOptions = null;
410                     this.el.unmask();
411                 },
412                 failure: function ( result, request) {
413                     Ext.MessageBox.alert(_('Failed'), _('Could not update tag.'));
414                     this.el.unmask();
415                 },
416                 scope: this 
417             });
418         }
419     },
420     
421     /**
422      * updates the title
423      */
424     onChange: function() {
425         if (this.recordTagsStore) {
426             this.title = _('Tags') + ' (' + this.recordTagsStore.getCount() + ')';
427             if (this.header) {
428                 var el = this.header.dom.children[2];
429                 el.innerHTML = this.title;
430             }
431         }
432     }
433     
434 });
435
436 /**
437  * @private Helper class to have tags processing in the standard form/record cycle
438  */
439 Tine.widgets.tags.TagFormField = Ext.extend(Ext.form.Field, {
440     /**
441      * @cfg {Ext.data.JsonStore} recordTagsStore a store where the record tags are in.
442      */
443     recordTagsStore: null,
444     
445     name: 'tags',
446     hidden: true,
447     labelSeparator: '',
448     /**
449      * @private
450      */
451     initComponent: function() {
452         Tine.widgets.tags.TagFormField.superclass.initComponent.call(this);
453         //this.hide();
454     },
455     /**
456      * returns tags data of the current record
457      */
458     getValue: function() {
459         var value = [];
460         this.recordTagsStore.each(function(tag){
461             if(tag.id.length > 5 && ! String(tag.id).match(/ext-record/)) {
462                 //if we have a valid id we just return the id
463                 value.push(tag.id);
464             } else {
465                 //it's a new tag and will be saved on the fly
466                 value.push(tag.data);
467             }
468         });
469         return value;
470     },
471     /**
472      * sets tags from an array of tag data objects (not records)
473      */
474     setValue: function(value){
475         // set empty value
476         value = value || []; 
477         
478         // replace template fields
479         Tine.Tinebase.Model.Tag.replaceTemplateField(value);
480         
481         this.recordTagsStore.loadData(value);
482     }
483
484 });
485
486 /**
487  * Dialog for editing a tag itself
488  */
489 Tine.widgets.tags.TagEditDialog = Ext.extend(Ext.Window, {
490     width: 200,
491     height: 300,
492     layout: 'fit',
493     margins: '0px 5px 0px 5px',
494     
495     initComponent: function() {
496         this.items = new Ext.form.FormPanel({
497             defaults: {
498                 xtype: 'textfield',
499                 anchor: '100%'
500             },
501             labelAlign: 'top',
502             items: [
503                 {
504                     name: 'name',
505                     fieldLabel: 'Name'
506                 },
507                 {
508                     name: 'description',
509                     fieldLabel: _('Description')
510                 },
511                 {
512                     name: 'color',
513                     fieldLabel: _('Color')
514                 }
515             ]
516             
517         });
518         
519         Tine.widgets.tags.TagEditDialog.superclass.initComponent.call(this);
520     }
521 });
522
523 Tine.widgets.tags.EditDialog = Ext.extend(Ext.Window, {
524     layout:'border',
525     width: 640,
526     heigh: 480,
527     
528     initComponent: function() {
529         this.items = [
530         {
531             region: 'west',
532             split: true
533         },
534         {
535             region: 'center',
536             split: true
537         }
538         ];
539         Tine.widgets.tags.EditDialog.superclass.call(this);
540     }
541 });