Image gallery from attachments in tooltips

Do you want to obtain a small image gallery display in a tooltip based on a files attached to an item?

Not too complicated to obtain this. We just need JavaScript tooltipster and DataTable library and CSS files. But first let’s build the code for getting attachments based on item id.

Type.registerNamespace('Shp');

Shp.Lists = function () {
            throw 'Cannot instantiate Shp.Lists static class';
};

 Shp.Lists.GetAttachments = function (listName, itemId, webUrl, success, fail) {
            /// <summary>Get attachments for an item</summary>
            /// <param name="listName" type="String" optional="false" mayBeNull="false">List name</param>
            /// <param name="itemId" type="Number" integer="true" optional="false" mayBeNull="false">List item</param>
            /// <param name="success" type="Function" optional="false" mayBeNull="false">Success</param>
            /// <param name="fail" type="Function" optional="true" mayBeNull="false">Fail</param>
            var e = Function.validateParameters(arguments, [{ name: 'listName', type: String, mayBeNull: false, optional: false },
                                                            { name: 'itemId', type: Number, integer: true, mayBeNull: false, optional: false },
                                                            { name: 'webUrl', type: String, mayBeNull: true, optional: false },
                                                            { name: 'success', type: Function, mayBeNull: false, optional: false },
                                                            { name: 'fail', type: Function, mayBeNull: false, optional: true }], true);
            if (e) throw 'Shp.Lists.GetAttachments was called with invalid parameters. ' + e;
            var fail = fail || (function (err) { alert(err); });
            var ctx = (webUrl === null) ? SP.ClientContext.get_current() : new SP.ClientContext(webUrl);

            Shp.Lists._GetAttachments(listName, itemId, ctx, success, fail);
        };

     Shp.Lists._GetAttachments = function (listName, itemId, ctx, success, fail) {
            var oList = ctx.get_web().get_lists().getByTitle(listName);
            var oListItem = oList.getItemById(itemId);
            var files = oListItem.get_attachmentFiles();
            ctx.load(files);

            ctx.executeQueryAsync(function () {
                var results = [];
                for (var i = 0; i < files.get_count() ; i++) {
                    var file = files.itemAt(i);
                    results.push({ 'fileName': file.get_fileName(), 'serverRelativeUrl': file.get_serverRelativeUrl() });
                }
                success(results);
            }, function (sender, args) {
                fail(args.get_message());
            });
        };

Shp.Lists.registerClass('Shp.Lists');

Now, let have a look in XSLT code. We need it to display the data.

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl" xmlns:SharePoint="Microsoft.SharePoint.WebControls">
  
    <xsl:output method="html" indent="yes"/>
    <xsl:template match="/">


      <div class="card-body">
          <table class="table table-striped table-bordered nowrap" id="dataTable" width="100%" cellspacing="0">
            <thead>
              <tr>
                <th>ID</th>
                <th>Product<br />Name</th>
                <th>Product<br />Code</th>
                <th>Description</th>
                <th>Country</th>
                <th>Supplier</th>
                <th>Available<br />Qty</th>
                <th>Currency</th>
                <th>Price</th>
              </tr>
            </thead>
            <tbody>
              <xsl:for-each select="/dsQueryResponse/Rows/Row">
                <tr>
                  <td>
                    <a title="Loading..." class="product-images" data-id="{@ID}">
                      <xsl:value-of select="@ID" disable-output-escaping="yes" />
                    </a>
                  </td>
                  <td>
                    <xsl:value-of select="@ProductName" disable-output-escaping="yes" />
                  </td>
                  <td>
                    <xsl:value-of select="@ProductCode" disable-output-escaping="yes" />
                  </td>
                  <td>
                    <xsl:value-of select="@ProductDescription" disable-output-escaping="yes" />
                  </td>
                  <td>
                    <xsl:value-of select="@CountryCode" disable-output-escaping="yes" />
                  </td>
                  <td>
                    <xsl:value-of select="@Supplier" disable-output-escaping="yes" />
                  </td>
                  <td>
                    <xsl:value-of select="@Qty" disable-output-escaping="yes" />
                  </td>
                  <td>
                    <xsl:value-of select="@Currency" disable-output-escaping="yes" />
                  </td>
                  <td>
                    <xsl:value-of select="@Price" disable-output-escaping="yes" />
                  </td>
                </tr>
              </xsl:for-each>
            </tbody>
          </table>

      </div>
         
    </xsl:template>
</xsl:stylesheet>

