1 /**
  2  * Copyright (C) 2005-2010 Alfresco Software Limited.
  3  *
  4  * This file is part of Alfresco
  5  *
  6  * Alfresco is free software: you can redistribute it and/or modify
  7  * it under the terms of the GNU Lesser General Public License as published by
  8  * the Free Software Foundation, either version 3 of the License, or
  9  * (at your option) any later version.
 10  *
 11  * Alfresco is distributed in the hope that it will be useful,
 12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 14  * GNU Lesser General Public License for more details.
 15  *
 16  * You should have received a copy of the GNU Lesser General Public License
 17  * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 18  */
 19 
 20 /**
 21  * TagLibrary
 22  * 
 23  * Module that manages the selection of tags in a form
 24  *
 25  * @namespace Alfresco
 26  * @class Alfresco.module.TagLibrary
 27  */
 28 (function()
 29 {
 30    /**
 31     * YUI Library aliases
 32     */
 33    var Dom = YAHOO.util.Dom,
 34       Event = YAHOO.util.Event;
 35 
 36    /**
 37     * Alfresco Slingshot aliases
 38     */
 39    var $html = Alfresco.util.encodeHTML;
 40 
 41    Alfresco.module.TagLibrary = function(htmlId)
 42    {
 43       Alfresco.module.TagLibrary.superclass.constructor.call(this, "Alfresco.module.TagLibrary", htmlId + "-tagLibrary", ["button"]);
 44       
 45       /**
 46        * TODO: Remove this hack.
 47        * The TagLibrary was trying to register itself with the htmlId. This isn't a good idea, as there will likely
 48        * already be a component registered with that id.
 49        * Unfortunately the TagLibrary code (and therefore it's historical users) rely on this id
 50        */
 51       this.id = htmlId;
 52 
 53       this.tagId =
 54       {
 55          id: 0,
 56          tags: {}
 57       };
 58       this.currentTags = [];
 59       
 60       this.setTags([]);
 61       
 62       return this;
 63    };
 64 
 65    YAHOO.extend(Alfresco.module.TagLibrary, Alfresco.component.Base,
 66    {
 67        /**
 68         * Object container for initialization options
 69         */
 70        options:
 71        {
 72           /**
 73            * Current siteId.
 74            * 
 75            * @property siteId
 76            * @type string
 77            * @default ""
 78            */
 79           siteId: "",
 80           
 81           /**
 82            * Maximum number of tags popular tags displayed
 83            * @property topN
 84            * @type integer
 85            * @default 10
 86            */
 87           topN: 10
 88        },
 89 
 90        /**
 91         * Balloon UI instance used for error reporting
 92         *
 93         * @property balloon
 94         * @type object
 95         */
 96        balloon: null,
 97        
 98       /**
 99        * Object literal used to generate unique tag ids
100        * 
101        * @property tagId
102        * @type object
103        */
104       tagId: null,
105       
106       /**
107        * Currently selected tags.
108        * 
109        * @type: array of strings
110        * @default empty array
111        */
112       currentTags: null,
113 
114       /**
115        * Sets the current list of tags.
116        * Use this method if the tags html and inputs have been generated on the server.
117        * If you create the taglibrary in javascript, use setTags to also update the UI.
118        *
119        * @method setCurrentTags
120        * @param tags {Array} String array of tags
121        */
122       setCurrentTags: function TagLibrary_setCurrentTags(tags)
123       {
124          this.currentTags = tags;
125          return this;
126       },
127       
128       
129       formsRuntime: null,
130       
131       /**
132        * Registers the tag library logic with the dom tree
133        *
134        * @method initialize
135        * @param formsRuntime {object} Instance of Alfresco.forms.Form
136        */
137       initialize: function TagLibrary_initialize(formsRuntime)
138       {
139          var me = this;
140 
141          var fnActionHandlerDiv = function TagLibrary_fnActionHandlerDiv(layer, args)
142          {
143             var owner = YAHOO.Bubbling.getOwnerByTagName(args[1].anchor, "li");
144             if (owner !== null)
145             {
146                var action = "";
147                action = owner.getAttribute("class");
148                if (typeof me[action] == "function")
149                {
150                   var tagName = me.findTagName(me, owner.id);
151                   me[action].call(me, tagName);
152                   args[1].stop = true;
153                }
154             }
155 
156             return true;
157          };
158          YAHOO.Bubbling.addDefaultAction("taglibrary-action", fnActionHandlerDiv);
159 
160          // load link for popular tags
161          Event.addListener(this.id + "-load-popular-tags-link", "click", this.onPopularTagsLinkClicked, this, true);
162          var enterKeyListenerA = new YAHOO.util.KeyListener(this.id + "-load-popular-tags-link",
163          {
164             keys: YAHOO.util.KeyListener.KEY.ENTER
165          },
166          {
167             fn: function TagLibrary_enterKeyListener(eventName, event, obj)
168             {
169                me.onPopularTagsLinkClicked(event[1], this);
170                return true;
171             },
172             scope: this,
173             correctScope: true
174          }, "keypress");
175          enterKeyListenerA.enable();
176          
177          // register the "enter" event on the tag text field to add the tag (otherwise the form gets submitted)
178          var enterKeyListener = new YAHOO.util.KeyListener(this.id + "-tag-input-field", 
179          {
180             keys: YAHOO.util.KeyListener.KEY.ENTER
181          }, 
182          {
183             fn: function TagLibrary_enterKeyListener(eventName, event, obj)
184             {
185                var valid = this.formsRuntime._runValidations(true);
186                if (valid)
187                {
188                   me.onAddTagButtonClick();
189                   this.balloon.hide();
190                }
191                Event.stopEvent(event[1]);
192                return false;
193             },
194             scope: this,
195             correctScope: true
196          }, "keypress");
197          enterKeyListener.enable();
198          
199          // button to add tag to list
200          var addTagButton = Alfresco.util.createYUIButton(this, "add-tag-button", this.onAddTagButtonClick,
201          {
202             type: "button",
203             disabled: (typeof formsRuntime != "undefined"),
204             htmlName: "-"
205          });
206 
207          // Add validators
208          if (formsRuntime)
209          {
210             var tagInput = Dom.get(this.id + "-tag-input-field");
211             
212             this.balloon = Alfresco.util.createBalloon(tagInput);
213             this.balloon.onClose.subscribe(function(e)
214             {
215                try
216                {
217                   tagInput.focus();
218                }
219                catch (e)
220                {
221                }
222             }, this, true);
223             
224             
225             var tagFormsRuntime = new Alfresco.forms.Form(formsRuntime.formId);
226             tagFormsRuntime.setShowSubmitStateDynamically(true, true);
227             tagFormsRuntime.setSubmitElements(addTagButton);
228             tagFormsRuntime.setAJAXSubmit(true);
229             tagFormsRuntime.doBeforeAjaxRequest =
230             {
231                fn: function TagLibrary_fnValidation(form, obj)
232                {
233                   return false;
234                },
235                obj: null,
236                scope: this
237             };
238 
239             // Create a custom validator for the tag name - this is almost identical to the node name validation
240             // in "forms-runtime.js" with the exception that double quotes are allowed as they are required for 
241             // entering tags that comprise of space separated words.
242             var tagNameValidation = function (field, args, event, form, silent, message)
243             {
244                if (!args)
245                {
246                   args = {};
247                }
248                args.pattern = /([\*\\\>\<\?\/\:\|]+)|([\.]?[\.]+$)/;
249                args.match = false;
250                return Alfresco.forms.validation.regexMatch(field, args, event, form, silent, message);
251             };
252             
253             var regexArgs = {};
254             regexArgs.pattern = /([\*\\\>\<\?\/\:\|]+)|([\.]?[\.]+$)/;
255             regexArgs.match = false;
256             var msg = Alfresco.util.message("validation-hint.tagName");
257             tagFormsRuntime.addValidation(this.id + "-tag-input-field", tagNameValidation, null, "keyup", msg);
258             tagFormsRuntime.addValidation(this.id + "-tag-input-field", Alfresco.forms.validation.mandatory);
259             tagFormsRuntime.addValidation(this.id + "-tag-input-field", Alfresco.forms.validation.length,
260             {
261                max: 256,
262                crop: true
263             }, "keyup");
264             
265             var scope = this;
266             tagFormsRuntime.addError = function InsituEditor_textBox_addError(msg, field)
267             {
268                scope.balloon.html(msg);
269                scope.balloon.show();
270             };
271             
272             this.formsRuntime = tagFormsRuntime;
273          }
274       },
275       
276       /**
277        * Generate ID alias for tag, suitable for DOM ID attribute
278        *
279        * @method generateTagId
280        * @param scope {object} instance that contains a tagId object (which stores the generated tag id mappings)
281        * @param tagName {string} Tag name
282        * @return {string} A unique DOM-safe ID for the tag
283        */
284       generateTagId: function TagLibrary_generateTagId(scope, tagName, action)
285       {
286          var id = 0,
287             tagId = scope.tagId;
288          
289          if (tagName in tagId.tags)
290          {
291             id = tagId.tags[tagName];
292          }
293          else
294          {
295            tagId.id++;
296            id = tagId.tags[tagName] = tagId.id;
297          }
298          return scope.id + "-" + action + "-" + id;
299       },
300       
301       /**
302        * Returns the tagName given a id generated by generateTagId.
303        *
304        * @method findTagName
305        * @param scope {object} instance that contains a tagId object (which stores the generated tag id mappings)
306        * @param tagId {string} Tag ID
307        */
308       findTagName: function TagLibrary_findTagName(scope, tagId)
309       {
310          var actionAndId = tagId.substring(scope.id.length + 1),
311             tagIdValue = actionAndId.substring(actionAndId.indexOf('-') + 1);
312          
313          for (var tag in scope.tagId.tags)
314          {
315             if (scope.tagId.tags[tag] == tagIdValue)
316             {
317                return tag;
318             }
319          }
320          return null;
321       },
322       
323       /**
324        * Adds an array of tags to the current tags.
325        * For each tag the html is generated, this function can therefore
326        * be used to set the tags when using the taglibrary as a client-side
327        * only component (no tags generated on the server)
328        *
329        * @method setTags
330        * @param tags {array} Array containing the tags (by name)
331        */
332       setTags: function TagLibrary_setTags(tags)
333       {
334          // first make sure that there are no previous tags available
335          var elTags = Dom.get(this.id + '-current-tags');
336          if (elTags !== null)
337          {
338             elTags.innerHTML = '';
339             this.currentTags = [];
340 
341             // add each tag to the list, also generating the html
342             for (var i = 0, ii = tags.length; i < ii; i++)
343             {
344                this._addTagImpl(tags[i]);
345             }
346 
347             // Show the popular tags load link
348             Dom.setStyle(this.id + "-load-popular-tags-link", "display", "inline");
349             Dom.get(this.id + "-popular-tags").innerHTML = "<li></li>";
350          }
351       },
352 
353       /**
354        * Get all tags currently selected
355        *
356        * @method getTags
357        */
358       getTags: function TagLibrary_getTags()
359       {
360          return this.currentTags;
361       },
362       
363       /**
364        * Updates a form with the currently selected tags.
365        *
366        * @method updateForm
367        * @param formId {string} the id of the form to update
368        * @param tagsFieldName {string} the name of the field to use to store the tags in
369        */
370       updateForm: function TagLibrary_updateForm(formId, tagsFieldName)
371       {
372          // construct the complete name to use for the field
373          var fullFieldName = tagsFieldName + '[]';
374          
375          // clean out the currently available tag inputs
376          var formElem = Dom.get(formId);
377          
378          // find all input fields, delete the inputs that match the field name
379          var inputs = formElem.getElementsByTagName("input"),
380             x, xx;
381          
382          // IMPORTANT: Do NOT optimize loop - loop bounds are modified inside
383          for (x = 0; x < inputs.length; x++)
384          {
385             if (inputs[x].name == fullFieldName)
386             {
387                 // remove the field
388                 inputs[x].parentNode.removeChild(inputs[x]);
389                 x--;
390             }
391          }
392          
393          // Find any previously created "tag" and "tag[]" input elements that were previously created
394          // and added to the form and remove them. This might have occured when the form save operation
395          // did not complete. If these are not removed then the tag data will at best be wrong and at
396          // worst cause and error.
397          var elementsToClear = Dom.getElementsBy(function(el)
398             {
399                return (el.name == tagsFieldName || el.name == fullFieldName);
400             }, "input", formElem);
401          for (i = 0; i < elementsToClear.length; i++)
402          {
403             formElem.removeChild(elementsToClear[i]);
404          }
405          
406          var tagName, elem;
407          if (this.currentTags.length > 0)
408          {
409             // generate inputs for the selected tags
410             for (x = 0, xx = this.currentTags.length; x < xx; x++)
411             {
412                tagName = this.currentTags[x];
413                elem = document.createElement('input');
414                elem.setAttribute('name', fullFieldName);
415                elem.setAttribute('value', tagName);
416                elem.setAttribute('type', 'hidden');
417                formElem.appendChild(elem);
418             }
419          }
420          else
421          {
422             elem = document.createElement('input');
423             elem.setAttribute('name', tagsFieldName);
424             elem.setAttribute('value', '');
425             elem.setAttribute('type', 'hidden');
426             formElem.appendChild(elem);
427          }
428       },
429 
430       /**
431        * Triggered by a click on one of the selected tags
432        *
433        * @method onRemoveTag
434        * @param tagName {string} Tag clicked
435        */
436       onRemoveTag: function TagLibrary_onRemoveTag(tagName)
437       {
438           this._removeTagImpl(tagName);
439       },
440       
441       /**
442        * Triggered by a click onto one of the popular tags.
443        *
444        * @method onAddTag
445        * @param tagName {string} Tag clicked
446        */
447       onAddTag: function TagLibrary_onAddTag(tagName)
448       {
449          this._addTagImpl(tagName);
450       },
451 
452       /**
453        * Loads the popular tags
454        *
455        * @method onPopularTagsLinkClicked
456        */
457       onPopularTagsLinkClicked: function TagLibrary_onPopularTagsLinkClicked(e, obj)
458       {
459          // load the popular tags through an ajax call
460          var url = YAHOO.lang.substitute(Alfresco.constants.PROXY_URI + "api/tagscopes/site/{site}/tags?d={d}&topN={tn}",
461          {
462             site: Alfresco.constants.SITE,
463             d: new Date().getTime(),
464             tn: this.options.topN
465          });
466          Alfresco.util.Ajax.request(
467          {
468             url: url,
469             method: "GET",
470             responseContentType : "application/json",
471             successCallback:
472             {
473                fn: this._onPopularTagsLoaded,
474                scope: this
475             },
476             failureMessage: this.msg("taglibrary.msg.failedLoadTags")
477          });
478          Event.stopEvent(e);
479       },
480       
481       /**
482        * Popular tags loaded handler
483        *
484        * @method _onPopularTagsLoaded
485        * @param response {object} Server response
486        * @private
487        */
488       _onPopularTagsLoaded: function TagLibrary__onPopularTagsLoaded(response)
489       {
490          this._displayPopularTags(response.json.tags);
491       },
492       
493       /**
494        * Update the UI with the popular tags loaded via AJAX.
495        *
496        * @method _displayPopularTags
497        * @param tags {array} Array of tags
498        * @private
499        */
500       _displayPopularTags: function TagLibrary__showPopularTags(tags)
501       {
502          // remove the popular tags load link
503          Dom.setStyle(this.id + "-load-popular-tags-link", "display", "none");
504 
505          // add all tags to the ui
506          var popularTagsElem = Dom.get(this.id + "-popular-tags"),
507             current = Alfresco.util.arrayToObject(this.currentTags),
508             tagName, elem, elemId;
509 
510          popularTagsElem.innerHTML = "";
511 
512          for (var i = 0, ii = tags.length; i < ii; i++)
513          {
514             tagName = tags[i].name;
515             if (!(tagName in current))
516             {
517                elem = document.createElement('li');
518                elemId = this.generateTagId(this, tagName, 'onAddTag');
519                elem.setAttribute('id', elemId);
520                elem.setAttribute('class', 'onAddTag');
521                elem.innerHTML = '<a href="#" class="taglibrary-action"><span>' + $html(tagName) + '</span><span class="add"> </span></a>';
522                popularTagsElem.appendChild(elem);
523             }
524          }
525       },
526 
527       /**
528        * Adds the content of the text field as a new tag.
529        *
530        * @method onAddTagButtonClick
531        */
532       onAddTagButtonClick: function TagLibrary_onAddTagButtonClick(type, args)
533       {
534          // get the text of the input field
535          var inputField = Dom.get(this.id + "-tag-input-field"),
536             text = inputField.value,
537             tags = Alfresco.util.getTags(text);
538          
539          for (var x = 0, xx = tags.length; x < xx; x++)
540          {
541             this._addTagImpl(tags[x]);
542          }
543          
544          // finally clear the text field
545          inputField.value = "";
546       },
547        
548       /**
549        * Fires a tags changed event.
550        *
551        * @method _fireTagsChangedEvent
552        */
553       _fireTagsChangedEvent: function TagLibrary__fireTagsChangedEvent()
554       {
555          // send out a message informing about the new set of tags
556          YAHOO.Bubbling.fire('onTagLibraryTagsChanged',
557          {
558             tags : this.currentTags
559          });
560       },
561 
562       /**
563        * Add a tag to the current set of selected tags
564        *
565        * @method _addTagImpl
566        * @param tagName {string} Name of tag
567        * @private
568        */
569       _addTagImpl: function TagLibrary__addTagImpl(tagName)
570       {
571          // sanity checks
572          if (tagName === null || tagName.length < 1)
573          {
574              return;
575          }
576          
577          // check whether the tag has already been added
578          for (var x = 0, xx = this.currentTags.length; x < xx; x++)
579          {
580             if (tagName == this.currentTags[x])
581             {
582                return;
583             }
584          }
585          
586          // add the tag to the internal data structure
587          this.currentTags.push(tagName);
588          
589          // add the tag to the UI
590          var currentTagsElem = Dom.get(this.id + "-current-tags"),
591             elem = document.createElement('li'),
592             elemId = this.generateTagId(this, tagName, 'onRemoveTag');
593          
594          elem.setAttribute('id', elemId);
595          elem.setAttribute('class', 'onRemoveTag');
596          elem.innerHTML = '<a href="#" class="taglibrary-action"><span>' + $html(tagName) + '</span><span class="remove"> </span></a>';
597          currentTagsElem.appendChild(elem);
598 
599          // inform interested parties about change
600          this._fireTagsChangedEvent();
601       },
602 
603       /**
604        * Remove a tag from the current set of selected tags
605        *
606        * @method _removeTagImpl
607        * @param tagName {string} Name of tag
608        * @private
609        */
610       _removeTagImpl: function TagLibrary__removeTagImpl(tagName)
611       {
612          // sanity checks
613          if (tagName === null || tagName.length < 1)
614          {
615              return;
616          }
617          
618          // IMPORTANT: Do NOT optimize loop - loop bounds are modified inside
619          for (var x = 0; x < this.currentTags.length; x++)
620          {
621             if (tagName == this.currentTags[x])
622             {
623                this.currentTags.splice(x, 1);
624                x--;
625             }
626          }
627 
628          // remove the ui element
629          var elemId = this.generateTagId(this, tagName, 'onRemoveTag'),
630             tagElemToRemove = Dom.get(elemId);
631          
632          tagElemToRemove.parentNode.removeChild(tagElemToRemove);
633          
634          // inform interested parties about change
635          this._fireTagsChangedEvent();
636       }
637    });     
638 })();