Monday, November 23, 2009

Alfresco Share - Reverse engineering the documentlibrary toolbar - part 2

In this part, we'll create the webscripts required to add our custom button to Alfresco Share. We will also extend the original javascript object to include our Button & event handler.

***************************************************
Part 2
YUI version 2.7.0 (as shown in utilities.js)
Using an alternate method to override the ootb toolbar than what is detailed on http://edlovesjava.blogspot.com/2008/12/extending-share-2-adding-new-content.html
***************************************************
Description of files


Other javascript files of importance:

Alfresco.js – Creates a number of Alfresco base classes & util methods & constants; This class encapsulates much of the YUI code.

<yui js files> -- There are a number of YUI javascript files that supply the foundation for many (if not all) of the events, widgets, etc of Share. I didn’t dig too far directly into these files.

The files in use by the toolbar component webscript:

toolbar.js – reusable from static location; contains YUI objects, event handlers, actions & callbacks; extends

toolbar.css – std css file; used for styling the toolbar

toolbar.get.properties – strings; containing the button text, etc

toolbar.get.js – Collects user preferences and loads them into the model

toolbar.get.html.ftl – layout details, instantiates dynamic objects as defined in toolbar.js

toolbar.get.head.ftl – incorporates js and css into page for the toolbar, as well as javascript files used directly by some of the widgets within the toolbar

toolbar.get.desc.xml – Standard webscript description file

The files used to populate the popup for our create-content metadata:

create-content.get.desc.xml - webscript descriptor

create-content.get.html.ftl - template for the html form

create-content.get.properties - simple properties file

***************************************************

High level description

  1. As found in part 1, the file template.toolbar.documentlibrary.xml will control whether or not our custom component is used in the container. I did not find any hierarchical directory loading order similar to how Documentum loads its wdk components with the custom directory last and any component in that directory will override any previously defined components.

  2. Once pointed to our toolbar component, we will render the toolbar by using our webscript. (be sure to load the webscript by refreshing webscripts at /share/service/index.html)

  3. Our toolbar webscript will extend the existing toolbar code, only changing what we need to for our code to run.


***************************************************

Code Detail:

  • ·         Note that this code is from the edlovesjava blog referenced above, the only change is how I implemented the YUI button in custom-extend-toolbar.js

o   I had issues when trying to build the code as supplied from the original blog. Therefore I took my own route when debugging the YUI based javascript file





The new webscripts, which we will create in the following steps, will be placed into the share/WEB-INF/classes/alfresco/site-webscripts/custom/components/documentlibrary.

Be sure to refresh the webscripts by navigating to http://<host>:<port>/share/service/index.html and clicking on the "refresh web scripts" button.

I have commented in the below files in red.

Click here to download project files

The details for the create-content form:

1. Descriptor for create-content form:


<webscript>
<shortname>create-content</shortname>
<description>Create Content module primarily for Document Library
</description>
<url>/components/documentlibrary/create-content</url>
</webscript>


2. Template for create-content form:


<div id="${args.htmlid}-dialog" class="create-folder">
<div class="hd">${msg("title")}</div>
<div class="bd">
<form id="${args.htmlid}-form" action="" method="post" accept-charset="utf-8">
<div class="yui-g">
<h2>${msg("header")}:</h2>
</div>
<div class="yui-gd">
<div class="yui-u first"><label for="${args.htmlid}-name">${msg("label.name")}:</label></div>
<div class="yui-u"><input id="${args.htmlid}-name" type="text" name="name" tabindex="1" />&nbsp;*</div>
</div>
<div class="yui-gd">
<div class="yui-u first"><label for="${args.htmlid}-title">${msg("label.title")}:</label></div>
<div class="yui-u"><input id="${args.htmlid}-title" type="text" name="title" tabindex="2" /></div>
</div>
<div class="yui-gd">
<div class="yui-u first"><label for="${args.htmlid}-description">${msg("label.description")}:</label></div>
<div class="yui-u"><textarea id="${args.htmlid}-description" name="description" rows="3" cols="20" tabindex="3" ></textarea></div>
</div>
<div class="yui-gd">
<div class="yui-u first"><label for="${args.htmlid}-content">${msg("label.content")}:</label></div>
<div class="yui-u"><textarea id="${args.htmlid}-content" name="content" rows="3" cols="20" tabindex="3" ></textarea></div>
</div>
<div class="bdft">
<input type="button" id="${args.htmlid}-ok" value="${msg("button.ok")}" tabindex="4" />
<input type="button" id="${args.htmlid}-cancel" value="${msg("button.cancel")}" tabindex="5" />
</div>
</form>
</div>
</div>

