Merge branch '2016.11-develop' into 2017.02
[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 =  i18n._('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                 this.addTagButton = new Ext.Button({
111                     text: '',
112                     width: 16,
113                     iconCls: 'action_add',
114                     tooltip: i18n._('Add a new personal tag'),
115                     scope: this,
116                     hidden: !Tine.Tinebase.common.hasRight('use_personal_tags', this.app.appName),
117                     handler: function() {
118                         Ext.Msg.prompt(i18n._('Add New Personal Tag'),
119                                        i18n._('Please note: You create a personal tag. Only you can see it!') + ' <br />' + i18n._('Enter tag name:'),
120                             function(btn, text) {
121                                 if (btn == 'ok'){
122                                     this.onTagAdd(text);
123                                 }
124                             },
125                         this, false, this.searchField.lastQuery);
126                     }
127                 })
128             ]
129
130         });
131
132         var tagTpl = new Ext.XTemplate(
133             '<tpl for=".">',
134                '<div class="x-widget-tag-tagitem" id="{id}">',
135                     '<div class="x-widget-tag-tagitem-color" style="background-color: {color};">&#160;</div>', 
136                     '<div class="x-widget-tag-tagitem-text" ext:qtip="', 
137                         '{[this.encode(values.name)]}', 
138                         '<tpl if="type == \'personal\' ">&nbsp;<i>(' + i18n._('personal') + ')</i></tpl>',
139                         '</i>&nbsp;[{occurrence}]',
140                         '<tpl if="description != null && description.length &gt; 1"><hr>{[this.encode(values.description)]}</tpl>" >',
141                         
142                         '&nbsp;{[this.encode(values.name)]}',
143                     '</div>',
144                 '</div>',
145             '</tpl>' ,{
146                 encode: function(value) {
147                     return Tine.Tinebase.common.doubleEncode(value);
148                 }
149             }
150         );
151         
152         this.dataView = new Ext.DataView({
153             store: this.recordTagsStore,
154             tpl: tagTpl,
155             autoHeight:true,
156             multiSelect: true,
157             overClass:'x-widget-tag-tagitem-over',
158             selectedClass:'x-widget-tag-tagitem-selected',
159             itemSelector:'div.x-widget-tag-tagitem',
160             emptyText: i18n._('No Tags to display')
161         });
162         this.dataView.on('contextmenu', function(dataView, selectedIdx, node, event){
163             if (!this.dataView.isSelected(selectedIdx)) {
164                 this.dataView.clearSelections();
165                 this.dataView.select(selectedIdx);
166             }
167             event.stopEvent();
168             
169             var selectedTags = this.dataView.getSelectedRecords();
170             var selectedTag = selectedTags.length == 1 ? selectedTags[0] : null;
171             
172             var allowDelete = true;
173             for (var i=0; i<selectedTags.length; i++) {
174                 if (selectedTags[i].get('type') == 'shared') {
175                     allowDelete = false;
176                 }
177             }
178             
179             var menu = new Ext.menu.Menu({
180                 plugins: [{
181                     ptype: 'ux.itemregistry',
182                     key:   'Tinebase-MainContextMenu'
183                 }],
184                 items: [
185                     new Ext.Action({
186                         scope: this,
187                         text: i18n.ngettext('Detach tag', 'Detach tags', selectedTags.length),
188                         iconCls: 'x-widget-tag-action-detach',
189                         handler: function() {
190                             for (var i=0,j=selectedTags.length; i<j; i++){
191                                 this.recordTagsStore.remove(selectedTags[i]);
192                             }
193                         }
194                     }),
195                     '-',
196                     {
197                         text: i18n._('Edit tag'),
198                         disabled: !(selectedTag && allowDelete),
199                         menu: {
200                             items: [
201                                 new Ext.Action({
202                                     text: i18n._('Rename Tag'),
203                                     selectedTag: selectedTag,
204                                     scope: this,
205                                     handler: function(action) {
206                                         var tag = action.selectedTag;
207                                         Ext.Msg.prompt(i18n._('Rename Tag') + ' "'+ tag.get('name') +'"', i18n._('Please enter a new name:'), function(btn, text){
208                                             if (btn == 'ok'){
209                                                 tag.set('name', text);
210                                                 this.onTagUpdate(tag);
211                                             }
212                                         }, this, false, tag.get('name'));
213                                     }
214                                 }),
215                                 new Ext.Action({
216                                     text: i18n._('Edit Description'),
217                                     selectedTag: selectedTag,
218                                     scope: this,
219                                     handler: function(action) {
220                                         var tag = action.selectedTag;
221                                         Ext.Msg.prompt(i18n._('Description for tag') + ' "'+ tag.get('name') +'"', i18n._('Please enter new description:'), function(btn, text){
222                                             if (btn == 'ok'){
223                                                 tag.set('description', text);
224                                                 this.onTagUpdate(tag);
225                                             }
226                                         }, this, 30, tag.get('description'));
227                                     }
228                                 }),
229                                 new Ext.Action({
230                                     text: i18n._('Change Color'),
231                                     iconCls: 'action_changecolor',
232                                     scope: this,
233                                     menu: new Ext.menu.ColorMenu({
234                                         // not working any longer ->
235                                         //value: selectedTag ? selectedTag.get('color') : '#FFFFFF',
236                                         // something like this should work -> 
237                                         // (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.)
238                                         //value: selectedTag ? Ext.util.Format.lowercase(selectedTag.get('color').substr(1)) : 'ffffff',
239                                         scope: this,
240                                         listeners: {
241                                             select: function(menu, color) {
242                                                 color = '#' + color;
243                                                 
244                                                 if (selectedTag.get('color') != color) {
245                                                     selectedTag.set('color', color);
246                                                     this.onTagUpdate(selectedTag);
247                                                 }
248                                             },
249                                             scope: this
250                                         }
251                                     })                                        
252                                 })                                    
253                             ]
254                         }
255                     },
256                     new Ext.Action({
257                         disabled: !allowDelete,
258                         scope: this,
259                         text: i18n.ngettext('Delete Tag', 'Delete Tags', selectedTags.length),
260                         iconCls: 'action_delete',
261                         handler: function() {
262                             var tagsToDelete = [];
263                             for (var i=0,j=selectedTags.length; i<j; i++){
264                                 // don't request to delete non existing tags
265                                 if (selectedTags[i].id.length > 20) {
266                                     tagsToDelete.push(selectedTags[i].id);
267                                 }
268                             }
269                             
270                             // @todo use correct strings: Realy -> Really / disapear -> disappear
271                             Ext.MessageBox.confirm(
272                                 i18n.ngettext('Really delete selected tag?', 'Really delete selected tags?', selectedTags.length),
273                                 i18n.ngettext('The selected tag will be deleted and disappear for all entries',
274                                                         'The selected tags will be removed and disappear for all entries', selectedTags.length), 
275                                 function(btn) {
276                                     if (btn == 'yes'){
277                                         Ext.MessageBox.wait(i18n._('Please wait a moment...'), i18n.ngettext('Deleting Tag', 'Deleting Tags', selectedTags.length));
278                                         Ext.Ajax.request({
279                                             params: {
280                                                 method: 'Tinebase.deleteTags', 
281                                                 ids: tagsToDelete
282                                             },
283                                             success: function(_result, _request) {
284                                                 // reset avail tag store
285                                                 this.availableTagsStore.lastOptions = null;
286                                                 
287                                                 for (var i=0,j=selectedTags.length; i<j; i++){
288                                                     this.recordTagsStore.remove(selectedTags[i]);
289                                                 }
290                                                 Ext.MessageBox.hide();
291                                             },
292                                             failure: function ( result, request) {
293                                                 Ext.MessageBox.alert(i18n._('Failed'), i18n._('Could not delete Tag(s).'));
294                                             },
295                                             scope: this 
296                                         });
297                                     }
298                             }, this);
299                         }
300                     })
301                 ]
302             });
303
304             if (! this.searchField.disabled) {
305                 menu.showAt(event.getXY());
306             }
307
308         },this);
309         
310         this.formField = {
311             layout: 'form',
312             items: new Tine.widgets.tags.TagFormField({
313                 tagsPanel: this,
314                 recordTagsStore: this.recordTagsStore
315             })
316         };
317         
318         this.items = [{
319             xtype: 'panel',
320             layout: 'fit',
321             bbar: this.bottomBar,
322             items: [
323                 this.dataView,
324                 this.formField
325             ]
326         }];
327         
328         Tine.widgets.dialog.MultipleEditDialogPlugin.prototype.registerSkipItem(this);
329         Tine.widgets.tags.TagPanel.superclass.initComponent.call(this);
330     },
331     
332     getFormField: function() {
333         return this.formField.items;
334     },
335     
336     /**
337      * @private
338      */
339     onTagAdd: function(tagName) {
340         if (tagName.length < 3) {
341             Ext.Msg.show({
342                title: i18n._('Notice'),
343                msg: i18n._('The minimum tag length is three.'),
344                buttons: Ext.Msg.OK,
345                animEl: 'elId',
346                icon: Ext.MessageBox.INFO
347             });
348         } else {
349             var isAttached = false;
350             this.recordTagsStore.each(function(tag){
351                 if(tag.data.name == tagName) {
352                     isAttached = true;
353                 }
354             },this);
355             
356             if (!isAttached) {
357                 var tagToAttach = false;
358                 this.availableTagsStore.each(function(tag){
359                     if(tag.data.name == tagName) {
360                         tagToAttach = tag;
361                     }
362                 }, this);
363                 
364                 if (!tagToAttach) {
365                     tagToAttach = new Tine.Tinebase.Model.Tag({
366                         name: tagName,
367                         type: 'personal',
368                         description: '',
369                         color: '#FFFFFF'
370                     });
371                     
372                     if (! Ext.isIE) {
373                         this.el.mask();
374                     }
375                     Ext.Ajax.request({
376                         params: {
377                             method: 'Tinebase.saveTag', 
378                             tag: tagToAttach.data
379                         },
380                         success: function(_result, _request) {
381                             var tagData = Ext.util.JSON.decode(_result.responseText);
382                             var newTag = new Tine.Tinebase.Model.Tag(tagData, tagData.id);
383                             this.recordTagsStore.add(newTag);
384                             
385                             // reset avail tag store
386                             this.availableTagsStore.lastOptions = null;
387                             this.el.unmask();
388                         },
389                         failure: function ( result, request) {
390                             Ext.MessageBox.alert(i18n._('Failed'), i18n._('Could not create tag.'));
391                             this.el.unmask();
392                         },
393                         scope: this 
394                     });
395                 } else {
396                     this.recordTagsStore.add(tagToAttach);
397                 }
398             }
399         }
400     },
401     onTagUpdate: function(tag) {
402         if (tag.get('name').length < 3) {
403             Ext.Msg.show({
404                title: i18n._('Notice'),
405                msg: i18n._('The minimum tag length is three.'),
406                buttons: Ext.Msg.OK,
407                animEl: 'elId',
408                icon: Ext.MessageBox.INFO
409             });
410         } else {
411             this.el.mask();
412             Ext.Ajax.request({
413                 params: {
414                     method: 'Tinebase.saveTag', 
415                     tag: tag.data
416                 },
417                 success: function(_result, _request) {
418                     // reset avail tag store
419                     this.availableTagsStore.lastOptions = null;
420                     this.el.unmask();
421                 },
422                 failure: function ( result, request) {
423                     Ext.MessageBox.alert(i18n._('Failed'), i18n._('Could not update tag.'));
424                     this.el.unmask();
425                 },
426                 scope: this 
427             });
428         }
429     },
430     
431     /**
432      * updates the title
433      */
434     onChange: function() {
435         if (this.recordTagsStore) {
436             this.title = i18n._('Tags') + ' (' + this.recordTagsStore.getCount() + ')';
437             if (this.header) {
438                 this.setTitle(this.title);
439             }
440         }
441     }
442     
443 });
444
445 /**
446  * @private Helper class to have tags processing in the standard form/record cycle
447  */
448 Tine.widgets.tags.TagFormField = Ext.extend(Ext.form.Field, {
449     /**
450      * @cfg {Ext.data.JsonStore} recordTagsStore a store where the record tags are in.
451      */
452     recordTagsStore: null,
453     
454     name: 'tags',
455     hidden: true,
456     labelSeparator: '',
457
458     requiredGrant: 'editGrant',
459
460     /**
461      * @private
462      */
463     initComponent: function() {
464         Tine.widgets.tags.TagFormField.superclass.initComponent.call(this);
465         //this.hide();
466     },
467     /**
468      * returns tags data of the current record
469      */
470     getValue: function() {
471         var value = [];
472         this.recordTagsStore.each(function(tag){
473             if(tag.id.length > 5 && ! String(tag.id).match(/ext-record/)) {
474                 //if we have a valid id we just return the id
475                 value.push(tag.id);
476             } else {
477                 //it's a new tag and will be saved on the fly
478                 value.push(tag.data);
479             }
480         });
481         return value;
482     },
483     /**
484      * sets tags from an array of tag data objects (not records)
485      */
486     setValue: function(value){
487         // set empty value
488         value = value || []; 
489         
490         // replace template fields
491         Tine.Tinebase.Model.Tag.replaceTemplateField(value);
492         
493         this.recordTagsStore.loadData(value);
494     },
495
496     setDisabled: function(disabled) {
497         // disable combo, btn, context
498         this.tagsPanel.searchField.setDisabled(disabled);
499         this.tagsPanel.addTagButton.setDisabled(disabled);
500     }
501
502 });
503
504 /**
505  * Dialog for editing a tag itself
506  */
507 Tine.widgets.tags.TagEditDialog = Ext.extend(Ext.Window, {
508     width: 200,
509     height: 300,
510     layout: 'fit',
511     margins: '0px 5px 0px 5px',
512     
513     initComponent: function() {
514         this.items = new Ext.form.FormPanel({
515             defaults: {
516                 xtype: 'textfield',
517                 anchor: '100%'
518             },
519             labelAlign: 'top',
520             items: [
521                 {
522                     name: 'name',
523                     fieldLabel: 'Name'
524                 },
525                 {
526                     name: 'description',
527                     fieldLabel: i18n._('Description')
528                 },
529                 {
530                     name: 'color',
531                     fieldLabel: i18n._('Color')
532                 }
533             ]
534             
535         });
536         
537         Tine.widgets.tags.TagEditDialog.superclass.initComponent.call(this);
538     }
539 });
540
541 Tine.widgets.tags.EditDialog = Ext.extend(Ext.Window, {
542     layout:'border',
543     width: 640,
544     heigh: 480,
545     
546     initComponent: function() {
547         this.items = [
548         {
549             region: 'west',
550             split: true
551         },
552         {
553             region: 'center',
554             split: true
555         }
556         ];
557         Tine.widgets.tags.EditDialog.superclass.call(this);
558     }
559 });