Our scope is to display a small image gallery in a toolip when we click in a link. So, for this, we need to run the following code when page loads.

jQuery('#dataTable').DataTable({
        scrollX: true,
        paging: false,
        fixedColumns: {
            leftColumns: 1
        },
        'aoColumnDefs': [
            { "bSortable": false, "aTargets": [0] }
        ],

        'drawCallback': function (settings) {
            jQuery('a[class*="product-images"]').tooltipster({
                trigger: 'click',
                contentAsHTML: true,
                interactive: true,
                theme: 'tooltipster-shadow',
                functionBefore: function (instance, helper) {
                    var $origin = jQuery(helper.origin);
                    var poId = $origin.attr('data-id');
                    if (Boolean($origin.data('loaded')) !== true) {
                        Shp.Lists.GetAttachments('Products list name', parseInt(poId), null, function (attachments) {
                            if (attachments.length === 0) {
                                instance.content('No image found');
                                $origin.data('loaded', true);
                                return;
                            }

                            var html = '';
                            for (var k = 0; k < attachments.length; k++) {
                                html += '<div class="mySlides">' +
                                    '<img src="' + attachments[k].serverRelativeUrl + '" style="width:100px; height: 100px">' +
                                    '</div>';
                            }
                            instance.content('<div class="slideshow-container">' + html + '</div>');

                        },
                            function (err) {
                                instance.content('Cannot get images:' + err);
                                $origin.data('loaded', true);
                            });
                    }
                }
            });
        }
    });

Of course we can apply different style on the gallery, but this is not the scope here. This is just to give you a start from where to start.

Advertisements

Attaching a file to a list item in SharePoint add-in model

Long time since I wrote about the SharePoint. Is still my job, I am still working on this, but I simply did not find the time to discover new things. Now, as SharePoint add-in model is more and more adopted in SharePoint development, due to the migration of large companies to Office 365, I am starting to face new challenges. One of this is how to attach a file to an existing list item.

<input type="file" id="attachment1" />

As HTML5 is the new standard for web, file control is able to read the content of a file as a binary string, so no more needed to server side languages. For rest, jQuery and SharePoint JavaScript libraries will do the work for you.


       Type.registerNamespace('Shp');

       Shp.Attachments = function () {
            /// <summary>Shp Attachments static class</summary>
            throw 'Cannot initiate Shp.Attachments static class';
        };

        Shp.Attachments.get_file = function (fileInput) {
            /// <summary>This method is used to get the content of the file as binary string</summmary>

            var deffered = jQuery.Deferred();
            var reader = new FileReader();
            reader.onload = function (e) { 
                deffered.resolve(e.target.result);
            };  

            reader.onerror = function (e) {
                deffered.reject(e.target.error);
            };

            reader.readAsBinaryString(fileInput.files[0]);
            return deffered.promise();
        };


        Shp.Attachments.add = function (listName, itemId, fileInput, webUrl, success, fail) {
            /// <summary>Add attachments</summary>
            /// <param>List name</param>
            /// <param>Item Id</param>
            /// <param>File input controls</param>
            /// <param>Web url</param>
            var e = Function.validateParameters(arguments, [{ name: 'listName', type: String, optional: false, mayBeNull: false },
                                                            { name: 'itemId', type: String, optional: false, mayBeNull: false },
                                                            { name: 'filesInput', type: HTMLElement, optional: false, mayBeNull: false },
                                                            { name: 'webUrl', type: String, optional: false, mayBeNull: true },
                                                            { name: 'success', type: Function, optional: false, mayBeNull: false },
                                                            { name: 'fail', type: Function, optional: true, mayBeNull: false }], true);
            if (e) throw e;


            var webUrl = webUrl || _spPageContextInfo.webAbsoluteUrl;
            var fail = fail || function (err) { alert(err); };

            Shp.Attachments.get_file(fileInput).then(function (fileContent) {
                var parts = fileInput.value.split("\\");
                var fileName = parts[parts.length - 1];
                // Attachments add internal method
                Shp.Attachments._add(listName, itemId, fileContent, fileName, webUrl, success, fail);
            });
        };

        Shp.Attachments._add = function (listName, itemId, fileContent, fileName, webUrl, success, fail) {

            var scriptBase = webUrl + "/_layouts/15/";
            jQuery.getScript(scriptBase + "SP.RequestExecutor.js", function () {

                var executor = new SP.RequestExecutor(webUrl);
                executor.executeAsync({
                    url: webUrl + "/_api/web/lists/GetByTitle('" + listName + "')/items(" + itemId + ")/AttachmentFiles/add(FileName='" + fileName + "')",
                    method: "POST",
                    binaryStringRequestBody: true,
                    body: fileContent,
                    state: "Update",
                    success: function () {
                        success(itemId);
                    },
                    fail: function (data) {
                        fail(data.responseText);
                    }

                });

            });
        };

        Shp.Attachments.registerClass('Shp.Attachments');