</code></pre>

3. Properties for create-content form:

## Titles
title=New Content
header=New Content Details

## Labels
label.name=Name
label.title=Title
label.description=Description
label.content=Content


The details for the toolbar webscript:


1.       Create a copy of toolbar.get.head.ftl & modify it as such:

<!-- DocListToolbar Assets -->
<link rel="stylesheet" type="text/css" href="${page.url.context}/components/documentlibrary/toolbar.css" />
<script type="text/javascript" src="${page.url.context}/components/documentlibrary/toolbar.js"></script>
<!-- custom Assets -->
link to our css
<link rel="stylesheet" type="text/css" href="${page.url.context}/components/documentlibrary/custom.toolbar.css" />
link to our javascript file; note that it is after toolbar.js

<script type="text/javascript" src="${page.url.context}/components/documentlibrary/custom-extend-toolbar.js"></script>
<!-- Simple Dialog Assets -->
<script type="text/javascript" src="${page.url.context}/modules/simple-dialog.js"></script>
<!-- File-Upload Assets -->
<link rel="stylesheet" type="text/css" href="${page.url.context}/modules/flash-upload.css" />
<script type="text/javascript" src="${page.url.context}/modules/flash-upload.js"></script>
<link rel="stylesheet" type="text/css" href="${page.url.context}/modules/html-upload.css" />
<script type="text/javascript" src="${page.url.context}/modules/html-upload.js"></script>
<script type="text/javascript" src="${page.url.context}/modules/file-upload.js"></script>

2.       Create a copy of toolbar.get.html.ftl & modify it as such:
<snip>
<div class="new-folder hideable DocListTree"><button id="${args.htmlid}-newFolder-button" name="newFolder">${msg("button.new-folder")}</button></div>
a div containing our new button
<div class="new-content hideable DocListTree"><button id="${args.htmlid}-newContent-button" name="newContent">${msg("button.new-content")}</button></div>
<div class="separator hideable DocListTree">&nbsp;</div>
</snip>

3.       Create a copy of toolbar.get.properties & modify is by adding the following line:

<snip>
button.new-content=New Content
</snip>

4.       Create toolbar.get.desc.xml

<webscript>
<shortname>DocLib Toolbar</shortname>
<description>Document Library: Toolbar Component</description
<url>/custom/components/documentlibrary/toolbar</url>
</webscript>



These are the parts of the webscript that will override the ootb toolbar.

Next are the javascript and css file for the new button. These files are deployed to the <share webapp>/components/documentlibrary directory:

The code for the css file:

.toolbar .new-content button
{
background: transparent url(images/content-new-16.png) no-repeat 12px 4px;
padding-left: 32px;
}
.toolbar .new-content .yui-button-disabled button
{
background-image: url(images/content-new-disabled-16.png);
}


The code for the javascript file:


Please don't attempt to copy/paste this code and expect it to compile. I have added non-code-safe comments, but I have also drastically altered the formatting to make it blog-friendly, so I may have clipped a char or two which could prevent compilation. 


Please use the downloaded tar file for runnable code.

/**
 * DocumentList Toolbar component.
 *
 * @namespace Alfresco
 * @class Alfresco.DocListToolbar
 */
(function()
{
   /**
    * YUI Library aliases
    */
   We create this class in order to leverage the setStyle function further in the code.
   var Dom = YAHOO.util.Dom;
   /**
    * Alfresco Slingshot aliases
    */
   The $html isn't used and can probably be removed (I didn't test), but the $combine var is used in building the actionUrl further in the code. I copied this line as-is, therefore $html was carried over.
   var $html = Alfresco.util.encodeHTML,
$combine = Alfresco.util.combinePaths;  

   /**
    * CustomDocListToolbar constructor.
    *
    * @param {String} htmlId The HTML id of the parent element
    * @return {Alfresco.DocListToolbar} The new DocListToolbar instance
    * @constructor
    */
   This is the constructor, which is referenced from the html template of our toolbar webscript. We do not have anything additional to provide to the constructor, therefore we call the superclass' constructor and return.
   Alfresco.CustomDocListToolbar = function(htmlId)
   {
                  Alfresco.CustomDocListToolbar.superclass.constructor.call(this,htmlId);
      return this;
 };
 
Commented line that should have been removed before posting this :)
   //Alfresco.CustomDocListToolbar.prototype =  

