1 /**
  2  * Discussion TopicReplies component.
  3  * 
  4  * @namespace Alfresco
  5  * @class Alfresco.TopicReplies
  6  */
  7 (function()
  8 {
  9    /**
 10     * YUI Library aliases
 11     */
 12    var Dom = YAHOO.util.Dom,
 13        Event = YAHOO.util.Event,
 14        Element = YAHOO.util.Element;
 15 
 16    /**
 17     * Alfresco Slingshot aliases
 18     */
 19    var $html = Alfresco.util.encodeHTML;
 20     
 21    /**
 22     * TopicReplies constructor.
 23     * 
 24     * @param {String} htmlId The HTML id of the parent element
 25     * @return {Alfresco.TopicReplies} The new Reply instance
 26     * @constructor
 27     */
 28    Alfresco.TopicReplies = function(htmlId)
 29    {
 30       /* Mandatory properties */
 31       this.name = "Alfresco.TopicReplies";
 32       this.id = htmlId;
 33       
 34       /* Initialise prototype properties */
 35       this.widgets = {};
 36       this.editData =
 37       {
 38          formDiv : null,
 39          viewDiv : null
 40       };
 41       
 42       /* Register this component */
 43       Alfresco.util.ComponentManager.register(this);
 44 
 45       /* Load YUI Components */
 46       Alfresco.util.YUILoaderHelper.require(["dom", "event", "element"], this.onComponentsLoaded, this);
 47       
 48       /* Decoupled event listeners */
 49       YAHOO.Bubbling.on("addReplyToPost", this.onAddReplyToPost, this);
 50       YAHOO.Bubbling.on("topicDataChanged", this.onTopicDataChanged, this);
 51             
 52       return this;
 53    };
 54    
 55    Alfresco.TopicReplies.prototype =
 56    {
 57       /**
 58        * Object container for initialization options
 59        *
 60        * @property options
 61        * @type object
 62        */
 63       options:
 64       {
 65          /**
 66           * Current siteId.
 67           * 
 68           * @property siteId
 69           * @type string
 70           */
 71          siteId: "",
 72          
 73          /**
 74           * Current containerId.
 75           * 
 76           * @property containerId
 77           * @type string
 78           */
 79          containerId: "discussions",
 80          
 81          /**
 82           * Reference to the topic for which to display replies.
 83           */
 84          topicRef: "",
 85          
 86          /**
 87           * Id of the topic to display
 88           * Note: this is solely used for generating the activities feed
 89           */
 90          topicId: "",
 91          
 92          /**
 93           * Title of the topic for which replies are displayed
 94           * Note: this is solely used for generating the activities feed
 95           */
 96          topicTitle: ""
 97       },
 98       
 99       /**
100        * Stores editing related data
101        */
102       editData : null,
103       
104       /**
105        * Stores the displayed data
106        */
107       repliesData: null,
108       
109       /**
110        * Object container for storing YUI widget instances.
111        * 
112        * @property widgets
113        * @type object
114        */
115       widgets: null,
116       
117       /**
118        * Set multiple initialization options at once.
119        *
120        * @method setOptions
121        * @param obj {object} Object literal specifying a set of options
122        */
123       setOptions: function TopicReplies_setOptions(obj)
124       {
125          this.options = YAHOO.lang.merge(this.options, obj);
126          return this;
127       },
128       
129       /**
130        * Set multiple initialization options at once.
131        *
132        * @method setOptions
133        * @param obj {object} Object literal specifying a set of options
134        */
135       setMessages: function TopicReplies_setMessages(obj)
136       {
137          Alfresco.util.addMessages(obj, this.name);
138          return this;
139       },
140       
141       /**
142        * Fired by YUILoaderHelper when required component script files have
143        * been loaded into the browser.
144        *
145        * @method onComponentsLoaded
146        */
147       onComponentsLoaded: function TopicReplies_onComponentsLoaded()
148       {
149          Event.onContentReady(this.id, this.onReady, this, true);
150       },
151    
152       /**
153        * Fired by YUI when parent element is available for scripting.
154        * Component initialisation, including instantiation of YUI widgets and event listener binding.
155        *
156        * @method onReady
157        */
158       onReady: function TopicReplies_onReady()
159       {   
160          // Hook action events.
161          var me = this;
162          var fnActionHandlerDiv = function TopicReplies_fnActionHandlerDiv(layer, args)
163          {
164             var owner = YAHOO.Bubbling.getOwnerByTagName(args[1].anchor, "div");
165             if (owner !== null)
166             {
167                var action = "";
168                action = owner.className;
169                if (typeof me[action] == "function")
170                {
171                   var id = '';
172                   id = owner.id;
173                   var nodeRef = '';
174                   nodeRef = id.substring((me.id + '-' + action + '-').length);
175                   nodeRef = me.toNodeRef(nodeRef);
176                   me[action].call(me, nodeRef);
177                   args[1].stop = true;
178                }
179             }
180             return true;
181          };
182          YAHOO.Bubbling.addDefaultAction("reply-action-link", fnActionHandlerDiv);
183 
184          // Hook the show/hide link
185          var fnShowHideChildrenHandler = function TopicReplies_fnShowHideChildrenHandler(layer, args)
186          {
187             var owner = YAHOO.Bubbling.getOwnerByTagName(args[1].anchor, "a");
188             if (owner !== null)
189             {
190                var action = "";
191                action = owner.className;
192                if (typeof me[action] == "function")
193                {
194                   var id = '';
195                   id = owner.id;
196                   var nodeRef = '';
197                   nodeRef = id.substring((me.id + '-' + action + '-').length);
198                   nodeRef = me.toNodeRef(nodeRef);
199                   me[action].call(me, nodeRef);
200                   args[1].stop = true;
201                }
202             }
203             return true;
204          };
205          YAHOO.Bubbling.addDefaultAction("showHideChildren", fnShowHideChildrenHandler);
206 
207          // initialize the mouse over listener
208          Alfresco.util.rollover.registerHandlerFunctions(this.id, this.onReplyElementMouseEntered, this.onReplyElementMouseExited, this);
209       },
210       
211       
212       // Bubble event management
213       
214       /**
215        * Tag selected handler (document details)
216        *
217        * @method onTagSelected
218        * @param tagId {string} Tag name.
219        * @param target {HTMLElement} Target element clicked.
220        */
221       onAddReplyToPost: function TopicReplies_addReplyToPost(layer, args)
222       {
223          var obj = args[1];
224          if (obj && (obj.postRef !== null))
225          {
226             this.onAddReply(obj.postRef);
227          }
228       },
229       
230       /**
231        * onLoadReplies handler
232        */
233       onTopicDataChanged: function TopicReplies_onTopicDataChanged(layer, args)
234       {
235          var oldRef = this.options.topicRef;
236          var obj = args[1];
237          if (obj && (obj.topicRef !== null) && (obj.topicId !== null) && (obj.topicTitle !== null))
238          {
239             this.options.topicRef = obj.topicRef;
240             this.options.topicId = obj.topicId;
241             this.options.topicTitle = obj.topicTitle;
242 
243             // load the data if not done so or if the topic has changed
244             if (this.repliesData === null || (oldRef != this.repliesData.topicRef))
245             {
246                this._loadRepliesData();
247             }
248          }
249       },
250       
251       /**
252        * Loads the replies data and updates the ui.
253        */      
254       _loadRepliesData: function TopicReplies__loadRepliesData()
255       {
256          // ajax request success handler
257          var loadRepliesDataSuccess = function TopicReplies_loadRepliesDataSuccess(response)
258          {
259             // set the loaded data
260             var data = response.json.items;
261             this.repliesData = data;
262             
263             // render the ui
264             this.renderUI();
265          };
266          
267          // construct the url to call
268          var url = YAHOO.lang.substitute(Alfresco.constants.URL_SERVICECONTEXT + "components/forum/post/site/{site}/{container}/{topicId}/replies?levels={levels}",
269          {
270             site : this.options.siteId,
271             container: this.options.containerId,
272             topicId: this.options.topicId,
273             levels: 999
274          });
275          
276          // execute ajax request
277          Alfresco.util.Ajax.request(
278          {
279             url: url,
280             successCallback:
281             {
282                fn: loadRepliesDataSuccess,
283                scope: this
284             },
285             failureMessage: this._msg("message.loadreplies.failure")
286          });
287       },
288 
289       /**
290        * Converts a html-id safe nodeRef to a real one.
291        * 
292        * @param safeRef {string} a nodeReference where the separators have been replaced by _
293        * @return {string} a valid node reference
294        */
295       toNodeRef: function(safeRef)
296       {
297          return safeRef.replace(/_/, '://').replace(/_/, '/');
298       },
299       
300       /**
301        * Converts a node reference to a html-id safe string.
302        * 
303        * @param nodeRef {string} a nodeReference where the separators have been replaced by _
304        * @return {string} a nodeRef string usable in html id values
305        */
306       toSafeRef: function(nodeRef)
307       {
308          return nodeRef.replace(':/', '').replace('/', '_').replace('/', '_');
309       },
310 
311       /**
312        * Renders the UI of the component
313        */
314       renderUI: function TopicReplies_renderUI()
315       {
316          // get the root element
317          var rootDiv = Dom.get(this.id + '-replies-root');
318          rootDiv.innerHTML = '';
319          var elem = new Element(rootDiv);
320          
321          // add the reply form element
322          var replyFormDiv = document.createElement("div");
323          replyFormDiv.setAttribute("id", "reply-add-form-" + this.toSafeRef(this.options.topicRef));
324          elem.appendChild(replyFormDiv);
325          
326          for (var x=0; x < this.repliesData.length; x++)
327          {
328             this.renderReply(rootDiv, this.repliesData[x], false);
329          }
330          
331          // attach the rollover listeners
332          Alfresco.util.rollover.registerListenersByClassName(this.id, 'reply', 'div');  
333          
334          // finally show the root div
335          Dom.removeClass(rootDiv, "hidden");
336       },
337       
338       /**
339        * Renders an individual reply element
340        */
341       renderReply: function TopicReplies_renderReply(parentDiv, data, highlight)
342       {
343          var replyDiv = document.createElement("div");
344          
345          // we first generate the general html for an element, this is the
346          // edit and view divs, the child replies div including the add reply div.
347          var safeRef = this.toSafeRef(data.nodeRef);
348          var html = '';
349          html += '<div class="reply" id="reply-' + safeRef + '">';
350          html += '</div>';
351          html += '<div id="reply-edit-form-' + safeRef + '" class="hidden"></div>';
352          html += '<div id="reply-add-form-' + safeRef + '" class="indented hidden"></div>';
353          html += '<div class="indented" id="replies-of-' + safeRef + '"></div>';
354          replyDiv.innerHTML = html;
355          parentDiv.appendChild(replyDiv);
356          
357          // render the reply content
358          var viewElem = Dom.get('reply-' + safeRef);
359          this.renderReplyView(viewElem, data);
360          
361          // render the children if they got already loaded
362          if (data.children !== undefined)
363          {
364             var repliesElem = Dom.get('replies-of-' + safeRef);
365             for (var x=0; x < data.children.length; x++)
366             {
367                this.renderReply(repliesElem, data.children[x], false);
368             }
369          }
370          
371          if (highlight)
372          {
373             Alfresco.util.Anim.pulse(viewElem);
374             this._scrollToElement(viewElem);
375          }
376       },
377       
378       /**
379        * Renders the view part of a reply element
380        */
381       renderReplyView: function TopicReplies_renderReplyView(div, data)
382       {
383          var safeRef = this.toSafeRef(data.nodeRef);
384          var html = '';
385                   
386          // render the actions
387          html += '<div class="nodeEdit">';
388          if (data.permissions.reply)
389          {
390             html += '<div class="onAddReply" id="' + this.id + '-onAddReply-' + safeRef + '">';
391             html += '<a href="#" class="reply-action-link">' + this._msg("action.reply") + '</a>';
392             html += '</div>';
393          }
394         
395          if (data.permissions.edit)
396          {
397             html += '<div class="onEditReply" id="' + this.id + '-onEditReply-' + safeRef + '">';
398             html += '<a href="#" class="reply-action-link">' + this._msg("action.edit") + '</a>';
399             html += '</div>';
400          }
401          html += '</div>';
402           
403          // avatar
404          html += '<div class="authorPicture">' + Alfresco.util.people.generateUserAvatarImg(data.author) + '</div>';
405 
406          // content            
407          html += '<div class="nodeContent">';
408          html += '<div class="userLink">' + Alfresco.util.people.generateUserLink(data.author) + ' ' + this._msg("post.said") + ': ';
409          if (data.isUpdated)
410          {
411             html += '<span class="theme-color-2 nodeStatus">(' + this._msg("post.updated") + ')</span>';
412          }
413          html += '</div>';
414             
415          html += '<div class="content yuieditor">' + data.content + '</div>';
416          html += '</div>';
417          
418          // footer part
419          html += '<div class="nodeFooter">';
420          html += '<span class="nodeAttrLabel replyTo">' + this._msg("replies") + ': </span>';
421          html += '<span class="nodeAttrValue">(' + (data.children !== undefined ? data.children.length : 0) + ') </span>';
422          if (data.replyCount > 0)
423          {
424             html += '<span class="nodeAttrValue">';
425             html += '<a href="#" class="showHideChildren" id="' + this.id + '-showHideChildren-' + safeRef + '">' + this._msg("replies.hide") + '</a>';
426             html += '</span>';
427          }
428          html += '<span class="separator"> </span>';
429          html += '<span class="nodeAttrLabel">' + this._msg("post.postedOn") + ': ' + '</span>';
430          html += '<span class="nodeAttrValue">' + Alfresco.util.formatDate(data.createdOn) + '</span>';
431          html += '</div>';
432          
433          div.innerHTML = html;
434       },
435 
436       /**
437        * Re-renders the view UI for a reply element
438        */
439       rerenderReplyUI: function TopicReplies_rerenderReplyUI(nodeRef, highlight)
440       {
441          // Get the view element and the data and update the html
442          var viewElem = Dom.get('reply-' + this.toSafeRef(nodeRef));
443          var data = this.findReplyDataObject(nodeRef);
444          this.renderReplyView(viewElem, data);
445          
446          if (highlight)
447          {
448             Alfresco.util.Anim.pulse(viewElem);
449          }
450       },
451       
452       
453       // Actions handlers
454       
455       /**
456        * Handler for the add reply action links.
457        */
458       onAddReply: function TopicReplies_onAddReply(nodeRef)
459       {
460          this._loadEditForm(nodeRef, false);
461       },
462 
463       /**
464        * Handler for the edit reply action links.
465        */
466       onEditReply: function TopicReplies_onEditReply(nodeRef)
467       {
468          this._loadEditForm(nodeRef, true);
469       },
470       
471       /**
472        * Handler for the show/hide replies toggle links
473        */
474       showHideChildren: function TopicReplies_showideChildren(nodeRef)
475       {
476          // get the replies element
477          var repliesElem = Dom.get('replies-of-' + this.toSafeRef(nodeRef));
478          if (Dom.hasClass(repliesElem, "hidden"))
479          {
480             this._showChildren(nodeRef);
481          }
482          else
483          {
484             this._hideChildren(nodeRef);
485          }
486       },
487 
488       /**
489        * Loads the reply add or edit form.
490        * 
491        * @param nodeRef {string} the parent nodeRef to which a child should be added or the reply that should be edited
492        * @param isEdit {boolean} if true nodeRef is edited, otherwise nodeRef is the parent of the new reply to be created
493        */      
494       _loadEditForm: function TopicReplies__loadEditForm(nodeRef, isEdit)
495       {          
496          // construct the id to use for the form elements
497          var formId = this.id + this.toSafeRef(nodeRef) + (isEdit ? "-edit" : "-add");
498          
499          // execute ajax request to load the form
500          Alfresco.util.Ajax.request(
501          {
502             url: Alfresco.constants.URL_SERVICECONTEXT + "modules/discussions/replies/reply-form",
503             dataObj:
504             {
505                htmlid : formId
506             },
507             successCallback:
508             {
509                fn: this._onEditFormLoaded,
510                scope: this,
511                obj:
512                {
513                   isEdit: isEdit,
514                   nodeRef: nodeRef,
515                   formId: formId
516                }
517             },
518             failureMessage: this._msg("message.loadeditform.failure")
519          });
520       },
521       
522       /**
523        * Request success handler for the loadReplyEditForm ajax request
524        */
525       _onEditFormLoaded: function TopicReplies__onEditFormLoaded(response, obj)
526       {
527          // make sure no other forms are displayed
528          this._hideOpenForms();
529          
530          // insert the form at the right location
531          var safeRef = this.toSafeRef(obj.nodeRef);
532          var formDiv = null;
533          if (obj.isEdit)
534          {
535             formDiv = Dom.get('reply-edit-form-' + safeRef);
536          }
537          else
538          {
539             formDiv = Dom.get('reply-add-form-' + safeRef);
540          }
541          formDiv.innerHTML = response.serverResponse.responseText;
542          
543          // find the data object for nodeRef.
544          // Note: this will be null in case of a reply to the topic itself.
545          var data = this.findReplyDataObject(obj.nodeRef);
546          
547          // insert current values into the form
548          var actionUrl = '';
549          var formTitle = '';
550          var content = '';
551          var submitButtonLabel = '';
552          var viewDiv = null;
553          if (obj.isEdit)
554          {
555             viewDiv = Dom.get('reply-' + safeRef);
556             actionUrl = YAHOO.lang.substitute(Alfresco.constants.PROXY_URI + "api/forum/post/node/{nodeRef}",
557             {
558                nodeRef: obj.nodeRef.replace(':/', '')
559             });
560             formTitle = this._msg('form.updateTitle');
561             submitButtonLabel = this._msg('action.update');
562             content = data.content;
563          }
564          else
565          {
566             actionUrl = YAHOO.lang.substitute(Alfresco.constants.PROXY_URI + "api/forum/post/node/{nodeRef}/replies",
567             {
568                nodeRef: obj.nodeRef.replace(':/', '')
569             });
570             
571             // for root replies we don't have a parent data object and therefore can
572             // tell whom to reply to
573             if (data !== null)
574             {
575                formTitle = this._msg('form.replyToTitle', Alfresco.util.people.generateUserLink(data.author));
576             }
577             else
578             {
579                formTitle = this._msg('form.replyTitle');
580             }
581             submitButtonLabel = this._msg('action.create');
582          }
583          
584          // set the values in the dom
585          var formId = obj.formId;
586          Dom.get(formId + "-form-title").innerHTML = formTitle;
587          Dom.get(formId + "-form").setAttribute("action", actionUrl);
588          Dom.get(formId + "-site").setAttribute("value", this.options.siteId);
589          Dom.get(formId + "-container").setAttribute("value", this.options.containerId);
590          Dom.get(formId + "-submit").setAttribute("value", submitButtonLabel);
591          Dom.get(formId + "-content").value = content;
592          
593          // store edit related data.
594          this.editData =
595          {
596             nodeRef: obj.nodeRef,
597             isEdit: obj.isEdit,
598             formId: formId,
599             viewDiv: viewDiv,
600             formDiv: formDiv
601          };
602          
603          // register the form logic        
604          this._registerEditForm(obj.nodeRef, formId, obj.isEdit);
605       },
606       
607       /**
608        * Registers the form logic
609        */
610       _registerEditForm: function TopicReplies__registerEditForm(nodeRef, formId, isEdit)
611       {
612          // register the okButton
613          this.widgets.okButton = new YAHOO.widget.Button(formId + "-submit",
614          {
615             type: "submit"
616          });
617          
618          // register the cancel button
619          this.widgets.cancelButton = new YAHOO.widget.Button(formId + "-cancel",
620          {
621             type: "button"
622          });
623          this.widgets.cancelButton.subscribe("click", this.onFormCancelButtonClick, this, true);
624          
625          // Instantiate and render the simple editor we use for the form
626          this.widgets.editor = new Alfresco.util.RichEditor(Alfresco.constants.HTML_EDITOR, formId + '-content',this.options.editorConfig);
627          this.widgets.editor.addPageUnloadBehaviour(this._msg("message.unsavedChanges.reply"));
628          this.widgets.editor.render();
629          
630          // Add validation to the rich text editor
631          var keyUpIdentifier = (Alfresco.constants.HTML_EDITOR === 'YAHOO.widget.SimpleEditor') ? 'editorKeyUp' : 'onKeyUp';
632          this.widgets.editor.subscribe(keyUpIdentifier, function (e)
633          {
634             /**
635              * Doing a form validation on every key stroke is process consuming, below we try to make sure we only do
636              * a form validation if it's necessarry.
637              * NOTE: Don't check for zero-length in commentsLength, due to HTML <br>, <span> tags, etc. possibly
638              * being present. Only a "Select all" followed by delete will clean all tags, otherwise leftovers will
639              * be there even if the form looks empty.
640              */                       
641             if (this.widgets.editor.getContent().length < 20 || this.widgets.okButton.get("disabled"))
642             {
643                // Submit was disabled and something has been typed, validate and submit will be enabled
644                this.widgets.editor.save();
645                this.widgets.form.updateSubmitElements();
646             }
647          }, this, true);
648 
649          // create the form that does the validation/submit
650          this.widgets.form = new Alfresco.forms.Form(formId + "-form");
651          var replyForm = this.widgets.form;
652          replyForm.setShowSubmitStateDynamically(true, false);
653          replyForm.addValidation(formId + "-content", Alfresco.forms.validation.mandatory, null);         
654          replyForm.setSubmitElements(this.widgets.okButton);
655          if (isEdit)
656          {
657             replyForm.setAjaxSubmitMethod(Alfresco.util.Ajax.PUT);
658          }
659          replyForm.setAJAXSubmit(true,
660          {
661             successMessage: this._msg("message.savereply.success"),
662             successCallback:
663             {
664                fn: this.onFormSubmitSuccess,
665                scope: this,
666                obj:
667                {
668                   nodeRef: nodeRef,
669                   isEdit: isEdit
670                }
671             },
672             failureMessage: this._msg("message.savereply.failure"),
673             failureCallback:
674             {
675                fn: this.onFormSubmitFailure,
676                scope: this
677             }
678          });
679          replyForm.setSubmitAsJSON(true);
680          replyForm.doBeforeFormSubmit =
681          {
682             fn: function(form, obj)
683             {
684                // disable buttons
685                this.widgets.okButton.set("disabled", true);
686                this.widgets.cancelButton.set("disabled", true);
687          
688                //Put the HTML back into the text area
689                this.widgets.editor.save();
690                
691                // show a wait message
692                this.widgets.feedbackMessage = Alfresco.util.PopupManager.displayMessage(
693                {
694                   text: Alfresco.util.message(this._msg("message.submitting")),
695                   spanClass: "wait",
696                   displayTime: 0
697                });
698             },
699             scope: this
700          };
701          replyForm.init();
702          
703          // now show the form
704          this._showForm();
705          
706          // finally scroll to the form
707          this._scrollToElement(this.editData.formDiv);
708       },
709       
710       /**
711        * Form submit success handler
712        */
713       onFormSubmitSuccess: function TopicReplies_onFormSubmitSuccess(response, obj)
714       {
715          // remove wait message
716          this.widgets.feedbackMessage.destroy();
717          
718          var data, parentElem;
719           
720          // in case of an edit reply, simply update the data/ui
721          if (obj.isEdit)
722          {
723             // update the data object for the reply
724             data = this.findReplyDataObject(obj.nodeRef);
725             YAHOO.lang.augmentObject(data, response.json.item, true);
726             
727             // rerender the ui
728             this.rerenderReplyUI(data.nodeRef, true);
729          }
730          // in case of a create, add the new data and insert a new reply element
731          else
732          {
733             // the logic here is slightly different for top level replies
734             if (this.options.topicRef == obj.nodeRef)
735             {
736                // add the data object
737                this.repliesData.push(response.json.item);
738                
739                // render the new reply
740                parentElem = Dom.get(this.id + '-replies-root');
741                this.renderReply(parentElem, response.json.item, true);
742             }
743             else
744             {
745                // add the data object
746                data = this.findReplyDataObject(obj.nodeRef);
747                // make sure the children array exists
748                if (data.children === undefined)
749                {
750                   data.children = [];
751                }
752                data.children.push(response.json.item);
753                
754                // render the new reply
755                parentElem = Dom.get('replies-of-' + this.toSafeRef(obj.nodeRef));
756                this.renderReply(parentElem, response.json.item, true);
757                
758                // rerender the parent reply, which will update the reply count
759                this.rerenderReplyUI(obj.nodeRef, false);
760             }
761             
762             // make sure the rolover listener gets attached to the new element
763             Alfresco.util.rollover.registerListenersByClassName(this.id, 'reply', 'div');  
764          }
765          
766          // finally hide the form / show the updated view in case of an edit
767          this._hideOpenForms();
768       },
769 
770       /**
771        * Form submit failure handler
772        */
773       onFormSubmitFailure: function TopicReplies_onFormSubmitFailure(response, obj)
774       {
775          // enable buttons
776          this.widgets.okButton.set("disabled", false);
777          this.widgets.cancelButton.set("disabled", false);
778          
779          // hide message
780          this.widgets.feedbackMessage.destroy();
781       },
782 
783       /**
784        * Edit form cancel button click handler
785        */
786       onFormCancelButtonClick: function TopicReplies_onFormCancelButtonClick(type, args)
787       {
788          this._hideOpenForms();
789       },
790       
791       /**
792        * Find the data object for a reply given its node reference.
793        * 
794        * @param nodeRef {string} the nodeRef of the reply to find the data for
795        * @return {string} a reply data object or null if not found
796        */
797       findReplyDataObject: function TopicReplies_findReplyDataObject(nodeRef)
798       {
799          return this._findReplyDataObjectImpl(this.repliesData, nodeRef);
800       },
801       
802       /**
803        * Implementation of findReplyDataObject
804        */
805       _findReplyDataObjectImpl: function TopicReplies__findReplyDataObjectImpl(arr, nodeRef)
806       {
807          for (var  x=0; x < arr.length; x++)
808          {
809             // check the element
810             if (arr[x].nodeRef == nodeRef)
811             {
812                return arr[x];
813             }
814             // check the children recursively
815             else if (arr[x].children !== undefined)
816             {
817                var result = this._findReplyDataObjectImpl(arr[x].children, nodeRef);
818                if (result !== null)
819                {
820                   return result;
821                }
822             }
823          }
824          return null;
825       },
826       
827       /**
828        * Shows the children of a reply
829        * 
830        * @param nodeRef the nodeRef of the reply for which children should be shown
831        */
832       _showChildren: function TopicReplies__showChildren(nodeRef)
833       {
834           // show the replies element
835           var repliesElem = Dom.get('replies-of-' + this.toSafeRef(nodeRef));
836           Dom.removeClass(repliesElem, "hidden");
837           
838           // the show/hide replies toggle link might not exist if there are no replies
839           var linkElem = Dom.get(this.id + '-showHideChildren-' + this.toSafeRef(nodeRef));
840           if (linkElem !== null)
841           {
842              linkElem.innerHTML = this._msg("replies.hide");
843           }
844       },
845       
846       /**
847        * Hides the children of a reply
848        * 
849        * @param nodeRef the nodeRef of the reply for which children should be hidden
850        */
851       _hideChildren: function TopicReplies__hideChildren(nodeRef)
852       {
853           var repliesElem = Dom.get('replies-of-' + this.toSafeRef(nodeRef));
854           Dom.addClass(repliesElem, "hidden");
855           var linkElem = Dom.get(this.id + '-showHideChildren-' + this.toSafeRef(nodeRef));
856           linkElem.innerHTML = this._msg("replies.show");
857       },
858       
859       /**
860        * Hides any open form, displays any hidden view element
861        */
862       _hideOpenForms: function TopicReplies__hideOpenForms()
863       {
864          if (this.editData.formDiv !== null)
865          {
866             Dom.addClass(this.editData.formDiv, "hidden");
867             this.editData.formDiv.innerHTML = '';
868             this.editData.formDiv = null;
869          }
870          if (this.editData.viewDiv !== null)
871          {
872             Dom.removeClass(this.editData.viewDiv, "hidden");
873             this.editData.viewDiv = null;
874          }
875       },
876       
877       /**
878        * Shows the already prepared form and hides the associated view element if any.
879        */
880       _showForm: function TopicReplies__showForm()
881       {
882          // hide the view element if any
883          if (this.editData.viewDiv !== null)
884          {
885             Dom.addClass(this.editData.viewDiv, "hidden");
886          }
887          
888          // show the form element
889          Dom.removeClass(this.editData.formDiv, "hidden");
890       },
891       
892       /**
893        * Vertically scrolls the browser window to the passed element
894        */
895       _scrollToElement: function TopicReplies__scrollToElement(el)
896       {
897          var yPos = Dom.getY(el);
898          if (YAHOO.env.ua.ie > 0)
899          {
900             yPos = yPos - (document.body.clientHeight / 3);
901          }
902          else
903          {
904             yPos = yPos - (window.innerHeight / 3);
905          }
906          window.scrollTo(0, yPos);
907       },
908       
909       // mouse hover functionality
910       
911       /** Called when the mouse enters into a list item. */
912       onReplyElementMouseEntered: function TopicReplies_onReplyElementMouseEntered(layer, args)
913       {
914          // only highlight if there are actions on the specific element
915          var nodeRef = args[1].target.id.substring(('reply-').length);
916          nodeRef = this.toNodeRef(nodeRef);
917          
918          var data = this.findReplyDataObject(nodeRef), permissions = data.permissions;
919          if (!(permissions.edit || permissions.reply || permissions['delete']))
920          {
921             return;
922          }
923          
924          Dom.addClass(args[1].target, 'over');
925       },
926       
927       /** Called whenever the mouse exits a list item. */
928       onReplyElementMouseExited: function TopicReplies_onReplyElementMouseExited(layer, args)
929       {
930          Dom.removeClass(args[1].target, 'over');
931       },
932 
933       /**
934        * Gets a custom message
935        *
936        * @method _msg
937        * @param messageId {string} The messageId to retrieve
938        * @return {string} The custom message
939        * @private
940        */
941       _msg: function TopicReplies__msg(messageId)
942       {
943          return Alfresco.util.message.call(this, messageId, "Alfresco.TopicReplies", Array.prototype.slice.call(arguments).slice(1));
944       }
945       
946    };
947 })();
948