1 /** 2 * Topic component. 3 * Shows and allows to edit a topic. 4 * 5 * @namespace Alfresco 6 * @class Alfresco.DiscussionsTopic 7 */ 8 (function() 9 { 10 /** 11 * YUI Library aliases 12 */ 13 var Dom = YAHOO.util.Dom, 14 Event = YAHOO.util.Event, 15 Element = YAHOO.util.Element; 16 17 /** 18 * Alfresco Slingshot aliases 19 */ 20 var $html = Alfresco.util.encodeHTML; 21 22 /** 23 * Topic constructor. 24 * 25 * @param {String} htmlId The HTML id of the parent element 26 * @return {Alfresco.TopicView} The new Topic instance 27 * @constructor 28 */ 29 Alfresco.DiscussionsTopic = function(htmlId) 30 { 31 /* Mandatory properties */ 32 this.name = "Alfresco.DiscussionsTopic"; 33 this.id = htmlId; 34 35 /* Initialise prototype properties */ 36 this.widgets = {}; 37 this.modules = {}; 38 this.tagId = 39 { 40 id: 0, 41 tags: {} 42 }; 43 44 /* Register this component */ 45 Alfresco.util.ComponentManager.register(this); 46 47 /* Load YUI Components */ 48 Alfresco.util.YUILoaderHelper.require(["datasource", "json", "connection", "event", "button", "menu", "editor"], this.onComponentsLoaded, this); 49 50 /* Decoupled event listeners */ 51 YAHOO.Bubbling.on("tagSelected", this.onTagSelected, this); 52 53 return this; 54 }; 55 56 Alfresco.DiscussionsTopic.prototype = 57 { 58 /** 59 * Object container for initialization options 60 * 61 * @property options 62 * @type object 63 */ 64 options: 65 { 66 /** 67 * Current siteId. 68 * 69 * @property siteId 70 * @type string 71 */ 72 siteId: "", 73 74 /** 75 * Current containerId. 76 * 77 * @property containerId 78 * @type string 79 */ 80 containerId: "discussions", 81 82 /** 83 * Id of the topic to display. 84 * 85 * @property topicId 86 * @type string 87 */ 88 topicId: "" 89 90 }, 91 92 /** 93 * Holds the data displayed in this component. 94 */ 95 topicData: null, 96 97 /** 98 * Object container for storing YUI widget instances. 99 * 100 * @property widgets 101 * @type object 102 */ 103 widgets: null, 104 105 /** 106 * Object container for storing module instances. 107 * 108 * @property modules 109 * @type object 110 */ 111 modules: null, 112 113 /** 114 * Object literal used to generate unique tag ids 115 * 116 * @property tagId 117 * @type object 118 */ 119 tagId: null, 120 121 /** 122 * Tells whether an action is currently ongoing. 123 * 124 * @property busy 125 * @type boolean 126 * @see _setBusy/_releaseBusy 127 */ 128 busy: false, 129 130 /** 131 * Set multiple initialization options at once. 132 * 133 * @method setOptions 134 * @param obj {object} Object literal specifying a set of options 135 */ 136 setOptions: function DiscussionsTopic_setOptions(obj) 137 { 138 this.options = YAHOO.lang.merge(this.options, obj); 139 return this; 140 }, 141 142 /** 143 * Set multiple initialization options at once. 144 * 145 * @method setOptions 146 * @param obj {object} Object literal specifying a set of options 147 */ 148 setMessages: function DiscussionsTopic_setMessages(obj) 149 { 150 Alfresco.util.addMessages(obj, this.name); 151 return this; 152 }, 153 154 /** 155 * Fired by YUILoaderHelper when required component script files have 156 * been loaded into the browser. 157 * 158 * @method onComponentsLoaded 159 */ 160 onComponentsLoaded: function DiscussionsTopic_onComponentsLoaded() 161 { 162 Event.onContentReady(this.id, this.onReady, this, true); 163 }, 164 165 166 /** 167 * Fired by YUI when parent element is available for scripting. 168 * Component initialisation, including instantiation of YUI widgets and event listener binding. 169 * 170 * @method onReady 171 */ 172 onReady: function DiscussionsTopic_onReady() 173 { 174 var me = this; 175 176 // Hook action events. 177 var fnActionHandlerDiv = function DiscussionsTopic_fnActionHandlerDiv(layer, args) 178 { 179 180 var owner = YAHOO.Bubbling.getOwnerByTagName(args[1].anchor, "div"); 181 if (owner !== null) 182 { 183 var action = ""; 184 action = owner.className; 185 if (typeof me[action] == "function") 186 { 187 me[action].call(me, me.topicData.name); 188 args[1].stop = true; 189 } 190 } 191 return true; 192 }; 193 YAHOO.Bubbling.addDefaultAction("topic-action-link-div", fnActionHandlerDiv); 194 195 // register tag action handler, which will issue tagSelected bubble events. 196 Alfresco.util.tags.registerTagActionHandler(this); 197 198 // initialize the mouse over listener 199 Alfresco.util.rollover.registerHandlerFunctions(this.id, this.onTopicElementMouseEntered, this.onTopicElementMouseExited, this); 200 201 // load the topic data 202 this._loadTopicData(); 203 }, 204 205 206 /** 207 * Loads the topic data and updates the ui. 208 */ 209 _loadTopicData: function DiscussionsTopic__loadTopicData() 210 { 211 // ajax request success handler 212 var loadTopicDataSuccess = function DiscussionsTopic__loadTopicData_loadTopicDataSuccess(response) 213 { 214 // set the loaded data 215 var data = response.json.item 216 this.topicData = data; 217 218 // render the ui 219 this.renderUI(); 220 221 // inform the comment components about the loaded post 222 this._fireTopicDataChangedEvent(); 223 }; 224 225 // construct url to call 226 var url = YAHOO.lang.substitute(Alfresco.constants.URL_SERVICECONTEXT + "components/forum/post/site/{site}/{container}/{topicId}", 227 { 228 site : this.options.siteId, 229 container: this.options.containerId, 230 topicId: this.options.topicId 231 }); 232 233 // execute ajax request 234 Alfresco.util.Ajax.request( 235 { 236 url: url, 237 successCallback: 238 { 239 fn: loadTopicDataSuccess, 240 scope: this 241 }, 242 failureMessage: this._msg("message.loadtopicdata.failure") 243 }); 244 }, 245 246 /** 247 * Renders the UI with the data available in the component. 248 */ 249 renderUI: function DiscussionsTopic_renderUI() 250 { 251 // get the container div 252 var viewDiv = Dom.get(this.id + '-topic-view-div'); 253 254 // render the topic and insert the resulting html 255 var html = this.renderTopic(this.topicData); 256 viewDiv.innerHTML = html; 257 258 // attach the rollover listeners 259 Alfresco.util.rollover.registerListenersByClassName(this.id, 'topic', 'div'); 260 }, 261 262 /** 263 * Renders the topic. 264 * 265 * @param data {object} the data object containing the topic data 266 * @return {string} html representing the data 267 */ 268 renderTopic: function DiscussionsTopic_renderTopic(data) 269 { 270 var html = ''; 271 272 html += '<div id="' + this.id + '-topicview" class="node topic topicview">' 273 274 // actions 275 html += '<div class="nodeEdit">'; 276 if (data.permissions.reply) 277 { 278 html += '<div class="onAddReply"><a href="#" class="topic-action-link-div">' + this._msg("action.reply") + '</a></div>'; 279 } 280 if (data.permissions.edit) 281 { 282 html += '<div class="onEditTopic"><a href="#" class="topic-action-link-div">' + this._msg("action.edit") + '</a></div>'; 283 } 284 if (data.permissions['delete']) 285 { 286 html += '<div class="onDeleteTopic"><a href="#" class="topic-action-link-div">' + this._msg("action.delete") + '</a></div>'; 287 } 288 html += '</div>'; 289 290 // avatar 291 html += '<div class="authorPicture">' + Alfresco.util.people.generateUserAvatarImg(data.author) + '</div>'; 292 293 // content 294 html += '<div class="nodeContent">'; 295 html += '<div class="nodeTitle"><a href="' + Alfresco.util.discussions.getTopicViewPage(this.options.siteId, this.options.containerId, data.name) + '">' + $html(data.title) + '</a> '; 296 if (data.isUpdated) 297 { 298 html += '<span class="theme-color-2 nodeStatus">(' + this._msg("post.updated") + ')</span>'; 299 } 300 html += '</div>'; 301 302 html += '<div class="published">'; 303 html += '<span class="nodeAttrLabel">' + this._msg("post.createdOn") + ': </span>'; 304 html += '<span class="nodeAttrValue">' + Alfresco.util.formatDate(data.createdOn) + '</span>'; 305 html += '<span class="separator"> </span>'; 306 html += '<span class="nodeAttrLabel">' + this._msg("post.author") + ': </span>'; 307 html += '<span class="nodeAttrValue">' + Alfresco.util.people.generateUserLink(data.author) + '</span>'; 308 html += '<br />'; 309 if (data.lastReplyBy) 310 { 311 html += '<span class="nodeAttrLabel">' + this._msg("post.lastReplyBy") + ': </span>'; 312 html += '<span class="nodeAttrValue">' + Alfresco.util.people.generateUserLink(data.lastReplyBy) + '</span>'; 313 html += '<span class="separator"> </span>'; 314 html += '<span class="nodeAttrLabel">' + this._msg("post.lastReplyOn") + ': </span>'; 315 html += '<span class="nodeAttrValue">' + Alfresco.util.formatDate(data.lastReplyOn) + '</span>'; 316 } 317 else 318 { 319 html += '<span class="nodeAttrLabel">' + this._msg("replies.label") + ': </span>'; 320 html += '<span class="nodeAttrValue">' + this._msg("replies.noReplies") + '</span>'; 321 } 322 html += '</div>'; 323 324 html += '<div class="userLink">' + Alfresco.util.people.generateUserLink(data.author) + ' ' + this._msg("said") + ':</div>'; 325 html += '<div class="content yuieditor">' + data.content + '</div>'; 326 html += '</div>' 327 // end view 328 329 // begin footer 330 html += '<div class="nodeFooter">'; 331 html += '<span class="nodeAttrLabel replyTo">' + this._msg("replies.label") + ': </span>'; 332 html += '<span class="nodeAttrValue">(' + data.totalReplyCount + ')</span>'; 333 html += '<span class="separator"> </span>'; 334 335 html += '<span class="nodeAttrLabel tagLabel">' + this._msg("tags.label") +': </span>'; 336 if (data.tags.length > 0) 337 { 338 for (var x=0; x < data.tags.length; x++) 339 { 340 if (x > 0) 341 { 342 html += ", "; 343 } 344 html += Alfresco.util.tags.generateTagLink(this, data.tags[x]); 345 } 346 } 347 else 348 { 349 html += '<span class="nodeAttrValue">' + this._msg("tags.noTags") + '</span>'; 350 } 351 html += '</div></div></div>'; 352 353 return html; 354 }, 355 356 /** 357 * Handler for add reply action link 358 */ 359 onAddReply: function DiscussionsTopic_onAddReply(htmlId, ownerId, param) 360 { 361 YAHOO.Bubbling.fire('addReplyToPost', 362 { 363 postRef : this.topicData.nodeRef 364 }); 365 }, 366 367 /** 368 * Handler for edit topic action link 369 */ 370 onEditTopic: function DiscussionsTopic_onEditTopic() 371 { 372 window.location.href = Alfresco.constants.URL_PAGECONTEXT + "site/" + this.options.siteId + "/discussions-createtopic?topicId=" + this.options.topicId; 373 }, 374 375 /** 376 * Handler for delete topic action link 377 */ 378 onDeleteTopic: function DiscussionsTopic_onDeleteTopic() 379 { 380 var me = this; 381 Alfresco.util.PopupManager.displayPrompt( 382 { 383 title: this._msg("message.confirm.delete.title"), 384 text: this._msg("message.confirm.delete", $html(this.topicData.title)), 385 buttons: [ 386 { 387 text: this._msg("button.delete"), 388 handler: function DiscussionsTopic_onDeleteTopic_delete() 389 { 390 this.destroy(); 391 me._deleteTopicConfirm.call(me); 392 } 393 }, 394 { 395 text: this._msg("button.cancel"), 396 handler: function DiscussionsTopic_onDeleteTopic_cancel() 397 { 398 this.destroy(); 399 }, 400 isDefault: true 401 }] 402 }); 403 }, 404 405 /** 406 * Delete topic implementation 407 */ 408 _deleteTopicConfirm: function DiscussionsTopic__deleteTopicConfirm() 409 { 410 // show busy message 411 if (! this._setBusy(this._msg('message.wait'))) 412 { 413 return; 414 } 415 416 // ajax request success handler 417 var onDeleted = function onDeleted(response) 418 { 419 var listUrl = YAHOO.lang.substitute(Alfresco.constants.URL_PAGECONTEXT + "site/{site}/discussions-topiclist", 420 { 421 site: this.options.siteId 422 }); 423 window.location = listUrl; 424 }; 425 426 // construct the url to call 427 var url = YAHOO.lang.substitute(Alfresco.constants.PROXY_URI + "api/forum/post/site/{site}/{container}/{topicId}?page=discussions-topicview", 428 { 429 site : this.options.siteId, 430 container: this.options.containerId, 431 topicId: encodeURIComponent(this.options.topicId) 432 }); 433 434 // perform the ajax request to delete the topic 435 Alfresco.util.Ajax.request( 436 { 437 url: url, 438 method: "DELETE", 439 responseContentType : "application/json", 440 successMessage: this._msg("message.delete.success"), 441 successCallback: 442 { 443 fn: onDeleted, 444 scope: this 445 }, 446 failureMessage: this._msg("message.delete.failure"), 447 failureCallback: 448 { 449 fn: function(response) 450 { 451 this._releaseBusy(); 452 }, 453 scope: this 454 } 455 }); 456 }, 457 458 /** 459 * Tag selected handler 460 * 461 * @method onTagSelected 462 * @param tagId {string} Tag name. 463 * @param target {HTMLElement} Target element clicked. 464 */ 465 onTagSelected: function DiscussionsTopic_onTagSelected(layer, args) 466 { 467 var obj = args[1]; 468 if (obj && (obj.tagName !== null)) 469 { 470 // construct the topic list url with initial active tag filter 471 var url = YAHOO.lang.substitute(Alfresco.constants.URL_PAGECONTEXT + "site/{site}/discussions-topiclist?filterId={filterId}&filterOwner={filterOwner}&filterData={filterData}", 472 { 473 site: this.options.siteId, 474 filterId: "tag", 475 filterOwner: "Alfresco.TagFilter", 476 filterData: encodeURIComponent(obj.tagName) 477 }); 478 479 window.location = url; 480 } 481 }, 482 483 /** 484 * Loads the edit form. 485 */ 486 _loadEditForm: function DiscussionsTopic__loadEditForm() 487 { 488 // Load the UI template from the server 489 Alfresco.util.Ajax.request( 490 { 491 url: Alfresco.constants.URL_SERVICECONTEXT + "modules/discussions/topic/edit-topic", 492 dataObj: 493 { 494 htmlid: this.id + "-form" 495 }, 496 successCallback: 497 { 498 fn: this.onEditFormLoaded, 499 scope: this 500 }, 501 failureMessage: this._msg("message.loadeditform.failure") 502 }); 503 }, 504 505 /** 506 * Event callback when dialog template has been loaded 507 * 508 * @method onFormLoaded 509 * @param response {object} Server response from load template XHR request 510 */ 511 onEditFormLoaded: function DiscussionsTopic_onEditFormLoaded(response) 512 { 513 // id to use for the form 514 var formId = this.id + "-form"; 515 516 // use the already loaded data 517 var data = this.topicData; 518 519 // find the edit div to populate 520 var editDiv = Dom.get(this.id + "-topic-edit-div"); 521 522 // insert the html 523 editDiv.innerHTML = response.serverResponse.responseText; 524 525 // insert current values into the form 526 var actionUrl = YAHOO.lang.substitute(Alfresco.constants.PROXY_URI + "api/forum/post/site/{site}/{container}/{topicId}", 527 { 528 site: this.options.siteId, 529 container : this.options.containerId, 530 topicId: this.options.topicId 531 }); 532 Dom.get(formId + "-form").setAttribute("action", actionUrl); 533 Dom.get(formId + "-site").setAttribute("value", this.options.siteId); 534 Dom.get(formId + "-container").setAttribute("value", this.options.containerId); 535 Dom.get(formId + "-title").setAttribute("value", data.title); 536 Dom.get(formId + "-content").value = data.content; 537 538 // and finally register the form handling 539 this._registerEditTopicForm(data, formId); 540 }, 541 542 /** 543 * Registers the form logic 544 */ 545 _registerEditTopicForm: function DiscussionsTopic__registerEditTopicForm(data, formId) 546 { 547 // add the tags that are already set on the post 548 if (this.modules.tagLibrary == undefined) 549 { 550 this.modules.tagLibrary = new Alfresco.module.TagLibrary(formId); 551 this.modules.tagLibrary.setOptions( 552 { 553 siteId: this.options.siteId 554 }); 555 } 556 this.modules.tagLibrary.setTags(this.topicData.tags); 557 558 // register the okButton 559 this.widgets.okButton = new YAHOO.widget.Button(formId + "-submit", 560 { 561 type: "submit" 562 }); 563 564 // register the cancel button 565 this.widgets.cancelButton = new YAHOO.widget.Button(formId + "-cancel", 566 { 567 type: "button" 568 }); 569 this.widgets.cancelButton.subscribe("click", this.onEditFormCancelButtonClick, this, true); 570 571 // instantiate the simple editor we use for the form 572 this.widgets.editor = new YAHOO.widget.SimpleEditor(formId + '-content', 573 { 574 height: '180px', 575 width: '700px', 576 dompath: false, //Turns on the bar at the bottom 577 animate: false, //Animates the opening, closing and moving of Editor windows 578 toolbar: Alfresco.util.editor.getTextOnlyToolbarConfig(this._msg) 579 }); 580 this.widgets.editor.addPageUnloadBehaviour(this._msg("message.unsavedChanges.reply")); 581 this.widgets.editor.render(); 582 583 // create the form that does the validation/submit 584 var editForm = new Alfresco.forms.Form(formId + "-form"); 585 editForm.setShowSubmitStateDynamically(true, false); 586 editForm.setSubmitElements(this.widgets.okButton); 587 editForm.setAjaxSubmitMethod(Alfresco.util.Ajax.PUT); 588 editForm.setAJAXSubmit(true, 589 { 590 successMessage: this._msg("message.savetopic.success"), 591 successCallback: 592 { 593 fn: this.onEditFormSubmitSuccess, 594 scope: this 595 }, 596 failureMessage: this._msg("message.savetopic.failure"), 597 failureCallback: 598 { 599 fn: this.onEditFormSubmitFailure, 600 scope: this 601 } 602 }); 603 editForm.setSubmitAsJSON(true); 604 editForm.doBeforeFormSubmit = 605 { 606 fn: function(form, obj) 607 { 608 // disable the buttons 609 this.widgets.okButton.set("disabled", true); 610 this.widgets.cancelButton.set("disabled", true); 611 612 //Put the HTML back into the text area 613 this.widgets.editor.saveHTML(); 614 615 // update the tags set in the form 616 this.modules.tagLibrary.updateForm(formId + "-form", "tags"); 617 618 // show a wait message 619 this.widgets.feedbackMessage = Alfresco.util.PopupManager.displayMessage( 620 { 621 text: Alfresco.util.message(this._msg("message.submitting")), 622 spanClass: "wait", 623 displayTime: 0 624 }); 625 }, 626 scope: this 627 }; 628 629 this.modules.tagLibrary.initialize(editForm); 630 editForm.init(); 631 632 // show the form and hide the view 633 this._showEditView(); 634 635 // TODO: disabled as it does not work correctly on IE. The focus is set 636 // but hitting tab moves the focus to the focus to the address bar instead 637 // of the editor 638 // focus the title text field 639 //Dom.get(formId + "-title").focus(); 640 }, 641 642 /** 643 * Edit form submit success handler 644 */ 645 onEditFormSubmitSuccess: function DiscussionsTopic_onEditFormSubmitSuccess(response, object) 646 { 647 // remove busy message 648 this._releaseBusy(); 649 650 // the response contains the new data for the comment. Render the comment html 651 // and insert it into the view element 652 this.topicData = response.json.item; 653 this.renderUI(); 654 655 // hide the form and show the ui 656 this._hideEditView(); 657 658 // inform the replies object about the update 659 this._fireTopicDataChangedEvent(); 660 }, 661 662 /** 663 * Edit form submit failure handler 664 */ 665 onEditFormSubmitFailure: function DiscussionsTopic_onEditFormSubmitFailure(response, object) 666 { 667 // remove busy message 668 this._releaseBusy(); 669 670 // enable the buttons 671 this.widgets.okButton.set("disabled", false); 672 this.widgets.cancelButton.set("disabled", false); 673 }, 674 675 /** 676 * Edit form cancel button click handler 677 */ 678 onEditFormCancelButtonClick: function(type, args) 679 { 680 this._hideEditView(); 681 }, 682 683 /** 684 * Hides the form and displays the view 685 */ 686 _hideEditView: function() 687 { 688 var editDiv = Dom.get(this.id + "-topic-edit-div"); 689 var viewDiv = Dom.get(this.id + "-topic-view-div"); 690 Dom.addClass(editDiv, "hidden"); 691 Dom.removeClass(viewDiv, "hidden"); 692 editDiv.innerHTML = ''; 693 }, 694 695 /** 696 * Hides the view and displays the form 697 */ 698 _showEditView: function() 699 { 700 var editDiv = Dom.get(this.id + "-topic-edit-div"); 701 var viewDiv = Dom.get(this.id + "-topic-view-div"); 702 Dom.addClass(viewDiv, "hidden"); 703 Dom.removeClass(editDiv, "hidden"); 704 }, 705 706 /** 707 * Called when the mouse enters into the topic div 708 */ 709 onTopicElementMouseEntered: function DiscussionsTopicList_onTopicElementMouseEntered(layer, args) 710 { 711 // make sure the user sees at least one action, otherwise we won't highlight 712 var permissions = this.topicData.permissions; 713 if (!(permissions.edit || permissions["delete"])) 714 { 715 return; 716 } 717 718 Dom.addClass(args[1].target, 'over'); 719 }, 720 721 /** 722 * Called whenever the mouse exits the topic div 723 */ 724 onTopicElementMouseExited: function DiscussionsTopicList_onTopicElementMouseExited(layer, args) 725 { 726 Dom.removeClass(args[1].target, 'over'); 727 }, 728 729 /** 730 * Fires a topic data changed bubble event 731 */ 732 _fireTopicDataChangedEvent: function DiscussionsTopicList__fireTopicDataChangedEvent() 733 { 734 var eventData = 735 { 736 topicRef: this.topicData.nodeRef, 737 topicTitle: this.topicData.title, 738 topicId: this.topicData.name 739 }; 740 YAHOO.Bubbling.fire("topicDataChanged", eventData); 741 }, 742 743 /** 744 * Displays the provided busyMessage but only in case 745 * the component isn't busy set. 746 * 747 * @return true if the busy state was set, false if the component is already busy 748 */ 749 _setBusy: function DiscussionsTopic__setBusy(busyMessage) 750 { 751 if (this.busy) 752 { 753 return false; 754 } 755 this.busy = true; 756 this.widgets.busyMessage = Alfresco.util.PopupManager.displayMessage( 757 { 758 text: busyMessage, 759 spanClass: "wait", 760 displayTime: 0 761 }); 762 return true; 763 }, 764 765 /** 766 * Removes the busy message and marks the component as non-busy 767 */ 768 _releaseBusy: function DiscussionsTopic__releaseBusy() 769 { 770 if (this.busy) 771 { 772 this.widgets.busyMessage.destroy(); 773 this.busy = false; 774 return true; 775 } 776 else 777 { 778 return false; 779 } 780 }, 781 782 /** 783 * Gets a custom message 784 * 785 * @method _msg 786 * @param messageId {string} The messageId to retrieve 787 * @return {string} The custom message 788 * @private 789 */ 790 _msg: function DiscussionsTopic_msg(messageId) 791 { 792 return Alfresco.util.message.call(this, messageId, "Alfresco.DiscussionsTopic", Array.prototype.slice.call(arguments).slice(1)); 793 } 794 }; 795 })(); 796