Here is the main difference between my javascript and the js listed in the edlovesjava blog. I cannot say which method is preferred. Both methods do allow for a reduction in ootb, core code.

The extend function takes three parameters: subtype, supertype, overrides. Overrides is optional, and most online examples do not demonstrate its use. But, in our situation, it's exactly what we need to add our code to precisely the literals we require.

   YAHOO.lang.extend(Alfresco.CustomDocListToolbar, Alfresco.DocListToolbar, {
/**
* Fired by YUI when parent element is available for scripting.
* Component initialisation, including instantiation of YUI widgets and event listener binding.
*
* @method onReady
*/
Fired by YUI when template's DOM has been constructed and ready for use.
onReady: function DLTB_onReady()
{
The createYUIButton method is a helper method that encapsulates some housekeeping tasks in creating a YAHOO.widget.Button. the parameters are (as from alfresco.js)
1. Component containing button; must have "id" parameter
2. DOM element ID of markup that button is created from the id contained in param #1 concatenated with "-<name>"
3. function registered with the button's click event
4. optional extra object parameters to pass to button constructor

// New Content button: user needs "create" access
this.widgets.newContent = Alfresco.util.createYUIButton(this,"newContent-button", this.onNewContent,
{
     disabled : true,
     value : "create"
});

The following is the ootb code from toolbar.js.                                            
// New Folder button: user needs "create" access
this.widgets.newFolder = Alfresco.util.createYUIButton(this, "newFolder-button", this.onNewFolder,
{
            disabled: true,
            value: "create"
});                   
// File Upload button: user needs  "create" access
this.widgets.fileUpload = Alfresco.util.createYUIButton(this, "fileUpload-button", this.onFileUpload,
{
           disabled: true,
           value: "create"
});

// Selected Items menu button
this.widgets.selectedItems = Alfresco.util.createYUIButton(this, "selectedItems-button", this.onSelectedItems,
{
            type: "menu",
            menu: "selectedItems-menu",
            disabled: true
});
// Clear the lazyLoad flag and fire init event to get menu rendered into the DOM
this.widgets.selectedItems.getMenu().lazyLoad = false;
this.widgets.selectedItems.getMenu().initEvent.fire();
this.widgets.selectedItems.getMenu().render();
                         // Customize button
                         this.widgets.customize = Alfresco.util.createYUIButton(this, "customize-button", this.onCustomize);
                         // Hide/Show NavBar button
this.widgets.hideNavBar = Alfresco.util.createYUIButton(this, "hideNavBar-button", this.onHideNavBar);
this.widgets.hideNavBar.set("label", this._msg(this.options.hideNavBar ? "button.navbar.show" : "button.navbar.hide"));


Dom.setStyle(this.id + "-navBar", "display", this.options.hideNavBar ? "none" : "block");                      

// RSS Feed link button

this.widgets.rssFeed = Alfresco.util.createYUIButton(this, "rssFeed-button", null,
{
           type: "link"
});

// Folder Up Navigation button
this.widgets.folderUp =  Alfresco.util.createYUIButton(this, "folderUp-button", this.onFolderUp,
{
            disabled: true
});

// DocLib Actions module
this.modules.actions = new Alfresco.module.DoclibActions();

// Reference to Document List component
this.modules.docList = Alfresco.util.ComponentManager.findFirst("Alfresco.DocumentList");

// Preferences service
this.services.preferences = new Alfresco.service.Preferences();

Note this setStyle function. During development, there were many times where the javascript would render to the screen, typically incorrect, but the style would be visibility: hidden. There's nothing in the css to show the elements. It's this javascript function that must fire in order to allow the toolbar to be seen.
// Finally show the component body here to prevent UI artifacts on YUI button decoration          Dom.setStyle(this.id + "-body", "visibility", "visible");
},

/**
* New Content button click handler
*
* @method onNewContent
* @param e
*            {object} DomEvent
* @param p_obj
*            {object} Object passed back from addListener method
*/
The event handler for our new button.
onNewContent : function DLTB_onNewContent(e, p_obj) {
the actionUrl is the url to the webscript that will actually implement our logic, that is, the create content. NOTE that at this point, I still have it pointed to the create folder action. You can follow the steps in the edlovesjava blog in order to create and implement the create content webscript.
var actionUrl = Alfresco.constants.PROXY_URI + $combine("slingshot/doclib/action/folder/site", this.options.siteId, this.options.containerId, this.currentPath);

Prepare the validation details for the popup form
var doSetupFormsValidation = function DLTB_oNF_doSetupFormsValidation(p_form) {
// Validation
// Name: mandatory value
p_form.addValidation(this.id + "-createContent-name",
Alfresco.forms.validation.mandatory, null, "keyup");
// Name: valid filename                                                                              
p_form.addValidation(this.id + "-createContent-name",                                                                                                               Alfresco.forms.validation.nodeName, null, "keyup");
p_form.setShowSubmitStateDynamically(true, false);
};

if form doesn't exist, build it
if (!this.modules.createContent) {                                                                          
this.modules.createContent = new Alfresco.module.SimpleDialog(this.id + "-createContent").setOptions( {
              width : "30em",
this file invokes the webscript that will create the popup form based on the create-content webscript we created earlier
              templateUrl :Alfresco.constants.URL_SERVICECONTEXT+"components/documentlibrary/create-content",
              actionUrl : actionUrl,
the validation variable as defined above
              doSetupFormsValidation : {                                                        
                            fn : doSetupFormsValidation,
                            scope : this
                                          },
Appears to be the callback that will set the cursor focus within the form
             firstFocus : this.id + "-createContent-name",
The success event handler, I believe this fires after the webscript as defined in the actionUrl completes
             onSuccess : {fn:functionDLTB_onCreateContent_callback(response) {
                            var file = response.json.results[0];
Using the Yahoo Broadcast layer, fire an event and supply some options. This calls out to all components that have 'attached' to this event via the YAHOO.Bubbling.on(<event>,<callback>,<scope>) function. In our case, "fileCreated" is not defined, so no action will occur. For example, the createFolder's event is "folderCreated" and it is defined in the parent componet's class which is contained in the <share-webapp>/components/documentlibrary/documentlist.js file.
                            YAHOO.Bubbling.fire("fileCreated", {
                                         name : file.name,
                                         parentPath : file.parentPath,
                                        nodeRef : file.nodeRef
                                                                                    });
Controls the display of the message that pops up after everything completes.                  
Alfresco.util.PopupManager.displayMessage( {
          text : this._msg("message.new-content.success",file.name)
                                                                       });
                                                        },
scope : this
                                     }
                 });
} else {
if the form has already been created, it is prepared for display
this.modules.createContent.setOptions( {
              actionUrl : actionUrl,
              clearForm : true
                                                              });
           }
display form
this.modules.createContent.show();
} end event handler
}); end yahoo extend
})(); end javascript class



After deploying the webscripts, the css, and the javascript files, we will be ready to switch to our customized toolbar by editing <share webapp>/WEB-INF/classes/alfresco/site-data/components template.toolbar.documentlibrary.xml
The contents of this file are updated to:

<component>
     <scope>template</scope>
     <region-id>toolbar</region-id>
     <source-id>documentlibrary</source-id>
     <url>/custom/components/documentlibrary/toolbar</url>
</component>

The <url> element is referenced from <share webapp>/WEB-INF/classes/alfresco/site-webscripts/components

Be sure to clear your browser’s cache before testing. For my testing, it would sometimes take a minute for the update to appear.

Please feel free to let me know if you run into any issues, or if any of my code doesn't appear to work. Also, please let me know if you have any comments about my method of extending the toolbar!

2 comments:

  1. Could you indicate where the source code is as the link does not work.
    Regards

    ReplyDelete
  2. All of the source should be at its original source on ed's blog:

    http://edlovesjava.blogspot.com/2008/12/extending-share-2-adding-new-content.html

    I don't want to claim that I wrote any of this from scratch, so I'll link back to his blog for the original code.

    (sorry for the very late response!)

    ReplyDelete