Once code is organized, attaching a file become an easy task.

 Shp.Attachments.add('Tickets', '2', document.getElementById('attachment1'), null, function (itemId) {
        alert(itemId);
    }, function (err) {
        alert('Error: ' + err);
    });

Shp.Attachments.add accepts following parameters:

  • List name as string
  • Item id as string
  • HTML file control as HTML element
  • Web URL. It can be nul and in this case is used _spPageContextInfo.webAbsoluteUrl
  • Success method to be executed if attachment is added
  • Fail method to be executed if attachment failed

Copy, adapt the code according to your needs and use it. Happy coding! If it helps, you can just say hi to me and I will be happy as well. 🙂

SharePoint survey and incomplete results

Is very easy to create SharePoint survey by the end users. Normally it covers all requirements, but there is only one minus. Very often, users click “Save and close” button instead of the “Next”, which creates incomplete results. Out of the box, you are allowed to see only complete results and only incomplete results created by you. That’s why I think is a very good idea to hide “Save and close” button and force users to complete all the questions. Definitely we can have some help from JavaScript.

function pageLoad() {
    var buttons = document.querySelectorAll('input[type="button"]');
    for(var j = 0; j < buttons.length; j++) {
        var button = buttons[j];
        if(button.value === 'Save and Close') {
            button.style.display = 'none';
        }
    }
}

Place this code in a text file, upload it on the server and display the content using a content editor web part you place on the page. Because you include everything in “pageLoad” function it will be executed when page is loading or after a postback.

Adding items in multiple lists using JavaScript CSOM

In general my ideas for my posts are generated by problems I am facing in my development activity. One of the problems working JavaScript CSOM is sometimes projects require very complex data operations, like adding items into multiple SharePoint lists. I would not call this a challenge, as you always can use classical examples and perform insert operations one by one, waiting for previous operation to be completed before starting a new one. But why not make it easier?

If you want to achieve this in a simple way, just take a look on the code below.

Type.registerNamespace('Shp');


Shp.Lists.AddItemsInLists = function (addItemsArguments, webUrl, success, fail) {
    /// <summary>Add items in multiple lists calling execute query async once</summary>
    /// <param name="addItemsArguments" type="Array" elementType="Object" elementMayBeNull="false" mayBeNull="false" optional="false">Add items arguments</param>
    /// <param name="webUrl" type="String" mayBeNull="true" optional="false">Web url</param>
    /// <param name="success" type="Function" mayBeNull="false" optional="false">Success</param>
    /// <param name="fail" type="Function" mayBeNull="false" optional="true">Fail</param>
    var e = Function.validateParameters(arguments, [{ name: 'addItemsArguments', type: Array, elementType: Object, elementMayBeNull: false, optional: false, mayBeNull: false },
                                                    { name: 'webUrl', type: String, optional: false, mayBeNull: true },
                                                    { name: 'success', type: Function, optional: false, mayBeNull: false },
                                                    { name: 'fail', type: Function, optional: true, mayBeNull: false }], true);
    if (e) throw e;
    var ctx = (typeof webUrl === 'undefined' || webUrl === null) ? SP.ClientContext.get_current() : new SP.ClientContext(webUrl);
    var fail = fail || function (err) { alert(err); };
    Shp.Lists._AddItemsInLists(addItemsArguments, ctx, success, fail);
};


Shp.Lists._AddItemsInLists = function (addItemsArguments, ctx, success, fail) {
    var results = {}, current, i, k;

    for (i = 0; i < addItemsArguments.length; i++) {

        current = addItemsArguments[i]; // Store current item

        // We check if list name property if defined before do other operations
        if (current.hasOwnProperty("listName") === true) {

            if (results.hasOwnProperty(current.listName) === false) results[current.listName] = []; // Results object doesn't have a slot for current list name, create it
            var oList = ctx.get_web().get_lists().getByTitle(current.listName);

            // We check if list items property is an array
            if (current.hasOwnProperty("listItems") === true && isArray(current.listItems) === true) {
                for (k = 0; k < current.listItems.length; k++) {
                    var listItem = current.listItems[k];
                    var oListItem = oList.addItem(new SP.ListItemCreationInformation());
                    for (var field in listItem) {
                        oListItem.set_item(field, listItem[field]);
                    }
                    oListItem.update();
                    ctx.load(oListItem);
                    results[current.listName].push(oListItem);
                }
            }
        }
    }

    ctx.executeQueryAsync(function () {
        success(results);
    }, function (sender, args) {
        fail(args.get_message());
    });

};

