Adding a diff tool (I) to Alfresco Share interface by Adei Mandaluniz
One of the features of the ECM system is the version control of the documents stored in it. This allows the system users to store previous versions (not just the content but also the metadata) and revert to any of them at any moment.
One of the most asked features by our customers is the feature to compare two versions of the same file, so that they know what has been changed from one version to another. Alfresco doesn't provide that feature out-of-the-box, but can be easily added.
Just by doing some research, you can find that there are a lot of text comparing tools written in Java or Javascript:
jsdifflib - https://github.com/cemerick/jsdifflib
google-diff-match-patch - http://code.google.com/p/google-diff-match-patch/
diff-util - http://tsrtesttest.appspot.com/raw/diff-util_row
google-diff-match-patch - http://code.google.com/p/google-diff-match-patch/
Diff Utils - http://code.google.com/p/java-diff-utils/
There are much more but those are the first ones turning up in Google. Depending on the requirements you will be using one or another. For this blog, we chose to use the Javascript version of the google-diff-match-patch library.
Implementing the extension
We will add the comparison viewer as an overlay/pop-up in the Share UI.
The first thing we need to do is add the “Compare” button to the Document Details page for the older versions of the document just as the picture shows in the background.
For that we need to modify the existing document-versions webscript adding the button:
<#if version_index != 0> <span class="hidden"><a id="${args.htmlid}-revert-a-${version_index}" class="revert" href="#">${msg("link.revert")}</a></span> <span class="hidden"><a id="${args.htmlid}-compare-a-${version_index}" class="compare" href="#">${msg("link.compare")}</a></span> </#if>
As you can see in the code above, a new span line has been added for the button. That line uses the “link.compare” property, so it will need to be added to the properties file:
link.download=Download link.revert=Revert link.compare=Compare message.revertComplete=The document was reverted. The page will be reloaded.
The style for the new span will also need to be added to the CSS file used by the webscript:
.document-versions .actions span a.compare { background-image: url(../images/diff-16.png); }
NOTE: Do not forget adding the image!
At this point you can test that the button is there by deploying it. Of course, the button will do nothing.
To make the button work, we will need to modify the document-version.js file by adding a listener for each “Compare” button. We can just follow the way Alfresco has done it for the “Revert” button:
var comparer = Dom.get(this.id + "-compare-a-" + i); if (comparer) { Event.addListener(comparer, "click", function (event, obj) { // Stop browser from using href attribute Event.preventDefault(event); // Find the index of the version link by looking at its id version = versions[obj.versionIndex]; // Find the version through the index and display the revert dialog for the version Alfresco.module.getCompareVersionInstance().show( { filename: this.options.filename, currentNodeRef: this.options.nodeRef, versionNodeRef: version.nodeRef, versionLabel: version.label }); }, { versionIndex: i }, this); }
As you’ll see in the code, it creates an Alfresco.module.CompareVersion instance. This is the javascript object that will show the result of the comparison.
/** * Diff tool component. * * Popups a YUI panel showing the difference between two versions. * * @namespace Alfresco.module * @class Alfresco.module.CompareVersion */ (function() { /** * CompareVersion constructor. * * CompareVersion is considered a singleton so constructor should be treated as private, * please use Alfresco.module.getCompareVersionInstance() instead. * * @param {string} htmlId The HTML id of the parent element * @return {Alfresco.module.CompareVersion} The new CompareVersion instance * @constructor * @private */ Alfresco.module.CompareVersion = function(containerId) { this.name = "Alfresco.module.CompareVersion"; this.id = containerId; var instance = Alfresco.util.ComponentManager.get(this.id); if (instance !== null) { throw new Error("An instance of Alfresco.module.CompareVersion already exists."); } /* Register this component */ Alfresco.util.ComponentManager.register(this); // Load YUI Components Alfresco.util.YUILoaderHelper.require(["button", "container", "datatable", "datasource"], this.onComponentsLoaded, this); return this; }; Alfresco.module.CompareVersion.prototype = { /** * The default config for the gui state for the compare dialog. * The user can override these properties in the show() method. * * @property defaultShowConfig * @type object */ defaultShowConfig: { filename: null, currentNodeRef: null, versionNodeRef: null, versionLabel: null }, /** * The merged result of the defaultShowConfig and the config passed in * to the show method. * * @property defaultShowConfig * @type object */ showConfig: {}, /** * Object container for storing YUI widget and HTMLElement instances. * * @property widgets * @type object */ widgets: {}, /** * Fired by YUILoaderHelper when required component script files have * been loaded into the browser. * * @method onComponentsLoaded */ onComponentsLoaded: function RV_onComponentsLoaded() { // Shortcut for dummy instance if (this.id === null) { return; } }, /** * Show can be called multiple times and will display the revert dialog * in different ways depending on the config parameter. * * @method show * @param config {object} describes how the revert dialog should be displayed * The config object is in the form of: * { * nodeRef: {string}, // the nodeRef to revert * version: {string} // the version to revert nodeRef to * } */ show: function RV_show(config) { // Merge the supplied config with default config and check mandatory properties this.showConfig = YAHOO.lang.merge(this.defaultShowConfig, config); if (this.showConfig.currentNodeRef === undefined && this.showConfig.versionNodeRef === undefined && this.showConfig.versionLabel === undefined) { throw new Error("A nodeRef and version must be provided"); } var templateUrl = YAHOO.lang.substitute(Alfresco.constants.URL_SERVICECONTEXT + "modules/document-details/diff-tool?htmlid={htmlid}¤tNodeRef={currentNodeRef}&fileName={fileName}&versionNodeRef={versionNodeRef}&versionLabel={versionLabel}", { htmlid: this.id, currentNodeRef: config.currentNodeRef, fileName: config.filename, versionNodeRef: config.versionNodeRef, versionLabel: config.versionLabel }); // If it hasn't load the gui (template) from the server Alfresco.util.Ajax.request( { url: templateUrl, successCallback: { fn: this.onTemplateLoaded, scope: this }, failureMessage: "Could not load html compare template", execScripts: true }); }, /** * Called when the compare dialog html template has been returned from the server. * Creates the YIU gui objects such as the panel. * * @method onTemplateLoaded * @param response {object} a Alfresco.util.Ajax.request response object */ onTemplateLoaded: function RV_onTemplateLoaded(response) { var Dom = YAHOO.util.Dom; // Inject the template from the XHR request into a new DIV element var containerDiv = document.createElement("div"); containerDiv.innerHTML = response.serverResponse.responseText; // Create the panel from the HTML returned in the server reponse var dialogDiv = YAHOO.util.Dom.getFirstChild(containerDiv); this.widgets.panel = Alfresco.util.createYUIPanel(dialogDiv); // Save a reference to the HTMLElement displaying texts so we can alter the texts later this.widgets.headerText = Dom.get(this.id + "-header-span"); this.widgets.versionsnMenuButton = Alfresco.util.createYUIButton(this, "version-button", this.onVersionSelectChange, { label: "Select version", title: "Select a version", type: "menu", menu: "version-menu" }); // Draw the differences this.drawDiffs(); this.widgets.cancelButton = Alfresco.util.createYUIButton(this, "cancel-button", this.onCancelButtonClick); // Show panel this._showPanel(); }, drawDiffs: function CP_drawDiffs() { // Diff_match var dmp = new diff_match_patch(); var text1 = YAHOO.util.Dom.get(this.id + "-previousVersion").value; var text2 = YAHOO.util.Dom.get(this.id + "-currentVersion").value; dmp.Diff_Timeout = parseFloat("3"); dmp.Diff_EditCost = parseFloat("4"); var d = dmp.diff_main(text2, text1); dmp.diff_cleanupSemantic(d); var ds = dmp.diff_prettyHtml(d); document.getElementById('diffoutputdiv').innerHTML = ds; }, /** * Fired when the user clicks the cancel button. * Closes the panel. * * @method onCancelButtonClick * @param event {object} a Button "click" event */ onCancelButtonClick: function RV_onCancelButtonClick() { // Hide the panel this.widgets.panel.hide(); }, /** * Adjust the gui according to the config passed into the show method. * * @method _applyConfig * @private */ _applyConfig: function RV__applyConfig() { var Dom = YAHOO.util.Dom; // Set the panel section var header = Alfresco.util.message("header.compare", this.name, { "0": this.showConfig.filename, "1": this.showConfig.versionLabel }); this.widgets.headerText["innerHTML"] = header; this.widgets.cancelButton.set("disabled", false); }, /** * Prepares the gui and shows the panel. * * @method _showPanel * @private */ _showPanel: function RV__showPanel() { // Apply the config before it is showed this._applyConfig(); // Show the revert panel this.widgets.panel.show(); } }; })(); Alfresco.module.getCompareVersionInstance = function() { var instanceId = "alfresco-compareVersion-instance"; return Alfresco.util.ComponentManager.get(instanceId) || new Alfresco.module.CompareVersion(instanceId); }
As you will see in the code above, it calls a webscript to get back the current content and the content of the version to compare it with:
var templateUrl = YAHOO.lang.substitute(Alfresco.constants.URL_SERVICECONTEXT + "modules/document-details/diff-tool?htmlid={htmlid}¤tNodeRef={currentNodeRef}&fileName={fileName}&versionNodeRef={versionNodeRef}&versionLabel={versionLabel}", { htmlid: this.id, currentNodeRef: config.currentNodeRef, fileName: config.filename, versionNodeRef: config.versionNodeRef, versionLabel: config.versionLabel });
The webscript will look like the following:
<webscript> <shortname>Show differences</shortname> <description>Show differences with the current version</description> <url>/modules/document-details/diff-tool</url> </webscript>
<#assign el=args.htmlid?html> <div id="${el}-dialog" class="compare-version"> <div class="hd"> <span id="${el}-header-span"></span> </div> <div class="bd"> <input type="hidden" id="${el}-currentVersion" name="currentVersion" value="${currentVersion?string}" /> <input type="hidden" id="${el}-previousVersion" name="previousVersion" value="${previousVersion?string}" /> <div id="diffoutputdiv" style="max-height:350px;max-width:800px;overflow:auto"></div> </div> <div class="bdft"> <input id="${el}-cancel-button" type="button" value="${msg("button.cancel")}" /> </div> </div> </div> <script type="text/javascript">//<![CDATA[ Alfresco.util.addMessages(${messages}, "Alfresco.module.DiffTool"); //]]></script>
diff-tool.get.properties :
header.compare=Comparing {0} with version {1}
function main() { // allow for content to be loaded from id if (args.currentNodeRef != null) { var currentContentString = remote.call("/api/node/content/" + args.currentNodeRef.replace(":/", "") + "/" + args.fileName + "?a=false"); var versionContentString = remote.call("/api/node/content/" + args.versionNodeRef.replace(":/", "") + "/" + args.fileName + "?a=false"); model.currentVersion = currentContentString model.previousVersion = versionContentString; model.versionLabel = args.versionLabel; } } main();
This webscript calls an Alfresco webscript to get the content of the different versions of the node and place the strings into two hidden fields so that the Alfresco.module.CompareVersion can perform the comparison and put the result diffoutputdiv div.