Shp.Lists.registerClass('Shp.Lists');

I have created a method “AddItemsInLists” for “Shp.Lists”, which is accepting 4 parameters:

  • addItemsArguments – which is an array of objects containing information about list items you want to insert.
  • webUrl – which is the URL of the web inside current site collection. If null, web for current context is considered.
  • success – function to be executed if everything goes fine.
  • fail – function to be executed if fails. It is optional and if not specified a simple alert containing the error is shown.

Let’s concentrate on “addItemsArguments” parameter. As I said, it is an array of objects, but let me explain how these objects should look like. Basically it should contain 2 properties: “listName” (name of the list where you want to insert items) and “listItems” (another array of objects, this time representing the items).

var firstListItems = { listName: 'Name of the list', listItems: [{ 'Field 1': 'Value', 'Field 2': 'Value' }, { 'Field 1': 'Value', 'Field 2': 'Value'}]  };
var secondListItems = { listName: 'Name of the 2nd list', listItems: [{ 'Field 1': 'Value', 'Field 2': 'Value' }, { 'Field 1': 'Value', 'Field 2': 'Value'}]  };
var addItemsArguments = [firstListItems, secondListItems];

Now calling the method I have created is easy and no rocket science is required.

Shp.Lists.AddItemsInLists([firstListItems, secondListItems, ], null, function (results) {
    /*  Do something with the results.
        You can get items inserted in a list like this: results['name of the list']  */
}, function (err) {
    alert(err); // Alert the error or do something elsel
});

Adding Chrome to browser identification list for Microsoft Ajax

Microsoft Ajax library can identify browser type and version for Firefox, Internet Explorer and Safari. Chrome is missing from this list and it seems Microsoft added Chrome to “init.js” but did not extend Microsoft Ajax. However, as Microsoft Ajax is not limited to SharePoint, you can add Chrome by adding the following code in another JavaScript file (be sure you load the file after Microsoft Ajax is loaded).

(function () {

    function execute() {
        Sys.Browser.Chrome = {};
        var a = navigator.userAgent.toLowerCase();
        if (a.toLowerCase().indexOf("chrome") != -1) {
            Sys.Browser.agent = Sys.Browser.Chrome;
            Sys.Browser.version = parseInt(a.substring(a.indexOf("chrome/") + 7));
        }
    }

    if (typeof (Sys) !== "undefined" && typeof (Sys.Browser) !== "undefined") {
        execute();
    }

})();

And now you can easily identify is browser is Chrome and what version is used.

// Check if browser is Chrome
if (Sys.Browser.agent === Sys.Browser.Chrome) {
    // Alert current Chrome version
    alert(Sys.Browser.agent);
}

Send email using SharePoint API

I see all over examples about how to send emails using SharePoint API. I am not going to reinvent the code, but I am just organizing it into a little bit more object orientated way. And again, Microsoft Ajax library is here to help us. As always, first step is to create the namespace where you want to place the objects.

Type.registerNamespace('Shp.Utility');

Once I done it, I need to create an object containing email properties.

Shp.Utility.EmailProperties = function (to, from, subject, body) {
    /// <summary>Create an instance of Shp.Utility.EmailProperties
    /// <param name="to" type="String" mayBeNull="false" optional="false">To</param>
    /// <param name="from" type="String" mayBeNull="false" optional="false">From</param>
    /// <param name="subject" type="String" mayBeNull="false" optional="false">Email Subject</param>
    /// <param name="body" type="String" mayBeNull="false" optional="false">Email Body</param>
    var e = Function.validateParameters(arguments, [{ name: 'to', type: Array, elementType: String, elementMayBeNull: false, optional: false, mayBeNull: false },
                                                    { name: 'from', type: String, optional: false, mayBeNull: false },
                                                    { name: 'subject', type: String, optional: false, mayBeNull: false },
                                                    { name: 'body', type: String, optional: false, mayBeNull: false }], true);
    if (e) throw e;

    this.to = to;
    this.from = from;
    this.subject = subject;
    this.body = body;
    this.cc = [];
    this.bcc = [];
}

Shp.Utility.EmailProperties.prototype = {


    add_cc: function (cc) {
        /// <signature>
        ///     <summary>Add CC to email properties</summary>
        ///     <param name="cc" type="String" mayBeNull="false" optional="false">CC</param>
        ///     <returns type="Shp.Utility.EmailProperties"></returns>
        /// </signature>
        /// <signature>
        ///     <summary>Add CC to email properties</summary>
        ///      <param name="cc" type="Array" elementType="String" elementMayBeNull="false" mayBeNull="false" optional="false">CC</param>
        ///      <returns type="Shp.Utility.EmailProperties"></</returns>
        /// </signature>
        var e1 = Function.validateParameters(arguments, [{ name: 'cc', type: String, optional: false, mayBeNull: false }], true);
        var e2 = Function.validateParameters(arguments, [{ name: 'cc', type: Array, elementType: String, elementMayBeNull: false, optional: false, mayBeNull: false }], true);
        if (e1 != null && e2 != null) throw e1 || e2;

        var instance = this;
        if (e1 == null)
            instance.cc.push(cc);
        if (e2 == null)
            instance.cc = Array.clone(cc);

        return instance;
    },

    add_bcc: function (bcc) {
        /// <signature>
        ///     <summary>Add CC to email properties</summary>
        ///     <param name="bcc" type="String" mayBeNull="false" optional="false">CC</param>
        ///     <returnts type="Shp.Utility.EmailProperties" />
        /// </signature>
        /// <signature>
        ///     <summary>Add CC to email properties</summary>
        ///     <param name="bcc" type="Array" elementType="String" elementMayBeNull="false" mayBeNull="false" optional="false">CC</param>
        ///     <returnts type="Shp.Utility.EmailProperties" />
        /// </signature>
        var e1 = Function.validateParameters(arguments, [{ name: 'bcc', type: String, optional: false, mayBeNull: false }], true);
        var e2 = Function.validateParameters(arguments, [{ name: 'bcc', type: Array, elementType: String, elementMayBeNull: false, optional: false, mayBeNull: false }], true);
        if (e1 != null && e2 != null) throw e1 || e2;

        var instance = this;
        if (e1 == null)
            instance.bcc.push(bcc);
        if (e2 == null)
            instance.bcc = Array.clone(bcc);

        return instance;

    },

    GenerateRequestBody: function () {
        /// <summary>Generate Ajax request body</summary>
        var instance = this;
        var request = {
            'properties': {
                '__metadata': { 'type': 'SP.Utilities.EmailProperties' },
                'From': instance.from,
                'To': { 'results': instance.to },
                'CC': { 'results': instance.cc },
                'BCC': { 'results': instance.bcc },
                'Body': instance.body,
                'Subject': instance.subject
            }
        }
        return JSON.stringify(request);
    }

}

Shp.Utility.EmailProperties.registerClass('Shp.Utility.EmailProperties');

Here is actually the point where the code is simplified. Creating email properties object makes you write less code in the Ajax call. Generate request body method is using all necessary properties of the object to create string representation of JSON object to be used as the request body.

var from = 'someone@domain.com';
var body = 'Email body'; // Pay attention, you might need to encode it
var subject = 'Email subject';
var to = ['person1@domain.com', 'person2@domain.com'];

// Create email properties object
var emailProperties = new Shp.Utility.EmailProperties(to, from, subject, body);

// Add CC and BCC
emailProperties.add_cc(['person3@domain.com', 'person4@domain.com']);
emailProperties.add_bcc(['person3@domain.com', 'person4@domain.com']);

// Forgot to add someone, no worries 
// Passing a string instead of array of strings makes the method not to act like a setter. Instead is adding email to CC or BCC field.
emailProperties.add_cc('forgotten@domain.com');
emailProperties.add_bcc('forgotten@domain.com');

Now, as we have the object containing the email properties, is time to handle the Ajax call.


Shp.Utility.Email = function () {
    /// <summary>Shp.Utility.Email static class</summary>
    throw 'You cannot initialize an instance of Shp.Utility.Email static class';
}


Shp.Utility.Email.SendEmail = function (emailProperties, success, fail) {
    /// <summary>Send an email using SharePoint API</summary>
    /// <param name="emailProperties" type="Shp.Utility.EmailProperties" optional="false" mayBeNull="false">Email Properties</param>
    /// <param name="success" type="Function" optional="false" mayBeNull="false">Success</param>
    /// <param name="fail" type="Function" optional="true" mayBeNull="false">Fail</param>
    var e = Function.validateParameters(arguments, [{ name: 'emailProperties', type: Shp.Utility.EmailProperties, optional: false, mayBeNull: false },
                                                    { name: 'success', type: Function, optional: false, mayBeNull: false },
                                                    { name: 'fail', type: Function, optional: true, mayBeNull: false }], true);
    if (e) throw e;


    Shp.Utility.Email._SendEmail(emailProperties, success, fail || function (errCode, errText) {
        alert('Send email Ajax call failed with error code ' + errCode + ':' + errText);
    });
}

Shp.Utility.Email._SendEmail = function (emailProperties, success, fail) {
    var ajax = new Sys.Net.WebRequest();
    ajax.set_url(_spPageContextInfo.webServerRelativeUrl + '/_api/SP.Utilities.Utility.SendEmail');
    ajax.set_httpVerb('POST');
    ajax.set_body(emailProperties.GenerateRequestBody());
    ajax.get_headers()['Accept'] = 'application/json;odata=verbose';
    ajax.get_headers()['Content-Type'] = 'application/json;odata=verbose';
    ajax.get_headers()['X-RequestDigest'] = $get('__REQUESTDIGEST').value;

    ajax.add_completed(function (executor, eventArgs) {
        if (executor.get_statusCode() >= 200 && executor.get_statusCode() < 300 && executor.get_responseAvailable() === true) {
            success(executor.get_responseData());
        }
        else {
            alert('Ajax failed with error code ' + executor.get_statusCode() + ':' + executor.get_responseData());
        }
    });

    ajax.invoke();
}

Shp.Utility.Email.registerClass('Shp.Utility.Email');

This class is very simple. It contains a method called “SendEmail” which accepts 3 arguments:

  • Email properties, which is the object created before
  • A function to be executed if Ajax is successfully, which accept obtained data from server as parameter.
  • A function to be executed if Ajax fails. This function accepts error code and error text as parameters, but is not mandatory. If not specified, an alert containing this information is shown.

As an example, check the code below.

Shp.Utility.Email.SendEmail(emailProperties, function (data) {
    // Do something with data
});

There one important thing I need to mention. It seems you can send emails only to the users registered to the SharePoint. If not registered, the user won’t receive emails, but the email doesn’t fail as the rest of registered users will receive it.

There is always a way to improve the code and if you have suggestions, please let me know. 🙂

Calculate working hours between two days with JavaScript

Calculating working hours between two days with JavaScript is a regular requirement in large companies. In general, managers want to measure clean performance of the employees, excluding weekends and what is outside the business hours (in my case 9 AM to 6 PM). The problem appears when you cannot use C# or Java or any other language of this type and you are limited to JavaScript. JavaScript cannot offer an elegant solution, but is still offering the functionality.

/// <Reference Name="MicrosoftAjax.js" />

function getBusinessHoursDiff(start, end) {
    /// <summary>Calculate business hours between two dates</summary>
    /// <param name="start" type="Date" maybNeNull="false" optional="false">Start date</param>
    /// <param name="end" type="Date" maybNeNull="false" optional="false">End date</param>
    var e = Function.validateParameters(arguments, [{ name: 'start', type: Date, maybNeNull: false, optional: false },
                                                    { name: 'end', type: Date, maybNeNull: false, optional: false }, ], true);

    // Passing incorrect parameters type will result in returning an empty string, you can change the logic as you wish here
    if (e) return '';

    // Define variables and clone the provides date, we do not want to modify them outside as are passed as reference
    var startDate = new Date(startDate.valueOf());
    var endDate = new Date(endDate.valueOf());
    var count = 0;

    for (var i = start.valueOf() ; i < end.valueOf() ; i = (start.setMinutes(start.getMinutes() + 1)).valueOf()) {
        if (start.getDay() != 0 && start.getDay() != 6 && start.getHours() >= 9 && start.getHours() < 18) {
            count++;
        }
    }

    return count / 60;
}

Function is simple, even to be honest I did find it after almost two days. Is also not perfect when it comes to performance as is calculating the different till seconds. Still, if you do not want to be so exact, you can calculate till hours level (setMinutes method with set setHours). I am not sure how suitable is this solution for you, but is for sure something you can start from.